pablogrois Claude Opus 4.8 commited on
Commit
01fdb49
·
1 Parent(s): d00f1ea

Predicción de cruce en reporte pre-partido: bloque defensivo + pasillo de ataque + xT

Browse files

Agrega modelos LightGBM (bloque defensivo, pasillo de ataque, xT por pasillo) al
reporte pre-partido: para A y B dibuja la cancha de ataque por pasillos y la de
bloque defensivo (bajo/medio/alto) + un insight del cruce. Inferencia con features
rolling 'a la fecha' precalculadas. No rompe el reporte si faltan datos.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

.gitignore CHANGED
@@ -34,3 +34,9 @@ vendor/data/processed/
34
  !vendor/data/modeling/attack_prediction_dataset.parquet
35
  !vendor/data/modeling/attack_matchup_gnn_bundle.pt
36
  !vendor/data/modeling/pv_distribution_gnn_bundle.pt
 
 
 
 
 
 
 
34
  !vendor/data/modeling/attack_prediction_dataset.parquet
35
  !vendor/data/modeling/attack_matchup_gnn_bundle.pt
36
  !vendor/data/modeling/pv_distribution_gnn_bundle.pt
37
+ # Modelos de cruce (bloque defensivo · pasillo de ataque · xT) + features para inferencia
38
+ !vendor/data/modeling/model_bloques__*.txt
39
+ !vendor/data/modeling/model_pasillos__*.txt
40
+ !vendor/data/modeling/model_peligro_pasillo__*.txt
41
+ !vendor/data/modeling/team_features_latest.parquet
42
+ !vendor/data/modeling/matchup_feat_cols.json
scripts/build_matchup_features.py ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Precalcula, por equipo (liga, temporada), su vector de features rolling 'a la fecha'
2
+ (la última fila disponible) para alimentar la predicción de cruce A-vs-B en el reporte
3
+ pre-partido, sin tener que re-correr todo el pipeline en runtime.
4
+
5
+ Sale:
6
+ vendor/data/modeling/team_features_latest.parquet (1 fila por equipo-temporada)
7
+ vendor/data/modeling/matchup_feat_cols.json (orden exacto de features del modelo)
8
+
9
+ Uso: python scripts/build_matchup_features.py
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import importlib.util
14
+ import json
15
+ from pathlib import Path
16
+
17
+ import pandas as pd
18
+
19
+ ROOT = Path(__file__).resolve().parents[1]
20
+ spec = importlib.util.spec_from_file_location("tm", ROOT / "scripts" / "train_models.py")
21
+ tm = importlib.util.module_from_spec(spec); spec.loader.exec_module(tm)
22
+ OUT = ROOT / "vendor" / "data" / "modeling"
23
+
24
+
25
+ def main() -> None:
26
+ df = pd.read_parquet(tm.DATA)
27
+ blk = df.assign(_b=df[tm.DEF].sum(axis=1) > 0).groupby("Competencia")["_b"].mean()
28
+ df = df[df["Competencia"].isin(blk[blk > 0.5].index)].copy()
29
+ m, feat = tm._assemble(df)
30
+ m = m.merge(df[["matchId", "teamId", "fecha", "TeamName"]].drop_duplicates(),
31
+ on=["matchId", "teamId"], how="left")
32
+
33
+ # columnas que definen el "estado a la fecha" de un equipo (lado propio)
34
+ self_cols = [c for c in m.columns if c.startswith(("self_", "selfH_", "selfA_"))]
35
+ keep = ["Competencia", "Temporada", "teamId", "TeamName", "fecha", "formation"] + self_cols
36
+ keep = [c for c in keep if c in m.columns]
37
+
38
+ # última fila por (liga, temporada, equipo) — su forma más reciente
39
+ m = m.sort_values(["Competencia", "Temporada", "teamId", "fecha", "matchId"])
40
+ latest = m[keep].dropna(subset=["self_def_H"]).groupby(
41
+ ["Competencia", "Temporada", "teamId"], as_index=False).last()
42
+
43
+ latest.to_parquet(OUT / "team_features_latest.parquet", index=False)
44
+ (OUT / "matchup_feat_cols.json").write_text(json.dumps(feat, ensure_ascii=False))
45
+ print(f"team_features_latest: {len(latest)} equipos-temporada | self_cols={len(self_cols)} | feat={len(feat)}")
46
+ print("ligas:", latest['Competencia'].nunique(), "| ej:",
47
+ latest[latest.Competencia.eq('Liga Profesional Argentina')]['TeamName'].dropna().unique()[:6].tolist())
48
+
49
+
50
+ if __name__ == "__main__":
51
+ main()
scripts/train_models.py ADDED
@@ -0,0 +1,206 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Entrena los dos modelos (bloque defensivo y pasillos) sobre el dataset RICO
2
+ matchteam_dataset.parquet. Arma features SIN leakage:
3
+ - ELO propio por (liga,temporada), pre-partido (de los resultados).
4
+ - Rolling "hasta la fecha" (promedio expandido de partidos PREVIOS) del equipo y del
5
+ rival, y splits LOCAL / VISITA (cómo juega/resulta de local vs visitante).
6
+ - Resultados rolling (win%, goles a favor/en contra) all/home/away.
7
+ - One-hot de la formación del partido (info pre-partido).
8
+ LightGBM por componente, normalizado a simplex; baseline = promedio propio.
9
+
10
+ Uso: python scripts/train_models.py
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from pathlib import Path
16
+ import sys
17
+
18
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
19
+
20
+ import lightgbm as lgb
21
+ import numpy as np
22
+ import pandas as pd
23
+
24
+ DATA = Path(__file__).resolve().parents[1] / "vendor" / "data" / "modeling" / "matchteam_dataset.parquet"
25
+ OUTDIR = DATA.parent
26
+ DEF = ["def_H", "def_M", "def_L"]
27
+ ATK = ["atk_Der", "atk_Centro", "atk_Izq"]
28
+ OFFXT = [f"offxt_{b}_{l}" for b in "HML" for l in ["Der", "Centro", "Izq"]]
29
+ DEFXT = [f"defxt_{b}_{l}" for b in "HML" for l in ["Der", "Centro", "Izq"]]
30
+ XTLANE = ["xt_lane_Der", "xt_lane_Centro", "xt_lane_Izq"] # xT absoluto por pasillo (objetivo Modelo 2-b)
31
+ DIST_GROUPS = {"def": DEF, "atk": ATK, "offxt": OFFXT, "defxt": DEFXT}
32
+ SCALARS = ["sh_buildup", "sh_counter", "sh_direct", "sh_setpiece", "sh_recovery", "sh_progr",
33
+ "cross_pct", "pct_long", "pct_short", "pct_pass_ok", "n_attacks", "xt_total",
34
+ "xg_total", "xt_per_attack", "n_seq", "n_pass", "n_shots", "avg_x", "avg_y",
35
+ "share_opp_half", "recovery_height", "n_progr", "goals_for", "goals_against", "win"] + XTLANE
36
+ RNG = np.random.default_rng(7)
37
+
38
+
39
+ def _norm_df(df, cols):
40
+ s = df[cols].sum(axis=1)
41
+ out = df[cols].div(s.where(s > 0), axis=0)
42
+ return out, s
43
+
44
+
45
+ def _elo(df: pd.DataFrame) -> pd.Series:
46
+ """ELO pre-partido por (liga,temporada). Devuelve serie alineada al índice de df."""
47
+ K, HOME = 24.0, 60.0
48
+ elo = pd.Series(np.nan, index=df.index)
49
+ for _, g in df.groupby(["Competencia", "Temporada"], sort=False):
50
+ rating: dict = {}
51
+ # ordenar por fecha y partido; procesar cada partido (2 filas) una vez
52
+ g = g.sort_values(["fecha", "matchId"])
53
+ for mid, mm in g.groupby("matchId", sort=False):
54
+ if len(mm) != 2:
55
+ for i in mm.index:
56
+ elo.loc[i] = rating.get(mm.loc[i, "teamId"], 1500.0)
57
+ continue
58
+ i1, i2 = mm.index
59
+ t1, t2 = mm.loc[i1, "teamId"], mm.loc[i2, "teamId"]
60
+ r1, r2 = rating.get(t1, 1500.0), rating.get(t2, 1500.0)
61
+ elo.loc[i1], elo.loc[i2] = r1, r2 # pre-partido
62
+ h1 = HOME if mm.loc[i1, "is_home"] else 0.0
63
+ h2 = HOME if mm.loc[i2, "is_home"] else 0.0
64
+ e1 = 1.0 / (1.0 + 10 ** ((r2 - r1 - h1 + h2) / 400.0))
65
+ gf1, ga1 = mm.loc[i1, "goals_for"], mm.loc[i1, "goals_against"]
66
+ s1 = 0.5 if pd.isna(gf1) or pd.isna(ga1) or gf1 == ga1 else (1.0 if gf1 > ga1 else 0.0)
67
+ gd = 1.0 if pd.isna(gf1) or pd.isna(ga1) else max(1.0, abs(gf1 - ga1)) ** 0.5
68
+ rating[t1] = r1 + K * gd * (s1 - e1)
69
+ rating[t2] = r2 + K * gd * ((1 - s1) - (1 - e1))
70
+ return elo
71
+
72
+
73
+ def _prep(df: pd.DataFrame) -> pd.DataFrame:
74
+ df = df.copy()
75
+ df["win"] = np.where(df["goals_for"] > df["goals_against"], 1.0,
76
+ np.where(df["goals_for"] < df["goals_against"], 0.0, 0.5))
77
+ # xT absoluto por pasillo = suma del xT por bloque en cada pasillo
78
+ for ln in ["Der", "Centro", "Izq"]:
79
+ df[f"xt_lane_{ln}"] = df[[f"offxt_{b}_{ln}" for b in "HML"]].sum(axis=1)
80
+ # normalizar grupos distribucionales a shares (para features de estilo/peligro)
81
+ feat = {}
82
+ for name, cols in DIST_GROUPS.items():
83
+ nd, _ = _norm_df(df, cols)
84
+ for c in cols:
85
+ feat["f_" + c] = nd[c]
86
+ for c in SCALARS:
87
+ feat["f_" + c] = pd.to_numeric(df[c], errors="coerce")
88
+ F = pd.DataFrame(feat, index=df.index)
89
+ df = pd.concat([df, F], axis=1)
90
+ df["elo"] = _elo(df)
91
+ return df
92
+
93
+
94
+ def _rolling(df: pd.DataFrame) -> pd.DataFrame:
95
+ fcols = [c for c in df.columns if c.startswith("f_")]
96
+ df = df.sort_values(["Competencia", "Temporada", "teamId", "fecha", "matchId"]).copy()
97
+ keys = ["Competencia", "Temporada", "teamId"]
98
+
99
+ def _prior(frame): # expanding mean de partidos previos
100
+ return frame.groupby([df[k] for k in keys], group_keys=False).apply(lambda x: x.shift(1).expanding().mean())
101
+
102
+ roll_all = _prior(df[fcols]); roll_all.columns = ["self_" + c[2:] for c in fcols]
103
+ # splits local/visita: expanding sobre el subconjunto, luego ffill al resto
104
+ def _split(is_home_val):
105
+ sub = df[fcols].where(df["is_home"] == is_home_val)
106
+ r = sub.groupby([df[k] for k in keys], group_keys=False).apply(lambda x: x.shift(1).expanding().mean())
107
+ r = r.groupby([df[k] for k in keys], group_keys=False).ffill()
108
+ return r
109
+ rh = _split(True); rh.columns = ["selfH_" + c[2:] for c in fcols]
110
+ ra = _split(False); ra.columns = ["selfA_" + c[2:] for c in fcols]
111
+ eloprior = df.groupby(keys, group_keys=False)["elo"].apply(lambda x: x) # ya es pre-partido
112
+ out = pd.concat([df[keys + ["matchId", "rival_teamId", "is_home", "formation"] + DEF + ATK + XTLANE],
113
+ df["elo"].rename("self_elo"), roll_all, rh, ra], axis=1)
114
+ return out
115
+
116
+
117
+ def _assemble(df: pd.DataFrame):
118
+ df = _prep(df)
119
+ roll = _rolling(df)
120
+ selfcols = [c for c in roll.columns if c.startswith(("self_", "selfH_", "selfA_"))]
121
+ oppmap = roll[["matchId", "teamId"] + selfcols].rename(
122
+ columns={"teamId": "rival_teamId", **{c: "opp_" + c for c in selfcols}})
123
+ m = roll.merge(oppmap, on=["matchId", "rival_teamId"], how="left")
124
+ m["is_home"] = m["is_home"].astype(float)
125
+ m["elo_diff"] = m["self_elo"] - m["opp_self_elo"]
126
+ # one-hot formación (top 12 + otras)
127
+ top = m["formation"].astype(str).value_counts().head(12).index
128
+ for f in top:
129
+ m[f"form_{f}"] = (m["formation"].astype(str) == f).astype(float)
130
+ feat_cols = (selfcols + [c for c in m.columns if c.startswith("opp_")]
131
+ + ["is_home", "elo_diff"] + [f"form_{f}" for f in top])
132
+ return m, feat_cols
133
+
134
+
135
+ def _kl(a, b):
136
+ a = np.clip(a, 1e-9, None); b = np.clip(b, 1e-9, None)
137
+ return float(np.mean(np.sum(a * (np.log(a) - np.log(b)), 1)))
138
+
139
+
140
+ def _train_one(m, feat_cols, target_cols, name):
141
+ sub = m.dropna(subset=["self_" + c for c in target_cols]).copy() # con historial rolling
142
+ tgt, tot = _norm_df(sub, target_cols)
143
+ keep = tot.values > 0
144
+ sub = sub[keep]; Y = tgt[keep].to_numpy(dtype=np.float32)
145
+ base = sub[["self_" + c for c in target_cols]].to_numpy(dtype=np.float32)
146
+ base = base / np.clip(base.sum(1, keepdims=True), 1e-9, None)
147
+ n = len(sub); idx = RNG.permutation(n); cut = int(n * 0.8); tr, te = idx[:cut], idx[cut:]
148
+ Xtr, Xte = sub[feat_cols].iloc[tr], sub[feat_cols].iloc[te]
149
+ vm = np.zeros(len(tr), bool); vm[RNG.permutation(len(tr))[:int(len(tr) * 0.12)]] = True
150
+ print(f"\n=== Modelo {name}: {n} (test {len(te)}) | {len(feat_cols)} features ===")
151
+ preds = []
152
+ for j in range(Y.shape[1]):
153
+ mdl = lgb.LGBMRegressor(n_estimators=700, learning_rate=0.025, num_leaves=31,
154
+ subsample=0.8, colsample_bytree=0.5, min_child_samples=40, verbose=-1)
155
+ mdl.fit(Xtr[~vm], Y[tr][~vm, j], eval_set=[(Xtr[vm], Y[tr][vm, j])],
156
+ callbacks=[lgb.early_stopping(50, verbose=False)])
157
+ preds.append(np.clip(mdl.predict(Xte), 0, None))
158
+ mdl.booster_.save_model(str(OUTDIR / f"model_{name}__{target_cols[j]}.txt"))
159
+ P = np.vstack(preds).T; P = P / np.clip(P.sum(1, keepdims=True), 1e-9, None)
160
+ mae = lambda a, b: float(np.mean(np.abs(a - b)))
161
+ print(f" modelo : MAE={mae(Y[te], P):.4f} KL={_kl(Y[te], P):.4f}")
162
+ print(f" baseline: MAE={mae(Y[te], base[te]):.4f} KL={_kl(Y[te], base[te]):.4f}")
163
+
164
+
165
+ def _train_abs(m, feat_cols, target_cols, name):
166
+ """Regresión de VALORES ABSOLUTOS (no shares). Eval MAE/RMSE vs promedio propio."""
167
+ sub = m.dropna(subset=["self_" + c for c in target_cols]).copy()
168
+ Y = sub[target_cols].to_numpy(dtype=np.float32)
169
+ base = sub[["self_" + c for c in target_cols]].to_numpy(dtype=np.float32)
170
+ n = len(sub); idx = RNG.permutation(n); cut = int(n * 0.8); tr, te = idx[:cut], idx[cut:]
171
+ Xtr, Xte = sub[feat_cols].iloc[tr], sub[feat_cols].iloc[te]
172
+ vm = np.zeros(len(tr), bool); vm[RNG.permutation(len(tr))[:int(len(tr) * 0.12)]] = True
173
+ print(f"\n=== Modelo {name} (xT absoluto): {n} (test {len(te)}) ===")
174
+ preds = []
175
+ for j in range(Y.shape[1]):
176
+ mdl = lgb.LGBMRegressor(n_estimators=700, learning_rate=0.025, num_leaves=31,
177
+ subsample=0.8, colsample_bytree=0.5, min_child_samples=40, verbose=-1)
178
+ mdl.fit(Xtr[~vm], Y[tr][~vm, j], eval_set=[(Xtr[vm], Y[tr][vm, j])],
179
+ callbacks=[lgb.early_stopping(50, verbose=False)])
180
+ preds.append(np.clip(mdl.predict(Xte), 0, None))
181
+ mdl.booster_.save_model(str(OUTDIR / f"model_{name}__{target_cols[j]}.txt"))
182
+ P = np.vstack(preds).T
183
+ mae = lambda a, b: float(np.mean(np.abs(a - b)))
184
+ rmse = lambda a, b: float(np.sqrt(np.mean((a - b) ** 2)))
185
+ print(f" modelo : MAE={mae(Y[te], P):.4f} RMSE={rmse(Y[te], P):.4f}")
186
+ print(f" baseline: MAE={mae(Y[te], base[te]):.4f} RMSE={rmse(Y[te], base[te]):.4f}")
187
+ print(f" (xT medio real por pasillo: {np.round(Y[te].mean(0), 3)})")
188
+
189
+
190
+ def main():
191
+ import argparse
192
+ ap = argparse.ArgumentParser(); ap.add_argument("--leagues", nargs="*", default=None); a = ap.parse_args()
193
+ df = pd.read_parquet(DATA)
194
+ if a.leagues:
195
+ df = df[df["Competencia"].isin(a.leagues)].copy()
196
+ blk = (df.assign(_b=df[DEF].sum(axis=1) > 0).groupby("Competencia")["_b"].mean())
197
+ df = df[df["Competencia"].isin(blk[blk > 0.5].index)].copy()
198
+ print("dataset:", len(df), "filas |", df["Competencia"].nunique(), "ligas |", df["matchId"].nunique(), "partidos")
199
+ m, feat = _assemble(df)
200
+ _train_one(m, feat, DEF, "bloques")
201
+ _train_one(m, feat, ATK, "pasillos")
202
+ _train_abs(m, feat, XTLANE, "peligro_pasillo")
203
+
204
+
205
+ if __name__ == "__main__":
206
+ main()
src/racing_reports/reports/matchup_models.py ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Predicción de cruce A-vs-B a partir de los modelos LightGBM entrenados
2
+ (bloque defensivo, pasillo de ataque y xT por pasillo).
3
+
4
+ Para cada equipo usa su vector de features rolling 'a la fecha' (precalculado en
5
+ ``team_features_latest.parquet``) y arma la fila del enfrentamiento: lado propio del
6
+ equipo + lado rival del oponente. Devuelve, para A y para B:
7
+ - dist. de bloque defensivo (alto / medio / bajo) — cómo va a defender
8
+ - dist. de ataque por pasillo (izq / centro / der) — por dónde va a atacar
9
+ - xT absoluto por pasillo (izq / centro / der) — peligro esperado por pasillo
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import threading
15
+ from pathlib import Path
16
+
17
+ import numpy as np
18
+ import pandas as pd
19
+
20
+ from racing_reports import vendor_env
21
+
22
+ _MODELDIR = vendor_env.DATA_DIR / "modeling"
23
+ _FEATS = _MODELDIR / "team_features_latest.parquet"
24
+ _FEATCOLS = _MODELDIR / "matchup_feat_cols.json"
25
+
26
+ DEF = ["def_H", "def_M", "def_L"] # alto, medio, bajo
27
+ ATK = ["atk_Der", "atk_Centro", "atk_Izq"] # derecha, centro, izquierda
28
+ XT = ["xt_lane_Der", "xt_lane_Centro", "xt_lane_Izq"]
29
+ LANES = ["Izq", "Centro", "Der"]
30
+
31
+ _LOCK = threading.Lock()
32
+ _CACHE: dict = {}
33
+
34
+
35
+ def _load():
36
+ with _LOCK:
37
+ if _CACHE:
38
+ return _CACHE
39
+ if not _FEATS.exists() or not _FEATCOLS.exists():
40
+ _CACHE["ok"] = False
41
+ return _CACHE
42
+ import lightgbm as lgb
43
+ feats = pd.read_parquet(_FEATS)
44
+ feat_cols = json.loads(_FEATCOLS.read_text(encoding="utf-8"))
45
+ boosters = {}
46
+ for name, cols in (("bloques", DEF), ("pasillos", ATK), ("peligro_pasillo", XT)):
47
+ paths = [_MODELDIR / f"model_{name}__{c}.txt" for c in cols]
48
+ if all(p.exists() for p in paths):
49
+ boosters[name] = [lgb.Booster(model_file=str(p)) for p in paths]
50
+ _CACHE.update(ok=bool(boosters) and len(feats) > 0, feats=feats,
51
+ feat_cols=feat_cols, boosters=boosters)
52
+ return _CACHE
53
+
54
+
55
+ def available() -> bool:
56
+ return bool(_load().get("ok"))
57
+
58
+
59
+ def _row_for(feats: pd.DataFrame, league: str, season: str | None, team: str):
60
+ """Última fila de features del equipo (por nombre) en la liga/temporada."""
61
+ q = feats[feats["Competencia"].astype(str) == str(league)]
62
+ q = q[q["TeamName"].astype(str).str.casefold() == str(team).casefold()]
63
+ if season:
64
+ qs = q[q["Temporada"].astype(str) == str(season)]
65
+ q = qs if not qs.empty else q
66
+ if q.empty:
67
+ return None
68
+ return q.sort_values("fecha").iloc[-1]
69
+
70
+
71
+ def _build_feature_row(self_r, opp_r, is_home: bool, feat_cols: list[str]) -> np.ndarray:
72
+ """Arma el vector en el orden exacto de feat_cols para un equipo (self) vs su rival (opp)."""
73
+ vals = {}
74
+ for c in feat_cols:
75
+ if c == "is_home":
76
+ vals[c] = 1.0 if is_home else 0.0
77
+ elif c == "elo_diff":
78
+ vals[c] = float(self_r.get("self_elo", 1500.0)) - float(opp_r.get("self_elo", 1500.0))
79
+ elif c.startswith("form_"):
80
+ vals[c] = 1.0 if str(self_r.get("formation")) == c[len("form_"):] else 0.0
81
+ elif c.startswith("opp_"):
82
+ base = c[len("opp_"):]
83
+ vals[c] = float(opp_r.get(base)) if pd.notna(opp_r.get(base)) else np.nan
84
+ else: # self / selfH / selfA
85
+ vals[c] = float(self_r.get(c)) if pd.notna(self_r.get(c)) else np.nan
86
+ return np.array([vals[c] for c in feat_cols], dtype=np.float64).reshape(1, -1)
87
+
88
+
89
+ def _predict_side(boosters, X, dist: bool):
90
+ p = np.array([b.predict(X)[0] for b in boosters], dtype=float)
91
+ p = np.clip(p, 0, None)
92
+ if dist:
93
+ s = p.sum()
94
+ p = p / s if s > 0 else np.full_like(p, 1 / len(p))
95
+ return p
96
+
97
+
98
+ def predict_matchup(league: str, season: str | None, team_a: str, team_b: str,
99
+ a_is_home: bool = True) -> dict | None:
100
+ """Predicción del enfrentamiento. Devuelve dict con A y B, o None si faltan modelos
101
+ o no se encuentra alguno de los equipos."""
102
+ c = _load()
103
+ if not c.get("ok"):
104
+ return None
105
+ feats, feat_cols, B = c["feats"], c["feat_cols"], c["boosters"]
106
+ if not all(k in B for k in ("bloques", "pasillos", "peligro_pasillo")):
107
+ return None
108
+ ra = _row_for(feats, league, season, team_a)
109
+ rb = _row_for(feats, league, season, team_b)
110
+ if ra is None or rb is None:
111
+ return {"error": f"Sin datos del modelo para "
112
+ + (team_a if ra is None else team_b) + f" en {league} {season or ''}".strip()}
113
+
114
+ def side(self_r, opp_r, is_home):
115
+ X = _build_feature_row(self_r, opp_r, is_home, feat_cols)
116
+ defb = _predict_side(B["bloques"], X, dist=True) # H, M, L
117
+ atk = _predict_side(B["pasillos"], X, dist=True) # Der, Centro, Izq
118
+ xt = _predict_side(B["peligro_pasillo"], X, dist=False)
119
+ return {
120
+ "bloque": {"alto": float(defb[0]), "medio": float(defb[1]), "bajo": float(defb[2])},
121
+ "pasillo": {"Der": float(atk[0]), "Centro": float(atk[1]), "Izq": float(atk[2])},
122
+ "xt": {"Der": float(xt[0]), "Centro": float(xt[1]), "Izq": float(xt[2])},
123
+ }
124
+
125
+ return {
126
+ "league": league, "season": season, "team_a": team_a, "team_b": team_b,
127
+ "a": side(ra, rb, a_is_home),
128
+ "b": side(rb, ra, not a_is_home),
129
+ }
src/racing_reports/reports/matchup_render.py ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Dibujo de la predicción de cruce sobre canchas:
2
+ - Ataque por pasillo: media cancha con 3 pasillos (Izq/Centro/Der), % y xT.
3
+ - Bloque defensivo: el 75% defensivo dividido en bajo (verde, cerca del arco propio),
4
+ medio (naranja) y alto (rojo), sombreado según el % previsto.
5
+ Devuelve figuras de matplotlib (una por cancha)."""
6
+ from __future__ import annotations
7
+
8
+ import matplotlib
9
+ matplotlib.use("Agg")
10
+ import matplotlib.pyplot as plt
11
+ from mplsoccer import VerticalPitch
12
+
13
+ PITCH = dict(pitch_type="opta", line_color="#cdd6e6", linewidth=1.2)
14
+ GREEN, ORANGE, RED = "#2bb673", "#f5912b", "#e0392b"
15
+ LANE_C = "#2E74D6"
16
+ TEXT = "#10243f"
17
+
18
+
19
+ def _pitch(ax, half=False):
20
+ p = VerticalPitch(half=half, pad_top=2, pitch_color="none", **PITCH)
21
+ p.draw(ax=ax)
22
+ return p
23
+
24
+
25
+ def attack_fig(side: dict, team: str) -> plt.Figure:
26
+ """Media cancha de ataque con los 3 pasillos sombreados por % y rotulados con % y xT."""
27
+ fig, ax = plt.subplots(figsize=(4.2, 5.2))
28
+ fig.patch.set_facecolor("white")
29
+ p = _pitch(ax, half=True)
30
+ pas, xt = side["pasillo"], side["xt"]
31
+ mx = max(pas.values()) or 1.0
32
+ # x = ancho (0-100). Izquierda a la izquierda del espectador.
33
+ bounds = [(0, 33.33, "Der"), (33.33, 66.67, "Centro"), (66.67, 100, "Izq")]
34
+ for x0, x1, lane in bounds:
35
+ frac = pas[lane]
36
+ ax.fill_betweenx([50, 100], x0, x1, color=LANE_C, alpha=0.12 + 0.55 * frac / mx, zorder=0.5)
37
+ ax.plot([x0, x0], [50, 100], color="#9fb0c9", lw=1, ls="--", zorder=1)
38
+ ax.text((x0 + x1) / 2, 88, f"{frac*100:.0f}%", ha="center", va="center",
39
+ fontsize=17, fontweight="bold", color=TEXT, zorder=3)
40
+ ax.text((x0 + x1) / 2, 79, lane, ha="center", va="center", fontsize=10.5, color="#42546e", zorder=3)
41
+ ax.text((x0 + x1) / 2, 73, f"xT {xt[lane]:.2f}", ha="center", va="center",
42
+ fontsize=9, color="#5a6b85", zorder=3)
43
+ ax.set_title(f"{team}\nPor dónde ataca", fontsize=12.5, fontweight="bold", color=TEXT, pad=6)
44
+ fig.tight_layout()
45
+ return fig
46
+
47
+
48
+ def defense_fig(side: dict, team: str) -> plt.Figure:
49
+ """Cancha con el 75% defensivo en 3 bandas: bajo (verde) cerca del arco propio,
50
+ medio (naranja) y alto (rojo); sombreado por % previsto."""
51
+ fig, ax = plt.subplots(figsize=(4.2, 5.2))
52
+ fig.patch.set_facecolor("white")
53
+ _pitch(ax, half=False)
54
+ blo = side["bloque"]
55
+ mx = max(blo.values()) or 1.0
56
+ # arco propio abajo (y=0). Bandas a lo largo: bajo [0-25], medio [25-50], alto [50-75].
57
+ bands = [(0, 25, "bajo", GREEN), (25, 50, "medio", ORANGE), (50, 75, "alto", RED)]
58
+ for y0, y1, name, col in bands:
59
+ frac = blo[name]
60
+ ax.fill_between([0, 100], y0, y1, color=col, alpha=0.18 + 0.6 * frac / mx, zorder=0.5)
61
+ ax.plot([0, 100], [y1, y1], color="#9fb0c9", lw=0.8, ls="--", zorder=1)
62
+ ax.text(50, (y0 + y1) / 2 + 3.5, f"{frac*100:.0f}%", ha="center", va="center",
63
+ fontsize=16, fontweight="bold", color=TEXT, zorder=3)
64
+ ax.text(50, (y0 + y1) / 2 - 4, f"Bloque {name}", ha="center", va="center",
65
+ fontsize=10.5, color="#33455f", zorder=3)
66
+ ax.annotate("arco propio", (50, 1.5), ha="center", va="bottom", fontsize=8.5,
67
+ color="#7a889e", zorder=3)
68
+ ax.set_title(f"{team}\nCómo defiende", fontsize=12.5, fontweight="bold", color=TEXT, pad=6)
69
+ fig.tight_layout()
70
+ return fig
71
+
72
+
73
+ def insight(pred: dict) -> str:
74
+ """Lectura breve del cruce a partir de las predicciones de A y B."""
75
+ a, b = pred["a"], pred["b"]
76
+ ta, tb = pred["team_a"], pred["team_b"]
77
+ lane_es = {"Izq": "izquierda", "Centro": "el centro", "Der": "derecha"}
78
+ blo_max = lambda s: max(s["bloque"], key=s["bloque"].get)
79
+ xt_max = lambda s: max(s["xt"], key=s["xt"].get)
80
+ out = []
81
+ # quién genera más peligro
82
+ xa, xb = sum(a["xt"].values()), sum(b["xt"].values())
83
+ mas, men = (ta, tb) if xa >= xb else (tb, ta)
84
+ out.append(f"El modelo espera más peligro total de <b>{mas}</b> ({max(xa,xb):.2f} vs {min(xa,xb):.2f} de xT).")
85
+ # pasillo principal de cada uno
86
+ out.append(f"<b>{ta}</b> apuntaría sobre todo por {lane_es[xt_max(a)]} "
87
+ f"(xT {a['xt'][xt_max(a)]:.2f}); <b>{tb}</b>, por {lane_es[xt_max(b)]} "
88
+ f"(xT {b['xt'][xt_max(b)]:.2f}).")
89
+ # bloque defensivo de cada uno
90
+ out.append(f"Defensivamente, <b>{ta}</b> se pararía mayormente en bloque {blo_max(a)} "
91
+ f"y <b>{tb}</b> en bloque {blo_max(b)}.")
92
+ # coincidencia de pasillo (ambos cargan el mismo lado)
93
+ if xt_max(a) == xt_max(b) and xt_max(a) != "Centro":
94
+ out.append(f"Ojo: ambos cargan el mismo sector ({lane_es[xt_max(a)]}), va a ser una zona disputada.")
95
+ return " ".join(out)
src/racing_reports/reports/pre_match.py CHANGED
@@ -191,6 +191,53 @@ def generate(
191
  output_path=out_path,
192
  )
193
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
194
  return ReportBundle(
195
  report_type="pre_match",
196
  html_path=Path(result_path),
 
191
  output_path=out_path,
192
  )
193
 
194
+ # --- Predicción de cruce (modelos LightGBM): canchas de ataque/defensa + insight ---
195
+ try:
196
+ from racing_reports.reports import matchup_models, matchup_render
197
+ from racing_reports.reports.common import save_figure
198
+
199
+ if matchup_models.available():
200
+ pred = matchup_models.predict_matchup(league, season, home_name, away_name, a_is_home=True)
201
+ if pred and "error" not in pred:
202
+ sec = "Predicción del modelo (cruce)"
203
+ fig_specs = [
204
+ ("modelo_ataque_home", f"Por dónde ataca — {home_name}", matchup_render.attack_fig(pred["a"], home_name)),
205
+ ("modelo_defensa_home", f"Cómo defiende — {home_name}", matchup_render.defense_fig(pred["a"], home_name)),
206
+ ("modelo_ataque_away", f"Por dónde ataca — {away_name}", matchup_render.attack_fig(pred["b"], away_name)),
207
+ ("modelo_defensa_away", f"Cómo defiende — {away_name}", matchup_render.defense_fig(pred["b"], away_name)),
208
+ ]
209
+ for nm, ttl, fig in fig_specs:
210
+ png_path, svg_path = save_figure(fig, out_path.parent, nm)
211
+ figures.append(FigureArtifact(name=nm, title=ttl, section=sec,
212
+ png_path=png_path, svg_path=svg_path))
213
+ # tabla resumen con números + insight
214
+ rows = []
215
+ for side, team in ((pred["a"], home_name), (pred["b"], away_name)):
216
+ rows.append({
217
+ "Equipo": team,
218
+ "Bloque alto %": round(side["bloque"]["alto"] * 100),
219
+ "Bloque medio %": round(side["bloque"]["medio"] * 100),
220
+ "Bloque bajo %": round(side["bloque"]["bajo"] * 100),
221
+ "Ataque Izq %": round(side["pasillo"]["Izq"] * 100),
222
+ "Ataque Centro %": round(side["pasillo"]["Centro"] * 100),
223
+ "Ataque Der %": round(side["pasillo"]["Der"] * 100),
224
+ "xT Izq": round(side["xt"]["Izq"], 3),
225
+ "xT Centro": round(side["xt"]["Centro"], 3),
226
+ "xT Der": round(side["xt"]["Der"], 3),
227
+ })
228
+ df_pred = pd.DataFrame(rows)
229
+ csv_path = out_path.parent / "modelo_cruce.csv"
230
+ df_pred.to_csv(csv_path, index=False)
231
+ preview = (f"<p style='margin-bottom:.6rem'>{matchup_render.insight(pred)}</p>"
232
+ + df_pred.to_html(index=False, escape=True))
233
+ tables.append(TableArtifact(
234
+ name="modelo_cruce", title="Predicción del modelo — resumen del cruce",
235
+ section=sec, csv_path=csv_path, preview_html=preview))
236
+ elif pred and "error" in pred:
237
+ warnings.append("Modelo de cruce: " + str(pred["error"]))
238
+ except Exception as exc: # nunca romper el reporte por la predicción
239
+ warnings.append(f"Predicción del modelo no disponible: {exc}")
240
+
241
  return ReportBundle(
242
  report_type="pre_match",
243
  html_path=Path(result_path),
vendor/data/modeling/matchup_feat_cols.json ADDED
@@ -0,0 +1 @@
 
 
1
+ ["self_elo", "self_def_H", "self_def_M", "self_def_L", "self_atk_Der", "self_atk_Centro", "self_atk_Izq", "self_offxt_H_Der", "self_offxt_H_Centro", "self_offxt_H_Izq", "self_offxt_M_Der", "self_offxt_M_Centro", "self_offxt_M_Izq", "self_offxt_L_Der", "self_offxt_L_Centro", "self_offxt_L_Izq", "self_defxt_H_Der", "self_defxt_H_Centro", "self_defxt_H_Izq", "self_defxt_M_Der", "self_defxt_M_Centro", "self_defxt_M_Izq", "self_defxt_L_Der", "self_defxt_L_Centro", "self_defxt_L_Izq", "self_sh_buildup", "self_sh_counter", "self_sh_direct", "self_sh_setpiece", "self_sh_recovery", "self_sh_progr", "self_cross_pct", "self_pct_long", "self_pct_short", "self_pct_pass_ok", "self_n_attacks", "self_xt_total", "self_xg_total", "self_xt_per_attack", "self_n_seq", "self_n_pass", "self_n_shots", "self_avg_x", "self_avg_y", "self_share_opp_half", "self_recovery_height", "self_n_progr", "self_goals_for", "self_goals_against", "self_win", "self_xt_lane_Der", "self_xt_lane_Centro", "self_xt_lane_Izq", "selfH_def_H", "selfH_def_M", "selfH_def_L", "selfH_atk_Der", "selfH_atk_Centro", "selfH_atk_Izq", "selfH_offxt_H_Der", "selfH_offxt_H_Centro", "selfH_offxt_H_Izq", "selfH_offxt_M_Der", "selfH_offxt_M_Centro", "selfH_offxt_M_Izq", "selfH_offxt_L_Der", "selfH_offxt_L_Centro", "selfH_offxt_L_Izq", "selfH_defxt_H_Der", "selfH_defxt_H_Centro", "selfH_defxt_H_Izq", "selfH_defxt_M_Der", "selfH_defxt_M_Centro", "selfH_defxt_M_Izq", "selfH_defxt_L_Der", "selfH_defxt_L_Centro", "selfH_defxt_L_Izq", "selfH_sh_buildup", "selfH_sh_counter", "selfH_sh_direct", "selfH_sh_setpiece", "selfH_sh_recovery", "selfH_sh_progr", "selfH_cross_pct", "selfH_pct_long", "selfH_pct_short", "selfH_pct_pass_ok", "selfH_n_attacks", "selfH_xt_total", "selfH_xg_total", "selfH_xt_per_attack", "selfH_n_seq", "selfH_n_pass", "selfH_n_shots", "selfH_avg_x", "selfH_avg_y", "selfH_share_opp_half", "selfH_recovery_height", "selfH_n_progr", "selfH_goals_for", "selfH_goals_against", "selfH_win", "selfH_xt_lane_Der", "selfH_xt_lane_Centro", "selfH_xt_lane_Izq", "selfA_def_H", "selfA_def_M", "selfA_def_L", "selfA_atk_Der", "selfA_atk_Centro", "selfA_atk_Izq", "selfA_offxt_H_Der", "selfA_offxt_H_Centro", "selfA_offxt_H_Izq", "selfA_offxt_M_Der", "selfA_offxt_M_Centro", "selfA_offxt_M_Izq", "selfA_offxt_L_Der", "selfA_offxt_L_Centro", "selfA_offxt_L_Izq", "selfA_defxt_H_Der", "selfA_defxt_H_Centro", "selfA_defxt_H_Izq", "selfA_defxt_M_Der", "selfA_defxt_M_Centro", "selfA_defxt_M_Izq", "selfA_defxt_L_Der", "selfA_defxt_L_Centro", "selfA_defxt_L_Izq", "selfA_sh_buildup", "selfA_sh_counter", "selfA_sh_direct", "selfA_sh_setpiece", "selfA_sh_recovery", "selfA_sh_progr", "selfA_cross_pct", "selfA_pct_long", "selfA_pct_short", "selfA_pct_pass_ok", "selfA_n_attacks", "selfA_xt_total", "selfA_xg_total", "selfA_xt_per_attack", "selfA_n_seq", "selfA_n_pass", "selfA_n_shots", "selfA_avg_x", "selfA_avg_y", "selfA_share_opp_half", "selfA_recovery_height", "selfA_n_progr", "selfA_goals_for", "selfA_goals_against", "selfA_win", "selfA_xt_lane_Der", "selfA_xt_lane_Centro", "selfA_xt_lane_Izq", "opp_self_elo", "opp_self_def_H", "opp_self_def_M", "opp_self_def_L", "opp_self_atk_Der", "opp_self_atk_Centro", "opp_self_atk_Izq", "opp_self_offxt_H_Der", "opp_self_offxt_H_Centro", "opp_self_offxt_H_Izq", "opp_self_offxt_M_Der", "opp_self_offxt_M_Centro", "opp_self_offxt_M_Izq", "opp_self_offxt_L_Der", "opp_self_offxt_L_Centro", "opp_self_offxt_L_Izq", "opp_self_defxt_H_Der", "opp_self_defxt_H_Centro", "opp_self_defxt_H_Izq", "opp_self_defxt_M_Der", "opp_self_defxt_M_Centro", "opp_self_defxt_M_Izq", "opp_self_defxt_L_Der", "opp_self_defxt_L_Centro", "opp_self_defxt_L_Izq", "opp_self_sh_buildup", "opp_self_sh_counter", "opp_self_sh_direct", "opp_self_sh_setpiece", "opp_self_sh_recovery", "opp_self_sh_progr", "opp_self_cross_pct", "opp_self_pct_long", "opp_self_pct_short", "opp_self_pct_pass_ok", "opp_self_n_attacks", "opp_self_xt_total", "opp_self_xg_total", "opp_self_xt_per_attack", "opp_self_n_seq", "opp_self_n_pass", "opp_self_n_shots", "opp_self_avg_x", "opp_self_avg_y", "opp_self_share_opp_half", "opp_self_recovery_height", "opp_self_n_progr", "opp_self_goals_for", "opp_self_goals_against", "opp_self_win", "opp_self_xt_lane_Der", "opp_self_xt_lane_Centro", "opp_self_xt_lane_Izq", "opp_selfH_def_H", "opp_selfH_def_M", "opp_selfH_def_L", "opp_selfH_atk_Der", "opp_selfH_atk_Centro", "opp_selfH_atk_Izq", "opp_selfH_offxt_H_Der", "opp_selfH_offxt_H_Centro", "opp_selfH_offxt_H_Izq", "opp_selfH_offxt_M_Der", "opp_selfH_offxt_M_Centro", "opp_selfH_offxt_M_Izq", "opp_selfH_offxt_L_Der", "opp_selfH_offxt_L_Centro", "opp_selfH_offxt_L_Izq", "opp_selfH_defxt_H_Der", "opp_selfH_defxt_H_Centro", "opp_selfH_defxt_H_Izq", "opp_selfH_defxt_M_Der", "opp_selfH_defxt_M_Centro", "opp_selfH_defxt_M_Izq", "opp_selfH_defxt_L_Der", "opp_selfH_defxt_L_Centro", "opp_selfH_defxt_L_Izq", "opp_selfH_sh_buildup", "opp_selfH_sh_counter", "opp_selfH_sh_direct", "opp_selfH_sh_setpiece", "opp_selfH_sh_recovery", "opp_selfH_sh_progr", "opp_selfH_cross_pct", "opp_selfH_pct_long", "opp_selfH_pct_short", "opp_selfH_pct_pass_ok", "opp_selfH_n_attacks", "opp_selfH_xt_total", "opp_selfH_xg_total", "opp_selfH_xt_per_attack", "opp_selfH_n_seq", "opp_selfH_n_pass", "opp_selfH_n_shots", "opp_selfH_avg_x", "opp_selfH_avg_y", "opp_selfH_share_opp_half", "opp_selfH_recovery_height", "opp_selfH_n_progr", "opp_selfH_goals_for", "opp_selfH_goals_against", "opp_selfH_win", "opp_selfH_xt_lane_Der", "opp_selfH_xt_lane_Centro", "opp_selfH_xt_lane_Izq", "opp_selfA_def_H", "opp_selfA_def_M", "opp_selfA_def_L", "opp_selfA_atk_Der", "opp_selfA_atk_Centro", "opp_selfA_atk_Izq", "opp_selfA_offxt_H_Der", "opp_selfA_offxt_H_Centro", "opp_selfA_offxt_H_Izq", "opp_selfA_offxt_M_Der", "opp_selfA_offxt_M_Centro", "opp_selfA_offxt_M_Izq", "opp_selfA_offxt_L_Der", "opp_selfA_offxt_L_Centro", "opp_selfA_offxt_L_Izq", "opp_selfA_defxt_H_Der", "opp_selfA_defxt_H_Centro", "opp_selfA_defxt_H_Izq", "opp_selfA_defxt_M_Der", "opp_selfA_defxt_M_Centro", "opp_selfA_defxt_M_Izq", "opp_selfA_defxt_L_Der", "opp_selfA_defxt_L_Centro", "opp_selfA_defxt_L_Izq", "opp_selfA_sh_buildup", "opp_selfA_sh_counter", "opp_selfA_sh_direct", "opp_selfA_sh_setpiece", "opp_selfA_sh_recovery", "opp_selfA_sh_progr", "opp_selfA_cross_pct", "opp_selfA_pct_long", "opp_selfA_pct_short", "opp_selfA_pct_pass_ok", "opp_selfA_n_attacks", "opp_selfA_xt_total", "opp_selfA_xg_total", "opp_selfA_xt_per_attack", "opp_selfA_n_seq", "opp_selfA_n_pass", "opp_selfA_n_shots", "opp_selfA_avg_x", "opp_selfA_avg_y", "opp_selfA_share_opp_half", "opp_selfA_recovery_height", "opp_selfA_n_progr", "opp_selfA_goals_for", "opp_selfA_goals_against", "opp_selfA_win", "opp_selfA_xt_lane_Der", "opp_selfA_xt_lane_Centro", "opp_selfA_xt_lane_Izq", "is_home", "elo_diff", "form_4231", "form_4231.0", "form_433", "form_442", "form_3421", "form_433.0", "form_442.0", "form_3421.0", "form_352", "form_4141.0", "form_343", "form_4141"]
vendor/data/modeling/model_bloques__def_H.txt ADDED
The diff for this file is too large to render. See raw diff
 
vendor/data/modeling/model_bloques__def_L.txt ADDED
The diff for this file is too large to render. See raw diff
 
vendor/data/modeling/model_bloques__def_M.txt ADDED
The diff for this file is too large to render. See raw diff
 
vendor/data/modeling/model_pasillos__atk_Centro.txt ADDED
The diff for this file is too large to render. See raw diff
 
vendor/data/modeling/model_pasillos__atk_Der.txt ADDED
The diff for this file is too large to render. See raw diff
 
vendor/data/modeling/model_pasillos__atk_Izq.txt ADDED
The diff for this file is too large to render. See raw diff
 
vendor/data/modeling/model_peligro_pasillo__xt_lane_Centro.txt ADDED
The diff for this file is too large to render. See raw diff
 
vendor/data/modeling/model_peligro_pasillo__xt_lane_Der.txt ADDED
The diff for this file is too large to render. See raw diff
 
vendor/data/modeling/model_peligro_pasillo__xt_lane_Izq.txt ADDED
The diff for this file is too large to render. See raw diff
 
vendor/data/modeling/team_features_latest.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d5a3e60860e442bab0870bad04bebfded42d96dd9a039d64bcabc3b28f759703
3
+ size 1604608