pablogrois Claude Opus 4.8 commited on
Commit
f4fc4ad
·
1 Parent(s): 0d8f152

Multitag: invertir colores en defensivo (y en tags negativos)

Browse files

El borde verde/rojo del z-score indicaba 'arriba/abajo de la media' sin tener
en cuenta si más es mejor o peor. Ahora:
- Paneles DEFENSIVOS: se invierte siempre (z alto = concedés más = rojo).
- Tags negativos en ofensivo (NEGATIVE_TAGS, hoy vacío): se invierten.
Aplica a matplotlib y plotly, multitag y centros. Neutral (|z|<0.5) sin cambios.

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

src/racing_reports/reports/triple_multitag.py CHANGED
@@ -656,7 +656,17 @@ def _zscore(col: pd.Series, value: float) -> float:
656
  return float((value - mu) / sd)
657
 
658
 
659
- def _z_color(z: float) -> str:
 
 
 
 
 
 
 
 
 
 
660
  a = abs(z)
661
  if a < 0.5:
662
  return "#F2C14E"
@@ -675,7 +685,7 @@ def _legend_handles():
675
  ]
676
 
677
 
678
- def _draw_panel(ax, side_df, tags, tag_groups, team, rng, *, title="", legend=False) -> None:
679
  n = len(tags)
680
  y = np.arange(n)[::-1]
681
  ax.axvspan(-1, 1, color="#000000", alpha=0.04, zorder=0)
@@ -686,6 +696,7 @@ def _draw_panel(ax, side_df, tags, tag_groups, team, rng, *, title="", legend=Fa
686
  has_team = team in side_df.index
687
  for i, tag in enumerate(tags):
688
  yi = y[i]
 
689
  for metric, (marker, unit) in metric_style.items():
690
  yj = yi + metric_dy[metric]
691
  col = side_df[(tag, metric)]
@@ -695,7 +706,7 @@ def _draw_panel(ax, side_df, tags, tag_groups, team, rng, *, title="", legend=Fa
695
  val = float(col.get(team, 0.0)) if has_team else 0.0
696
  z = _zscore(col, val) if has_team else 0.0
697
  ax.scatter([z], [yj], marker=marker, s=140, color=METRIC_COLOR[metric],
698
- edgecolors=_z_color(z), linewidths=2.0, zorder=4)
699
  txt = f"{val:.2f} xG" if metric == "xg" else f"{int(round(val))} {unit}"
700
  ax.annotate(txt, (z, yj), textcoords="offset points", xytext=(9, 0), va="center",
701
  fontsize=7, bbox=dict(boxstyle="round,pad=0.18", fc="#FFFFFF",
@@ -720,8 +731,9 @@ def _draw_panel(ax, side_df, tags, tag_groups, team, rng, *, title="", legend=Fa
720
  def plot_triple(side_df: pd.DataFrame, team: str, side_label: str, title_team: str, seed: int = 7) -> plt.Figure:
721
  """Figura de z-scores triple multitag (1 panel) para un equipo y un lado."""
722
  rng = np.random.default_rng(seed)
 
723
  fig, ax = plt.subplots(figsize=(11, 0.62 * len(TAGS) + 2.0), facecolor="#FFFFFF")
724
- _draw_panel(ax, side_df, TAGS, TAG_GROUPS, team, rng, legend=True)
725
  fig.suptitle(f"Z-Score Multi-Tag — Triple Análisis | {side_label}\n{title_team}",
726
  fontsize=13, fontweight="bold")
727
  fig.tight_layout(rect=(0, 0, 1, 0.97))
@@ -742,6 +754,7 @@ def _grouped_centros_df(detail_df: pd.DataFrame) -> pd.DataFrame:
742
  def plot_centros(detail_df: pd.DataFrame, team: str, side_label: str, title_team: str, seed: int = 7) -> plt.Figure:
743
  """Figura de z-scores de centros: 2 paneles (detalle por tipo·lado + agrupado)."""
744
  rng = np.random.default_rng(seed)
 
745
  grouped_df = _grouped_centros_df(detail_df)
746
  grouped_groups = [[g] for g in CENTROS_GROUPED]
747
  fig, (axd, axg) = plt.subplots(
@@ -749,9 +762,9 @@ def plot_centros(detail_df: pd.DataFrame, team: str, side_label: str, title_team
749
  gridspec_kw={"width_ratios": [1.15, 1.0]},
750
  )
751
  _draw_panel(axd, detail_df, CENTROS_DETAIL, CENTROS_DETAIL_GROUPS, team, rng,
752
- title="Desglose por tipo · lado", legend=True)
753
  _draw_panel(axg, grouped_df, list(CENTROS_GROUPED), grouped_groups, team, rng,
754
- title="Vista agrupada")
755
  fig.suptitle(f"Z-Score Centros — Triple Análisis | {side_label}\n{title_team}",
756
  fontsize=13, fontweight="bold")
757
  fig.tight_layout(rect=(0, 0, 1, 0.96))
@@ -786,7 +799,7 @@ def _fmt_val(metric: str, v: float) -> str:
786
 
787
 
788
  def _plotly_panel(fig, side_df: pd.DataFrame, tags: list[str], tag_groups: list[list[str]],
789
- team: str, row: int, col: int, rng, show_legend: bool) -> None:
790
  import plotly.graph_objects as go
791
  n = len(tags)
792
  ybase = {t: (n - 1 - i) for i, t in enumerate(tags)}
@@ -816,7 +829,7 @@ def _plotly_panel(fig, side_df: pd.DataFrame, tags: list[str], tag_groups: list[
816
  zz, vv = z[i], float(c.iloc[i])
817
  hx.append(zz); hy.append(ybase[tag] + _METRIC_DY[metric])
818
  hcd.append([team, _fmt_val(metric, vv), tag, mlabel])
819
- hborder.append(_z_color(zz)) # borde según magnitud/signo del z
820
  htext.append(_fmt_val(metric, vv))
821
  fig.add_trace(go.Scatter(
822
  x=hx, y=hy, mode="markers+text", legendgroup=mlabel, showlegend=False,
@@ -843,8 +856,9 @@ def _plotly_panel(fig, side_df: pd.DataFrame, tags: list[str], tag_groups: list[
843
  def plotly_triple(side_df: pd.DataFrame, team: str, side_label: str, title_team: str, seed: int = 7):
844
  from plotly.subplots import make_subplots
845
  rng = np.random.default_rng(seed)
 
846
  fig = make_subplots(rows=1, cols=1)
847
- _plotly_panel(fig, side_df, TAGS, TAG_GROUPS, team, 1, 1, rng, show_legend=True)
848
  fig.update_layout(template="plotly_white", hovermode="closest",
849
  height=_ROW_PX * len(TAGS) + 180,
850
  font=dict(size=13),
@@ -856,12 +870,13 @@ def plotly_triple(side_df: pd.DataFrame, team: str, side_label: str, title_team:
856
  def plotly_centros(detail_df: pd.DataFrame, team: str, side_label: str, title_team: str, seed: int = 7):
857
  from plotly.subplots import make_subplots
858
  rng = np.random.default_rng(seed)
 
859
  grouped_df = _grouped_centros_df(detail_df)
860
  grouped_groups = [[g] for g in CENTROS_GROUPED]
861
  fig = make_subplots(rows=1, cols=2, horizontal_spacing=0.18,
862
  subplot_titles=("Desglose por tipo · lado", "Vista agrupada"))
863
- _plotly_panel(fig, detail_df, CENTROS_DETAIL, CENTROS_DETAIL_GROUPS, team, 1, 1, rng, show_legend=True)
864
- _plotly_panel(fig, grouped_df, list(CENTROS_GROUPED), grouped_groups, team, 1, 2, rng, show_legend=False)
865
  fig.update_layout(template="plotly_white", hovermode="closest",
866
  height=_ROW_PX * len(CENTROS_DETAIL) + 200,
867
  font=dict(size=13),
 
656
  return float((value - mu) / sd)
657
 
658
 
659
+ # Tags "negativos" en OFENSIVO: más = peor para el equipo (se invierten los colores
660
+ # verde/rojo). Vacío por ahora — casi todos los tags del multitag son recursos ofensivos
661
+ # donde más = más producción. El defensivo se invierte SIEMPRE aparte (ver is_def).
662
+ NEGATIVE_TAGS: set[str] = set()
663
+
664
+
665
+ def _z_color(z: float, invert: bool = False) -> str:
666
+ # invert=True → un z alto es MALO (rojo) y un z bajo es BUENO (verde). Se usa para
667
+ # los paneles defensivos (lo que concedés) y para tags negativos en ofensivo.
668
+ if invert:
669
+ z = -z
670
  a = abs(z)
671
  if a < 0.5:
672
  return "#F2C14E"
 
685
  ]
686
 
687
 
688
+ def _draw_panel(ax, side_df, tags, tag_groups, team, rng, *, title="", legend=False, is_def=False) -> None:
689
  n = len(tags)
690
  y = np.arange(n)[::-1]
691
  ax.axvspan(-1, 1, color="#000000", alpha=0.04, zorder=0)
 
696
  has_team = team in side_df.index
697
  for i, tag in enumerate(tags):
698
  yi = y[i]
699
+ inv = is_def ^ (tag in NEGATIVE_TAGS) # defensivo o tag negativo → invertir verde/rojo
700
  for metric, (marker, unit) in metric_style.items():
701
  yj = yi + metric_dy[metric]
702
  col = side_df[(tag, metric)]
 
706
  val = float(col.get(team, 0.0)) if has_team else 0.0
707
  z = _zscore(col, val) if has_team else 0.0
708
  ax.scatter([z], [yj], marker=marker, s=140, color=METRIC_COLOR[metric],
709
+ edgecolors=_z_color(z, inv), linewidths=2.0, zorder=4)
710
  txt = f"{val:.2f} xG" if metric == "xg" else f"{int(round(val))} {unit}"
711
  ax.annotate(txt, (z, yj), textcoords="offset points", xytext=(9, 0), va="center",
712
  fontsize=7, bbox=dict(boxstyle="round,pad=0.18", fc="#FFFFFF",
 
731
  def plot_triple(side_df: pd.DataFrame, team: str, side_label: str, title_team: str, seed: int = 7) -> plt.Figure:
732
  """Figura de z-scores triple multitag (1 panel) para un equipo y un lado."""
733
  rng = np.random.default_rng(seed)
734
+ is_def = "defensiv" in side_label.lower()
735
  fig, ax = plt.subplots(figsize=(11, 0.62 * len(TAGS) + 2.0), facecolor="#FFFFFF")
736
+ _draw_panel(ax, side_df, TAGS, TAG_GROUPS, team, rng, legend=True, is_def=is_def)
737
  fig.suptitle(f"Z-Score Multi-Tag — Triple Análisis | {side_label}\n{title_team}",
738
  fontsize=13, fontweight="bold")
739
  fig.tight_layout(rect=(0, 0, 1, 0.97))
 
754
  def plot_centros(detail_df: pd.DataFrame, team: str, side_label: str, title_team: str, seed: int = 7) -> plt.Figure:
755
  """Figura de z-scores de centros: 2 paneles (detalle por tipo·lado + agrupado)."""
756
  rng = np.random.default_rng(seed)
757
+ is_def = "defensiv" in side_label.lower()
758
  grouped_df = _grouped_centros_df(detail_df)
759
  grouped_groups = [[g] for g in CENTROS_GROUPED]
760
  fig, (axd, axg) = plt.subplots(
 
762
  gridspec_kw={"width_ratios": [1.15, 1.0]},
763
  )
764
  _draw_panel(axd, detail_df, CENTROS_DETAIL, CENTROS_DETAIL_GROUPS, team, rng,
765
+ title="Desglose por tipo · lado", legend=True, is_def=is_def)
766
  _draw_panel(axg, grouped_df, list(CENTROS_GROUPED), grouped_groups, team, rng,
767
+ title="Vista agrupada", is_def=is_def)
768
  fig.suptitle(f"Z-Score Centros — Triple Análisis | {side_label}\n{title_team}",
769
  fontsize=13, fontweight="bold")
770
  fig.tight_layout(rect=(0, 0, 1, 0.96))
 
799
 
800
 
801
  def _plotly_panel(fig, side_df: pd.DataFrame, tags: list[str], tag_groups: list[list[str]],
802
+ team: str, row: int, col: int, rng, show_legend: bool, is_def: bool = False) -> None:
803
  import plotly.graph_objects as go
804
  n = len(tags)
805
  ybase = {t: (n - 1 - i) for i, t in enumerate(tags)}
 
829
  zz, vv = z[i], float(c.iloc[i])
830
  hx.append(zz); hy.append(ybase[tag] + _METRIC_DY[metric])
831
  hcd.append([team, _fmt_val(metric, vv), tag, mlabel])
832
+ hborder.append(_z_color(zz, is_def ^ (tag in NEGATIVE_TAGS))) # verde/rojo (invertido si defensivo o tag negativo)
833
  htext.append(_fmt_val(metric, vv))
834
  fig.add_trace(go.Scatter(
835
  x=hx, y=hy, mode="markers+text", legendgroup=mlabel, showlegend=False,
 
856
  def plotly_triple(side_df: pd.DataFrame, team: str, side_label: str, title_team: str, seed: int = 7):
857
  from plotly.subplots import make_subplots
858
  rng = np.random.default_rng(seed)
859
+ is_def = "defensiv" in side_label.lower()
860
  fig = make_subplots(rows=1, cols=1)
861
+ _plotly_panel(fig, side_df, TAGS, TAG_GROUPS, team, 1, 1, rng, show_legend=True, is_def=is_def)
862
  fig.update_layout(template="plotly_white", hovermode="closest",
863
  height=_ROW_PX * len(TAGS) + 180,
864
  font=dict(size=13),
 
870
  def plotly_centros(detail_df: pd.DataFrame, team: str, side_label: str, title_team: str, seed: int = 7):
871
  from plotly.subplots import make_subplots
872
  rng = np.random.default_rng(seed)
873
+ is_def = "defensiv" in side_label.lower()
874
  grouped_df = _grouped_centros_df(detail_df)
875
  grouped_groups = [[g] for g in CENTROS_GROUPED]
876
  fig = make_subplots(rows=1, cols=2, horizontal_spacing=0.18,
877
  subplot_titles=("Desglose por tipo · lado", "Vista agrupada"))
878
+ _plotly_panel(fig, detail_df, CENTROS_DETAIL, CENTROS_DETAIL_GROUPS, team, 1, 1, rng, show_legend=True, is_def=is_def)
879
+ _plotly_panel(fig, grouped_df, list(CENTROS_GROUPED), grouped_groups, team, 1, 2, rng, show_legend=False, is_def=is_def)
880
  fig.update_layout(template="plotly_white", hovermode="closest",
881
  height=_ROW_PX * len(CENTROS_DETAIL) + 200,
882
  font=dict(size=13),