データ分析や機械学習における外れ値の扱い方

データ分析や機械学習の分野において、「外れ値」の存在は避けて通れない課題の一つです。外れ値とは、データセットの中で他の大多数のデータポイントから大きくかけ離れた値のことを指します。これらは測定誤差、入力ミス、あるいは特異な事象など、様々な原因で発生し得ます。

一見些細な存在に思える外れ値ですが、分析結果やモデルの性能に深刻な悪影響を及ぼす可能性があります。例えば、平均値のような統計量は外れ値に極めて敏感であり、たった一つの極端な値によってデータ全体の代表値が大きく歪められてしまうことがあります。また、データの分散を不当に増大させ、統計的な検定の結果を誤らせたり、機械学習モデルが外れ値に過剰に適合してしまい、未知のデータに対する予測精度(汎化性能)を低下させたりする原因ともなります。

このような問題を防ぎ、より信頼性の高い分析や精度の高いモデル構築を行うためには、外れ値に適切に対処することが不可欠です。外れ値処理の目的は、単に極端な値を取り除くことではなく、データの特性を正しく捉え、分析のロバスト性(頑健性)を高め、モデルの性能を向上させることにあります。本記事では、数ある外れ値処理手法の中でも、特に「ウィンザー化(Winsorization)」と呼ばれるテクニックに焦点を当て、その概念から具体的な実装方法、さらに実践における注意点を解説していきます。

ウィンザー化とは何か?

ウィンザー化(Winsorization)は、データセット内の外れ値を処理するための統計的な手法の一つです。この手法の最大の特徴は、外れ値を単純に「削除」するのではなく、あらかじめ設定した閾値に基づいて「置き換える」という点にあります。これにより、外れ値の極端な影響を抑制しつつ、データのサンプルサイズを維持できるというメリットがあります。

基本的な定義と「置き換え」の考え方

ウィンザー化では、データの分布において、下限と上限のパーセンタイル点を特定し、それらの点よりも外側にある値を、それぞれ最も近いパーセンタイル点(閾値)の値で置き換えます。例えば、データの下位5%と上位5%の値をウィンザー化する場合、下位5%に位置する全ての値は5パーセンタイル点の値に、上位5%に位置する全ての値は95パーセンタイル点の値に置き換えられます。

この操作は、極端に小さい値をある程度「引き上げ(フロアリング)」、極端に大きい値をある程度「引き下げる(キャッピング)」と表現できます。これにより、データの中心的な傾向を歪めることなく、外れ値の影響を緩和することができます。

ウィンザー化の具体的な仕組みと閾値の決定方法

ウィンザー化を適用する際には、どの程度の値を外れ値とみなし、どの値で置き換えるかという「閾値」を決定する必要があります。この閾値の決定方法には、主に以下のものが挙げられます。

  1. パーセンタイル法 (Percentile method)
    • 最も一般的で直感的な方法です。データの分布の両端から特定のパーセンテージ(例えば、1%、5%、10%など)を閾値として設定します。例えば、両端5%をウィンザー化する場合、下位5パーセンタイル値より小さい値は全て5パーセンタイル値に、上位95パーセンタイル値より大きい値は全て95パーセンタイル値に置き換えられます。どのパーセンテージを選択するかは、データの特性や分析の目的に応じて決定されます。
  2. ガウス分布(正規分布)に基づく方法 (Gaussian method)
    • データが正規分布に従うと仮定できる場合に用いられます。平均値から標準偏差の何倍離れているか(例:平均 ± 3σ)を閾値とします。この範囲外の値が置き換えの対象となります。ただし、データが正規分布に従わない場合には適切でない可能性があります。
  3. 四分位範囲 (IQR) に基づく方法 (Interquartile Range method)
    • 箱ひげ図で外れ値を定義する際にも用いられる考え方です。まず、データの上位25%点(第三四分位数、Q3)と下位25%点(第一四分位数、Q1)の差である四分位範囲(IQR = Q3 – Q1)を計算します。そして、Q1 - k * IQR より小さい値、および Q3 + k * IQR より大きい値を外れ値とみなし、これらの閾値で置き換えます。k の値は通常1.5が用いられますが、より厳しい基準を設ける場合は3.0などが使われることもあります。この方法は、データの分布形状に比較的頑健(ロバスト)であるという特徴があります。
  4. 中央値絶対偏差 (MAD) に基づく方法 (Median Absolute Deviation method)
    • 中央値と、各データポイントと中央値との差の絶対値の中央値(MAD)を利用する方法です。平均値や標準偏差よりも外れ値の影響を受けにくい中央値とMADを用いるため、よりロバストな外れ値検出・処理が可能です。閾値は通常、中央値 ± k * MAD のように設定され、k は定数(例えば2.5や3.0)です。

閾値の選択は、ウィンザー化の効果を大きく左右するため、慎重に行う必要があります。ドメイン知識、データの探索的分析(EDA)、そして最終的なモデルの性能評価などを通じて、最適な閾値を見つけることが重要です。

ウィンザー化前後のヒストグラムの比較

他の外れ値処理方法との比較

ウィンザー化以外にも、外れ値に対処するための様々なアプローチが存在します。

  • 削除 (Trimming / Clipping)
    • 最も単純な方法で、外れ値と判断されたデータポイントをデータセットから完全に除去します。実装は容易ですが、サンプルサイズが減少し、場合によっては有用な情報を失う可能性があります。特にデータ量が少ない場合には慎重な判断が必要です。ウィンザー化は値を置き換えるのに対し、削除は文字通り取り除く点が大きな違いです。
  • 変換 (Transformation)
    • 対数変換、平方根変換、Box-Cox変換などを用いてデータの分布形状を変化させ、外れ値の影響を相対的に小さくする方法です。特に裾の重い分布(skewed distribution)に対して有効な場合があります。ただし、変換後のデータの解釈が難しくなることや、全ての外れ値に有効とは限らない点がデメリットです。ウィンザー化が特定の値の置き換えに焦点を当てるのに対し、変換はデータ全体のスケールや分布を変えます。
  • ビニング (Binning / Discretization)
    • 連続値をいくつかの区間(ビン)に分割し、各データポイントを所属するビンの代表値(例えば、ビンの平均値や中央値)で置き換える方法です。これにより、個々の外れ値の影響を平滑化できます。また、カテゴリ変数として扱うことも可能です。ウィンザー化が極端な値のみを対象とするのに対し、ビニングは値の範囲全体を離散化します。

使い分けのヒント

  • 外れ値が明らかにエラーであり、かつデータ量が十分に多い場合:削除も選択肢に入ります。
  • データの分布が大きく歪んでおり、線形モデルなど特定の分布を仮定するモデルを使用する場合:変換が有効なことがあります。
  • 外れ値の影響を抑制しつつ、サンプルサイズを維持したい場合や、外れ値が完全にノイズとは言い切れない場合:ウィンザー化が適しています。
  • 連続変数をカテゴリ変数として扱いたい、またはノイズの影響をより広範囲に平滑化したい場合:ビニングが考慮されます。

どの手法を選択するかは、データの特性、分析の目的、そして外れ値が発生した背景などを総合的に考慮して決定する必要があります。多くの場合、単一の手法に固執するのではなく、複数の手法を試し、その結果を比較検討することが推奨されます。

ウィンザー化の考慮事項とベストプラクティス

ウィンザー化は外れ値に対して有効な手法ですが、適用にあたってはいくつかの考慮事項と、効果を最大限に引き出すためのベストプラクティスが存在します。

メリット

ウィンザー化の主な利点は以下の通りです。

  • データの分布を大きく歪めにくい:外れ値を完全に削除するのではなく、閾値で置き換えるため、元々のデータの分布形状を比較的維持しやすいです。これにより、データの中心的な傾向やばらつき具合を大きく損なうことなく、外れ値の影響を軽減できます。
  • サンプルサイズを維持できる:データポイントを削除しないため、分析やモデル学習に利用できるサンプルサイズが変わりません。特にデータ量が限られている場合に大きな利点となります。
  • 外れ値の影響を効果的に抑制できる:極端な値を丸め込むことで、平均値や分散などの統計量の歪みを防ぎ、モデルが外れ値に過剰に反応するのを抑制します。
  • 実装が比較的容易:多くの統計ソフトウェアやプログラミング言語のライブラリでサポートされており、比較的簡単に実装することができます。

デメリット

一方で、ウィンザー化には以下のような潜在的な欠点も存在します。

  • 閾値の設定が恣意的になる可能性がある:どの程度の値を外れ値とみなし、どのパーセンタイルでキャップ/フロアするかという閾値の決定は、分析者の判断に委ねられる部分が大きく、客観的な基準を見つけるのが難しい場合があります。不適切な閾値設定は、かえって分析結果を歪める可能性もあります。
  • 本来のデータの情報を一部失う可能性がある:外れ値を置き換えるという行為は、元々のデータが持っていた情報を一部変更することを意味します。置き換えられた値は、もはや観測された生のデータではありません。
  • 多変量データの場合の複雑性:複数の変数が絡み合う多変量データにおいては、どの変数にウィンザー化を適用すべきか、また各変数に対してどの程度の閾値を設定するかの判断がより複雑になります。変数間の相関関係も考慮に入れる必要があります。
  • 重要な情報を持つ外れ値の見逃し:全てではありませんが、外れ値の中には単なるノイズではなく、システムのエラー、不正行為、あるいは非常に稀だが重要な現象を示唆している場合があります。ウィンザー化によってこれらの情報がマスキングされてしまうと、重要な洞察を見逃すリスクがあります。

ベストプラクティス

ウィンザー化を効果的に活用するためのベストプラクティスは以下の通りです。

  • ウィンザー化は万能ではないことを理解する:外れ値処理の一つの選択肢であり、常に最良の方法とは限りません。データの特性や分析の目的に応じて、他の手法との比較検討が重要です。
  • 閾値選択の慎重な検討:キャップ/フロアするパーセンタイルの選択は、ドメイン知識を参考にしたり、探索的データ分析(EDA)でデータの分布をよく観察したり、いくつかの異なる閾値を試して結果を比較検討する(試行錯誤)など、慎重に行うべきです。
  • ウィンザー化前後の比較検証:ウィンザー化を適用する前後で、記述統計量(平均、中央値、分散など)の変化や、機械学習モデルの性能(予測精度、頑健性など)がどのように変わるかを確認しましょう。これにより、ウィンザー化が意図した通りの効果を発揮しているか、あるいは予期せぬ影響を与えていないかを評価できます。
  • 外れ値の原因究明の努力:外れ値を機械的に処理する前に、なぜその外れ値が発生したのか、その原因を探ることが非常に重要です。それが単なる入力ミスや測定誤差であれば処理の対象となりますが、もし重要な異常や特異なパターンを示しているのであれば、安易に処理せず、その情報を深掘りするべきです。

ウィンザー化は、外れ値による悪影響を軽減するための強力なツールですが、その適用は慎重な判断と検証を伴うべきです。上記のメリット・デメリットを理解し、ベストプラクティスに従うことで、より信頼性の高いデータ分析とモデル構築に繋がるでしょう。

Pythonによるウィンザー化の実装

Pythonでは、いくつかのライブラリを利用してウィンザー化を比較的簡単に実装できます。ここでは、代表的なライブラリであるSciPyFeature-engineを使った実装例を紹介します。

SciPyを使った基本的な実装例 (scipy.stats.mstats.winsorize)

SciPyは、科学技術計算のための基本的な機能を提供するライブラリで、その中の scipy.stats.mstats.winsorize 関数を用いることでウィンザー化を実行できます。

基本的な使い方

winsorize 関数の主な引数は以下の通りです。

  • a: ウィンザー化を適用する配列(NumPy配列など)。
  • limits: ウィンザー化するデータの割合を指定します。
    • 単一の浮動小数点数を与えた場合、両側からその割合でウィンザー化します(例:0.05 は下位5%と上位5%)。
    • 2つの浮動小数点数からなるタプルやリストを与えた場合、それぞれ下限と上限の割合として扱います(例:(0.05, 0.1) は下位5%と上位10%)。
  • inclusive: 閾値を含むかどうかを指定します(デフォルトは (True, True))。
  • nan_policy: NaN値の扱い方を指定します('propagate', 'raise', 'omit')。デフォルトは 'propagate' で、NaNはそのまま残ります。

コード例

import numpy as np
from scipy.stats.mstats import winsorize

# サンプルデータの生成 (外れ値を含む)
data = np.array([10, 12, 15, 11, 13, 100, 12, 14, 16, -20, 11])
print(f"元のデータ: {data}")

# 両側5%をウィンザー化 (実際にはデータ数が少ないため、各1点が置き換わる)
# limits=0.05 はデータ数が少ないため、挙動を明確にするために0.1 (各10%)とする
# もしくは、より多くのデータ点を持つサンプルで試すのが適切
# ここでは、データ点の数に基づいてlimitsを調整し、最小と最大の1点ずつを対象とするイメージで
# limits=(1/len(data), 1/len(data)) としてもよいが、ここでは固定値で示す
winsorized_data_both = winsorize(data, limits=0.1) # 下位10%と上位10%
print(f"両側10%ウィンザー化後のデータ: {winsorized_data_both}")

# 下側5%のみをウィンザー化
winsorized_data_lower = winsorize(data, limits=(0.1, 0)) # 下位10%のみ
print(f"下側10%ウィンザー化後のデータ: {winsorized_data_lower}")

# 上側5%のみをウィンザー化
winsorized_data_upper = winsorize(data, limits=(0, 0.1)) # 上位10%のみ
print(f"上側10%ウィンザー化後のデータ: {winsorized_data_upper}")

実行結果

元のデータ: [ 10  12  15  11  13 100  12  14  16 -20  11]
両側10%ウィンザー化後のデータ: [10 12 15 11 13 16 12 14 16 10 11] # -20が10に、100が16に置き換え
下側10%ウィンザー化後のデータ: [ 10  12  15  11  13 100  12  14  16  10  11] # -20が10に置き換え
上側10%ウィンザー化後のデータ: [10 12 15 11 13 16 12 14 16 -20 11] # 100が16に置き換え

実装上のポイント

  • winsorize 関数はNumPy配列を扱うため、PandasのSeriesやDataFrameの特定の列に適用する場合は、一度NumPy配列に変換するか、apply メソッドなどと組み合わせて使用します。
  • limits の指定は、データの分布を考慮して慎重に決定する必要があります。小さすぎると外れ値の影響が残り、大きすぎると有用な情報を失う可能性があります。
  • 多次元配列に対しても適用可能ですが、その場合は軸 (axis パラメータ、デフォルトは None で全体をフラットにして処理) を指定できます。

Feature-engineライブラリを使った実践的な実装例 (feature_engine.outliers.Winsorizer)

Feature-engineは、機械学習のパイプラインに組み込みやすい形で特徴量エンジニアリングの機能を提供するライブラリです。Winsorizer クラスを使うことで、より柔軟かつ実践的なウィンザー化が可能です。

Feature-engineの利点

  • Scikit-learnのTransformer APIと互換性があり、パイプラインに容易に組み込めます。
  • 複数の閾値設定方法('gaussian', 'iqr', 'mad', 'quantiles')をサポートしています。
  • 処理対象の変数を指定できます。
  • 学習データで決定した閾値を、テストデータや新しいデータに一貫して適用できます。

基本的な使い方

Winsorizer クラスの主な初期化パラメータは以下の通りです。

  • capping_method: ウィンザー化の方法を指定します。
    • 'gaussian': 平均 ± tail * 標準偏差 を閾値とします。
    • 'iqr': Q1 – tail * IQR と Q3 + tail * IQR を閾値とします。
    • 'mad': 中央値 ± tail * MAD を閾値とします(MADは正規分布を仮定して調整されます)。
    • 'quantiles': fold で指定されたパーセンタイルを閾値とします。
  • tail: capping_method'gaussian', 'iqr', 'mad' の場合に、閾値を決定するための係数を指定します(例:'gaussian'tail=3 なら3σ)。
  • fold: capping_method'quantiles' の場合に、両端からどの程度の割合をキャップするかを指定します(例:0.05 なら下位5%と上位5%)。
  • variables: ウィンザー化を適用する変数のリストを指定します。指定しない場合は、数値型の全ての変数に適用されます。
  • missing_values: 欠損値の扱い方を指定します('raise' または 'ignore')。

コード例

import pandas as pd
from feature_engine.outliers import Winsorizer
from rich import print
from rich.console import Console
from rich.table import Table


def display(df: pd.DataFrame, title: str = "DataFrame"):
    console = Console()
    table = Table(title=title, show_lines=True)

    # ヘッダー追加
    for column in df.columns:
        table.add_column(str(column), justify="center", style="cyan", no_wrap=True)

    # 行データ追加
    for _, row in df.iterrows():
        table.add_row(*[str(value) for value in row])

    console.print(table)


if __name__ == "__main__":
    # サンプルデータの生成 (Pandas DataFrame)
    data_df = pd.DataFrame({
        'feature1': [10, 12, 15, 11, 13, 100, 12, 14, 16, -20, 11, 9],
        'feature2': [5, 6, 7, 5, 8, 25, 6, 7, 9, -10, 5, 6]
    })
    display(data_df, title="元のDataFrame")
    print("")

    # Quantile法 (両端5%) でウィンザー化
    winsorizer_quantile = Winsorizer(
        capping_method='quantiles',
        tail='both',
        fold=0.05,
        variables=['feature1', 'feature2'],
    )
   
    winsorizer_quantile.fit(data_df)    # 学習データで閾値を学習
    df_winsorized_quantile = winsorizer_quantile.transform(data_df) # 学習した閾値を使ってデータを変換
    display(df_winsorized_quantile, title="Quantile法 (両端5%) ウィンザー化後のDataFrame")
    print(f"学習された閾値 (Quantile法): {winsorizer_quantile.right_tail_caps_}, {winsorizer_quantile.left_tail_caps_}\n")

    # IQR法 (k=1.5) でウィンザー化
    winsorizer_iqr = Winsorizer(
        capping_method='iqr',
        tail='both', fold=1.5,
        variables=['feature1'],
    )
    winsorizer_iqr.fit(data_df[['feature1']])   # 'feature1'のみを対象とする例
    df_winsorized_iqr = winsorizer_iqr.transform(data_df[['feature1']])
    display(df_winsorized_iqr, title="IQR法 (k=1.5) ウィンザー化後のDataFrame (feature1のみ)")
    print(f"学習された閾値 (IQR法, feature1): 右側キャップ={winsorizer_iqr.right_tail_caps_['feature1']}, 左側キャップ={winsorizer_iqr.left_tail_caps_['feature1']}\n")

    # Gaussian法 (3σ) でウィンザー化
    winsorizer_gaussian = Winsorizer(
        capping_method='gaussian',
        tail='both',
        fold=3,
        variables=['feature2'],
    )
    winsorizer_gaussian.fit(data_df[['feature2']])
    df_winsorized_gaussian = winsorizer_gaussian.transform(data_df[['feature2']])
    display(df_winsorized_gaussian, title="Gaussian法 (3σ) ウィンザー化後のDataFrame (feature2のみ)")
    print(f"学習された閾値 (Gaussian法, feature2): 右側キャップ={winsorizer_gaussian.right_tail_caps_['feature2']}, 左側キャップ={winsorizer_gaussian.left_tail_caps_['feature2']}")

実行結果

     元のDataFrame
┏━━━━━━━━━━┳━━━━━━━━━━┓
┃ feature1 ┃ feature2 ┃
┡━━━━━━━━━━╇━━━━━━━━━━┩
│    10    │    5     │
├──────────┼──────────┤
│    12    │    6     │
├──────────┼──────────┤
│    15    │    7     │
├──────────┼──────────┤
│    11    │    5     │
├──────────┼──────────┤
│    13    │    8     │
├──────────┼──────────┤
│   100    │    25    │
├──────────┼──────────┤
│    12    │    6     │
├──────────┼──────────┤
│    14    │    7     │
├──────────┼──────────┤
│    16    │    9     │
├──────────┼──────────┤
│   -20    │   -10    │
├──────────┼──────────┤
│    11    │    5     │
├──────────┼──────────┤
│    9     │    6     │
└──────────┴──────────┘

            Quantile法 (両端5%)
         ウィンザー化後のDataFrame
┏━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━┓
┃      feature1      ┃      feature2       ┃
┡━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━┩
│        10.0        │         5.0         │
├────────────────────┼─────────────────────┤
│        12.0        │         6.0         │
├────────────────────┼─────────────────────┤
│        15.0        │         7.0         │
├────────────────────┼─────────────────────┤
│        11.0        │         5.0         │
├────────────────────┼─────────────────────┤
│        13.0        │         8.0         │
├────────────────────┼─────────────────────┤
│ 53.79999999999994  │  16.19999999999999  │
├────────────────────┼─────────────────────┤
│        12.0        │         6.0         │
├────────────────────┼─────────────────────┤
│        14.0        │         7.0         │
├────────────────────┼─────────────────────┤
│        16.0        │         9.0         │
├────────────────────┼─────────────────────┤
│ -4.049999999999999 │ -1.7499999999999991 │
├────────────────────┼─────────────────────┤
│        11.0        │         5.0         │
├────────────────────┼─────────────────────┤
│        9.0         │         6.0         │
└────────────────────┴─────────────────────┘
学習された閾値 (Quantile法): {'feature1': 53.79999999999994, 'feature2': 16.19999999999999}, {'feature1':
-4.049999999999999, 'feature2': -1.7499999999999991}

   IQR法
  (k=1.5)
ウィンザー化
後のDataFram
     e
(feature1の
    み)
┏━━━━━━━━━━┓
┃ feature1 ┃
┡━━━━━━━━━━┩
│   10.0   │
├──────────┤
│   12.0   │
├──────────┤
│   15.0   │
├──────────┤
│   11.0   │
├──────────┤
│   13.0   │
├──────────┤
│   19.5   │
├──────────┤
│   12.0   │
├──────────┤
│   14.0   │
├──────────┤
│   16.0   │
├──────────┤
│   5.5    │
├──────────┤
│   11.0   │
├──────────┤
│   9.0    │
└──────────┘
学習された閾値 (IQR法, feature1): 右側キャップ=19.5, 左側キャップ=5.5

 Gaussian法
    (3σ)
ウィンザー化
後のDataFram
     e
(feature2の
    み)
┏━━━━━━━━━━┓
┃ feature2 ┃
┡━━━━━━━━━━┩
│    5     │
├──────────┤
│    6     │
├──────────┤
│    7     │
├──────────┤
│    5     │
├──────────┤
│    8     │
├──────────┤
│    25    │
├──────────┤
│    6     │
├──────────┤
│    7     │
├──────────┤
│    9     │
├──────────┤
│   -10    │
├──────────┤
│    5     │
├──────────┤
│    6     │
└──────────┘
学習された閾値 (Gaussian法, feature2): 右側キャップ=28.336206706752304, 左側キャップ=-15.16954004008564

実装上のポイント

  • fit() メソッドで学習データからウィンザー化の閾値を計算し、transform() メソッドでその閾値に基づいてデータを変換します。これにより、学習データとテストデータに一貫した処理を適用できます。
  • variables パラメータで処理対象の列を明示的に指定できるため、特定の列にのみウィンザー化を適用したい場合に便利です。
  • capping_methodtail/fold パラメータを組み合わせることで、様々な基準でウィンザー化を実行できます。
  • scikit-learnのパイプライン (sklearn.pipeline.Pipeline) に組み込むことで、前処理の一環としてウィンザー化を体系的に実行し、ハイパーパラメータチューニングの対象とすることも可能です。

おわりに

本記事では、データ分析や機械学習における外れ値処理の一手法である「ウィンザー化」について、その基本的な概念から具体的な閾値設定方法、メリット・デメリット、加えてPythonを用いた実装例を紹介しました。

ウィンザー化は、外れ値の極端な影響を抑制しつつサンプルサイズを維持できる有効な手段です。しかし、閾値の設定には慎重な検討が必要であり、万能な手法ではありません。最も重要なのは、外れ値の発生原因を理解しようと努め、データの文脈や分析の目的に応じて適切な処理方法を選択することです。

ウィンザー化を道具の一つとして理解し、他の外れ値処理手法(削除、変換、ビニングなど)や、より高度なロバスト統計の手法と合わせて検討することで、データの潜在的な価値を最大限に引き出し、より信頼性の高い知見を得ることができます。データを慎重に扱い試行錯誤を重ねることで、質の高い分析が可能となります。

More Information