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