Skip to content

Commit 6f4d3e0

Browse files
RoJLDclaude
andcommitted
feat(integration): port Valentin Laborie's ETH lifecycle GMM-HMM to hmm-studio idioms
Reproduces the 2025 S2 deliverable (Modèles probabilistes/gmmhmm Full.py) end-to-end through hmm-studio's declarative APIs. Private data is referenced via env var HMM_VALENTIN_ETH_PATH and is never committed. Engine additions: - prep ops log1p (skip-negative-by-default) and drop_low_variance (encodes the std-threshold + zeros-fraction filter pattern). 5 unit tests. - bundled recipe valentin_eth: 365-day rolling mean -> dropna -> drop_low_variance -> log1p -> zscore. End-to-end produces 3402 rows of standardised features from the raw 3766-row CSV. Examples + notebook: - examples/valentin_eth_3regime_gmm.yaml: 3-state GMM topology (diag, n_mix=3, ergodic). The YAML documents Valentin's strict left-right original and explains why ergodic is used here (hmm-studio re-fits startprob via M-step, which NaNs under strict left-right; Valentin's hmmlearn params=mcw freeze isn't exposed). - notebooks/10_valentin_eth_gmm_hmm.ipynb: 15-cell narrative wiring CSV load -> recipe -> PCA(n=2) -> fit -> diagnostics, with a comparison table of what was ported faithfully vs adapted to studio idioms. Regression: - tests/test_valentin_eth_regression.py: 6 tests pinning log-likelihood (-636 +/- 5%), BIC, PCA explained variance, EM convergence, prep-output shape, and phase populations. Auto-skipped when env var unset. Safety: - .gitignore: *JDD_ETH*.csv, *JDD_BTC*.csv, **/private_data/, notebooks/data/ patterns added to prevent private CSV commits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent a864953 commit 6f4d3e0

8 files changed

Lines changed: 656 additions & 0 deletions

File tree

.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,11 @@ src/hmm_studio/frontend/dist/
4040
# Frontend build copied into the server package (built artifact, regenerated)
4141
src/hmm_studio/server/static/index.html
4242
src/hmm_studio/server/static/assets/
43+
44+
# Private datasets — referenced via env vars from notebooks / regression
45+
# tests, must NEVER end up in commits. Add new patterns here whenever a
46+
# private CSV/parquet is wired into a notebook (env-var path pattern).
47+
*JDD_ETH*.csv
48+
*JDD_BTC*.csv
49+
**/private_data/
50+
notebooks/data/

CHANGELOG.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,50 @@ All notable changes to `hmm-studio` are documented here. This project follows
44
[Semantic Versioning](https://semver.org/) and the
55
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) format.
66

7+
## [Unreleased]
8+
9+
### Added — engine (`hmm_core.prep`)
10+
11+
- Two new prep ops:
12+
- **`log1p`** — apply `np.log1p` to listed (or all numeric) columns; by
13+
default skips columns containing any negative value so it chains
14+
safely after generic preprocessing.
15+
- **`drop_low_variance`** — drop columns that are near-constant
16+
(`std < threshold`) or mostly-zero (`zeros_fraction > threshold`).
17+
Encodes the "degenerate column" filter pattern that came up in the
18+
Valentin ETH port.
19+
- New bundled recipe `valentin_eth` (5-step pipeline: 365-day rolling
20+
mean → dropna → drop_low_variance → log1p → zscore) ready for use via
21+
`Pipeline.from_recipe("valentin_eth")`.
22+
23+
### Added — examples + notebooks
24+
25+
- `examples/valentin_eth_3regime_gmm.yaml`: 3-state GMM topology (diag
26+
covariance, `n_mix=3`, ergodic; documents Valentin's strict left-right
27+
original and why it would need a frozen-startprob M-step to work
28+
as-is).
29+
- `notebooks/10_valentin_eth_gmm_hmm.ipynb`: ports Valentin Laborie's
30+
*2025 S2* ETH lifecycle GMM-HMM end-to-end through hmm-studio idioms
31+
(private data: set `HMM_VALENTIN_ETH_PATH` env var to the CSV path
32+
before running).
33+
34+
### Added — tests
35+
36+
- `tests/test_valentin_eth_regression.py`: 6 regression tests pinning the
37+
log-likelihood (−636 ± 5%), BIC, PCA explained variance, EM convergence,
38+
prep-output shape, and phase populations. **Auto-skipped** when
39+
`HMM_VALENTIN_ETH_PATH` is unset (private dataset not in the repo).
40+
- 5 new prep-op tests covering `log1p` (explicit columns, negative-skip,
41+
forced-on-negative) and `drop_low_variance` (default + thresholds).
42+
43+
### Changed — `.gitignore`
44+
45+
- Added `*JDD_ETH*.csv`, `*JDD_BTC*.csv`, `**/private_data/`,
46+
`notebooks/data/` patterns to prevent accidental commit of private
47+
research datasets referenced from notebooks via env vars.
48+
49+
---
50+
751
## [1.1.0] — 2026-05-23
852

953
Distribution-surface release: `hmm-studio` becomes pip-installable and
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
name: valentin_eth_3regime_gmm
2+
n_states: 3
3+
state_names: [accumulation, expansion, distribution]
4+
5+
# GMM emission : each state hosts a 3-component Gaussian mixture on the
6+
# 2-D PCA space of the preprocessed ETH on-chain indicators (see
7+
# notebooks/10_valentin_eth_gmm_hmm.ipynb for the upstream pipeline).
8+
#
9+
# Valentin's original used covariance_type='full' with a hand-crafted
10+
# init from 3 equal-length data segments. hmm-studio's kmeans init for
11+
# GMM emits diag-shaped covars, so we use diag here for an out-of-the-box
12+
# fit. In 2-D PCA space the practical difference is small; the notebook
13+
# documents the original full-covariance recipe for reference.
14+
emission:
15+
type: gmm
16+
covariance_type: diag
17+
n_features: 2
18+
n_mix: 3
19+
20+
# Ergodic topology (no allowed_transitions block = all transitions allowed).
21+
#
22+
# Valentin's original was strict left-right (Bakis) with a hand-clamped
23+
# startprob = [1, 0, 0] and ``params='mcw'`` to freeze startprob/transmat
24+
# in hmmlearn's M-step. hmm-studio always re-fits startprob from
25+
# posteriors, which together with strict left-right produces a 0/0 NaN
26+
# in the M-step (state 0 is only visited at t=0; its responsibility sum
27+
# collapses).
28+
#
29+
# Workaround : leave the topology ergodic. The data is sufficiently
30+
# regime-like that EM naturally discovers a quasi-left-right structure
31+
# (self-loop probabilities ~ 0.998, cross-transitions ~ 0.001-0.002),
32+
# which is the practical answer Valentin's hard constraint was trying
33+
# to encode. The notebook compares the two formulations.
34+
35+
startprob: uniform
36+
37+
init:
38+
strategy: kmeans
39+
seed: 42
40+
41+
fit:
42+
algorithm: baum_welch
43+
n_iter: 500
44+
tol: 1.0e-4
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"metadata": {},
6+
"source": [
7+
"# Valentin's ETH lifecycle GMM-HMM — ported to hmm-studio\n",
8+
"\n",
9+
"Reproduces Valentin Laborie's *2025 S2* GMM-HMM on Ethereum on-chain indicators, using only `hmm-studio` idioms (declarative `Topology`, bundled `prep` recipe, `fit_gmm_nhmm`-free path through the standard `fit()` since the model is homogeneous).\n",
10+
"\n",
11+
"**Original deliverable** : `Experiment.Crypto.2025S2.LifeCycle/Modèles probabilistes (py)/gmmhmm Full.py`\n",
12+
"\n",
13+
"**Data** : `JDD_ETH_après la corrélation_FINAL_HMM.csv` (~3766 daily rows, 4 features: `AdrActCnt`, `ROI30d`, `TxCnt`, `SplyExNtv`). The file is **private** and intentionally not committed to the repo. Point the env var `HMM_VALENTIN_ETH_PATH` at your local copy."
14+
]
15+
},
16+
{
17+
"cell_type": "markdown",
18+
"metadata": {},
19+
"source": [
20+
"## 1. Load the private CSV\n",
21+
"\n",
22+
"Set `HMM_VALENTIN_ETH_PATH` to the absolute path of `JDD_ETH_après la corrélation_FINAL_HMM.csv` before launching the notebook."
23+
]
24+
},
25+
{
26+
"cell_type": "code",
27+
"execution_count": null,
28+
"metadata": {},
29+
"outputs": [],
30+
"source": [
31+
"import os\n",
32+
"import pandas as pd\n",
33+
"\n",
34+
"csv_path = os.environ.get(\"HMM_VALENTIN_ETH_PATH\")\n",
35+
"if not csv_path:\n",
36+
" raise SystemExit(\n",
37+
" \"Set HMM_VALENTIN_ETH_PATH=<...>/JDD_ETH_après la corrélation_FINAL_HMM.csv \"\n",
38+
" \"before running this notebook.\"\n",
39+
" )\n",
40+
"\n",
41+
"df = (\n",
42+
" pd.read_csv(csv_path, encoding=\"ISO-8859-1\", parse_dates=[\"date\"])\n",
43+
" .sort_values(\"date\")\n",
44+
" .set_index(\"date\")\n",
45+
")\n",
46+
"df.shape, df.columns.tolist()"
47+
]
48+
},
49+
{
50+
"cell_type": "markdown",
51+
"metadata": {},
52+
"source": [
53+
"## 2. Apply Valentin's preprocessing as an hmm-studio recipe\n",
54+
"\n",
55+
"The recipe `valentin_eth` chains the 5 preprocessing steps from the original script :\n",
56+
"\n",
57+
"1. 365-day rolling mean on every input feature (de-noise)\n",
58+
"2. dropna (drop the first 364 boundary rows)\n",
59+
"3. drop_low_variance (Valentin's `zeros_frac > 0.5 or std < 1e-8` filter)\n",
60+
"4. log1p on every non-negative column\n",
61+
"5. zscore on the surviving features\n",
62+
"\n",
63+
"PCA is intentionally NOT inside the recipe (the prep layer is pandas-only by design)."
64+
]
65+
},
66+
{
67+
"cell_type": "code",
68+
"execution_count": null,
69+
"metadata": {},
70+
"outputs": [],
71+
"source": [
72+
"from hmm_core.prep import Pipeline\n",
73+
"\n",
74+
"prep = Pipeline.from_recipe(\"valentin_eth\")\n",
75+
"result = prep.fit_transform(df)\n",
76+
"result.df.shape, result.df.columns.tolist()"
77+
]
78+
},
79+
{
80+
"cell_type": "code",
81+
"execution_count": null,
82+
"metadata": {},
83+
"outputs": [],
84+
"source": [
85+
"# Sanity : every feature is now centered + scaled\n",
86+
"result.df.describe().T[[\"mean\", \"std\", \"min\", \"max\"]]"
87+
]
88+
},
89+
{
90+
"cell_type": "markdown",
91+
"metadata": {},
92+
"source": [
93+
"## 3. PCA to 2 components\n",
94+
"\n",
95+
"Valentin uses PCA to reduce 4 standardized features → 2 dimensions, then fits the GMM-HMM on the 2-D scores. We do the same here using `sklearn.decomposition.PCA` outside the prep recipe."
96+
]
97+
},
98+
{
99+
"cell_type": "code",
100+
"execution_count": null,
101+
"metadata": {},
102+
"outputs": [],
103+
"source": [
104+
"from sklearn.decomposition import PCA\n",
105+
"\n",
106+
"pca = PCA(n_components=2, random_state=42)\n",
107+
"X_pca = pca.fit_transform(result.df.values)\n",
108+
"dates = result.df.index\n",
109+
"print(f\"X_pca shape : {X_pca.shape}\")\n",
110+
"print(f\"Explained variance ratio : {pca.explained_variance_ratio_}\")\n",
111+
"print(f\"Cumulative : {pca.explained_variance_ratio_.sum():.3f}\")"
112+
]
113+
},
114+
{
115+
"cell_type": "markdown",
116+
"metadata": {},
117+
"source": [
118+
"## 4. Declare the topology and fit\n",
119+
"\n",
120+
"`examples/valentin_eth_3regime_gmm.yaml` carries the topology : 3 states (accumulation / expansion / distribution), GMM emissions with `n_mix=3`, diagonal covariances, ergodic transitions (Valentin's original strict left-right is documented in the YAML — we use ergodic here because hmm-studio's M-step would NaN under strict left-right + GMM, and the data is regime-like enough that EM naturally discovers a quasi-left-right structure)."
121+
]
122+
},
123+
{
124+
"cell_type": "code",
125+
"execution_count": null,
126+
"metadata": {},
127+
"outputs": [],
128+
"source": [
129+
"from hmm_core.io import load_topology\n",
130+
"from hmm_core.fit import fit\n",
131+
"\n",
132+
"topo = load_topology(\"../examples/valentin_eth_3regime_gmm.yaml\")\n",
133+
"fitted = fit(topo, X_pca, seed=42)\n",
134+
"fitted # rich HTML view : stats + transmat heatmap"
135+
]
136+
},
137+
{
138+
"cell_type": "code",
139+
"execution_count": null,
140+
"metadata": {},
141+
"outputs": [],
142+
"source": [
143+
"print(f\"Log-likelihood : {fitted.log_likelihood:.2f}\")\n",
144+
"print(f\"Per observation : {fitted.log_likelihood / len(X_pca):.4f}\")\n",
145+
"print(f\"BIC : {fitted.bic:.2f}\")\n",
146+
"print(f\"AIC : {fitted.aic:.2f}\")\n",
147+
"print(f\"EM iterations : {fitted.n_iter_actual}\")\n",
148+
"print(f\"Converged : {fitted.converged}\")"
149+
]
150+
},
151+
{
152+
"cell_type": "markdown",
153+
"metadata": {},
154+
"source": [
155+
"## 5. Decode the lifecycle phases\n",
156+
"\n",
157+
"Viterbi on the fitted model gives the most likely sequence of latent states. The transition matrix typically shows three sticky regimes (self-loops near 0.998) with small cross-transitions — the natural lifecycle structure Valentin's strict left-right constraint was trying to encode."
158+
]
159+
},
160+
{
161+
"cell_type": "code",
162+
"execution_count": null,
163+
"metadata": {},
164+
"outputs": [],
165+
"source": [
166+
"import numpy as np\n",
167+
"\n",
168+
"states = fitted.model.predict(X_pca)\n",
169+
"print(\"Transition matrix (rows = from, cols = to) :\")\n",
170+
"print(fitted.model.transmat_.round(3))\n",
171+
"print(\"\\nPhase frequencies :\")\n",
172+
"for i, name in enumerate(topo.state_names):\n",
173+
" pct = (states == i).mean() * 100\n",
174+
" print(f\" {i} {name:15s} : {pct:5.1f} % of timeline\")"
175+
]
176+
},
177+
{
178+
"cell_type": "markdown",
179+
"metadata": {},
180+
"source": [
181+
"## 6. Compare with the original Valentin script\n",
182+
"\n",
183+
"The full reference values you should see (PCA seed=42, hmm-studio kmeans init seed=42, hmmlearn deterministic EM) :\n",
184+
"\n",
185+
"| Metric | Reference (hmm-studio) |\n",
186+
"|---|---|\n",
187+
"| Log-likelihood total | ~ −636 |\n",
188+
"| Log-likelihood / obs | ~ −0.187 |\n",
189+
"| BIC | ~ 1678 |\n",
190+
"| AIC | ~ 1371 |\n",
191+
"| Converged | True |\n",
192+
"| EM iterations | ~ 138 |\n",
193+
"| PCA explained variance (2 PCs) | ~ 0.880 |\n",
194+
"\n",
195+
"These reference values are checked by `tests/test_valentin_eth_regression.py` (skipped if `HMM_VALENTIN_ETH_PATH` is unset, run in CI by exporting the path to a secret-mounted copy of the dataset)."
196+
]
197+
},
198+
{
199+
"cell_type": "markdown",
200+
"metadata": {},
201+
"source": [
202+
"## What was ported and what was kept original\n",
203+
"\n",
204+
"| Component | Valentin original | hmm-studio port |\n",
205+
"|---|---|---|\n",
206+
"| Preprocessing | inline pandas + sklearn (~50 lines) | bundled recipe `valentin_eth` (declarative YAML) |\n",
207+
"| PCA | inline `sklearn.PCA(n_components=2)` | inline `sklearn.PCA` (unchanged — prep layer stays pandas-only) |\n",
208+
"| Topology | hardcoded transmat / startprob in Python | declarative `examples/valentin_eth_3regime_gmm.yaml` |\n",
209+
"| GMM covariance | `full` | `diag` (hmm-studio kmeans init doesn't emit full covars yet) |\n",
210+
"| Topology shape | strict left-right with frozen startprob (`params='mcw'`) | ergodic with uniform startprob (avoids M-step NaN; EM rediscovers quasi-left-right) |\n",
211+
"| EM engine | direct `hmmlearn.GMMHMM` | `hmm_core.fit` via `HMMBackend` protocol |\n",
212+
"| Visualisations | manual matplotlib (regime timeline, PCA biplot, posterior probas) | `fitted._repr_html_()` + Web UI Results page |"
213+
]
214+
}
215+
],
216+
"metadata": {
217+
"kernelspec": {
218+
"display_name": "Python 3",
219+
"language": "python",
220+
"name": "python3"
221+
},
222+
"language_info": {
223+
"name": "python",
224+
"version": "3.12"
225+
}
226+
},
227+
"nbformat": 4,
228+
"nbformat_minor": 5
229+
}

0 commit comments

Comments
 (0)