Первичная версия: стилометрия реплик, 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

143
lab1/stylometry/stats.py Normal file
View File

@@ -0,0 +1,143 @@
"""Статистические критерии для сравнения моделей."""
from __future__ import annotations
from typing import Sequence
import numpy as np
from scipy.stats import binomtest, friedmanchisquare, wilcoxon
from sklearn.metrics import accuracy_score, f1_score
def mcnemar_exact(
y_true: Sequence,
predictions_a: Sequence,
predictions_b: Sequence,
) -> dict[str, float]:
"""Точный критерий Мак-Немара для двух парных предсказаний."""
y_true = np.asarray(y_true)
correct_a = np.asarray(predictions_a) == y_true
correct_b = np.asarray(predictions_b) == y_true
b = int(np.sum(correct_a & ~correct_b))
c = int(np.sum(~correct_a & correct_b))
n = b + c
p_value = 1.0 if n == 0 else float(binomtest(min(b, c), n, 0.5).pvalue)
return {
"b": b,
"c": c,
"n_discordant": n,
"statistic": float(min(b, c)),
"p_value": p_value,
}
def wilcoxon_signed_rank(
scores_a: Sequence[float],
scores_b: Sequence[float],
) -> dict[str, float]:
"""Парный критерий Уилкоксона для результатов по фолдам."""
scores_a = np.asarray(scores_a, dtype=float)
scores_b = np.asarray(scores_b, dtype=float)
differences = scores_a - scores_b
if np.allclose(differences, 0):
return {"statistic": 0.0, "p_value": 1.0, "n": int(len(scores_a))}
statistic, p_value = wilcoxon(scores_a, scores_b, alternative="two-sided")
return {
"statistic": float(statistic),
"p_value": float(p_value),
"n": int(len(scores_a)),
}
def friedman_test(*score_arrays: Sequence[float]) -> dict[str, float]:
"""Критерий Фридмана для сравнения нескольких моделей."""
arrays = [np.asarray(values, dtype=float) for values in score_arrays]
if len(arrays) < 3:
raise ValueError("Для критерия Фридмана нужно хотя бы три модели.")
if len({len(values) for values in arrays}) != 1:
raise ValueError("Все массивы должны быть одинаковой длины.")
statistic, p_value = friedmanchisquare(*arrays)
return {"statistic": float(statistic), "p_value": float(p_value)}
def holm_bonferroni(p_values: Sequence[float]) -> list[float]:
"""Поправка Холма—Бонферрони для множественных сравнений."""
values = np.asarray(p_values, dtype=float)
n = len(values)
order = np.argsort(values)
adjusted = np.empty(n, dtype=float)
previous = 0.0
for rank, index in enumerate(order):
value = min(1.0, values[index] * (n - rank))
previous = max(previous, value)
adjusted[index] = previous
return adjusted.tolist()
def bootstrap_difference(
y_true: Sequence,
predictions_a: Sequence,
predictions_b: Sequence,
metric: str = "macro_f1",
labels: Sequence | None = None,
n_boot: int = 1000,
seed: int = 42,
) -> dict[str, float]:
"""Доверительный интервал разницы метрик двух моделей."""
y_true = np.asarray(y_true)
predictions_a = np.asarray(predictions_a)
predictions_b = np.asarray(predictions_b)
labels_array = np.asarray(labels) if labels is not None else np.unique(y_true)
def _metric(true_values: np.ndarray, predicted_values: np.ndarray) -> float:
if metric == "macro_f1":
return float(
f1_score(
true_values,
predicted_values,
labels=labels_array,
average="macro",
zero_division=0,
)
)
if metric == "accuracy":
return float(accuracy_score(true_values, predicted_values))
raise ValueError(f"Неизвестная метрика: {metric}")
rng = np.random.default_rng(seed)
differences = []
n_samples = len(y_true)
for _ in range(n_boot):
indices = rng.integers(0, n_samples, n_samples)
differences.append(
_metric(y_true[indices], predictions_a[indices])
- _metric(y_true[indices], predictions_b[indices])
)
values = np.asarray(differences)
return {
"mean_difference": float(values.mean()),
"lower": float(np.percentile(values, 2.5)),
"upper": float(np.percentile(values, 97.5)),
"n_boot": int(n_boot),
}
def cochran_q(correctness_matrix: np.ndarray) -> dict[str, float]:
"""Критерий Кокрана для сравнения нескольких бинарных классификаторов.
``correctness_matrix`` имеет форму (объекты, модели), значения 0/1.
"""
matrix = np.asarray(correctness_matrix, dtype=float)
if matrix.ndim != 2:
raise ValueError("Ожидается матрица (объекты, модели).")
k = matrix.shape[1]
column_sums = matrix.sum(axis=0)
row_sums = matrix.sum(axis=1)
total = matrix.sum()
numerator = (k - 1) * (k * np.sum(column_sums**2) - total**2)
denominator = k * total - np.sum(row_sums**2)
statistic = 0.0 if denominator == 0 else float(numerator / denominator)
from scipy.stats import chi2
p_value = float(chi2.sf(statistic, df=k - 1))
return {"statistic": statistic, "p_value": p_value, "df": k - 1}