pablogrois Claude Opus 4.8 commited on
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%: kernel gaussiano sobre la diferencia media por variable (en z).
291
- # rms = dist/√nvars = desvíos promedio por variable; sim = 100·exp(-rms²/2).
292
- # 100% = idéntico; rms 0.4 ≈ 92%; rms 1 61%; rms 2 ≈ 14%.
293
- n_vars = max(1, zmat.shape[1])
294
- import math
 
 
 
 
 
 
 
 
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": round(100.0 * math.exp(-0.5 * (float(dist[i]) ** 2) / n_vars), 1),
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: = distancia promedio entre TODOS los pares del pool
292
+ # (referencia del 50%). sim = 100·2^(-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.number="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,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">Top equipos (<span x-text="tvResult?.pool_size"></span> en el pool)</h2>
 
 
 
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();