Noptus commited on
Commit
e9fa286
·
verified ·
1 Parent(s): 32d8f0a

Add reproducible v4 weekly champion release

Browse files
README.md ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ tags:
3
+ - numerai
4
+ - tabular-regression
5
+ - finance
6
+ - lightgbm
7
+ - catboost
8
+ - weekly-model
9
+ pipeline_tag: tabular-regression
10
+ ---
11
+
12
+ # Numerai Weekly Champion v4
13
+
14
+ This repository publishes the exact model bundle currently used by Noptus' validated Numerai submission pipeline. It is intended as a reproducible research artifact and a starting point for ensemble-diversity work—not as investment advice or a promise of tournament performance.
15
+
16
+ ## Current release
17
+
18
+ - Release: `v4-20260801-180455`
19
+ - Verified live round: `1333`
20
+ - Data schema: Numerai `v5.2`
21
+ - Inputs: 780 `medium` features plus 8 public benchmark-model columns
22
+ - Components: two benchmark-aware LightGBM models, six multi-target LightGBM models, one residual LightGBM model, and one CatBoost model
23
+ - Bundle size: approximately 142 MiB
24
+ - SHA-256: `79db5f41f3506e8e10a8b96c60a927f6a9ca202e49e03304ceaa9d304116b8d9`
25
+
26
+ The bundle was promoted over the previous local champion on a 57-era untouched holdout:
27
+
28
+ | Metric | v4 | previous champion |
29
+ |---|---:|---:|
30
+ | Mean Numerai CORR | 0.010453 | 0.001821 |
31
+ | Sharpe | 0.7712 | 0.1499 |
32
+ | Positive-era consistency | 75.44% | 52.63% |
33
+ | Maximum drawdown proxy | -0.01360 | -0.02329 |
34
+
35
+ These are historical offline measurements, not live-performance guarantees. The model remains experimental, can decay under regime change, and should not be used to make financial decisions.
36
+
37
+ ## Load and predict
38
+
39
+ Install the pinned runtime dependencies:
40
+
41
+ ```bash
42
+ pip install -r requirements.txt
43
+ ```
44
+
45
+ Download the files and run inference on the public Numerai live and benchmark-model frames:
46
+
47
+ ```python
48
+ from huggingface_hub import hf_hub_download
49
+ import joblib
50
+ import pandas as pd
51
+
52
+ from inference import predict_ranked
53
+
54
+ repo_id = "Noptus/numerai-weekly-v4"
55
+ model_path = hf_hub_download(repo_id, "ensemble_v4.pkl")
56
+ bundle = joblib.load(model_path)
57
+
58
+ live = pd.read_parquet("live.parquet")
59
+ benchmarks = pd.read_parquet("live_benchmark_models.parquet")
60
+ benchmark_columns = [c for c in benchmarks.columns if c != "era"]
61
+ live = live.join(benchmarks[benchmark_columns], how="left")
62
+
63
+ submission = pd.DataFrame(
64
+ {"prediction": predict_ranked(live, bundle)},
65
+ index=live.index,
66
+ )
67
+ submission.index.name = "id"
68
+ submission.to_csv("predictions.csv")
69
+ ```
70
+
71
+ The included command-line entry point performs the same base inference:
72
+
73
+ ```bash
74
+ python inference.py \
75
+ --model ensemble_v4.pkl \
76
+ --live live.parquet \
77
+ --benchmarks live_benchmark_models.parquet \
78
+ --output predictions.csv
79
+ ```
80
+
81
+ The production system derives several slot-specific submissions by applying different feature and benchmark neutralization settings after this base ensemble. Those operational credentials and live submissions are intentionally excluded.
82
+
83
+ ## Reproducibility and safety
84
+
85
+ `manifest.json` records the source revision, metric split, dependency versions, and hashes. Numerai datasets, target labels, live predictions, API credentials, and staking information are not included.
86
+
87
+ The checkpoint uses Python pickle serialization because it contains native LightGBM and CatBoost estimators. Pickle can execute code while loading: verify the SHA-256 and load only artifacts you trust. Reconstructing the component estimators in native, non-pickle formats is planned for a later release.
88
+
89
+ ## Research context
90
+
91
+ Three subsequent frozen-prediction experiments did not displace this champion:
92
+
93
+ - extra tree families were highly redundant with the core (pairwise prediction correlations 0.81–0.94);
94
+ - equal and shrinkage weighting lost to purged walk-forward coordinate ascent;
95
+ - a raw-magnitude residual stack lost to the existing rank blend.
96
+
97
+ Negative results are retained because they narrow the useful next step: seek genuinely different input signal—currently the official v5.3 feature families—rather than adding more tree implementations over the same v5.2 inputs.
98
+
99
+ ## License and use
100
+
101
+ No explicit model or software license has been selected for this first release. Numerai data and benchmark-model files are governed by their own terms and are not redistributed here. Verify the applicable terms before reuse or redistribution.
__pycache__/inference.cpython-311.pyc ADDED
Binary file (10.6 kB). View file
 
inference.py ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Standalone, credential-free inference for the Numerai weekly v4 bundle."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ import joblib
11
+ import numpy as np
12
+ import pandas as pd
13
+ from scipy.stats import norm, rankdata
14
+
15
+
16
+ REQUIRED_COMPONENTS = (
17
+ "benchmark_era_boost",
18
+ "multi_target",
19
+ "residual",
20
+ "catboost",
21
+ )
22
+
23
+
24
+ def load_bundle(path: str | Path) -> dict[str, Any]:
25
+ """Load a trusted model bundle and validate its public inference contract."""
26
+ bundle = joblib.load(Path(path))
27
+ if not isinstance(bundle, dict):
28
+ raise TypeError("expected a dictionary model bundle")
29
+ missing = [name for name in REQUIRED_COMPONENTS if name not in bundle]
30
+ if missing:
31
+ raise ValueError(f"bundle is missing components: {', '.join(missing)}")
32
+ if "calibrated_weights" not in bundle or "config" not in bundle:
33
+ raise ValueError("bundle is missing calibrated_weights or config")
34
+ return bundle
35
+
36
+
37
+ def _columns(bundle: dict[str, Any]) -> tuple[list[str], list[str], list[str]]:
38
+ config = bundle["config"]
39
+ features = config.get("features")
40
+ benchmark_columns = config.get("bench_cols")
41
+ all_columns = config.get("all_feature_cols")
42
+ if not all(isinstance(value, list) for value in (features, benchmark_columns, all_columns)):
43
+ raise ValueError("bundle config does not contain serialized feature-name lists")
44
+ return features, benchmark_columns, all_columns
45
+
46
+
47
+ def _gaussianize(values: np.ndarray) -> np.ndarray:
48
+ ranked = rankdata(values, method="average") / (len(values) + 1)
49
+ return norm.ppf(ranked)
50
+
51
+
52
+ def predict(frame: pd.DataFrame, bundle: dict[str, Any]) -> np.ndarray:
53
+ """Return the exact pre-neutralization Gaussian v4 ensemble prediction."""
54
+ features, _, all_columns = _columns(bundle)
55
+ missing = sorted(set(all_columns) - set(frame.columns))
56
+ if missing:
57
+ preview = ", ".join(missing[:8])
58
+ raise ValueError(f"input is missing {len(missing)} columns; first missing: {preview}")
59
+
60
+ n_rows = len(frame)
61
+ if n_rows < 2:
62
+ raise ValueError("at least two rows are required for cross-sectional ranking")
63
+
64
+ x_full = frame[all_columns].to_numpy()
65
+ x_features = frame[features].to_numpy()
66
+ components: dict[str, np.ndarray] = {}
67
+
68
+ models = bundle["benchmark_era_boost"]
69
+ components["benchmark_era_boost"] = np.mean(
70
+ [model.predict(x_full) for model in models], axis=0
71
+ )
72
+
73
+ target_predictions = [
74
+ rankdata(model.predict(x_full), method="average") / n_rows
75
+ for model in bundle["multi_target"].values()
76
+ ]
77
+ components["multi_target"] = np.mean(target_predictions, axis=0)
78
+
79
+ models = bundle["residual"]
80
+ components["residual"] = np.mean(
81
+ [model.predict(x_features) for model in models], axis=0
82
+ )
83
+
84
+ models = bundle["catboost"]
85
+ components["catboost"] = np.mean(
86
+ [model.predict(x_full) for model in models], axis=0
87
+ )
88
+
89
+ for optional_name in ("xgboost", "lgb_dart"):
90
+ models = bundle.get(optional_name)
91
+ if models:
92
+ components[optional_name] = np.mean(
93
+ [model.predict(x_full) for model in models], axis=0
94
+ )
95
+
96
+ horizon_models = bundle.get("horizon60")
97
+ if horizon_models:
98
+ components["horizon60"] = np.mean(
99
+ [
100
+ rankdata(model.predict(x_full), method="average") / n_rows
101
+ for model in horizon_models.values()
102
+ ],
103
+ axis=0,
104
+ )
105
+
106
+ weights = {
107
+ name: float(weight)
108
+ for name, weight in bundle["calibrated_weights"].items()
109
+ if name in components
110
+ }
111
+ total_weight = sum(weights.values())
112
+ if total_weight <= 0:
113
+ raise ValueError("bundle has no positively weighted active components")
114
+
115
+ ensemble = np.zeros(n_rows, dtype=np.float64)
116
+ for name, weight in weights.items():
117
+ component_rank = rankdata(components[name], method="average") / (n_rows + 1)
118
+ ensemble += (weight / total_weight) * component_rank
119
+ return _gaussianize(ensemble)
120
+
121
+
122
+ def predict_ranked(frame: pd.DataFrame, bundle: dict[str, Any]) -> np.ndarray:
123
+ """Return submission-shaped predictions strictly between zero and one."""
124
+ gaussian_prediction = predict(frame, bundle)
125
+ return rankdata(gaussian_prediction, method="average") / (len(frame) + 1)
126
+
127
+
128
+ def main() -> int:
129
+ parser = argparse.ArgumentParser(description=__doc__)
130
+ parser.add_argument("--model", type=Path, required=True)
131
+ parser.add_argument("--live", type=Path, required=True)
132
+ parser.add_argument("--benchmarks", type=Path, required=True)
133
+ parser.add_argument("--output", type=Path, required=True)
134
+ args = parser.parse_args()
135
+
136
+ bundle = load_bundle(args.model)
137
+ live = pd.read_parquet(args.live)
138
+ benchmarks = pd.read_parquet(args.benchmarks)
139
+ benchmark_columns = [column for column in benchmarks.columns if column != "era"]
140
+ live = live.join(benchmarks[benchmark_columns], how="left")
141
+ predictions = predict_ranked(live, bundle)
142
+
143
+ output = pd.DataFrame({"prediction": predictions}, index=live.index)
144
+ output.index.name = "id"
145
+ args.output.parent.mkdir(parents=True, exist_ok=True)
146
+ output.to_csv(args.output)
147
+ print(f"wrote {len(output):,} predictions to {args.output}")
148
+ return 0
149
+
150
+
151
+ if __name__ == "__main__":
152
+ raise SystemExit(main())
manifest.json ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "release": "v4-20260801-180455",
3
+ "published_date": "2026-08-14",
4
+ "verified_live_round": 1333,
5
+ "source": {
6
+ "repository": "https://github.com/Noptus/numerai-tournament",
7
+ "commit": "19fd7d8f6baea4af96f87d7295c83025fde53984",
8
+ "worktree_had_uncommitted_research_changes": true
9
+ },
10
+ "data": {
11
+ "version": "v5.2",
12
+ "included": false,
13
+ "feature_set": "medium",
14
+ "feature_count": 780,
15
+ "benchmark_model_column_count": 8
16
+ },
17
+ "validation": {
18
+ "calibration_eras": 120,
19
+ "purge_eras": 8,
20
+ "requested_holdout_eras": 60,
21
+ "scored_holdout_eras": 57,
22
+ "mean_corr": 0.010453262665121218,
23
+ "std_corr": 0.01355527777438434,
24
+ "sharpe": 0.771157561470213,
25
+ "consistency": 0.7543859649122807,
26
+ "max_drawdown_proxy": -0.013603461343878933
27
+ },
28
+ "inference_verification": {
29
+ "live_rows": 7016,
30
+ "production_function": "submit_v4.predict_v4",
31
+ "max_absolute_difference": 0.0,
32
+ "exact_array_equal": true,
33
+ "all_finite": true
34
+ },
35
+ "artifacts": {
36
+ "ensemble_v4.pkl": {
37
+ "sha256": "79db5f41f3506e8e10a8b96c60a927f6a9ca202e49e03304ceaa9d304116b8d9"
38
+ },
39
+ "promotion.json": {
40
+ "sha256": "fc158b8bbfc1ec33fdb556357c1b6a47db9423622beaac54aacd60649c573d16"
41
+ }
42
+ },
43
+ "runtime": {
44
+ "python": "3.11.15",
45
+ "numpy": "2.4.6",
46
+ "pandas": "3.0.3",
47
+ "scipy": "1.17.1",
48
+ "scikit_learn": "1.9.0",
49
+ "lightgbm": "4.6.0",
50
+ "catboost": "1.2.10",
51
+ "joblib": "1.5.3"
52
+ }
53
+ }
requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ huggingface-hub>=0.34,<2
2
+ joblib==1.5.3
3
+ numpy==2.4.6
4
+ pandas==3.0.3
5
+ scipy==1.17.1
6
+ scikit-learn==1.9.0
7
+ lightgbm==4.6.0
8
+ catboost==1.2.10
9
+ pyarrow>=14