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.
@@ -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)