Для запуска А/В теста необходимым минимумом является фиксация ошибок первого и второго рода, расчет MDE (минимальный наблюдаемый эффект). Однако при расчете результатов теста далеко не всегда получается достичь MDE заданного размера, в таком случае вероятность достижения значимости значительно уменьшается. Помимо этого даже при статистически значимом результате существует вероятность ошибки, что наши результаты являются выбросом или просто случайностью. В таких случаях необходимо применить дополнительный арсенал инструментов для работы с данными.

В этой статье я фокусируюсь на error Type-M/Type-S. Но для начала давайте вспомним, что из себя представляют статистические ошибки первого и второго рода.

Рассматривая классическое тестирование гипотез (подсчет результатов А/В теста): есть нулеваяH_0и альтернативнаяH_1.

  1. Ошибка первого рода (Type I): отклоняемH_0, когда на самом делеH_0 верна.
    Например: аналитик ложно замечает разницу описательных статистиках выборок.

    Вероятностью наступления такого события называется –\alphaуровень значимости:

    \alpha = P(reject\ H_0\  |\  true \ H_0)
  2. Ошибка второго рода (Type II): не отклоняемH_0, когда на самом деле вернаH_1.

    \beta = P(do \ not\ reject\ H_0\ |\ H_1\ true)

    Тогда мощностью теста является:

    1-\beta=P(reject\ H_0\ |\ H_1\ true)

    Чем выше мощность, тем меньше риск пропустить реальный эффект.

Для ML/DS специалистов эти типы ошибок (в рамках бинарной классификации) обозначаются:

  • Type I – False Positive (FP) (ложно положительный результат, ложная тревога)

  • Type II – False Negative (FN) (ложно отрицательный результат, пропустили реальный эффект)

Когда этого недостаточно

Если мощность низкая, то среди «значимых» результатов:

  • Знак эффекта может быть неверным (мы «ловим» эффект не в ту сторону).

  • Оценённая величина эффекта условно на значимости часто сильно завышена.

Gelman и Carlin предложили смотреть не только на мощность и ошибки I/II рода, но и на ошибки типа S (Sign) и типа M (Magnitude), которые описывают, насколько часто значимый эффект имеет неверный знак и насколько условно значимые оценки систематически завышают истинную величину эффекта [1]. В контексте дизайна экспериментов их подход показывает, что при низкой мощности классический фокус на p‑value приводит к «экстремальным» значимым результатам, и поэтому уже при планировании теста нужно оценивать не только вероятность обнаружить эффект, но и риск перепутать его направление и масштаб.

Давайте представим что мы тестируем гипотезу о равенстве истинного эффекта нулю против альтернативной гипотезы с наличием реального эффекта, в двустороннем тесте. Это утверждение можно записать следующим образом:

H_0:\mu=0 \ \ vs.\ \ H_1:\mu\neq0

Так же предполагается, что тестовая статистика имеет нормальное распределение. При обработке результатов A/B теста обычно берется разница между тестовой и контрольной группой. Для сравнения качества ML моделей берется разница метрик. Так же можно рассматривать данные ошибки в контексте весов моделей регрессии.

Для определения новых понятий понадобятся следующие обозначения: пусть\theta– истинный эффект,\hat{\theta}– оценка эффекта, а также есть правило определения значимости (например можно взять|Z|>z^{1-\alpha/2})

  • Type S error (Sign): вероятность того, что при статистически значимом результате знак оценённого эффекта неверен.

    Type\ S=P(sign(\hat{\theta})\neq sign(\theta)\ |\  declared\ significant)
  • Type M error (Magnitude): ожидаемый коэффициент завышения (или занижения) размера эффекта (exaggeration ratio), другими словами насколько условно значимый эффект завышает истинный по модулю.

    Type\ M=\frac{E(|\hat\theta|\ |\ declared\ significant)}{|\theta|}

Моделирование рассчета

В качестве примера я предлагаю рассмотреть кейс сравнения двух ML моделей, однако все вычисления можно перенести на подсчет эффекта в рамках A/B теста.

Подготовка данных и обучение моделей
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.datasets import load_breast_cancer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import confusion_matrix, accuracy_score
from scipy.stats import norm

# Загрузка данных
data_bc = load_breast_cancer(as_frame=True)
X = data_bc.data
y = data_bc.target

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.3, random_state=42, stratify=y
)

# Обучение моделей с разными параметрами
# Модель А
model_A = LogisticRegression(max_iter=1000, random_state=33)
model_A.fit(X_train, y_train)

# Модель В
model_B = LogisticRegression(C=0.1, max_iter=1000, random_state=33)
model_B.fit(X_train, y_train)

proba_A = model_A.predict_proba(X_test)[:, 1]
proba_B = model_B.predict_proba(X_test)[:, 1]

# Функция для рассчета ошибок
def error_table(threshold, y_true, proba):
    y_pred = (proba >= threshold).astype(int)
    tn, fp, fn, tp = confusion_matrix(y_true, y_pred).ravel()
    
    fpr = fp / (fp + tn)  # эмпирическая alpha
    fnr = fn / (fn + tp)  # эмпирическая beta
    
    return pd.Series(
        {
            "threshold": threshold,
            "TN": tn,
            "FP (Type I)": fp,
            "FN (Type II)": fn,
            "TP": tp,
            "[alpha] FPR = FP / (FP+TN)": fpr,
            "[beta] FNR = FN / (FN+TP)": fnr
        }
    )

В данной статье, для простоты и наглядности я не буду оптимизировать границу отсечения классов по скору.

Вычисление ошибок
threshold = 0.5
pred_A = (proba_A >= threshold).astype(int)
pred_B = (proba_B >= threshold).astype(int)
errors_A = error_table(threshold, y_test, proba_A)
errors_B = error_table(threshold, y_test, proba_B)

print("Ошибки I/II рода для модели A:\n", errors_A)
print("\nОшибки I/II рода для модели B:\n", errors_B)

acc_A = accuracy_score(y_test, pred_A)
acc_B = accuracy_score(y_test, pred_B)
effect_obs = acc_B - acc_A

print("\nНаблюдаемый эффект (accuracy_B - accuracy_A):", effect_obs)

Ошибки I/II рода для модели A:
threshold 0.5
TN 57
FP (Type I) 7
FN (Type II) 2
TP 105
[alpha] FPR = FP / (FP+TN) 0.109375
[beta] FNR = FN / (FN+TP) 0.018692
dtype: float64

Ошибки I/II рода для модели B:
threshold 0.5
TN 59
FP (Type I) 5
FN (Type II) 2
TP 105
[alpha] FPR = FP / (FP+TN) 0.078125
[beta] FNR = FN / (FN+TP) 0.018692
dtype: float64

Наблюдаемый эффект (accuracy_B - accuracy_A): 0.011695906432748537

Бутстрап эффекта и SE (стандартная ошибка)
rng = np.random.default_rng(0)
n = len(X_test) // 2
B = 100000
threshold = 0.5 

pA = model_A.predict_proba(X_test)[:, 1]
pB = model_B.predict_proba(X_test)[:, 1]
predA = (pA >= threshold).astype(int)
predB = (pB >= threshold).astype(int)
y = y_test.to_numpy()
corrA = (predA == y).astype(int)
corrB = (predB == y).astype(int)

idx = rng.integers(0, n, size=(B, n))
boot_effects = corrB[idx].mean(1) - corrA[idx].mean(1)

true_effect_hat = boot_effects.mean()
se_hat = boot_effects.std(ddof=1)

print("\nОценка эффекта по бутстрэпу:", true_effect_hat)
print("Оценка SE эффекта по бутстрэпу:", se_hat)

Оценка эффекта по бутстрэпу: 0.023563647058823543
Оценка SE эффекта по бутстрэпу: 0.01638026036908619

Вычисление ошибок type-S/M
def simulate_type_s_m(true_effect, se, alpha=0.05, n_sims=100000, random_state=13):
    rng = np.random.default_rng(random_state)
    sims = rng.normal(loc=true_effect, scale=se, size=n_sims)

    z = sims / se
    p_values = 2 * (1 - norm.cdf(np.abs(z)))
    significant = p_values < alpha

    sims_sig = sims[significant]
    sign_hat = np.sign(sims_sig)
    true_sign = np.sign(true_effect)
    type_s = np.mean(sign_hat != true_sign)

    exaggeration_ratio = np.abs(sims_sig) / abs(true_effect)
    type_m = exaggeration_ratio.mean()

    power = significant.mean()
    return sims, significant, type_s, type_m, power

sims, significant, type_s, type_m, power = simulate_type_s_m(true_effect_hat, se_hat)

print("\nОценки Type S/M и мощности:")
print("power =", round(power, 3), ", Type S =", round(type_s, 5), ", Type M =", round(type_m, 3))

Оценки Type S/M и мощности: power = 0.302 , Type S = 0.00109 , Type M = 1.803

Скрытый текст
plt.style.use("seaborn-v0_8-whitegrid")

# Распределения скоров для моделей A и B
plt.figure(figsize=(8, 4))
plt.hist(proba_A, bins=30, alpha=0.6, label="Model A", density=True)
plt.hist(proba_B, bins=30, alpha=0.6, label="Model B", density=True)
plt.axvline(threshold, color="red", linestyle="--", label=f"threshold = {threshold}")
plt.title("Распределение предсказанных вероятностей (модели A и B)")
plt.xlabel("Предсказанная вероятность положительного класса")
plt.ylabel("Плотность")
plt.legend()
plt.tight_layout()

# Бутстрэп-распределение эффекта (accuracy_B - accuracy_A)
plt.figure(figsize=(8, 4))
plt.hist(boot_effects, bins=30, alpha=0.7, density=True, color="steelblue")
plt.axvline(true_effect_hat, color="black", linestyle="-",
            label=f"mean effect = {true_effect_hat:.4f}")
plt.axvline(0, color="red", linestyle="--", label="нулевой эффект (0)")
plt.title("Бутстрэп-распределение эффекта (accuracy_B - accuracy_A)")
plt.xlabel("Размер эффекта (uplift по accuracy)")
plt.ylabel("Плотность")
plt.legend()
plt.tight_layout()

# Распределение симулированных оценок эффекта и выделение значимых
plt.figure(figsize=(8, 4))
plt.hist(sims, bins=40, alpha=0.4, density=True, label="все симулированные эффекты")
plt.hist(sims[significant], bins=40, alpha=0.7, density=True, label="значимые эффекты")
plt.axvline(true_effect_hat, color="black", linestyle="-", label="true_effect_hat")
plt.axvline(0, color="red", linestyle="--", label="нулевой эффект (0)")
plt.title(
    f"Симулированные оценки эффекта (Type S/M)\n"
    f"power={power:.2f}, Type S={type_s:.3f}, Type M={type_m:.2f}"
)
plt.xlabel("Размер эффекта (симуляции)")
plt.ylabel("Плотность")
plt.legend()
plt.tight_layout()

plt.show()

Выводы

Наблюдаемый uplift accuracy_B − accuracy_A ≈ 0.0117, а бутстрэп‑оценка эффекта ≈ 0.0236 с SE ≈ 0.0164. Это означает, что реальный прирост по accuracy небольшой, но заметный. Для бизнеса это означает небольшой прирост в качестве (значит экономия), за счет снижения FP.

  • Мощность ≈ 0.30: при «истинном» приросте качества такого размера, примерно в 30% можно наблюдать статистически значимое отличие, в остальных случаях эффект не был бы обнаружен.

  • Type S ≈ 0.001: на множестве всех, стат. значимых, результатов вероятность перепутать направление эффекта практически нулевая. Направление (знак) эффекта хорошо определено.

  • Type M ≈ 1.8: на множестве всех стат. значимых экспериментов оценённый uplift в среднем почти в 1.8 раза больше истинного. Это означает, что прирост в качестве на стат. значимых результатах систематически завышается.


Источники

  1. https://library.virginia.edu/data/articles/assessing-type-s-and-type-m-errors