by Draft Analytica

Copa del Mundo 2026

World Cup Pass
Metodologia

Modelos end to end

Documento completo renderizado desde reports/modelos_end_to_end_detallado.md.

Reporte tecnico

Pipeline productivo y capas de modelo

Completo

Draft Analytica - Manual detallado de modelos end to end

Fecha de auditoria: 2026-06-30

Este documento explica como estan armados los modelos del repo, desde la data y feature stores hasta el payload de produccion. Esta escrito para poder tocar el sistema con criterio: que cambia cada perilla, que numeros salieron, que quedo en produccion y que quedo como research/shadow.

0. Resumen mental del sistema

El sistema productivo no es un unico modelo. Es una arquitectura por capas:

  1. `v4_1_weighted_softmax_elo_form`: modelo principal de probabilidades 1X2.
  2. `p2f_v4_1_elo_conditional_draw_threshold`: capa de decision para mejorar

etiquetas de empate sin cambiar probabilidades.

  1. `ensemble_v3_1`: baseline Poisson/rating para goles esperados.
  2. `p3g_2026_06_29`: XGBoost residual de lambdas, no promovido directo.
  3. `p3h_2026_06_29`: modelo estabilizado de goles/marcadores, en produccion.
  4. Simulacion de torneo: usa P3H para forma de marcadores y repondera buckets

para respetar probabilidades v4.1.

  1. Odds/ROI: existe infraestructura de EV, pero P2F no alimenta actualmente el

benchmark de cuotas porque ese benchmark usa probabilidades, no la etiqueta final.

Versiones canonicas en `src/prediction_modes.py`:

UsoVersion
1X2 principal`v4_1_weighted_softmax_elo_form`
Scorelines/goles`p3h_2026_06_29`
Poisson baseline`ensemble_v3_1`
Torneo`hybrid_v4_1_p2f_p3h_fast_v1`

1. Data: que entra al sistema

1.1 Resultados internacionales

Archivo principal:

  • `data/processed/v4/international_matches_clean.csv`

Script:

  • `scripts/v4/ingest_international_results.py`

Este script normaliza:

  • `match_date`
  • `home_team`
  • `away_team`
  • `home_score`
  • `away_score`
  • `tournament`
  • `neutral`
  • `country`
  • `city`
  • `match_importance`

Clasificacion de importancia en `scripts/v4/common.py`:

Texto del torneoClase
contiene `friendly``friendly`
contiene `qualif``qualifier`
contiene `world cup`, `euro`, `copa america`, `afcon`, `asian cup`, `gold cup``major_tournament`
contiene `nations league``competitive`
otro`other`

Tambien deriva:

  • `is_friendly`
  • `is_qualifier`
  • `is_major_tournament`
  • normalizacion de nombres
  • `match_key` para deduplicacion

1.2 Elo dinamico as-of

Archivo:

  • `data/processed/v4/team_dynamic_elo_asof.csv`

Script:

  • `scripts/v4/build_dynamic_elo_asof.py`

Funcion central:

  • `build_dynamic_elo(matches)`

Cada equipo empieza en Elo `1500.0`.

Para cada partido ordenado por fecha:

home_pre = rating previo local, default 1500
away_pre = rating previo visitante, default 1500
home_advantage = 0 si neutral, 50 si no neutral
expected_home = 1 / (1 + 10 ^ (-((home_pre + home_advantage) - away_pre) / 400))
outcome_home = 1 si gana home, 0.5 si empate, 0 si pierde home
change = k * margin_multiplier * (outcome_home - expected_home)
home_post = home_pre + change
away_post = away_pre - change
elo_diff_pre = (home_pre + home_advantage) - away_pre

`k` depende de la importancia:

match_importancek
friendly15
qualifier30
major_tournament45
competitive35
other25

`margin_multiplier`:

Diferencia de golesMultiplicador
0 o 11.00
21.25
3+1.50

Punto importante: la fila guarda `elo_pre_*` antes de actualizar. Por eso es leakage-safe: el modelo no ve el resultado del partido que intenta predecir.

1.3 Forma reciente as-of

Archivo:

  • `data/processed/v4/team_form_asof_features.csv`

Script:

  • `scripts/v4/build_team_form_asof_features.py`

Funcion central:

  • `build_form_features(matches, elo)`

Por cada partido, antes de agregar el resultado actual al historial, calcula para cada equipo:

FeatureSignificado
`last_5_points`puntos en ultimos 5 partidos previos
`last_10_points`puntos en ultimos 10 partidos previos
`last_20_points`puntos en ultimos 20 partidos previos
`goals_for_last_10`goles a favor ultimos 10
`goals_against_last_10`goles en contra ultimos 10
`goal_diff_last_10`diferencia de gol ultimos 10
`win_rate_last_10`tasa de victorias ultimos 10
`clean_sheet_rate_last_10`tasa de porterias a cero ultimos 10
`opponent_adjusted_points_last_10`puntos ponderados por Elo rival
`opponent_adjusted_goal_diff_last_10`diferencial ponderado por Elo rival
`average_opponent_elo_last_10`fuerza media de rivales recientes
`days_since_last_match`descanso desde ultimo partido
`competitive_match_form`forma en partidos no amistosos
`friendly_form`forma en amistosos
`major_tournament_form`forma en torneos grandes
`matches_available_before_match`cobertura historica previa

1.4 FIFA ranking as-of

Archivo:

  • `data/processed/v4/fifa_rankings_asof.csv`

Script:

  • `scripts/v4/ingest_fifa_rankings_history.py`

El feature store busca el ultimo snapshot FIFA con fecha menor o igual al `match_date`. Se marca si es historico real, mixto o snapshot actual forecast-only.

1.5 Market value / squad features

Archivo:

  • `data/processed/v4/team_squad_market_asof_features.csv`

Script:

  • `scripts/v4/build_squad_market_asof_features.py`

Estas features existen en el feature store, pero para v4.1 productivo no entran al softmax. Se marcan como `forecast_only` cuando son snapshot actual.

1.6 Feature store v4 final

Archivo:

  • `data/processed/v4/match_feature_store_asof.csv`

Metadata:

  • `rows = 49493`
  • generado: `2026-06-28T09:34:14+00:00`
  • nota: features leakage-safe con Elo pre-match, forma previa y FIFA historico

as-of cuando existe.

Script:

  • `scripts/v4/build_v4_feature_store.py`

Une:

  • resultados limpios
  • Elo as-of
  • forma as-of
  • FIFA as-of
  • market/squad forecast-only

La fila final de cada partido contiene lo que el modelo puede usar o auditar.

2. v4.1 Weighted Softmax 1X2

2.1 Que es

Modelo productivo principal de probabilidades 1X2:

  • `home_win`
  • `draw`
  • `away_win`

Nombre:

  • `v4_1_weighted_softmax_elo_form`

Servicio:

  • `backend/services/v4_prediction_service.py`

Entrenamiento:

  • `scripts/v4/train_v4_1_weighted_model.py`

Base comun:

  • `scripts/v4/train_v4_candidate_model.py`

Tipo de modelo:

  • regresion logistica multinomial
  • softmax
  • perdida cross entropy ponderada por clase
  • L2 regularization
  • temperature scaling en validacion

2.2 Features exactas

`FEATURE_ORDER`:

OrdenFeatureFormula de entrenamiento/runtime
1`elo_diff``elo_diff_pre / 400`
2`form_points_diff``(last_10_points_home - last_10_points_away) / 30`
3`form_goal_diff``(goal_diff_last_10_home - goal_diff_last_10_away) / 30`
4`opponent_elo_diff``(avg_opp_elo_home - avg_opp_elo_away) / 400`
5`rest_diff``(log1p(days_home) - log1p(days_away)) / 4`
6`is_neutral`1 si neutral, 0 si no
7`is_friendly`1 si amistoso
8`is_qualifier`1 si qualifier
9`is_major_tournament`1 si torneo grande

En runtime, si el partido no es neutral, se agrega `home_advantage = 50` puntos en la diferencia Elo. En partidos neutrales, `home_advantage = 0`.

2.3 Split temporal

Funcion:

  • `split_temporally(dataset)`

Politica:

SplitRegla
Trainyear < 2010
Validation2010 <= year < 2018
Testyear >= 2018

Si alguna particion queda vacia, hay fallback 70/15/15, pero en este run se uso el split temporal real.

Conteos v4.1:

SplitPartidos
Train33,588
Validation7,709
Test8,196 en artifact de entrenamiento

Nota: algunos backtests posteriores de P2F/XGB usan 8,155 o 8,174 filas porque operan sobre subconjuntos/archivos generados para decision/odds. El modelo base entrenado reporta 8,196.

2.4 Normalizacion

Antes de entrenar:

x_raw = features
mean = media de train
std = desviacion de train, si 0 entonces 1
x_scaled = (x_raw - mean) / std
x_model = [1.0, x_scaled...]  # intercepto

Las medias/std quedan guardadas dentro del CSV de pesos:

`data/processed/v4/diagnostics/v4_1_weighted_softmax_weights.csv`

2.5 Formula del softmax

Para cada partido:

logits = x_model @ W
logits_temp = logits / temperature
probabilities = softmax(logits_temp)

Con:

softmax(z_i) = exp(z_i) / sum_j exp(z_j)

La matriz `W` tiene una columna por clase:

  • `home_win`
  • `draw`
  • `away_win`

2.6 Entrenamiento

Funcion:

  • `train_weighted_softmax(...)`

Hiperparametros:

ParametroValor
epochs700
learning_rate0.08
l20.01
class_weight_home_win0.82
class_weight_draw1.55
class_weight_away_win1.10

La perdida es cross entropy ponderada:

loss = - sum_i class_weight[y_i] * log(p_i[y_i]) + L2

Gradiente:

residual = (probs - y_onehot) * sample_weight
grad = X.T @ residual / sum(sample_weight)
grad[1:] += l2 * weights[1:]   # no regulariza intercepto
weights -= learning_rate * grad

Por que esos pesos:

  • `draw = 1.55`: obliga al modelo a tomar mas en serio la clase empate.
  • `home_win = 0.82`: reduce sesgo hacia home win.
  • `away_win = 1.10`: levanta un poco away wins.

2.7 Temperature scaling

Funcion:

  • `select_acceptance_temperature(...)`

Candidatos:

0.60, 0.65, 0.70, 0.75, 0.80, 0.85, 0.90, 1.00,
1.10, 1.20, 1.35, 1.50, 1.75, 2.00

Regla:

  1. Filtra candidatos con `ECE <= 0.030`.
  2. Dentro de esos, escoge menor validation log loss.
  3. Si ninguno pasa, escoge menor ECE y luego log loss.

Temperatura elegida:

temperature = 0.8

2.8 Pesos aprendidos

Pesos exactos:

Featurehome_windrawaway_win
intercept0.1664600.107204-0.273664
elo_diff0.518408-0.027380-0.491028
form_points_diff-0.0145400.0038030.010737
form_goal_diff0.220758-0.003664-0.217094
opponent_elo_diff0.147124-0.011795-0.135329
rest_diff-0.029782-0.0134280.043210
is_neutral-0.057181-0.0370400.094221
is_friendly-0.0691220.0471770.021945
is_qualifier0.0188720.016048-0.034920
is_major_tournament-0.0135450.015080-0.001535

Lectura humana:

  • `elo_diff` es la senal mas fuerte: si A tiene mas Elo, sube `home_win` y baja

`away_win`.

  • `form_goal_diff` tambien empuja fuerte al ganador.
  • `opponent_elo_diff` premia forma conseguida contra rivales fuertes.
  • Los pesos de `draw` son pequenos. El empate es dificil: no se comporta como

"equipo A fuerte" o "equipo B fuerte"; por eso usamos class weighting y P2F.

  • `is_friendly` levanta algo el empate: amistosos suelen ser mas ruidosos.

2.9 Metricas v4.1 base

Artifact:

  • `data/processed/v4/diagnostics/v4_1_weighted_softmax_metrics.csv`
SplitTempMatchesAccuracyBalanced accLog lossBrierRPSECEDraw F1
train1.033,5880.5507320.4922800.9462380.5600800.5393420.0709070.267261
validation0.87,7090.5671290.5073400.9182580.5395240.5019310.0282760.262139
test0.88,1960.5916300.5210360.8808900.5167680.4851890.0199070.228192

2.10 Metricas por clase

Validation:

ClaseSupportPredichosPrecisionRecallF1
home_win3,6934,1760.6554120.7411320.695641
draw1,8001,3510.3056990.2294440.262139
away_win2,2162,1820.5600370.5514440.555707

Test:

ClaseSupportPredichosPrecisionRecallF1
home_win3,9234,6770.6615350.7886820.719535
draw1,8921,1230.3063220.1818180.228192
away_win2,3812,3960.5888980.5926080.590747

Problema: en test el modelo base solo predice 1,123 empates contra 1,892 reales. Por eso se diseno P2F.

2.11 Acceptance gates v4.1

Archivo:

  • `data/processed/v4/diagnostics/v4_1_weighted_softmax_acceptance.csv`
CheckValorTargetPaso
validation_non_neutral_hw_argmax0.565399<= 0.58True
validation_draw_f10.262139>= 0.22True
validation_ece0.028276<= 0.03True

Por eso v4.1 fue aceptable como base probabilistica.

2.12 Runtime v4.1

Funcion:

  • `predict_match_v4(...)`

Pasos:

  1. Carga estado con `_load_v4_state`.
  2. Reconstruye historial de cada equipo.
  3. Usa `get_team_state_asof` con `bisect_left`: solo eventos estrictamente

anteriores a `as_of`.

  1. Calcula features runtime.
  2. Escala features con medias/std de train.
  3. Aplica `softmax((x @ W) / temperature)`.
  4. Si es neutral, calcula A vs B y B vs A, remapea y promedia:
probs_ab = modelo(A, B)
probs_ba = modelo(B, A)
probs_ba_mapped = [away_from_ba, draw_from_ba, home_from_ba]
probs = (probs_ab + probs_ba_mapped) / 2

Esto reduce sesgo de orden en partidos neutrales.

  1. Puede aplicar live overlay WC2026 si la politica lo permite.
  2. Aplica decision P2F.
  3. Devuelve probabilidades, decision, features, metadata y warnings.

3. P2F: decision layer de empate

3.1 Que es

Modelo/capa:

  • `p2f_v4_1_elo_conditional_draw_threshold`

Version:

  • `p2f_2026_06_29`

Codigo:

  • `backend/services/v4_prediction_service.py`

Metadata:

  • `models/v4/p2f_v4_1_elo_conditional_threshold_metadata.json`

No cambia probabilidades. Solo decide la etiqueta final servida.

3.2 Regla

abs_elo_gap < 50:
    segment = close
    draw_threshold = 0.325

50 <= abs_elo_gap < 150:
    segment = medium
    draw_threshold = 0.375

abs_elo_gap >= 150:
    segment = lopsided
    decision = argmax

Si hay threshold:

if p_draw >= draw_threshold:
    pred = draw
else:
    pred = max(home_win, away_win)

3.3 Por que existe

v4.1 calibraba razonablemente, pero como decision argmax seguia prediciendo pocos empates. P2F aumenta recall de empates sin tocar probabilidades.

3.4 Metricas P2F

Validation:

PoliticaAccuracyMacro F1Draw F1Draw precisionDraw recallPred draw rateActual draw rate
argmax0.5671290.5044960.2621390.3056990.2294440.1752500.233493
P2F selected0.5500060.5155680.3368170.2993520.3850000.3002980.233493

Test:

PoliticaAccuracyMacro F1Draw F1Draw precisionDraw recallPred draw rateActual draw rate
argmax0.5910480.5120780.2266670.3043870.1805630.1369710.230901
P2F selected0.5714290.5237390.2970250.2896060.3048330.2430410.230901

Lectura:

  • baja accuracy global ~1.96 puntos en test
  • sube macro F1
  • sube mucho draw recall
  • P2F predice empates a una tasa mas parecida a la realidad
  • log loss/ECE no cambian porque probabilidades no cambian

3.5 Por que no usamos P2E offset

P2E:

  • `p2e_v4_1_draw_probability_offset`

Logro:

  • validation draw F1 hasta `0.344444`
  • selected offset `0.075`, validation draw F1 `0.343273`
  • test draw F1 `0.314873`

Pero no fue produccion porque cambiaba probabilidades:

SplitOffsetLog lossECEDraw calibration gap
validation baseline0.0000.9182580.0282760.070492
validation P2E0.0750.9403380.0641110.119052
test baseline0.0000.8809500.0196640.057899
test P2E0.0750.9014650.0678550.107518

Decision:

  • `promote_to_production = false`
  • `diagnostic_only = true`

Razon: mejoraba etiqueta dura de empate, pero rompia calibracion.

4. XGBoost 1X2 challengers

4.1 XGBoost 1X2 directo

Reporte:

  • `reports/xgboost_1x2_challenger_report.md`

Decision:

  • no shadow
  • no produccion

Comparacion test:

ModeloAccuracyBalanced accLog lossBrierECEDraw F1Draw gap
v4.10.5910480.5204110.8809500.5168620.0196640.2266670.057899
XGBoost0.5786640.5322650.8916240.5259600.0404330.2794030.074268

XGBoost mejora algo draw F1/balanced accuracy, pero empeora log loss, Brier, ECE y draw calibration gap. No se promueve.

4.2 XGBoost calibrated blend

Reporte:

  • `reports/xgboost_calibration_blend_audit.md`

Seleccion:

  • calibrator: `xgb_isotonic_ovr`
  • alpha v4.1 weight: `0.1`

Resultado:

  • `promote_to_shadow = true`
  • `promote_to_production = false`

Por que:

  • mejora log loss/ECE
  • pero destruye draw F1 en test (`0.0`)
  • sirve como sombra probabilistica, no como decision publica.

5. Ratings v3 y v31

5.1 Para que sirven

Los ratings no son el modelo 1X2 principal. Son una capa de fuerza de equipo que alimenta Poisson/goles:

ratings -> expected goals -> score matrix -> scorelines/over-under/simulacion

5.2 v3

Archivo:

  • `src/model_ratings_v3.py`

Formula:

model_score_v3 = 0.95 * fifa_component + 0.05 * supporting_data_component

`supporting_data_component` combina:

ComponentePeso
Elo support0.40
Historical support0.35
Reliability/data quality0.25

Si no hay FIFA:

model_score_v3 = supporting_data_component * 0.55

5.3 v31

Archivo:

  • `src/model_ratings_v31.py`

Idea:

model_score_v31 = base_v3 * base_weight + xg * xg_weight + market * market_weight

Con config seleccionada auditada:

base_v3_weight = 0.78
xg_weight = 0.14
market_weight = 0.08

Si no hay xG/market value:

model_score_v31 = model_score_v3

5.4 Backtest v31 vs v3

Archivo:

  • `data/processed/diagnostics/model_v31_backtest_summary.json`
ModeloMatchesAccuracyLog lossBrierCalibration error
v31,2480.51521.0246820.6060880.049100
v311,2480.52641.0193400.6024820.037001

Acceptance rule:

calibration_error <= v3 + 0.02
log_loss <= v3 + 0.03

Resultado:

  • accepted = true

6. Poisson ensemble_v3_1

6.1 Que es

Modelo:

  • `PoissonGoalModel`

Archivo:

  • `src/models/poisson.py`

Factory:

  • `backend/services/model_service.py`

Version productiva base:

  • `ensemble_v3_1`

6.2 Parametros ensemble_v3_1

ParametroValor
baseline_goals1.28
attack_weight0.35
defense_weight0.25
rating_weight1.05
min_expected_goals0.25
max_expected_goals3.25
min_rating_adjustment0.62
max_rating_adjustment1.38
rating_column`model_score_v31`
fallback columns`model_score_v3`, `model_score_v2`, `ranking_score`, `adjusted_power_rating`

6.3 Formula expected goals

Para equipo A contra B:

global_avg = media de avg_goals_for
attack_factor_a = avg_goals_for_a / global_avg
defense_factor_b = avg_goals_against_b / global_avg
rating_diff = rating_a - rating_b
rating_adjustment_a = clamp(1 + (rating_diff / 100) * rating_weight, min_adj, max_adj)

lambda_a =
    baseline_goals
    * attack_factor_a ^ attack_weight
    * defense_factor_b ^ defense_weight
    * rating_adjustment_a

Lo mismo para B con rating_diff invertido.

Luego se clampa:

lambda in [0.25, 3.25]

6.4 Score matrix Poisson

Para cada marcador `i-j`:

P(A=i) = exp(-lambda_a) * lambda_a^i / i!
P(B=j) = exp(-lambda_b) * lambda_b^j / j!
P(i-j) = P(A=i) * P(B=j)

Luego se suman celdas:

home_win = sum(i > j)
draw = sum(i == j)
away_win = sum(i < j)

Importante: en produccion, este 1X2 derivado de goles no es el principal. Es auxiliar/diagnostico cuando v4.1 esta activo.

7. P3G learned lambda residual

7.1 Que es

Modelo:

  • `p3g_learned_lambda_residual`

Version:

  • `p3g_2026_06_29`

Archivos:

  • `src/models/p3g_lambda_residual.py`
  • `models/goal_model/p3g_home_goals_xgb.pkl`
  • `models/goal_model/p3g_away_goals_xgb.pkl`
  • `models/goal_model/p3g_goal_model_metadata.json`

Tipo:

  • XGBoost `count:poisson`
  • un booster para goles home
  • un booster para goles away
  • aprende residual sobre lambda v31, no reemplaza Poisson desde cero

Formula:

lambda_p3g = lambda_v31 * exp(f(features))

Implementacion:

base_margin = log(lambda_v31)
objective = count:poisson

7.2 Features P3G

Incluye:

  • lambdas v31
  • log lambdas v31
  • total esperado
  • diferencia/ratio de lambdas
  • Elo home/away/diff
  • ratings home/away/diff
  • goles for/against ultimos 10
  • total goals ultimos 10
  • draw rate ultimos 10
  • form points diff
  • neutral / tournament / qualifier / friendly
  • rest diff
  • xG for/against ultimos 10
  • cobertura xG

Lista exacta en:

  • `src/models/p3g_lambda_residual.py`

7.3 Parametros XGBoost

ParametroValor
objective`count:poisson`
eval_metric`poisson-nloglik`
eta0.03
max_depth2
min_child_weight10
subsample0.85
colsample_bytree0.85
lambda10.0
alpha1.0
tree_method`hist`
seed20260629

Caps:

lambda_min = 0.15
lambda_max = 4.50

Split:

train before 2014
validation 2014-2019
test 2022+

7.4 Resultado P3G

P3G no se promovio.

Razon:

  • validation mejora
  • test empeora scoreline log loss
  • over 2.5 calibration se mueve mal
  • xG coverage historica es baja (`0.175481`)

Metricas clave:

VariantSplitScoreline log lossExact top1Total goals biasOver 2.5 gap1X2 log loss
baseline_v31validation3.093460.0862070.0424200.0048360.976725
P3Gvalidation3.091090.1120690.1138660.0104550.961192
baseline_v31test2.867140.0937500.0498260.0453620.942998
P3Gtest2.880710.109375-0.0561030.0189090.950645

Decision:

  • `promote_to_shadow = false`
  • `promote_to_production = false`

8. P3H stabilized lambda model

8.1 Que es

Modelo:

  • `p3h_stabilized_lambda_model`

Version:

  • `p3h_2026_06_29`

Archivos:

  • `src/models/p3h_stabilized_lambda_model.py`
  • `backend/services/p3h_goal_service.py`
  • `models/goal_model/p3h_goal_model_metadata.json`

P3H toma P3G, pero lo estabiliza contra Poisson v31.

8.2 Formula P3H

Formula del metadata:

log_lambda_p3h =
    log_lambda_v31
    + gamma * clip(log_lambda_p3g - log_lambda_v31, -clip_value, clip_value)

lambda_p3h *= exp(total_delta / 2)

Parametros elegidos:

ParametroValor
gamma0.8
clip_value0.75
total_delta-0.04
rho_strategy`global_regularized`

Interpretacion:

  • `gamma = 0.8`: toma buena parte de la correccion P3G.
  • `clip = 0.75`: evita que P3G haga saltos enormes.
  • `total_delta = -0.04`: baja levemente el volumen total de goles.
  • `rho`: ajuste Dixon-Coles para dependencia en marcadores bajos.

8.3 Dixon-Coles

P3H primero genera matriz Poisson y luego puede ajustar celdas bajas:

0-0 *= 1 - lambda_home * lambda_away * rho
0-1 *= 1 + lambda_home * rho
1-0 *= 1 + lambda_away * rho
1-1 *= 1 - rho

Despues normaliza la matriz.

Rho se clampa en:

[-0.20, 0.20]

8.4 Seleccion

Grid:

  • gamma: 0.00 a 1.00
  • clip: 0.15 a 0.75
  • total_delta: -0.08 a 0.08
  • rho: varias estrategias

Politica:

  • metrica primaria: rolling mean scoreline log loss
  • no aceptar folds materialmente peores
  • guardrails en test, total goals, over 2.5, exact score y P2F untouched

8.5 Metricas P3H

Artifact:

  • `data/processed/diagnostics/p3h_goal_model_metrics.json`

Selected candidate:

gamma_0.80_clip_0.75_td_-0.04_rho_global_regularized
MetricaValor
rolling_mean_scoreline_log_loss3.121997
rolling_baseline_mean_scoreline_log_loss3.149634
validation_scoreline_log_loss3.084525
test_scoreline_log_loss2.876983
all_scoreline_log_loss3.069456
all_exact_score_top1_accuracy0.122596
all_total_goals_bias-0.082618
all_over_2_5_gap_abs0.006928
all_one_x_two_log_loss0.924768
all_one_x_two_brier0.543093

Decision:

  • `promote_to_shadow = true`
  • `promote_to_production = true`
  • `production_change_recommended = true`

8.6 Que sirve P3H

P3H sirve:

  • expected goals
  • total expected goals
  • most likely score
  • top scorelines
  • score matrix
  • over/under 1.5, 2.5, 3.5
  • goal-derived 1X2 diagnostico

Pero:

goal_derived_1x2_usage = diagnostic_only_not_principal

El 1X2 principal sigue siendo v4.1/P2F.

9. Pipeline productivo de prediccion

Archivo:

  • `backend/services/prediction_pipeline.py`

Funcion principal:

  • `predict_match_distribution(...)`

Flujo:

  1. Valida que `model_version` sea v4.
  2. Llama `predict_match_v4`.
  3. Si no es historical replay, llama `predict_p3h_goal_distribution`.
  4. Construye `MatchDistribution`.
  5. Devuelve:
  6. probabilidades v4.1
  7. decision P2F
  8. argmax original
  9. features
  10. score matrix P3H
  11. expected goals
  12. top scorelines
  13. over/under
  14. warnings/data quality

Separacion clave:

CapaUso
v4.1 probabilitiesprobabilidad oficial 1X2
P2F decisionetiqueta final servida
P3H scorelinesmarcadores/goles/over-under
P3H goal-derived 1X2diagnostico, no principal

10. Simulacion de torneo

Archivo:

  • `backend/services/simulation_service.py`

Funcion:

  • `simulate_world_cup_2026_official(...)`

Cuando se pide modelo v4.1:

  1. Usa `ensemble_v3_1` como baseline Poisson.
  2. Usa `p3h_2026_06_29` como scoreline model.
  3. Usa motor fast.
  4. Marca:
scoreline_matrix_source = p3h_stabilized_lambda_model_reweighted_to_v4_1x2

10.1 Reponderacion v4.1 en simulacion

Archivo:

  • `src/simulation/poisson_probability_cache.py`

Funcion:

  • `_apply_v4_probabilities_to_score_matrix(package)`

Hace:

  1. Calcula matriz P3H.
  2. Suma buckets actuales:
  3. team_a_win
  4. draw
  5. team_b_win
  6. Obtiene target v4.1:
target_home = v4["prob_team_a_win"]
target_draw = v4["prob_draw"]
target_away = v4["prob_team_b_win"]
  1. Multiplica cada celda del bucket por:
target_bucket / current_bucket_sum
  1. Normaliza.

Resultado:

  • se preserva la forma relativa de marcadores P3H dentro de cada bucket
  • pero el total home/draw/away respeta v4.1

Nota: P2F es decision servida; la simulacion sigue siendo probability-driven.

11. Odds, EV y ROI

Archivos:

  • `scripts/backtests/backtest_market_edge_v4_1.py`
  • `scripts/backtests/backtest_worldcup_market_edge_v4_1.py`
  • `src/odds/ev.py`
  • `src/odds/metrics.py`

Formula EV:

EV = model_prob * decimal_odds - 1
edge_prob = model_prob - market_prob_novig

Se apuesta si:

EV >= ev_threshold
edge_prob >= edge_threshold
min_odds <= odds <= max_odds

ROI:

profit = stake * (odds - 1) si gana, -stake si pierde
ROI = total_profit / total_staked

Estado actual:

  • historico `market_edge_v4_1`: no hubo cuotas cruzadas, ROI no calculable.
  • WC2026 actual: future odds only, sin resultados, ROI `null`.
  • auditoria P2F/Poisson dice `odds_benchmark_uses_p2f = false`.

Razon: el benchmark de cuotas usa probabilidades, y P2F no cambia probabilidades. P2F solo cambia etiqueta final.

12. Modelos no productivos / shadow

12.1 P2E draw offset

Subia `p_draw` con offset positivo. Mejoraba draw F1 pero rompia calibracion.

Decision:

  • diagnostic only
  • no produccion

12.2 XGBoost 1X2

Mejoraba algo empate, pero empeoraba probabilidad/calibracion.

Decision:

  • no shadow
  • no produccion

12.3 XGBoost calibrated blend

Mejoraba log loss/ECE pero casi eliminaba draw F1.

Decision:

  • shadow probabilistico
  • no produccion

12.4 P3G directo

Aprendia lambdas con XGBoost, pero test y over/under no pasaron guardrails.

Decision:

  • no shadow
  • no produccion directo
  • usado como fuente dentro de P3H estabilizado

12.5 P1A/P1A.2/P1A.4

Hay artefactos P1A/P1A.2/P1A.4 en el repo. Son candidatos/shadow/canary con features as-of mas amplias. No son el default publico de produccion. Sirven como investigacion para futuras versiones, no como el modelo canonico actual.

12.5.1 P1A goal model

Artefacto:

  • `models/p1a/goal_model_p1a.json`

Version:

  • `goal_model_p1a_independent_poisson_v1`

Familia:

  • `independent_poisson_regression`

Features:

FeatureUso
`elo_home_pre`Elo home antes del partido
`elo_away_pre`Elo away antes del partido
`elo_diff`diferencia Elo prepartido
`attack_form_home`ataque reciente home
`attack_form_away`ataque reciente away
`defense_form_home`defensa reciente home
`defense_form_away`defensa reciente away
`rest_diff`diferencia de descanso
`neutral`flag cancha neutral
`competition_weight`peso por importancia
`is_friendly`flag amistoso
`is_qualifier`flag eliminatoria
`is_major_tournament`flag torneo grande

Entrenamiento:

split train < 2010
dos regresiones Poisson independientes: home goals y away goals
L2 = 0.08
max_iterations = 60
standardization aprendida en train

Inferencia:

eta_home = beta_home_intercept + X_scaled @ beta_home
eta_away = beta_away_intercept + X_scaled @ beta_away
lambda_home = exp(clip(eta_home, -3, 2))
lambda_away = exp(clip(eta_away, -3, 2))

Luego genera matriz 0-10 goles, renormaliza y deriva 1X2 sumando:

home_win = sum(home_goals > away_goals)
draw = sum(home_goals == away_goals)
away_win = sum(home_goals < away_goals)

Metricas reportadas:

SplitNGoal LL/matchRMSEMAELog Loss 1X2BrierECEDraw F1
Validation7,709-2.91471.33030.97130.90490.53360.01430.0000
Test8,124-2.89151.29540.97680.87760.51570.01590.0000

Decision:

benchmark_passed = true
decision = offline_candidate_only_not_default
default = false

Por que no fue default:

  • su 1X2 argmax casi no detecta empates (`Draw F1 = 0.0`)
  • lambdas podian ser extremas
  • no tenia loader productivo completo en MatchDistribution
  • no tenia fallback/feature flag publico robusto en esa etapa
  • la comparacion favorable contra Poisson v3.1 no era un benchmark ex ante

completamente limpio porque v3.1 usaba ratings snapshot actuales

12.5.2 v4.2 P1A weighted softmax candidate

Artefacto:

  • `models/v4_2/v4_2_p1a_candidate.json`

Version:

  • `v4_2_p1a_weighted_softmax_candidate`

Seleccion:

  • ablation: `plus_decay_form`

Features base de v4.1 mas decay form:

  • `weighted_points_diff_5`
  • `weighted_points_diff_10`
  • `weighted_goal_diff_5`
  • `weighted_goal_diff_10`
  • `recency_weighted_form_diff`

Entrenamiento:

ParametroValor
epochs650
learning_rate0.07
l20.015
temperature0.9
home_win weight0.82
draw weight1.55
away_win weight1.10

Decision:

promotable = false
decision = do_not_promote_keep_v4_1_default
default = false

Checks:

CheckPaso
validation_log_losstrue
validation_brier_scoretrue
validation_ecetrue
test_log_losstrue
test_brier_scoretrue
test_ecefalse

Lectura: P1A decay form mejoraba un poco log loss/Brier en algunos segmentos, pero no mejoraba calibracion global de forma suficientemente estable. Por eso v4.1 quedo como default.

12.5.3 P1A.2/P1A.4 serving status

P1A.2 agrego:

  • regularizacion/shrinkage
  • clipping mas conservador
  • calibracion validation
  • Dixon-Coles basico
  • diagnostics de totals/low score/top-k
  • loader de inferencia
  • MatchDistribution shadow
  • flags/fallback/promotion gates

Estado:

  • `P1A.2`: shadow only
  • `P1A.4`: canary/internal bajo feature flag
  • no public/admin default

Razon general: todavia habia degradacion fuerte en Draw F1 o falta de estabilidad suficiente contra v4.1/P2F.

13. Que significa cada metrica

MetricaSignificadoMejor direccion
Accuracyporcentaje de partidos donde la clase predicha fue correctamayor
Balanced accuracypromedio de recalls por clase; no deja que home_win domine todomayor
Precision drawde los empates predichos, cuantos fueron empates realesmayor
Recall drawde los empates reales, cuantos detectamosmayor
Draw F1balance precision/recall del empatemayor
Log losscastiga probabilidades malas; muy sensible a exceso de confianzamenor
Brier scoreerror cuadratico entre probabilidades y resultado one-hotmenor
RPSversion ordinal/cumulativa para 1X2menor
ECEerror de calibracion; confianza vs acierto observadomenor
Scoreline log losslog loss del marcador exactomenor
Exact score top1frecuencia en que el marcador mas probable fue exactomayor
Total goals biasgoles esperados promedio menos goles reales promediocerca de 0
Over 2.5 gapdiferencia entre probabilidad predicha y tasa real de over 2.5menor
ROIprofit / stake en backtest de apuestasmayor, pero requiere muestra y no leakage
CLVclosing line value, compara cuota tomada vs cierremayor

14. Donde tocar cada cosa

Quieres cambiarToca aquiRiesgo
Features 1X2`scripts/v4/build_v4_feature_store.py`, `train_v4_candidate_model.py`alto, puede cambiar calibracion
Pesos de clase v4.1`scripts/v4/train_v4_1_weighted_model.py`medio-alto, afecta draw/home/away
Temperatura`select_acceptance_temperature` o metadata/weights regenalto si se fuerza manualmente
Umbrales P2F`backend/services/v4_prediction_service.py`medio, no cambia probabilidades
Ratings v31`src/model_ratings_v31.py` y scripts build ratingsmedio-alto, afecta goles
Parametros Poisson`backend/services/model_service.py`alto, afecta lambdas/simulacion
P3G features/params`src/models/p3g_lambda_residual.py`, train script P3Galto, overfit posible
P3H shrinkagemetadata P3H y `p3h_stabilized_lambda_model.py`alto, afecta scorelines/over-under
Torneo`simulation_service.py`, `poisson_probability_cache.py`alto
EV/ROI`scripts/backtests/backtest_*market_edge*`medio, cuidar leakage

15. La frase de presentacion correcta

El sistema productivo separa probabilidad, decision y marcador:

v4.1 calcula probabilidades 1X2 calibradas.
P2F ajusta la decision de empate en partidos parejos sin tocar esas probabilidades.
P3H produce goles, marcadores, over/under y matrices para simulacion.
La simulacion usa P3H para forma de marcadores, reponderada para respetar v4.1.

Esa separacion es intencional. Nos permite mejorar la decision de empate sin romper calibracion, y mejorar marcadores sin reemplazar el 1X2 principal.

16. Fuentes principales

TemaArchivo
Versiones canonicas`src/prediction_modes.py`
API/payload match`backend/services/prediction_pipeline.py`
v4.1 runtime`backend/services/v4_prediction_service.py`
v4.1 training`scripts/v4/train_v4_1_weighted_model.py`
v4 dataset/split`scripts/v4/train_v4_candidate_model.py`
feature store`scripts/v4/build_v4_feature_store.py`
dynamic Elo`scripts/v4/build_dynamic_elo_asof.py`
rolling form`scripts/v4/build_team_form_asof_features.py`
metrics helpers`scripts/v4/common.py`
P2F metadata`models/v4/p2f_v4_1_elo_conditional_threshold_metadata.json`
P2E metadata`models/xgboost/p2e_v4_1_draw_offset_metadata.json`
ratings v3`src/model_ratings_v3.py`
ratings v31`src/model_ratings_v31.py`
Poisson`src/models/poisson.py`
Poisson factory`backend/services/model_service.py`
P3G`src/models/p3g_lambda_residual.py`
P3H`src/models/p3h_stabilized_lambda_model.py`
P3H service`backend/services/p3h_goal_service.py`
torneo`backend/services/simulation_service.py`
reponderacion score matrix`src/simulation/poisson_probability_cache.py`
odds/EV`scripts/backtests/backtest_market_edge_v4_1.py`
Copa del Mundo 2026 by Draft Analytica. Una experiencia especial para seguir el Mundial 2026 con mas contexto.