scistackstats 0.1.41__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.
- scistackstats-0.1.41/.gitignore +37 -0
- scistackstats-0.1.41/PKG-INFO +35 -0
- scistackstats-0.1.41/README.md +6 -0
- scistackstats-0.1.41/pyproject.toml +72 -0
- scistackstats-0.1.41/src/scistackstats/__init__.py +59 -0
- scistackstats-0.1.41/src/scistackstats/catalog.py +186 -0
- scistackstats-0.1.41/src/scistackstats/codegen.py +148 -0
- scistackstats-0.1.41/src/scistackstats/describe.py +212 -0
- scistackstats-0.1.41/src/scistackstats/design.py +181 -0
- scistackstats-0.1.41/src/scistackstats/effects.py +112 -0
- scistackstats-0.1.41/src/scistackstats/engine.py +255 -0
- scistackstats-0.1.41/src/scistackstats/export.py +221 -0
- scistackstats-0.1.41/src/scistackstats/format.py +653 -0
- scistackstats-0.1.41/src/scistackstats/overlays.py +118 -0
- scistackstats-0.1.41/src/scistackstats/restore.py +169 -0
- scistackstats-0.1.41/src/scistackstats/run.py +422 -0
- scistackstats-0.1.41/src/scistackstats/seed.py +85 -0
- scistackstats-0.1.41/src/scistackstats/spec.py +316 -0
- scistackstats-0.1.41/src/scistackstats/tables.py +267 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
.venv
|
|
2
|
+
.DS_Store
|
|
3
|
+
*/*/__pycache__
|
|
4
|
+
others_projects/sciforge
|
|
5
|
+
scistack-gui/extension/node_modules/
|
|
6
|
+
scistack-gui/frontend/node_modules/__pycache__/
|
|
7
|
+
*.pyc
|
|
8
|
+
__pycache__/
|
|
9
|
+
*.pyc
|
|
10
|
+
*.pyo
|
|
11
|
+
*.egg-info/
|
|
12
|
+
# Python package build output (rebuilt fresh by CI on every publish; a
|
|
13
|
+
# committed dist/ gets deleted by `rm -rf dist` mid-build and dirties the
|
|
14
|
+
# tree for hatch-vcs). Excludes scistack-gui/extension/dist/, which is an
|
|
15
|
+
# intentionally committed compiled bundle, not a Python build artifact.
|
|
16
|
+
/dist/
|
|
17
|
+
/*/dist/
|
|
18
|
+
# Generated database artifacts (DuckDB data/lineage + write-ahead logs)
|
|
19
|
+
*.duckdb
|
|
20
|
+
*.duckdb.wal
|
|
21
|
+
*.wal
|
|
22
|
+
scistack-gui/frontend/node_modules/
|
|
23
|
+
# Compiled output of the frontend's React-free unit tests (npm test in
|
|
24
|
+
# scistack-gui/frontend). Regenerated by `tsc -p tsconfig.test.json`; unlike
|
|
25
|
+
# extension/dist/, nothing loads it at runtime.
|
|
26
|
+
scistack-gui/frontend/dist/
|
|
27
|
+
# mkdocs build output (regenerated by `mkdocs build`)
|
|
28
|
+
/site/
|
|
29
|
+
# Runtime output of code_export_service (pipeline-to-code export) — timestamped
|
|
30
|
+
# per-run files, not source.
|
|
31
|
+
/exports/
|
|
32
|
+
*.log
|
|
33
|
+
output.txt
|
|
34
|
+
scimatlab/tests/matlab/test-failures.txt
|
|
35
|
+
|
|
36
|
+
# csv-stats is its own repo (github.com/mtillman14/csv-stats), cloned here for editable dev
|
|
37
|
+
/csv-stats/
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: scistackstats
|
|
3
|
+
Version: 0.1.41
|
|
4
|
+
Summary: Statistics for scistack: StatStudio designs, descriptives and tests from Plot Studio roles (math by csv-stats)
|
|
5
|
+
Author: SciStack Contributors
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: hypothesis-testing,provenance,scidb,scistack,statistics
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Science/Research
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Requires-Dist: csv-stats[full]>=0.2
|
|
21
|
+
Requires-Dist: numpy>=1.21
|
|
22
|
+
Requires-Dist: pandas>=1.5
|
|
23
|
+
Requires-Dist: scistacklog==0.1.41
|
|
24
|
+
Requires-Dist: scistackplot==0.1.41
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# scistackstats
|
|
31
|
+
|
|
32
|
+
Statistics from the same design a figure draws: StatStudio's engine,
|
|
33
|
+
usable from code. Every number is computed by [csv-stats](https://github.com/mtillman14/csv-stats).
|
|
34
|
+
|
|
35
|
+
See `docs/claude/statistics-design.md` for the design and the module docstrings for the API.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# scistackstats
|
|
2
|
+
|
|
3
|
+
Statistics from the same design a figure draws: StatStudio's engine,
|
|
4
|
+
usable from code. Every number is computed by [csv-stats](https://github.com/mtillman14/csv-stats).
|
|
5
|
+
|
|
6
|
+
See `docs/claude/statistics-design.md` for the design and the module docstrings for the API.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling", "hatch-vcs", "hatch-dependency-coversion==0.0.1a4"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[tool.hatch.version]
|
|
6
|
+
source = "vcs"
|
|
7
|
+
raw-options = { search_parent_directories = true, local_scheme = "no-local-version" }
|
|
8
|
+
|
|
9
|
+
# Sibling SciStack packages are pinned to EXACTLY this build's version at build
|
|
10
|
+
# time (lockstep releases), so [project] dependencies lists them unversioned. The
|
|
11
|
+
# extra "dynamic" entry is what makes hatchling run the hook. Every sibling in
|
|
12
|
+
# dependencies must be named here: CI's build job fails a wheel whose internal
|
|
13
|
+
# dependency is not "==<its own version>".
|
|
14
|
+
[tool.hatch.metadata.hooks.dependency-coversion]
|
|
15
|
+
override-versions-of = ["scistackplot", "scistacklog"]
|
|
16
|
+
|
|
17
|
+
[project]
|
|
18
|
+
name = "scistackstats"
|
|
19
|
+
dynamic = ["version", "dependency-coversioning"]
|
|
20
|
+
description = "Statistics for scistack: StatStudio designs, descriptives and tests from Plot Studio roles (math by csv-stats)"
|
|
21
|
+
readme = "README.md"
|
|
22
|
+
license = "MIT"
|
|
23
|
+
requires-python = ">=3.10"
|
|
24
|
+
authors = [
|
|
25
|
+
{ name = "SciStack Contributors" }
|
|
26
|
+
]
|
|
27
|
+
keywords = [
|
|
28
|
+
"statistics",
|
|
29
|
+
"hypothesis-testing",
|
|
30
|
+
"scidb",
|
|
31
|
+
"scistack",
|
|
32
|
+
"provenance",
|
|
33
|
+
]
|
|
34
|
+
classifiers = [
|
|
35
|
+
"Development Status :: 4 - Beta",
|
|
36
|
+
"Intended Audience :: Science/Research",
|
|
37
|
+
"License :: OSI Approved :: MIT License",
|
|
38
|
+
"Operating System :: OS Independent",
|
|
39
|
+
"Programming Language :: Python :: 3",
|
|
40
|
+
"Programming Language :: Python :: 3.10",
|
|
41
|
+
"Programming Language :: Python :: 3.11",
|
|
42
|
+
"Programming Language :: Python :: 3.12",
|
|
43
|
+
"Programming Language :: Python :: 3.13",
|
|
44
|
+
"Topic :: Scientific/Engineering :: Information Analysis",
|
|
45
|
+
"Typing :: Typed",
|
|
46
|
+
]
|
|
47
|
+
dependencies = [
|
|
48
|
+
"pandas>=1.5",
|
|
49
|
+
"numpy>=1.21",
|
|
50
|
+
# Every statistic is computed by csv-stats (S6); [full] has the tests.
|
|
51
|
+
"csv-stats[full]>=0.2",
|
|
52
|
+
"scistackplot",
|
|
53
|
+
"scistacklog",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
[project.optional-dependencies]
|
|
57
|
+
dev = [
|
|
58
|
+
"pytest>=7.0",
|
|
59
|
+
"pytest-cov>=4.0",
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
[tool.hatch.build.targets.sdist]
|
|
63
|
+
include = [
|
|
64
|
+
"/src",
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
[tool.hatch.build.targets.wheel]
|
|
68
|
+
packages = ["src/scistackstats"]
|
|
69
|
+
|
|
70
|
+
[tool.pytest.ini_options]
|
|
71
|
+
testpaths = ["tests"]
|
|
72
|
+
pythonpath = ["src"]
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""
|
|
2
|
+
scistackstats — statistics from the same design a figure draws.
|
|
3
|
+
|
|
4
|
+
StatStudio's engine, usable from code (the GUI is a thin wrapper). The design
|
|
5
|
+
is a plot-layer ``PlotSpec`` (grouping layers = factors, separate figures and
|
|
6
|
+
panels = strata, collapse chain = unit of analysis), so a table and a figure
|
|
7
|
+
built from the same design summarise the same rows. Every number is computed
|
|
8
|
+
by csv-stats (``csvstats``), through :mod:`scistackstats.engine`.
|
|
9
|
+
|
|
10
|
+
::
|
|
11
|
+
|
|
12
|
+
from scistackplot import PlotSpec, Role, PlotKind
|
|
13
|
+
from scistackstats import seed_from_plot, load_tables, describe_table, run
|
|
14
|
+
|
|
15
|
+
plot = PlotSpec(measures=["Ratings"], roles={"brand": Role.GROUP, "subject": Role.COLLAPSE},
|
|
16
|
+
groups=["brand"], kind=PlotKind.BAR)
|
|
17
|
+
spec = seed_from_plot(plot).spec
|
|
18
|
+
tables = load_tables(source, spec) # any scistackplot DataSource
|
|
19
|
+
descriptives = describe_table(spec, tables) # mean (SD) per brand, across subjects
|
|
20
|
+
report = run(spec, tables) # paired comparisons of the brands, Holm-adjusted
|
|
21
|
+
print(export_comparisons(report, spec, "md", tables=tables)) # txt md csv latex html json
|
|
22
|
+
|
|
23
|
+
Design: ``docs/claude/statistics-design.md``; plan ``.claude/plan-statistics.md``.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
from .codegen import NotExportable, generate_stats_function, why_not_exportable
|
|
29
|
+
from .catalog import TESTS, available_tests, select_tests, why_unavailable
|
|
30
|
+
from .describe import CELL_LAYOUTS, STATISTICS, DescriptivesTable, describe_table
|
|
31
|
+
from .design import Design, Factor, Tiers, comparison_tiers, derive_design
|
|
32
|
+
from .effects import EFFECT_SIZES, DifferenceSpec, default_effect_size, difference_spec
|
|
33
|
+
from .engine import CsvStatsEngine, EngineRefusal, FakeEngine, get_engine, use_engine
|
|
34
|
+
from .export import FORMATS, export_comparisons, export_descriptives, render
|
|
35
|
+
from .format import Grid, Labels, apa_sentence, comparisons_grid, descriptives_grids, labels_for, omnibus_grid, sweep_grid
|
|
36
|
+
from .run import Comparison, StatsReport, run
|
|
37
|
+
from .seed import SeedError, Seeded, seed_from_plot
|
|
38
|
+
from .spec import DV, Analysis, DescriptiveSpec, FormatSpec, StatsSpec, StatsSpecError
|
|
39
|
+
from .tables import ANALYSIS_KIND, UNIT, AnalysisFrame, AnalysisFrameError, analysis_frame, load_tables
|
|
40
|
+
|
|
41
|
+
__all__ = [
|
|
42
|
+
"ANALYSIS_KIND", "Analysis", "AnalysisFrame", "AnalysisFrameError", "CELL_LAYOUTS", "Comparison",
|
|
43
|
+
"CsvStatsEngine", "DV", "DescriptiveSpec", "DescriptivesTable", "Design", "DifferenceSpec",
|
|
44
|
+
"EFFECT_SIZES", "EngineRefusal", "Factor", "FakeEngine", "STATISTICS", "SeedError", "Seeded",
|
|
45
|
+
"StatsReport", "StatsSpec", "StatsSpecError", "TESTS", "Tiers", "UNIT", "analysis_frame",
|
|
46
|
+
"available_tests", "comparison_tiers", "default_effect_size", "derive_design", "describe_table",
|
|
47
|
+
"difference_spec", "get_engine", "load_tables", "run", "seed_from_plot", "select_tests",
|
|
48
|
+
"use_engine", "why_unavailable",
|
|
49
|
+
"FORMATS", "FormatSpec", "Grid", "Labels", "apa_sentence", "comparisons_grid", "descriptives_grids",
|
|
50
|
+
"export_comparisons", "export_descriptives", "labels_for", "omnibus_grid", "render", "sweep_grid",
|
|
51
|
+
"NotExportable", "generate_stats_function", "why_not_exportable",
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
from importlib import metadata as _metadata
|
|
56
|
+
|
|
57
|
+
__version__ = _metadata.version("scistackstats")
|
|
58
|
+
except Exception: # pragma: no cover - running from a source tree
|
|
59
|
+
__version__ = "0.0.0"
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""Which test runs where, and why not — the one refusal owner.
|
|
2
|
+
|
|
3
|
+
:func:`why_unavailable` answers "can test T run on this design?" with ``None``
|
|
4
|
+
or a reason. The GUI greys a menu entry with that reason, and the runner
|
|
5
|
+
records it as a refusal. :func:`select_tests` picks the tests for an analysis
|
|
6
|
+
from the design, and always says why (S12: never switch silently).
|
|
7
|
+
|
|
8
|
+
This module knows the *statistical* requirements: a paired test needs a
|
|
9
|
+
within factor and a unit, a two-sample test needs two levels, and so on.
|
|
10
|
+
What csv-stats does not support *yet* is csv-stats' to say: those come back
|
|
11
|
+
from the engine at run time as ``not yet in csv-stats: …`` refusals. Copying
|
|
12
|
+
csv-stats' support matrix here would give it a second owner.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from .design import BETWEEN, MIXED, WITHIN, Design
|
|
21
|
+
from .spec import Analysis
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class StatTest:
|
|
26
|
+
id: str
|
|
27
|
+
label: str
|
|
28
|
+
#: "pairwise" (two levels of one factor) or "omnibus" (every factor).
|
|
29
|
+
role: str
|
|
30
|
+
family: str
|
|
31
|
+
#: For pairwise tests: the factor kind it needs.
|
|
32
|
+
kind: str | None = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
TESTS: dict[str, StatTest] = {
|
|
36
|
+
t.id: t
|
|
37
|
+
for t in (
|
|
38
|
+
StatTest("ttest_ind_welch", "Welch's t-test", "pairwise", "parametric", BETWEEN),
|
|
39
|
+
StatTest("ttest_ind_student", "Student's t-test", "pairwise", "parametric", BETWEEN),
|
|
40
|
+
StatTest("ttest_paired", "Paired t-test", "pairwise", "parametric", WITHIN),
|
|
41
|
+
StatTest("mann_whitney", "Mann–Whitney U", "pairwise", "nonparametric", BETWEEN),
|
|
42
|
+
StatTest("wilcoxon", "Wilcoxon signed-rank", "pairwise", "nonparametric", WITHIN),
|
|
43
|
+
StatTest("spm_ttest2", "SPM two-sample t (1-D curves)", "pairwise", "spm", BETWEEN),
|
|
44
|
+
StatTest("spm_ttest_paired", "SPM paired t (1-D curves)", "pairwise", "spm", WITHIN),
|
|
45
|
+
StatTest("anova", "ANOVA", "omnibus", "parametric"),
|
|
46
|
+
StatTest("kruskal", "Kruskal–Wallis", "omnibus", "nonparametric"),
|
|
47
|
+
StatTest("friedman", "Friedman", "omnibus", "nonparametric"),
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _design_problem(design: Design) -> str | None:
|
|
53
|
+
if not design.factors:
|
|
54
|
+
return "nothing is grouped: add a grouping layer to compare its levels"
|
|
55
|
+
if design.mixed:
|
|
56
|
+
names = ", ".join(design.mixed)
|
|
57
|
+
return f"{names}: some units at one level, some at several; set within or between first"
|
|
58
|
+
if design.within and design.unit is None:
|
|
59
|
+
return "a within-subject factor needs an experimental unit (collapse to one, without weight by N)"
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def why_unavailable(test_id: str, design: Design) -> str | None:
|
|
64
|
+
"""``None`` if ``test_id`` can run on ``design``, else the reason."""
|
|
65
|
+
info = TESTS.get(test_id)
|
|
66
|
+
if info is None:
|
|
67
|
+
return f"unknown test {test_id!r}"
|
|
68
|
+
problem = _design_problem(design)
|
|
69
|
+
if problem:
|
|
70
|
+
return problem
|
|
71
|
+
if info.role == "pairwise":
|
|
72
|
+
primary = design.factors[0]
|
|
73
|
+
if len(primary.levels) < 2:
|
|
74
|
+
return f"{primary.name} has fewer than 2 levels here"
|
|
75
|
+
if primary.kind != info.kind:
|
|
76
|
+
return f"{info.label} needs a {info.kind}-subject factor; {primary.name} is {primary.kind}"
|
|
77
|
+
return None
|
|
78
|
+
# Omnibus
|
|
79
|
+
if test_id == "kruskal":
|
|
80
|
+
if len(design.factors) != 1 or design.factors[0].kind != BETWEEN:
|
|
81
|
+
return "Kruskal–Wallis takes one between-subject factor"
|
|
82
|
+
if len(design.factors[0].levels) < 2:
|
|
83
|
+
return f"{design.factors[0].name} has fewer than 2 levels here"
|
|
84
|
+
if test_id == "friedman":
|
|
85
|
+
if len(design.factors) != 1 or design.factors[0].kind != WITHIN:
|
|
86
|
+
return "Friedman takes one within-subject factor"
|
|
87
|
+
if len(design.factors[0].levels) < 3:
|
|
88
|
+
return "Friedman needs 3+ levels; with 2, the Wilcoxon test is the test"
|
|
89
|
+
if test_id == "anova":
|
|
90
|
+
thin = [f.name for f in design.factors if len(f.levels) < 2]
|
|
91
|
+
if thin:
|
|
92
|
+
return f"{', '.join(thin)} has fewer than 2 levels here"
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclass(frozen=True)
|
|
97
|
+
class Selection:
|
|
98
|
+
omnibus: str | None
|
|
99
|
+
omnibus_reason: str | None
|
|
100
|
+
pairwise: str | None
|
|
101
|
+
pairwise_reason: str | None
|
|
102
|
+
#: Why these tests (shown in the GUI and the report).
|
|
103
|
+
why: str
|
|
104
|
+
|
|
105
|
+
def to_dict(self) -> dict[str, Any]:
|
|
106
|
+
return {"omnibus": self.omnibus, "omnibus_reason": self.omnibus_reason,
|
|
107
|
+
"pairwise": self.pairwise, "pairwise_reason": self.pairwise_reason, "why": self.why}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _pairwise_default(analysis: Analysis, design: Design) -> tuple[str | None, str]:
|
|
111
|
+
primary = design.factors[0] if design.factors else None
|
|
112
|
+
if primary is None:
|
|
113
|
+
return None, "no grouping layer"
|
|
114
|
+
within = primary.kind == WITHIN
|
|
115
|
+
if analysis.family == "spm":
|
|
116
|
+
test = "spm_ttest_paired" if within else "spm_ttest2"
|
|
117
|
+
elif analysis.family == "nonparametric":
|
|
118
|
+
test = "wilcoxon" if within else "mann_whitney"
|
|
119
|
+
elif within:
|
|
120
|
+
test = "ttest_paired"
|
|
121
|
+
else:
|
|
122
|
+
test = "ttest_ind_student" if analysis.equal_var else "ttest_ind_welch"
|
|
123
|
+
because = f"{primary.name} is {primary.kind}-subject" + (" (overridden)" if primary.overridden else "")
|
|
124
|
+
if test.startswith("ttest_ind"):
|
|
125
|
+
because += ", equal variances assumed" if analysis.equal_var else ", equal variances not assumed"
|
|
126
|
+
return test, because
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _omnibus_default(analysis: Analysis, design: Design) -> tuple[str | None, str]:
|
|
130
|
+
if not design.factors:
|
|
131
|
+
return None, "no grouping layer"
|
|
132
|
+
if analysis.family == "spm":
|
|
133
|
+
return None, "SPM on curves runs the pairwise tests; an SPM ANOVA is not yet in csv-stats"
|
|
134
|
+
if analysis.family == "parametric":
|
|
135
|
+
return "anova", "parametric family"
|
|
136
|
+
if len(design.factors) == 1:
|
|
137
|
+
f = design.factors[0]
|
|
138
|
+
if f.kind == WITHIN:
|
|
139
|
+
return "friedman", f"{f.name} is within-subject"
|
|
140
|
+
return "kruskal", f"{f.name} is between-subject"
|
|
141
|
+
return None, "no rank-based test for a factorial design; the pairwise rank tests still run"
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def select_tests(analysis: Analysis, design: Design) -> Selection:
|
|
145
|
+
"""The omnibus and pairwise tests for ``analysis`` on ``design``.
|
|
146
|
+
|
|
147
|
+
An override (``omnibus_test`` / ``pairwise_test``) wins, and is still
|
|
148
|
+
checked by :func:`why_unavailable`.
|
|
149
|
+
"""
|
|
150
|
+
omnibus = omnibus_reason = pairwise = pairwise_reason = None
|
|
151
|
+
whys: list[str] = []
|
|
152
|
+
if analysis.omnibus:
|
|
153
|
+
if analysis.omnibus_test:
|
|
154
|
+
omnibus, because = analysis.omnibus_test, "chosen"
|
|
155
|
+
if TESTS.get(omnibus) and TESTS[omnibus].role != "omnibus":
|
|
156
|
+
omnibus_reason = f"{TESTS[omnibus].label} is not an omnibus test"
|
|
157
|
+
else:
|
|
158
|
+
omnibus, because = _omnibus_default(analysis, design)
|
|
159
|
+
if omnibus is None:
|
|
160
|
+
omnibus_reason = because
|
|
161
|
+
elif omnibus_reason is None:
|
|
162
|
+
omnibus_reason = why_unavailable(omnibus, design)
|
|
163
|
+
if omnibus:
|
|
164
|
+
whys.append(f"omnibus: {TESTS[omnibus].label if omnibus in TESTS else omnibus} ({because})")
|
|
165
|
+
if analysis.pairwise:
|
|
166
|
+
if analysis.pairwise_test:
|
|
167
|
+
pairwise, because = analysis.pairwise_test, "chosen"
|
|
168
|
+
if TESTS.get(pairwise) and TESTS[pairwise].role != "pairwise":
|
|
169
|
+
pairwise_reason = f"{TESTS[pairwise].label} is not a pairwise test"
|
|
170
|
+
else:
|
|
171
|
+
pairwise, because = _pairwise_default(analysis, design)
|
|
172
|
+
if pairwise is None:
|
|
173
|
+
pairwise_reason = because
|
|
174
|
+
elif pairwise_reason is None:
|
|
175
|
+
pairwise_reason = why_unavailable(pairwise, design)
|
|
176
|
+
if pairwise:
|
|
177
|
+
whys.append(f"pairwise: {TESTS[pairwise].label if pairwise in TESTS else pairwise} ({because})")
|
|
178
|
+
return Selection(omnibus, omnibus_reason, pairwise, pairwise_reason, "; ".join(whys) or "nothing selected")
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def available_tests(design: Design) -> list[dict[str, Any]]:
|
|
182
|
+
"""Every test with its availability, for the GUI's "+ Add test" menu."""
|
|
183
|
+
return [
|
|
184
|
+
{"id": t.id, "label": t.label, "role": t.role, "family": t.family, "reason": why_unavailable(t.id, design)}
|
|
185
|
+
for t in TESTS.values()
|
|
186
|
+
]
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Code export: a ``stat_`` function that reproduces a StatStudio table (Stage 11).
|
|
2
|
+
|
|
3
|
+
The generated function **calls scistackstats** with the spec embedded as a
|
|
4
|
+
literal. It does not restate the statistics: design derivation, test
|
|
5
|
+
selection, correction families and every csv-stats call stay in their one
|
|
6
|
+
owner, so the exported code cannot drift from what StatStudio showed. It
|
|
7
|
+
receives the long-format table ``for_each`` hands a ``stat_`` function
|
|
8
|
+
(schema keys as columns, ``as_table`` on), rebuilds the ``LongTable`` with
|
|
9
|
+
the level order and schema the preview used, and returns the report dict
|
|
10
|
+
(stored by scidb's ``stat_`` machinery: lineage, ``finalized``, ``scidb
|
|
11
|
+
report``).
|
|
12
|
+
|
|
13
|
+
First pass: a DV must be a single-column Variable whose factors are its
|
|
14
|
+
schema keys. :func:`why_not_exportable` refuses, with the reason, for chosen
|
|
15
|
+
``ColName`` columns, joined grouping variables, variant rows or variant
|
|
16
|
+
axes, and an x variable. Rebuilding those tables from ``for_each`` inputs
|
|
17
|
+
would be a second copy of the source's join and melt logic.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import keyword
|
|
23
|
+
import pprint
|
|
24
|
+
import re
|
|
25
|
+
from dataclasses import dataclass, field
|
|
26
|
+
from typing import Any
|
|
27
|
+
|
|
28
|
+
from scistacklog import Log
|
|
29
|
+
|
|
30
|
+
from scistackplot import LongTable
|
|
31
|
+
|
|
32
|
+
from .spec import StatsSpec
|
|
33
|
+
|
|
34
|
+
LAYER = "scistackstats"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class NotExportable(ValueError):
|
|
38
|
+
"""This stats spec cannot be exported as code yet; the message says why."""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _ident(text: str) -> str:
|
|
42
|
+
name = re.sub(r"\W+", "_", str(text)).strip("_").lower() or "stats"
|
|
43
|
+
if name[0].isdigit() or keyword.iskeyword(name):
|
|
44
|
+
name = f"v_{name}"
|
|
45
|
+
return name
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def default_function_name(stats_spec: StatsSpec) -> str:
|
|
49
|
+
return f"stat_{_ident(stats_spec.targets[0].variable)}"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def input_params(stats_spec: StatsSpec) -> dict[str, str]:
|
|
53
|
+
"""``{parameter: variable}``: ``df`` for one variable, ``df_<name>`` for several."""
|
|
54
|
+
variables = list(dict.fromkeys(dv.variable for dv in stats_spec.targets))
|
|
55
|
+
if len(variables) == 1:
|
|
56
|
+
return {"df": variables[0]}
|
|
57
|
+
return {f"df_{_ident(v)}": v for v in variables}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def why_not_exportable(stats_spec: StatsSpec, tables: dict[str, LongTable]) -> str | None:
|
|
61
|
+
design = stats_spec.design
|
|
62
|
+
if any(dv.columns is not None for dv in stats_spec.targets):
|
|
63
|
+
return "chosen columns of a multi-column variable are not exported as code yet"
|
|
64
|
+
if design.factor_variables:
|
|
65
|
+
return "joined grouping variables are not exported as code yet"
|
|
66
|
+
if design.x_measure:
|
|
67
|
+
return "an x variable is not exported as code yet"
|
|
68
|
+
if design.variant_sets:
|
|
69
|
+
return "variant rows are not exported as code yet"
|
|
70
|
+
for dv in stats_spec.targets:
|
|
71
|
+
table = tables.get(dv.variable)
|
|
72
|
+
if table is None:
|
|
73
|
+
return f"no table loaded for {dv.variable}"
|
|
74
|
+
if table.variant_factors:
|
|
75
|
+
names = [f.name for f in table.variant_factors]
|
|
76
|
+
return f"{dv.variable} has variant axes ({', '.join(names)}); they are not exported as code yet"
|
|
77
|
+
if table.field_factors:
|
|
78
|
+
return f"{dv.variable} is a multi-column variable; it is not exported as code yet"
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass
|
|
83
|
+
class StatsFunction:
|
|
84
|
+
name: str
|
|
85
|
+
source: str
|
|
86
|
+
#: ``{parameter: variable}`` the function takes.
|
|
87
|
+
inputs: dict[str, str] = field(default_factory=dict)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _table_literal(variable: str, param: str, table: LongTable) -> list[str]:
|
|
91
|
+
factors = list(table.factor_names)
|
|
92
|
+
levels = {f: [str(v) for v in table.factor(f).levels] for f in factors}
|
|
93
|
+
return [
|
|
94
|
+
f" {variable!r}: LongTable.from_frame(",
|
|
95
|
+
f" {param},",
|
|
96
|
+
f" factors=[c for c in {factors!r} if c in {param}.columns],",
|
|
97
|
+
f" measures=[{variable!r}],",
|
|
98
|
+
f" name={variable!r},",
|
|
99
|
+
f" schema_levels={list(table.schema_levels)!r},",
|
|
100
|
+
f" level_order={levels!r},",
|
|
101
|
+
" ),",
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def generate_stats_function(stats_spec: StatsSpec, tables: dict[str, LongTable], *,
|
|
106
|
+
function_name: str | None = None) -> StatsFunction:
|
|
107
|
+
"""The ``stat_`` function source (module docstring). Raises
|
|
108
|
+
:class:`NotExportable` with the reason when the spec is not exportable."""
|
|
109
|
+
reason = why_not_exportable(stats_spec, tables)
|
|
110
|
+
if reason:
|
|
111
|
+
Log.info("[stats codegen] not exportable: %s", reason, layer=LAYER)
|
|
112
|
+
raise NotExportable(reason)
|
|
113
|
+
name = function_name or default_function_name(stats_spec)
|
|
114
|
+
if not name.isidentifier() or keyword.iskeyword(name):
|
|
115
|
+
raise NotExportable(f"{name!r} is not a valid function name")
|
|
116
|
+
params = input_params(stats_spec)
|
|
117
|
+
spec_literal = pprint.pformat(stats_spec.to_dict(), indent=1, width=96, sort_dicts=True)
|
|
118
|
+
dvs = ", ".join(dv.key for dv in stats_spec.targets)
|
|
119
|
+
analyses = ", ".join(f"{a.id} ({a.family})" for a in stats_spec.comparisons) or "descriptives only"
|
|
120
|
+
lines = [
|
|
121
|
+
f"def {name}({', '.join(params)}):",
|
|
122
|
+
f' """StatStudio statistics for {dvs}: {analyses}.',
|
|
123
|
+
"",
|
|
124
|
+
" Generated by scistackstats. The statistics are scistackstats' and",
|
|
125
|
+
" csv-stats' own (this function calls them with the saved settings), so",
|
|
126
|
+
" the result is what StatStudio showed for the same rows.",
|
|
127
|
+
' """',
|
|
128
|
+
" from scistackplot import LongTable",
|
|
129
|
+
" from scistackstats import StatsSpec, describe_table, run",
|
|
130
|
+
"",
|
|
131
|
+
" spec = StatsSpec.from_dict(",
|
|
132
|
+
*(" " + line for line in spec_literal.splitlines()),
|
|
133
|
+
" )",
|
|
134
|
+
" tables = {",
|
|
135
|
+
]
|
|
136
|
+
for param, variable in params.items():
|
|
137
|
+
lines += [" " + line for line in _table_literal(variable, param, tables[variable])]
|
|
138
|
+
lines += [
|
|
139
|
+
" }",
|
|
140
|
+
" report = run(spec, tables).to_dict()",
|
|
141
|
+
' report["descriptives"] = describe_table(spec, tables).to_dict()',
|
|
142
|
+
" return report",
|
|
143
|
+
"",
|
|
144
|
+
]
|
|
145
|
+
source = "\n".join(lines)
|
|
146
|
+
Log.info("[stats codegen] %s: %d input(s), %d analysis(es), %d line(s)", name, len(params),
|
|
147
|
+
len(stats_spec.comparisons), len(lines), layer=LAYER)
|
|
148
|
+
return StatsFunction(name=name, source=source, inputs=params)
|