rstatistics 0.1.0__py3-none-any.whl
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.
- rstat/__init__.py +20 -0
- rstat/describe.py +313 -0
- rstat/distributions.py +489 -0
- rstat/htest.py +411 -0
- rstat/rformat.py +160 -0
- rstat/rng.py +162 -0
- rstat/special.py +310 -0
- rstatistics-0.1.0.dist-info/METADATA +121 -0
- rstatistics-0.1.0.dist-info/RECORD +12 -0
- rstatistics-0.1.0.dist-info/WHEEL +5 -0
- rstatistics-0.1.0.dist-info/licenses/LICENSE +21 -0
- rstatistics-0.1.0.dist-info/top_level.txt +1 -0
rstat/__init__.py
ADDED
|
@@ -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"
|
rstat/describe.py
ADDED
|
@@ -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)
|