Spaces:
Running
Running
Commit ·
1931f95
1
Parent(s): a7f1436
Equipos y variables: similitud calibrada 2^(-d/dbar), default 10, resultados plegables
Browse files- Similitud = 100·2^(-d/dbar), dbar = distancia media entre todos los pares del pool
(50% = equipo promedio; cada dbar, se divide por dos). Más interpretable.
- 'Mostrar' arranca en 10 y devuelve 10 (binding por string).
- Sección Top equipos (lista + radar) plegable con flechita.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
src/racing_reports/team_vars.py
CHANGED
|
@@ -15,6 +15,7 @@ from __future__ import annotations
|
|
| 15 |
|
| 16 |
import threading
|
| 17 |
|
|
|
|
| 18 |
import pandas as pd
|
| 19 |
|
| 20 |
from racing_reports import vendor_env
|
|
@@ -287,18 +288,26 @@ def similar_teams(league: str, season: str, team: str, variables: list[str],
|
|
| 287 |
& (pool["equipo"].astype(str) == str(team)))
|
| 288 |
dist = dist[~is_self.values]
|
| 289 |
order = dist.sort_values().head(top).index
|
| 290 |
-
# Similitud 0-100%:
|
| 291 |
-
#
|
| 292 |
-
# 100%
|
| 293 |
-
|
| 294 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 295 |
teams = [{
|
| 296 |
"equipo": str(pool.loc[i, "equipo"]),
|
| 297 |
"competencia": str(pool.loc[i, "competencia"]),
|
| 298 |
"temporada": str(pool.loc[i, "temporada"]),
|
| 299 |
"partidos": int(pool.loc[i, "n_partidos"]),
|
| 300 |
"distancia": round(float(dist[i]), 2),
|
| 301 |
-
"similitud":
|
| 302 |
} for i in order]
|
| 303 |
return {"equipo": str(team), "competencia": str(league), "temporada": str(season),
|
| 304 |
"scope": scope_label,
|
|
|
|
| 15 |
|
| 16 |
import threading
|
| 17 |
|
| 18 |
+
import numpy as np
|
| 19 |
import pandas as pd
|
| 20 |
|
| 21 |
from racing_reports import vendor_env
|
|
|
|
| 288 |
& (pool["equipo"].astype(str) == str(team)))
|
| 289 |
dist = dist[~is_self.values]
|
| 290 |
order = dist.sort_values().head(top).index
|
| 291 |
+
# Similitud 0-100% calibrada: d̄ = distancia promedio entre TODOS los pares del pool
|
| 292 |
+
# (referencia del 50%). sim = 100·2^(-d/d̄) → cada d̄ de distancia, se divide por dos.
|
| 293 |
+
# 100% en d=0; 84% en 0.25·d̄; 71% en 0.5·d̄; 50% en d̄; 25% en 2·d̄.
|
| 294 |
+
from scipy.spatial.distance import pdist
|
| 295 |
+
Z = zmat.to_numpy()
|
| 296 |
+
if len(Z) > 1500: # cota para pools enormes (scope "todas")
|
| 297 |
+
sidx = np.random.default_rng(0).choice(len(Z), 1500, replace=False)
|
| 298 |
+
Z = Z[sidx]
|
| 299 |
+
dbar = float(pdist(Z).mean()) if len(Z) > 1 else 0.0
|
| 300 |
+
|
| 301 |
+
def _sim(d):
|
| 302 |
+
return round(100.0 * (2.0 ** (-float(d) / dbar)), 1) if dbar > 1e-9 else (100.0 if d < 1e-9 else 0.0)
|
| 303 |
+
|
| 304 |
teams = [{
|
| 305 |
"equipo": str(pool.loc[i, "equipo"]),
|
| 306 |
"competencia": str(pool.loc[i, "competencia"]),
|
| 307 |
"temporada": str(pool.loc[i, "temporada"]),
|
| 308 |
"partidos": int(pool.loc[i, "n_partidos"]),
|
| 309 |
"distancia": round(float(dist[i]), 2),
|
| 310 |
+
"similitud": _sim(dist[i]),
|
| 311 |
} for i in order]
|
| 312 |
return {"equipo": str(team), "competencia": str(league), "temporada": str(season),
|
| 313 |
"scope": scope_label,
|
src/racing_reports/web/templates/home.html
CHANGED
|
@@ -277,9 +277,9 @@
|
|
| 277 |
</div>
|
| 278 |
<div>
|
| 279 |
<label class="block text-xs text-gray-400 mb-1">Mostrar</label>
|
| 280 |
-
<select x-model
|
| 281 |
-
<option
|
| 282 |
-
<option
|
| 283 |
</select>
|
| 284 |
</div>
|
| 285 |
</div>
|
|
@@ -331,8 +331,11 @@
|
|
| 331 |
<p class="text-xs text-red-300 mt-2" x-show="tvError" x-text="tvError"></p>
|
| 332 |
</section>
|
| 333 |
|
| 334 |
-
<section x-show="tvResult" class="space-y-3">
|
| 335 |
-
<h2 class="text-lg font-semibold text-racing-500
|
|
|
|
|
|
|
|
|
|
| 336 |
<p class="text-xs text-yellow-200" x-show="tvResult?.warning" x-text="tvResult?.warning"></p>
|
| 337 |
<template x-for="(t, i) in (tvResult?.teams || [])" :key="t.equipo">
|
| 338 |
<div class="bg-ink-700 border border-ink-600 rounded-2xl p-4" x-data="{ open: false }">
|
|
@@ -365,6 +368,7 @@
|
|
| 365 |
<div class="text-sm font-semibold text-racing-500 mb-2">Radar comparativo del top</div>
|
| 366 |
<img x-show="tvResult?.radar_b64" :src="'data:image/png;base64,'+tvResult?.radar_b64" alt="Radar top equipos" class="w-full max-w-2xl mx-auto rounded-lg">
|
| 367 |
</div>
|
|
|
|
| 368 |
</section>
|
| 369 |
|
| 370 |
<!-- ---- Perfil de un equipo (z-score) ---- -->
|
|
@@ -1036,7 +1040,7 @@
|
|
| 1036 |
tvLeagues: {}, tvLeagueList: [], tvLeague: '__all__', tvSeason: '__all__', tvSeasons: [], tvAllSeasons: [],
|
| 1037 |
tvVarLabels: [], tvLabelToCol: {}, tvColToLabel: {}, tvTypeCols: {}, tvTypeList: [],
|
| 1038 |
tvSpecs: [{variable:'', direction:'alto'}], tvResult: null, tvLoading: false, tvError: '', tvLoaded: false,
|
| 1039 |
-
tvLsOptions: [], tvPool: [], tvPoolIn: '', tvTop: 10, // pool por liga-temporada + nº de equipos
|
| 1040 |
tvsPool: [], tvsPoolIn: '', // pool por liga-temporada (similares)
|
| 1041 |
tvVarsOpen: true, tvpVarsOpen: true, // desplegables de variables (2 y 3)
|
| 1042 |
// Perfil de un equipo (z-score)
|
|
@@ -1231,7 +1235,7 @@
|
|
| 1231 |
try {
|
| 1232 |
const r = await fetch('/api/team-vars/search', {
|
| 1233 |
method:'POST', headers:{'Content-Type':'application/json'},
|
| 1234 |
-
body: JSON.stringify({league:this.tvLeague, season:this.tvSeason, specs, top:(this.tvTop||10), pool}),
|
| 1235 |
});
|
| 1236 |
if (!r.ok) throw new Error((await r.json().catch(()=>({detail:r.statusText}))).detail);
|
| 1237 |
this.tvResult = await r.json();
|
|
|
|
| 277 |
</div>
|
| 278 |
<div>
|
| 279 |
<label class="block text-xs text-gray-400 mb-1">Mostrar</label>
|
| 280 |
+
<select x-model="tvTop" class="bg-ink-800 border border-ink-600 rounded-lg px-3 py-2 text-sm">
|
| 281 |
+
<option value="5">5 equipos</option><option value="10">10 equipos</option>
|
| 282 |
+
<option value="20">20 equipos</option><option value="30">30 equipos</option>
|
| 283 |
</select>
|
| 284 |
</div>
|
| 285 |
</div>
|
|
|
|
| 331 |
<p class="text-xs text-red-300 mt-2" x-show="tvError" x-text="tvError"></p>
|
| 332 |
</section>
|
| 333 |
|
| 334 |
+
<section x-show="tvResult" class="space-y-3" x-data="{ tvResultOpen: true }">
|
| 335 |
+
<h2 class="text-lg font-semibold text-racing-500 cursor-pointer select-none flex items-center gap-2" @click="tvResultOpen=!tvResultOpen">
|
| 336 |
+
<span class="text-sm transition-transform" :class="tvResultOpen ? 'rotate-90' : ''">▶</span>
|
| 337 |
+
Top equipos (<span x-text="tvResult?.pool_size"></span> en el pool)</h2>
|
| 338 |
+
<div x-show="tvResultOpen" class="space-y-3">
|
| 339 |
<p class="text-xs text-yellow-200" x-show="tvResult?.warning" x-text="tvResult?.warning"></p>
|
| 340 |
<template x-for="(t, i) in (tvResult?.teams || [])" :key="t.equipo">
|
| 341 |
<div class="bg-ink-700 border border-ink-600 rounded-2xl p-4" x-data="{ open: false }">
|
|
|
|
| 368 |
<div class="text-sm font-semibold text-racing-500 mb-2">Radar comparativo del top</div>
|
| 369 |
<img x-show="tvResult?.radar_b64" :src="'data:image/png;base64,'+tvResult?.radar_b64" alt="Radar top equipos" class="w-full max-w-2xl mx-auto rounded-lg">
|
| 370 |
</div>
|
| 371 |
+
</div>
|
| 372 |
</section>
|
| 373 |
|
| 374 |
<!-- ---- Perfil de un equipo (z-score) ---- -->
|
|
|
|
| 1040 |
tvLeagues: {}, tvLeagueList: [], tvLeague: '__all__', tvSeason: '__all__', tvSeasons: [], tvAllSeasons: [],
|
| 1041 |
tvVarLabels: [], tvLabelToCol: {}, tvColToLabel: {}, tvTypeCols: {}, tvTypeList: [],
|
| 1042 |
tvSpecs: [{variable:'', direction:'alto'}], tvResult: null, tvLoading: false, tvError: '', tvLoaded: false,
|
| 1043 |
+
tvLsOptions: [], tvPool: [], tvPoolIn: '', tvTop: '10', // pool por liga-temporada + nº de equipos
|
| 1044 |
tvsPool: [], tvsPoolIn: '', // pool por liga-temporada (similares)
|
| 1045 |
tvVarsOpen: true, tvpVarsOpen: true, // desplegables de variables (2 y 3)
|
| 1046 |
// Perfil de un equipo (z-score)
|
|
|
|
| 1235 |
try {
|
| 1236 |
const r = await fetch('/api/team-vars/search', {
|
| 1237 |
method:'POST', headers:{'Content-Type':'application/json'},
|
| 1238 |
+
body: JSON.stringify({league:this.tvLeague, season:this.tvSeason, specs, top:(Number(this.tvTop)||10), pool}),
|
| 1239 |
});
|
| 1240 |
if (!r.ok) throw new Error((await r.json().catch(()=>({detail:r.statusText}))).detail);
|
| 1241 |
this.tvResult = await r.json();
|