"""Анализ ошибок по метаданным персонажей.""" from __future__ import annotations from pathlib import Path from typing import Sequence import pandas as pd from .evaluation import per_class_report DEFAULT_METADATA_PATH = Path(__file__).resolve().parents[1] / "data" / "metadata" / "characters.csv" METADATA_COLUMNS = ("gender", "age_group", "species", "role") def load_character_metadata(path: str | Path | None = None) -> pd.DataFrame: """Читает таблицу пола, возраста, вида и роли персонажей.""" path = Path(path) if path is not None else DEFAULT_METADATA_PATH if not path.exists(): raise FileNotFoundError(f"Нет файла метаданных: {path}") frame = pd.read_csv(path) required = {"character", *METADATA_COLUMNS} missing = required - set(frame.columns) if missing: raise ValueError(f"В метаданных нет колонок: {sorted(missing)}") frame = frame.copy() frame["character"] = frame["character"].astype(str).str.strip() return frame def subgroup_report( y_true: Sequence, y_pred: Sequence, labels: Sequence, metadata: pd.DataFrame, group_column: str = "gender", ) -> pd.DataFrame: """Сводка F-меры по подгруппам персонажей.""" if group_column not in metadata.columns: raise ValueError(f"Нет колонки {group_column!r} в метаданных.") per_class = per_class_report(y_true, y_pred, labels) per_class = per_class.merge( metadata[["character", group_column]], left_on="class", right_on="character", how="left", ) rows = [] for value, group in per_class.groupby(group_column, dropna=False): support = int(group["support"].sum()) weighted = float((group["f1"] * group["support"]).sum() / max(support, 1)) rows.append( { "group_column": group_column, "group": str(value), "n_characters": int(len(group)), "n_samples": support, "weighted_f1": weighted, "mean_class_f1": float(group["f1"].mean()), "min_class_f1": float(group["f1"].min()), "max_class_f1": float(group["f1"].max()), } ) return pd.DataFrame(rows).sort_values(["weighted_f1"], ascending=False) def group_confusions( y_true: Sequence, y_pred: Sequence, metadata: pd.DataFrame, group_column: str = "gender", ) -> pd.DataFrame: """Считает, как часто ошибки происходят внутри и между группами.""" frame = pd.DataFrame({"true_character": list(y_true), "pred_character": list(y_pred)}) frame = frame.merge( metadata[["character", group_column]].rename( columns={"character": "true_character", group_column: "true_group"} ), on="true_character", how="left", ).merge( metadata[["character", group_column]].rename( columns={"character": "pred_character", group_column: "pred_group"} ), on="pred_character", how="left", ) errors = frame[frame["true_character"] != frame["pred_character"]] table = ( errors.groupby(["true_group", "pred_group"], dropna=False) .size() .reset_index(name="count") .sort_values("count", ascending=False) ) total = max(int(table["count"].sum()), 1) table["share_of_errors"] = table["count"] / total table["same_group"] = table["true_group"] == table["pred_group"] return table