Views
No views yet
p60 P(player reaches 60 minutes)
non60 E[points | under 60 minutes]
bucket P(bucket k | 60+ minutes) k = 0:(<=0) 1:(1-2) 2:(3-9) 3:(>=10)
bucketreg E[points | 60+ minutes, bucket k] four regressors
pred_if60 = Σ_k calibrated_p_k × bucketreg_k
pred = p60 × pred_if60 + (1 - p60) × non60GKP = base
DEF / MID / FWD = 0.75 × base + 0.25 × direct
output clipped at 0.0| Path | What it is |
|---|---|
base/*.ubj | The 28 multibucket models, XGBoost binary format |
base/bucket_calibration.json | Gamma + weights applied to bucket probabilities |
blend/direct_{GKP,DEF,MID,FWD}.json | The direct regressors |
blend/blend.json | Blend weights and the exact formula above |
blend/feature_cols.json | The 251 features, in the order the models expect |
walk_forward_accuracy.json | Per-gameweek evaluation, see below |
direct_GKP.json ships for reproducibility but is not used at inference —
blend.json lists GKP under excluded_positions..ubj binary, .json text), not pickle.
Both load with Booster.load_model in any XGBoost ≥ 2.0 and carry no executable
code, so you are not trusting us not to have put something nasty in a pickle.1from huggingface_hub import snapshot_download
2import xgboost as xgb, json, numpy as np
3
4path = snapshot_download("Qazybek/smartplay-fpl-v12")
5cols = json.load(open(f"{path}/blend/feature_cols.json")) # 251, order matters
6blend = json.load(open(f"{path}/blend/blend.json"))
7
8# X must be a (n, 251) array whose columns are `cols`, in that order.
9def predict(X, position):
10 def load(p):
11 b = xgb.Booster(); b.load_model(p); return b
12 d = xgb.DMatrix(X, feature_names=cols)
13
14 p60 = load(f"{path}/base/p60_{position}.ubj").predict(d)
15 non60 = np.clip(load(f"{path}/base/non60_{position}.ubj").predict(d), 0, None)
16 probs = load(f"{path}/base/bucket_{position}.ubj").predict(d) # (n, 4)
17
18 cal = json.load(open(f"{path}/base/bucket_calibration.json"))[position]
19 w = np.array([cal["w0"], cal["w1"], cal["w2"], cal["w3"]])
20 p = np.clip(probs, 1e-12, 1.0) ** cal["gamma"] * w
21 probs = p / p.sum(1, keepdims=True)
22
23 bucket_preds = np.column_stack([
24 np.clip(load(f"{path}/base/bucketreg_{position}_{k}.ubj").predict(d), 0, None)
25 for k in range(4)
26 ])
27 base = p60 * (probs * bucket_preds).sum(1) + (1 - p60) * non60
28
29 if position in blend["excluded_positions"]:
30 return np.clip(base, 0, None)
31 direct = load(f"{path}/blend/direct_{position}.json").predict(d)
32 return np.clip(blend["multibucket_weight"] * base
33 + blend["direct_weight"] * direct, 0, None)walk_forward_accuracy.json.| Metric | v12 | previous version |
|---|---|---|
| MAE | 0.979 | 0.994 |
| NDCG@10 | 0.380 | 0.352 |
| Spearman | 0.729 | 0.728 |
1@software{smartplay_fpl_v12,
2 title = {SmartPlay v12: Expected Points for Fantasy Premier League},
3 author = {SmartPlay},
4 year = {2026},
5 url = {https://huggingface.co/Qazybek/smartplay-fpl-v12}
6}