[ 
https://issues.apache.org/jira/browse/STATISTICS-101?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18115618#comment-18115618
 ] 

Alex Herbert commented on STATISTICS-101:
-----------------------------------------

h2. Implementation Details

The zeta distribution is defined over all positive integers. However the 
implementation is limited to a 32-bit signed integer by the 
DiscreteDistribution interface. This leads to parameterisations that can 
truncate the distribution. This occurs as s -> 1.
||s||x||sf(x; s)||
|1.20889000...|2147483647|0.01|
|1.10440113...|2147483647|0.1|
|1.03141836...|2147483647|0.5|
|1.00477515...|2147483647|0.9|

The survival function (sf) can be implemented using the Hurwitz zeta function 
for any parameters x and s with efficient evaluation and trivial loss of 
precision. But the cumulative probability function (cdf) can be evaluated 
either using 1 - sf, or a direct summation of the power terms of the series.

When the sf > 0.5 then the cdf will lose precision. The number of bits of 
precision lost is expected to be b-bits for a sf value > 1 - b^(b+1). For 
example
||sf||bits lost in 1 - sf||
|0.5|0|
|0.75|1|
|0.875|2|

If the survival function is above 0.5 evaluating the cdf by summation of terms 
is increasingly expensive as s -> 1. Allowing 2-bits of precision loss is a 
limit of 0.875 with the following number of terms x for different s:
||s||x||sf(x; s)||
|1.04565582...|10|0.875|
|1.02575353...|100|0.875|
|1.01784271...|1000|0.875|
|1.01364468...|10000|0.875|
|1.01104559...|100000|0.875|
|1.00927825...|1000000|0.875|
|1.00799849...|10000000|0.875|

There is always a parameterisation where summation of the terms will be too 
expensive for some use cases. Likewise there is no small number of terms where 
precision loss can be limited in all cases. The following shows the value of s 
for the sf(x=10). Each successive entry in the table will lose another bit of 
precision if the cdf = 1 - sf:
||s||x||sf(x; s)||
|1.23838860...|10|0.5|
|1.09852360...|10|0.75|
|1.04565582...|10|0.875|
|1.02204999...|10|0.9375|
|1.01084329...|10|0.96875|

Note that the implementation of the zeta function requires 9 calls to Math.pow 
and some additional overhead. So if the number of terms is small it is faster 
to compute the sum of the series of power terms rather than the zeta function.

To avoid excessive runtimes I have implemented the distribution using the 
survival function as the definitive cumulative probability result. This is 
accurate over the entire domain. The cdf is implemented using a sum of the 
power series if the number of terms is 10 or less; otherwise it uses 1 - sf. 
From the table above this should allow the cdf to maintain accuracy for s > 
1.045.

The cumulative probability range function, p(a < x <= b), is implemented in the 
same way. This must be considered increasingly less precise as the computed 
probability drops below 0.5 and the range b - a >= 10.
h2. High Precision CDF

The cumulative probability can be computed manually using a sum of the power 
terms. This must be normalised by the value of the zeta function for s. Note 
that the probability for x=1 is the term 1 / zeta(s). So the power terms can be 
computed and the distribution used to provide the normalisation factor. Summing 
the terms in ascending order of magnitude will reduce cumulative error and can 
be done for example using:
{code:java}
double s = ...
ZetaDistribution dist = ZetaDistribution.of(s);
double cdfX = IntStream.range(0, x)
                       .mapToDouble(y -> Math.pow(x - y, -s))
                       .sum() * dist.probability(1);
{code}
Summing in descending order of magnitude for the power terms (k=1 to x) can be 
used to compute the CDF for all 1 <= k <= x in a single pass loop. There may be 
loss of precision for large k due to error in the cumulative sum. This could be 
mitigated using an extended precision sum, for example using a double-double 
(DD) number from Commons Numbers:
{code:java}
doube s = ...
int x = ...
double[] cdf = new double[x];
double p1 = ZetaDistribution.of(s).probability(1);
cdf[0] = p1;
DD sum = DD.ONE;
for (int k = 2; k <= x; k++) {
    sum = sum.add(Math.pow(k, -s));
    cdf[k - 1] = sum.doubleValue() * p1;
}
{code}
h2. Sampling

Sampling can be done by inversion of the survival function for probability p. 
This will be slow due to the use of a search for x where sf(x; s) - p == 0. A 
single call to the survival function is also moderately expensive.

No zeta distribution sampler exists in Commons RNG. But a rejection method is 
supplied in:
{noformat}
Devroye, L (2015)
Non-uniform random variate generation.
Springer New York, NY. pp 550-552.
{noformat}
I have implemented this method and tested it where the quantiles of the 
distribution are spread over a reasonable range for x. This requires an s 
parameter in [1.1, 1.35]. When s -> 1 the samples are very skewed to x=1; when 
s -> large the samples are skewed towards infinity which is clipped to 
2147483647.

This implementation can be migrated to Commons RNG.

 

> Add a zeta distribution
> -----------------------
>
>                 Key: STATISTICS-101
>                 URL: https://issues.apache.org/jira/browse/STATISTICS-101
>             Project: Commons Statistics
>          Issue Type: New Feature
>          Components: distribution
>    Affects Versions: 1.3
>            Reporter: Alex Herbert
>            Priority: Minor
>
> The [zeta distribution 
> (wikipedia)|https://en.wikipedia.org/wiki/Zeta_distribution] is the large N 
> limit of the zipf distribution:
> {noformat}
> pmf(x) = k^-s / zeta(s)
> {noformat}
> Where zeta(s) is the Riemann zeta function.
> This can be implemented using the Hurwitz zeta function added for the zipf 
> distribution in [STATISTICS-100].



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to