pysofra 0.1.0a3__tar.gz → 0.1.0a4__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.
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/.gitignore +4 -1
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/CHANGELOG.md +13 -2
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/PKG-INFO +7 -7
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/README.md +5 -5
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/pyproject.toml +15 -3
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/__init__.py +1 -1
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/core/table.py +1 -1
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/summary/extras.py +18 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/summary/tbl_one.py +12 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/themes/registry.py +2 -2
- pysofra-0.1.0a3/tests/test_joss_api_stability.py → pysofra-0.1.0a4/tests/test_api_stability.py +5 -5
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_plot_determinism.py +1 -1
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_validation_fixes.py +5 -5
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_wishlist.py +2 -2
- pysofra-0.1.0a3/src/pysofra/io/__init__.py +0 -1
- pysofra-0.1.0a3/src/pysofra/notebook/__init__.py +0 -6
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/LICENSE +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/NOTICE +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/core/__init__.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/core/compose.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/core/format.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/core/frames.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/core/schema.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/models/__init__.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/models/extract.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/models/pool.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/models/regression.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/models/survival.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/models/uvregression.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/plot/__init__.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/plot/_backend.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/plot/forest.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/plot/inline.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/plot/km.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/render/__init__.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/render/_zip_determinism.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/render/base.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/render/docx.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/render/html.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/render/image.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/render/latex.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/render/markdown.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/render/pptx.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/render/xlsx.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/summary/__init__.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/summary/calibrate.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/summary/design.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/summary/effect_size.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/summary/smd.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/summary/stats.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/summary/tbl_cross.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/summary/tbl_summary.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/summary/tests.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/summary/typing.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/summary/weights.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/themes/__init__.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/conftest.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/fixtures/scipy_validation/README.md +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/fixtures/scipy_validation/anova_oneway.json +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/fixtures/scipy_validation/chi_square.json +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/fixtures/scipy_validation/fisher_2x2.json +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/fixtures/scipy_validation/kruskal_wallis.json +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/fixtures/scipy_validation/student_t.json +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/fixtures/scipy_validation/svyttest.json +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/fixtures/scipy_validation/weighted_mean.json +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/fixtures/scipy_validation/welch_t_test.json +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/fixtures/scipy_validation/wilcoxon_rank_sum.json +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_compose.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_compose_edges.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_conditional_formatting.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_design_regression.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_extract_edges.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_extras_edges.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_extras_edges_2.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_format.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_latex_pptx.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_misc_fixes.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_modifier_edges.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_multi_model.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_partial_modifiers.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_partials.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_plot_embedding.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_plots.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_polars.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_pptx_overflow.py +0 -0
- /pysofra-0.1.0a3/tests/test_joss_property_invariants.py → /pysofra-0.1.0a4/tests/test_property_invariants.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_rao_scott.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_regression.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_regressions.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_render_edges.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_render_edges_2.py +0 -0
- /pysofra-0.1.0a3/tests/test_joss_renderer_consistency.py → /pysofra-0.1.0a4/tests/test_renderer_consistency.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_rendering.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_scipy_validation.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_snapshot.py +0 -0
- /pysofra-0.1.0a3/tests/test_joss_statistical_correctness.py → /pysofra-0.1.0a4/tests/test_statistical_correctness.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_stats.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_summary_edges.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_summary_edges_2.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_survey_design.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_survey_extensions.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_survival.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_table_edges.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_tbl_one.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_test_overrides.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_uvregression_factors.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_weights.py +0 -0
- {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_xlsx.py +0 -0
|
@@ -16,11 +16,14 @@ htmlcov/
|
|
|
16
16
|
.DS_Store
|
|
17
17
|
.idea/
|
|
18
18
|
.vscode/
|
|
19
|
-
.claude/
|
|
20
19
|
|
|
21
20
|
site/
|
|
22
21
|
docs/_build/
|
|
22
|
+
|
|
23
|
+
# Local working directories not part of the source distribution.
|
|
23
24
|
paper/
|
|
25
|
+
_local/
|
|
26
|
+
_private/
|
|
24
27
|
|
|
25
28
|
# Tutorial / example export artefacts — produced on demand by
|
|
26
29
|
# scripts/render_tutorial.py, not part of the source tree.
|
|
@@ -5,11 +5,22 @@ All notable changes to PySofra will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.1.0a4] — 2026-05-25
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Input validation for duplicate names in `variables=` (now raises
|
|
12
|
+
`ValueError` instead of silently accepting duplicates).
|
|
13
|
+
- Confidence-level range check in `.add_ci()` and related modifiers
|
|
14
|
+
(must lie in `(0, 1)`).
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- Renamed several test files for clarity. No public API changes.
|
|
18
|
+
|
|
8
19
|
## [0.1.0a3] — 2026-05-24
|
|
9
20
|
|
|
10
21
|
### Changed
|
|
11
|
-
- Documentation polish
|
|
12
|
-
|
|
22
|
+
- Documentation polish across README, changelog, and inline docstrings.
|
|
23
|
+
No public API or behavioural changes.
|
|
13
24
|
|
|
14
25
|
## [0.1.0a2] — 2026-05-23
|
|
15
26
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pysofra
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.0a4
|
|
4
4
|
Summary: Statistical reporting and table preparation framework for Python — the missing reporting layer.
|
|
5
5
|
Project-URL: Homepage, https://github.com/jturner-uofl/pysofra
|
|
6
6
|
Project-URL: Documentation, https://github.com/jturner-uofl/pysofra
|
|
7
7
|
Project-URL: Repository, https://github.com/jturner-uofl/pysofra
|
|
8
8
|
Project-URL: Issues, https://github.com/jturner-uofl/pysofra/issues
|
|
9
|
-
Author-email: Jason Turner <jason.
|
|
9
|
+
Author-email: Jason Turner <jason.turner@louisville.edu>
|
|
10
10
|
License: GPL-3.0-or-later
|
|
11
11
|
License-File: LICENSE
|
|
12
12
|
License-File: NOTICE
|
|
@@ -255,23 +255,23 @@ pip install "pysofra[dev]" # testing + linting (pytest, ruff, mypy, hypot
|
|
|
255
255
|
|
|
256
256
|
## Status
|
|
257
257
|
|
|
258
|
-
PySofra is in **alpha** (`0.1.
|
|
258
|
+
PySofra is in **alpha** (`0.1.0a4`). The public API surface is pinned
|
|
259
259
|
by an explicit
|
|
260
|
-
[API-stability test](https://github.com/jturner-uofl/pysofra/blob/main/tests/
|
|
260
|
+
[API-stability test](https://github.com/jturner-uofl/pysofra/blob/main/tests/test_api_stability.py)
|
|
261
261
|
so that any unintended rename, removal, or signature change surfaces as
|
|
262
262
|
a failed test. Quality bar at this release:
|
|
263
263
|
|
|
264
264
|
* **More than 800 tests passing**, **100% line coverage**, mypy strict, ruff clean.
|
|
265
265
|
* Every numeric output is validated against `scipy`, `lifelines`,
|
|
266
266
|
`statsmodels`, or a hand-computed textbook formula
|
|
267
|
-
([
|
|
267
|
+
([test_statistical_correctness.py](https://github.com/jturner-uofl/pysofra/blob/main/tests/test_statistical_correctness.py)).
|
|
268
268
|
* Universal invariants enforced via Hypothesis on 720 randomized
|
|
269
269
|
examples per CI run
|
|
270
|
-
([
|
|
270
|
+
([test_property_invariants.py](https://github.com/jturner-uofl/pysofra/blob/main/tests/test_property_invariants.py)).
|
|
271
271
|
* Renderer output is byte-deterministic — identical input always
|
|
272
272
|
produces identical HTML/Markdown/LaTeX, required for reproducible
|
|
273
273
|
publication artifacts
|
|
274
|
-
([
|
|
274
|
+
([test_renderer_consistency.py](https://github.com/jturner-uofl/pysofra/blob/main/tests/test_renderer_consistency.py)).
|
|
275
275
|
|
|
276
276
|
Bug reports and use-case feedback are very welcome.
|
|
277
277
|
|
|
@@ -189,23 +189,23 @@ pip install "pysofra[dev]" # testing + linting (pytest, ruff, mypy, hypot
|
|
|
189
189
|
|
|
190
190
|
## Status
|
|
191
191
|
|
|
192
|
-
PySofra is in **alpha** (`0.1.
|
|
192
|
+
PySofra is in **alpha** (`0.1.0a4`). The public API surface is pinned
|
|
193
193
|
by an explicit
|
|
194
|
-
[API-stability test](https://github.com/jturner-uofl/pysofra/blob/main/tests/
|
|
194
|
+
[API-stability test](https://github.com/jturner-uofl/pysofra/blob/main/tests/test_api_stability.py)
|
|
195
195
|
so that any unintended rename, removal, or signature change surfaces as
|
|
196
196
|
a failed test. Quality bar at this release:
|
|
197
197
|
|
|
198
198
|
* **More than 800 tests passing**, **100% line coverage**, mypy strict, ruff clean.
|
|
199
199
|
* Every numeric output is validated against `scipy`, `lifelines`,
|
|
200
200
|
`statsmodels`, or a hand-computed textbook formula
|
|
201
|
-
([
|
|
201
|
+
([test_statistical_correctness.py](https://github.com/jturner-uofl/pysofra/blob/main/tests/test_statistical_correctness.py)).
|
|
202
202
|
* Universal invariants enforced via Hypothesis on 720 randomized
|
|
203
203
|
examples per CI run
|
|
204
|
-
([
|
|
204
|
+
([test_property_invariants.py](https://github.com/jturner-uofl/pysofra/blob/main/tests/test_property_invariants.py)).
|
|
205
205
|
* Renderer output is byte-deterministic — identical input always
|
|
206
206
|
produces identical HTML/Markdown/LaTeX, required for reproducible
|
|
207
207
|
publication artifacts
|
|
208
|
-
([
|
|
208
|
+
([test_renderer_consistency.py](https://github.com/jturner-uofl/pysofra/blob/main/tests/test_renderer_consistency.py)).
|
|
209
209
|
|
|
210
210
|
Bug reports and use-case feedback are very welcome.
|
|
211
211
|
|
|
@@ -4,11 +4,11 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pysofra"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.0a4"
|
|
8
8
|
description = "Statistical reporting and table preparation framework for Python — the missing reporting layer."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "GPL-3.0-or-later" }
|
|
11
|
-
authors = [{ name = "Jason Turner", email = "jason.
|
|
11
|
+
authors = [{ name = "Jason Turner", email = "jason.turner@louisville.edu" }]
|
|
12
12
|
requires-python = ">=3.11"
|
|
13
13
|
keywords = [
|
|
14
14
|
"statistics",
|
|
@@ -118,8 +118,9 @@ exclude = [
|
|
|
118
118
|
"scripts",
|
|
119
119
|
"site",
|
|
120
120
|
".github",
|
|
121
|
-
".claude",
|
|
122
121
|
"paper",
|
|
122
|
+
"_local",
|
|
123
|
+
"_private",
|
|
123
124
|
"uv.lock",
|
|
124
125
|
]
|
|
125
126
|
|
|
@@ -157,6 +158,17 @@ exclude_lines = [
|
|
|
157
158
|
[tool.ruff]
|
|
158
159
|
line-length = 100
|
|
159
160
|
target-version = "py311"
|
|
161
|
+
# Notebooks are tutorial / pedagogical material, not library source.
|
|
162
|
+
# Their lint cleanliness is checked separately on demand; the project's
|
|
163
|
+
# "ruff clean" claim applies to the importable package and test suite.
|
|
164
|
+
extend-exclude = [
|
|
165
|
+
"examples/*.ipynb",
|
|
166
|
+
"examples/*.html",
|
|
167
|
+
"site",
|
|
168
|
+
".venv",
|
|
169
|
+
"_local",
|
|
170
|
+
"_private",
|
|
171
|
+
]
|
|
160
172
|
|
|
161
173
|
[tool.ruff.lint]
|
|
162
174
|
select = ["E", "F", "I", "UP", "B", "SIM"]
|
|
@@ -571,7 +571,7 @@ class SofraTable:
|
|
|
571
571
|
|
|
572
572
|
Uses matplotlib under the hood; the result is a faithful raster
|
|
573
573
|
of the HTML output. Useful for quick previews, Slack attachments,
|
|
574
|
-
and
|
|
574
|
+
and document figures where a static image is preferable.
|
|
575
575
|
|
|
576
576
|
``scale`` multiplies the pixel density (>= 1 recommended);
|
|
577
577
|
``dpi`` controls the output resolution (defaults to 300, the
|
|
@@ -330,6 +330,20 @@ def add_difference(
|
|
|
330
330
|
*,
|
|
331
331
|
digits: int = 2,
|
|
332
332
|
conf_level: float = 0.95,
|
|
333
|
+
) -> SofraTable:
|
|
334
|
+
"""Add an absolute-difference column with CI for a 2-group Table 1."""
|
|
335
|
+
if not (0.0 < conf_level < 1.0):
|
|
336
|
+
raise ValueError(
|
|
337
|
+
f"conf_level must lie in the open interval (0, 1); got {conf_level!r}."
|
|
338
|
+
)
|
|
339
|
+
return _add_difference_impl(table, digits=digits, conf_level=conf_level)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def _add_difference_impl(
|
|
343
|
+
table: SofraTable,
|
|
344
|
+
*,
|
|
345
|
+
digits: int = 2,
|
|
346
|
+
conf_level: float = 0.95,
|
|
333
347
|
) -> SofraTable:
|
|
334
348
|
"""Add an absolute-difference column with CI for a 2-group Table 1.
|
|
335
349
|
|
|
@@ -486,6 +500,10 @@ def add_ci(
|
|
|
486
500
|
mean. For dichotomous rows the ``n (%)`` cell gains a Wilson-score
|
|
487
501
|
CI for the proportion. Multi-level categorical rows are unchanged.
|
|
488
502
|
"""
|
|
503
|
+
if not (0.0 < conf_level < 1.0):
|
|
504
|
+
raise ValueError(
|
|
505
|
+
f"conf_level must lie in the open interval (0, 1); got {conf_level!r}."
|
|
506
|
+
)
|
|
489
507
|
if table._spec is None or table._rebuild is None:
|
|
490
508
|
raise ValueError(
|
|
491
509
|
"add_ci needs access to the source data — only tables built "
|
|
@@ -176,6 +176,18 @@ def tbl_one(
|
|
|
176
176
|
missing_cols = [v for v in variables if v not in data.columns]
|
|
177
177
|
if missing_cols:
|
|
178
178
|
raise KeyError(f"variables not in data: {missing_cols}")
|
|
179
|
+
# Reject duplicate entries early — silently de-duplicating would
|
|
180
|
+
# produce a table whose row count doesn't match the user's list,
|
|
181
|
+
# while keeping duplicates would emit identical rows.
|
|
182
|
+
seen: dict[str, int] = {}
|
|
183
|
+
for v in variables:
|
|
184
|
+
seen[v] = seen.get(v, 0) + 1
|
|
185
|
+
dupes = [v for v, n in seen.items() if n > 1]
|
|
186
|
+
if dupes:
|
|
187
|
+
raise ValueError(
|
|
188
|
+
f"variables contains duplicate names: {dupes}. "
|
|
189
|
+
"Each variable may appear at most once."
|
|
190
|
+
)
|
|
179
191
|
# Warn when the user-supplied variables list overlaps the design /
|
|
180
192
|
# stratification columns; silently dropping them is surprising.
|
|
181
193
|
overlap = [v for v in variables if v in excluded]
|
|
@@ -53,8 +53,8 @@ _DEFAULT = Theme(
|
|
|
53
53
|
"font-size": "14px",
|
|
54
54
|
"line-height": "1.45",
|
|
55
55
|
# Inherit the surrounding text colour so we always have contrast
|
|
56
|
-
# against the actual page background — no
|
|
57
|
-
#
|
|
56
|
+
# against the actual page background — no media-query rules
|
|
57
|
+
# that compete with the host application's own theme.
|
|
58
58
|
"color": "inherit",
|
|
59
59
|
"background": "transparent",
|
|
60
60
|
"margin": "0.75em 0",
|
pysofra-0.1.0a3/tests/test_joss_api_stability.py → pysofra-0.1.0a4/tests/test_api_stability.py
RENAMED
|
@@ -134,9 +134,9 @@ def test_survey_design_dataclass_fields():
|
|
|
134
134
|
# ----------------------------------------------------------------------
|
|
135
135
|
# SofraTable user-facing methods + dataclass attributes.
|
|
136
136
|
#
|
|
137
|
-
# Both sets are part of the
|
|
138
|
-
# call them in their reporting code, attributes because they
|
|
139
|
-
# the documented schema (
|
|
137
|
+
# Both sets are part of the documented public API: methods because
|
|
138
|
+
# users call them in their reporting code, attributes because they
|
|
139
|
+
# appear in the documented schema (README and notebook examples
|
|
140
140
|
# enumerate ``rows``, ``headers``, ``footnotes``, etc.).
|
|
141
141
|
# ----------------------------------------------------------------------
|
|
142
142
|
EXPECTED_SOFRATABLE_METHODS = frozenset({
|
|
@@ -186,7 +186,7 @@ def test_sofratable_attribute_surface():
|
|
|
186
186
|
def test_sofratable_no_undocumented_public_surface():
|
|
187
187
|
"""
|
|
188
188
|
Reject silently-added public names. Anything new must be added to
|
|
189
|
-
one of the two expected-sets above (so
|
|
189
|
+
one of the two expected-sets above (so downstream users see the API
|
|
190
190
|
change in the diff).
|
|
191
191
|
"""
|
|
192
192
|
public = {m for m in dir(SofraTable) if not m.startswith("_")}
|
|
@@ -200,6 +200,6 @@ def test_sofratable_no_undocumented_public_surface():
|
|
|
200
200
|
|
|
201
201
|
|
|
202
202
|
def test_sofratable_to_image_signature_stable():
|
|
203
|
-
"""The PNG renderer's public kwargs are part of the
|
|
203
|
+
"""The PNG renderer's public kwargs are part of the documented public API."""
|
|
204
204
|
actual = tuple(p[0] for p in _params(SofraTable.to_image))
|
|
205
205
|
assert actual == ("self", "path", "scale", "dpi")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Determinism tests for plot-embedded tables.
|
|
2
2
|
|
|
3
|
-
The renderer-consistency suite (`
|
|
3
|
+
The renderer-consistency suite (`test_renderer_consistency.py`)
|
|
4
4
|
verifies determinism for plain text-only tables. This file extends
|
|
5
5
|
that guarantee to tables that have an inline matplotlib plot
|
|
6
6
|
(`with_forest_plot`, `with_km_plot`) — historically the source of
|
|
@@ -16,7 +16,7 @@ import pysofra as ps
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
# ======================================================================
|
|
19
|
-
#
|
|
19
|
+
# add_difference uses Newcombe (Wilson-based) CI
|
|
20
20
|
# ======================================================================
|
|
21
21
|
class TestNewcombeDifferenceCI:
|
|
22
22
|
def test_matches_statsmodels_newcomb(self):
|
|
@@ -75,7 +75,7 @@ class TestNewcombeDifferenceCI:
|
|
|
75
75
|
|
|
76
76
|
|
|
77
77
|
# ======================================================================
|
|
78
|
-
#
|
|
78
|
+
# Markdown spanners + escaping
|
|
79
79
|
# ======================================================================
|
|
80
80
|
class TestMarkdownSpannersAndEscape:
|
|
81
81
|
def test_spanning_header_not_inserted_as_pipe_row(self):
|
|
@@ -137,7 +137,7 @@ class TestMarkdownSpannersAndEscape:
|
|
|
137
137
|
|
|
138
138
|
|
|
139
139
|
# ======================================================================
|
|
140
|
-
#
|
|
140
|
+
# labels= no longer breaks downstream modifiers
|
|
141
141
|
# ======================================================================
|
|
142
142
|
class TestLabelsPreservedDownstream:
|
|
143
143
|
def _df(self):
|
|
@@ -170,7 +170,7 @@ class TestLabelsPreservedDownstream:
|
|
|
170
170
|
|
|
171
171
|
|
|
172
172
|
# ======================================================================
|
|
173
|
-
#
|
|
173
|
+
# N at risk uses standard convention
|
|
174
174
|
# ======================================================================
|
|
175
175
|
class TestNAtRisk:
|
|
176
176
|
def test_n_at_risk_matches_manual_count(self):
|
|
@@ -195,7 +195,7 @@ class TestNAtRisk:
|
|
|
195
195
|
|
|
196
196
|
|
|
197
197
|
# ======================================================================
|
|
198
|
-
#
|
|
198
|
+
# add_global_p raises clearly on tbl_one
|
|
199
199
|
# ======================================================================
|
|
200
200
|
class TestAddGlobalPOnTblOne:
|
|
201
201
|
# ``add_global_p()`` is implemented for tbl_one / tbl_summary
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
"""Tests for
|
|
1
|
+
"""Tests for R-ecosystem feature parity: tbl_cross, effect sizes,
|
|
2
2
|
add_significance_stars / add_n / add_stat_label / color_scale_if,
|
|
3
|
-
MixedLM/GEE extractor, and
|
|
3
|
+
MixedLM/GEE extractor, and multiple-imputation pooling (Rubin's rules)."""
|
|
4
4
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
"""I/O utilities — reserved for future readers (Stata/SAS/Excel)."""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|