coeftable 0.1.0__py3-none-any.whl

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/theme.py ADDED
@@ -0,0 +1,187 @@
1
+ """Colour roles, direction semantics and table chrome."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ from dataclasses import dataclass
7
+ from typing import Literal
8
+
9
+ type Role = Literal["favorable", "unfavorable", "inconclusive", "neutral"]
10
+ type Direction = Literal["higher_is_better", "lower_is_better", "neutral"]
11
+ type ColorRule = Callable[[float | None, float | None, float | None, float], Role]
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class Theme:
16
+ """Colour, typography and chrome slots for a table.
17
+
18
+ Roles are named by meaning rather than by colour so that a palette can be
19
+ swapped without the calling code implying a value judgement.
20
+
21
+ Parameters
22
+ ----------
23
+ favorable, unfavorable, inconclusive, neutral
24
+ Colours for the four semantic roles.
25
+ header_bg, header_fg, column_label_bg
26
+ Title, subtitle and column-label chrome.
27
+ band
28
+ Fill for alternating row-key blocks.
29
+ surface
30
+ Table background; also the colour of the estimate tick inside a bar.
31
+ rule, axis, muted, text
32
+ Divider, axis, secondary-text and body-text colours.
33
+ value_size, ci_size, table_font_size
34
+ CSS font sizes.
35
+ na_text
36
+ Text substituted for a missing estimate.
37
+ border_style
38
+ ``"boxed"`` draws a solid rule on all four table sides (the
39
+ default). ``"minimal"`` drops the left and right rules for a more
40
+ textual, publication-style layout with only top and bottom rules.
41
+ border_color
42
+ Colour used for structural table chrome: the heading divider,
43
+ column-label rules, row-group borders, and the table frame
44
+ (including the table-body top rule). If `None` (the default),
45
+ ``header_bg`` is reused so that these borders and the title
46
+ banner share a colour. Set this independently to keep the title
47
+ banner light while still drawing visible chrome rules. Does
48
+ *not* affect ``rule``, which styles lighter dividers within the
49
+ body-row region (nest-key separators, forest-plot axis lines).
50
+ """
51
+
52
+ favorable: str = "#55A868"
53
+ unfavorable: str = "#C44E52"
54
+ inconclusive: str = "#8C8C8C"
55
+ neutral: str = "#4C72B0"
56
+
57
+ header_bg: str = "#4C72B0"
58
+ header_fg: str = "#FFFFFF"
59
+ column_label_bg: str = "#8FA9CE"
60
+ band: str = "#F2F5FA"
61
+ surface: str = "#FFFFFF"
62
+ rule: str = "#C7C8CD"
63
+ axis: str = "#72767E"
64
+ muted: str = "#72767E"
65
+ text: str = "#343538"
66
+
67
+ value_size: str = "15px"
68
+ ci_size: str = "11px"
69
+ table_font_size: str = "16px"
70
+ na_text: str = "\u2014"
71
+ border_style: Literal["boxed", "minimal"] = "boxed"
72
+ border_color: str | None = None
73
+
74
+ def color(self, role: Role) -> str:
75
+ """Return the colour registered for `role`.
76
+
77
+ Parameters
78
+ ----------
79
+ role
80
+ Semantic role.
81
+
82
+ Returns
83
+ -------
84
+ str
85
+ Hex colour string.
86
+ """
87
+ match role:
88
+ case "favorable":
89
+ return self.favorable
90
+ case "unfavorable":
91
+ return self.unfavorable
92
+ case "inconclusive":
93
+ return self.inconclusive
94
+ case "neutral":
95
+ return self.neutral
96
+ case _:
97
+ raise ValueError(f"Unknown role: {role!r}")
98
+
99
+
100
+ def role_for(
101
+ lower: float | None,
102
+ upper: float | None,
103
+ ref: float,
104
+ direction: Direction,
105
+ ) -> Role:
106
+ """Map an interval to a semantic role.
107
+
108
+ An interval lying entirely on one side of `ref` is favorable or unfavorable
109
+ according to `direction`; one that spans `ref`, or that is unbounded on the
110
+ deciding side, is inconclusive. A `direction` of ``"neutral"`` always yields
111
+ ``"neutral"``, so a table making no directional claim does not look like a
112
+ table full of null results.
113
+
114
+ Parameters
115
+ ----------
116
+ lower, upper
117
+ Interval bounds. `None` means unbounded on that side.
118
+ ref
119
+ Reference value the interval is compared against.
120
+ direction
121
+ Which side of `ref` counts as favorable.
122
+
123
+ Returns
124
+ -------
125
+ Role
126
+ The resolved role.
127
+ """
128
+ if direction == "neutral":
129
+ return "neutral"
130
+ if lower is not None and lower > ref:
131
+ return "favorable" if direction == "higher_is_better" else "unfavorable"
132
+ if upper is not None and upper < ref:
133
+ return "unfavorable" if direction == "higher_is_better" else "favorable"
134
+ return "inconclusive"
135
+
136
+
137
+ BLUE = Theme()
138
+
139
+ COLORBLIND = Theme(
140
+ favorable="#0072B2",
141
+ unfavorable="#D55E00",
142
+ inconclusive="#999999",
143
+ neutral="#0072B2",
144
+ header_bg="#0072B2",
145
+ column_label_bg="#7FB8DC",
146
+ band="#EEF5FA",
147
+ )
148
+
149
+ MONO = Theme(
150
+ # Grayscale-only: direction is never colour-coded (favorable and
151
+ # unfavorable share a shade), but significance is. A result whose
152
+ # interval clears the reference (favorable/unfavorable) renders in a
153
+ # dark, high-contrast gray; an inconclusive result is rendered in a
154
+ # light gray to visually recede; neutral (no directional claim) sits
155
+ # in between.
156
+ favorable="#2B2B2B",
157
+ unfavorable="#2B2B2B",
158
+ inconclusive="#B0B0B0",
159
+ neutral="#6E6E6E",
160
+ header_bg="#343538",
161
+ column_label_bg="#72767E",
162
+ band="#F4F4F4",
163
+ )
164
+
165
+ TEXTUAL = Theme(
166
+ # A quieter, publication-style theme: a muted palette, a light title
167
+ # banner, no row banding, and borders confined to the top and bottom
168
+ # rules -- closer to a printed table than a dashboard.
169
+ favorable="#2E7D32",
170
+ unfavorable="#C62828",
171
+ inconclusive="#9E9E9E",
172
+ neutral="#455A64",
173
+ header_bg="#F5F5F5",
174
+ header_fg="#1A1A1A",
175
+ column_label_bg="#FAFAFA",
176
+ band="#FFFFFF",
177
+ surface="#FFFFFF",
178
+ rule="#E0E0E0",
179
+ axis="#616161",
180
+ muted="#757575",
181
+ text="#212121",
182
+ border_style="minimal",
183
+ border_color="#A8A8A8",
184
+ )
185
+
186
+
187
+ DEFAULT = TEXTUAL
@@ -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,12 @@
1
+ coeftable/__init__.py,sha256=Te3yTkSGN6qw21tEIDUSgrRFIQOqgAhjm_EiMPqxHyQ,2101
2
+ coeftable/_version.py,sha256=n_5vdJsPNu7wZ57LGuRL585uvll-hiuvZUBWzdG0RQU,520
3
+ coeftable/format.py,sha256=dVB0ZS4jSVPzSeUqAHU14xnzdkbCRoQOLO5TUNAwYjM,6402
4
+ coeftable/frame.py,sha256=CLomTQYWx39PqnQZkOjN-w9wrxyvwQzlNTOPef-Aygk,16798
5
+ coeftable/render.py,sha256=dQghm3DNZaqlrx_zmrXSBbkxoPzHysDaB821kpsVBgk,5781
6
+ coeftable/spec.py,sha256=sTa4qZsiD5g8XlJGPAfWPewBBSbIos_qQ_KuFFJn7qI,12407
7
+ coeftable/svg.py,sha256=nQOSzRzYLT2fQ6V0aRy-SJqpePhDPXle9RbbrEF0m_I,6375
8
+ coeftable/theme.py,sha256=h865hbxC37LPnzuOs0Ss7OOc8pkddSYavSViu-l1U5w,5846
9
+ coeftable-0.1.0.dist-info/METADATA,sha256=WoVh8mvBviU5DZwC6jdNGGnPMpHzw70SYvNBKo5aqEQ,7174
10
+ coeftable-0.1.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
11
+ coeftable-0.1.0.dist-info/licenses/LICENSE,sha256=KWGy7AY1cdpA9QiaSSaJXX0qWHpMwQtkkIcw5hw_nts,1067
12
+ coeftable-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.31.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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.