Для запуска А/В теста необходимым минимумом является фиксация ошибок первого и второго рода, расчет MDE (минимальный наблюдаемый эффект). Однако при расчете результатов теста далеко не всегда получается достичь MDE заданного размера, в таком случае вероятность достижения значимости значительно уменьшается. Помимо этого даже при статистически значимом результате существует вероятность ошибки, что наши результаты являются выбросом или просто случайностью. В таких случаях необходимо применить дополнительный арсенал инструментов для работы с данными.
В этой статье я фокусируюсь на error Type-M/Type-S. Но для начала давайте вспомним, что из себя представляют статистические ошибки первого и второго рода.
Рассматривая классическое тестирование гипотез (подсчет результатов А/В теста): есть нулеваяи альтернативная
.
Ошибка первого рода (Type I): отклоняем
, когда на самом деле
верна.
Например: аналитик ложно замечает разницу описательных статистиках выборок.
Вероятностью наступления такого события называется –уровень значимости:
Ошибка второго рода (Type II): не отклоняем
, когда на самом деле верна
.
Тогда мощностью теста является:
Чем выше мощность, тем меньше риск пропустить реальный эффект.
Для ML/DS специалистов эти типы ошибок (в рамках бинарной классификации) обозначаются:
Type I – False Positive (FP) (ложно положительный результат, ложная тревога)
Type II – False Negative (FN) (ложно отрицательный результат, пропустили реальный эффект)
Когда этого недостаточно
Если мощность низкая, то среди «значимых» результатов:
Знак эффекта может быть неверным (мы «ловим» эффект не в ту сторону).
Оценённая величина эффекта условно на значимости часто сильно завышена.
Gelman и Carlin предложили смотреть не только на мощность и ошибки I/II рода, но и на ошибки типа S (Sign) и типа M (Magnitude), которые описывают, насколько часто значимый эффект имеет неверный знак и насколько условно значимые оценки систематически завышают истинную величину эффекта [1]. В контексте дизайна экспериментов их подход показывает, что при низкой мощности классический фокус на p‑value приводит к «экстремальным» значимым результатам, и поэтому уже при планировании теста нужно оценивать не только вероятность обнаружить эффект, но и риск перепутать его направление и масштаб.
Давайте представим что мы тестируем гипотезу о равенстве истинного эффекта нулю против альтернативной гипотезы с наличием реального эффекта, в двустороннем тесте. Это утверждение можно записать следующим образом:
Так же предполагается, что тестовая статистика имеет нормальное распределение. При обработке результатов A/B теста обычно берется разница между тестовой и контрольной группой. Для сравнения качества ML моделей берется разница метрик. Так же можно рассматривать данные ошибки в контексте весов моделей регрессии.
Для определения новых понятий понадобятся следующие обозначения: пусть– истинный эффект,
– оценка эффекта, а также есть правило определения значимости (например можно взять
)
Type S error (Sign): вероятность того, что при статистически значимом результате знак оценённого эффекта неверен.
Type M error (Magnitude): ожидаемый коэффициент завышения (или занижения) размера эффекта (exaggeration ratio), другими словами насколько условно значимый эффект завышает истинный по модулю.
Моделирование рассчета
В качестве примера я предлагаю рассмотреть кейс сравнения двух 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 раза больше истинного. Это означает, что прирост в качестве на стат. значимых результатах систематически завышается.
