Первичная версия: стилометрия реплик, LSA и протокол экспериментов

This commit is contained in:
2026-10-24 01:17:36 +03:00
commit c390224500
52 changed files with 10099 additions and 0 deletions

View File

@@ -0,0 +1,99 @@
"""Анализ ошибок по метаданным персонажей."""
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