100 lines
3.6 KiB
Python
100 lines
3.6 KiB
Python
"""Анализ ошибок по метаданным персонажей."""
|
||
|
||
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
|