panelcast 0.14.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.
Files changed (150) hide show
  1. panelcast-0.14.0/LICENSE +21 -0
  2. panelcast-0.14.0/PKG-INFO +316 -0
  3. panelcast-0.14.0/README.md +261 -0
  4. panelcast-0.14.0/pyproject.toml +163 -0
  5. panelcast-0.14.0/setup.cfg +4 -0
  6. panelcast-0.14.0/src/panelcast/__init__.py +73 -0
  7. panelcast-0.14.0/src/panelcast/_data/datasets/aero.yaml +82 -0
  8. panelcast-0.14.0/src/panelcast/_data/datasets/aoty_full.yaml +37 -0
  9. panelcast-0.14.0/src/panelcast/_data/examples/aerospace/descriptor.yaml +82 -0
  10. panelcast-0.14.0/src/panelcast/_data/examples/aerospace/flights.csv +40 -0
  11. panelcast-0.14.0/src/panelcast/cli/__init__.py +51 -0
  12. panelcast-0.14.0/src/panelcast/cli/backtest_cmd.py +94 -0
  13. panelcast-0.14.0/src/panelcast/cli/commands.py +814 -0
  14. panelcast-0.14.0/src/panelcast/cli/doctor_cmd.py +61 -0
  15. panelcast-0.14.0/src/panelcast/cli/main.py +66 -0
  16. panelcast-0.14.0/src/panelcast/cli/preflight_cmd.py +101 -0
  17. panelcast-0.14.0/src/panelcast/cli/run.py +1000 -0
  18. panelcast-0.14.0/src/panelcast/cli/runs_cmd.py +528 -0
  19. panelcast-0.14.0/src/panelcast/cli/select_cmd.py +238 -0
  20. panelcast-0.14.0/src/panelcast/cli/stack_cmd.py +62 -0
  21. panelcast-0.14.0/src/panelcast/cli/stages.py +350 -0
  22. panelcast-0.14.0/src/panelcast/config/descriptor.py +404 -0
  23. panelcast-0.14.0/src/panelcast/config/gates.py +47 -0
  24. panelcast-0.14.0/src/panelcast/config/loader.py +70 -0
  25. panelcast-0.14.0/src/panelcast/config/pipeline_yaml.py +328 -0
  26. panelcast-0.14.0/src/panelcast/data/alignment.py +95 -0
  27. panelcast-0.14.0/src/panelcast/data/chronology.py +87 -0
  28. panelcast-0.14.0/src/panelcast/data/cleaning.py +541 -0
  29. panelcast-0.14.0/src/panelcast/data/imputation.py +82 -0
  30. panelcast-0.14.0/src/panelcast/data/ingest.py +196 -0
  31. panelcast-0.14.0/src/panelcast/data/lineage.py +210 -0
  32. panelcast-0.14.0/src/panelcast/data/manifests.py +205 -0
  33. panelcast-0.14.0/src/panelcast/data/split.py +308 -0
  34. panelcast-0.14.0/src/panelcast/data/split_types.py +99 -0
  35. panelcast-0.14.0/src/panelcast/data/validation.py +244 -0
  36. panelcast-0.14.0/src/panelcast/doctor.py +178 -0
  37. panelcast-0.14.0/src/panelcast/evaluation/__init__.py +62 -0
  38. panelcast-0.14.0/src/panelcast/evaluation/calibration.py +619 -0
  39. panelcast-0.14.0/src/panelcast/evaluation/conformal.py +137 -0
  40. panelcast-0.14.0/src/panelcast/evaluation/cv.py +394 -0
  41. panelcast-0.14.0/src/panelcast/evaluation/decomposition.py +112 -0
  42. panelcast-0.14.0/src/panelcast/evaluation/metrics.py +355 -0
  43. panelcast-0.14.0/src/panelcast/evaluation/ppc.py +205 -0
  44. panelcast-0.14.0/src/panelcast/evaluation/prior_predictive.py +384 -0
  45. panelcast-0.14.0/src/panelcast/evaluation/ranking.py +132 -0
  46. panelcast-0.14.0/src/panelcast/evaluation/slices.py +223 -0
  47. panelcast-0.14.0/src/panelcast/features/__init__.py +24 -0
  48. panelcast-0.14.0/src/panelcast/features/album_type.py +140 -0
  49. panelcast-0.14.0/src/panelcast/features/artist.py +56 -0
  50. panelcast-0.14.0/src/panelcast/features/base.py +208 -0
  51. panelcast-0.14.0/src/panelcast/features/basis.py +141 -0
  52. panelcast-0.14.0/src/panelcast/features/collaboration.py +158 -0
  53. panelcast-0.14.0/src/panelcast/features/core.py +115 -0
  54. panelcast-0.14.0/src/panelcast/features/errors.py +107 -0
  55. panelcast-0.14.0/src/panelcast/features/gbm_offset.py +324 -0
  56. panelcast-0.14.0/src/panelcast/features/genre.py +211 -0
  57. panelcast-0.14.0/src/panelcast/features/history.py +181 -0
  58. panelcast-0.14.0/src/panelcast/features/packs/__init__.py +7 -0
  59. panelcast-0.14.0/src/panelcast/features/packs/aoty.py +26 -0
  60. panelcast-0.14.0/src/panelcast/features/pca.py +5 -0
  61. panelcast-0.14.0/src/panelcast/features/pipeline.py +185 -0
  62. panelcast-0.14.0/src/panelcast/features/registry.py +125 -0
  63. panelcast-0.14.0/src/panelcast/features/temporal.py +172 -0
  64. panelcast-0.14.0/src/panelcast/gpu_memory/__init__.py +48 -0
  65. panelcast-0.14.0/src/panelcast/gpu_memory/admission.py +83 -0
  66. panelcast-0.14.0/src/panelcast/gpu_memory/calibration_store.py +262 -0
  67. panelcast-0.14.0/src/panelcast/gpu_memory/estimate.py +248 -0
  68. panelcast-0.14.0/src/panelcast/gpu_memory/measure.py +129 -0
  69. panelcast-0.14.0/src/panelcast/gpu_memory/platform.py +134 -0
  70. panelcast-0.14.0/src/panelcast/gpu_memory/query.py +194 -0
  71. panelcast-0.14.0/src/panelcast/gpu_memory/runtime_predictor.py +160 -0
  72. panelcast-0.14.0/src/panelcast/io/paths.py +27 -0
  73. panelcast-0.14.0/src/panelcast/io/readers.py +20 -0
  74. panelcast-0.14.0/src/panelcast/io/writers.py +9 -0
  75. panelcast-0.14.0/src/panelcast/model_preflight.py +385 -0
  76. panelcast-0.14.0/src/panelcast/model_preflight_data.py +125 -0
  77. panelcast-0.14.0/src/panelcast/models/__init__.py +1 -0
  78. panelcast-0.14.0/src/panelcast/models/baselines/__init__.py +37 -0
  79. panelcast-0.14.0/src/panelcast/models/baselines/core.py +480 -0
  80. panelcast-0.14.0/src/panelcast/models/bayes/__init__.py +72 -0
  81. panelcast-0.14.0/src/panelcast/models/bayes/diagnostics.py +450 -0
  82. panelcast-0.14.0/src/panelcast/models/bayes/fit.py +827 -0
  83. panelcast-0.14.0/src/panelcast/models/bayes/io.py +382 -0
  84. panelcast-0.14.0/src/panelcast/models/bayes/likelihoods.py +835 -0
  85. panelcast-0.14.0/src/panelcast/models/bayes/model.py +1035 -0
  86. panelcast-0.14.0/src/panelcast/models/bayes/model_math.py +43 -0
  87. panelcast-0.14.0/src/panelcast/models/bayes/predict.py +615 -0
  88. panelcast-0.14.0/src/panelcast/models/bayes/priors.py +350 -0
  89. panelcast-0.14.0/src/panelcast/models/bayes/rollout.py +214 -0
  90. panelcast-0.14.0/src/panelcast/models/bayes/transforms.py +126 -0
  91. panelcast-0.14.0/src/panelcast/paths.py +107 -0
  92. panelcast-0.14.0/src/panelcast/pipelines/__init__.py +67 -0
  93. panelcast-0.14.0/src/panelcast/pipelines/backtest.py +340 -0
  94. panelcast-0.14.0/src/panelcast/pipelines/build_features.py +496 -0
  95. panelcast-0.14.0/src/panelcast/pipelines/compare_baselines.py +390 -0
  96. panelcast-0.14.0/src/panelcast/pipelines/create_splits.py +425 -0
  97. panelcast-0.14.0/src/panelcast/pipelines/diagnose.py +257 -0
  98. panelcast-0.14.0/src/panelcast/pipelines/errors.py +194 -0
  99. panelcast-0.14.0/src/panelcast/pipelines/evaluate.py +2087 -0
  100. panelcast-0.14.0/src/panelcast/pipelines/manifest.py +367 -0
  101. panelcast-0.14.0/src/panelcast/pipelines/orchestrator.py +1958 -0
  102. panelcast-0.14.0/src/panelcast/pipelines/predict_next.py +929 -0
  103. panelcast-0.14.0/src/panelcast/pipelines/prepare_dataset.py +287 -0
  104. panelcast-0.14.0/src/panelcast/pipelines/publication.py +1296 -0
  105. panelcast-0.14.0/src/panelcast/pipelines/sensitivity.py +1340 -0
  106. panelcast-0.14.0/src/panelcast/pipelines/stages.py +807 -0
  107. panelcast-0.14.0/src/panelcast/pipelines/stamps.py +84 -0
  108. panelcast-0.14.0/src/panelcast/pipelines/train_bayes.py +1876 -0
  109. panelcast-0.14.0/src/panelcast/pipelines/training_summary.py +198 -0
  110. panelcast-0.14.0/src/panelcast/preflight/__init__.py +236 -0
  111. panelcast-0.14.0/src/panelcast/preflight/cache.py +177 -0
  112. panelcast-0.14.0/src/panelcast/preflight/calibrate.py +249 -0
  113. panelcast-0.14.0/src/panelcast/preflight/check.py +250 -0
  114. panelcast-0.14.0/src/panelcast/preflight/full_check.py +737 -0
  115. panelcast-0.14.0/src/panelcast/preflight/mini_run.py +407 -0
  116. panelcast-0.14.0/src/panelcast/preflight/output.py +240 -0
  117. panelcast-0.14.0/src/panelcast/py.typed +0 -0
  118. panelcast-0.14.0/src/panelcast/reporting/__init__.py +57 -0
  119. panelcast-0.14.0/src/panelcast/reporting/curves.py +166 -0
  120. panelcast-0.14.0/src/panelcast/reporting/figures.py +1181 -0
  121. panelcast-0.14.0/src/panelcast/reporting/html_report.py +219 -0
  122. panelcast-0.14.0/src/panelcast/reporting/model_card.py +1144 -0
  123. panelcast-0.14.0/src/panelcast/reporting/tables.py +667 -0
  124. panelcast-0.14.0/src/panelcast/select/__init__.py +34 -0
  125. panelcast-0.14.0/src/panelcast/select/confirmation.py +373 -0
  126. panelcast-0.14.0/src/panelcast/select/orchestrate.py +510 -0
  127. panelcast-0.14.0/src/panelcast/select/prior_screen.py +198 -0
  128. panelcast-0.14.0/src/panelcast/select/rules.py +200 -0
  129. panelcast-0.14.0/src/panelcast/select/runner.py +1130 -0
  130. panelcast-0.14.0/src/panelcast/select/scoring.py +424 -0
  131. panelcast-0.14.0/src/panelcast/select/space.py +453 -0
  132. panelcast-0.14.0/src/panelcast/select/stacking.py +499 -0
  133. panelcast-0.14.0/src/panelcast/select/tiers.py +219 -0
  134. panelcast-0.14.0/src/panelcast/utils/environment.py +150 -0
  135. panelcast-0.14.0/src/panelcast/utils/git_state.py +177 -0
  136. panelcast-0.14.0/src/panelcast/utils/hashing.py +93 -0
  137. panelcast-0.14.0/src/panelcast/utils/jax_cache.py +57 -0
  138. panelcast-0.14.0/src/panelcast/utils/logging.py +152 -0
  139. panelcast-0.14.0/src/panelcast/utils/random.py +69 -0
  140. panelcast-0.14.0/src/panelcast/visualization/__init__.py +68 -0
  141. panelcast-0.14.0/src/panelcast/visualization/charts.py +535 -0
  142. panelcast-0.14.0/src/panelcast/visualization/dashboard.py +612 -0
  143. panelcast-0.14.0/src/panelcast/visualization/export.py +306 -0
  144. panelcast-0.14.0/src/panelcast/visualization/theme.py +101 -0
  145. panelcast-0.14.0/src/panelcast.egg-info/PKG-INFO +316 -0
  146. panelcast-0.14.0/src/panelcast.egg-info/SOURCES.txt +148 -0
  147. panelcast-0.14.0/src/panelcast.egg-info/dependency_links.txt +1 -0
  148. panelcast-0.14.0/src/panelcast.egg-info/entry_points.txt +2 -0
  149. panelcast-0.14.0/src/panelcast.egg-info/requires.txt +33 -0
  150. panelcast-0.14.0/src/panelcast.egg-info/top_level.txt +1 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jack Wenenn
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,316 @@
1
+ Metadata-Version: 2.4
2
+ Name: panelcast
3
+ Version: 0.14.0
4
+ Summary: Hierarchical Bayesian prediction for bounded scores of events nested in entities over time, configured by a YAML descriptor
5
+ Author-email: Jack Wenenn <jcwenenn@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/cupidthatbtc/panelcast
8
+ Project-URL: Repository, https://github.com/cupidthatbtc/panelcast
9
+ Project-URL: Issues, https://github.com/cupidthatbtc/panelcast/issues
10
+ Keywords: bayesian,numpyro,jax,hierarchical,forecasting,panel-data
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
20
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
21
+ Requires-Python: >=3.11
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: numpy<3,>=1.26
25
+ Requires-Dist: pandas<3,>=2.2
26
+ Requires-Dist: pandera<1,>=0.18
27
+ Requires-Dist: pyarrow>=15.0
28
+ Requires-Dist: pydantic<3,>=2.6
29
+ Requires-Dist: pyyaml>=6.0
30
+ Requires-Dist: scikit-learn<2,>=1.4
31
+ Requires-Dist: scipy<2,>=1.11
32
+ Requires-Dist: statsmodels<1,>=0.14
33
+ Requires-Dist: structlog>=24.1
34
+ Requires-Dist: arviz<1,>=0.18
35
+ Requires-Dist: xarray>=2024.1
36
+ Requires-Dist: typer>=0.12
37
+ Requires-Dist: rich>=13.7
38
+ Requires-Dist: matplotlib<4,>=3.8
39
+ Requires-Dist: seaborn>=0.13
40
+ Requires-Dist: uncertainties>=3.1
41
+ Requires-Dist: jax>=0.8.2
42
+ Requires-Dist: numpyro>=0.19
43
+ Requires-Dist: jinja2>=3.1
44
+ Requires-Dist: plotly>=6.1
45
+ Provides-Extra: gpu
46
+ Requires-Dist: nvidia-ml-py>=13.580.65; extra == "gpu"
47
+ Provides-Extra: dev
48
+ Requires-Dist: pytest>=8.0; extra == "dev"
49
+ Requires-Dist: pytest-cov>=4.1; extra == "dev"
50
+ Requires-Dist: pytest-timeout>=2.4.0; extra == "dev"
51
+ Requires-Dist: mypy>=1.8; extra == "dev"
52
+ Provides-Extra: comparison
53
+ Requires-Dist: pymc>=5.13; extra == "comparison"
54
+ Dynamic: license-file
55
+
56
+ # panelcast
57
+
58
+ [![CI](https://github.com/cupidthatbtc/panelcast/actions/workflows/ci.yml/badge.svg)](https://github.com/cupidthatbtc/panelcast/actions/workflows/ci.yml)
59
+ [![Nightly](https://github.com/cupidthatbtc/panelcast/actions/workflows/nightly.yml/badge.svg)](https://github.com/cupidthatbtc/panelcast/actions/workflows/nightly.yml)
60
+ [![codecov](https://codecov.io/gh/cupidthatbtc/panelcast/branch/main/graph/badge.svg)](https://codecov.io/gh/cupidthatbtc/panelcast)
61
+ ![Python](https://img.shields.io/badge/python-%3E%3D3.11-blue)
62
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
63
+ [![pixi](https://img.shields.io/badge/pixi-package%20manager-brightgreen)](https://pixi.sh)
64
+ ![Status: beta](https://img.shields.io/badge/status-beta-blue)
65
+
66
+ > **Scope — finished software; open domain-model research.**
67
+ >
68
+ > The *software* claim is complete: panelcast is feature-complete
69
+ > infrastructure for specifying, fitting, evaluating, and auditing
70
+ > hierarchical panel models — reproducibility, leakage controls, diagnostics,
71
+ > and domain portability are the finished, tested deliverable. The
72
+ > *statistical* claim for the flagship AOTY domain is partially established:
73
+ > on a representative ~800-artist / ~5,182-album AOTY subset (skewness −2.08),
74
+ > the published fit **passes the convergence gate** at the amended publication
75
+ > configuration with the 0.13.0 entity-obs default (R-hat 1.00, bulk ESS
76
+ > 1,119, 0 divergences), and the baseline benchmark runs on the same real
77
+ > splits. Two items remain open **as domain-model research, not
78
+ > package-completeness prerequisites**: (1) the **skewness and max**
79
+ > posterior-predictive p-values stay pinned by a bounded-skew mismatch — six
80
+ > likelihood families plus a dequantization toggle were tried with **none
81
+ > resolving them**, though the entity-obs default cleared q10 and q90; (2)
82
+ > the numbers come from the validated subset, not the full eligible corpus
83
+ > (~62k albums with ≥10 ratings; #15). See [`MODEL_CARD.md`](MODEL_CARD.md)
84
+ > and [`docs/LIKELIHOOD_CANDIDATES.md`](docs/LIKELIHOOD_CANDIDATES.md). Treat
85
+ > the subset numbers as real but not final. Canonical numbers:
86
+ > [`.audit/release_results.json`](.audit/release_results.json).
87
+
88
+ **Hierarchical Bayesian prediction for bounded scores of events nested in entities over time — configured by one YAML descriptor.**
89
+
90
+ Lots of forecasting problems share a shape: *entities* accumulate a history of
91
+ *events*, each event carries a *bounded score* and a noisy *observation count*,
92
+ and you want to predict the next score. Musicians release albums rated 0–100.
93
+ Airframes fly test flights scored 0–10. Candidates contest elections with a
94
+ vote share in [0, 1]. panelcast models that shape once — partial pooling across
95
+ entities, a time-varying entity effect, album-to-album (event-to-event)
96
+ dependence, and review-count-scaled noise — and lets you point it at a new
97
+ domain with a single descriptor file and **zero source changes**.
98
+
99
+ The emphasis is the infrastructure *around* the model as much as the model
100
+ itself: leakage controls, data lineage, preflight gates, and
101
+ convergence/calibration diagnostics as first-class, gating checks.
102
+
103
+ ## Domains
104
+
105
+ Every dataset-specific name (columns, target bounds, date formats, posterior
106
+ prefixes, feature blocks) flows through a single `DatasetDescriptor`. Each field
107
+ **defaults to its AOTY value**, so a new domain only states what differs.
108
+
109
+ | Domain | Entity → Event | Bounded score | Status |
110
+ |---|---|---|---|
111
+ | **Album of the Year** (flagship) | Artist → Album | `User_Score` ∈ [0, 100] | Built-in defaults + the `aoty` feature pack (genre, album-type, collaboration). Run with no `--dataset` flag. |
112
+ | **Aerospace** (worked example) | Airframe → Test flight | `Perf_Score` ∈ [0, 10] | Bundled descriptor `configs/datasets/aero.yaml` + end-to-end portability test. One YAML, no music-specific code. |
113
+ | **US elections** | Candidate/seat → Contest | Vote share ∈ [0, 1] | Sibling project (`elections_pred`) that retargets this pipeline — lives in its own repo, not bundled here. |
114
+
115
+ The contract that `--dataset aoty_full` is byte-identical to running with no flag
116
+ at all is enforced by `tests/e2e/test_domain_portability.py`. See
117
+ [`docs/PORTING.md`](docs/PORTING.md) for the full walkthrough.
118
+
119
+ **Replications.** [panelcast-replications](https://github.com/cupidthatbtc/panelcast-replications)
120
+ re-analyses published panel studies through this pipeline — one descriptor YAML
121
+ per paper, zero source changes: Berry–Reese–Larkey 1999 (baseball aging/ability)
122
+ and Strittmatter–Sunde–Zegners 2020 (chess cognitive life cycle), with the
123
+ diagnostic ladder and identification caveats written up in full.
124
+
125
+ ## Model structure
126
+
127
+ Hierarchical partial pooling across entities; a time-varying entity effect via a
128
+ Gaussian random walk; AR(1) event-to-event dependence; heteroscedastic
129
+ observation noise scaled by observation count; non-centered parameterization
130
+ (`LocScaleReparam`) plus a sigma-ref reparameterization to break the
131
+ multiplicative funnel; Student-t likelihood with a soft-clip to the target
132
+ bounds. The default Student-t is one of nine selectable observation families
133
+ (`--likelihood-family`: also `normal`, `skew_studentt`, `skew_normal`,
134
+ `split_normal`, `beta`, `mixture`, `beta_binomial`, `beta_ceiling`), with an optional
135
+ integer-aware dequantization toggle. Optional per-entity overdispersion with a
136
+ lognormal variance prior is available behind a gate. Built on
137
+ [NumPyro](https://num.pyro.ai/) / JAX.
138
+
139
+ ## How it compares — and what it's for
140
+
141
+ On the ~5,000-album AOTY subset, against baselines fit on the same real splits
142
+ (within-entity temporal holdout, N = 653):
143
+
144
+ | | MAE | R² | 80% cov | 95% cov |
145
+ |---|---:|---:|---:|---:|
146
+ | **panelcast** | **5.28** | **0.498** | 0.830 | 0.968 |
147
+ | ridge | 5.38 | 0.498 | 0.879 | 0.965 |
148
+ | gradient boosting | 5.58 | 0.471 | 0.763 | 0.888 |
149
+ | entity mean | 6.11 | 0.322 | 0.818 | 0.925 |
150
+
151
+ The model **leads on MAE** and ties ridge on R² (0.498 each), while carrying the
152
+ only *modeled* intervals, near-nominal at 0.83/0.97. The MAE margin over ridge is
153
+ modest (5.28 vs 5.38); the decisive gaps are CRPS and calibration — the
154
+ gradient-boosted regressor lands close on raw error but **under-covers** badly
155
+ (0.76/0.89), its intervals a bolt-on rather than a modeled quantity. On the cold-start
156
+ (never-seen entity) split it leads outright (MAE 6.82, R² 0.117, 95% coverage 0.965).
157
+ Point accuracy was never the deliverable, though — *calibrated uncertainty* is:
158
+ intervals as a modeled quantity, an interpretable between-entity vs residual variance
159
+ decomposition, and a generative model you can interrogate — and the model now wins on
160
+ accuracy too. Full table, cold-start behaviour, and the R²-by-history gradient:
161
+ [`docs/BASELINES.md`](docs/BASELINES.md).
162
+
163
+ ## Example output
164
+
165
+ The flagship AOTY model, fit on a ~5,000-album subset (within-artist temporal
166
+ holdout). The pipeline's `report` stage renders these automatically.
167
+
168
+ Predicted vs. actual on held-out next albums (95% interval), and interval
169
+ calibration (predicted vs. empirical coverage, ~650 albums/bin):
170
+
171
+ <img src="docs/images/aoty_predictions.png" height="300" alt="Predicted vs. actual scores on held-out next albums with 95% intervals"> <img src="docs/images/aoty_reliability.png" height="300" alt="Interval calibration: predicted vs. empirical coverage by bin">
172
+
173
+ What the model learned — posterior densities of the headline parameters (94% HDI):
174
+ the average album sits near 71/100, and album-to-album dependence (`rho`) is weak
175
+ once the artist level is centered out:
176
+
177
+ <img src="docs/images/aoty_posterior.png" width="85%" alt="Posterior densities of the headline model parameters (94% HDI)">
178
+
179
+ Convergence and posterior geometry — per-chain traces and densities for the
180
+ headline parameters (4 chains, well-mixed), and their pairwise joint posterior
181
+ (round contours, no funnels — the non-centered parameterization doing its job):
182
+
183
+ <img src="docs/images/aoty_trace.png" height="320" alt="MCMC trace and per-chain density for the headline parameters, showing well-mixed chains"> <img src="docs/images/aoty_pairplot.png" height="320" alt="Pairwise joint posterior of the hyperparameters with no funnel geometry">
184
+
185
+ ## Install
186
+
187
+ **Prerequisite:** Python ≥ 3.11.
188
+
189
+ ```bash
190
+ pip install panelcast
191
+ panelcast --help
192
+ ```
193
+
194
+ For the exact tested environment or repository development, use
195
+ [pixi](https://pixi.sh):
196
+
197
+ ```bash
198
+ git clone https://github.com/cupidthatbtc/panelcast.git
199
+ cd panelcast
200
+ pixi install
201
+ pixi run panelcast --help
202
+ ```
203
+
204
+ > `pixi.lock` is the reproducible environment: it pins the full stack, notably
205
+ > the tightly coupled JAX/NumPyro pair. A plain pip installation obeys the tested
206
+ > dependency bounds but does not promise an identical solver result. Use pixi
207
+ > for publication or reproduction work.
208
+ >
209
+ > **Tested platforms:** the wheel-install CI matrix installs the built wheel
210
+ > into a fresh environment and runs the CPU demo on Linux, macOS, and Windows
211
+ > for Python 3.11–3.13. Python 3.14 is expected to work (the pixi environment
212
+ > runs it) but is not yet part of the pip matrix.
213
+
214
+ ## 60-second quickstart (aerospace example)
215
+
216
+ Retarget the whole pipeline to a non-music domain with no code changes, using
217
+ the bundled synthetic aerospace dataset (committed under `examples/aerospace/`:
218
+ 8 airframes flying ~39 sequential test flights scored 0–10):
219
+
220
+ ```bash
221
+ # Run the entire pipeline end-to-end on the example, at tiny scale
222
+ panelcast demo
223
+ ```
224
+
225
+ `demo` reads the bundled aerospace descriptor and CSV from the installed wheel
226
+ (the checkout copies live under `examples/aerospace/`). The descriptor remaps the
227
+ columns, switches the score bounds to [0, 10], drops the music-specific feature
228
+ packs, and adds the domain's own numeric covariates. It runs data → splits →
229
+ features → train → evaluate → predict → report, finishing with a generated
230
+ model card under `outputs/<run_id>/reports/`. The model code is untouched.
231
+
232
+ The committed CSV is regenerated from the shared synthetic generator with
233
+ `python scripts/generate_aero_example.py`. To benchmark the model against simple
234
+ baselines on the splits it just produced:
235
+
236
+ ```bash
237
+ panelcast compare --baselines --dataset aero
238
+ ```
239
+
240
+ To run the flagship AOTY domain instead, point at your data and omit `--dataset`:
241
+
242
+ ```bash
243
+ export AOTY_DATASET_PATH="/path/to/aoty_data.csv"
244
+ panelcast run --preflight-only # GPU-memory / schema / calibration gate
245
+ panelcast run # full pipeline
246
+ panelcast stage train --verbose # or run a single stage
247
+ ```
248
+
249
+ See [`docs/CLI.md`](docs/CLI.md) for the complete command reference.
250
+
251
+ ## Features
252
+
253
+ - Leak-safe data pipeline and evaluation (within-entity temporal split + an
254
+ entity-disjoint secondary check)
255
+ - Explicit data contract and lineage from raw CSV to final artifacts
256
+ - Preflight gates (GPU memory, schema validation, calibration) before expensive runs
257
+ - Convergence + PPC + coverage diagnostics as first-class, gating checks
258
+ - Sensitivity matrix over priors, splits, and feature ablations
259
+ - Publication-ready artifacts: tables, figures, model card, citations
260
+ - Domain portability proven by an end-to-end test, not just asserted — the
261
+ *apparatus* (descriptor → pipeline) runs on a new domain with zero source
262
+ changes; predictive accuracy off the flagship domain is untested by construction
263
+
264
+ ## Documentation
265
+
266
+ - [`docs/GETTING_STARTED.md`](docs/GETTING_STARTED.md) — step-by-step startup guide (start here)
267
+ - [`docs/PORTING.md`](docs/PORTING.md) — retarget to a new domain (the aerospace walkthrough)
268
+ - [`docs/EXTENSIBILITY.md`](docs/EXTENSIBILITY.md) — adding features safely
269
+ - [`docs/CLI.md`](docs/CLI.md) — complete CLI reference
270
+ - [`docs/API.md`](docs/API.md) — supported Python import surface and its semver guarantee
271
+ - [`docs/LEAKAGE_CONTROLS.md`](docs/LEAKAGE_CONTROLS.md) — guardrails and leakage prevention
272
+ - [`docs/EVALUATION_PROTOCOL.md`](docs/EVALUATION_PROTOCOL.md) — metrics, diagnostics, and thresholds
273
+ - [`docs/PROJECT_STRUCTURE.md`](docs/PROJECT_STRUCTURE.md) — directory and file layout
274
+ - [`docs/DATA_CONTRACT.md`](docs/DATA_CONTRACT.md) — raw schema and cleaned artifacts
275
+ - [`docs/LINEAGE.md`](docs/LINEAGE.md) — repository lineage (the private predecessor, the 2026-06-20 migration) and JOSS submission timing
276
+ - [`MODEL_CARD.md`](MODEL_CARD.md) — intended use, results, and limitations
277
+
278
+ A note on results: at the amended publication configuration the published fit
279
+ **passes the convergence gate** on a real ~800-artist / ~5,182-album AOTY
280
+ subset (R-hat 1.00, bulk ESS 1,119, 0 divergences) under the default
281
+ **Student-t** likelihood on the `offset_logit` transformed scale with the
282
+ 0.13.0 entity-obs default:
283
+
284
+ ```bash
285
+ panelcast run --preset publication # 4 chains × 5000, Student-t likelihood
286
+ panelcast diagnose # convergence + PPC of that run
287
+ panelcast compare --baselines # the model vs. simple baselines
288
+ ```
289
+
290
+ What the subset validates: leak-safe splits with role-based names, an honest
291
+ baseline comparison (`panelcast compare`) on the same real splits, and a
292
+ convergent publication-scale fit on real, strongly left-skewed data — the
293
+ software behaves end-to-end under production settings. What convergence does
294
+ *not* establish: that the likelihood is correctly specified (the PPC pins say
295
+ it is not), or that subset results transfer to the full corpus. Both remain
296
+ open as domain-model research: the **skewness and max** posterior-predictive
297
+ p-values stay pinned at the extremes from a symmetric-likelihood /
298
+ left-skewed-target mismatch — six likelihood families (`beta`,
299
+ `skew_studentt`, `skew_normal`, `split_normal`, `beta_binomial`, `mixture`)
300
+ plus a dequantization toggle were tried and **none resolves them**, though the
301
+ entity-obs default cleared the q10 and q90 pins (see
302
+ [`docs/LIKELIHOOD_CANDIDATES.md`](docs/LIKELIHOOD_CANDIDATES.md)) — and this is
303
+ the validated subset, not the full eligible corpus (~62k albums with ≥10 user
304
+ ratings), which needs the full dataset and a GPU (#15).
305
+
306
+ Intentionally out of scope for the package claim: a resolved AOTY likelihood
307
+ (that is the open research above), full-corpus results (#15), and predictive
308
+ accuracy on non-flagship domains (portability is structural — the pipeline
309
+ runs — not a transferred accuracy claim). The code, the diagnostics, and the
310
+ honest naming of what is and isn't resolved are the point. Every headline
311
+ number here derives from the canonical release-result manifest,
312
+ [`.audit/release_results.json`](.audit/release_results.json), and drift fails CI.
313
+
314
+ ## License
315
+
316
+ MIT License. See [LICENSE](LICENSE) for details.
@@ -0,0 +1,261 @@
1
+ # panelcast
2
+
3
+ [![CI](https://github.com/cupidthatbtc/panelcast/actions/workflows/ci.yml/badge.svg)](https://github.com/cupidthatbtc/panelcast/actions/workflows/ci.yml)
4
+ [![Nightly](https://github.com/cupidthatbtc/panelcast/actions/workflows/nightly.yml/badge.svg)](https://github.com/cupidthatbtc/panelcast/actions/workflows/nightly.yml)
5
+ [![codecov](https://codecov.io/gh/cupidthatbtc/panelcast/branch/main/graph/badge.svg)](https://codecov.io/gh/cupidthatbtc/panelcast)
6
+ ![Python](https://img.shields.io/badge/python-%3E%3D3.11-blue)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
8
+ [![pixi](https://img.shields.io/badge/pixi-package%20manager-brightgreen)](https://pixi.sh)
9
+ ![Status: beta](https://img.shields.io/badge/status-beta-blue)
10
+
11
+ > **Scope — finished software; open domain-model research.**
12
+ >
13
+ > The *software* claim is complete: panelcast is feature-complete
14
+ > infrastructure for specifying, fitting, evaluating, and auditing
15
+ > hierarchical panel models — reproducibility, leakage controls, diagnostics,
16
+ > and domain portability are the finished, tested deliverable. The
17
+ > *statistical* claim for the flagship AOTY domain is partially established:
18
+ > on a representative ~800-artist / ~5,182-album AOTY subset (skewness −2.08),
19
+ > the published fit **passes the convergence gate** at the amended publication
20
+ > configuration with the 0.13.0 entity-obs default (R-hat 1.00, bulk ESS
21
+ > 1,119, 0 divergences), and the baseline benchmark runs on the same real
22
+ > splits. Two items remain open **as domain-model research, not
23
+ > package-completeness prerequisites**: (1) the **skewness and max**
24
+ > posterior-predictive p-values stay pinned by a bounded-skew mismatch — six
25
+ > likelihood families plus a dequantization toggle were tried with **none
26
+ > resolving them**, though the entity-obs default cleared q10 and q90; (2)
27
+ > the numbers come from the validated subset, not the full eligible corpus
28
+ > (~62k albums with ≥10 ratings; #15). See [`MODEL_CARD.md`](MODEL_CARD.md)
29
+ > and [`docs/LIKELIHOOD_CANDIDATES.md`](docs/LIKELIHOOD_CANDIDATES.md). Treat
30
+ > the subset numbers as real but not final. Canonical numbers:
31
+ > [`.audit/release_results.json`](.audit/release_results.json).
32
+
33
+ **Hierarchical Bayesian prediction for bounded scores of events nested in entities over time — configured by one YAML descriptor.**
34
+
35
+ Lots of forecasting problems share a shape: *entities* accumulate a history of
36
+ *events*, each event carries a *bounded score* and a noisy *observation count*,
37
+ and you want to predict the next score. Musicians release albums rated 0–100.
38
+ Airframes fly test flights scored 0–10. Candidates contest elections with a
39
+ vote share in [0, 1]. panelcast models that shape once — partial pooling across
40
+ entities, a time-varying entity effect, album-to-album (event-to-event)
41
+ dependence, and review-count-scaled noise — and lets you point it at a new
42
+ domain with a single descriptor file and **zero source changes**.
43
+
44
+ The emphasis is the infrastructure *around* the model as much as the model
45
+ itself: leakage controls, data lineage, preflight gates, and
46
+ convergence/calibration diagnostics as first-class, gating checks.
47
+
48
+ ## Domains
49
+
50
+ Every dataset-specific name (columns, target bounds, date formats, posterior
51
+ prefixes, feature blocks) flows through a single `DatasetDescriptor`. Each field
52
+ **defaults to its AOTY value**, so a new domain only states what differs.
53
+
54
+ | Domain | Entity → Event | Bounded score | Status |
55
+ |---|---|---|---|
56
+ | **Album of the Year** (flagship) | Artist → Album | `User_Score` ∈ [0, 100] | Built-in defaults + the `aoty` feature pack (genre, album-type, collaboration). Run with no `--dataset` flag. |
57
+ | **Aerospace** (worked example) | Airframe → Test flight | `Perf_Score` ∈ [0, 10] | Bundled descriptor `configs/datasets/aero.yaml` + end-to-end portability test. One YAML, no music-specific code. |
58
+ | **US elections** | Candidate/seat → Contest | Vote share ∈ [0, 1] | Sibling project (`elections_pred`) that retargets this pipeline — lives in its own repo, not bundled here. |
59
+
60
+ The contract that `--dataset aoty_full` is byte-identical to running with no flag
61
+ at all is enforced by `tests/e2e/test_domain_portability.py`. See
62
+ [`docs/PORTING.md`](docs/PORTING.md) for the full walkthrough.
63
+
64
+ **Replications.** [panelcast-replications](https://github.com/cupidthatbtc/panelcast-replications)
65
+ re-analyses published panel studies through this pipeline — one descriptor YAML
66
+ per paper, zero source changes: Berry–Reese–Larkey 1999 (baseball aging/ability)
67
+ and Strittmatter–Sunde–Zegners 2020 (chess cognitive life cycle), with the
68
+ diagnostic ladder and identification caveats written up in full.
69
+
70
+ ## Model structure
71
+
72
+ Hierarchical partial pooling across entities; a time-varying entity effect via a
73
+ Gaussian random walk; AR(1) event-to-event dependence; heteroscedastic
74
+ observation noise scaled by observation count; non-centered parameterization
75
+ (`LocScaleReparam`) plus a sigma-ref reparameterization to break the
76
+ multiplicative funnel; Student-t likelihood with a soft-clip to the target
77
+ bounds. The default Student-t is one of nine selectable observation families
78
+ (`--likelihood-family`: also `normal`, `skew_studentt`, `skew_normal`,
79
+ `split_normal`, `beta`, `mixture`, `beta_binomial`, `beta_ceiling`), with an optional
80
+ integer-aware dequantization toggle. Optional per-entity overdispersion with a
81
+ lognormal variance prior is available behind a gate. Built on
82
+ [NumPyro](https://num.pyro.ai/) / JAX.
83
+
84
+ ## How it compares — and what it's for
85
+
86
+ On the ~5,000-album AOTY subset, against baselines fit on the same real splits
87
+ (within-entity temporal holdout, N = 653):
88
+
89
+ | | MAE | R² | 80% cov | 95% cov |
90
+ |---|---:|---:|---:|---:|
91
+ | **panelcast** | **5.28** | **0.498** | 0.830 | 0.968 |
92
+ | ridge | 5.38 | 0.498 | 0.879 | 0.965 |
93
+ | gradient boosting | 5.58 | 0.471 | 0.763 | 0.888 |
94
+ | entity mean | 6.11 | 0.322 | 0.818 | 0.925 |
95
+
96
+ The model **leads on MAE** and ties ridge on R² (0.498 each), while carrying the
97
+ only *modeled* intervals, near-nominal at 0.83/0.97. The MAE margin over ridge is
98
+ modest (5.28 vs 5.38); the decisive gaps are CRPS and calibration — the
99
+ gradient-boosted regressor lands close on raw error but **under-covers** badly
100
+ (0.76/0.89), its intervals a bolt-on rather than a modeled quantity. On the cold-start
101
+ (never-seen entity) split it leads outright (MAE 6.82, R² 0.117, 95% coverage 0.965).
102
+ Point accuracy was never the deliverable, though — *calibrated uncertainty* is:
103
+ intervals as a modeled quantity, an interpretable between-entity vs residual variance
104
+ decomposition, and a generative model you can interrogate — and the model now wins on
105
+ accuracy too. Full table, cold-start behaviour, and the R²-by-history gradient:
106
+ [`docs/BASELINES.md`](docs/BASELINES.md).
107
+
108
+ ## Example output
109
+
110
+ The flagship AOTY model, fit on a ~5,000-album subset (within-artist temporal
111
+ holdout). The pipeline's `report` stage renders these automatically.
112
+
113
+ Predicted vs. actual on held-out next albums (95% interval), and interval
114
+ calibration (predicted vs. empirical coverage, ~650 albums/bin):
115
+
116
+ <img src="docs/images/aoty_predictions.png" height="300" alt="Predicted vs. actual scores on held-out next albums with 95% intervals"> <img src="docs/images/aoty_reliability.png" height="300" alt="Interval calibration: predicted vs. empirical coverage by bin">
117
+
118
+ What the model learned — posterior densities of the headline parameters (94% HDI):
119
+ the average album sits near 71/100, and album-to-album dependence (`rho`) is weak
120
+ once the artist level is centered out:
121
+
122
+ <img src="docs/images/aoty_posterior.png" width="85%" alt="Posterior densities of the headline model parameters (94% HDI)">
123
+
124
+ Convergence and posterior geometry — per-chain traces and densities for the
125
+ headline parameters (4 chains, well-mixed), and their pairwise joint posterior
126
+ (round contours, no funnels — the non-centered parameterization doing its job):
127
+
128
+ <img src="docs/images/aoty_trace.png" height="320" alt="MCMC trace and per-chain density for the headline parameters, showing well-mixed chains"> <img src="docs/images/aoty_pairplot.png" height="320" alt="Pairwise joint posterior of the hyperparameters with no funnel geometry">
129
+
130
+ ## Install
131
+
132
+ **Prerequisite:** Python ≥ 3.11.
133
+
134
+ ```bash
135
+ pip install panelcast
136
+ panelcast --help
137
+ ```
138
+
139
+ For the exact tested environment or repository development, use
140
+ [pixi](https://pixi.sh):
141
+
142
+ ```bash
143
+ git clone https://github.com/cupidthatbtc/panelcast.git
144
+ cd panelcast
145
+ pixi install
146
+ pixi run panelcast --help
147
+ ```
148
+
149
+ > `pixi.lock` is the reproducible environment: it pins the full stack, notably
150
+ > the tightly coupled JAX/NumPyro pair. A plain pip installation obeys the tested
151
+ > dependency bounds but does not promise an identical solver result. Use pixi
152
+ > for publication or reproduction work.
153
+ >
154
+ > **Tested platforms:** the wheel-install CI matrix installs the built wheel
155
+ > into a fresh environment and runs the CPU demo on Linux, macOS, and Windows
156
+ > for Python 3.11–3.13. Python 3.14 is expected to work (the pixi environment
157
+ > runs it) but is not yet part of the pip matrix.
158
+
159
+ ## 60-second quickstart (aerospace example)
160
+
161
+ Retarget the whole pipeline to a non-music domain with no code changes, using
162
+ the bundled synthetic aerospace dataset (committed under `examples/aerospace/`:
163
+ 8 airframes flying ~39 sequential test flights scored 0–10):
164
+
165
+ ```bash
166
+ # Run the entire pipeline end-to-end on the example, at tiny scale
167
+ panelcast demo
168
+ ```
169
+
170
+ `demo` reads the bundled aerospace descriptor and CSV from the installed wheel
171
+ (the checkout copies live under `examples/aerospace/`). The descriptor remaps the
172
+ columns, switches the score bounds to [0, 10], drops the music-specific feature
173
+ packs, and adds the domain's own numeric covariates. It runs data → splits →
174
+ features → train → evaluate → predict → report, finishing with a generated
175
+ model card under `outputs/<run_id>/reports/`. The model code is untouched.
176
+
177
+ The committed CSV is regenerated from the shared synthetic generator with
178
+ `python scripts/generate_aero_example.py`. To benchmark the model against simple
179
+ baselines on the splits it just produced:
180
+
181
+ ```bash
182
+ panelcast compare --baselines --dataset aero
183
+ ```
184
+
185
+ To run the flagship AOTY domain instead, point at your data and omit `--dataset`:
186
+
187
+ ```bash
188
+ export AOTY_DATASET_PATH="/path/to/aoty_data.csv"
189
+ panelcast run --preflight-only # GPU-memory / schema / calibration gate
190
+ panelcast run # full pipeline
191
+ panelcast stage train --verbose # or run a single stage
192
+ ```
193
+
194
+ See [`docs/CLI.md`](docs/CLI.md) for the complete command reference.
195
+
196
+ ## Features
197
+
198
+ - Leak-safe data pipeline and evaluation (within-entity temporal split + an
199
+ entity-disjoint secondary check)
200
+ - Explicit data contract and lineage from raw CSV to final artifacts
201
+ - Preflight gates (GPU memory, schema validation, calibration) before expensive runs
202
+ - Convergence + PPC + coverage diagnostics as first-class, gating checks
203
+ - Sensitivity matrix over priors, splits, and feature ablations
204
+ - Publication-ready artifacts: tables, figures, model card, citations
205
+ - Domain portability proven by an end-to-end test, not just asserted — the
206
+ *apparatus* (descriptor → pipeline) runs on a new domain with zero source
207
+ changes; predictive accuracy off the flagship domain is untested by construction
208
+
209
+ ## Documentation
210
+
211
+ - [`docs/GETTING_STARTED.md`](docs/GETTING_STARTED.md) — step-by-step startup guide (start here)
212
+ - [`docs/PORTING.md`](docs/PORTING.md) — retarget to a new domain (the aerospace walkthrough)
213
+ - [`docs/EXTENSIBILITY.md`](docs/EXTENSIBILITY.md) — adding features safely
214
+ - [`docs/CLI.md`](docs/CLI.md) — complete CLI reference
215
+ - [`docs/API.md`](docs/API.md) — supported Python import surface and its semver guarantee
216
+ - [`docs/LEAKAGE_CONTROLS.md`](docs/LEAKAGE_CONTROLS.md) — guardrails and leakage prevention
217
+ - [`docs/EVALUATION_PROTOCOL.md`](docs/EVALUATION_PROTOCOL.md) — metrics, diagnostics, and thresholds
218
+ - [`docs/PROJECT_STRUCTURE.md`](docs/PROJECT_STRUCTURE.md) — directory and file layout
219
+ - [`docs/DATA_CONTRACT.md`](docs/DATA_CONTRACT.md) — raw schema and cleaned artifacts
220
+ - [`docs/LINEAGE.md`](docs/LINEAGE.md) — repository lineage (the private predecessor, the 2026-06-20 migration) and JOSS submission timing
221
+ - [`MODEL_CARD.md`](MODEL_CARD.md) — intended use, results, and limitations
222
+
223
+ A note on results: at the amended publication configuration the published fit
224
+ **passes the convergence gate** on a real ~800-artist / ~5,182-album AOTY
225
+ subset (R-hat 1.00, bulk ESS 1,119, 0 divergences) under the default
226
+ **Student-t** likelihood on the `offset_logit` transformed scale with the
227
+ 0.13.0 entity-obs default:
228
+
229
+ ```bash
230
+ panelcast run --preset publication # 4 chains × 5000, Student-t likelihood
231
+ panelcast diagnose # convergence + PPC of that run
232
+ panelcast compare --baselines # the model vs. simple baselines
233
+ ```
234
+
235
+ What the subset validates: leak-safe splits with role-based names, an honest
236
+ baseline comparison (`panelcast compare`) on the same real splits, and a
237
+ convergent publication-scale fit on real, strongly left-skewed data — the
238
+ software behaves end-to-end under production settings. What convergence does
239
+ *not* establish: that the likelihood is correctly specified (the PPC pins say
240
+ it is not), or that subset results transfer to the full corpus. Both remain
241
+ open as domain-model research: the **skewness and max** posterior-predictive
242
+ p-values stay pinned at the extremes from a symmetric-likelihood /
243
+ left-skewed-target mismatch — six likelihood families (`beta`,
244
+ `skew_studentt`, `skew_normal`, `split_normal`, `beta_binomial`, `mixture`)
245
+ plus a dequantization toggle were tried and **none resolves them**, though the
246
+ entity-obs default cleared the q10 and q90 pins (see
247
+ [`docs/LIKELIHOOD_CANDIDATES.md`](docs/LIKELIHOOD_CANDIDATES.md)) — and this is
248
+ the validated subset, not the full eligible corpus (~62k albums with ≥10 user
249
+ ratings), which needs the full dataset and a GPU (#15).
250
+
251
+ Intentionally out of scope for the package claim: a resolved AOTY likelihood
252
+ (that is the open research above), full-corpus results (#15), and predictive
253
+ accuracy on non-flagship domains (portability is structural — the pipeline
254
+ runs — not a transferred accuracy claim). The code, the diagnostics, and the
255
+ honest naming of what is and isn't resolved are the point. Every headline
256
+ number here derives from the canonical release-result manifest,
257
+ [`.audit/release_results.json`](.audit/release_results.json), and drift fails CI.
258
+
259
+ ## License
260
+
261
+ MIT License. See [LICENSE](LICENSE) for details.