coeftable 0.1.0__tar.gz → 0.2.1__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 → coeftable-0.2.1}/.github/workflows/post-release.yml +1 -0
- coeftable-0.2.1/.github/workflows/publish.yml +60 -0
- coeftable-0.2.1/.github/workflows/release.yml +116 -0
- {coeftable-0.1.0 → coeftable-0.2.1}/.gitignore +2 -0
- coeftable-0.2.1/CHANGELOG.md +42 -0
- {coeftable-0.1.0 → coeftable-0.2.1}/PKG-INFO +183 -13
- coeftable-0.2.1/README.md +321 -0
- coeftable-0.2.1/docs/images/trend-example.png +0 -0
- {coeftable-0.1.0 → coeftable-0.2.1}/pyproject.toml +1 -0
- {coeftable-0.1.0 → coeftable-0.2.1}/src/coeftable/__init__.py +14 -1
- {coeftable-0.1.0 → coeftable-0.2.1}/src/coeftable/_version.py +2 -2
- coeftable-0.2.1/src/coeftable/format.py +418 -0
- coeftable-0.2.1/src/coeftable/frame.py +245 -0
- coeftable-0.2.1/src/coeftable/grid.py +268 -0
- {coeftable-0.1.0 → coeftable-0.2.1}/src/coeftable/render.py +4 -4
- coeftable-0.2.1/src/coeftable/series.py +365 -0
- coeftable-0.2.1/src/coeftable/spec.py +1293 -0
- coeftable-0.2.1/src/coeftable/svg.py +1408 -0
- {coeftable-0.1.0 → coeftable-0.2.1}/tests/test_format.py +52 -1
- {coeftable-0.1.0 → coeftable-0.2.1}/tests/test_frame.py +29 -15
- {coeftable-0.1.0 → coeftable-0.2.1}/tests/test_public_api.py +2 -0
- {coeftable-0.1.0 → coeftable-0.2.1}/tests/test_render.py +2 -2
- coeftable-0.2.1/tests/test_series.py +274 -0
- coeftable-0.2.1/tests/test_sparkline.py +842 -0
- {coeftable-0.1.0 → coeftable-0.2.1}/tests/test_spec.py +37 -0
- coeftable-0.2.1/tests/test_svg.py +1506 -0
- {coeftable-0.1.0 → coeftable-0.2.1}/uv.lock +38 -0
- coeftable-0.1.0/.github/workflows/publish.yml +0 -38
- coeftable-0.1.0/README.md +0 -152
- coeftable-0.1.0/src/coeftable/format.py +0 -237
- coeftable-0.1.0/src/coeftable/frame.py +0 -442
- coeftable-0.1.0/src/coeftable/spec.py +0 -437
- coeftable-0.1.0/src/coeftable/svg.py +0 -210
- coeftable-0.1.0/tests/test_svg.py +0 -90
- {coeftable-0.1.0 → coeftable-0.2.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {coeftable-0.1.0 → coeftable-0.2.1}/.github/release.yml +0 -0
- {coeftable-0.1.0 → coeftable-0.2.1}/.github/workflows/ci.yml +0 -0
- {coeftable-0.1.0 → coeftable-0.2.1}/.pre-commit-config.yaml +0 -0
- {coeftable-0.1.0 → coeftable-0.2.1}/LICENSE +0 -0
- {coeftable-0.1.0 → coeftable-0.2.1}/Makefile +0 -0
- {coeftable-0.1.0 → coeftable-0.2.1}/docs/images/example.png +0 -0
- {coeftable-0.1.0 → coeftable-0.2.1}/noxfile.py +0 -0
- {coeftable-0.1.0 → coeftable-0.2.1}/src/coeftable/theme.py +0 -0
- {coeftable-0.1.0 → coeftable-0.2.1}/tests/test_package.py +0 -0
- {coeftable-0.1.0 → coeftable-0.2.1}/tests/test_theme.py +0 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
name: Publish library
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
tags:
|
|
7
|
+
- "v*"
|
|
8
|
+
workflow_call:
|
|
9
|
+
inputs:
|
|
10
|
+
ref:
|
|
11
|
+
description: "Git ref to build. Defaults to the triggering ref."
|
|
12
|
+
required: false
|
|
13
|
+
type: string
|
|
14
|
+
publish:
|
|
15
|
+
description: "Upload the built package to PyPI."
|
|
16
|
+
required: false
|
|
17
|
+
default: false
|
|
18
|
+
type: boolean
|
|
19
|
+
workflow_dispatch:
|
|
20
|
+
|
|
21
|
+
jobs:
|
|
22
|
+
build-package:
|
|
23
|
+
runs-on: ubuntu-latest
|
|
24
|
+
permissions:
|
|
25
|
+
attestations: write
|
|
26
|
+
contents: read
|
|
27
|
+
id-token: write
|
|
28
|
+
steps:
|
|
29
|
+
- uses: actions/checkout@v6
|
|
30
|
+
with:
|
|
31
|
+
# The version is derived from the tag by hatch-vcs, so the build
|
|
32
|
+
# must happen on the tag itself -- building the branch head would
|
|
33
|
+
# produce a development version instead of the release version.
|
|
34
|
+
ref: ${{ inputs.ref }}
|
|
35
|
+
fetch-depth: 0
|
|
36
|
+
persist-credentials: false
|
|
37
|
+
- uses: hynek/build-and-inspect-python-package@v2
|
|
38
|
+
with:
|
|
39
|
+
attest-build-provenance-github: true
|
|
40
|
+
|
|
41
|
+
publish:
|
|
42
|
+
runs-on: ubuntu-latest
|
|
43
|
+
# A tag pushed by a workflow using the default token does not raise a
|
|
44
|
+
# push event, so a release cut by automation asks for the upload
|
|
45
|
+
# explicitly. Any other run sitting on a tag -- a hand-pushed tag, or a
|
|
46
|
+
# manual run selecting one, which is how a stranded release gets
|
|
47
|
+
# published -- is also a release build. A run on a branch never is.
|
|
48
|
+
if: inputs.publish || startsWith(github.ref, 'refs/tags')
|
|
49
|
+
environment:
|
|
50
|
+
name: publish
|
|
51
|
+
url: https://pypi.org/p/coeftable
|
|
52
|
+
needs: build-package
|
|
53
|
+
permissions:
|
|
54
|
+
id-token: write
|
|
55
|
+
steps:
|
|
56
|
+
- uses: actions/download-artifact@v8
|
|
57
|
+
with:
|
|
58
|
+
name: Packages
|
|
59
|
+
path: dist
|
|
60
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
inputs:
|
|
6
|
+
bump:
|
|
7
|
+
description: "Version bump"
|
|
8
|
+
required: true
|
|
9
|
+
type: choice
|
|
10
|
+
options:
|
|
11
|
+
- patch
|
|
12
|
+
- minor
|
|
13
|
+
- major
|
|
14
|
+
default: patch
|
|
15
|
+
version:
|
|
16
|
+
description: "Explicit version override (e.g. 1.2.3) -- takes precedence over bump"
|
|
17
|
+
required: false
|
|
18
|
+
type: string
|
|
19
|
+
|
|
20
|
+
permissions:
|
|
21
|
+
contents: write
|
|
22
|
+
|
|
23
|
+
concurrency:
|
|
24
|
+
group: release
|
|
25
|
+
cancel-in-progress: false
|
|
26
|
+
|
|
27
|
+
jobs:
|
|
28
|
+
tag:
|
|
29
|
+
runs-on: ubuntu-latest
|
|
30
|
+
outputs:
|
|
31
|
+
tag: ${{ steps.version.outputs.tag }}
|
|
32
|
+
steps:
|
|
33
|
+
- uses: actions/checkout@v6
|
|
34
|
+
with:
|
|
35
|
+
fetch-depth: 0
|
|
36
|
+
|
|
37
|
+
- name: Compute next version
|
|
38
|
+
id: version
|
|
39
|
+
env:
|
|
40
|
+
BUMP: ${{ inputs.bump }}
|
|
41
|
+
VERSION_OVERRIDE: ${{ inputs.version }}
|
|
42
|
+
run: |
|
|
43
|
+
set -euo pipefail
|
|
44
|
+
|
|
45
|
+
latest=$(git tag -l 'v*' --sort=-v:refname | sed 's/^v//' | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' | head -n1 || true)
|
|
46
|
+
latest="${latest:-0.0.0}"
|
|
47
|
+
|
|
48
|
+
if [ -n "$VERSION_OVERRIDE" ]; then
|
|
49
|
+
next="${VERSION_OVERRIDE#v}"
|
|
50
|
+
if ! [[ "$next" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
|
51
|
+
echo "::error::Invalid version '$next' -- must be MAJOR.MINOR.PATCH"
|
|
52
|
+
exit 1
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
IFS='.' read -r next_major next_minor next_patch <<< "$next"
|
|
56
|
+
IFS='.' read -r latest_major latest_minor latest_patch <<< "$latest"
|
|
57
|
+
if [ "$((next_major * 1000000 + next_minor * 1000 + next_patch))" -le \
|
|
58
|
+
"$((latest_major * 1000000 + latest_minor * 1000 + latest_patch))" ]; then
|
|
59
|
+
echo "::error::Explicit version '$next' is not greater than the latest release 'v$latest'"
|
|
60
|
+
exit 1
|
|
61
|
+
fi
|
|
62
|
+
else
|
|
63
|
+
IFS='.' read -r major minor patch <<< "$latest"
|
|
64
|
+
case "$BUMP" in
|
|
65
|
+
major) next="$((major + 1)).0.0" ;;
|
|
66
|
+
minor) next="${major}.$((minor + 1)).0" ;;
|
|
67
|
+
patch) next="${major}.${minor}.$((patch + 1))" ;;
|
|
68
|
+
*) echo "::error::Unknown bump type '$BUMP'"; exit 1 ;;
|
|
69
|
+
esac
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
tag="v${next}"
|
|
73
|
+
if git rev-parse "$tag" >/dev/null 2>&1; then
|
|
74
|
+
echo "::error::Tag $tag already exists"
|
|
75
|
+
exit 1
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
echo "Latest: v$latest -> Next: $tag"
|
|
79
|
+
echo "tag=$tag" >> "$GITHUB_OUTPUT"
|
|
80
|
+
|
|
81
|
+
- name: Create and push tag
|
|
82
|
+
run: |
|
|
83
|
+
git config user.name "github-actions[bot]"
|
|
84
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
85
|
+
git tag -a "${{ steps.version.outputs.tag }}" -m "Release ${{ steps.version.outputs.tag }}"
|
|
86
|
+
git push origin "${{ steps.version.outputs.tag }}"
|
|
87
|
+
|
|
88
|
+
- name: Create GitHub release
|
|
89
|
+
env:
|
|
90
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
91
|
+
run: gh release create "${{ steps.version.outputs.tag }}" --generate-notes
|
|
92
|
+
|
|
93
|
+
publish:
|
|
94
|
+
# Called directly rather than left to the tag-push trigger on the publish
|
|
95
|
+
# workflow: the tag above is pushed with the workflow token, and GitHub
|
|
96
|
+
# does not raise workflow-triggering events for it.
|
|
97
|
+
needs: tag
|
|
98
|
+
uses: ./.github/workflows/publish.yml
|
|
99
|
+
permissions:
|
|
100
|
+
attestations: write
|
|
101
|
+
contents: read
|
|
102
|
+
id-token: write
|
|
103
|
+
with:
|
|
104
|
+
ref: ${{ needs.tag.outputs.tag }}
|
|
105
|
+
publish: true
|
|
106
|
+
|
|
107
|
+
changelog:
|
|
108
|
+
# Same reason as the publish job above: the release is created with the
|
|
109
|
+
# workflow token, so its published event never reaches the changelog
|
|
110
|
+
# workflow. Runs after the release exists, since the changelog is built
|
|
111
|
+
# from the published release notes.
|
|
112
|
+
needs: tag
|
|
113
|
+
uses: ./.github/workflows/post-release.yml
|
|
114
|
+
permissions:
|
|
115
|
+
contents: write
|
|
116
|
+
pull-requests: write
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<a id="v0.2.0"></a>
|
|
2
|
+
# [v0.2.0](https://github.com/kylejcaron/coeftable/releases/tag/v0.2.0) - 2026-08-04
|
|
3
|
+
|
|
4
|
+
<!-- Release notes generated using configuration in .github/release.yml at v0.2.0 -->
|
|
5
|
+
|
|
6
|
+
## What's Changed
|
|
7
|
+
### Maintenance
|
|
8
|
+
* ci: add workflow_dispatch release workflow with semver bump by [@kylejcaron](https://github.com/kylejcaron) in [#7](https://github.com/kylejcaron/coeftable/pull/7)
|
|
9
|
+
* Sparkline column: inline line plots with uncertainty by [@kylejcaron](https://github.com/kylejcaron) in [#10](https://github.com/kylejcaron/coeftable/pull/10)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
**Full Changelog**: https://github.com/kylejcaron/coeftable/compare/v0.1.0...v0.2.0
|
|
13
|
+
|
|
14
|
+
[Changes][v0.2.0]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
<a id="v0.1.0"></a>
|
|
18
|
+
# [v0.1.0](https://github.com/kylejcaron/coeftable/releases/tag/v0.1.0) - 2026-07-28
|
|
19
|
+
|
|
20
|
+
<!-- Release notes generated using configuration in .github/release.yml at main -->
|
|
21
|
+
|
|
22
|
+
## What's Changed
|
|
23
|
+
### Maintenance
|
|
24
|
+
* chore: install pre-commit hooks during setup by [@kylejcaron](https://github.com/kylejcaron) in [#1](https://github.com/kylejcaron/coeftable/pull/1)
|
|
25
|
+
* ci: drop redundant lint and typecheck jobs by [@kylejcaron](https://github.com/kylejcaron) in [#2](https://github.com/kylejcaron/coeftable/pull/2)
|
|
26
|
+
* Theming by [@kylejcaron](https://github.com/kylejcaron) in [#3](https://github.com/kylejcaron/coeftable/pull/3)
|
|
27
|
+
* docs patch: restore Parameters section header on Forest docstring by [@kylejcaron](https://github.com/kylejcaron) in [#4](https://github.com/kylejcaron/coeftable/pull/4)
|
|
28
|
+
* chore: complete package metadata for PyPI publishing by [@kylejcaron](https://github.com/kylejcaron) in [#5](https://github.com/kylejcaron/coeftable/pull/5)
|
|
29
|
+
* docs: add hero image, fix stale theming section in README by [@kylejcaron](https://github.com/kylejcaron) in [#6](https://github.com/kylejcaron/coeftable/pull/6)
|
|
30
|
+
|
|
31
|
+
## New Contributors
|
|
32
|
+
* [@kylejcaron](https://github.com/kylejcaron) made their first contribution in [#1](https://github.com/kylejcaron/coeftable/pull/1)
|
|
33
|
+
|
|
34
|
+
**Full Changelog**: https://github.com/kylejcaron/coeftable/commits/v0.1.0
|
|
35
|
+
|
|
36
|
+
[Changes][v0.1.0]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
[v0.2.0]: https://github.com/kylejcaron/coeftable/compare/v0.1.0...v0.2.0
|
|
40
|
+
[v0.1.0]: https://github.com/kylejcaron/coeftable/tree/v0.1.0
|
|
41
|
+
|
|
42
|
+
<!-- Generated by https://github.com/rhysd/changelog-from-release v3.9.1 -->
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: coeftable
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Publication-quality summary tables for estimates with uncertainty.
|
|
5
5
|
Project-URL: Homepage, https://github.com/kylejcaron/coeftable
|
|
6
6
|
Project-URL: Repository, https://github.com/kylejcaron/coeftable
|
|
@@ -45,6 +45,7 @@ Requires-Dist: nox>=2025.5; extra == 'dev'
|
|
|
45
45
|
Requires-Dist: pandas>=2.2; extra == 'dev'
|
|
46
46
|
Requires-Dist: polars>=1.0; extra == 'dev'
|
|
47
47
|
Requires-Dist: prek>=0.4.5; extra == 'dev'
|
|
48
|
+
Requires-Dist: pyarrow>=25.0.0; extra == 'dev'
|
|
48
49
|
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
49
50
|
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
50
51
|
Requires-Dist: ruff>=0.15; extra == 'dev'
|
|
@@ -91,6 +92,31 @@ leave it as the last expression in a cell, no extra call needed. Outside a noteb
|
|
|
91
92
|
object: `table.gt().as_raw_html()` for an HTML string, `table.gt().save("t.png")` for an
|
|
92
93
|
image, `table.gt().tab_options(...)` to keep styling with great_tables' own API.
|
|
93
94
|
|
|
95
|
+
## Data shape
|
|
96
|
+
|
|
97
|
+
coeftable expects a dataframe where every row is a single comparison. The
|
|
98
|
+
resolution logic maps pairs of upper / lower bound columns to each estimate, so
|
|
99
|
+
your data should be **wide in triples** — one point-estimate column and (when
|
|
100
|
+
applicable) its lower and upper bound columns — rather than in long format with
|
|
101
|
+
a `parameter` column.
|
|
102
|
+
|
|
103
|
+
**Dimensions:**
|
|
104
|
+
- `rows` — the label for each row in the table (e.g. a metric name).
|
|
105
|
+
- `nest` — an optional secondary label stacked below each row.
|
|
106
|
+
- `groups` — an optional column whose values produce section headers.
|
|
107
|
+
- `split_columns` — an optional column whose values produce repeated column
|
|
108
|
+
groups side by side, useful for comparing methods.
|
|
109
|
+
|
|
110
|
+
**Series columns bend this rule.** A point estimate is one number (plus
|
|
111
|
+
bounds), so a triple of scalar columns holds it. A `.sparkline(...)` series
|
|
112
|
+
is N points, not one -- most naturally via the companion-frame door, a
|
|
113
|
+
separate long frame with one row per point, joined by the table's row/nest/
|
|
114
|
+
split keys. Or, when the series is already collapsed onto its row, its
|
|
115
|
+
`value` / `ci` columns can instead hold a *list* per row directly. Either
|
|
116
|
+
way a row of the table is still one row; the series column just carries
|
|
117
|
+
more data per row than an estimate column does. See
|
|
118
|
+
[Trend over time](#trend-over-time) for both shapes.
|
|
119
|
+
|
|
94
120
|
## Experiment table
|
|
95
121
|
|
|
96
122
|
Build a complete experiment results table with multiple estimates, a forest
|
|
@@ -189,17 +215,161 @@ table = (
|
|
|
189
215
|
)
|
|
190
216
|
```
|
|
191
217
|
|
|
192
|
-
##
|
|
218
|
+
## Trend over time
|
|
193
219
|
|
|
194
|
-
|
|
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.
|
|
220
|
+

|
|
199
221
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
222
|
+
Add a `.sparkline(...)` column to plot a metric's trajectory next to its
|
|
223
|
+
point estimate: an inline SVG line with a shaded credible interval and a
|
|
224
|
+
dashed reference line (pass `show_endpoint=True` to also label the last
|
|
225
|
+
value). Below, `ref=0.0` draws the reference line that Latency's series
|
|
226
|
+
crosses as its credible interval narrows over three weeks of data:
|
|
227
|
+
|
|
228
|
+
There are two front doors for the series data, the same list-columns vs.
|
|
229
|
+
companion-frame choice used elsewhere in coeftable:
|
|
230
|
+
|
|
231
|
+
**A long companion frame** — the shape most real series data already
|
|
232
|
+
arrives in: a SQL export, a dbt model, an experimentation platform's daily
|
|
233
|
+
metrics table. Pass `data=` a separate frame with one row per point, and
|
|
234
|
+
`value` / `ci` / `x` name *scalar* columns on it. coeftable groups the
|
|
235
|
+
companion frame by the table's `rows` (+ `nest`, + `split_columns`) keys
|
|
236
|
+
and collapses each group into a series internally:
|
|
237
|
+
|
|
238
|
+
```python
|
|
239
|
+
import datetime as dt
|
|
240
|
+
import pandas as pd
|
|
241
|
+
import polars as pl
|
|
242
|
+
import coeftable as ct
|
|
243
|
+
|
|
244
|
+
dates = [dt.date(2024, 1, 1), dt.date(2024, 1, 8), dt.date(2024, 1, 15)]
|
|
245
|
+
|
|
246
|
+
trend = pl.DataFrame(
|
|
247
|
+
{
|
|
248
|
+
"metric": ["Revenue", "Latency"],
|
|
249
|
+
"lift": [3.4, 0.5],
|
|
250
|
+
"lift_lb": [1.2, -1.0],
|
|
251
|
+
"lift_ub": [5.7, 2.0],
|
|
252
|
+
}
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
history = pd.DataFrame(
|
|
256
|
+
{
|
|
257
|
+
"metric": ["Revenue", "Revenue", "Revenue", "Latency", "Latency", "Latency"],
|
|
258
|
+
"date": dates + dates,
|
|
259
|
+
"lift": [1.5, 2.4, 3.4, -1.0, 0.2, 1.5],
|
|
260
|
+
"lift_lb": [0.3, 1.4, 2.6, -2.5, -0.6, 1.0],
|
|
261
|
+
"lift_ub": [2.7, 3.4, 4.2, 0.5, 1.0, 2.0],
|
|
262
|
+
}
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
(
|
|
266
|
+
ct.CoefTable(trend, rows="metric")
|
|
267
|
+
.estimate("Lift %", "lift", ci=("lift_lb", "lift_ub"), fmt=ct.Percent(signed=True))
|
|
268
|
+
.sparkline(
|
|
269
|
+
"Trend",
|
|
270
|
+
value="lift",
|
|
271
|
+
ci=("lift_lb", "lift_ub"),
|
|
272
|
+
x="date",
|
|
273
|
+
data=history,
|
|
274
|
+
ref=0.0,
|
|
275
|
+
axis_fmt=ct.DateAxis(),
|
|
276
|
+
)
|
|
277
|
+
)
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
**List columns on the main frame** — if the series is already collapsed
|
|
281
|
+
onto its row (e.g. from a prior `.group_by(...).agg(...)`, or a source that
|
|
282
|
+
natively stores arrays), `value` / `ci` / `x` can instead name columns
|
|
283
|
+
whose cells each hold one list of points per row:
|
|
284
|
+
|
|
285
|
+
```python
|
|
286
|
+
import datetime as dt
|
|
287
|
+
import polars as pl
|
|
288
|
+
import coeftable as ct
|
|
289
|
+
|
|
290
|
+
dates = [dt.date(2024, 1, 1), dt.date(2024, 1, 8), dt.date(2024, 1, 15)]
|
|
291
|
+
|
|
292
|
+
trend = pl.DataFrame(
|
|
293
|
+
{
|
|
294
|
+
"metric": ["Revenue", "Latency"],
|
|
295
|
+
"lift": [3.4, 0.5],
|
|
296
|
+
"lift_lb": [1.2, -1.0],
|
|
297
|
+
"lift_ub": [5.7, 2.0],
|
|
298
|
+
"history": [
|
|
299
|
+
[1.5, 2.4, 3.4],
|
|
300
|
+
[-1.0, 0.2, 1.5],
|
|
301
|
+
],
|
|
302
|
+
"history_lb": [
|
|
303
|
+
[0.3, 1.4, 2.6],
|
|
304
|
+
[-2.5, -0.6, 1.0],
|
|
305
|
+
],
|
|
306
|
+
"history_ub": [
|
|
307
|
+
[2.7, 3.4, 4.2],
|
|
308
|
+
[0.5, 1.0, 2.0],
|
|
309
|
+
],
|
|
310
|
+
"date": [dates, dates],
|
|
311
|
+
}
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
(
|
|
315
|
+
ct.CoefTable(trend, rows="metric")
|
|
316
|
+
.estimate("Lift %", "lift", ci=("lift_lb", "lift_ub"), fmt=ct.Percent(signed=True))
|
|
317
|
+
.sparkline(
|
|
318
|
+
"Trend",
|
|
319
|
+
value="history",
|
|
320
|
+
ci=("history_lb", "history_ub"),
|
|
321
|
+
x="date",
|
|
322
|
+
ref=0.0,
|
|
323
|
+
axis_fmt=ct.DateAxis(),
|
|
324
|
+
)
|
|
325
|
+
)
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
Both render the same column. Reach for the companion frame first — it
|
|
329
|
+
matches how most series data actually arrives, one row per observation.
|
|
330
|
+
Reach for list columns only when the series is already collapsed onto its
|
|
331
|
+
row.
|
|
332
|
+
|
|
333
|
+
Since `x` is always shared table-wide (dates must line up across rows), a
|
|
334
|
+
series with fewer points than its neighbours visibly occupies only part of
|
|
335
|
+
its cell's width rather than stretching to fill it — this is intentional,
|
|
336
|
+
not a bug: `x` position reflects where a point falls in the shared domain,
|
|
337
|
+
never the row's own extent.
|
|
338
|
+
|
|
339
|
+
Want a plain trend line with no uncertainty band — no `ci`, no ribbon?
|
|
340
|
+
great_tables' own `.gt().fmt_nanoplot(...)` covers that directly.
|
|
341
|
+
`.sparkline(...)` exists specifically for the estimate-with-interval case.
|
|
342
|
+
|
|
343
|
+
**Shaping the y-axis.** Each row's domain fits tightly to its own data by
|
|
344
|
+
default (`scale="row"`, `autoscale="tight"`). Four ways to change that,
|
|
345
|
+
shown together against the same noisy series:
|
|
346
|
+
|
|
347
|
+
```python
|
|
348
|
+
import polars as pl
|
|
349
|
+
import coeftable as ct
|
|
350
|
+
|
|
351
|
+
trend = pl.DataFrame(
|
|
352
|
+
{
|
|
353
|
+
"metric": ["Revenue"],
|
|
354
|
+
"lift": [[1.0, 1.05, 0.95, 1.02, 0.98, 300.0]],
|
|
355
|
+
}
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
(
|
|
359
|
+
ct.CoefTable(trend, rows="metric")
|
|
360
|
+
# Default: fits tightly to this row's own min/max. A single outlier
|
|
361
|
+
# like the 300.0 here dominates and flattens the rest of the series.
|
|
362
|
+
.sparkline("Tight (default)", value="lift", ref=1.0)
|
|
363
|
+
# autoscale="robust" fits an IQR/Tukey fence instead of raw min/max,
|
|
364
|
+
# so the outlier doesn't flatten the rest. It still draws -- clipped
|
|
365
|
+
# to the domain edge and flagged with a clip-cap marker, never hidden.
|
|
366
|
+
.sparkline("Robust", value="lift", ref=1.0, autoscale="robust")
|
|
367
|
+
# max_ylim=N narrows whatever domain scale/autoscale would have
|
|
368
|
+
# produced -- clamping to `ref +/- N`, only if the natural domain
|
|
369
|
+
# would have exceeded that ceiling. Composes with autoscale.
|
|
370
|
+
.sparkline("Ceiling", value="lift", ref=1.0, max_ylim=0.5)
|
|
371
|
+
# ylim=(lo, hi) is an absolute override, replacing scale/autoscale/
|
|
372
|
+
# max_ylim entirely.
|
|
373
|
+
.sparkline("Override", value="lift", ref=1.0, ylim=(0.9, 1.1))
|
|
374
|
+
)
|
|
375
|
+
```
|