Spaces:
Running
Running
Commit ·
bf4531b
1
Parent(s): 281bc09
Tablas bloque bajo: diferencia (±x) vs media de liga-temporada + selector de temporada
Browse files- Artefacto con columna temporada; medias ponderadas por intentos por liga-temporada
- Cada métrica muestra (±x) vs esa media (pv coloreado); columna 'media liga' retirada
- Selector de temporada junto a liga y equipo
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
src/racing_reports/api/routes_lowblock.py
CHANGED
|
@@ -24,8 +24,8 @@ def data(league: str = "__all__") -> dict:
|
|
| 24 |
|
| 25 |
|
| 26 |
@router.get("/team", dependencies=[Depends(require_auth)])
|
| 27 |
-
def team(league: str, equipo: str) -> dict:
|
| 28 |
try:
|
| 29 |
-
return lowblock_data.team_tables(league, equipo)
|
| 30 |
except ValueError as exc:
|
| 31 |
raise HTTPException(status_code=404, detail=str(exc))
|
|
|
|
| 24 |
|
| 25 |
|
| 26 |
@router.get("/team", dependencies=[Depends(require_auth)])
|
| 27 |
+
def team(league: str, equipo: str, season: str | None = None) -> dict:
|
| 28 |
try:
|
| 29 |
+
return lowblock_data.team_tables(league, equipo, season)
|
| 30 |
except ValueError as exc:
|
| 31 |
raise HTTPException(status_code=404, detail=str(exc))
|
src/racing_reports/lowblock_data.py
CHANGED
|
@@ -55,8 +55,11 @@ def _df() -> pd.DataFrame:
|
|
| 55 |
|
| 56 |
def options() -> dict:
|
| 57 |
df = _df()
|
|
|
|
|
|
|
| 58 |
return {
|
| 59 |
"leagues": sorted(df["liga"].unique().tolist()),
|
|
|
|
| 60 |
"recursos": RECURSO_LABELS,
|
| 61 |
"metricas": METRICA_LABELS,
|
| 62 |
"lados": {"of": "Ofensivo", "def": "Defensivo"},
|
|
@@ -67,36 +70,43 @@ def scatter_rows(league: str) -> list[dict]:
|
|
| 67 |
df = _df()
|
| 68 |
if league and league != "__all__":
|
| 69 |
df = df[df["liga"] == league]
|
| 70 |
-
cols = ["liga", "equipo", "lado", "recurso", "n_intentos", "pct_exito", "pv_jugada",
|
| 71 |
"pv_jugada_exitosa", "evento_clave",
|
| 72 |
"tiros_jugada", "xg_acum", "xg_jugada", "xg_jugada_exitosa", "goles", "pct_goles"]
|
| 73 |
out = df[cols].where(pd.notna(df[cols]), None)
|
| 74 |
return out.to_dict("records")
|
| 75 |
|
| 76 |
|
| 77 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
df = _df()
|
| 79 |
liga_df = df[df["liga"] == league]
|
|
|
|
|
|
|
| 80 |
if liga_df.empty:
|
| 81 |
-
raise ValueError(f"Liga sin datos: {league}")
|
| 82 |
-
# medias de la liga ponderadas por intentos (para la referencia "vs liga")
|
| 83 |
out = {}
|
| 84 |
for lado in ("of", "def"):
|
| 85 |
sub = liga_df[liga_df["lado"] == lado]
|
| 86 |
-
|
|
|
|
| 87 |
for rec, g in sub.groupby("recurso"):
|
| 88 |
w = g["n_intentos"].clip(lower=1)
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
(g["pct_exito"] * w).sum() / w.sum()),
|
| 94 |
-
"xg_jugada": round(float((g["xg_jugada"].fillna(0) * w).sum() / w.sum()), 4),
|
| 95 |
-
}
|
| 96 |
rows = sub[sub["equipo"] == equipo].sort_values("pv_jugada", ascending=False)
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
"recursos": RECURSO_LABELS, **out}
|
|
|
|
| 55 |
|
| 56 |
def options() -> dict:
|
| 57 |
df = _df()
|
| 58 |
+
seasons = {lg: sorted(g["temporada"].astype(str).unique(), reverse=True)
|
| 59 |
+
for lg, g in df.groupby("liga")}
|
| 60 |
return {
|
| 61 |
"leagues": sorted(df["liga"].unique().tolist()),
|
| 62 |
+
"seasons": seasons,
|
| 63 |
"recursos": RECURSO_LABELS,
|
| 64 |
"metricas": METRICA_LABELS,
|
| 65 |
"lados": {"of": "Ofensivo", "def": "Defensivo"},
|
|
|
|
| 70 |
df = _df()
|
| 71 |
if league and league != "__all__":
|
| 72 |
df = df[df["liga"] == league]
|
| 73 |
+
cols = ["liga", "temporada", "equipo", "lado", "recurso", "n_intentos", "pct_exito", "pv_jugada",
|
| 74 |
"pv_jugada_exitosa", "evento_clave",
|
| 75 |
"tiros_jugada", "xg_acum", "xg_jugada", "xg_jugada_exitosa", "goles", "pct_goles"]
|
| 76 |
out = df[cols].where(pd.notna(df[cols]), None)
|
| 77 |
return out.to_dict("records")
|
| 78 |
|
| 79 |
|
| 80 |
+
DIF_METRICS = {"pct_exito": 1, "pv_jugada": 2, "pv_jugada_exitosa": 2,
|
| 81 |
+
"tiros_jugada": 3, "xg_jugada": 4, "pct_goles": 2}
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def team_tables(league: str, equipo: str, season: str | None = None) -> dict:
|
| 85 |
df = _df()
|
| 86 |
liga_df = df[df["liga"] == league]
|
| 87 |
+
if season:
|
| 88 |
+
liga_df = liga_df[liga_df["temporada"].astype(str) == str(season)]
|
| 89 |
if liga_df.empty:
|
| 90 |
+
raise ValueError(f"Liga/temporada sin datos: {league} {season or ''}")
|
|
|
|
| 91 |
out = {}
|
| 92 |
for lado in ("of", "def"):
|
| 93 |
sub = liga_df[liga_df["lado"] == lado]
|
| 94 |
+
# media de la liga ponderada por intentos, por recurso y métrica
|
| 95 |
+
medias: dict[str, dict] = {}
|
| 96 |
for rec, g in sub.groupby("recurso"):
|
| 97 |
w = g["n_intentos"].clip(lower=1)
|
| 98 |
+
medias[rec] = {}
|
| 99 |
+
for m, nd in DIF_METRICS.items():
|
| 100 |
+
v = (g[m] * w).sum() / w.sum()
|
| 101 |
+
medias[rec][m] = round(float(v), nd) if pd.notna(v) else None
|
|
|
|
|
|
|
|
|
|
| 102 |
rows = sub[sub["equipo"] == equipo].sort_values("pv_jugada", ascending=False)
|
| 103 |
+
recs = rows.where(pd.notna(rows), None).to_dict("records")
|
| 104 |
+
for r in recs:
|
| 105 |
+
mu = medias.get(r["recurso"], {})
|
| 106 |
+
r["dif"] = {}
|
| 107 |
+
for m, nd in DIF_METRICS.items():
|
| 108 |
+
if r.get(m) is not None and mu.get(m) is not None:
|
| 109 |
+
r["dif"][m] = round(float(r[m]) - mu[m], nd)
|
| 110 |
+
out[lado] = {"rows": recs, "liga_media": medias}
|
| 111 |
+
return {"league": league, "season": season, "equipo": equipo,
|
| 112 |
"recursos": RECURSO_LABELS, **out}
|
src/racing_reports/web/templates/home.html
CHANGED
|
@@ -573,6 +573,12 @@
|
|
| 573 |
<template x-for="l in lbLeagues" :key="l"><option :value="l" x-text="l"></option></template>
|
| 574 |
</select>
|
| 575 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 576 |
<div>
|
| 577 |
<label class="block text-xs text-gray-400 mb-1">Equipo</label>
|
| 578 |
<select x-model="lbtEquipo" class="bg-ink-800 border border-ink-600 rounded-lg px-3 py-2 text-sm">
|
|
@@ -582,7 +588,7 @@
|
|
| 582 |
<button @click="lbtRun()" class="bg-racing-500 hover:bg-racing-600 text-racing-900 font-semibold px-4 py-2 rounded-lg text-sm">Generar tablas</button>
|
| 583 |
</div>
|
| 584 |
<p class="text-xs text-red-300" x-show="lbtError" x-text="lbtError"></p>
|
| 585 |
-
<p class="text-xs text-gray-500 mb-2" x-show="lbtResult">Peligro/jugada = pvAdded medio por intento (‰): un valor negativo significa que en promedio esos intentos PIERDEN valor de posesión (el costo de los fallos supera lo que generan los aciertos). Evento clave = nº de ataques en los que ese recurso fue la acción que MÁS peligro agregó. Rompe-líneas no muestra % éxito (el proveedor solo marca los completados). El tiro desde fuera del área no muestra pv ni evento clave: el tiro REALIZA el peligro en vez de agregarlo (el gol tiene pvAdded nulo y el errado negativo) — usar % éxito (= % gol), xG y goles.</p>
|
| 586 |
<template x-for="lado in ['of','def']" :key="lado">
|
| 587 |
<div x-show="lbtResult" class="mb-4">
|
| 588 |
<div class="text-xs uppercase tracking-wide mb-1" :class="lado==='of' ? 'text-racing-200' : 'text-yellow-200'"
|
|
@@ -590,7 +596,7 @@
|
|
| 590 |
<table class="w-full text-xs">
|
| 591 |
<thead><tr class="text-gray-400 text-left">
|
| 592 |
<th class="py-1">Recurso</th><th class="text-right">Intentos</th><th class="text-right">% éxito</th>
|
| 593 |
-
<th class="text-right">Peligro/jugada (‰)</th>
|
| 594 |
<th class="text-right">Peligro/jug. exitosa</th><th class="text-right">Evento clave</th>
|
| 595 |
<th class="text-right">Tiros/jugada</th><th class="text-right">xG/jugada</th><th class="text-right">Goles</th><th class="text-right">% goles</th>
|
| 596 |
</tr></thead>
|
|
@@ -599,15 +605,14 @@
|
|
| 599 |
<tr class="border-t border-ink-600">
|
| 600 |
<td class="py-1" x-text="lbRecursos[r.recurso]||r.recurso"></td>
|
| 601 |
<td class="text-right" x-text="r.n_intentos"></td>
|
| 602 |
-
<td class="text-right" x-text="r.pct_exito!=null ? r.pct_exito+'%' : '—'"></td>
|
| 603 |
-
<td class="text-right font-semibold" x-text="r.pv_jugada!=null ? r.pv_jugada : '—'"></td>
|
| 604 |
-
<td class="text-right
|
| 605 |
-
<td class="text-right" x-text="r.pv_jugada_exitosa ?? '—'"></td>
|
| 606 |
<td class="text-right" x-text="r.evento_clave ?? '—'"></td>
|
| 607 |
-
<td class="text-right" x-text="r.tiros_jugada ?? '—'"></td>
|
| 608 |
-
<td class="text-right" x-text="r.xg_jugada ?? '—'"></td>
|
| 609 |
<td class="text-right" x-text="r.goles ?? '—'"></td>
|
| 610 |
-
<td class="text-right" x-text="r.pct_goles!=null ? r.pct_goles+'%' : '—'"></td>
|
| 611 |
</tr>
|
| 612 |
</template>
|
| 613 |
</tbody>
|
|
@@ -738,6 +743,7 @@
|
|
| 738 |
lbPinned: {},
|
| 739 |
lbtLeague: '', lbtEquipo: '', lbtEquipos: [], lbtResult: null, lbtError: '',
|
| 740 |
lbBuscar: '', lbEquiposPlot: [],
|
|
|
|
| 741 |
|
| 742 |
async init() { await this.loadLeagues(); await this.loadSeasons(false); await this.loadTeams(); this._refreshHealth(); },
|
| 743 |
async _getJSON(u){ const r=await fetch(u); if(!r.ok) throw new Error((await r.json().catch(()=>({detail:r.statusText}))).detail); return r.json(); },
|
|
@@ -966,6 +972,7 @@
|
|
| 966 |
try {
|
| 967 |
const o = await this._getJSON('/api/lowblock/options');
|
| 968 |
this.lbLeagues = o.leagues||[]; this.lbRecursos = o.recursos||{}; this.lbMetricas = o.metricas||{};
|
|
|
|
| 969 |
this.lbLoaded = true;
|
| 970 |
await this.$nextTick();
|
| 971 |
this.lbX = {...this.lbX}; this.lbY = {...this.lbY};
|
|
@@ -1053,6 +1060,8 @@
|
|
| 1053 |
},
|
| 1054 |
async lbtLoadTeams(){
|
| 1055 |
if (!this.lbtLeague){ this.lbtEquipos=[]; return; }
|
|
|
|
|
|
|
| 1056 |
try {
|
| 1057 |
const d = await this._getJSON(`/api/lowblock/data?league=${encodeURIComponent(this.lbtLeague)}`);
|
| 1058 |
this.lbtEquipos = [...new Set((d.rows||[]).map(r=>r.equipo))].sort();
|
|
@@ -1060,11 +1069,17 @@
|
|
| 1060 |
this.lbtEquipo = this.lbtEquipos.includes('Racing de Santander') ? 'Racing de Santander' : (this.lbtEquipos[0]||'');
|
| 1061 |
} catch(e){ this.lbtEquipos=[]; }
|
| 1062 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1063 |
async lbtRun(){
|
| 1064 |
this.lbtError=''; this.lbtResult=null;
|
| 1065 |
if (!this.lbtLeague || !this.lbtEquipo){ this.lbtError='Elegí liga y equipo.'; return; }
|
| 1066 |
try {
|
| 1067 |
-
this.lbtResult = await this._getJSON(`/api/lowblock/team?league=${encodeURIComponent(this.lbtLeague)}&equipo=${encodeURIComponent(this.lbtEquipo)}`);
|
| 1068 |
} catch(e){ this.lbtError = 'Error: '+e; }
|
| 1069 |
},
|
| 1070 |
async ejRunPost(){
|
|
|
|
| 573 |
<template x-for="l in lbLeagues" :key="l"><option :value="l" x-text="l"></option></template>
|
| 574 |
</select>
|
| 575 |
</div>
|
| 576 |
+
<div>
|
| 577 |
+
<label class="block text-xs text-gray-400 mb-1">Temporada</label>
|
| 578 |
+
<select x-model="lbtSeason" class="bg-ink-800 border border-ink-600 rounded-lg px-3 py-2 text-sm">
|
| 579 |
+
<template x-for="s in (lbSeasons[lbtLeague]||[])" :key="s"><option :value="s" x-text="s"></option></template>
|
| 580 |
+
</select>
|
| 581 |
+
</div>
|
| 582 |
<div>
|
| 583 |
<label class="block text-xs text-gray-400 mb-1">Equipo</label>
|
| 584 |
<select x-model="lbtEquipo" class="bg-ink-800 border border-ink-600 rounded-lg px-3 py-2 text-sm">
|
|
|
|
| 588 |
<button @click="lbtRun()" class="bg-racing-500 hover:bg-racing-600 text-racing-900 font-semibold px-4 py-2 rounded-lg text-sm">Generar tablas</button>
|
| 589 |
</div>
|
| 590 |
<p class="text-xs text-red-300" x-show="lbtError" x-text="lbtError"></p>
|
| 591 |
+
<p class="text-xs text-gray-500 mb-2" x-show="lbtResult">Peligro/jugada = pvAdded medio por intento (‰): un valor negativo significa que en promedio esos intentos PIERDEN valor de posesión (el costo de los fallos supera lo que generan los aciertos). Evento clave = nº de ataques en los que ese recurso fue la acción que MÁS peligro agregó. Entre paréntesis: diferencia vs la media (ponderada por intentos) de esa liga-temporada. Rompe-líneas no muestra % éxito (el proveedor solo marca los completados). El tiro desde fuera del área no muestra pv ni evento clave: el tiro REALIZA el peligro en vez de agregarlo (el gol tiene pvAdded nulo y el errado negativo) — usar % éxito (= % gol), xG y goles.</p>
|
| 592 |
<template x-for="lado in ['of','def']" :key="lado">
|
| 593 |
<div x-show="lbtResult" class="mb-4">
|
| 594 |
<div class="text-xs uppercase tracking-wide mb-1" :class="lado==='of' ? 'text-racing-200' : 'text-yellow-200'"
|
|
|
|
| 596 |
<table class="w-full text-xs">
|
| 597 |
<thead><tr class="text-gray-400 text-left">
|
| 598 |
<th class="py-1">Recurso</th><th class="text-right">Intentos</th><th class="text-right">% éxito</th>
|
| 599 |
+
<th class="text-right">Peligro/jugada (‰)</th>
|
| 600 |
<th class="text-right">Peligro/jug. exitosa</th><th class="text-right">Evento clave</th>
|
| 601 |
<th class="text-right">Tiros/jugada</th><th class="text-right">xG/jugada</th><th class="text-right">Goles</th><th class="text-right">% goles</th>
|
| 602 |
</tr></thead>
|
|
|
|
| 605 |
<tr class="border-t border-ink-600">
|
| 606 |
<td class="py-1" x-text="lbRecursos[r.recurso]||r.recurso"></td>
|
| 607 |
<td class="text-right" x-text="r.n_intentos"></td>
|
| 608 |
+
<td class="text-right"><span x-text="r.pct_exito!=null ? r.pct_exito+'%' : '—'"></span><span class="text-gray-500" x-text="lbDif(r,'pct_exito')"></span></td>
|
| 609 |
+
<td class="text-right font-semibold"><span x-text="r.pv_jugada!=null ? r.pv_jugada : '—'"></span><span class="font-normal" :class="((r.dif||{}).pv_jugada||0)>=0 ? 'text-racing-200' : 'text-red-300'" x-text="lbDif(r,'pv_jugada')"></span></td>
|
| 610 |
+
<td class="text-right"><span x-text="r.pv_jugada_exitosa ?? '—'"></span><span class="text-gray-500" x-text="lbDif(r,'pv_jugada_exitosa')"></span></td>
|
|
|
|
| 611 |
<td class="text-right" x-text="r.evento_clave ?? '—'"></td>
|
| 612 |
+
<td class="text-right"><span x-text="r.tiros_jugada ?? '—'"></span><span class="text-gray-500" x-text="lbDif(r,'tiros_jugada')"></span></td>
|
| 613 |
+
<td class="text-right"><span x-text="r.xg_jugada ?? '—'"></span><span class="text-gray-500" x-text="lbDif(r,'xg_jugada')"></span></td>
|
| 614 |
<td class="text-right" x-text="r.goles ?? '—'"></td>
|
| 615 |
+
<td class="text-right"><span x-text="r.pct_goles!=null ? r.pct_goles+'%' : '—'"></span><span class="text-gray-500" x-text="lbDif(r,'pct_goles')"></span></td>
|
| 616 |
</tr>
|
| 617 |
</template>
|
| 618 |
</tbody>
|
|
|
|
| 743 |
lbPinned: {},
|
| 744 |
lbtLeague: '', lbtEquipo: '', lbtEquipos: [], lbtResult: null, lbtError: '',
|
| 745 |
lbBuscar: '', lbEquiposPlot: [],
|
| 746 |
+
lbSeasons: {}, lbtSeason: '',
|
| 747 |
|
| 748 |
async init() { await this.loadLeagues(); await this.loadSeasons(false); await this.loadTeams(); this._refreshHealth(); },
|
| 749 |
async _getJSON(u){ const r=await fetch(u); if(!r.ok) throw new Error((await r.json().catch(()=>({detail:r.statusText}))).detail); return r.json(); },
|
|
|
|
| 972 |
try {
|
| 973 |
const o = await this._getJSON('/api/lowblock/options');
|
| 974 |
this.lbLeagues = o.leagues||[]; this.lbRecursos = o.recursos||{}; this.lbMetricas = o.metricas||{};
|
| 975 |
+
this.lbSeasons = o.seasons||{};
|
| 976 |
this.lbLoaded = true;
|
| 977 |
await this.$nextTick();
|
| 978 |
this.lbX = {...this.lbX}; this.lbY = {...this.lbY};
|
|
|
|
| 1060 |
},
|
| 1061 |
async lbtLoadTeams(){
|
| 1062 |
if (!this.lbtLeague){ this.lbtEquipos=[]; return; }
|
| 1063 |
+
const ss = this.lbSeasons[this.lbtLeague]||[];
|
| 1064 |
+
if (!ss.includes(this.lbtSeason)) this.lbtSeason = ss[0]||'';
|
| 1065 |
try {
|
| 1066 |
const d = await this._getJSON(`/api/lowblock/data?league=${encodeURIComponent(this.lbtLeague)}`);
|
| 1067 |
this.lbtEquipos = [...new Set((d.rows||[]).map(r=>r.equipo))].sort();
|
|
|
|
| 1069 |
this.lbtEquipo = this.lbtEquipos.includes('Racing de Santander') ? 'Racing de Santander' : (this.lbtEquipos[0]||'');
|
| 1070 |
} catch(e){ this.lbtEquipos=[]; }
|
| 1071 |
},
|
| 1072 |
+
lbDif(r, m){
|
| 1073 |
+
const d = (r.dif||{})[m];
|
| 1074 |
+
if (d==null) return '';
|
| 1075 |
+
const s = (d>0?'+':'') + d;
|
| 1076 |
+
return ` (${s})`;
|
| 1077 |
+
},
|
| 1078 |
async lbtRun(){
|
| 1079 |
this.lbtError=''; this.lbtResult=null;
|
| 1080 |
if (!this.lbtLeague || !this.lbtEquipo){ this.lbtError='Elegí liga y equipo.'; return; }
|
| 1081 |
try {
|
| 1082 |
+
this.lbtResult = await this._getJSON(`/api/lowblock/team?league=${encodeURIComponent(this.lbtLeague)}&equipo=${encodeURIComponent(this.lbtEquipo)}&season=${encodeURIComponent(this.lbtSeason)}`);
|
| 1083 |
} catch(e){ this.lbtError = 'Error: '+e; }
|
| 1084 |
},
|
| 1085 |
async ejRunPost(){
|
vendor/data/lowblock/team_resources.parquet
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:2ab8ad8a165c43d5790dea6fc45afa5afeb733c140f2074f0157aa376eee1049
|
| 3 |
+
size 237224
|