stats3 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.
- stats3/__init__.py +88 -0
- stats3/__version__.py +1 -0
- stats3/_data.py +269 -0
- stats3/checks.py +461 -0
- stats3/core.py +530 -0
- stats3/counts.py +643 -0
- stats3/effects.py +481 -0
- stats3/formula.py +433 -0
- stats3/jupyter.py +77 -0
- stats3/masking.py +213 -0
- stats3/models.py +1497 -0
- stats3/py.typed +0 -0
- stats3/ranks.py +750 -0
- stats3-0.1.0.dist-info/METADATA +232 -0
- stats3-0.1.0.dist-info/RECORD +18 -0
- stats3-0.1.0.dist-info/WHEEL +5 -0
- stats3-0.1.0.dist-info/licenses/LICENSE +21 -0
- stats3-0.1.0.dist-info/top_level.txt +1 -0
stats3/__init__.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""R's statistical tests on Polars, for Polars, pandas and tidy3 data.
|
|
2
|
+
|
|
3
|
+
from stats3 import *
|
|
4
|
+
|
|
5
|
+
penguins >> t_test("body_mass_g ~ sex", by="species") # Polars or pandas
|
|
6
|
+
t_test(penguins, "body_mass_g ~ sex", by="species") # data first
|
|
7
|
+
penguins >> group_by(species) >> t_test(body_mass_g ~ sex) # tidy3, notebook
|
|
8
|
+
|
|
9
|
+
Each test returns one row per group, with R's defaults and R's results, in
|
|
10
|
+
the kind of frame it was given.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from stats3.__version__ import __version__
|
|
14
|
+
from stats3.checks import bartlett_test, ks_test, levene_test, shapiro_test, var_test
|
|
15
|
+
from stats3.core import aov, t_test, tukey_hsd
|
|
16
|
+
from stats3.counts import binom_test, chisq_test, fisher_test, prop_test
|
|
17
|
+
from stats3.effects import (
|
|
18
|
+
cohens_d,
|
|
19
|
+
eta_squared,
|
|
20
|
+
hedges_g,
|
|
21
|
+
p_adjust,
|
|
22
|
+
power_prop_test,
|
|
23
|
+
power_t_test,
|
|
24
|
+
)
|
|
25
|
+
from stats3.models import (
|
|
26
|
+
Gamma,
|
|
27
|
+
anova,
|
|
28
|
+
augment,
|
|
29
|
+
binomial,
|
|
30
|
+
gaussian,
|
|
31
|
+
glance,
|
|
32
|
+
glm,
|
|
33
|
+
inverse_gaussian,
|
|
34
|
+
lm,
|
|
35
|
+
poisson,
|
|
36
|
+
predict,
|
|
37
|
+
quasibinomial,
|
|
38
|
+
quasipoisson,
|
|
39
|
+
tidy,
|
|
40
|
+
)
|
|
41
|
+
from stats3.ranks import cor_test, kruskal_test, wilcox_test
|
|
42
|
+
|
|
43
|
+
# In IPython / Jupyter, turn on unquoted formulas and bare column names in
|
|
44
|
+
# stats3 calls (from the next cell on), as tidy3 does for its verbs.
|
|
45
|
+
try:
|
|
46
|
+
from stats3.jupyter import enable as _enable_notebook
|
|
47
|
+
|
|
48
|
+
_enable_notebook()
|
|
49
|
+
except Exception: # pragma: no cover - never let the shell block an import
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
__all__ = [
|
|
53
|
+
"anova",
|
|
54
|
+
"aov",
|
|
55
|
+
"augment",
|
|
56
|
+
"bartlett_test",
|
|
57
|
+
"binom_test",
|
|
58
|
+
"binomial",
|
|
59
|
+
"chisq_test",
|
|
60
|
+
"cohens_d",
|
|
61
|
+
"cor_test",
|
|
62
|
+
"eta_squared",
|
|
63
|
+
"fisher_test",
|
|
64
|
+
"Gamma",
|
|
65
|
+
"gaussian",
|
|
66
|
+
"glance",
|
|
67
|
+
"glm",
|
|
68
|
+
"hedges_g",
|
|
69
|
+
"inverse_gaussian",
|
|
70
|
+
"kruskal_test",
|
|
71
|
+
"ks_test",
|
|
72
|
+
"levene_test",
|
|
73
|
+
"lm",
|
|
74
|
+
"p_adjust",
|
|
75
|
+
"poisson",
|
|
76
|
+
"power_prop_test",
|
|
77
|
+
"power_t_test",
|
|
78
|
+
"predict",
|
|
79
|
+
"prop_test",
|
|
80
|
+
"quasibinomial",
|
|
81
|
+
"quasipoisson",
|
|
82
|
+
"shapiro_test",
|
|
83
|
+
"t_test",
|
|
84
|
+
"tidy",
|
|
85
|
+
"tukey_hsd",
|
|
86
|
+
"var_test",
|
|
87
|
+
"wilcox_test",
|
|
88
|
+
]
|
stats3/__version__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
stats3/_data.py
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"""Data in and out: pandas, Polars and tidy3 frames, without importing them.
|
|
2
|
+
|
|
3
|
+
Every test works on a ``Data``: a Polars LazyFrame, the grouping columns, and
|
|
4
|
+
the factor levels that order a grouping factor's levels. ``Data.read`` builds
|
|
5
|
+
one from whatever the caller passed, and ``Data.write`` turns the result back
|
|
6
|
+
into the same kind of frame:
|
|
7
|
+
|
|
8
|
+
======================== ================== ===========================
|
|
9
|
+
input groups factor levels
|
|
10
|
+
======================== ================== ===========================
|
|
11
|
+
Polars DataFrame / Lazy ``by=`` ``Enum`` columns
|
|
12
|
+
pandas DataFrame ``by=`` ``category`` columns
|
|
13
|
+
tidy3 TidyFrame its ``group_by`` its recorded levels
|
|
14
|
+
dict of columns ``by=`` none
|
|
15
|
+
======================== ================== ===========================
|
|
16
|
+
|
|
17
|
+
pandas and tidy3 are recognised by their class and module, so stats3 imports
|
|
18
|
+
neither; a TidyFrame can only exist when tidy3 is already loaded.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import sys
|
|
24
|
+
from collections.abc import Mapping
|
|
25
|
+
from typing import Any, Callable
|
|
26
|
+
|
|
27
|
+
import polars as pl
|
|
28
|
+
|
|
29
|
+
OUTPUTS = ("polars", "pandas", "tidy3")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _is_pandas_frame(value: Any) -> bool:
|
|
33
|
+
cls = type(value)
|
|
34
|
+
return cls.__name__ == "DataFrame" and cls.__module__.startswith("pandas")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _is_pandas_series(value: Any) -> bool:
|
|
38
|
+
cls = type(value)
|
|
39
|
+
return cls.__name__ == "Series" and cls.__module__.startswith("pandas")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _is_tidy(value: Any) -> bool:
|
|
43
|
+
cls = type(value)
|
|
44
|
+
return cls.__name__ == "TidyFrame" and cls.__module__.startswith("tidy3")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def is_frame(value: Any) -> bool:
|
|
48
|
+
"""True for anything stats3 can read as a table."""
|
|
49
|
+
return (
|
|
50
|
+
isinstance(value, (pl.DataFrame, pl.LazyFrame))
|
|
51
|
+
or _is_pandas_frame(value)
|
|
52
|
+
or _is_tidy(value)
|
|
53
|
+
or (isinstance(value, Mapping) and not isinstance(value, str))
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _tidy_expr(value: Any) -> bool:
|
|
58
|
+
"""A tidy3 expression (``col("x")``, ``sex == "male"``), duck-typed."""
|
|
59
|
+
node = getattr(value, "node", None)
|
|
60
|
+
return isinstance(node, tuple) and type(value).__module__.startswith("tidy3")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def column_name(spec: Any) -> str | None:
|
|
64
|
+
"""The column a spec names, or None when it is a computed expression.
|
|
65
|
+
|
|
66
|
+
Accepts a string (tidy3's bare notebook names are strings too), a Polars
|
|
67
|
+
column expression, or a tidy3 ``col("x")``.
|
|
68
|
+
"""
|
|
69
|
+
if isinstance(spec, str):
|
|
70
|
+
return str(spec)
|
|
71
|
+
if isinstance(spec, pl.Expr):
|
|
72
|
+
if spec.meta.is_column():
|
|
73
|
+
return spec.meta.output_name()
|
|
74
|
+
return None
|
|
75
|
+
if _tidy_expr(spec) and spec.node[0] in {"col", "name"}:
|
|
76
|
+
return str(spec.node[1])
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def expression(spec: Any) -> pl.Expr:
|
|
81
|
+
"""A Polars expression for a column name, Polars expression, or tidy3
|
|
82
|
+
expression (compiled with tidy3, which is loaded if one exists)."""
|
|
83
|
+
if isinstance(spec, pl.Expr):
|
|
84
|
+
return spec
|
|
85
|
+
name = column_name(spec)
|
|
86
|
+
if name is not None:
|
|
87
|
+
return pl.col(name)
|
|
88
|
+
if _tidy_expr(spec):
|
|
89
|
+
return sys.modules["tidy3.expr"].to_polars(spec)
|
|
90
|
+
raise TypeError(f"expected a column name or expression, got {spec!r}")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class Data:
|
|
94
|
+
"""One input table, read once: rows, groups, and factor levels."""
|
|
95
|
+
|
|
96
|
+
def __init__(
|
|
97
|
+
self,
|
|
98
|
+
lf: pl.LazyFrame,
|
|
99
|
+
groups: list[str],
|
|
100
|
+
levels: dict[str, list[Any]],
|
|
101
|
+
kind: str,
|
|
102
|
+
source: Any = None,
|
|
103
|
+
):
|
|
104
|
+
self._lf = lf
|
|
105
|
+
self._groups = groups
|
|
106
|
+
self._levels = levels
|
|
107
|
+
self.kind = kind
|
|
108
|
+
self.source = source
|
|
109
|
+
self._names: list[str] | None = None
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def columns(self) -> list[str]:
|
|
113
|
+
if self._names is None:
|
|
114
|
+
self._names = list(self._lf.collect_schema().names())
|
|
115
|
+
return self._names
|
|
116
|
+
|
|
117
|
+
@classmethod
|
|
118
|
+
def read(cls, data: Any, by: Any, verb: str) -> "Data":
|
|
119
|
+
if isinstance(data, pl.LazyFrame):
|
|
120
|
+
out = cls(data, [], {}, "lazy")
|
|
121
|
+
elif isinstance(data, pl.DataFrame):
|
|
122
|
+
out = cls(data.lazy(), [], {}, "polars")
|
|
123
|
+
elif _is_pandas_frame(data):
|
|
124
|
+
levels = {
|
|
125
|
+
str(name): list(data[name].cat.categories)
|
|
126
|
+
for name, dtype in data.dtypes.items()
|
|
127
|
+
if str(dtype) == "category"
|
|
128
|
+
}
|
|
129
|
+
try:
|
|
130
|
+
frame = pl.from_pandas(data)
|
|
131
|
+
except ImportError as e:
|
|
132
|
+
raise ImportError(
|
|
133
|
+
f"{verb}(): reading this pandas frame needs pyarrow: "
|
|
134
|
+
"pip install 'stats3[pandas]'"
|
|
135
|
+
) from e
|
|
136
|
+
out = cls(frame.lazy(), [], levels, "pandas")
|
|
137
|
+
elif _is_tidy(data):
|
|
138
|
+
out = cls(data.lazy(), _tidy_groups(data), _tidy_levels(data), "tidy3", data)
|
|
139
|
+
elif isinstance(data, Mapping):
|
|
140
|
+
out = cls(pl.DataFrame(dict(data)).lazy(), [], {}, "polars")
|
|
141
|
+
else:
|
|
142
|
+
raise TypeError(
|
|
143
|
+
f"{verb}(): expected a Polars or pandas DataFrame, a tidy3 frame, "
|
|
144
|
+
f"or a dict of columns; got {type(data).__name__}"
|
|
145
|
+
)
|
|
146
|
+
if by is not None:
|
|
147
|
+
if out._groups:
|
|
148
|
+
raise ValueError(f"{verb}(): by= cannot be used on a grouped frame")
|
|
149
|
+
specs = by if isinstance(by, (list, tuple)) else [by]
|
|
150
|
+
out._groups = [out.column(spec, "by", verb) for spec in specs]
|
|
151
|
+
return out
|
|
152
|
+
|
|
153
|
+
def column(self, spec: Any, role: str, verb: str) -> str:
|
|
154
|
+
name = column_name(spec)
|
|
155
|
+
if name is None:
|
|
156
|
+
raise ValueError(f"{verb}(): {role} must name one column, got {spec!r}")
|
|
157
|
+
if name not in self.columns:
|
|
158
|
+
shown = ", ".join(repr(c) for c in self.columns[:20])
|
|
159
|
+
raise KeyError(f"{verb}(): column {name!r} does not exist. Columns: {shown}")
|
|
160
|
+
return name
|
|
161
|
+
|
|
162
|
+
def levels_of(self, name: str) -> list[Any] | None:
|
|
163
|
+
"""A column's factor levels in order, or None when it is not a factor."""
|
|
164
|
+
if name in self._levels:
|
|
165
|
+
return list(self._levels[name])
|
|
166
|
+
dtype = self._lf.collect_schema()[name]
|
|
167
|
+
return dtype.categories.to_list() if isinstance(dtype, pl.Enum) else None
|
|
168
|
+
|
|
169
|
+
def write(self, result: pl.DataFrame, as_: str | None = None) -> Any:
|
|
170
|
+
"""The result as the kind of frame that came in (or ``as_``)."""
|
|
171
|
+
kind = as_ or self.kind
|
|
172
|
+
if kind == "lazy":
|
|
173
|
+
return result.lazy()
|
|
174
|
+
if kind == "polars":
|
|
175
|
+
return result
|
|
176
|
+
if kind == "pandas":
|
|
177
|
+
return _to_pandas(result, {g: self._levels[g] for g in self._groups if g in self._levels})
|
|
178
|
+
if kind == "tidy3":
|
|
179
|
+
return _to_tidy(result, self)
|
|
180
|
+
raise ValueError(f"as_ must be one of {OUTPUTS}, got {as_!r}")
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _tidy_groups(frame: Any) -> list[str]:
|
|
184
|
+
tidy3 = sys.modules["tidy3"]
|
|
185
|
+
groups = tidy3.group_vars(frame) if hasattr(tidy3, "group_vars") else frame._groups
|
|
186
|
+
return [str(g) for g in (groups or [])]
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _tidy_levels(frame: Any) -> dict[str, list[Any]]:
|
|
190
|
+
accessor = getattr(frame, "factor_levels", None)
|
|
191
|
+
levels = accessor() if callable(accessor) else getattr(frame, "_category_levels", {})
|
|
192
|
+
return {str(name): list(values) for name, values in (levels or {}).items()}
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _to_pandas(result: pl.DataFrame, levels: dict[str, list[Any]]) -> Any:
|
|
196
|
+
try:
|
|
197
|
+
frame = result.to_pandas()
|
|
198
|
+
except ImportError as e:
|
|
199
|
+
raise ImportError(
|
|
200
|
+
"returning a pandas frame needs pandas and pyarrow: pip install 'stats3[pandas]'"
|
|
201
|
+
) from e
|
|
202
|
+
for name, values in levels.items():
|
|
203
|
+
frame[name] = frame[name].astype("category").cat.set_categories(values)
|
|
204
|
+
return frame
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _to_tidy(result: pl.DataFrame, data: Data) -> Any:
|
|
208
|
+
"""A tidy3 frame that keeps the grouping columns' factor levels."""
|
|
209
|
+
if "tidy3" not in sys.modules:
|
|
210
|
+
raise ImportError("as_='tidy3' needs tidy3: pip install tidy3")
|
|
211
|
+
levels = {g: data._levels[g] for g in data._groups if g in data._levels}
|
|
212
|
+
if data.source is not None:
|
|
213
|
+
try:
|
|
214
|
+
return type(data.source)(result.lazy(), category_levels=levels)
|
|
215
|
+
except TypeError:
|
|
216
|
+
pass
|
|
217
|
+
return sys.modules["tidy3"].tidy(result)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class Step:
|
|
221
|
+
"""A test waiting for data: ``data >> step``, or ``step(data)``.
|
|
222
|
+
|
|
223
|
+
pandas and Polars frames have no ``>>`` of their own, and tidy3 frames
|
|
224
|
+
hand ``>>`` to the right-hand side, so ``__rrshift__`` receives them all.
|
|
225
|
+
"""
|
|
226
|
+
|
|
227
|
+
__slots__ = ("_fn", "name", "by", "as_")
|
|
228
|
+
|
|
229
|
+
def __init__(self, fn: Callable[[Data], pl.DataFrame], name: str, by: Any = None, as_: str | None = None):
|
|
230
|
+
self._fn = fn
|
|
231
|
+
self.name = name
|
|
232
|
+
self.by = by
|
|
233
|
+
self.as_ = as_
|
|
234
|
+
|
|
235
|
+
def __rrshift__(self, data: Any) -> Any:
|
|
236
|
+
frame = Data.read(data, self.by, self.name)
|
|
237
|
+
return frame.write(self._fn(frame), self.as_)
|
|
238
|
+
|
|
239
|
+
def __call__(self, data: Any) -> Any:
|
|
240
|
+
return self.__rrshift__(data)
|
|
241
|
+
|
|
242
|
+
def __repr__(self) -> str:
|
|
243
|
+
return f"<stats3 {self.name}: pipe data into it with >>>"
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def step(fn: Callable[..., Step]) -> Callable[..., Any]:
|
|
247
|
+
"""Make a test usable as a pipe step or with the data first.
|
|
248
|
+
|
|
249
|
+
``t_test("y ~ g", by="g2")`` returns a ``Step``;
|
|
250
|
+
``t_test(frame, "y ~ g", by="g2")`` runs it on ``frame`` at once.
|
|
251
|
+
Every test gets ``by=`` (dplyr's ``.by``) and ``as_=`` (the output frame
|
|
252
|
+
type, by default the input's).
|
|
253
|
+
"""
|
|
254
|
+
import functools
|
|
255
|
+
|
|
256
|
+
@functools.wraps(fn)
|
|
257
|
+
def wrapper(*args: Any, by: Any = None, as_: str | None = None, **kwargs: Any) -> Any:
|
|
258
|
+
if as_ is not None and as_ not in OUTPUTS:
|
|
259
|
+
raise ValueError(f"as_ must be one of {OUTPUTS}, got {as_!r}")
|
|
260
|
+
if args and is_frame(args[0]) and not isinstance(args[0], str):
|
|
261
|
+
data, rest = args[0], args[1:]
|
|
262
|
+
made = fn(*rest, **kwargs)
|
|
263
|
+
made.by, made.as_ = by, as_
|
|
264
|
+
return made(data)
|
|
265
|
+
made = fn(*args, **kwargs)
|
|
266
|
+
made.by, made.as_ = by, as_
|
|
267
|
+
return made
|
|
268
|
+
|
|
269
|
+
return wrapper
|