pysofra 0.1.0a2__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.
Files changed (109) hide show
  1. pysofra-0.1.0a4/.gitignore +48 -0
  2. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/CHANGELOG.md +26 -0
  3. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/PKG-INFO +8 -8
  4. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/README.md +6 -6
  5. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/pyproject.toml +18 -7
  6. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/src/pysofra/__init__.py +1 -1
  7. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/src/pysofra/core/table.py +1 -1
  8. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/src/pysofra/summary/extras.py +18 -0
  9. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/src/pysofra/summary/stats.py +6 -6
  10. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/src/pysofra/summary/tbl_one.py +12 -0
  11. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/src/pysofra/themes/registry.py +2 -2
  12. pysofra-0.1.0a2/tests/test_joss_api_stability.py → pysofra-0.1.0a4/tests/test_api_stability.py +7 -8
  13. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/tests/test_extract_edges.py +1 -1
  14. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/tests/test_extras_edges.py +3 -3
  15. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/tests/test_misc_fixes.py +7 -8
  16. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/tests/test_plot_determinism.py +1 -1
  17. pysofra-0.1.0a2/tests/test_joss_property_invariants.py → pysofra-0.1.0a4/tests/test_property_invariants.py +1 -1
  18. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/tests/test_regressions.py +7 -7
  19. pysofra-0.1.0a2/tests/test_joss_renderer_consistency.py → pysofra-0.1.0a4/tests/test_renderer_consistency.py +1 -1
  20. pysofra-0.1.0a2/tests/test_joss_statistical_correctness.py → pysofra-0.1.0a4/tests/test_statistical_correctness.py +6 -6
  21. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/tests/test_validation_fixes.py +7 -8
  22. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/tests/test_wishlist.py +2 -2
  23. pysofra-0.1.0a2/.gitignore +0 -90
  24. pysofra-0.1.0a2/src/pysofra/io/__init__.py +0 -1
  25. pysofra-0.1.0a2/src/pysofra/notebook/__init__.py +0 -6
  26. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/LICENSE +0 -0
  27. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/NOTICE +0 -0
  28. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/src/pysofra/core/__init__.py +0 -0
  29. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/src/pysofra/core/compose.py +0 -0
  30. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/src/pysofra/core/format.py +0 -0
  31. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/src/pysofra/core/frames.py +0 -0
  32. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/src/pysofra/core/schema.py +0 -0
  33. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/src/pysofra/models/__init__.py +0 -0
  34. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/src/pysofra/models/extract.py +0 -0
  35. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/src/pysofra/models/pool.py +0 -0
  36. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/src/pysofra/models/regression.py +0 -0
  37. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/src/pysofra/models/survival.py +0 -0
  38. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/src/pysofra/models/uvregression.py +0 -0
  39. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/src/pysofra/plot/__init__.py +0 -0
  40. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/src/pysofra/plot/_backend.py +0 -0
  41. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/src/pysofra/plot/forest.py +0 -0
  42. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/src/pysofra/plot/inline.py +0 -0
  43. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/src/pysofra/plot/km.py +0 -0
  44. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/src/pysofra/render/__init__.py +0 -0
  45. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/src/pysofra/render/_zip_determinism.py +0 -0
  46. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/src/pysofra/render/base.py +0 -0
  47. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/src/pysofra/render/docx.py +0 -0
  48. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/src/pysofra/render/html.py +0 -0
  49. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/src/pysofra/render/image.py +0 -0
  50. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/src/pysofra/render/latex.py +0 -0
  51. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/src/pysofra/render/markdown.py +0 -0
  52. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/src/pysofra/render/pptx.py +0 -0
  53. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/src/pysofra/render/xlsx.py +0 -0
  54. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/src/pysofra/summary/__init__.py +0 -0
  55. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/src/pysofra/summary/calibrate.py +0 -0
  56. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/src/pysofra/summary/design.py +0 -0
  57. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/src/pysofra/summary/effect_size.py +0 -0
  58. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/src/pysofra/summary/smd.py +0 -0
  59. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/src/pysofra/summary/tbl_cross.py +0 -0
  60. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/src/pysofra/summary/tbl_summary.py +0 -0
  61. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/src/pysofra/summary/tests.py +0 -0
  62. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/src/pysofra/summary/typing.py +0 -0
  63. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/src/pysofra/summary/weights.py +0 -0
  64. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/src/pysofra/themes/__init__.py +0 -0
  65. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/tests/conftest.py +0 -0
  66. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/tests/fixtures/scipy_validation/README.md +0 -0
  67. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/tests/fixtures/scipy_validation/anova_oneway.json +0 -0
  68. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/tests/fixtures/scipy_validation/chi_square.json +0 -0
  69. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/tests/fixtures/scipy_validation/fisher_2x2.json +0 -0
  70. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/tests/fixtures/scipy_validation/kruskal_wallis.json +0 -0
  71. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/tests/fixtures/scipy_validation/student_t.json +0 -0
  72. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/tests/fixtures/scipy_validation/svyttest.json +0 -0
  73. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/tests/fixtures/scipy_validation/weighted_mean.json +0 -0
  74. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/tests/fixtures/scipy_validation/welch_t_test.json +0 -0
  75. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/tests/fixtures/scipy_validation/wilcoxon_rank_sum.json +0 -0
  76. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/tests/test_compose.py +0 -0
  77. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/tests/test_compose_edges.py +0 -0
  78. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/tests/test_conditional_formatting.py +0 -0
  79. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/tests/test_design_regression.py +0 -0
  80. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/tests/test_extras_edges_2.py +0 -0
  81. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/tests/test_format.py +0 -0
  82. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/tests/test_latex_pptx.py +0 -0
  83. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/tests/test_modifier_edges.py +0 -0
  84. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/tests/test_multi_model.py +0 -0
  85. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/tests/test_partial_modifiers.py +0 -0
  86. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/tests/test_partials.py +0 -0
  87. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/tests/test_plot_embedding.py +0 -0
  88. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/tests/test_plots.py +0 -0
  89. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/tests/test_polars.py +0 -0
  90. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/tests/test_pptx_overflow.py +0 -0
  91. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/tests/test_rao_scott.py +0 -0
  92. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/tests/test_regression.py +0 -0
  93. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/tests/test_render_edges.py +0 -0
  94. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/tests/test_render_edges_2.py +0 -0
  95. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/tests/test_rendering.py +0 -0
  96. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/tests/test_scipy_validation.py +0 -0
  97. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/tests/test_snapshot.py +0 -0
  98. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/tests/test_stats.py +0 -0
  99. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/tests/test_summary_edges.py +0 -0
  100. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/tests/test_summary_edges_2.py +0 -0
  101. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/tests/test_survey_design.py +0 -0
  102. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/tests/test_survey_extensions.py +0 -0
  103. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/tests/test_survival.py +0 -0
  104. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/tests/test_table_edges.py +0 -0
  105. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/tests/test_tbl_one.py +0 -0
  106. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/tests/test_test_overrides.py +0 -0
  107. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/tests/test_uvregression_factors.py +0 -0
  108. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/tests/test_weights.py +0 -0
  109. {pysofra-0.1.0a2 → pysofra-0.1.0a4}/tests/test_xlsx.py +0 -0
@@ -0,0 +1,48 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .venv/
7
+ venv/
8
+ .env
9
+
10
+ .pytest_cache/
11
+ .mypy_cache/
12
+ .ruff_cache/
13
+ .coverage
14
+ htmlcov/
15
+
16
+ .DS_Store
17
+ .idea/
18
+ .vscode/
19
+
20
+ site/
21
+ docs/_build/
22
+
23
+ # Local working directories not part of the source distribution.
24
+ paper/
25
+ _local/
26
+ _private/
27
+
28
+ # Tutorial / example export artefacts — produced on demand by
29
+ # scripts/render_tutorial.py, not part of the source tree.
30
+ *.docx
31
+ *.pptx
32
+ *.xlsx
33
+ examples/tutorial_*.docx
34
+ examples/tutorial_*.pptx
35
+ examples/tutorial_*.xlsx
36
+ examples/tutorial_*.png
37
+ !tests/fixtures/**/*.docx
38
+ !tests/fixtures/**/*.pptx
39
+ !tests/fixtures/**/*.xlsx
40
+
41
+ # Hypothesis example database (auto-managed)
42
+ .hypothesis/
43
+
44
+ # uv writes a lockfile when ``uv add`` / ``uv pip install`` is used
45
+ # locally; the project pins its runtime versions explicitly in
46
+ # pyproject.toml, so the uv lockfile is a local convenience that
47
+ # should not ship.
48
+ uv.lock
@@ -5,6 +5,32 @@ 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
+
19
+ ## [0.1.0a3] — 2026-05-24
20
+
21
+ ### Changed
22
+ - Documentation polish across README, changelog, and inline docstrings.
23
+ No public API or behavioural changes.
24
+
25
+ ## [0.1.0a2] — 2026-05-23
26
+
27
+ ### Fixed
28
+ - Theme styling now survives notebook viewers that strip `<style>` blocks
29
+ (e.g. GitHub's notebook viewer). Critical theme properties (font, border,
30
+ padding) are emitted as inline `style` attributes on each table element, so
31
+ `jama` vs `nejm` vs `clinical` vs `minimal` stay visibly distinct everywhere.
32
+ - README image and link URLs are now absolute so they render on PyPI.
33
+
8
34
  ## [0.1.0a1] — 2026-05-20
9
35
 
10
36
  ### Added
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pysofra
3
- Version: 0.1.0a2
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.s.turner@gmail.com>
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
@@ -111,7 +111,7 @@ Description-Content-Type: text/markdown
111
111
  - **One immutable object, seven output formats** — build a `SofraTable` once, render to HTML / Markdown / LaTeX / DOCX / PPTX / XLSX / PNG, all byte-deterministic across processes
112
112
  - **Auto-dispatched statistical tests** — Welch, Wilcoxon, ANOVA, Kruskal–Wallis, Fisher, χ², Rao–Scott, design-adjusted *t* — picked by variable kind, overridable per-row
113
113
  - **Inline forest plots and KM curves** — embed matplotlib figures directly into the table; the same `SofraTable` renders them across every backend
114
- - **Statistically correct** — every numeric output validated against `scipy` / `statsmodels` / `lifelines` at machine precision (and cross-checked against R's `gtsummary` for the JSS paper)
114
+ - **Statistically correct** — every numeric output validated against `scipy` / `statsmodels` / `lifelines` at machine precision, with cross-checks against R's `gtsummary`
115
115
  - **Method-chainable and immutable** — every modifier returns a new table; no in-place mutation, no global state, fully reproducible
116
116
 
117
117
  <div align="center">
@@ -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.0a2`). The public API surface is pinned
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/test_joss_api_stability.py)
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
- ([test_joss_statistical_correctness.py](https://github.com/jturner-uofl/pysofra/blob/main/tests/test_joss_statistical_correctness.py)).
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
- ([test_joss_property_invariants.py](https://github.com/jturner-uofl/pysofra/blob/main/tests/test_joss_property_invariants.py)).
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
- ([test_joss_renderer_consistency.py](https://github.com/jturner-uofl/pysofra/blob/main/tests/test_joss_renderer_consistency.py)).
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
 
@@ -45,7 +45,7 @@
45
45
  - **One immutable object, seven output formats** — build a `SofraTable` once, render to HTML / Markdown / LaTeX / DOCX / PPTX / XLSX / PNG, all byte-deterministic across processes
46
46
  - **Auto-dispatched statistical tests** — Welch, Wilcoxon, ANOVA, Kruskal–Wallis, Fisher, χ², Rao–Scott, design-adjusted *t* — picked by variable kind, overridable per-row
47
47
  - **Inline forest plots and KM curves** — embed matplotlib figures directly into the table; the same `SofraTable` renders them across every backend
48
- - **Statistically correct** — every numeric output validated against `scipy` / `statsmodels` / `lifelines` at machine precision (and cross-checked against R's `gtsummary` for the JSS paper)
48
+ - **Statistically correct** — every numeric output validated against `scipy` / `statsmodels` / `lifelines` at machine precision, with cross-checks against R's `gtsummary`
49
49
  - **Method-chainable and immutable** — every modifier returns a new table; no in-place mutation, no global state, fully reproducible
50
50
 
51
51
  <div align="center">
@@ -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.0a2`). The public API surface is pinned
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/test_joss_api_stability.py)
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
- ([test_joss_statistical_correctness.py](https://github.com/jturner-uofl/pysofra/blob/main/tests/test_joss_statistical_correctness.py)).
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
- ([test_joss_property_invariants.py](https://github.com/jturner-uofl/pysofra/blob/main/tests/test_joss_property_invariants.py)).
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
- ([test_joss_renderer_consistency.py](https://github.com/jturner-uofl/pysofra/blob/main/tests/test_joss_renderer_consistency.py)).
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.0a2"
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.s.turner@gmail.com" }]
11
+ authors = [{ name = "Jason Turner", email = "jason.turner@louisville.edu" }]
12
12
  requires-python = ">=3.11"
13
13
  keywords = [
14
14
  "statistics",
@@ -101,9 +101,8 @@ packages = ["src/pysofra"]
101
101
 
102
102
  [tool.hatch.build.targets.sdist]
103
103
  # The sdist that goes on PyPI ships the library + tests + license +
104
- # changelog. The JSS paper bundle (paper/) is intentionally NOT
105
- # included: it lives in git for source consumers and reviewers
106
- # download it from the submission tarball, not from PyPI.
104
+ # changelog. Docs, examples, build scripts, and CI config are excluded
105
+ # users get the importable package, not the development surround.
107
106
  include = [
108
107
  "src/pysofra",
109
108
  "tests",
@@ -114,13 +113,14 @@ include = [
114
113
  "pyproject.toml",
115
114
  ]
116
115
  exclude = [
117
- "paper",
118
116
  "docs",
119
117
  "examples",
120
118
  "scripts",
121
119
  "site",
122
120
  ".github",
123
- ".claude",
121
+ "paper",
122
+ "_local",
123
+ "_private",
124
124
  "uv.lock",
125
125
  ]
126
126
 
@@ -158,6 +158,17 @@ exclude_lines = [
158
158
  [tool.ruff]
159
159
  line-length = 100
160
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
+ ]
161
172
 
162
173
  [tool.ruff.lint]
163
174
  select = ["E", "F", "I", "UP", "B", "SIM"]
@@ -50,7 +50,7 @@ from .summary.tbl_summary import tbl_summary
50
50
  from .summary.tests import available_tests
51
51
  from .themes.registry import available_themes, register_theme
52
52
 
53
- __version__ = "0.1.0a2"
53
+ __version__ = "0.1.0a4"
54
54
 
55
55
  __all__ = [
56
56
  "CellPart",
@@ -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 submission figures where reviewers want a visual.
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 "
@@ -45,12 +45,12 @@ def continuous_stats(series: pd.Series) -> ContinuousStats:
45
45
  # subtract`` (and similar). Under ``filterwarnings = error`` — which
46
46
  # this project's own pyproject.toml sets and which is a common
47
47
  # user-side ``-W error`` posture — those warnings escalate to
48
- # exceptions and crash ``tbl_one`` on perfectly legal data. The R6
49
- # audit fixed the ``int(np.inf) → OverflowError`` path in
50
- # ``infer_kind`` but didn't reach this downstream stats site.
51
- # Wrap arithmetic in ``np.errstate`` + ``catch_warnings`` so the
52
- # stats compute cleanly to ``nan`` / ``inf`` (which the formatters
53
- # then render as em-dash).
48
+ # exceptions and crash ``tbl_one`` on perfectly legal data. An
49
+ # earlier fix in ``infer_kind`` handled the ``int(np.inf) →
50
+ # OverflowError`` path but did not reach this downstream stats
51
+ # site. Wrap arithmetic in ``np.errstate`` + ``catch_warnings`` so
52
+ # the stats compute cleanly to ``nan`` / ``inf`` (which the
53
+ # formatters then render as em-dash).
54
54
  with np.errstate(invalid="ignore", over="ignore"), warnings.catch_warnings():
55
55
  warnings.simplefilter("ignore", RuntimeWarning)
56
56
  mean = float(np.mean(arr))
@@ -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 prefers-color-scheme
57
- # hacks that fight Jupyter's own theme.
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",
@@ -1,12 +1,11 @@
1
- """JOSS-grade API-stability snapshot.
1
+ """Public API-stability snapshot.
2
2
 
3
3
  Freezes the public surface of ``pysofra`` so any unintended rename,
4
4
  removal, or signature change surfaces as a failed test instead of a
5
5
  silent breakage downstream.
6
6
 
7
7
  If you intentionally change the API, update the constants in this file
8
- **and** bump the project version per semver. This is the contract with
9
- downstream users that JOSS will scrutinise.
8
+ **and** bump the project version per semver.
10
9
  """
11
10
 
12
11
  from __future__ import annotations
@@ -135,9 +134,9 @@ def test_survey_design_dataclass_fields():
135
134
  # ----------------------------------------------------------------------
136
135
  # SofraTable user-facing methods + dataclass attributes.
137
136
  #
138
- # Both sets are part of the JSS API contract: methods because users
139
- # call them in their reporting code, attributes because they appear in
140
- # the documented schema (the paper, README, and notebook examples
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
141
140
  # enumerate ``rows``, ``headers``, ``footnotes``, etc.).
142
141
  # ----------------------------------------------------------------------
143
142
  EXPECTED_SOFRATABLE_METHODS = frozenset({
@@ -187,7 +186,7 @@ def test_sofratable_attribute_surface():
187
186
  def test_sofratable_no_undocumented_public_surface():
188
187
  """
189
188
  Reject silently-added public names. Anything new must be added to
190
- one of the two expected-sets above (so reviewers see the API
189
+ one of the two expected-sets above (so downstream users see the API
191
190
  change in the diff).
192
191
  """
193
192
  public = {m for m in dir(SofraTable) if not m.startswith("_")}
@@ -201,6 +200,6 @@ def test_sofratable_no_undocumented_public_surface():
201
200
 
202
201
 
203
202
  def test_sofratable_to_image_signature_stable():
204
- """The PNG renderer's public kwargs are part of the JSS API contract."""
203
+ """The PNG renderer's public kwargs are part of the documented public API."""
205
204
  actual = tuple(p[0] for p in _params(SofraTable.to_image))
206
205
  assert actual == ("self", "path", "scale", "dpi")
@@ -236,7 +236,7 @@ class TestExtrasFinal:
236
236
  def test_add_global_p_metadata_missing_f_test(self):
237
237
  # A SofraTable whose metadata['model'] doesn't have .f_test now
238
238
  # raises NotImplementedError rather than silently inserting an
239
- # em-dash column (which a JOSS reviewer flagged as misleading).
239
+ # em-dash column (which would be misleading).
240
240
  from pysofra.core.schema import Cell, HeaderCell, HeaderRow, Row
241
241
  from pysofra.core.table import SofraTable
242
242
 
@@ -353,9 +353,9 @@ class TestTypingCorners:
353
353
  def test_datetime_falls_through_to_categorical(self):
354
354
  # Backwards-compatible fallback: PySofra doesn't natively
355
355
  # summarise datetimes, so it returns ``"categorical"`` (so the
356
- # caller's table doesn't crash). The warning emitted by the
357
- # Fix is verified in
358
- # tests/test_audit_regressions.py::TestInferKindWarnsOnTemporal.
356
+ # caller's table doesn't crash). The warning emitted alongside
357
+ # the fallback is verified in
358
+ # tests/test_regressions.py::TestInferKindWarnsOnTemporal.
359
359
  import pytest
360
360
 
361
361
  from pysofra.summary.typing import infer_kind
@@ -1,8 +1,7 @@
1
- """Audit-discovered defects pinned by regression tests.
1
+ """Targeted regression tests for previously-fixed defects.
2
2
 
3
- Each test pins a finding from the most recent god-mode audit pass.
4
- The names are deliberately literal so that any regression is easy to
5
- locate.
3
+ Each test pins a specific bug so that any regression is easy to
4
+ locate by name.
6
5
  """
7
6
 
8
7
  from __future__ import annotations
@@ -15,7 +14,7 @@ import pysofra as ps
15
14
 
16
15
 
17
16
  # ----------------------------------------------------------------------
18
- # Audit P4 — predictor / adjust_for overlap raises cleanly
17
+ # Predictor / adjust_for overlap raises cleanly
19
18
  # ----------------------------------------------------------------------
20
19
  class TestUvregressionOverlapErrors:
21
20
  def test_predictor_also_in_adjust_for_raises(self):
@@ -46,7 +45,7 @@ class TestUvregressionOverlapErrors:
46
45
 
47
46
 
48
47
  # ----------------------------------------------------------------------
49
- # Audit P2 — formula-API model works through the design refit
48
+ # Formula-API model works through the design refit
50
49
  # ----------------------------------------------------------------------
51
50
  class TestFormulaAPIRoundTrip:
52
51
  def test_design_refit_with_smf_ols(self):
@@ -69,7 +68,7 @@ class TestFormulaAPIRoundTrip:
69
68
 
70
69
 
71
70
  # ----------------------------------------------------------------------
72
- # Audit P3 — high-cardinality factor scales reasonably
71
+ # High-cardinality factor scales reasonably
73
72
  # ----------------------------------------------------------------------
74
73
  class TestHighCardinalityFactor:
75
74
  def test_50_level_factor_completes_quickly(self):
@@ -91,7 +90,7 @@ class TestHighCardinalityFactor:
91
90
 
92
91
 
93
92
  # ----------------------------------------------------------------------
94
- # Audit P1 — Newcombe handles imbalanced n
93
+ # Newcombe handles imbalanced n
95
94
  # ----------------------------------------------------------------------
96
95
  class TestNewcombeImbalanced:
97
96
  def test_imbalanced_n_produces_valid_ci(self):
@@ -1,6 +1,6 @@
1
1
  """Determinism tests for plot-embedded tables.
2
2
 
3
- The renderer-consistency suite (`test_joss_renderer_consistency.py`)
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
@@ -1,4 +1,4 @@
1
- """JOSS-grade property-based invariant testing.
1
+ """Property-based invariant testing via Hypothesis.
2
2
 
3
3
  Uses Hypothesis to generate hundreds of random DataFrames and exercise
4
4
  PySofra against invariants that must hold *for every input*:
@@ -1,7 +1,7 @@
1
- """Regression tests for issues caught by the full-system audit.
1
+ """Targeted regression tests for previously-fixed defects.
2
2
 
3
- Every test here corresponds to a real bug the audit surfaced and the
4
- patch that closed it. Each test should fail if the patch is reverted.
3
+ Each test corresponds to a real bug and the patch that closed it.
4
+ Each test should fail if the patch is reverted.
5
5
  """
6
6
 
7
7
  from __future__ import annotations
@@ -630,9 +630,9 @@ class TestInferKindInfSafe:
630
630
  through. Under ``filterwarnings = error`` (the project's own
631
631
  pyproject.toml gate, and a common user ``-W error`` posture), that
632
632
  warning escalates to an exception and the table build crashes
633
- despite the R6 fix. ``test_inf_does_not_leak_runtime_warning``
634
- below pins the strict-warning behaviour so the inf path is
635
- end-to-end clean.
633
+ despite the earlier ``int(np.inf)`` fix in ``infer_kind``.
634
+ ``test_inf_does_not_leak_runtime_warning`` below pins the
635
+ strict-warning behaviour so the inf path is end-to-end clean.
636
636
  """
637
637
 
638
638
  def test_inf_in_numeric_column_does_not_crash(self):
@@ -685,7 +685,7 @@ class TestInferKindInfSafe:
685
685
 
686
686
 
687
687
  # ----------------------------------------------------------------------
688
- # SofraTable must round-trip through pickle ( audit, §F.2)
688
+ # SofraTable must round-trip through pickle
689
689
  # ----------------------------------------------------------------------
690
690
  class TestSofraTablePicklability:
691
691
  """A SofraTable produced by a builder must survive ``pickle.dumps``/
@@ -1,4 +1,4 @@
1
- """JOSS-grade cross-renderer consistency validation.
1
+ """Cross-renderer consistency validation.
2
2
 
3
3
  A single ``SofraTable`` must render to HTML / Markdown / LaTeX / DOCX /
4
4
  PPTX / XLSX with **consistent content**: the same cell texts, the
@@ -1,13 +1,13 @@
1
- """JOSS-grade statistical correctness validation.
1
+ """Statistical correctness validation against independent references.
2
2
 
3
3
  Every numeric routine in PySofra is verified against an independent
4
4
  reference: scipy / lifelines / statsmodels direct calls, hand-computed
5
- textbook formulas, or published reference values. A JOSS reviewer
6
- should be able to point at any number rendered by ``tbl_one`` /
7
- ``tbl_regression`` / ``tbl_survival`` / ``tbl_uvregression`` and have
8
- this file demonstrate where it came from.
5
+ textbook formulas, or published reference values. Any number rendered
6
+ by ``tbl_one`` / ``tbl_regression`` / ``tbl_survival`` /
7
+ ``tbl_uvregression`` can be traced back through this file to where it
8
+ came from.
9
9
 
10
- Each test in this file names its reference source in the docstring.
10
+ Each test names its reference source in the docstring.
11
11
  """
12
12
 
13
13
  from __future__ import annotations
@@ -1,8 +1,7 @@
1
1
  """Regression tests for input-validation edge cases.
2
2
 
3
- Each test pins a specific defect the external auditor flagged, so any
4
- future regression surfaces with a clear name pointing back at the
5
- finding.
3
+ Each test pins a specific defect so any future regression surfaces
4
+ with a clear name pointing back at the finding.
6
5
  """
7
6
 
8
7
  from __future__ import annotations
@@ -17,7 +16,7 @@ import pysofra as ps
17
16
 
18
17
 
19
18
  # ======================================================================
20
- # BLOCKER #4 — add_difference uses Newcombe (Wilson-based) CI
19
+ # add_difference uses Newcombe (Wilson-based) CI
21
20
  # ======================================================================
22
21
  class TestNewcombeDifferenceCI:
23
22
  def test_matches_statsmodels_newcomb(self):
@@ -76,7 +75,7 @@ class TestNewcombeDifferenceCI:
76
75
 
77
76
 
78
77
  # ======================================================================
79
- # BLOCKER #7 / HIGH #8 — Markdown spanners + escaping
78
+ # Markdown spanners + escaping
80
79
  # ======================================================================
81
80
  class TestMarkdownSpannersAndEscape:
82
81
  def test_spanning_header_not_inserted_as_pipe_row(self):
@@ -138,7 +137,7 @@ class TestMarkdownSpannersAndEscape:
138
137
 
139
138
 
140
139
  # ======================================================================
141
- # HIGH #11 — labels= no longer breaks downstream modifiers
140
+ # labels= no longer breaks downstream modifiers
142
141
  # ======================================================================
143
142
  class TestLabelsPreservedDownstream:
144
143
  def _df(self):
@@ -171,7 +170,7 @@ class TestLabelsPreservedDownstream:
171
170
 
172
171
 
173
172
  # ======================================================================
174
- # HIGH #13 — N at risk uses standard convention
173
+ # N at risk uses standard convention
175
174
  # ======================================================================
176
175
  class TestNAtRisk:
177
176
  def test_n_at_risk_matches_manual_count(self):
@@ -196,7 +195,7 @@ class TestNAtRisk:
196
195
 
197
196
 
198
197
  # ======================================================================
199
- # HIGH #10 — add_global_p raises clearly on tbl_one
198
+ # add_global_p raises clearly on tbl_one
200
199
  # ======================================================================
201
200
  class TestAddGlobalPOnTblOne:
202
201
  # ``add_global_p()`` is implemented for tbl_one / tbl_summary
@@ -1,6 +1,6 @@
1
- """Tests for the R-ecosystem wishlist features: tbl_cross, effect sizes,
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 MI pooling (Rubin's rules)."""
3
+ MixedLM/GEE extractor, and multiple-imputation pooling (Rubin's rules)."""
4
4
 
5
5
  from __future__ import annotations
6
6
 
@@ -1,90 +0,0 @@
1
- __pycache__/
2
- *.py[cod]
3
- *.egg-info/
4
- dist/
5
- build/
6
- .venv/
7
- venv/
8
- .env
9
-
10
- .pytest_cache/
11
- .mypy_cache/
12
- .ruff_cache/
13
- .coverage
14
- htmlcov/
15
-
16
- .DS_Store
17
- .idea/
18
- .vscode/
19
- .claude/
20
-
21
- site/
22
- docs/_build/
23
-
24
- # Tutorial / example export artefacts — produced on demand by
25
- # scripts/render_tutorial.py, not part of the source tree.
26
- *.docx
27
- *.pptx
28
- *.xlsx
29
- examples/tutorial_*.docx
30
- examples/tutorial_*.pptx
31
- examples/tutorial_*.xlsx
32
- examples/tutorial_*.png
33
- !tests/fixtures/**/*.docx
34
- !tests/fixtures/**/*.pptx
35
- !tests/fixtures/**/*.xlsx
36
-
37
- # Hypothesis example database (auto-managed)
38
- .hypothesis/
39
-
40
- # uv writes a lockfile when ``uv add`` / ``uv pip install`` is used
41
- # locally; the project pins its runtime versions explicitly in
42
- # pyproject.toml + paper/replication/requirements.txt, so the uv
43
- # lockfile is a local convenience that should not ship.
44
- uv.lock
45
-
46
- # JSS paper bundle — paper/ contains the manuscript, bibliography,
47
- # embedded figures, and replication archive. The .tex / .bib / .py /
48
- # .R sources are ALWAYS tracked. Replication outputs are tracked in
49
- # the text formats reviewers diff (json / tex / md / html / png /
50
- # pdf / svg) so the archive ships with a working baseline; only the
51
- # heavy regenerable Office binaries (docx / pptx / xlsx) and the
52
- # fontconfig / mpl cache directories are ignored.
53
- paper/replication/trial.csv
54
- paper/replication/table*.docx
55
- paper/replication/table*.pptx
56
- paper/replication/table*.xlsx
57
- paper/replication/.mplconfig/
58
-
59
- # Submission-required text outputs and figures: explicit re-include.
60
- # The blanket repo-wide ``*.docx`` rule at the top of this file would
61
- # otherwise eat ``paper/replication/table*.html`` etc. — these
62
- # negations keep them tracked.
63
- !paper/replication/paper_outputs*.json
64
- !paper/replication/table*.html
65
- !paper/replication/table*.tex
66
- !paper/replication/table*.md
67
- !paper/replication/figures/
68
- !paper/replication/figures/*
69
-
70
- # The `paper/figures/` directory IS tracked because paper.tex embeds
71
- # its PDFs/PNGs via \includegraphics. To regenerate, run
72
- # paper/replication/example_trial.py then copy the desired files in.
73
- !paper/figures/
74
- !paper/figures/*
75
-
76
- # LaTeX build artefacts (in case anyone compiles paper.tex locally).
77
- paper/*.aux
78
- paper/*.log
79
- paper/*.out
80
- paper/*.toc
81
- paper/*.bbl
82
- paper/*.blg
83
- paper/*.synctex.gz
84
- paper/paper.pdf
85
-
86
-
87
- # paper/ is excluded from the public repo until JSS submission lands.
88
- # It stays in the local working tree for editing; the replication
89
- # archive moves into the repo at submission time.
90
- paper/
@@ -1 +0,0 @@
1
- """I/O utilities — reserved for future readers (Stata/SAS/Excel)."""
@@ -1,6 +0,0 @@
1
- """Notebook helpers.
2
-
3
- Most of the notebook integration lives on :class:`SofraTable._repr_html_`.
4
- This module is reserved for future notebook-specific extensions (e.g.
5
- ipywidgets-based controls).
6
- """
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes