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