coeftable 0.1.0__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.
@@ -0,0 +1,3 @@
1
+ ## Summary
2
+
3
+ <!-- What does this PR change, and why? -->
@@ -0,0 +1,10 @@
1
+ changelog:
2
+ categories:
3
+ - title: New Features
4
+ labels: [feature, enhancement]
5
+ - title: Bug Fixes
6
+ labels: [fix, bug]
7
+ - title: Documentation
8
+ labels: [documentation]
9
+ - title: Maintenance
10
+ labels: ["*"]
@@ -0,0 +1,33 @@
1
+ name: ci
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ prek:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v6
14
+ - uses: astral-sh/setup-uv@v6
15
+ - run: make setup
16
+ - name: prek check
17
+ uses: j178/prek-action@v1
18
+ env:
19
+ SKIP: no-commit-to-branch
20
+
21
+ test:
22
+ runs-on: ubuntu-latest
23
+ needs: prek
24
+ strategy:
25
+ fail-fast: false
26
+ matrix:
27
+ python-version: ["3.12", "3.13", "3.14"]
28
+ steps:
29
+ - uses: actions/checkout@v6
30
+ - uses: astral-sh/setup-uv@v6
31
+ with:
32
+ python-version: ${{ matrix.python-version }}
33
+ - run: uv run --with nox nox -s "tests-${{ matrix.python-version }}"
@@ -0,0 +1,24 @@
1
+ name: Post-release
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+ workflow_dispatch:
7
+
8
+ jobs:
9
+ changelog:
10
+ name: Update changelog
11
+ runs-on: ubuntu-latest
12
+ permissions:
13
+ contents: write
14
+ pull-requests: write
15
+ steps:
16
+ - uses: actions/checkout@v6
17
+ with:
18
+ ref: main
19
+ - uses: rhysd/changelog-from-release/action@v3
20
+ with:
21
+ file: CHANGELOG.md
22
+ github_token: ${{ secrets.GITHUB_TOKEN }}
23
+ commit_summary_template: "update changelog for %s changes"
24
+ pull_request: true
@@ -0,0 +1,38 @@
1
+ name: Publish library
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ tags:
7
+ - "v*"
8
+
9
+ jobs:
10
+ build-package:
11
+ runs-on: ubuntu-latest
12
+ permissions:
13
+ attestations: write
14
+ id-token: write
15
+ steps:
16
+ - uses: actions/checkout@v6
17
+ with:
18
+ fetch-depth: 0
19
+ persist-credentials: false
20
+ - uses: hynek/build-and-inspect-python-package@v2
21
+ with:
22
+ attest-build-provenance-github: true
23
+
24
+ publish:
25
+ runs-on: ubuntu-latest
26
+ if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
27
+ environment:
28
+ name: publish
29
+ url: https://pypi.org/p/coeftable
30
+ needs: build-package
31
+ permissions:
32
+ id-token: write
33
+ steps:
34
+ - uses: actions/download-artifact@v8
35
+ with:
36
+ name: Packages
37
+ path: dist
38
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,18 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ .nox/
5
+ dist/
6
+ build/
7
+ *.egg-info/
8
+ .pytest_cache/
9
+ .ruff_cache/
10
+ .coverage
11
+ coverage.xml
12
+ .ipynb_checkpoints/
13
+ .DS_Store
14
+ src/coeftable/_version.py
15
+ .kata.toml
16
+ .kata.local.toml
17
+ # roborev snapshots
18
+ /.roborev/
@@ -0,0 +1,28 @@
1
+ ci:
2
+ autofix_prs: false
3
+
4
+ repos:
5
+ - repo: https://github.com/astral-sh/ruff-pre-commit
6
+ rev: v0.15.6
7
+ hooks:
8
+ - id: ruff-check
9
+ types_or: [python, pyi, jupyter]
10
+ args: ["--fix", "--output-format=full"]
11
+ - id: ruff-format
12
+ types_or: [python, pyi, jupyter]
13
+ - repo: https://github.com/astral-sh/ty-pre-commit
14
+ rev: v0.0.49
15
+ hooks:
16
+ - id: ty
17
+ - repo: https://github.com/pre-commit/pre-commit-hooks
18
+ rev: v6.0.0
19
+ hooks:
20
+ - id: no-commit-to-branch
21
+ args: [--branch, main]
22
+ stages: [pre-commit, pre-merge-commit, pre-push, manual]
23
+ - id: debug-statements
24
+ - id: trailing-whitespace
25
+ - id: end-of-file-fixer
26
+ - id: check-toml
27
+ - id: check-yaml
28
+ - id: check-added-large-files
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kyle Caron
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,20 @@
1
+ .PHONY: setup tests tests-all lint typecheck prek
2
+
3
+ setup:
4
+ uv sync --all-extras
5
+ uv run prek install
6
+
7
+ tests:
8
+ uv run pytest
9
+
10
+ tests-all:
11
+ uv run nox -s tests
12
+
13
+ lint:
14
+ uv run nox -s lint
15
+
16
+ typecheck:
17
+ uv run nox -s typecheck
18
+
19
+ prek:
20
+ SKIP=no-commit-to-branch uv run prek run --all-files
@@ -0,0 +1,205 @@
1
+ Metadata-Version: 2.4
2
+ Name: coeftable
3
+ Version: 0.1.0
4
+ Summary: Publication-quality summary tables for estimates with uncertainty.
5
+ Project-URL: Homepage, https://github.com/kylejcaron/coeftable
6
+ Project-URL: Repository, https://github.com/kylejcaron/coeftable
7
+ Project-URL: Issues, https://github.com/kylejcaron/coeftable/issues
8
+ Author-email: Kyle Caron <kyle.j.caron@gmail.com>
9
+ License: MIT License
10
+
11
+ Copyright (c) 2026 Kyle Caron
12
+
13
+ Permission is hereby granted, free of charge, to any person obtaining a copy
14
+ of this software and associated documentation files (the "Software"), to deal
15
+ in the Software without restriction, including without limitation the rights
16
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
+ copies of the Software, and to permit persons to whom the Software is
18
+ furnished to do so, subject to the following conditions:
19
+
20
+ The above copyright notice and this permission notice shall be included in all
21
+ copies or substantial portions of the Software.
22
+
23
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29
+ SOFTWARE.
30
+ License-File: LICENSE
31
+ Keywords: confidence-interval,forest-plot,great-tables,statistics,tables
32
+ Classifier: Development Status :: 3 - Alpha
33
+ Classifier: Intended Audience :: Science/Research
34
+ Classifier: License :: OSI Approved :: MIT License
35
+ Classifier: Programming Language :: Python :: 3
36
+ Classifier: Programming Language :: Python :: 3.12
37
+ Classifier: Programming Language :: Python :: 3.13
38
+ Classifier: Programming Language :: Python :: 3.14
39
+ Classifier: Topic :: Scientific/Engineering
40
+ Requires-Python: >=3.12
41
+ Requires-Dist: great-tables>=0.22
42
+ Requires-Dist: narwhals>=2.24
43
+ Provides-Extra: dev
44
+ Requires-Dist: nox>=2025.5; extra == 'dev'
45
+ Requires-Dist: pandas>=2.2; extra == 'dev'
46
+ Requires-Dist: polars>=1.0; extra == 'dev'
47
+ Requires-Dist: prek>=0.4.5; extra == 'dev'
48
+ Requires-Dist: pytest-cov>=5.0; extra == 'dev'
49
+ Requires-Dist: pytest>=8.0; extra == 'dev'
50
+ Requires-Dist: ruff>=0.15; extra == 'dev'
51
+ Requires-Dist: ty>=0.0.49; extra == 'dev'
52
+ Description-Content-Type: text/markdown
53
+
54
+ # coeftable
55
+
56
+ Lightweight, report-ready summary tables for estimates with uncertainty. Renders
57
+ inline forest plots, builds on great_tables HTML output, and works with pandas,
58
+ polars, or pyarrow frames.
59
+
60
+ ![Rendered experiment results table with grouped sections, nested variants, and an inline forest plot](docs/images/example.png)
61
+
62
+ ## Installation
63
+
64
+ ```bash
65
+ uv add coeftable
66
+ ```
67
+
68
+ ## Quick start
69
+
70
+ The one-line form declares a table with a single estimate column:
71
+
72
+ ```python
73
+ import polars as pl
74
+ import coeftable as ct
75
+
76
+ df = pl.DataFrame(
77
+ {
78
+ "metric": ["Revenue", "Latency"],
79
+ "est": [3.4, 0.5],
80
+ "lb": [1.2, -1.0],
81
+ "ub": [5.7, 2.0],
82
+ }
83
+ )
84
+
85
+ ct.CoefTable(df, rows="metric", estimate="est", ci=("lb", "ub"))
86
+ ```
87
+
88
+ A `CoefTable` renders itself in marimo, Jupyter, and any other `_repr_html_`-aware viewer —
89
+ leave it as the last expression in a cell, no extra call needed. Outside a notebook, use
90
+ `.gt()` to reach the underlying [great_tables](https://posit-dev.github.io/great-tables/)
91
+ object: `table.gt().as_raw_html()` for an HTML string, `table.gt().save("t.png")` for an
92
+ image, `table.gt().tab_options(...)` to keep styling with great_tables' own API.
93
+
94
+ ## Experiment table
95
+
96
+ Build a complete experiment results table with multiple estimates, a forest
97
+ plot column, grouped row sections, nested variants, and direction hints:
98
+
99
+ ```python
100
+ import polars as pl
101
+ import coeftable as ct
102
+
103
+ experiment = pl.DataFrame(
104
+ {
105
+ "area": ["Core", "Core", "Ops", "Ops"],
106
+ "metric": ["Revenue", "Revenue", "Latency", "Latency"],
107
+ "variant": ["B", "C", "B", "C"],
108
+ "att": [12400.0, -3100.0, 40.0, 120.0],
109
+ "att_lb": [4200.0, -9800.0, -80.0, 45.0],
110
+ "att_ub": [20600.0, 3600.0, 160.0, 195.0],
111
+ "rel": [3.4, -1.2, 0.5, 2.0],
112
+ "rel_lb": [1.2, -4.0, -1.0, 0.8],
113
+ "rel_ub": [5.7, 1.6, 2.0, 3.2],
114
+ }
115
+ )
116
+
117
+ (
118
+ ct.CoefTable(experiment, rows="metric", nest="variant", groups="area")
119
+ .estimate("Lift Amount", "att", ci=("att_lb", "att_ub"), fmt=ct.Number(compact=True))
120
+ .estimate("Lift %", "rel", ci=("rel_lb", "rel_ub"), fmt=ct.Percent(signed=True))
121
+ .forest("Lift Plot", of="Lift %", ref=0.0, symmetric=True)
122
+ .header("Experiment Results", "Example Experiment")
123
+ .with_direction({"Latency": "lower_is_better"})
124
+ )
125
+ ```
126
+
127
+ ## Comparing methods
128
+
129
+ Use `split_columns` to compare multiple methods side by side. Each value in the
130
+ split column produces its own set of estimate / forest columns:
131
+
132
+ ```python
133
+ import polars as pl
134
+ import coeftable as ct
135
+
136
+ methods = pl.DataFrame(
137
+ {
138
+ "metric": ["Revenue", "Revenue", "Latency", "Latency"],
139
+ "method": ["A", "B", "A", "B"],
140
+ "est": [3.4, 3.1, 0.5, 0.6],
141
+ "lb": [1.2, 1.0, -1.0, -0.8],
142
+ "ub": [5.7, 5.2, 2.0, 2.1],
143
+ }
144
+ )
145
+
146
+ (
147
+ ct.CoefTable(
148
+ methods, rows="metric", split_columns="method", estimate="est", ci=("lb", "ub")
149
+ )
150
+ .header("Cohort Revenue by Method")
151
+ )
152
+ ```
153
+
154
+ ## Theming
155
+
156
+ Four built-in themes are available from `coeftable.theme`:
157
+
158
+ ```python
159
+ from coeftable.theme import BLUE, COLORBLIND, DEFAULT, MONO, TEXTUAL
160
+
161
+ DEFAULT # Alias for TEXTUAL -- what CoefTable uses if you don't set a theme
162
+ TEXTUAL # Minimal, publication-style: muted colours, light chrome
163
+ BLUE # The original blue-grey palette
164
+ COLORBLIND # Colourblind-safe palette
165
+ MONO # Grayscale for mono journals
166
+ ```
167
+
168
+ Apply one with `.with_theme(...)`:
169
+
170
+ ```python
171
+ table.with_theme(BLUE)
172
+ ```
173
+
174
+ Customise a theme with `dataclasses.replace`:
175
+
176
+ ```python
177
+ from dataclasses import replace
178
+
179
+ my_theme = replace(BLUE, favorable="#0072B2")
180
+ ```
181
+
182
+ Use `with_direction` to mark rows where lower values are favourable (reusing
183
+ the `df` frame from [Quick start](#quick-start)):
184
+
185
+ ```python
186
+ table = (
187
+ ct.CoefTable(df, rows="metric", estimate="est", ci=("lb", "ub"))
188
+ .with_direction({"Latency": "lower_is_better"})
189
+ )
190
+ ```
191
+
192
+ ## Data shape
193
+
194
+ coeftable expects a dataframe where every row is a single comparison. The
195
+ resolution logic maps pairs of upper / lower bound columns to each estimate, so
196
+ your data should be **wide in triples** — one point-estimate column and (when
197
+ applicable) its lower and upper bound columns — rather than in long format with
198
+ a `parameter` column.
199
+
200
+ **Dimensions:**
201
+ - `rows` — the label for each row in the table (e.g. a metric name).
202
+ - `nest` — an optional secondary label stacked below each row.
203
+ - `groups` — an optional column whose values produce section headers.
204
+ - `split_columns` — an optional column whose values produce repeated column
205
+ groups side by side, useful for comparing methods.
@@ -0,0 +1,152 @@
1
+ # coeftable
2
+
3
+ Lightweight, report-ready summary tables for estimates with uncertainty. Renders
4
+ inline forest plots, builds on great_tables HTML output, and works with pandas,
5
+ polars, or pyarrow frames.
6
+
7
+ ![Rendered experiment results table with grouped sections, nested variants, and an inline forest plot](docs/images/example.png)
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ uv add coeftable
13
+ ```
14
+
15
+ ## Quick start
16
+
17
+ The one-line form declares a table with a single estimate column:
18
+
19
+ ```python
20
+ import polars as pl
21
+ import coeftable as ct
22
+
23
+ df = pl.DataFrame(
24
+ {
25
+ "metric": ["Revenue", "Latency"],
26
+ "est": [3.4, 0.5],
27
+ "lb": [1.2, -1.0],
28
+ "ub": [5.7, 2.0],
29
+ }
30
+ )
31
+
32
+ ct.CoefTable(df, rows="metric", estimate="est", ci=("lb", "ub"))
33
+ ```
34
+
35
+ A `CoefTable` renders itself in marimo, Jupyter, and any other `_repr_html_`-aware viewer —
36
+ leave it as the last expression in a cell, no extra call needed. Outside a notebook, use
37
+ `.gt()` to reach the underlying [great_tables](https://posit-dev.github.io/great-tables/)
38
+ object: `table.gt().as_raw_html()` for an HTML string, `table.gt().save("t.png")` for an
39
+ image, `table.gt().tab_options(...)` to keep styling with great_tables' own API.
40
+
41
+ ## Experiment table
42
+
43
+ Build a complete experiment results table with multiple estimates, a forest
44
+ plot column, grouped row sections, nested variants, and direction hints:
45
+
46
+ ```python
47
+ import polars as pl
48
+ import coeftable as ct
49
+
50
+ experiment = pl.DataFrame(
51
+ {
52
+ "area": ["Core", "Core", "Ops", "Ops"],
53
+ "metric": ["Revenue", "Revenue", "Latency", "Latency"],
54
+ "variant": ["B", "C", "B", "C"],
55
+ "att": [12400.0, -3100.0, 40.0, 120.0],
56
+ "att_lb": [4200.0, -9800.0, -80.0, 45.0],
57
+ "att_ub": [20600.0, 3600.0, 160.0, 195.0],
58
+ "rel": [3.4, -1.2, 0.5, 2.0],
59
+ "rel_lb": [1.2, -4.0, -1.0, 0.8],
60
+ "rel_ub": [5.7, 1.6, 2.0, 3.2],
61
+ }
62
+ )
63
+
64
+ (
65
+ ct.CoefTable(experiment, rows="metric", nest="variant", groups="area")
66
+ .estimate("Lift Amount", "att", ci=("att_lb", "att_ub"), fmt=ct.Number(compact=True))
67
+ .estimate("Lift %", "rel", ci=("rel_lb", "rel_ub"), fmt=ct.Percent(signed=True))
68
+ .forest("Lift Plot", of="Lift %", ref=0.0, symmetric=True)
69
+ .header("Experiment Results", "Example Experiment")
70
+ .with_direction({"Latency": "lower_is_better"})
71
+ )
72
+ ```
73
+
74
+ ## Comparing methods
75
+
76
+ Use `split_columns` to compare multiple methods side by side. Each value in the
77
+ split column produces its own set of estimate / forest columns:
78
+
79
+ ```python
80
+ import polars as pl
81
+ import coeftable as ct
82
+
83
+ methods = pl.DataFrame(
84
+ {
85
+ "metric": ["Revenue", "Revenue", "Latency", "Latency"],
86
+ "method": ["A", "B", "A", "B"],
87
+ "est": [3.4, 3.1, 0.5, 0.6],
88
+ "lb": [1.2, 1.0, -1.0, -0.8],
89
+ "ub": [5.7, 5.2, 2.0, 2.1],
90
+ }
91
+ )
92
+
93
+ (
94
+ ct.CoefTable(
95
+ methods, rows="metric", split_columns="method", estimate="est", ci=("lb", "ub")
96
+ )
97
+ .header("Cohort Revenue by Method")
98
+ )
99
+ ```
100
+
101
+ ## Theming
102
+
103
+ Four built-in themes are available from `coeftable.theme`:
104
+
105
+ ```python
106
+ from coeftable.theme import BLUE, COLORBLIND, DEFAULT, MONO, TEXTUAL
107
+
108
+ DEFAULT # Alias for TEXTUAL -- what CoefTable uses if you don't set a theme
109
+ TEXTUAL # Minimal, publication-style: muted colours, light chrome
110
+ BLUE # The original blue-grey palette
111
+ COLORBLIND # Colourblind-safe palette
112
+ MONO # Grayscale for mono journals
113
+ ```
114
+
115
+ Apply one with `.with_theme(...)`:
116
+
117
+ ```python
118
+ table.with_theme(BLUE)
119
+ ```
120
+
121
+ Customise a theme with `dataclasses.replace`:
122
+
123
+ ```python
124
+ from dataclasses import replace
125
+
126
+ my_theme = replace(BLUE, favorable="#0072B2")
127
+ ```
128
+
129
+ Use `with_direction` to mark rows where lower values are favourable (reusing
130
+ the `df` frame from [Quick start](#quick-start)):
131
+
132
+ ```python
133
+ table = (
134
+ ct.CoefTable(df, rows="metric", estimate="est", ci=("lb", "ub"))
135
+ .with_direction({"Latency": "lower_is_better"})
136
+ )
137
+ ```
138
+
139
+ ## Data shape
140
+
141
+ coeftable expects a dataframe where every row is a single comparison. The
142
+ resolution logic maps pairs of upper / lower bound columns to each estimate, so
143
+ your data should be **wide in triples** — one point-estimate column and (when
144
+ applicable) its lower and upper bound columns — rather than in long format with
145
+ a `parameter` column.
146
+
147
+ **Dimensions:**
148
+ - `rows` — the label for each row in the table (e.g. a metric name).
149
+ - `nest` — an optional secondary label stacked below each row.
150
+ - `groups` — an optional column whose values produce section headers.
151
+ - `split_columns` — an optional column whose values produce repeated column
152
+ groups side by side, useful for comparing methods.
Binary file
@@ -0,0 +1,30 @@
1
+ """Test, lint, and typecheck sessions."""
2
+
3
+ import nox
4
+
5
+ nox.options.default_venv_backend = "uv"
6
+ nox.options.reuse_existing_virtualenvs = True
7
+
8
+ PYTHON_VERSIONS = ["3.12", "3.13", "3.14"]
9
+
10
+
11
+ @nox.session(python=PYTHON_VERSIONS)
12
+ def tests(session):
13
+ """Run the test suite against every supported Python version."""
14
+ session.install("-e", ".[dev]")
15
+ session.run("pytest")
16
+
17
+
18
+ @nox.session(python="3.12")
19
+ def lint(session):
20
+ """Check formatting and lint rules."""
21
+ session.install("ruff>=0.15")
22
+ session.run("ruff", "check", "src", "tests")
23
+ session.run("ruff", "format", "--check", "src", "tests")
24
+
25
+
26
+ @nox.session(python="3.12")
27
+ def typecheck(session):
28
+ """Run the static type checker."""
29
+ session.install("-e", ".[dev]")
30
+ session.run("ty", "check", "src", "tests")