Сравнение методов обнаружения выбросов

Сопоставление методов обнаружения выбросов

Использование статистики битвы из сезона 2023 года в Мэйджор Лиг Бейзбола

Шохеи Отани, фото Эрик Дрост на Фликре, CC BY 2.0

Обнаружение выбросов – это задача обучения без учителя для выявления аномалий (необычных наблюдений) в заданном наборе данных. Эта задача полезна во многих случаях реального мира, когда наш набор данных уже «заражен» аномалиями. Scikit-learn реализует несколько алгоритмов обнаружения выбросов, и в случаях, когда у нас есть неиспорченная базовая линия, мы также можем использовать эти алгоритмы для обнаружения новизны, полусупервизионная задача, предсказывающая, являются ли новые наблюдения выбросами.

Обзор

Четыре алгоритма обнаружения выбросов, которые мы сравним, это:

  • Эллиптическая оболочка подходит для нормально распределенных данных с низкой размерностью. Как следует из названия, она использует многомерное нормальное распределение для создания меры расстояния, разделяющей выбросы от внутренних значений.
  • Локальный фактор выброса сравнивает локальную плотность наблюдения с плотностью его соседей. Наблюдения с намного более низкой плотностью, чем у их соседей, считаются выбросами.
  • Одноклассовая метод опорных векторов (SVM) с стохастическим градиентным спуском (SGD) является приближенным решением O(n) One-Class SVM. Обратите внимание, что O(n²) One-Class SVM хорошо работает на нашем небольшом примере набора данных, но может быть непрактичным в вашем конкретном случае использования.
  • Изолирующий лес является подходом на основе деревьев, где выбросы быстрее изолированы случайными разбиениями, чем внутренние значения.

Поскольку наша задача неконтролируемая, у нас нет основы сравнения точности этих алгоритмов. Вместо этого мы хотим увидеть, как их результаты (особенно рейтинги игроков) отличаются друг от друга и получить некоторое интуитивное представление о их поведении и ограничениях, чтобы мы могли знать, когда предпочтительнее одно перед другим.

Давайте сравним несколько из этих методов, используя две метрики успеваемости битов сезона Мэйджор Лиг Бейзбола (MLB) 2023 года:

  • Процент достижения базы (OBP), скорость, с которой битер достигает базы (ударяя, ходя или получая попадание)
  • Слаг (SLG), среднее количество общего числа баз в проходе

Существует множество более сложных метрик успеваемости битеров, включая ОBP плюс SLG (OPS), взвешенное среднее достижение базы (wOBA) и скорректированный весовой рейтинг созданных бегов (WRC+). Однако мы увидим, что в дополнение к тому, что они широко используются и легко понятны, ОBP и SLG умеренно коррелированы и примерно нормально распределены, что делает их хорошо подходящими для этого сравнения.

Подготовка набора данных

Мы используем пакет pybaseball для получения данных о битве. Этот пакет Python имеет лицензию MIT и возвращает данные из Fangraphs.com, Baseball-Reference.com и других источников, которые в свою очередь получили официальные записи из Мэйджор Лиг Бейзбола.

Мы используем статистику 2023 года по ударам в бейсболе от pybaseball, которую можно получить, используя функции batting_stats (FanGraphs) или batting_stats_bref (Baseball Reference). Оказывается, имена игроков отформатированы более правильно в FanGraphs, но команды и лиги игроков от Baseball Reference лучше форматированы в случае с трейдами. Для улучшения читаемости нам на самом деле нужно объединить три таблицы: FanGraphs, Baseball Reference и таблицу для поиска ключей.

from pybaseball import (cache, batting_stats_bref, batting_stats,                         playerid_reverse_lookup)import pandas as pdcache.enable()  # избегаем лишних запросов при повторном запускеМИНИМАЛЬНОЕ_КОЛИЧЕСТВО_ПОЯВЛЕНИЙ_НА_ПЛИТЕ = 200# для повышения читаемости и установки разумного значения сортировкиdf_bref = batting_stats_bref(2023).query(f"PA >= {МИНИМАЛЬНОЕ_КОЛИЧЕСТВО_ПОЯВЛЕНИЙ_НА_ПЛИТЕ}"                                        ).rename(columns={"Lev":"League",                                                          "Tm":"Team"}                                                )df_bref["League"] = \  df_bref["League"].str.replace("Maj-","").replace("AL,NL","NL/AL"                                                  ).astype('category')df_fg = batting_stats(2023, qual=МИНИМАЛЬНОЕ_КОЛИЧЕСТВО_ПОЯВЛЕНИЙ_НА_ПЛИТЕ)key_mapping = \  playerid_reverse_lookup(df_bref["mlbID"].to_list(), key_type='mlbam'                         )[["key_mlbam","key_fangraphs"]                          ].rename(columns={"key_mlbam":"mlbID",                                            "key_fangraphs":"IDfg"}                                  )df = df_fg.drop(columns="Team"               ).merge(key_mapping, how="inner", on="IDfg"                      ).merge(df_bref[["mlbID","League","Team"]],                              how="inner", on="mlbID"                             ).sort_values(["League","Team","Name"])

Исследование данных

Сначала заметим, что эти метрики отличаются по среднему и дисперсии и умеренно коррелируют друг с другом. Также мы замечаем, что каждая метрика является достаточно симметричной, с медианой, близкой к среднему значению.

print(df[["OBP","SLG"]].describe().round(3))print(f"\nКорреляция: {df[['OBP','SLG']].corr()['SLG']['OBP']:.3f}")

           OBP      SLGcount  362.000  362.000mean     0.320    0.415std      0.034    0.068min      0.234    0.22725%      0.300    0.36750%      0.318    0.41475%      0.340    0.460max      0.416    0.654Корреляция: 0.630

Давайте визуализируем эту совместную дистрибуцию с помощью:

  • Диаграммы рассеяния игроков, окрашенных по Национальной лиге (NL) и Американской лиге (AL)
  • Бивариантного графика оценки плотности ядра (KDE) игроков, который сглаживает диаграмму рассеяния с помощью гауссовой функции для оценки плотности
  • Маргинальных графиков KDE для каждой метрики
import matplotlib.pyplot as pltimport seaborn as snsg = sns.JointGrid(data=df, x="OBP", y="SLG", height=5)g = g.plot_joint(func=sns.scatterplot, data=df, hue="League",                 palette={"AL":"blue","NL":"maroon","NL/AL":"green"},                 alpha=0.6                )g.fig.suptitle("Процент удержания на плате относительно процента успешных ударов\nСезон 2023 года, минимум "               f"{МИНИМАЛЬНОЕ_КОЛИЧЕСТВО_ПОЯВЛЕНИЙ_НА_ПЛИТЕ} появлений на плате"              )g.figure.subplots_adjust(top=0.9)sns.kdeplot(x=df["OBP"], color="orange", ax=g.ax_marg_x, alpha=0.5)sns.kdeplot(y=df["SLG"], color="orange", ax=g.ax_marg_y, alpha=0.5)sns.kdeplot(data=df, x="OBP", y="SLG",            ax=g.ax_joint, color="orange", alpha=0.5           )df_extremes = df[ df["OBP"].isin([df["OBP"].min(),df["OBP"].max()])                  | df["OPS"].isin([df["OPS"].min(),df["OPS"].max()])                ]for _,row in df_extremes.iterrows():    g.ax_joint.annotate(row["Name"], (row["OBP"], row["SLG"]),size=6,                      xycoords='data', xytext=(-3, 0),                        textcoords='offset points', ha="right",                      alpha=0.7)plt.show()

В верхнем правом углу диаграммы рассеяния показано скопление превосходства в попадании, соответствующее тяжелым верхним хвостам распределений SLG и OBP. Эта небольшая группа отличается в возможности попадания на базу и нанесении дополнительных ударов по мячу. Как мы считаем их выбросами (из-за их отдаленности от большинства популяции игроков) по отношению к внутреним точкам (из-за их близости друг к другу) зависит от определения, применяемого нашим выбранным алгоритмом.

Применение алгоритмов обнаружения выбросов

Обычно, алгоритмы обнаружения выбросов в библиотеке scikit-learn имеют методы fit() и predict(), но есть и исключения, а также различия между алгоритмами в их аргументах. Мы рассмотрим каждый алгоритм индивидуально, но мы подгоним каждый к матрице признаков (n=2) для каждого игрока (m=453). Затем мы оценим не только каждого игрока, но и сетку значений, охватывающую диапазон каждого признака, чтобы помочь нам визуализировать предсказательную функцию.

Для визуализации границ решений необходимо выполнить следующие действия:

  1. Создать 2D-сетку значений функций ввода meshgrid.
  2. Применить decision_function к каждой точке на meshgrid, что требует разбора сетки.
  3. Вернуть форму предсказаний обратно в сетку.
  4. Построить предсказания.

Мы будем использовать сетку размером 200×200 для охвата существующих наблюдений плюс некоторого запаса, но вы можете настроить сетку в соответствии с вашей желаемой скоростью и разрешением.

import numpy as npX = df[["OBP","SLG"]].to_numpy()GRID_RESOLUTION = 200disp_x_range, disp_y_range = ( (.6*X[:,i].min(), 1.2*X[:,i].max())                                for i in [0,1]                             )xx, yy = np.meshgrid(np.linspace(*disp_x_range, GRID_RESOLUTION),                      np.linspace(*disp_y_range, GRID_RESOLUTION)                    )grid_shape = xx.shapegrid_unstacked = np.c_[xx.ravel(), yy.ravel()]

Эллиптическая оболочка

Форма эллиптической оболочки определяется ковариационной матрицей данных, которая задает дисперсию признака i на главной диагонали [i,i] и ковариацию признаков i и j в позициях [i,j]. Поскольку ковариационная матрица чувствительна к выбросам, этот алгоритм использует оценку линейной комбинации наименьших ковариаций (MCD), которая рекомендуется для унимодальных и симметричных распределений, с перемешиванием, определенным входным аргументом random_state для воспроизводимости. Эта устойчивая ковариационная матрица снова пригодится нам позднее.

Поскольку мы хотим сравнить оценки выбросов в их ранжировании, а не в бинарной классификации выброс/внутренние точки, мы используем decision_function для оценки игроков.

from sklearn.covariance import EllipticEnvelopeell = EllipticEnvelope(random_state=17).fit(X)df["outlier_score_ell"] = ell.decision_function(X)Z_ell = ell.decision_function(grid_unstacked).reshape(grid_shape)

Локальный фактор выброса

Этот подход к измерению изоляции основан на k-ближайших соседях (KNN). Мы вычисляем общую дистанцию от каждого наблюдения до его ближайших соседей для определения локальной плотности, а затем сравниваем локальную плотность каждого наблюдения с плотностью его соседей. Наблюдения с низкой локальной плотностью по сравнению с плотностью их соседей считаются выбросами.

Выбор числа соседей для включения: В KNN, правило хорошего тона состоит в том, чтобы выбрать K = sqrt(N), где N – количество наблюдений. Исходя из этого правила, мы получаем K, близкое к 20 (которое, кстати, является значением K по умолчанию для LOF). Вы можете увеличить или уменьшить K, чтобы уменьшить переобучение или недообучение соответственно.

K = int(np.sqrt(X.shape[0]))print(f"Использование K={K} ближайших соседей.")

Использование K=19 ближайших соседей.

Выбор меры расстояния: Обратите внимание, что наши признаки коррелируют и имеют разные дисперсии, поэтому евклидово расстояние не очень содержательно. Мы воспользуемся расстоянием Махаланобиса, которое учитывает масштаб признаков и их корреляцию.

При расчете расстояния Махаланобиса мы будем использовать устойчивую оценку ковариации. Если бы мы не рассчитали ее уже с помощью эллиптической огибающей, мы могли бы рассчитать ее непосредственно.

from scipy.spatial.distance import pdist, squareform# Если у нас еще не было эллиптической огибающей,# мы могли бы рассчитать устойчивую оценку ковариации:#   from sklearn.covariance import MinCovDet#   robust_cov = MinCovDet().fit(X).covariance_# Но мы можем просто использовать ее из эллиптической огибающей:robust_cov = ell.covariance_print(f"Устойчивая оценка ковариации:\n{np.round(robust_cov,5)}\n")inv_robust_cov = np.linalg.inv(robust_cov)D_mahal = squareform(pdist(X, 'mahalanobis', VI=inv_robust_cov))print(f"Матрица расстояний Махаланобиса размером {D_mahal.shape}, "      f"например:\n{np.round(D_mahal[:5,:5],3)}...\n...\n")

Устойчивая оценка ковариации:[[0.00077 0.00095] [0.00095 0.00366]]Матрица расстояний Махаланобиса размером (362, 362), например:[[0.    2.86  1.278 0.964 0.331] [2.86  0.    2.63  2.245 2.813] [1.278 2.63  0.    0.561 0.956] [0.964 2.245 0.561 0.    0.723] [0.331 2.813 0.956 0.723 0.   ]]......

Подгонка метода локального выброса: Обратите внимание, что при использовании пользовательской матрицы расстояний мы должны передать metric="precomputed" конструктору, а затем саму матрицу расстояний методу fit. (Подробнее см. документацию для получения дополнительной информации.)

Также обратите внимание, что в отличие от других алгоритмов, при использовании LOF нас инструктируют не использовать метод score_samples для оценки существующих наблюдений; этот метод следует использовать только для обнаружения новизны.

from sklearn.neighbors import LocalOutlierFactorlof = LocalOutlierFactor(n_neighbors=K, metric="precomputed", novelty=True                        ).fit(D_mahal)df["outlier_score_lof"] = lof.negative_outlier_factor_

Создание границы решений: Поскольку мы использовали настраиваемую метрику расстояния, нам также необходимо вычислить это метрическое расстояние между каждой точкой в сетке и исходными наблюдениями. Ранее мы использовали пространственную меру pdist для парных расстояний между каждым элементом одного набора, но теперь мы используем cdist, чтобы вернуть расстояния от каждого элемента первого набора в каждый элемент второго набора.

from scipy.spatial.distance import cdistD_mahal_grid = cdist(XA=grid_unstacked, XB=X,                      metric='mahalanobis', VI=inv_robust_cov                    )Z_lof = lof.decision_function(D_mahal_grid).reshape(grid_shape)

Машина опорных векторов (SGD-One-Class SVM)

SVM используют трюк с ядерной функцией, чтобы преобразовать признаки в пространство более высокой размерности, где можно идентифицировать разделяющую гиперплоскость. Для ядра радиальной базисной функции (RBF) требуется стандартизация входных данных, но, как отмечено в документации по StandardScaler, этот масштабировщик чувствителен к выбросам, поэтому мы используем RobustScaler. Мы перенаправим масштабированные входные данные в аппроксимацию ядра Нистрёма, как предлагает документация по SGDOneClassSVM.

from sklearn.pipeline import make_pipelinefrom sklearn.preprocessing import RobustScalerfrom sklearn.kernel_approximation import Nystroemfrom sklearn.linear_model import SGDOneClassSVMsuv = make_pipeline(            RobustScaler(),            Nystroem(random_state=17),            SGDOneClassSVM(random_state=17)).fit(X)df["outlier_score_svm"] = suv.decision_function(X)Z_svm = suv.decision_function(grid_unstacked).reshape(grid_shape)

Isolation Forest

Этот подход на основе деревьев к измерению изоляции выполняет случайное рекурсивное разделение. Если среднее количество разбиений, необходимых для изоляции определенного наблюдения, низкое, то это наблюдение считается более кандидатом-выбросом. Как и Random Forests и другие модели на основе деревьев, Isolation Forest не предполагает, что признаки имеют нормальное распределение или требуют их масштабирования. По умолчанию строится 100 деревьев. В нашем примере используются только два признака, поэтому мы не включаем выборку признаков.

from sklearn.ensemble import IsolationForestiso = IsolationForest(random_state=17).fit(X)df["outlier_score_iso"] = iso.score_samples(X)Z_iso = iso.decision_function(grid_unstacked).reshape(grid_shape)

Результаты: изучение границ решений

Обратите внимание, что прогнозы этих моделей имеют различные распределения. Мы применяем QuantileTransformer, чтобы сделать их более визуально сопоставимыми на определенной сетке. Из документации следует:

Обратите внимание, что эта трансформация является нелинейной. Она может исказить линейные корреляции между переменными, измеряемыми в одной и той же шкале, но делает переменные, измеренные в разных шкалах, более прямо сравнимыми.

from adjustText import adjust_textfrom sklearn.preprocessing import QuantileTransformerN_QUANTILES = 8 # Это столько же цветных секций в диаграммеN_CALLOUTS=15  # На этой диаграмме обозначаются столько же всплывающих значений вверхуfig, axs = plt.subplots(2, 2, figsize=(12, 12), sharex=True, sharey=True)fig.suptitle("Сравнение алгоритмов идентификации выбросов",size=20)fig.supxlabel("On-Base Percentage (OBP)")fig.supylabel("Slugging (SLG)")ax_ell = axs[0,0]ax_lof = axs[0,1]ax_svm = axs[1,0]ax_iso = axs[1,1]model_abbrs = ["ell","iso","lof","svm"]qt = QuantileTransformer(n_quantiles=N_QUANTILES)for ax, nm, abbr, zz in zip( [ax_ell,ax_iso,ax_lof,ax_svm],                             ["Elliptic Envelope","Isolation Forest",                             "Local Outlier Factor","One-class SVM"],                             model_abbrs,                            [Z_ell,Z_iso,Z_lof,Z_svm]                           ):    ax.title.set_text(nm)    outlier_score_var_nm = f"outlier_score_{abbr}"        qt.fit(np.sort(zz.reshape(-1,1)))    zz_qtl = qt.transform(zz.reshape(-1,1)).reshape(zz.shape)    cs = ax.contourf(xx, yy, zz_qtl, cmap=plt.cm.OrRd.reversed(),                      levels=np.linspace(0,1,N_QUANTILES)                    )    ax.scatter(X[:, 0], X[:, 1], s=20, c="b", edgecolor="k", alpha=0.5)        df_callouts = df.sort_values(outlier_score_var_nm).head(N_CALLOUTS)    texts = [ ax.text(row["OBP"], row["SLG"], row["Name"], c="b",                      size=9, alpha=1.0)              for _,row in df_callouts.iterrows()            ]    adjust_text(texts,                 df_callouts["OBP"].values, df_callouts["SLG"].values,                 arrowprops=dict(arrowstyle='->', color="b", alpha=0.6),                 ax=ax               )plt.tight_layout(pad=2)plt.show()for var in ["OBP","SLG"]:    df[f"Pctl_{var}"] = 100*(df[var].rank()/df[var].size).round(3)model_score_vars = [f"outlier_score_{nm}" for nm in model_abbrs]  model_rank_vars = [f"Rank_{nm.upper()}" for nm in model_abbrs]df[model_rank_vars] = df[model_score_vars].rank(axis=0).astype(int)    # Усреднение рангов произвольно; нам только нужен обратный порядокdf["Rank_avg"] = df[model_rank_vars].mean(axis=1)print("Обратный отсчет до наибольшего выброса...\n")print(    df.sort_values("Rank_avg",ascending=False                  ).tail(N_CALLOUTS)[["Name","AB","PA","H","2B","3B",                                      "HR","BB","HBP","SO","OBP",                                      "Pctl_OBP","SLG","Pctl_SLG"                                     ] +                              [f"Rank_{nm.upper()}" for nm in model_abbrs]                            ].to_string(index=False))

Обратный отсчет до самого яркого представителя...            Имя  AB  PA   H  2B  3B  HR  BB  HBP  SO   OBP  Pctl_OBP   SLG  Pctl_SLG  Rank_ELL  Rank_ISO  Rank_LOF  Rank_SVM   Austin Barnes 178 200  32   5   0   2  17    2  43 0.256       2.6 0.242       0.6        19         7        25        12   J.D. Martinez 432 479 117  27   2  33  34    2 149 0.321      52.8 0.572      98.1        15        18         5        15      Yandy Diaz 525 600 173  35   0  22  65    8  94 0.410      99.2 0.522      95.4        13        15        13        10       Jose Siri 338 364  75  13   2  25  20    2 130 0.267       5.5 0.494      88.4         8        14        15        13       Juan Soto 568 708 156  32   1  35 132    2 129 0.410      99.2 0.519      95.0        12        13        11        11    Mookie Betts 584 693 179  40   1  39  96    8 107 0.408      98.6 0.579      98.3         7        10        20         7   Rob Refsnyder 202 243  50   9   1   1  33    5  47 0.365      90.5 0.317       6.6         5        19         2        14  Yordan Alvarez 410 496 120  24   1  31  69   13  92 0.407      98.3 0.583      98.6         6         9        18         6 Freddie Freeman 637 730 211  59   2  29  72   16 121 0.410      99.2 0.567      97.8         9        11         9         8      Matt Olson 608 720 172  27   3  54 104    4 167 0.389      96.5 0.604      99.2        11         6         7         9   Austin Hedges 185 212  34   5   0   1  11    2  47 0.234       0.3 0.227       0.3        10         1         4         3     Aaron Judge 367 458  98  16   0  37  88    0 130 0.406      98.1 0.613      99.4         3         5         6         4Ronald Acuna Jr. 643 735 217  35   4  41  80    9  84 0.416     100.0 0.596      98.9         2         3        10         2    Corey Seager 477 536 156  42   0  33  49    4  88 0.390      97.0 0.623      99.7         4         4         3         5   Shohei Ohtani 497 599 151  26   8  44  91    3 143 0.412      99.7 0.654     100.0         1         2         1         1

Анализ и выводы

Похоже, что четыре реализации в основном согласны в определении выбросов, но существуют заметные различия в оценках и также в простоте использования.

Эллиптическая Оболочка имеет более узкие контуры вокруг малой оси эллипса, поэтому она склонна выделять интересных игроков, которые идут вразрез общей корреляции между характеристиками. Например, вне поля игрок Рэйс Хосе Сири рассматривается как выброс по этому алгоритму из-за его высокого SLG (88-й процентиль) по сравнению с низким OBP (5-й процентиль), что соответствует агрессивному бэтеру, который сильно бьет по краевым мячам, и либо отправляет их в даль, либо не достигает контакта.

Эллиптическая Оболочка также проста в использовании без дополнительной настройки и обеспечивает робастную ковариационную матрицу. Если у вас низкоразмерные данные и разумные ожидания, что они будут нормально распределены (что часто не является случаем), вы можете попробовать этот простой подход в первую очередь.

One-class SVM имеет более равномерно распределенные контуры, поэтому он склонен подчеркивать наблюдения вдоль общего направления корреляции больше, чем Эллиптическая Оболочка. Звездные первые бейсмены Фредди Фримен (Доджерс) и Янди Диаз (Рэйс) более сильно оцениваются по этому алгоритму, чем по другим, так как их SLG и OBP оба отличные (99-й и 97-й процентиль для Фримена, 99-й и 95-й для Диаза).

Для ядра RBF требовался дополнительный этап стандартизации, но оно, кажется, хорошо работает на этом простом примере без заточки.

Local Outlier Factor обнаружил упомянутый ранее “кластер превосходства” с помощью небольшого бимодального контура (едва заметного на графике). Поскольку игроки Доджерс Мукки Беттс и Рональд Акуна-младший окружены другими отличными бэтерами, включая Фримена и Йордана Альвареса, он оценивается только как 20-й наиболее сильный выброс по LOF, в отличие от других алгоритмов, где оценка была 10-ой или даже выше. Напротив, игрок Рейс Марселл Озуна имеет несколько более низкий SLG и значительно более низкий OBP, чем Беттс, но по LOF он является более явным выбросом из-за меньшей плотности его окружения.

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

Isolation Forest склонен подчеркивать наблюдения в углах пространства характеристик, так как разделения распределены по характеристикам. Запасной ловец Остин Хеджес, который играл за Пайретс и Рэйнджерс в 2023 году и подписал контракт с Гардианс на 2024 год, является сильным игроком в обороне, но является самым слабым бэтером (с не менее чем 200 появлениями на плате) по обоим характеристикам SLG и OBP. Хеджес может быть особо выделен в единственном разделении либо по OBP, либо по OPS, что делает его наиболее явным выбросом. Isolation Forest – единственный алгоритм, который не оценил Шохея Отани как самого явного выброса: поскольку Отани уступил Акуна-младшему в OBP, и тот и другой могут быть выделены в единственном разделении только по одному из признаков.

Как и с обычными обучаемыми деревьями, Isolation Forest не экстраполирует, поэтому лучше всего подходит для подгонки к зараженному набору данных для обнаружения выбросов, чем для подгонки к набору данных без аномалий для обнаружения новшеств (где он не будет присваивать более сильных оценок новым выбросам, чем уже существующим наблюдениям).

Хотя Isolation Forest хорошо сработал сразу из коробки, его невозможность оценить Шохея Отани как самого большого выброса в бейсболе (и, вероятно, во всех профессиональных спортах) показывает основное ограничение любого детектора выбросов: данные, которые вы используете для его подгонки.

Мы не только исключили статистику по обороне (извините, Остин Хеджес), мы также не стали включать статистику по питчингу. Потому что металлом больше не стреляют… за исключением Отани, чей сезон включал второй по лучшему батту на матч (BAA) и 11-е место по заработанным очкам (ERA) в бейсболе (минимально 100 иннингов), комплит-гейм шатдаун и матч, в котором он сделал 10 забить и выполнил два хоум-рана.

Была высказана предположения, что Шохей Отани – продвинутый инопланетянин, притворяющийся человеком, но кажется, что более вероятно, что есть два продвинутых инопланетянина, притворяющихся одним и тем же человеком. К сожалению, один из них только что перенес операцию на локте и не будет бросать в 2024 году… но другой только что заключил рекордный контракт на 10 лет на сумму 700 миллионов долларов. И благодаря обнаружению выбросов, теперь мы можем понять почему!