【NumPy】マスク配列の代替手法:NaN、ブールフィルタリング、Pandasを徹底比較

2025-06-06

NumPyには、配列の一部を「無効」または「欠損」として扱い、その部分のデータが計算に含まれないようにするための強力な機能として「マスク配列」があります。これは、データの欠損やノイズが存在する科学技術計算において非常に有用です。

マスク配列とは?

通常のNumPy配列はすべての要素が有効なデータとして扱われますが、マスク配列は通常の配列(データ配列)と、それに対応するブール値の配列(マスク)の組み合わせで構成されます。

  • マスク (Mask)
    データ配列と同じ形状を持つブール値の配列です。
    • True の場合:その位置のデータは「マスクされている」(無効、欠損)ことを意味します。
    • False の場合:その位置のデータは「マスクされていない」(有効)ことを意味します。
  • データ配列 (Data Array)
    実際の数値データが含まれる配列です。

マスク配列は通常 numpy.ma モジュールを通じて作成・操作されます。

マスク配列の作成

マスク配列を作成するにはいくつかの方法があります。

  1. import numpy.ma as ma
    import numpy as np
    
    data = np.array([1, 2, 3, 4, 5])
    mask = np.array([False, False, True, False, False]) # 3番目の要素をマスク
    masked_array = ma.array(data, mask=mask)
    print(masked_array)
    # 出力: [1 2 -- 4 5] (ここで '--' はマスクされた要素を示す)
    
  2. 特定の値をマスクする

    data = np.array([1, 2, -999, 4, 5])
    masked_array = ma.masked_values(data, -999) # -999をマスク
    print(masked_array)
    # 出力: [1 2 -- 4 5]
    
  3. 条件に基づいてマスクする

    data = np.array([10, 20, 0, 40, 50])
    masked_array = ma.masked_less_equal(data, 0) # 0以下の値をマスク
    print(masked_array)
    # 出力: [10 20 -- 40 50]
    

マスク配列の操作

マスク配列は、多くの点で通常のNumPy配列と同様に操作できますが、マスクされた要素は計算から自動的に除外されます。

  1. 算術演算

    marr = ma.array([1, 2, 3, 4, 5], mask=[False, False, True, False, False])
    print(marr + 10)
    # 出力: [11 12 -- 14 15] (マスクされた要素はそのままマスクされる)
    
  2. 統計関数
    sum(), mean(), std() などの統計関数は、マスクされた要素を無視して計算されます。

    marr = ma.array([1, 2, 3, 4, 5], mask=[False, False, True, False, False])
    print(marr.sum())
    # 出力: 12 (1+2+4+5)
    print(marr.mean())
    # 出力: 3.0 (12 / 4, 4つの有効な要素のみで平均を計算)
    
  3. スライシングとインデックス付け
    マスク配列は通常の配列と同様にスライスやインデックス付けが可能です。マスク情報も正しく引き継がれます。

    marr = ma.array([1, 2, 3, 4, 5], mask=[False, False, True, False, False])
    print(marr[1:4])
    # 出力: [2 -- 4]
    

マスク配列の利点

  • パフォーマンス
    C言語で実装されているNumPyの他の機能と同様に、マスク配列の操作も効率的に行われます。
  • コードの簡潔さ
    マスクされた要素は自動的に計算から除外されるため、手動で欠損値を処理するロジック(if文など)を記述する必要が減り、コードが簡潔になります。
  • 欠損値の扱い
    データセットに欠損値がある場合、それらをマスクすることで、NaN(Not a Number)や特定の値で埋めるよりも、より堅牢で直感的な計算が可能になります。

マスクの操作

  • マスクされたデータを通常の配列に戻す
    marr.filled(fill_value) を使うと、マスクされた要素を特定のfill_value(例えば0やNaN)で埋めて、通常のNumPy配列に戻すことができます。
  • マスクの反転
    ma.MaskedArray.harden_mask() でマスクを永久に固定したり、ma.MaskedArray.soften_mask() でマスクを解除したりできます。
  • マスクの取得
    marr.mask でマスク配列そのものを取得できます。
marr = ma.array([1, 2, 3, 4, 5], mask=[False, False, True, False, False])
print(marr.filled(0))
# 出力: [1 2 0 4 5]


NumPyのマスク配列は非常に便利ですが、通常のNumPy配列とは異なる特性を持つため、いくつかの点で混乱やエラーが生じやすいです。

データ型とマスクの不一致 (TypeError: mask and data must be of the same dtype when mask is not boolean)

エラーの内容
マスク配列を作成する際に、mask引数にブール型以外の配列を渡すと発生することがあります。

原因
numpy.ma.array()mask 引数には、ブール型の配列(True / False)を指定する必要があります。

トラブルシューティング
マスクとして使用する配列がブール型であることを確認してください。条件式からマスクを生成する場合、結果は通常ブール型になりますが、意図せず整数型などが割り当てられている可能性があります。


import numpy.ma as ma
import numpy as np

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

# 間違った例: 0/1の整数配列をマスクとして渡す
# mask_wrong = np.array([0, 0, 1, 0, 0])
# masked_array = ma.array(data, mask=mask_wrong) # TypeErrorが発生する可能性がある

# 正しい例: ブール型の配列をマスクとして渡す
mask_correct = np.array([False, False, True, False, False])
masked_array = ma.array(data, mask=mask_correct)
print(masked_array)

形状の不一致 (ValueError: Mask and data must have the same shape)

エラーの内容
データ配列とマスク配列の形状が異なる場合に発生します。

原因
マスク配列は、データ配列の各要素に対応するマスク情報を持つため、両者の形状が完全に一致している必要があります。

トラブルシューティング
データ配列とマスク配列の shape 属性を確認し、一致していることを確認します。必要であれば、reshape() やブロードキャストのルールを考慮してマスクを生成し直します。


import numpy.ma as ma
import numpy as np

data = np.array([1, 2, 3, 4, 5])
# 間違った例: 異なる形状のマスク
# mask_wrong = np.array([False, True])
# masked_array = ma.array(data, mask=mask_wrong) # ValueErrorが発生

# 正しい例: 同じ形状のマスク
mask_correct = np.array([False, False, True, False, False])
masked_array = ma.array(data, mask=mask_correct)
print(masked_array)

# 2次元配列の場合も同様
data_2d = np.array([[1, 2], [3, 4]])
mask_2d_correct = np.array([[False, True], [False, False]])
masked_array_2d = ma.array(data_2d, mask=mask_2d_correct)
print(masked_array_2d)

マスクされた値の予期せぬ変更

問題の内容
マスクされた配列の要素に値を代入しても、マスクが解除されずに値が変更されない、または意図しない挙動になることがあります。

原因
マスク配列のマスクは、hard_masksoft_mask の2つの状態を持っています。デフォルトは soft_mask で、マスクされた要素に新しい値を代入するとマスクが解除されます。しかし、harden_mask() を使用してマスクを「ハード」にした場合、代入してもマスクは解除されません。また、元のマスク配列がビューとして共有されている場合も注意が必要です。

トラブルシューティング

  • 配列がビューとして共有されている場合に、予期せぬ変更を避けるには、copy=True を指定して新しいマスク配列を作成するか、harden_mask() を使用する前に元のマスク配列をコピーすることを検討します。
  • マスクが「ハード」になっていないか確認します。必要であれば soften_mask() を使用してマスクを解除可能にします。


import numpy.ma as ma
import numpy as np

marr = ma.array([1, 2, 3, 4, 5], mask=[False, False, True, False, False])
print(f"初期状態: {marr}") # 初期状態: [1 2 -- 4 5]

# soft_mask の場合(デフォルト)
marr[2] = 99
print(f"soft_maskでの代入後: {marr}") # soft_maskでの代入後: [ 1 2 99 4 5] (マスクが解除される)

# hard_mask の場合
marr_hard = ma.array([1, 2, 3, 4, 5], mask=[False, False, True, False, False])
marr_hard.harden_mask() # マスクをハードにする
print(f"ハードマスク: {marr_hard}") # ハードマスク: [1 2 -- 4 5]

marr_hard[2] = 99
print(f"ハードマスクでの代入後: {marr_hard}") # ハードマスクでの代入後: [1 2 -- 4 5] (値が変更されない)

# マスクされた値を取得しようとすると
try:
    print(marr_hard[2])
except ma.MaskError as e:
    print(f"マスクされた要素へのアクセスエラー: {e}") # MaskedArray内のマスクされた要素にアクセスしようとするとエラー

通常のNumPy配列との混同

問題の内容
マスク配列を通常のNumPy配列として扱おうとすると、マスク情報が失われたり、期待しない計算結果になったりします。

原因
マスク配列は numpy.ndarray のサブクラスですが、その動作はマスクされた要素を考慮するように変更されています。マスク情報を意識せずに操作すると、意図しない結果につながります。

トラブルシューティング

  • マスク情報を保持したまま計算を行う場合は、numpy.ma モジュール内の関数や、マスク配列のメソッドを使用します。
  • マスクされた値を「埋める」必要がある場合は、masked_array.filled(fill_value) を使用して通常のNumPy配列に変換します。


import numpy.ma as ma
import numpy as np

marr = ma.array([1, 2, 3, 4, 5], mask=[False, False, True, False, False])

# マスクされた配列の平均
print(f"マスク配列の平均: {marr.mean()}") # マスク配列の平均: 3.0 (1, 2, 4, 5 の平均)

# 通常のNumPy配列に変換して平均を計算(マスクされた要素は含まれる)
normal_array = marr.filled(0) # マスクされた部分を0で埋める
print(f"埋めた配列の平均: {normal_array.mean()}") # 埋めた配列の平均: 3.0 (1, 2, 0, 4, 5 の平均) - この例ではたまたま同じだが、状況によって異なる

# 注意: 直接ndarrayにキャストするとマスク情報が失われる
# arr_cast = np.asarray(marr) # マスク情報が失われる
# print(arr_cast) # 出力: [1 2 3 4 5] (マスクされていたはずの3も有効な値として表示される)

ma.masked_where の条件式

問題の内容
ma.masked_where(condition, a) を使用する際、condition が期待通りにマスクを生成しない。

原因
ma.masked_where は、conditionTrue の場所をマスクします。この condition はブール値の配列である必要があり、また a と同じ形状を持つことが望ましいです。

トラブルシューティング
condition 式が意図するブール配列を生成しているか、print(condition) などで確認してください。特に、浮動小数点数の比較(== など)は誤差により予期しない結果を生むことがあるため、np.isclose() などを検討します。


import numpy.ma as ma
import numpy as np

data = np.array([1.0, 2.0, np.nan, 4.0, 5.0])

# Nan値をマスクする場合(よくあるパターン)
masked_array_nan = ma.masked_invalid(data) # np.ma.masked_invalidが便利
print(f"NaNをマスク: {masked_array_nan}")

# 特定の値(例: 2.0)をマスクする場合
# 間違った例: 浮動小数点数の直接比較は避けるべき
# masked_array_wrong = ma.masked_where(data == 2.0, data) # 浮動小数点数の比較は注意が必要
# print(f"間違った比較(2.0をマスク): {masked_array_wrong}")

# 正しい例: 特定の値をマスク(特に整数値の場合)
masked_array_value = ma.masked_values(data, 2.0)
print(f"特定の値をマスク(2.0をマスク): {masked_array_value}")

# 条件式でマスクする場合
masked_array_cond = ma.masked_where(data < 3.0, data)
print(f"条件式でマスク(3.0未満をマスク): {masked_array_cond}")

メモリ使用量

問題の内容
非常に大きな配列に対してマスク配列を作成すると、メモリ使用量が増大し、MemoryError が発生することがあります。

原因
マスク配列は、データ配列に加えて、同じ形状のブール型マスク配列を内部に保持するため、通常のNumPy配列の約2倍のメモリを消費します(データ型による)。

  • 大規模なデータ処理には、Daskなどのより高レベルのライブラリや、メモリ効率の良いデータ構造を検討します。
  • 本当にマスク配列が必要か再検討し、必要であればデータをチャンクに分割して処理するなどの工夫をします。
  • 可能であれば、より小さいデータ型(np.float32np.int16 など)を使用することを検討します。


ここでは、NumPyのマスク配列 (numpy.ma) を使った基本的な操作から、より実践的な例までをコードとともに解説します。

まず、必要なライブラリをインポートします。

import numpy as np
import numpy.ma as ma

print("--- NumPy マスク配列操作のプログラミング例 ---")

マスク配列の作成

例1-1: 既存のデータとマスクから作成 データ配列と、どの要素をマスクするかを示すブール型マスク配列を直接指定して作成します。True の位置がマスクされます。

print("\n--- 例1-1: 既存のデータとマスクから作成 ---")
data = np.array([10, 20, 30, 40, 50])
mask = np.array([False, False, True, False, False]) # 30をマスク

marr = ma.array(data, mask=mask)
print(f"元のデータ: {data}")
print(f"マスク:     {mask}")
print(f"マスク配列: {marr}")
# 出力例: マスク配列: [10 20 -- 40 50]

例1-2: 条件に基づいてマスクする (ma.masked_where) 特定の条件を満たす要素をマスクする際に使います。条件が True の場所がマスクされます。

print("\n--- 例1-2: 条件に基づいてマスクする (ma.masked_where) ---")
data = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])

# 5より大きい値をマスクする
marr_gt_5 = ma.masked_where(data > 5, data)
print(f"データ: {data}")
print(f"5より大きい値をマスク: {marr_gt_5}")
# 出力例: 5より大きい値をマスク: [1 2 3 4 5 -- -- -- -- --]

# 偶数をマスクする
marr_even = ma.masked_where(data % 2 == 0, data)
print(f"偶数をマスク: {marr_even}")
# 出力例: 偶数をマスク: [1 -- 3 -- 5 -- 7 -- 9 --]

例1-3: 特定の値をマスクする (ma.masked_values) データ内の特定の値(例えば、欠損値を示す特別なコード)をマスクする場合に便利です。

print("\n--- 例1-3: 特定の値をマスクする (ma.masked_values) ---")
data = np.array([100, 200, -999, 400, -999, 600])

# -999 をマスクする
marr_val = ma.masked_values(data, -999)
print(f"データ: {data}")
print(f"欠損値 (-999) をマスク: {marr_val}")
# 出力例: 欠損値 (-999) をマスク: [100 200 -- 400 -- 600]

例1-4: 無効な値(NaN, inf)をマスクする (ma.masked_invalid) 数値計算でよく発生する np.nan (Not a Number) や np.inf (Infinity) を簡単にマスクできます。

print("\n--- 例1-4: 無効な値(NaN, inf)をマスクする (ma.masked_invalid) ---")
data = np.array([1.0, 2.5, np.nan, 4.0, np.inf, 6.0, -np.inf])

# 無効な値(NaN, inf, -inf)をマスク
marr_inv = ma.masked_invalid(data)
print(f"データ: {data}")
print(f"無効な値をマスク: {marr_inv}")
# 出力例: 無効な値をマスク: [1.0 2.5 -- 4.0 -- 6.0 --]

マスク配列の操作

マスク配列は、マスクされた要素を無視して計算を行います。

例2-1: 算術演算 通常のNumPy配列と同様に演算が可能です。マスクされた要素は、結果の配列でもマスクされたままになります。

print("\n--- 例2-1: 算術演算 ---")
marr = ma.array([1, 2, 3, 4, 5], mask=[False, False, True, False, False])
print(f"元のマスク配列: {marr}")

result_add = marr + 10
print(f"10を足す: {result_add}") # 出力例: 10を足す: [11 12 -- 14 15]

result_mul = marr * 2
print(f"2を掛ける: {result_mul}") # 出力例: 2を掛ける: [ 2  4 --  8 10]

例2-2: 統計関数 sum(), mean(), std() などの統計関数は、マスクされた要素を除外して計算します。

print("\n--- 例2-2: 統計関数 ---")
marr = ma.array([10, 20, 30, 40, 50], mask=[False, False, True, False, False])
print(f"マスク配列: {marr}")

print(f"合計 (sum): {marr.sum()}")    # 出力例: 合計 (sum): 120 (10+20+40+50)
print(f"平均 (mean): {marr.mean()}") # 出力例: 平均 (mean): 30.0 (120 / 4)
print(f"最大値 (max): {marr.max()}") # 出力例: 最大値 (max): 50
print(f"最小値 (min): {marr.min()}") # 出力例: 最小値 (min): 10

例2-3: スライシングとインデックス付け マスク配列も通常のNumPy配列と同様にスライシングやインデックス付けができます。マスク情報は保持されます。

print("\n--- 例2-3: スライシングとインデックス付け ---")
marr = ma.array([1, 2, 3, 4, 5, 6, 7, 8], mask=[False, False, True, False, False, True, False, False])
print(f"元のマスク配列: {marr}")

# スライシング
sliced_marr = marr[2:6]
print(f"スライス ([2:6]): {sliced_marr}") # 出力例: スライス ([2:6]): [-- 4 5 --]

# 特定のインデックスにアクセス (マスクされた要素の場合)
try:
    print(f"インデックス2の値: {marr[2]}")
except ma.MaskError as e:
    print(f"インデックス2へのアクセスエラー: {e}") # マスクされた要素に直接アクセスするとMaskError
    print(f"マスクされた要素のデータ値: {marr.data[2]}") # マスクされた要素の生データにアクセスするには .data 属性を使う
    print(f"インデックス4の値: {marr[4]}") # 出力例: インデックス4の値: 5

マスクの操作

例3-1: マスクの取得と変更 .mask 属性でマスク配列自体にアクセスできます。マスクを直接変更することも可能です。

print("\n--- 例3-1: マスクの取得と変更 ---")
marr = ma.array([1, 2, 3, 4, 5], mask=[False, False, True, False, False])
print(f"元のマスク配列: {marr}")
print(f"現在のマスク: {marr.mask}")

# マスクを更新 (要素1と4を新たにマスクする)
marr.mask[[0, 3]] = True
print(f"マスク更新後: {marr}")
print(f"更新されたマスク: {marr.mask}")
# 出力例:
# マスク更新後: [-- 2 -- -- 5]
# 更新されたマスク: [ True False  True  True False]

例3-2: マスクを解除する (soften_mask, 値の代入) デフォルトでは、マスク配列の要素に値を代入すると、その要素のマスクは解除されます(soft mask)。

print("\n--- 例3-2: マスクを解除する (soften_mask, 値の代入) ---")
marr = ma.array([1, 2, 3, 4, 5], mask=[False, False, True, False, False])
print(f"元のマスク配列: {marr}")

marr[2] = 99 # マスクされていた3番目の要素に99を代入
print(f"値代入後: {marr}") # 出力例: 値代入後: [ 1  2 99  4  5] (マスクが解除された)
print(f"マスクの状態: {marr.mask}") # 出力例: マスクの状態: [False False False False False]

# harden_mask() でマスクを固定した場合
marr_hard = ma.array([1, 2, 3, 4, 5], mask=[False, False, True, False, False])
marr_hard.harden_mask() # マスクを固定する
print(f"\nハードマスクのマスク配列: {marr_hard}")
marr_hard[2] = 99 # 値を代入してもマスクは解除されない
print(f"ハードマスクに値代入後: {marr_hard}") # 出力例: ハードマスクに値代入後: [1 2 -- 4 5]

例3-3: マスクされたデータを通常のNumPy配列に戻す (filled) マスクされた要素を特定の値で埋めて、通常の numpy.ndarray オブジェクトとして取得します。

print("\n--- 例3-3: マスクされたデータを通常のNumPy配列に戻す (filled) ---")
marr = ma.array([10, 20, 30, 40, 50], mask=[False, False, True, False, False])
print(f"マスク配列: {marr}")

# マスクされた要素を0で埋める
filled_zeros = marr.filled(0)
print(f"0で埋めた配列: {filled_zeros}")
# 出力例: 0で埋めた配列: [10 20  0 40 50]
print(f"型: {type(filled_zeros)}") # 出力例: 型: <class 'numpy.ndarray'>

# マスクされた要素をNaNで埋める
filled_nan = marr.filled(np.nan)
print(f"NaNで埋めた配列: {filled_nan}")
# 出力例: NaNで埋めた配列: [10. 20. nan 40. 50.]

複数のマスク配列の操作

例4-1: マスク配列同士の演算 マスク配列同士で演算を行うと、両方の配列でマスクされている要素、あるいは一方でもマスクされている要素は結果でもマスクされます。

print("\n--- 例4-1: マスク配列同士の演算 ---")
marr1 = ma.array([1, 2, 3, 4, 5], mask=[False, False, True, False, False]) # 3がマスク
marr2 = ma.array([10, 20, 30, 40, 50], mask=[False, True, False, False, False]) # 20がマスク

print(f"marr1: {marr1}")
print(f"marr2: {marr2}")

# 加算
result_add = marr1 + marr2
print(f"marr1 + marr2: {result_add}")
# 出力例: marr1 + marr2: [11 -- -- 44 55] (3と20の場所が両方マスクされる)
# Explanation: (1+10), (2はマスク), (3はマスク), (4+40), (5+50)

# 乗算
result_mul = marr1 * marr2
print(f"marr1 * marr2: {result_mul}")
# 出力例: marr1 * marr2: [10 -- -- 160 250]

NumPyのマスク配列は、numpy.ma モジュールを通じて利用します。

import numpy as np
import numpy.ma as ma

マスク配列を作成するにはいくつかの基本的な方法があります。

a. データ配列とマスク配列から直接作成 最も基本的な方法です。データと、マスクしたい部分をTrueにしたブール配列を渡します。

print("--- 1a. データ配列とマスク配列から直接作成 ---")
data = np.array([10, 20, 30, 40, 50])
mask = np.array([False, False, True, False, True]) # 30と50をマスクする

masked_array = ma.array(data, mask=mask)
print("元のデータ:", data)
print("マスク配列:", masked_array)
# 出力例: [10 20 -- 40 --]
print("マスク部分:", masked_array.mask)
# 出力例: [False False  True False  True]
print("-" * 30)

b. 特定の値をマスクする ma.masked_values() を使用すると、配列内の特定の値を自動的にマスクできます。

print("--- 1b. 特定の値をマスクする ---")
data_with_sentinel = np.array([1, 2, -999, 4, -999, 6])
masked_values_array = ma.masked_values(data_with_sentinel, -999)
print("元のデータ (欠損値: -999):", data_with_sentinel)
print("マスクされた配列:", masked_values_array)
# 出力例: [1 2 -- 4 -- 6]
print("-" * 30)

c. 条件に基づいてマスクする (ma.masked_where) 特定の条件を満たす要素をマスクする際に非常に便利です。条件がTrueの要素がマスクされます。

print("--- 1c. 条件に基づいてマスクする (ma.masked_where) ---")
temperatures = np.array([25, 28, 15, 30, 10, 22])
# 20度未満の温度をマスクする
cold_temperatures = ma.masked_where(temperatures < 20, temperatures)
print("元の温度データ:", temperatures)
print("20度未満をマスク:", cold_temperatures)
# 出力例: [25 28 -- 30 -- 22]

# 2次元配列の例
data_2d = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
mask_2d = ma.masked_where(data_2d % 2 == 0, data_2d) # 偶数をマスク
print("\n2次元配列の偶数をマスク:")
print(data_2d)
print(mask_2d)
# 出力例:
# [[-- 2 --]
#  [4 -- 6]
#  [-- 8 --]]
print("-" * 30)

d. 無効な値(NaN, Inf)をマスクする (ma.masked_invalid) 浮動小数点数演算で発生しやすい np.nan (Not a Number) や np.inf (Infinity) を自動的にマスクできます。

print("--- 1d. 無効な値(NaN, Inf)をマスクする (ma.masked_invalid) ---")
sensor_readings = np.array([1.5, 2.3, np.nan, 4.1, np.inf, 0.8])
masked_invalid_array = ma.masked_invalid(sensor_readings)
print("元のセンサーデータ:", sensor_readings)
print("無効値をマスク:", masked_invalid_array)
# 出力例: [1.5 2.3 -- 4.1 -- 0.8]
print("-" * 30)

マスク配列は、通常のNumPy配列と同様に多くの操作ができますが、マスクされた要素は自動的に計算から除外されます。

a. 算術演算

print("--- 2a. 算術演算 ---")
marr1 = ma.array([1, 2, 3, 4, 5], mask=[False, False, True, False, False])
marr2 = ma.array([6, 7, 8, 9, 10], mask=[True, False, False, False, True])

print("marr1:", marr1)
print("marr2:", marr2)

# 足し算
print("marr1 + marr2:", marr1 + marr2)
# 出力例: [-- 9 -- 13 --] (マスクされた要素は結果もマスクされる)

# スカラーとの乗算
print("marr1 * 2:", marr1 * 2)
# 出力例: [ 2  4 --  8 10]
print("-" * 30)

b. 統計関数 sum(), mean(), min(), max(), std() などの統計関数は、マスクされた要素を無視して計算します。

print("--- 2b. 統計関数 ---")
data_for_stats = np.array([10, 20, 30, 40, 50, 60])
mask_for_stats = np.array([False, True, False, True, False, False]) # 20と40をマスク
marr_stats = ma.array(data_for_stats, mask=mask_for_stats)
print("統計計算用配列:", marr_stats)
# 出力例: [10 -- 30 -- 50 60]

print("有効な要素の合計:", marr_stats.sum())
# 出力例: 150.0 (10 + 30 + 50 + 60)
print("有効な要素の平均:", marr_stats.mean())
# 出力例: 37.5 (150 / 4)
print("有効な要素の最小値:", marr_stats.min())
# 出力例: 10
print("有効な要素の最大値:", marr_stats.max())
# 出力例: 60
print("-" * 30)

c. マスクの変更 既存のマスク配列のマスクを後から変更することも可能です。

print("--- 2c. マスクの変更 ---")
modifiable_marr = ma.array([1, 2, 3, 4, 5])
print("初期状態:", modifiable_marr) # [1 2 3 4 5]

# 2番目の要素をマスクする
modifiable_marr[1] = ma.masked
print("2番目をマスク:", modifiable_marr) # [1 -- 3 4 5]

# 全ての要素をマスクする
modifiable_marr.mask = True
print("全てをマスク:", modifiable_marr) # [-- -- -- -- --]

# 全てのマスクを解除する
modifiable_marr.mask = ma.nomask # または False
print("全てのマスクを解除:", modifiable_marr) # [1 2 3 4 5]

# 特定の要素のマスクを解除する (ソフトマスクの場合)
marr_soft = ma.array([1, 2, 3, 4, 5], mask=[True, True, True, True, True])
print("ソフトマスク初期:", marr_soft) # [-- -- -- -- --]
marr_soft[2] = 99 # マスクされた要素に代入すると、マスクが解除される (soft_maskの挙動)
print("ソフトマスクで代入後:", marr_soft) # [-- -- 99 -- --]
print("-" * 30)

d. マスクされた配列からデータを取得する マスクされたデータを除外した有効なデータのみを取得したり、マスクされた部分を特定の値で埋めたりできます。

print("--- 2d. マスクされた配列からデータを取得する ---")
result_marr = ma.array([10, 20, 30, 40, 50], mask=[False, True, False, False, True])
print("元のマスク配列:", result_marr) # [10 -- 30 40 --]

# マスクされていない有効なデータのみを取得
compressed_data = result_marr.compressed()
print("圧縮されたデータ (マスクされていない値):", compressed_data)
# 出力例: [10 30 40]

# マスクされた要素を特定の値で埋めて通常のNumPy配列として取得
filled_data_zero = result_marr.filled(0)
print("0で埋めたデータ:", filled_data_zero)
# 出力例: [10 0 30 40 0]

filled_data_nan = result_marr.filled(np.nan)
print("NaNで埋めたデータ:", filled_data_nan)
# 出力例: [10. 20. nan 40. nan] (fill_valueが元のデータ型と互換性がない場合、データ型が変更されることがある)
print("-" * 30)


NumPyのマスク配列 (Masked Array) は欠損データを扱う強力な方法ですが、状況によっては他のアプローチの方が適している場合もあります。ここでは、マスク配列の代替となる一般的なプログラミング方法をいくつかご紹介します。

NaN (Not a Number) を使用する

NumPyの浮動小数点数型では、欠損値を表す標準的な方法として np.nan (Not a Number) があります。多くのNumPy関数はNaNを適切に扱い、計算から除外したり、結果をNaNにしたりします。

メリット

  • データ型を統一できるため、マスク配列のように追加のマスク配列を保持する必要がない。
  • NumPyの多くの集計関数(例: np.nansum, np.nanmean)がNaNを自動的にスキップしてくれる。
  • 浮動小数点数データに普遍的に適用できる。

デメリット

  • 一部の操作ではNaNが伝播し、結果全体がNaNになることがある。
  • 欠損値がどのような理由で発生したのかというメタ情報を保持できない。
  • 整数型の配列には直接適用できない(NaNは浮動小数点数)。

コード例

import numpy as np

print("--- 1. NaN (Not a Number) を使用する ---")
data_with_nan = np.array([10., 20., np.nan, 40., np.nan, 60.])
print("元のデータ (NaNを含む):", data_with_nan)

# NaNを無視して合計を計算
print("NaNを無視した合計 (np.nansum):", np.nansum(data_with_nan))
# 出力例: 130.0 (10 + 20 + 40 + 60)

# NaNを無視して平均を計算
print("NaNを無視した平均 (np.nanmean):", np.nanmean(data_with_nan))
# 出力例: 32.5 (130 / 4)

# NaNを含む配列での通常の計算(NaNが伝播する例)
print("通常の合計 (np.sum) - NaNが伝播:", np.sum(data_with_nan))
# 出力例: nan
print("-" * 30)

ブールインデックス配列でデータをフィルタリングする

マスク配列と似ていますが、計算のたびに明示的にブールインデックス配列を作成し、有効なデータのみを抽出して計算を行う方法です。

メリット

  • データが非常に大きく、マスク配列のメモリオーバーヘッドを避けたい場合に有効。
  • マスク配列のような特別なオブジェクトを扱う必要がないため、概念的にシンプル。
  • 非常に柔軟で、任意の条件に基づいてデータをフィルタリングできる。

デメリット

  • 複数の異なるマスク条件で繰り返し操作を行う場合、コードが冗長になる可能性がある。
  • 元の配列のインデックス情報が失われる場合がある。
  • 操作ごとにフィルタリングのステップを明示的に記述する必要がある。

コード例

import numpy as np

print("--- 2. ブールインデックス配列でデータをフィルタリングする ---")
data_original = np.array([10, 20, -1, 40, -1, 60]) # -1を欠損値として扱う

# 有効なデータを示すブールマスクを作成
valid_mask = data_original != -1
print("有効なデータのマスク:", valid_mask)
# 出力例: [ True  True False  True False  True]

# マスクを使って有効なデータのみを抽出
valid_data = data_original[valid_mask]
print("有効なデータ:", valid_data)
# 出力例: [10 20 40 60]

# 有効なデータで計算を行う
print("有効なデータの合計:", np.sum(valid_data))
print("有効なデータの平均:", np.mean(valid_data))

# 欠損値に特定の値を埋め込みたい場合
filled_data = np.where(data_original == -1, 0, data_original)
print("0で欠損値を埋めたデータ:", filled_data)
# 出力例: [10 20  0 40  0 60]
print("-" * 30)

Pandas DataFrame を使用する

Pythonでデータ分析を行う場合、PandasライブラリはNumPyの上に構築されており、欠損値(NaN)を扱うための高レベルな機能を提供します。

メリット

  • マスク配列のような概念を明示的に意識する必要がないことが多い。
  • 時系列データや異種混合データなど、より複雑なデータセットの分析に適している。
  • データにラベル(列名、インデックス)を付けることができ、データの管理と理解が容易。
  • NaNが標準的な欠損値として扱われ、欠損値処理のための豊富なメソッド(dropna(), fillna()など)がある。

デメリット

  • 大規模な数値計算の純粋なパフォーマンスでは、NumPyの低レベル操作に若干劣る可能性がある(ただし、ほとんどの用途では十分高速)。
  • NumPyのみを使用する場合に比べて、依存関係が増える。
import pandas as pd
import numpy as np

print("--- 3. Pandas DataFrame を使用する ---")
data_pandas = {'A': [10, 20, np.nan, 40, 50],
               'B': [1, np.nan, 3, 4, 5]}
df = pd.DataFrame(data_pandas)
print("元のDataFrame (NaNを含む):\n", df)

# NaNを含む行を削除
df_dropped = df.dropna()
print("\nNaNを含む行を削除したDataFrame:\n", df_dropped)

# NaNを0で埋める
df_filled = df.fillna(0)
print("\nNaNを0で埋めたDataFrame:\n", df_filled)

# 特定の列の平均を計算 (NaNを自動で無視)
print("\n列 'A' の平均:", df['A'].mean())
print("列 'B' の合計:", df['B'].sum())
print("-" * 30)

どの方法を選ぶべきか?

  • マスクされているという状態そのものに意味があり、欠損値の存在を明示的に伝えたい場合NumPyのマスク配列が適しています。
  • 欠損値処理を頻繁に行う、より複雑なデータセットを扱う場合、またはデータにラベル付けしたい場合Pandas DataFrameが最も効率的で高機能な選択肢となるでしょう。
  • 特定の条件(例: 値が-999の場合)でデータを「無効」として扱いたいが、NaNを使いたくない場合ブールインデックス配列によるフィルタリングが柔軟で直接的です。
  • 簡単な数値計算でNaNが自然に発生する場合NaN を直接使用し、np.nansum などのnan対応関数を使うのが最もシンプルです。