rstatistics 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rstatistics-0.1.0/LICENSE +21 -0
- rstatistics-0.1.0/PKG-INFO +121 -0
- rstatistics-0.1.0/README.md +84 -0
- rstatistics-0.1.0/pyproject.toml +25 -0
- rstatistics-0.1.0/rstat/__init__.py +20 -0
- rstatistics-0.1.0/rstat/describe.py +313 -0
- rstatistics-0.1.0/rstat/distributions.py +489 -0
- rstatistics-0.1.0/rstat/htest.py +411 -0
- rstatistics-0.1.0/rstat/rformat.py +160 -0
- rstatistics-0.1.0/rstat/rng.py +162 -0
- rstatistics-0.1.0/rstat/special.py +310 -0
- rstatistics-0.1.0/rstatistics.egg-info/PKG-INFO +121 -0
- rstatistics-0.1.0/rstatistics.egg-info/SOURCES.txt +14 -0
- rstatistics-0.1.0/rstatistics.egg-info/dependency_links.txt +1 -0
- rstatistics-0.1.0/rstatistics.egg-info/top_level.txt +1 -0
- rstatistics-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ccc-claude-opus
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rstatistics
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: R's probability and statistics functions in pure Python: d/p/q/r distributions, t.test and friends, summary, quantile, with R's numbers and R's printed output
|
|
5
|
+
License: MIT License
|
|
6
|
+
|
|
7
|
+
Copyright (c) 2026 ccc-claude-opus
|
|
8
|
+
|
|
9
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
11
|
+
in the Software without restriction, including without limitation the rights
|
|
12
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
13
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
14
|
+
furnished to do so, subject to the following conditions:
|
|
15
|
+
|
|
16
|
+
The above copyright notice and this permission notice shall be included in all
|
|
17
|
+
copies or substantial portions of the Software.
|
|
18
|
+
|
|
19
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
20
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
21
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
22
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
23
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
24
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
25
|
+
SOFTWARE.
|
|
26
|
+
|
|
27
|
+
Project-URL: Homepage, https://github.com/ccc-claude-opus/rupy/tree/main/rstat
|
|
28
|
+
Keywords: statistics,probability,R,distributions,hypothesis-testing
|
|
29
|
+
Classifier: Programming Language :: Python :: 3
|
|
30
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
31
|
+
Classifier: Operating System :: OS Independent
|
|
32
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
33
|
+
Requires-Python: >=3.10
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
License-File: LICENSE
|
|
36
|
+
Dynamic: license-file
|
|
37
|
+
|
|
38
|
+
# rstat
|
|
39
|
+
|
|
40
|
+
R's probability and statistics functions in pure Python, with R's numbers and R's
|
|
41
|
+
printed output. There are no dependencies: vectors are plain `list[float]`.
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
pip install rstatistics
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The package installs as `rstatistics` and is imported as `rstat`.
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
import rstat as r
|
|
51
|
+
|
|
52
|
+
x = [5.1, 4.9, 6.2, 5.8, 6.0, 5.5, 5.3, 6.1]
|
|
53
|
+
print(r.t_test(x, mu=5.0))
|
|
54
|
+
print(r.summary(x))
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
One Sample t-test
|
|
59
|
+
|
|
60
|
+
data: x
|
|
61
|
+
t = 3.5696, df = 7, p-value = 0.009101
|
|
62
|
+
alternative hypothesis: true mean is not equal to 5
|
|
63
|
+
95 percent confidence interval:
|
|
64
|
+
5.206762 6.018238
|
|
65
|
+
sample estimates:
|
|
66
|
+
mean of x
|
|
67
|
+
5.6125
|
|
68
|
+
|
|
69
|
+
Min. 1st Qu. Median Mean 3rd Qu. Max.
|
|
70
|
+
4.900 5.250 5.650 5.612 6.025 6.200
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
That is what R prints for `t.test(x, mu = 5)` and `summary(x)`.
|
|
74
|
+
|
|
75
|
+
## Names
|
|
76
|
+
|
|
77
|
+
R's names, with `_` for `.`: `t.test` is `t_test`, `lower.tail` is `lower_tail`,
|
|
78
|
+
`var.equal` is `var_equal`. R's `range`, `round` and `signif` of vectors are
|
|
79
|
+
`range_`, `round_` and `signif_`.
|
|
80
|
+
|
|
81
|
+
## What there is
|
|
82
|
+
|
|
83
|
+
| Area | Functions |
|
|
84
|
+
| --- | --- |
|
|
85
|
+
| Distributions (`d`, `p`, `q`) | `norm`, `unif`, `exp`, `gamma`, `chisq`, `beta`, `t`, `f` (`df`, `pf`, `qf`), `lnorm`, `weibull`, `cauchy`, `logis`, `binom`, `pois`, `geom`, `nbinom` (`d`, `p`), `hyper` (`d`, `p`); `lower_tail` and `log_p` as in R |
|
|
86
|
+
| Random numbers | `set_seed(seed)` returns a generator with R's numbers: `runif`, `rnorm`, `rexp`, `rlnorm`, `sample`, `sample_from` |
|
|
87
|
+
| Descriptive | `mean`, `var`, `sd`, `cov`, `cor` (pearson, spearman, kendall), `median`, `quantile` (types 1–9), `IQR`, `mad`, `fivenum`, `rank`, `scale`, `weighted_mean`, `summary` |
|
|
88
|
+
| Tests | `t_test` (one-sample, Welch, pooled, paired), `var_test`, `chisq_test`, `chisq_test_table`, `binom_test`, `prop_test`, `cor_test`; each returns an `HTest` that prints as R's `htest` and has `statistic`, `parameter`, `p_value`, `conf_int`, `estimate` |
|
|
89
|
+
| Special functions | `gamma`, `lgamma`, `beta`, `lbeta`, `choose`, `lchoose` |
|
|
90
|
+
| Printing | `format` (R's `format` of a numeric vector), `format_num`, `format_pval`, `print_vector`, `signif` |
|
|
91
|
+
|
|
92
|
+
### Random numbers
|
|
93
|
+
|
|
94
|
+
R keeps one generator for the session; rstat passes it explicitly. After
|
|
95
|
+
`set_seed(n)` the numbers are R's after `set.seed(n)` (Mersenne-Twister, normal
|
|
96
|
+
numbers by inversion, R ≥ 3.6's rejection sampling):
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
g = r.set_seed(42)
|
|
100
|
+
print(r.print_vector(g.rnorm(5)))
|
|
101
|
+
# [1] 1.3709584 -0.5646982 0.3631284 0.6328626 0.4042683
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## How close to R
|
|
105
|
+
|
|
106
|
+
`tests/` holds R scripts, their output in R 4.6, and the same programs in Python;
|
|
107
|
+
`python tests/run.py` compares them (`--regen` reruns R first). The printed output
|
|
108
|
+
of the tests, `format`, `summary`, `quantile` and the random numbers is identical
|
|
109
|
+
to R's; distribution functions agree with R to a relative 1e-12. One difference:
|
|
110
|
+
R names the data of a test from the call (`data: c(20, 30, 25, 25)`), rstat from
|
|
111
|
+
the `data_name` argument (default `x`, `x and y`, ...).
|
|
112
|
+
|
|
113
|
+
## rupy
|
|
114
|
+
|
|
115
|
+
rstat is written in [rupy](https://github.com/ccc-claude-opus/rupy)'s typed subset
|
|
116
|
+
of Python, and rupy has it built in: `rupy run` compiles a program that imports
|
|
117
|
+
rstat to Rust, `rupy wasm` to WebAssembly, with the same output as CPython.
|
|
118
|
+
|
|
119
|
+
## License
|
|
120
|
+
|
|
121
|
+
MIT
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# rstat
|
|
2
|
+
|
|
3
|
+
R's probability and statistics functions in pure Python, with R's numbers and R's
|
|
4
|
+
printed output. There are no dependencies: vectors are plain `list[float]`.
|
|
5
|
+
|
|
6
|
+
```
|
|
7
|
+
pip install rstatistics
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
The package installs as `rstatistics` and is imported as `rstat`.
|
|
11
|
+
|
|
12
|
+
```python
|
|
13
|
+
import rstat as r
|
|
14
|
+
|
|
15
|
+
x = [5.1, 4.9, 6.2, 5.8, 6.0, 5.5, 5.3, 6.1]
|
|
16
|
+
print(r.t_test(x, mu=5.0))
|
|
17
|
+
print(r.summary(x))
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
One Sample t-test
|
|
22
|
+
|
|
23
|
+
data: x
|
|
24
|
+
t = 3.5696, df = 7, p-value = 0.009101
|
|
25
|
+
alternative hypothesis: true mean is not equal to 5
|
|
26
|
+
95 percent confidence interval:
|
|
27
|
+
5.206762 6.018238
|
|
28
|
+
sample estimates:
|
|
29
|
+
mean of x
|
|
30
|
+
5.6125
|
|
31
|
+
|
|
32
|
+
Min. 1st Qu. Median Mean 3rd Qu. Max.
|
|
33
|
+
4.900 5.250 5.650 5.612 6.025 6.200
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
That is what R prints for `t.test(x, mu = 5)` and `summary(x)`.
|
|
37
|
+
|
|
38
|
+
## Names
|
|
39
|
+
|
|
40
|
+
R's names, with `_` for `.`: `t.test` is `t_test`, `lower.tail` is `lower_tail`,
|
|
41
|
+
`var.equal` is `var_equal`. R's `range`, `round` and `signif` of vectors are
|
|
42
|
+
`range_`, `round_` and `signif_`.
|
|
43
|
+
|
|
44
|
+
## What there is
|
|
45
|
+
|
|
46
|
+
| Area | Functions |
|
|
47
|
+
| --- | --- |
|
|
48
|
+
| Distributions (`d`, `p`, `q`) | `norm`, `unif`, `exp`, `gamma`, `chisq`, `beta`, `t`, `f` (`df`, `pf`, `qf`), `lnorm`, `weibull`, `cauchy`, `logis`, `binom`, `pois`, `geom`, `nbinom` (`d`, `p`), `hyper` (`d`, `p`); `lower_tail` and `log_p` as in R |
|
|
49
|
+
| Random numbers | `set_seed(seed)` returns a generator with R's numbers: `runif`, `rnorm`, `rexp`, `rlnorm`, `sample`, `sample_from` |
|
|
50
|
+
| Descriptive | `mean`, `var`, `sd`, `cov`, `cor` (pearson, spearman, kendall), `median`, `quantile` (types 1–9), `IQR`, `mad`, `fivenum`, `rank`, `scale`, `weighted_mean`, `summary` |
|
|
51
|
+
| Tests | `t_test` (one-sample, Welch, pooled, paired), `var_test`, `chisq_test`, `chisq_test_table`, `binom_test`, `prop_test`, `cor_test`; each returns an `HTest` that prints as R's `htest` and has `statistic`, `parameter`, `p_value`, `conf_int`, `estimate` |
|
|
52
|
+
| Special functions | `gamma`, `lgamma`, `beta`, `lbeta`, `choose`, `lchoose` |
|
|
53
|
+
| Printing | `format` (R's `format` of a numeric vector), `format_num`, `format_pval`, `print_vector`, `signif` |
|
|
54
|
+
|
|
55
|
+
### Random numbers
|
|
56
|
+
|
|
57
|
+
R keeps one generator for the session; rstat passes it explicitly. After
|
|
58
|
+
`set_seed(n)` the numbers are R's after `set.seed(n)` (Mersenne-Twister, normal
|
|
59
|
+
numbers by inversion, R ≥ 3.6's rejection sampling):
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
g = r.set_seed(42)
|
|
63
|
+
print(r.print_vector(g.rnorm(5)))
|
|
64
|
+
# [1] 1.3709584 -0.5646982 0.3631284 0.6328626 0.4042683
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## How close to R
|
|
68
|
+
|
|
69
|
+
`tests/` holds R scripts, their output in R 4.6, and the same programs in Python;
|
|
70
|
+
`python tests/run.py` compares them (`--regen` reruns R first). The printed output
|
|
71
|
+
of the tests, `format`, `summary`, `quantile` and the random numbers is identical
|
|
72
|
+
to R's; distribution functions agree with R to a relative 1e-12. One difference:
|
|
73
|
+
R names the data of a test from the call (`data: c(20, 30, 25, 25)`), rstat from
|
|
74
|
+
the `data_name` argument (default `x`, `x and y`, ...).
|
|
75
|
+
|
|
76
|
+
## rupy
|
|
77
|
+
|
|
78
|
+
rstat is written in [rupy](https://github.com/ccc-claude-opus/rupy)'s typed subset
|
|
79
|
+
of Python, and rupy has it built in: `rupy run` compiles a program that imports
|
|
80
|
+
rstat to Rust, `rupy wasm` to WebAssembly, with the same output as CPython.
|
|
81
|
+
|
|
82
|
+
## License
|
|
83
|
+
|
|
84
|
+
MIT
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "rstatistics"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "R's probability and statistics functions in pure Python: d/p/q/r distributions, t.test and friends, summary, quantile, with R's numbers and R's printed output"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { file = "LICENSE" }
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
dependencies = []
|
|
13
|
+
keywords = ["statistics", "probability", "R", "distributions", "hypothesis-testing"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
"Topic :: Scientific/Engineering :: Mathematics",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.urls]
|
|
22
|
+
Homepage = "https://github.com/ccc-claude-opus/rupy/tree/main/rstat"
|
|
23
|
+
|
|
24
|
+
[tool.setuptools]
|
|
25
|
+
packages = ["rstat"]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""rstat: R's probability and statistics functions in Python.
|
|
2
|
+
|
|
3
|
+
The names are R's, with `_` for `.` (`t.test` is `t_test`); arguments are R's
|
|
4
|
+
too (`lower.tail` is `lower_tail`). Printed results look as in R. The package
|
|
5
|
+
is written in rupy's typed subset of Python, so rupy compiles it to Rust or
|
|
6
|
+
WebAssembly as well."""
|
|
7
|
+
|
|
8
|
+
from rstat.distributions import (
|
|
9
|
+
dbeta, dbinom, dcauchy, dchisq, dexp, df, dgamma, dgeom, dhyper, dlnorm, dlogis, dnbinom, dnorm, dpois, dt,
|
|
10
|
+
dunif, dweibull, pbeta, pbinom, pcauchy, pchisq, pexp, pf, pgamma, pgeom, phyper, plnorm, plogis, pnbinom,
|
|
11
|
+
pnorm, ppois, pt, punif, pweibull, qbeta, qbinom, qcauchy, qchisq, qexp, qf, qgamma, qgeom, qlnorm, qlogis,
|
|
12
|
+
qnorm, qpois, qt, qunif, qweibull,
|
|
13
|
+
)
|
|
14
|
+
from rstat.describe import IQR, Named, Summary, cor, cov, fivenum, mad, mean, median, quantile, rank, scale, sd, summary, var, weighted_mean
|
|
15
|
+
from rstat.htest import HTest, binom_test, chisq_test, chisq_test_table, cor_test, prop_test, t_test, var_test
|
|
16
|
+
from rstat.rformat import format_vec, format_vec as format, format_num, format_pval, print_vector, signif
|
|
17
|
+
from rstat.rng import RNG, set_seed
|
|
18
|
+
from rstat.special import beta, choose, gamma, lbeta, lchoose, lgamma
|
|
19
|
+
|
|
20
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
"""Descriptive statistics with R's algorithms (two-pass means and variances,
|
|
2
|
+
the nine quantile types) and R's printed form (named vectors, `summary`)."""
|
|
3
|
+
|
|
4
|
+
import math
|
|
5
|
+
|
|
6
|
+
from rstat.rformat import format_vec, format_num, print_named, signif
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Named:
|
|
10
|
+
"""A named numeric vector, printed as R prints one."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, names: list[str], values: list[float], digits: int = 7) -> None:
|
|
13
|
+
self.names = names
|
|
14
|
+
self.values = values
|
|
15
|
+
self.digits = digits
|
|
16
|
+
|
|
17
|
+
def __str__(self) -> str:
|
|
18
|
+
return print_named(self.names, format_vec(self.values, self.digits))
|
|
19
|
+
|
|
20
|
+
def __repr__(self) -> str:
|
|
21
|
+
return self.__str__()
|
|
22
|
+
|
|
23
|
+
def __getitem__(self, i: int) -> float:
|
|
24
|
+
return self.values[i]
|
|
25
|
+
|
|
26
|
+
def __len__(self) -> int:
|
|
27
|
+
return len(self.values)
|
|
28
|
+
|
|
29
|
+
def get(self, name: str) -> float:
|
|
30
|
+
"""The value named `name`."""
|
|
31
|
+
return self.values[self.names.index(name)]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _check(x: list[float]) -> None:
|
|
35
|
+
if len(x) == 0:
|
|
36
|
+
raise ValueError("'x' must have at least one element")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def mean(x: list[float]) -> float:
|
|
40
|
+
"""R's mean: the sum over n, corrected by a second pass."""
|
|
41
|
+
_check(x)
|
|
42
|
+
n = float(len(x))
|
|
43
|
+
s = 0.0
|
|
44
|
+
for v in x:
|
|
45
|
+
s += v
|
|
46
|
+
s /= n
|
|
47
|
+
if not math.isinf(s) and not math.isnan(s):
|
|
48
|
+
t = 0.0
|
|
49
|
+
for v in x:
|
|
50
|
+
t += v - s
|
|
51
|
+
s += t / n
|
|
52
|
+
return s
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def var(x: list[float]) -> float:
|
|
56
|
+
"""The sample variance (denominator n - 1)."""
|
|
57
|
+
n = len(x)
|
|
58
|
+
if n < 2:
|
|
59
|
+
return math.nan
|
|
60
|
+
m = mean(x)
|
|
61
|
+
s = 0.0
|
|
62
|
+
for v in x:
|
|
63
|
+
s += (v - m) * (v - m)
|
|
64
|
+
return s / float(n - 1)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def sd(x: list[float]) -> float:
|
|
68
|
+
"""The sample standard deviation."""
|
|
69
|
+
return math.sqrt(var(x))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def cov(x: list[float], y: list[float]) -> float:
|
|
73
|
+
"""The sample covariance."""
|
|
74
|
+
if len(x) != len(y):
|
|
75
|
+
raise ValueError("incompatible dimensions")
|
|
76
|
+
n = len(x)
|
|
77
|
+
if n < 2:
|
|
78
|
+
return math.nan
|
|
79
|
+
mx = mean(x)
|
|
80
|
+
my = mean(y)
|
|
81
|
+
s = 0.0
|
|
82
|
+
for i in range(n):
|
|
83
|
+
s += (x[i] - mx) * (y[i] - my)
|
|
84
|
+
return s / float(n - 1)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def rank(x: list[float]) -> list[float]:
|
|
88
|
+
"""Ranks, ties getting their average rank (R's `rank`)."""
|
|
89
|
+
n = len(x)
|
|
90
|
+
order = sorted(range(n), key=lambda i: x[i])
|
|
91
|
+
r = [0.0] * n
|
|
92
|
+
i = 0
|
|
93
|
+
while i < n:
|
|
94
|
+
j = i
|
|
95
|
+
while j + 1 < n and x[order[j + 1]] == x[order[i]]:
|
|
96
|
+
j += 1
|
|
97
|
+
avg = (float(i) + float(j)) / 2.0 + 1.0
|
|
98
|
+
for k in range(i, j + 1):
|
|
99
|
+
r[order[k]] = avg
|
|
100
|
+
i = j + 1
|
|
101
|
+
return r
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def cor(x: list[float], y: list[float], method: str = "pearson") -> float:
|
|
105
|
+
"""The correlation of x and y: "pearson", "spearman" or "kendall" (tau-b)."""
|
|
106
|
+
if len(x) != len(y):
|
|
107
|
+
raise ValueError("incompatible dimensions")
|
|
108
|
+
if method == "spearman":
|
|
109
|
+
return cor(rank(x), rank(y), "pearson")
|
|
110
|
+
if method == "kendall":
|
|
111
|
+
n = len(x)
|
|
112
|
+
conc = 0.0
|
|
113
|
+
tx = 0.0
|
|
114
|
+
ty = 0.0
|
|
115
|
+
for i in range(n):
|
|
116
|
+
for j in range(i + 1, n):
|
|
117
|
+
a = x[i] - x[j]
|
|
118
|
+
b = y[i] - y[j]
|
|
119
|
+
sa = 1.0 if a > 0.0 else (-1.0 if a < 0.0 else 0.0)
|
|
120
|
+
sb = 1.0 if b > 0.0 else (-1.0 if b < 0.0 else 0.0)
|
|
121
|
+
conc += sa * sb
|
|
122
|
+
tx += sa * sa
|
|
123
|
+
ty += sb * sb
|
|
124
|
+
return conc / math.sqrt(tx * ty)
|
|
125
|
+
mx = mean(x)
|
|
126
|
+
my = mean(y)
|
|
127
|
+
sxy = 0.0
|
|
128
|
+
sxx = 0.0
|
|
129
|
+
syy = 0.0
|
|
130
|
+
for i in range(len(x)):
|
|
131
|
+
dx = x[i] - mx
|
|
132
|
+
dy = y[i] - my
|
|
133
|
+
sxy += dx * dy
|
|
134
|
+
sxx += dx * dx
|
|
135
|
+
syy += dy * dy
|
|
136
|
+
r = sxy / math.sqrt(sxx * syy)
|
|
137
|
+
return max(-1.0, min(1.0, r))
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def median(x: list[float]) -> float:
|
|
141
|
+
"""The median."""
|
|
142
|
+
_check(x)
|
|
143
|
+
s = sorted(x)
|
|
144
|
+
n = len(s)
|
|
145
|
+
half = (n + 1) // 2
|
|
146
|
+
if n % 2 == 1:
|
|
147
|
+
return s[half - 1]
|
|
148
|
+
return mean([s[half - 1], s[half]])
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def quantile(x: list[float], probs: list[float] | None = None, type: int = 7) -> Named:
|
|
152
|
+
"""Sample quantiles of the nine types of R's `quantile` (default 7; probs
|
|
153
|
+
default 0, 0.25, 0.5, 0.75, 1), named as R names them ("25%")."""
|
|
154
|
+
_check(x)
|
|
155
|
+
ps = [0.0, 0.25, 0.5, 0.75, 1.0] if probs is None else probs
|
|
156
|
+
s = sorted(x)
|
|
157
|
+
n = len(s)
|
|
158
|
+
out: list[float] = []
|
|
159
|
+
eps = 2.220446049250313e-16
|
|
160
|
+
for p in ps:
|
|
161
|
+
if p < 0.0 or p > 1.0:
|
|
162
|
+
raise ValueError("'probs' outside [0,1]")
|
|
163
|
+
if type == 7:
|
|
164
|
+
index = 1.0 + float(max(n - 1, 0)) * p
|
|
165
|
+
lo = int(math.floor(index))
|
|
166
|
+
hi = int(math.ceil(index))
|
|
167
|
+
q = s[lo - 1]
|
|
168
|
+
h = index - float(lo)
|
|
169
|
+
if index > float(lo) and s[hi - 1] != q:
|
|
170
|
+
q = (1.0 - h) * q + h * s[hi - 1]
|
|
171
|
+
out.append(q)
|
|
172
|
+
continue
|
|
173
|
+
if type <= 3:
|
|
174
|
+
nppm = float(n) * p - 0.5 if type == 3 else float(n) * p
|
|
175
|
+
j = int(math.floor(nppm + 4.0 * eps))
|
|
176
|
+
if type == 1:
|
|
177
|
+
h = 1.0 if nppm > float(j) else 0.0
|
|
178
|
+
elif type == 2:
|
|
179
|
+
h = ((1.0 if nppm > float(j) else 0.0) + 1.0) / 2.0
|
|
180
|
+
else:
|
|
181
|
+
h = 1.0 if (nppm != float(j) or j % 2 == 1) else 0.0
|
|
182
|
+
else:
|
|
183
|
+
a = 0.0
|
|
184
|
+
b = 1.0
|
|
185
|
+
if type == 5:
|
|
186
|
+
a = 0.5
|
|
187
|
+
b = 0.5
|
|
188
|
+
elif type == 6:
|
|
189
|
+
a = 0.0
|
|
190
|
+
b = 0.0
|
|
191
|
+
elif type == 8:
|
|
192
|
+
a = 1.0 / 3.0
|
|
193
|
+
b = 1.0 / 3.0
|
|
194
|
+
elif type == 9:
|
|
195
|
+
a = 3.0 / 8.0
|
|
196
|
+
b = 3.0 / 8.0
|
|
197
|
+
fuzz = 4.0 * eps
|
|
198
|
+
nppm = a + p * (float(n) + 1.0 - a - b)
|
|
199
|
+
j = int(math.floor(nppm + fuzz))
|
|
200
|
+
h = nppm - float(j)
|
|
201
|
+
if abs(h) < fuzz:
|
|
202
|
+
h = 0.0
|
|
203
|
+
# x padded as R pads it: x[1], x[1], x..., x[n], x[n]
|
|
204
|
+
xs = [s[0], s[0]] + s + [s[n - 1], s[n - 1]]
|
|
205
|
+
lo_v = xs[j + 1]
|
|
206
|
+
hi_v = xs[j + 2]
|
|
207
|
+
if h == 1.0:
|
|
208
|
+
q = hi_v
|
|
209
|
+
elif 0.0 < h < 1.0 and lo_v != hi_v:
|
|
210
|
+
q = (1.0 - h) * lo_v + h * hi_v
|
|
211
|
+
else:
|
|
212
|
+
q = lo_v
|
|
213
|
+
out.append(q)
|
|
214
|
+
names = [_trim_pct(100.0 * p) for p in ps]
|
|
215
|
+
return Named(names, out)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _trim_pct(v: float) -> str:
|
|
219
|
+
"""`paste0(formatC(100 * probs, format = "fg", width = 1, digits = 7), "%")`."""
|
|
220
|
+
s = format_num(v, 7).strip()
|
|
221
|
+
if "." in s and "e" not in s:
|
|
222
|
+
s = s.rstrip("0").rstrip(".")
|
|
223
|
+
return s + "%"
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def IQR(x: list[float], type: int = 7) -> float:
|
|
227
|
+
"""The interquartile range."""
|
|
228
|
+
q = quantile(x, [0.25, 0.75], type)
|
|
229
|
+
return q[1] - q[0]
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def mad(x: list[float], constant: float = 1.4826) -> float:
|
|
233
|
+
"""The median absolute deviation (scaled to estimate σ of normal data)."""
|
|
234
|
+
m = median(x)
|
|
235
|
+
return constant * median([abs(v - m) for v in x])
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def fivenum(x: list[float]) -> list[float]:
|
|
239
|
+
"""Tukey's five numbers: minimum, lower hinge, median, upper hinge, maximum."""
|
|
240
|
+
_check(x)
|
|
241
|
+
s = sorted(x)
|
|
242
|
+
n = len(s)
|
|
243
|
+
n4 = float(math.floor((float(n) + 3.0) / 2.0)) / 2.0
|
|
244
|
+
d = [1.0, n4, (float(n) + 1.0) / 2.0, float(n) + 1.0 - n4, float(n)]
|
|
245
|
+
out: list[float] = []
|
|
246
|
+
for v in d:
|
|
247
|
+
lo = int(math.floor(v))
|
|
248
|
+
hi = int(math.ceil(v))
|
|
249
|
+
out.append(0.5 * (s[lo - 1] + s[hi - 1]))
|
|
250
|
+
return out
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def scale(x: list[float]) -> list[float]:
|
|
254
|
+
"""x centered by its mean and divided by its standard deviation."""
|
|
255
|
+
m = mean(x)
|
|
256
|
+
s = sd(x)
|
|
257
|
+
return [(v - m) / s for v in x]
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def weighted_mean(x: list[float], w: list[float]) -> float:
|
|
261
|
+
"""The weighted mean."""
|
|
262
|
+
sw = 0.0
|
|
263
|
+
swx = 0.0
|
|
264
|
+
for i in range(len(x)):
|
|
265
|
+
sw += w[i]
|
|
266
|
+
swx += w[i] * x[i]
|
|
267
|
+
return swx / sw
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
class Summary:
|
|
271
|
+
"""`summary(x)` of a numeric vector, printed as R prints it."""
|
|
272
|
+
|
|
273
|
+
def __init__(self, values: list[float]) -> None:
|
|
274
|
+
self.names = ["Min.", "1st Qu.", "Median", "Mean", "3rd Qu.", "Max."]
|
|
275
|
+
self.values = values
|
|
276
|
+
|
|
277
|
+
def __str__(self) -> str:
|
|
278
|
+
# R's format.summaryDefault: 4 significant digits, after zapping
|
|
279
|
+
# values tiny next to the largest
|
|
280
|
+
digits = 4
|
|
281
|
+
mx = max(abs(v) for v in self.values)
|
|
282
|
+
zap = [v for v in self.values]
|
|
283
|
+
if mx > 0.0:
|
|
284
|
+
lim = 10.0 ** (float(math.floor(math.log10(mx))) - 7.0 - digits + 1.0)
|
|
285
|
+
zap = [0.0 if abs(v) < lim else v for v in self.values]
|
|
286
|
+
return print_named(self.names, format_vec(zap, digits))
|
|
287
|
+
|
|
288
|
+
def __repr__(self) -> str:
|
|
289
|
+
return self.__str__()
|
|
290
|
+
|
|
291
|
+
def get(self, name: str) -> float:
|
|
292
|
+
return self.values[self.names.index(name)]
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def summary(x: list[float]) -> Summary:
|
|
296
|
+
"""Minimum, quartiles, mean and maximum (R's `summary` of a numeric vector)."""
|
|
297
|
+
q = quantile(x)
|
|
298
|
+
return Summary([q[0], q[1], q[2], mean(x), q[3], q[4]])
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def range_(x: list[float]) -> list[float]:
|
|
302
|
+
"""`range(x)`: the minimum and the maximum."""
|
|
303
|
+
return [min(x), max(x)]
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def round_(x: float, digits: int = 0) -> float:
|
|
307
|
+
"""R's `round` (to even, at the decimal digit)."""
|
|
308
|
+
return round(x, digits)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def signif_(x: float, digits: int = 6) -> float:
|
|
312
|
+
"""R's `signif`."""
|
|
313
|
+
return signif(x, digits)
|