Первичная версия: стилометрия реплик, LSA и протокол экспериментов
This commit is contained in:
99
lab1/stylometry/metadata.py
Normal file
99
lab1/stylometry/metadata.py
Normal 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
|
||||
Reference in New Issue
Block a user