NumPy配列の中央値を求めるnumpy.medianの基本

NumPy配列の中央値を求める numpy.median の詳細な説明

はじめに:データ分析における代表値の重要性

データは現代社会における最も貴重な資産の一つです。科学研究からビジネス戦略、医療診断に至るまで、私たちはデータから洞察を得て、意思決定を行っています。膨大なデータセットを扱う際、まず最初に行うことの一つは、そのデータの性質を要約し、全体像を把握することです。この要約プロセスにおいて不可欠なのが、データの「中心的な傾向」を示す代表値です。

代表値にはいくつかの種類があります。最も一般的に知られているのは平均値 (Mean) です。データセット内のすべての値を合計し、項目の数で割ることで得られます。平均値は計算が容易であり、統計理論においても重要な役割を果たしますが、極端な値(外れ値)に大きく影響されるという弱点があります。例えば、多くの普通の収入の人々と数人の億万長者の収入を平均すると、平均収入は多くの人々の実際の収入よりもはるかに高くなる可能性があります。

ここで登場するのが、もう一つの強力な代表値である中央値 (Median) です。中央値は、データセットを昇順または降順に並べたときに、ちょうど真ん中に位置する値です。データの数が多い場合、中央値は外れ値の影響をほとんど受けません。これは、所得や住宅価格のような分布が歪んでいるデータセットを分析する際に特に有用です。中央値は、データの「典型的な値」を把握するための堅牢な尺度と言えます。

Pythonにおける数値計算とデータ分析のデファクトスタンダードライブラリであるNumPyは、効率的かつ柔軟にこれらの統計計算を実行するための強力なツールを提供しています。本記事では、NumPyライブラリに用意されている、配列の中央値を計算するための主要な関数である numpy.median に焦点を当て、その基本的な使い方から詳細な挙動、応用例、さらには内部的な側面までを深く掘り下げて解説します。約5000語に及ぶ詳細な説明を通じて、numpy.median をデータ分析の強力な武器として使いこなせるようになることを目指します。

中央値の定義と性質

numpy.median の詳細に入る前に、中央値の数学的な定義と性質をもう少し詳しく見てみましょう。

中央値 (Median) は、データを小さい順(または大きい順)に並べたときに、順位的に真ん中にくる値です。

  • データ数が奇数の場合: データを昇順に並べたとき、ちょうど真ん中にくる値が中央値です。
    例: [1, 3, 5, 7, 9] の中央値は 5 です。
    例: [10, 20, 30] の中央値は 20 です。

  • データ数が偶数の場合: データを昇順に並べたとき、真ん中にくる2つの値の平均値が中央値です。
    例: [1, 2, 4, 5] の中央値は (2 + 4) / 2 = 3 です。
    例: [10, 20, 30, 40] の中央値は (20 + 30) / 2 = 25 です。

この定義からわかるように、中央値はデータの値そのものよりも、値の相対的な順序に依存します。この性質こそが、中央値が外れ値に対して頑健である理由です。データセットに極端に大きな値や小さな値が含まれていても、それらの値がデータの順序に与える影響は限定的であり、中央に位置する値(または値のペア)が大きく変わることは少ないからです。

一方、平均値はデータセット内のすべての値の合計に依存するため、一つの外れ値が存在するだけで、平均値は大きく引きずられてしまいます。

特徴 平均値 (Mean) 中央値 (Median)
定義 全値の合計 / 項目の数 順序付けされたデータの真ん中の値
計算方法 総和と個数が必要 順序付けが必要
外れ値の影響 非常に受けやすい ほとんど受けない(頑健である)
データの型 数値データに限定される 数値データに限定される(順序付け可能な場合)
利点 計算が容易、多くの統計手法の基礎 外れ値に強く、分布が歪んでいても有効
欠点 外れ値に弱い 計算に順序付けが必要(大規模データでコストがかかる場合がある)、分布の詳細を反映しにくい
適した状況 正規分布に近いデータ、外れ値が少ない場合 外れ値が多いデータ、所得や不動産価格など分布が歪んでいる場合

このように、平均値と中央値は異なる特性を持ち、分析するデータの性質や目的に応じて適切に使い分けることが重要です。

numpy.median の基本的な使い方

NumPyライブラリは、多次元配列(ndarray)を効率的に操作するための機能を提供しており、その中に統計関数群が含まれています。numpy.median はその中核をなす関数の一つです。

numpy.median 関数のシグネチャ(主要な引数)は以下のようになります。

python
numpy.median(a, axis=None, out=None, overwrite_input=False, keepdims=False)

各引数について詳しく見ていきましょう。

  • a: 中央値を計算したい入力配列(ndarray または array_like)。配列ライクなオブジェクト(リストやタプルなど)も受け付けますが、内部的にはNumPy配列に変換されます。
  • axis: 中央値を計算する軸を指定します。
    • None (デフォルト): 配列全体をフラット化(1次元化)して中央値を計算します。結果はスカラ値になります。
    • 整数のタスクまたは整数のタプル: 指定された軸に沿って中央値を計算します。例えば、2次元配列で axis=0 を指定すると、各列の中央値が計算され、1次元の配列が返されます。axis=1 なら各行の中央値が計算されます。タプルで複数の軸を指定することも可能です。結果の配列は、指定した軸が削除された次元になります。
  • out: 中央値を格納するための代替出力配列を指定します。指定した場合、計算結果はこの配列に書き込まれます。形状とデータ型は期待される出力と一致している必要があります。デフォルトは None で、新しい配列が作成されます。
  • overwrite_input: True の場合、中央値を計算するために内部で入力配列 a がソートされる際に、その場で(インプレースで)ソートを行います。これによりメモリ使用量を削減できますが、元の入力配列 a が変更されるという副作用があります。デフォルトは False で、入力配列は変更されません(コピーが作成されてソートされます)。この引数は、データ型が浮動小数点型の場合にのみ有効です。
  • keepdims: True の場合、中央値を計算した軸が結果から削除されずに、サイズ1の次元として残されます。これにより、結果の配列は元の配列と同じ次元数を持つようになります。デフォルトは False です。

それでは、具体的なコード例を通じて numpy.median の使い方を見ていきましょう。

一次元配列での基本使用例

最も簡単なケースは、一次元配列の中央値を計算することです。

“`python
import numpy as np

奇数個のデータ

data1 = np.array([1, 3, 5, 7, 9])
median1 = np.median(data1)
print(f”データ1: {data1}”)
print(f”中央値1 (奇数個): {median1}”) # 出力: 5.0

偶数個のデータ

data2 = np.array([1, 2, 4, 5])
median2 = np.median(data2)
print(f”\nデータ2: {data2}”)
print(f”中央値2 (偶数個): {median2}”) # 出力: 3.0

リストを直接渡す

data3_list = [10, 30, 20, 50, 40]
median3 = np.median(data3_list)
print(f”\nデータ3 (リスト): {data3_list}”)
print(f”中央値3 (リスト): {median3}”) # 出力: 30.0
“`

一次元配列の場合、axis=None がデフォルトであり、配列全体の中央値が計算されます。結果はスカラ値(NumPyの0次元配列)になります。NumPyは内部でデータをソートし、奇数個の場合は真ん中の値、偶数個の場合は真ん中の2つの値の平均を計算します。データ型は、入力が整数型でも浮動小数点型に変換されることに注意してください(偶数個の場合に平均を取る際に浮動小数点数になる可能性があるため)。

多次元配列での軸の指定 (axis 引数)

numpy.median の強力な機能の一つは、多次元配列の特定の軸に沿って中央値を計算できることです。これにより、例えば行列の各行や各列の中央値を効率的に計算できます。

二次元配列 (axis=0 または axis=1)

“`python
import numpy as np

data_2d = np.array([
[10, 20, 30],
[40, 50, 60],
[70, 80, 90]
])

print(f”二次元配列:\n{data_2d}”)

axis=None (デフォルト): 配列全体をフラット化して中央値を計算

median_all = np.median(data_2d)
print(f”\n配列全体の中央値: {median_all}”) # 出力: 50.0 (配列を [10, 20, 30, 40, 50, 60, 70, 80, 90] として計算)

axis=0: 各列の中央値を計算

median_cols = np.median(data_2d, axis=0)
print(f”\n各列の中央値 (axis=0): {median_cols}”)

出力: [40. 50. 60.]

計算内訳:

1列目: median([10, 40, 70]) = 40.0

2列目: median([20, 50, 80]) = 50.0

3列目: median([30, 60, 90]) = 60.0

axis=1: 各行の中央値を計算

median_rows = np.median(data_2d, axis=1)
print(f”\n各行の中央値 (axis=1): {median_rows}”)

出力: [20. 50. 80.]

計算内訳:

1行目: median([10, 20, 30]) = 20.0

2行目: median([40, 50, 60]) = 50.0

3行目: median([70, 80, 90]) = 80.0

“`

axis を指定すると、計算結果の次元数は元の配列より1つ減ります。例えば、(3, 3) の2次元配列に対して axis=0 または axis=1 を指定すると、結果は (3,) の1次元配列になります。

三次元配列での軸の指定

三次元配列の場合、axis は 0, 1, 2 のいずれか、またはこれらのタプルで指定できます。

“`python
import numpy as np

data_3d = np.arange(1, 25).reshape(2, 3, 4)
print(f”三次元配列 (形状 {data_3d.shape}):\n{data_3d}”)

axis=None: 配列全体をフラット化して中央値を計算

median_all_3d = np.median(data_3d)
print(f”\n配列全体の中央値: {median_all_3d}”) # 出力: 12.5 (arange(1, 25) の中央値)

axis=0: 最初の軸 (2) に沿って中央値を計算

median_axis0 = np.median(data_3d, axis=0)
print(f”\n中央値 (axis=0, 形状 {median_axis0.shape}):\n{median_axis0}”)

出力形状は (3, 4)

例えば、結果の [0, 0] は data_3d[:, 0, 0] ([1, 13]) の中央値 (7.0)

axis=1: 2番目の軸 (3) に沿って中央値を計算

median_axis1 = np.median(data_3d, axis=1)
print(f”\n中央値 (axis=1, 形状 {median_axis1.shape}):\n{median_axis1}”)

出力形状は (2, 4)

例えば、結果の [0, 0] は data_3d[0, :, 0] ([1, 5, 9]) の中央値 (5.0)

axis=2: 3番目の軸 (4) に沿って中央値を計算

median_axis2 = np.median(data_3d, axis=2)
print(f”\n中央値 (axis=2, 形状 {median_axis2.shape}):\n{median_axis2}”)

出力形状は (2, 3)

例えば、結果の [0, 0] は data_3d[0, 0, :] ([1, 2, 3, 4]) の中央値 (2.5)

axis=(0, 1): 最初の2つの軸に沿って中央値を計算

median_axis01 = np.median(data_3d, axis=(0, 1))
print(f”\n中央値 (axis=(0, 1), 形状 {median_axis01.shape}):\n{median_axis01}”)

出力形状は (4,)

例えば、結果の [0] は data_3d[:, :, 0] ([[1], [5], [9]], [[13], [17], [21]]) をフラット化した [1, 5, 9, 13, 17, 21] の中央値 (11.0)

“`

axis 引数にタプルを指定すると、指定した複数の軸に沿って中央値が計算されます。例えば、形状 (L, M, N) の配列に対して axis=(0, 1) を指定すると、結果の形状は (N,) となります。これは、形状 (L, M, N) の配列を、サイズ L*M の要素を持つ N 個の「列」として扱い、それぞれの「列」の中央値を計算しているとイメージできます。

keepdims 引数

keepdims=True を指定すると、中央値を計算した軸が結果の配列から削除されず、サイズ1の次元として保持されます。これは、元の配列と同じ次元数の結果を得たい場合に便利です。特に、ブロードキャスティングを利用して元の配列と結果を組み合わせる際に役立ちます。

“`python
import numpy as np

data_2d = np.array([
[10, 20, 30],
[40, 50, 60],
[70, 80, 90]
])

print(f”二次元配列:\n{data_2d}”)

keepdims=False (デフォルト)

median_cols_no_keep = np.median(data_2d, axis=0, keepdims=False)
print(f”\n各列の中央値 (keepdims=False): {median_cols_no_keep} (形状 {median_cols_no_keep.shape})”)

出力: [40. 50. 60.] (形状 (3,))

keepdims=True

median_cols_keep = np.median(data_2d, axis=0, keepdims=True)
print(f”\n各列の中央値 (keepdims=True): {median_cols_keep} (形状 {median_cols_keep.shape})”)

出力: [[40. 50. 60.]] (形状 (1, 3))

ブロードキャスティングの例

データから列の中央値を引く操作を考えます。

keepdims=False の結果だと形状が合わないため、直接引き算できません。

print(data_2d – median_cols_no_keep) # エラーまたは意図しない挙動

keepdims=True の結果は形状が合う (3, 3) – (1, 3) -> ブロードキャストで (3, 3) – (3, 3) に

data_centered_by_median = data_2d – median_cols_keep
print(f”\n各列の中央値でデータを中心化:\n{data_centered_by_median}”)

出力:

[[-30. -30. -30.]

[ 0. 0. 0.]

[ 30. 30. 30.]]

“`

keepdims=True を使うと、結果の次元構造が元の配列とより整合性が取れるため、その後の数値計算、特にブロードキャスティングを利用した配列操作が容易になります。

out 引数

out 引数を使用すると、計算結果を既存のNumPy配列に格納できます。これは、特に大規模な計算を繰り返し行う場合や、メモリの使用を厳密に管理したい場合に役立ちます。新しい配列を毎回作成するオーバーヘッドを避けることができます。

“`python
import numpy as np

data = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])

結果を格納するための配列を事前に作成

期待される出力はスカラ値の中央値なので、形状 ()、 dtype=float64 の配列を作成

result_array = np.empty(shape=(), dtype=np.float64)

out 引数を使用して結果を result_array に格納

np.median(data, out=result_array)

print(f”入力データ: {data}”)
print(f”結果が格納された配列: {result_array}”)
print(f”結果の値: {result_array.item()}”) # スカラ値を取り出す

出力:

入力データ: [ 1 2 3 4 5 6 7 8 9 10]

結果が格納された配列: 5.5

結果の値: 5.5

多次元の場合

data_2d = np.array([[1, 2], [3, 4]])

axis=0 で計算する場合、結果は形状 (2,)

result_array_2d = np.empty(shape=(2,), dtype=np.float64)
np.median(data_2d, axis=0, out=result_array_2d)
print(f”\n入力データ2D:\n{data_2d}”)
print(f”結果が格納された配列2D (axis=0): {result_array_2d}”)

出力:

入力データ2D:

[[1 2]

[3 4]]

結果が格納された配列2D (axis=0): [2. 3.]

“`

out 引数に指定する配列は、計算結果が格納されるべき適切な形状とデータ型を持っている必要があります。そうでない場合、エラーが発生します。

overwrite_input 引数

overwrite_input=True は、パフォーマンスを向上させる可能性がある重要な引数ですが、使用には注意が必要です。

中央値を計算するために、NumPyは通常、入力配列のコピーを作成し、そのコピーをソートします。これは、元の入力配列を変更しないようにするための安全策です。しかし、配列が大きい場合、コピーの作成には時間とメモリが必要になります。

overwrite_input=True を指定すると、NumPyは入力配列 a を直接ソートします。これによりコピーの作成が不要になり、特に大規模な配列に対してメモリ使用量を削減し、計算時間を短縮できる可能性があります。

ただし、副作用として、中央値計算のために numpy.median を呼び出した後、元の入力配列 a の要素の順序が変更されてしまいます入力配列を後続の処理でオリジナルの順序で使用する必要がある場合は、この引数を False のままにするか、事前に明示的にコピーを作成する必要があります。

“`python
import numpy as np

data_original = np.array([5, 2, 8, 1, 9, 4, 7, 3, 6])
data_for_overwrite = data_original.copy() # コピーを作成して実験

print(f”元のデータ (original): {data_original}”)
print(f”overwrite_input=True 用データ: {data_for_overwrite}”)

overwrite_input=False (デフォルト)

median_no_overwrite = np.median(data_original, overwrite_input=False)
print(f”\n中央値 (overwrite_input=False): {median_no_overwrite}”)
print(f”処理後の元のデータ (original): {data_original}”) # 元の配列は変わらない

出力:

中央値 (overwrite_input=False): 5.0

処理後の元のデータ (original): [5 2 8 1 9 4 7 3 6]

overwrite_input=True

median_with_overwrite = np.median(data_for_overwrite, overwrite_input=True)
print(f”\n中央値 (overwrite_input=True): {median_with_overwrite}”)
print(f”処理後の overwrite_input=True 用データ: {data_for_overwrite}”) # 元の配列がソートされている!

出力:

中央値 (overwrite_input=True): 5.0

処理後の overwrite_input=True 用データ: [1 2 3 4 5 6 7 8 9]

“`

overwrite_input=True は、入力配列が不要になる最後の処理として中央値を計算する場合などに有効です。また、この引数は入力配列が浮動小数点数型 (float) または複素数型 (complex) の場合にのみサポートされます。整数型 (int) の場合は、NumPyは常に内部で浮動小数点数型のコピーを作成してソートするため、この引数を True にしても意味はありません。

中央値の計算方法:NumPyの内部処理

numpy.median は、NumPyがどのように中央値を計算しているかを理解することで、そのパフォーマンス特性や注意点(特に overwrite_input)がより明確になります。

中央値の計算は、本質的にはデータを順序付けることから始まります。しかし、配列全体を完全にソートする必要はありません。中央値は、順序付けられた配列において特定の位置(または2つの位置)にある値によって決まります。これは、選択問題 (selection problem) として知られる計算問題であり、「N個の要素を持つ配列から k番目に小さい要素を見つける」という問題です。中央値は、N個の要素の中から約 N/2 番目に小さい要素を見つける問題に対応します。

一般的なソートアルゴリズム(例えばクイックソートやマージソート)は、平均的に O(N log N) の計算時間を要します。一方、選択問題を効率的に解くアルゴリズム(例えばクイックセレクトやイントロセレクトの選択パート)は、平均的には O(N) の計算時間で済みます。最悪ケースでは O(N^2) になることがありますが、NumPyの内部実装では、これを避けるために洗練されたアルゴリズムが使われています。

NumPyの median 関数は、内部でこの選択アルゴリズムを利用しています。具体的には、必要に応じて部分的なソートまたは選択操作を実行して、中央値となる要素を見つけ出します。これにより、特に巨大な配列に対して、配列全体をソートする場合よりも効率的に中央値を計算することができます。

overwrite_input=True を指定した場合、NumPyは入力配列に対して直接この部分的なソート/選択操作を行います。overwrite_input=False の場合は、まず入力配列のコピーを作成し、そのコピーに対して操作を行います。このコピー作成のオーバーヘッドが、overwrite_input=True を使用することで削減される可能性のあるコストです。

多次元配列の場合、axis 引数で指定された軸に沿って、各「スライス」(例えば、axis=0 なら各列)に対して独立にこの中央値計算が行われます。これは、NumPyのブロードキャスティングやユニバーサル関数 (ufunc) の考え方に近い、軸ごとの並列処理として効率的に実装されています。

データ型と欠損値 (NaN) の扱い

numpy.median を使う上で、データ型と欠損値 (NaN) の扱いは重要な考慮事項です。

データ型

numpy.median は様々な数値データ型(整数型、浮動小数点型)の配列を処理できます。ただし、計算結果のデータ型には注意が必要です。

  • 入力が整数型で、データ数が奇数の場合:中央値は入力配列の要素の一つとなり、結果は整数型になり得ます。
  • 入力が整数型で、データ数が偶数の場合:中央値は真ん中の2つの値の平均となり、小数点以下が発生する可能性があるため、結果は浮動小数点型(通常 float64)になります。
  • 入力が浮動小数点型の場合:結果は通常、浮動小数点型になります。

“`python
import numpy as np

int_data_odd = np.array([1, 2, 3], dtype=np.int32)
int_median_odd = np.median(int_data_odd)
print(f”整数データ(奇数): {int_data_odd}, 中央値: {int_median_odd} (型: {int_median_odd.dtype})”) # 出力: 整数データ(奇数): [1 2 3], 中央値: 2.0 (型: float64) – 平均計算の可能性を考慮しfloatになる

int_data_even = np.array([1, 2, 3, 4], dtype=np.int32)
int_median_even = np.median(int_data_even)
print(f”整数データ(偶数): {int_data_even}, 中央値: {int_median_even} (型: {int_median_even.dtype})”) # 出力: 整数データ(偶数): [1 2 3 4], 中央値: 2.5 (型: float64)

float_data = np.array([1.0, 2.0, 3.0], dtype=np.float32)
float_median = np.median(float_data)
print(f”浮動小数点データ: {float_data}, 中央値: {float_median} (型: {float_median.dtype})”) # 出力: 浮動小数点データ: [1. 2. 3.], 中央値: 2.0 (型: float32)
“`

結果が float64 になることが多いのは、NumPyがデフォルトで高い精度を好むためです。計算中にオーバーフローや精度の問題を避けるために、より広い範囲と高い精度を持つデータ型が選択される傾向があります。

欠損値 (NaN)

NumPy配列に NaN (Not a Number) が含まれている場合、numpy.median の挙動は重要です。デフォルトでは、NumPyの多くの統計関数と同様に、NaN は計算結果に伝播します。つまり、中央値を計算する対象となるデータセット(配列全体、または特定の軸に沿ったスライス)の中に一つでも NaN が含まれている場合、その中央値は NaN になります。

“`python
import numpy as np

data_with_nan = np.array([1, 2, np.nan, 4, 5])
median_with_nan = np.median(data_with_nan)
print(f”NaNを含むデータ: {data_with_nan}, 中央値: {median_with_nan}”) # 出力: NaNを含むデータ: [ 1. 2. nan 4. 5.], 中央値: nan

data_2d_with_nan = np.array([
[10, 20, np.nan],
[40, 50, 60],
[np.nan, 80, 90]
])

print(f”\nNaNを含む二次元配列:\n{data_2d_with_nan}”)

axis=0 の場合

median_cols_with_nan = np.median(data_2d_with_nan, axis=0)
print(f”\n各列の中央値 (axis=0) with NaN: {median_cols_with_nan}”)

出力: [nan 50. 60.]

1列目: [10, 40, nan] -> nan

2列目: [20, 50, 80] -> 50.0

3列目: [nan, 60, 90] -> 60.0

axis=1 の場合

median_rows_with_nan = np.median(data_2d_with_nan, axis=1)
print(f”\n各行の中央値 (axis=1) with NaN: {median_rows_with_nan}”)

出力: [nan 50. nan]

1行目: [10, 20, nan] -> nan

2行目: [40, 50, 60] -> 50.0

3行目: [nan, 80, 90] -> nan

“`

多くのデータ分析のシナリオでは、欠損値を無視して有効な値のみに基づいて中央値を計算したい場合があります。このような場合のために、NumPyは numpy.nanmedian という別の関数を提供しています。nanmedian は、計算対象の配列またはスライスから NaN を除外し、残った有効な値の中央値を計算します。

“`python
import numpy as np

data_with_nan = np.array([1, 2, np.nan, 4, 5])
nan_median = np.nanmedian(data_with_nan)
print(f”NaNを含むデータ: {data_with_nan}, nanmedian: {nan_median}”) # 出力: NaNを含むデータ: [ 1. 2. nan 4. 5.], nanmedian: 3.0 ( [1, 2, 4, 5] の中央値 (2+4)/2=3)

data_2d_with_nan = np.array([
[10, 20, np.nan],
[40, 50, 60],
[np.nan, 80, 90]
])

axis=0 の場合

nan_median_cols = np.nanmedian(data_2d_with_nan, axis=0)
print(f”\n各列の nanmedian (axis=0) with NaN: {nan_median_cols}”)

出力: [25. 50. 75.]

1列目: [10, 40, nan] -> nanmedian([10, 40]) = 25.0

2列目: [20, 50, 80] -> nanmedian([20, 50, 80]) = 50.0

3列目: [nan, 60, 90] -> nanmedian([60, 90]) = 75.0

axis=1 の場合

nan_median_rows = np.nanmedian(data_2d_with_nan, axis=1)
print(f”\n各行の nanmedian (axis=1) with NaN: {nan_median_rows}”)

出力: [15. 50. 85.]

1行目: [10, 20, nan] -> nanmedian([10, 20]) = 15.0

2行目: [40, 50, 60] -> nanmedian([40, 50, 60]) = 50.0

3行目: [nan, 80, 90] -> nanmedian([80, 90]) = 85.0

“`

numpy.nanmediannumpy.median と同様に axis, out, keepdims 引数を持ちます。overwrite_input 引数も同様にサポートされますが、内部でNaNを除外するためにデータのコピーまたは再配置が必要になるため、overwrite_input=True の効果は numpy.median ほど顕著ではない場合があります。

NaNの扱いはデータ分析において非常に重要です。numpy.mediannumpy.nanmedian の違いを理解し、分析の目的に合わせて適切な関数を選択することが不可欠です。

他の統計関数との比較

NumPyには median 以外にも様々な統計関数があります。中央値がこれらの関数とどのように異なるのか、どのような状況で使い分けるべきかを理解することは、データ分析において非常に役立ちます。

numpy.mean (平均値)

前述の通り、平均値はデータの合計を項目数で割った値です。
numpy.mean(a, axis=None, dtype=None, out=None, keepdims=False)

  • 中央値との違い: 平均値はすべての値に影響されますが、中央値は値の順序にのみ影響されます。これにより、中央値は外れ値に対して頑健ですが、平均値は外れ値に弱いです。
  • 使い分け:
    • データが正規分布に近い場合や、すべての値を計算に含めたい場合は平均値が適しています。
    • データに外れ値が多い場合や、分布が歪んでいる(例えば、所得や資産の分布のように一部に極端な値がある)場合は、中央値の方が代表的な傾向をより適切に示します。

“`python
import numpy as np

data_with_outlier = np.array([1, 2, 3, 4, 100])

mean_val = np.mean(data_with_outlier)
median_val = np.median(data_with_outlier)

print(f”外れ値を含むデータ: {data_with_outlier}”)
print(f”平均値: {mean_val}”) # 出力: 22.0 (100に引っ張られる)
print(f”中央値: {median_val}”) # 出力: 3.0 (外れ値に影響されない)
“`

この例から、外れ値が存在する場合に平均値が大きく変動するのに対し、中央値は安定していることがわかります。

numpy.average (加重平均)

numpy.average は、各データポイントに重みを付けて平均値を計算する関数です。
numpy.average(a, axis=None, weights=None, returned=False)

  • 中央値との違い: average は平均値の一般化であり、ここでもすべての値(とそれに対応する重み)が計算に影響します。中央値のように順序に基づくものではありません。
  • 使い分け: 各データポイントの重要度が異なる場合(例えば、異なるサンプルサイズのグループの平均を結合する場合など)に使用します。中央値は重みを考慮できません。

numpy.percentile (パーセンタイル)

numpy.percentile(a, q, axis=None, out=None, overwrite_input=False, keepdims=False, interpolation='linear') は、データの q-パーセンタイルを計算する関数です。パーセンタイルは、データを小さい順に並べたときに、指定された割合 q (0から100までの数値) の位置にくる値を示します。

  • 中央値との関係: 中央値は、定義上、50パーセンタイルに相当します。つまり、np.median(a)np.percentile(a, 50) と同じ結果を返します(ただし、計算方法の微細な違いにより、非常にまれに浮動小数点数の結果がわずかに異なる場合があります)。
  • 使い分け:
    • 特定のパーセンタイル(例: 四分位数 (25, 50, 75パーセンタイル)、10パーセンタイル、90パーセンタイルなど)を知りたい場合は percentile を使用します。
    • 中央値のみを知りたい場合は median を使用します。median は中央値に特化しているため、多くの場合 percentile より高速に計算できます。これは、percentile が任意の q に対して機能する必要があるのに対し、median は特定の q=50 のケースに最適化されているためです。

“`python
import numpy as np

data = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])

median_val = np.median(data)
percentile_50 = np.percentile(data, 50)

print(f”データ: {data}”)
print(f”中央値: {median_val}”) # 出力: 5.5
print(f”50パーセンタイル: {percentile_50}”) # 出力: 5.5
print(f”25パーセンタイル: {np.percentile(data, 25)}”) # 出力: 3.25
print(f”75パーセンタイル: {np.percentile(data, 75)}”) # 出力: 7.75
“`

中央値はパーセンタイルの特殊なケースであるため、np.percentile(a, 50, ...)np.median(a, ...) はほぼ同義ですが、median は中央値計算に最適化されている分、コードの意図も明確になり、推奨されることが多いです。

numpy.nanmedian に対応するものとして、欠損値を無視してパーセンタイルを計算する numpy.nanpercentile も存在します。

応用例

numpy.median は様々な分野で応用されています。その外れ値に対する頑健性から、特にデータの要約や前処理において重宝されます。

1. 探索的データ分析 (EDA)

データセットの基本的な特性を理解するために、中央値は平均値と並んで最もよく計算される統計量の一つです。特に、データの分布が非対称である場合(例:所得、不動産価格、ウェブサイトの訪問回数など)や、測定に大きな誤差が含まれる可能性がある場合に、中央値はデータの典型的な値をより信頼性高く示します。

Pythonのデータ分析ライブラリであるPandasはNumPyを基盤としており、データフレームの .median() メソッドは内部でNumPyの関数を利用しています。

“`python
import numpy as np
import pandas as pd

収入データ(少数の高額所得者を含む)

income_data = np.array([300, 350, 400, 450, 500, 600, 700, 800, 1000, 10000]) # 単位: 万円

mean_income = np.mean(income_data)
median_income = np.median(income_data)

print(f”収入データ: {income_data}”)
print(f”平均収入: {mean_income:.2f}万円”) # 出力: 1510.00万円 (10000に大きく引っ張られている)
print(f”中央収入: {median_income:.2f}万円”) # 出力: 550.00万円 (より実態に近い感覚)

Pandas DataFrameでの使用

df = pd.DataFrame({‘income’: income_data})
print(f”\nPandas DataFrameでの平均収入: {df[‘income’].mean():.2f}万円”)
print(f”Pandas DataFrameでの中央収入: {df[‘income’].median():.2f}万円”)
“`

この例では、中央値が少数の極端な高額所得者によって歪められた平均値よりも、大多数の人の収入の感覚により近い値を示していることがわかります。

2. 画像処理 (メディアンフィルタ)

画像処理において、メディアンフィルタはノイズ除去、特にソルト&ペッパーノイズ(画像中のランダムなピクセルが非常に明るいまたは暗い値になるノイズ)に非常に効果的な手法です。メディアンフィルタは、画像の各ピクセルに対して、そのピクセルとその近傍のピクセル(ウィンドウと呼ばれる領域内のピクセル)の値の中央値を計算し、元のピクセルの値をその中央値で置き換えるという処理を行います。

NumPy自体には直接的なメディアンフィルタ関数はありませんが、SciPyライブラリの scipy.ndimage.median_filter などがNumPy配列を引数に取り、内部で中央値計算を利用しています。NumPyの median 関数は、このフィルターの実装において、特定のウィンドウ内の中央値を計算する核心部分として使われる可能性があります。

“`python

SciPyを使ったメディアンフィルタの概念例(実行にはscipyが必要)

import numpy as np

from scipy.ndimage import median_filter

import matplotlib.pyplot as plt

from skimage import data

# サンプル画像とノイズ生成

original_image = data.camera()

noisy_image = original_image.copy()

salt_pepper_ratio = 0.05 # 5%のピクセルにノイズ

# ノイズを追加(ランダムな位置に0または255の値を設定)

row, col = noisy_image.shape

num_pixels = row * col

num_noise_pixels = int(num_pixels * salt_pepper_ratio)

# ノイズピクセルの位置をランダムに選択

noise_row = np.random.randint(0, row, num_noise_pixels)

noise_col = np.random.randint(0, col, num_noise_pixels)

# ノイズの値をランダムに選択 (0 or 255)

noise_vals = np.random.choice([0, 255], num_noise_pixels)

# ノイズを適用

noisy_image[noise_row, noise_col] = noise_vals

# メディアンフィルタを適用 (ウィンドウサイズ 3×3)

filtered_image = median_filter(noisy_image, size=3)

# 結果の表示(matplotlibが必要)

# fig, axes = plt.subplots(1, 3, figsize=(12, 4))

# ax = axes.ravel()

# ax[0].imshow(original_image, cmap=’gray’)

# ax[0].set_title(“Original Image”)

# ax[1].imshow(noisy_image, cmap=’gray’)

# ax[1].set_title(“Noisy Image (Salt & Pepper)”)

# ax[2].imshow(filtered_image, cmap=’gray’)

# ax[2].set_title(“Filtered Image (Median Filter)”)

# for a in ax:

# a.set_axis_off()

# plt.tight_layout()

# plt.show()

“`

メディアンフィルタがソルト&ペッパーノイズに効果的なのは、ノイズピクセル(0や255)がウィンドウ内の値のリストにおいて極端な値となり、中央値計算によって無視される傾向があるためです。平均値フィルタと比較すると、メディアンフィルタはエッジをぼかす影響が少ないという利点もあります。

3. 統計モデリングと機械学習

中央値は、統計モデリングの過程でデータ分布を理解したり、特徴量を前処理したりする際に使用されます。また、回帰問題における損失関数として、平均絶対誤差 (Mean Absolute Error, MAE) は中央値回帰と密接に関連しており、外れ値に強いモデルを構築するのに役立つことがあります。

パフォーマンスに関する考察

numpy.median のパフォーマンスは、入力配列のサイズ、次元、そして overwrite_input 引数の値に依存します。

  • サイズ: 配列のサイズ N が大きくなるにつれて、中央値の計算時間は増加します。NumPyが内部で選択アルゴリズム(平均 O(N))やソートアルゴリズム(平均 O(N log N))を使用しているため、計算量は通常、サイズに対して線形または対数線形になります。ソートに比べ、選択アルゴリズムは大規模データに対してより高速な中央値計算を提供します。
  • 次元と軸: 多次元配列の場合、axis を指定すると、指定された軸に沿った多数の小さな配列に対して中央値計算が並列的に行われます。これは、単に配列全体をフラット化して計算するよりも効率的になる場合があります。計算時間とメモリ使用量は、結果の形状(指定した軸の数やサイズ)に依存します。
  • overwrite_input: 前述の通り、overwrite_input=True は入力配列のコピー作成を省略できるため、特に大規模な浮動小数点型配列に対して、計算時間とメモリ使用量を削減できる可能性があります。ただし、元のデータが変更されるリスクを理解しておく必要があります。

NumPyは、パフォーマンスクリティカルな部分をCやFortranで実装し、線形代数ライブラリ(BLASやLAPACK)などを活用することで、Python単体で実装するよりもはるかに高速な数値計算を実現しています。numpy.median もこの恩恵を受けており、多くの場合、手書きのPythonコードで中央値を計算するよりもはるかに高速です。

大規模データセットを扱う際には、overwrite_input=True の利用を検討したり、計算リソース(メモリ、CPU)を考慮に入れたりすることが重要です。また、非常にメモリが限られている環境で極めて巨大なデータを扱う場合は、逐次的に中央値を推定するアルゴリズムなど、他の手法が必要になる可能性もありますが、多くの一般的なデータ分析タスクでは numpy.median は十分なパフォーマンスを提供します。

よくある間違いとトラブルシューティング

numpy.median を使用する際に遭遇しやすい問題とその解決策をいくつか紹介します。

  • axis の指定ミス: 多次元配列で意図した軸とは異なる軸の中央値を計算してしまうことはよくあります。計算結果の形状 (.shape) を確認し、期待通りの次元になっているか常にチェックしましょう。また、keepdims=True を利用して結果の次元数を元の配列と一致させると、形状の不一致によるエラーを防ぎやすくなります。
  • NaNの扱い忘れ: 配列に NaN が含まれているにも関わらず、numpy.median をそのまま使用すると、結果が NaN になってしまいます。欠損値を無視して計算したい場合は、必ず numpy.nanmedian を使用しましょう。また、NaN が全く存在しないことが確実な場合にのみ numpy.median を使用するのが安全です。
  • overwrite_input=True の副作用: overwrite_input=True を使用したために、後続の処理で必要だった元の配列の順序が変更されてしまい、予期しない結果やエラーが発生することがあります。この引数を使用する際は、入力配列がその後不要になるか、または事前にコピーを作成していることを確認してください。
  • 空の配列: numpy.median に空の配列を渡すと、ValueError が発生します。配列が空になる可能性がある場合は、事前に配列のサイズを確認するなどのエラーハンドリングが必要です。

“`python
import numpy as np

空の配列を渡す例

empty_array = np.array([])
try:
median_of_empty = np.median(empty_array)
except ValueError as e:
print(f”エラー発生: {e}”) # 出力: エラー発生: Input array must be non-empty.
“`

  • データ型の注意: 特に整数型配列の場合、中央値が小数点を含む値になる可能性があることを理解しておきましょう。結果は通常 float 型になります。計算結果を特定の整数型に格納したい場合は、明示的な型変換が必要になることがあります(ただし、小数点以下は切り捨てまたは四捨五入されます)。

これらの注意点を理解しておくことで、numpy.median をより安全かつ効果的に使用することができます。

まとめ:numpy.median を使いこなす

本記事では、NumPyライブラリにおける中央値計算関数 numpy.median について、その基本的な使い方から詳細な引数、内部的な計算方法、データ型や欠損値の扱い、他の統計関数との比較、応用例、パフォーマンスに関する考察、そしてよくある間違いまで、約5000語にわたって詳細に解説しました。

numpy.median は、外れ値に強く、データの中心的な傾向を把握するための強力なツールです。特に、所得分布や試験の点数、センサーデータなど、外れ値が含まれやすいデータや、分布が非対称なデータを分析する際に、平均値と並んで、あるいは平均値以上に重要な代表値となり得ます。

NumPyの強力な多次元配列操作機能と組み合わせることで、データセット全体だけでなく、特定の軸に沿った部分集合の中央値を効率的に計算できます。axis 引数、keepdims 引数を適切に使い分けることで、複雑なデータ構造に対しても柔軟に対応し、その後のデータ処理や可視化にスムーズに繋げることができます。

また、overwrite_input 引数はパフォーマンス最適化のために存在しますが、元のデータを変更するという重要な副作用を伴うため、使用には十分な注意が必要です。欠損値 NaN の存在は numpy.median の結果を NaN にするため、欠損値を無視して計算したい場合は numpy.nanmedian を使用することが不可欠です。

データ分析、統計モデリング、画像処理など、様々な分野でNumPy配列を扱う際に、numpy.median は欠かせない関数の一つです。本記事で提供した詳細な情報を参考に、ぜひご自身のデータ分析タスクで numpy.median を効果的に活用してください。データの中心を知ることは、データから意味のある洞察を引き出すための第一歩であり、中央値はそのための堅牢な手段を提供してくれます。

謝辞と参考文献

本記事の執筆にあたり、NumPyの公式ドキュメントは最も重要な情報源となりました。最新かつ正確な情報を得るためには、常に公式ドキュメントを参照することをお勧めします。

データ分析や統計学の基礎に関する知識は、中央値のような統計量を適切に解釈し使用するために役立ちます。関連する書籍やオンラインリソースも合わせて学習することをお勧めします。

データは語りかけてきます。その声を聞き取るために、numpy.median のような適切なツールを使いこなす能力を磨いていきましょう。

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール