Difference between revisions of "Aggregate"

m
 
(27 intermediate revisions by 5 users not shown)
Line 1: Line 1:
 
[[Category:Transforming functions]]
 
[[Category:Transforming functions]]
''New to [[What's new in Analytica 4.2?|Analytica 4.2]]''
+
[[Category:Array Library]]
  
= Aggregate( X,map : Array[I] ; I, targetIndex : Index ) =
+
== Aggregate(x, map, <code>I</code>, <code>J</code>) ==
 +
Aggregates the values in array «x» over index «<code>I</code>» to produce a result indexed by «<code>J</code>», a coarser grained value. For example, «<code>I</code>» and «<code>J</code>» might be days and months,  or countries and continents, respectivly.  Parameter «map» is indexed by «<code>I</code>». It gives the value of «<code>J</code>» for each element of «<code>I</code>». It gives a warning if any value in «map» is not in index «<code>J</code>», unless you set «<code>noMapError</code>» to True.
  
[[Aggregate]] converts an array «X» indexed by a fine-grained index «I» (e.g., Months) into an array indexed by a coarser index «targetIndex» (e.g., Years), by aggregating all the elements that map to the same «targetIndex» value.  The «map» parameter specifies how the elements of «I» map to the elements of «targetIndex».  Each cell of «map» should be an element of «targetIndex» (or, if you also specify an optional ''positional:true'' parameter, then each element of «map» can specify an integer position along «targetIndex»).
 
  
In its standard usage, [[Aggregate]] [[Sum|sums]] all elements from «X» that map to the same element along «targetIndex»However, the type of aggregation can be changed using an optional «type» parameter, containing the name of or handle to any valid aggregation function, such as [[Max]], [[Min]], [[Product]], [[Average]] or [[Median]].
+
For example, suppose we want to aggregate <code>Population_by_country</code> indexed by <code>Country</code> to produce an array of populations by  <code>Continent</code>We need a map <code>Continent_by_country</code> giving the name of the continent containing each  (indexed by <code>Country</code>):
  
= Library =
+
<code>Aggregate(Population_by_country, Continent_by_country, Country, Continent)</code>
  
Array functions
+
returns the population by continent.
  
= Examples =
+
Here's an example for how to aggregate from months to years:
 +
:<code>Index Months := Sequence(MakeDate(2009, 1, 1), MakeDate(2019, 12, 1), dateUnit: "M")</code>
 +
:<code>Index Years := 2009..2020</code>
 +
:<code>Year_by_months := DatePart(Months, "Y")</code>
 +
:<code>Revenue_by_month := { an array indexed by Months }</code>
 +
:<code>Revenue_by_year := Aggregate(Revenue_by_month, Year_by_months, Months, Years)</code>
  
Index Months := [[Sequence]]([[MakeDate]](2009,1,1),[[MakeDate]](2020,1,1),dateUnit:"M")
+
==Optional Parameters==
Index Years := 2009..2020
+
=== Type ===
MonthsToYears := [[DatePart]](Months,"Y")
+
By default, Aggregate sums the values. But you can set another method for <code>Type</code> parameter, including <code>Average, Max, Min, Median, First, Last, Count</code>, or <code>Sum</code>. In fact, you can use any [[Array-reducing functions|array-reducing function]] -- i.e. a function that reduces the number of dimensions over an index. You can even use a [[User-Defined function|user-defined function]] that is an [[Array-reducing functions|array-reducing function]]. The «type» parameter may be a text value -- e.g. <code>"Product"</code> -- or a handle to the function -- e.g. <code>Handle(Product)</code>.
Revenue_by_month := { array indexed by Months }
 
&nbsp;
 
Revenue_by_year := [[Aggregate]](Revenue_by_month,MonthsToYears,Months,Years)
 
  
= Aggregation types =
+
=== Position ===
 +
<code>False</code> by default. If <code>True</code>, «map» must contain the integer positions of the corresponding element of «targetIndex» rather than their values.
  
The optional «type» parameter specifies the aggregation type.  The parameter names an existing [[:Category:Array-reducing functions|array-reducing function]], either built-in or [[User-Defined Functions|user-defined]]You can supply either the textual name of the function, or a handle to the function (or even an array to these). Built-in functions that can be applied include [[Sum]] (default), [[Max]], [[Min]], [[Average]] or [[Mean]], [[Product]], [[Median]], [[SDeviation]], [[Variance]], [[Skewness]], [[Kurtosis]], and [[Join]]. 
+
===DefaultValue ===
 +
The value in the result array when no value in «x» maps to that value of «targetIndex»No warning is issued when there are result cells with no data. The default «defaultValue» is [[Null]]. If you'd like to use a different value, such as 0, you can specify the value like so:
  
Revenue_dev := [[Aggregate]](Revenue_by_month,MonthToYear,Month,Year,Type:"SDeviation")
+
<code>Aggregate(x, map, i, targetIndex, type: "SDeviation", defaultValue: 0)</code>
Peak_revenue:= [[Aggregate]](Revenue_by_month,MonthToYear,Month,Year,Type:[[Handle]](Max))
 
Ave_revenue := [[Aggregate]](Revenue_by_month,MonthToYear,Month,Year,Type:"Average")
 
  
You can also create your own custom aggregation method in the form of a [[User-Defined Function]]. The function should accept an array parameter and an index, and should [[:Category:Array-reducing functions|reduce]] the array so that the result eliminates the indicated index. 
+
<tip title="Note">Aggregate ignores [[Null]] values in «x».</tip>
  
The following would all be examples of legal aggregation functions:
+
===noMapError===
Function First(A:Array[I] ; I:Index) := A[@I=1]
+
(new to [[Analytica 5.0]])
Function Last(A:Array[I] ; I:Index) := A[@I=[[Size]](I)]
 
Function Middle(A:Array[I] ; I:Index) := A[@I=[[Floor]]([[Size]](I)/2)+1]
 
Function Count(A:Array[I] ; I:Index) := [[Size]](I)
 
Function PosMax(A:Array[I] ; I:Index) := [[ArgMax]](A,I,position:true)
 
  
These would be used in the same fasion as other types:
+
When the value in «map» is not in the target index «J» an error is normally issued (except for «map»=[[Null]] values), which is often useful for debugging your expression. But, if your target index «J» is intentionally a subset of the full set of values in «map», you can set «noMapError» to true to suppress the error and ignore the values in «x» that don't map into «J».
Opening_Revenue := [[Aggregate]](Revenue_by_month,MonthToYear,Month,Year,Type:"First")
 
Closing_Revenue := [[Aggregate]](Revenue_by_month,MonthToYear,Month,Year,Type:"Last")
 
Best_Month := [[Aggregate]](Revenue_by_month,MonthToYear,Month,Year,Type:Handle(PosMax))
 
  
 +
==== Example ====
  
 +
The «noMapError» parameter is useful when tallying values for the US states only in this example.
  
Note: Aggregation (aka conglomeration) functions can also be utilized by the [[MdTable]] function. The requirements are the same.
+
::[[image:Aggregate_noMapError.png]]
 +
 
 +
:<code>[[Aggregate]]( Annual_usage, City_to_province, City, US_State, noMapError:true )</code>
 +
 
 +
== Aggregation type ==
 +
By default, Aggregate sums over specified elements. Use the optional «type» parameter to specify another aggregation method, such as [[Sum]] (default), 'Count', [[Max]], [[Min]], [[Average]] or [[Mean]], [[Product]], [[Median]], [[SDeviation]], [[Variance]], and [[JoinText]] -- or any  [[:Category:Array-reducing functions|array-reducing function]]. You can specify the identifier of the function as a text value, or a handle to the function. It could even be an array of functions.  
  
= Positional Maps =
+
:<code>Revenue_dev := Aggregate(Revenue_by_month, MonthToYear, Month, Year, Type: "SDeviation")</code>
 +
:<code>Peak_revenue := Aggregate(Revenue_by_month, MonthToYear, Month, Year, Type: Handle(Max))</code>
 +
:<code>Ave_revenue := Aggregate(Revenue_by_month,  MonthToYear, Month, Year, Type: "Average")</code>
  
The «map» parameters to specifies how each element in the original index «I» maps into the «targetIndex».  This is usually a many-to-one mappingYour «map» is necessarily indexed by «I», and each element of «map» specifies an element of «targetIndex».  When specifying the target values, you can do so either [[Associative vs. Positional_Indexing|by value or by position]].  The default assumes that map contains values from «targetIndex», but by specifying the optional «positional» parameter as true, «map» is interpreted as containing positions.
+
You can also create your own custom aggregation method in the form of a [[User-Defined Function]]The function should accept an array parameter and an index, and should [[:Category:Array-reducing functions|reduce]] the array so that the result eliminates the indicated index, for example:
 +
:<code>Function First(A: Array[I]; I: Index) := A[@I = 1]</code>
 +
:<code>Opening_Revenue := Aggregate(Revenue_by_month, MonthToYear, Month, Year, Type: "First")</code>
  
Variable M_to_Y_pos := [[Floor]]( (@Month-1)/12 ) + 1
+
:<code>Function Last(A: Array[I]; I: Index) := A[@I = Size(I)]</code>
Variable Annual_Rev := [[Aggregate]](Revenue_by_month,M_to_Y_pos,Month,Year,positional:true)
+
:<code>Closing_Revenue := Aggregate(Revenue_by_month, MonthToYear, Month, Year, Type: "Last")</code>
 +
(Note that 'First' and 'Last', as text, are already built-in options in [[Analytica 5.0]]).
  
The «map» may contain [[Null]] values.  With a positional mapping, any «I»-position in which «map» is [[Null]] does not get included in the aggregated result.  With a value-based mapping, these elements are included only if one of the elements of «targetIndex» is [[«null»]] (which is uncommon).
+
:<code>Function PosMax(A: Array[I]; I: Index) := ArgMax(A, I, position: true)</code>
 +
:<code>Best_Month := Aggregate(Revenue_by_month, MonthToYear, Month, Year, Type: Handle(PosMax))</code>
  
= Default Value =
+
To return a "Count", you simply need to use a Boolean expression for the first parameter. No need to specify the «type» parameter, which defaults to 'Sum'.  For example, to count the number of months in each year with positive revenue:
 +
:<code>Aggregate(Revenue_by_month > 0, MonthToYear, Month, Year)</code>
 +
in [[Analytica 5.0]] and later, "Count" is accepted as a «type», which counts the number of non-null entries (zeros are counted).
  
It is possible that no data maps to a particular value/position in «targetIndex».  This can occur when that value (or position) never occurs within «map», or when all data in «X» that would map to that position is [[Null]]-valued(Note: Aggregate ignores null values in «X»).
+
Note: Aggregation (aka conglomeration) functions can also be utilized by the [[MdTable]] functionThe requirements are the same.
  
For result cells with no data, [[«null»]] is returnedNo warning is issued when there are result cells with no dataIf you'd like to use a different value, such as 0, you can specify the value in the optional «defaultValue» parameter, e.g.:
+
== Positional Maps ==
 +
The «map» parameters to specifies how each element in the original index «<code>I</code>» maps into the «<code>J</code>».  This is usually a many-to-one mappingYour «map» is necessarily indexed by «<code>I</code>», and each element of «map» specifies an element of «<code>J</code>»When specifying the target values, you can do so either [[Associative vs. Positional_Indexing|by value or by position]].  The default assumes that map contains values from «targetIndex», but by specifying the optional «positional» parameter as true, «map» is interpreted as containing positions.
  
[[Aggregate]](X,map,I,J,type:"SDeviation",defaultValue:0)
+
:<code>Variable M_to_Y_pos := Floor((@Month-1)/12) + 1</code>
 +
:<code>Variable Annual_Rev := Aggregate(Revenue_by_month, M_to_Y_pos, Month, Year, positional: true)</code>
  
= Inverses of Aggregate =
+
The «map» may contain [[Null]] values.  With a positional mapping, any «I»-position in which «map» is [[Null]] does not get included in the aggregated result.  With a value-based mapping, these elements are included only if one of the elements of «targetIndex» is [[«null»]] (which is uncommon).
  
 +
== Inverses of Aggregate (De-aggregation) ==
 
Since [[Aggregate]] is used to map data from a finer-grained index to a coarser-grained index, it is natural to ask how the inverse is accomplished -- namely, mapping from a coarse-grained index to a fine-grain index.
 
Since [[Aggregate]] is used to map data from a finer-grained index to a coarser-grained index, it is natural to ask how the inverse is accomplished -- namely, mapping from a coarse-grained index to a fine-grain index.
  
 
Just as with aggregate, there are many potential types of de-aggregation. For example, when de-aggregating annual data down to months, we might use the annual value for every month in that year, or split it uniformly among all the target months, or perhaps assign it to only one month, using zero for all other months in that year.
 
Just as with aggregate, there are many potential types of de-aggregation. For example, when de-aggregating annual data down to months, we might use the annual value for every month in that year, or split it uniformly among all the target months, or perhaps assign it to only one month, using zero for all other months in that year.
  
The following uses the annual value for every month in that year.  In the example, ''Annual_val'' is indexed by ''Year'', while the result is indexed by ''Month'':
+
The following uses the annual value for every month in that year.  In the example, <code>Annual_val</code> is indexed by<code>Year</code>, while the result is indexed by <code>Month</code>:
Annual_val[ Year = MonthToYear ]
+
:<code>Annual_val[Year = MonthToYear]</code>
  
 
To spread the value uniformly across all months, we can do this:
 
To spread the value uniformly across all months, we can do this:
[[Var..Do|Var]] MonthsPerYear := [[Aggregate]](1,MonthToYear,Month,Year);
+
:<code>Var MonthsPerYear := Aggregate(1, MonthToYear, Month, Year);</code>
Annual_val[ Year = MonthToYear ] / MonthsPerYear
+
:<code>(Annual_val/MonthsPerYear)[Year = MonthToYear]</code>
 +
 
 +
More generally, if we have a weighting, <code>w</code>, indexed by <code>Month</code>, which specifies the proportion of annual revenue each month receives, we can use:
 +
:<code>w*Annual_val[Year = MonthsToYear]</code>
 +
 
 +
If you need to ensure that <code>w</code> is normalized for each month, use as the weight:
 +
:<code>w := (w0/Aggregate(1, MonthToYear, Month, Year)[Year = MonthToYear])</code>
 +
 
 +
where <code>w0</code> is the unnormalized weight. Using <code>w0 := 1</code> yields the earlier example of spreading evenly across all months for the year.
 +
 
 +
Using an array <code>w</code> that has a 1 in the first month of each year, and 0 for all remaining months, yields a de-aggregation that assigns the full annual value to the first month of each year:
 +
:<code>Index Firsts := Unique(MonthToYear, Month);</code>
 +
:<code>(@[Firsts = Month] > 0)*Annual_val[Year = MonthToYear]</code>
 +
 
 +
== Other methods for Aggregation ==
 +
In many existing Analytica models, one will see the following method used for aggregation:
 +
:<code>Sum((MonthToYear = Year) * Revenue, Month)</code>
 +
 
 +
This expression is functionally equivalent to:
 +
:<code>Aggregate(Revenue, MonthToYear, Month, Year)</code>
 +
 
 +
However, the use of [[Aggregate]] will evaluate much faster.  When using the dot-product method with [[Sum]], the intermediate expression <code>(MonthToYear = Year</code>) expands to a full 2-D array (<code>Month x Year</code>), which even though it is very sparse, still consumes lots of memory and time to process.  In rough terms, the [[Sum]]-based method is ''O(n<sup>2</sup>)'' complexity, while [[Aggregate]] is only ''O(n)''.
 +
 
 +
The [[Frequency]] function can be used to aggregate, with either "sum" type aggregation (using the data itself as the weighting parameter) or "count" type aggregation.  [[Frequency]] is in fact quite efficient for this purpose, but the usage for the purpose is unintuitive, making [[Aggregate]] the preferred alternative.
 +
 
 +
[[Slice Assignment]] can be used in a procedural-programming style to aggregate.  This is a very general-purpose approach (can be employed for a variety of aggregation types) and can also be quite efficient. The general approach is demonstrated here (for [[Max]]-type aggregation):
 +
:<code>Var res := Null;</code>
 +
:<code>For m := Month Do (</code>
 +
::<code>res[Year = MonthToYear] := Max([res[Year = MonthToYear], Revenue[Month = m]])</code>
 +
:<code>);</code>
 +
:<code>res</code>
 +
 
 +
==History==
  
= See Also =
+
* [[Analytica 5.0]]: «noMapError» added.
 +
* [[Analytica 5.0]]: Multithreaded evaluation (large aggregations operations can benefit from concurrency).
 +
*[[What's new in Analytica 4.2?|Analytica 4.2]]
 +
**[[Aggregate]] function introduced
  
 +
== See Also ==
 +
* [[Other_array_functions#Aggregate|Aggregate]]
 
* [[Subscript-Slice Operator]] -- standard reindexing
 
* [[Subscript-Slice Operator]] -- standard reindexing
 
* [[Frequency]] -- basically aggregation with "count" type
 
* [[Frequency]] -- basically aggregation with "count" type
 
* [[MdTable]] -- another function that accepts an aggregation/conglomeration parameter
 
* [[MdTable]] -- another function that accepts an aggregation/conglomeration parameter
 +
* [[Array-reducing functions]]

Latest revision as of 17:39, 19 June 2018


Aggregate(x, map, I, J)

Aggregates the values in array «x» over index «I» to produce a result indexed by «J», a coarser grained value. For example, «I» and «J» might be days and months, or countries and continents, respectivly. Parameter «map» is indexed by «I». It gives the value of «J» for each element of «I». It gives a warning if any value in «map» is not in index «J», unless you set «noMapError» to True.


For example, suppose we want to aggregate Population_by_country indexed by Country to produce an array of populations by Continent. We need a map Continent_by_country giving the name of the continent containing each (indexed by Country):

Aggregate(Population_by_country, Continent_by_country, Country, Continent)

returns the population by continent.

Here's an example for how to aggregate from months to years:

Index Months := Sequence(MakeDate(2009, 1, 1), MakeDate(2019, 12, 1), dateUnit: "M")
Index Years := 2009..2020
Year_by_months := DatePart(Months, "Y")
Revenue_by_month := { an array indexed by Months }
Revenue_by_year := Aggregate(Revenue_by_month, Year_by_months, Months, Years)

Optional Parameters

Type

By default, Aggregate sums the values. But you can set another method for Type parameter, including Average, Max, Min, Median, First, Last, Count, or Sum. In fact, you can use any array-reducing function -- i.e. a function that reduces the number of dimensions over an index. You can even use a user-defined function that is an array-reducing function. The «type» parameter may be a text value -- e.g. "Product" -- or a handle to the function -- e.g. Handle(Product).

Position

False by default. If True, «map» must contain the integer positions of the corresponding element of «targetIndex» rather than their values.

DefaultValue

The value in the result array when no value in «x» maps to that value of «targetIndex». No warning is issued when there are result cells with no data. The default «defaultValue» is Null. If you'd like to use a different value, such as 0, you can specify the value like so:

Aggregate(x, map, i, targetIndex, type: "SDeviation", defaultValue: 0)

Note
Aggregate ignores Null values in «x».

noMapError

(new to Analytica 5.0)

When the value in «map» is not in the target index «J» an error is normally issued (except for «map»=Null values), which is often useful for debugging your expression. But, if your target index «J» is intentionally a subset of the full set of values in «map», you can set «noMapError» to true to suppress the error and ignore the values in «x» that don't map into «J».

Example

The «noMapError» parameter is useful when tallying values for the US states only in this example.

Aggregate noMapError.png
Aggregate( Annual_usage, City_to_province, City, US_State, noMapError:true )

Aggregation type

By default, Aggregate sums over specified elements. Use the optional «type» parameter to specify another aggregation method, such as Sum (default), 'Count', Max, Min, Average or Mean, Product, Median, SDeviation, Variance, and JoinText -- or any array-reducing function. You can specify the identifier of the function as a text value, or a handle to the function. It could even be an array of functions.

Revenue_dev := Aggregate(Revenue_by_month, MonthToYear, Month, Year, Type: "SDeviation")
Peak_revenue := Aggregate(Revenue_by_month, MonthToYear, Month, Year, Type: Handle(Max))
Ave_revenue := Aggregate(Revenue_by_month, MonthToYear, Month, Year, Type: "Average")

You can also create your own custom aggregation method in the form of a User-Defined Function. The function should accept an array parameter and an index, and should reduce the array so that the result eliminates the indicated index, for example:

Function First(A: Array[I]; I: Index) := A[@I = 1]
Opening_Revenue := Aggregate(Revenue_by_month, MonthToYear, Month, Year, Type: "First")
Function Last(A: Array[I]; I: Index) := A[@I = Size(I)]
Closing_Revenue := Aggregate(Revenue_by_month, MonthToYear, Month, Year, Type: "Last")

(Note that 'First' and 'Last', as text, are already built-in options in Analytica 5.0).

Function PosMax(A: Array[I]; I: Index) := ArgMax(A, I, position: true)
Best_Month := Aggregate(Revenue_by_month, MonthToYear, Month, Year, Type: Handle(PosMax))

To return a "Count", you simply need to use a Boolean expression for the first parameter. No need to specify the «type» parameter, which defaults to 'Sum'. For example, to count the number of months in each year with positive revenue:

Aggregate(Revenue_by_month > 0, MonthToYear, Month, Year)

in Analytica 5.0 and later, "Count" is accepted as a «type», which counts the number of non-null entries (zeros are counted).

Note: Aggregation (aka conglomeration) functions can also be utilized by the MdTable function. The requirements are the same.

Positional Maps

The «map» parameters to specifies how each element in the original index «I» maps into the «J». This is usually a many-to-one mapping. Your «map» is necessarily indexed by «I», and each element of «map» specifies an element of «J». When specifying the target values, you can do so either by value or by position. The default assumes that map contains values from «targetIndex», but by specifying the optional «positional» parameter as true, «map» is interpreted as containing positions.

Variable M_to_Y_pos := Floor((@Month-1)/12) + 1
Variable Annual_Rev := Aggregate(Revenue_by_month, M_to_Y_pos, Month, Year, positional: true)

The «map» may contain Null values. With a positional mapping, any «I»-position in which «map» is Null does not get included in the aggregated result. With a value-based mapping, these elements are included only if one of the elements of «targetIndex» is «null» (which is uncommon).

Inverses of Aggregate (De-aggregation)

Since Aggregate is used to map data from a finer-grained index to a coarser-grained index, it is natural to ask how the inverse is accomplished -- namely, mapping from a coarse-grained index to a fine-grain index.

Just as with aggregate, there are many potential types of de-aggregation. For example, when de-aggregating annual data down to months, we might use the annual value for every month in that year, or split it uniformly among all the target months, or perhaps assign it to only one month, using zero for all other months in that year.

The following uses the annual value for every month in that year. In the example, Annual_val is indexed byYear, while the result is indexed by Month:

Annual_val[Year = MonthToYear]

To spread the value uniformly across all months, we can do this:

Var MonthsPerYear := Aggregate(1, MonthToYear, Month, Year);
(Annual_val/MonthsPerYear)[Year = MonthToYear]

More generally, if we have a weighting, w, indexed by Month, which specifies the proportion of annual revenue each month receives, we can use:

w*Annual_val[Year = MonthsToYear]

If you need to ensure that w is normalized for each month, use as the weight:

w := (w0/Aggregate(1, MonthToYear, Month, Year)[Year = MonthToYear])

where w0 is the unnormalized weight. Using w0 := 1 yields the earlier example of spreading evenly across all months for the year.

Using an array w that has a 1 in the first month of each year, and 0 for all remaining months, yields a de-aggregation that assigns the full annual value to the first month of each year:

Index Firsts := Unique(MonthToYear, Month);
(@[Firsts = Month] > 0)*Annual_val[Year = MonthToYear]

Other methods for Aggregation

In many existing Analytica models, one will see the following method used for aggregation:

Sum((MonthToYear = Year) * Revenue, Month)

This expression is functionally equivalent to:

Aggregate(Revenue, MonthToYear, Month, Year)

However, the use of Aggregate will evaluate much faster. When using the dot-product method with Sum, the intermediate expression (MonthToYear = Year) expands to a full 2-D array (Month x Year), which even though it is very sparse, still consumes lots of memory and time to process. In rough terms, the Sum-based method is O(n2) complexity, while Aggregate is only O(n).

The Frequency function can be used to aggregate, with either "sum" type aggregation (using the data itself as the weighting parameter) or "count" type aggregation. Frequency is in fact quite efficient for this purpose, but the usage for the purpose is unintuitive, making Aggregate the preferred alternative.

Slice Assignment can be used in a procedural-programming style to aggregate. This is a very general-purpose approach (can be employed for a variety of aggregation types) and can also be quite efficient. The general approach is demonstrated here (for Max-type aggregation):

Var res := Null;
For m := Month Do (
res[Year = MonthToYear] := Max([res[Year = MonthToYear], Revenue[Month = m]])
);
res

History

See Also

Comments


You are not allowed to post comments.