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.
- coeftable-0.1.0/.github/PULL_REQUEST_TEMPLATE.md +3 -0
- coeftable-0.1.0/.github/release.yml +10 -0
- coeftable-0.1.0/.github/workflows/ci.yml +33 -0
- coeftable-0.1.0/.github/workflows/post-release.yml +24 -0
- coeftable-0.1.0/.github/workflows/publish.yml +38 -0
- coeftable-0.1.0/.gitignore +18 -0
- coeftable-0.1.0/.pre-commit-config.yaml +28 -0
- coeftable-0.1.0/LICENSE +21 -0
- coeftable-0.1.0/Makefile +20 -0
- coeftable-0.1.0/PKG-INFO +205 -0
- coeftable-0.1.0/README.md +152 -0
- coeftable-0.1.0/docs/images/example.png +0 -0
- coeftable-0.1.0/noxfile.py +30 -0
- coeftable-0.1.0/pyproject.toml +102 -0
- coeftable-0.1.0/src/coeftable/__init__.py +66 -0
- coeftable-0.1.0/src/coeftable/_version.py +24 -0
- coeftable-0.1.0/src/coeftable/format.py +237 -0
- coeftable-0.1.0/src/coeftable/frame.py +442 -0
- coeftable-0.1.0/src/coeftable/render.py +153 -0
- coeftable-0.1.0/src/coeftable/spec.py +437 -0
- coeftable-0.1.0/src/coeftable/svg.py +210 -0
- coeftable-0.1.0/src/coeftable/theme.py +187 -0
- coeftable-0.1.0/tests/test_format.py +111 -0
- coeftable-0.1.0/tests/test_frame.py +297 -0
- coeftable-0.1.0/tests/test_package.py +16 -0
- coeftable-0.1.0/tests/test_public_api.py +112 -0
- coeftable-0.1.0/tests/test_render.py +142 -0
- coeftable-0.1.0/tests/test_spec.py +133 -0
- coeftable-0.1.0/tests/test_svg.py +90 -0
- coeftable-0.1.0/tests/test_theme.py +120 -0
- coeftable-0.1.0/uv.lock +862 -0
|
@@ -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
|
coeftable-0.1.0/LICENSE
ADDED
|
@@ -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.
|
coeftable-0.1.0/Makefile
ADDED
|
@@ -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
|
coeftable-0.1.0/PKG-INFO
ADDED
|
@@ -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
|
+

|
|
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
|
+

|
|
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")
|