4.2. データクレンジング#

生物実験やフィールド調査で得られるデータには、天候の影響や記録ミスなどにより外れ値outlier)や欠損値missing value, N/A)が含まれていることがよくあります。こうしたデータをそのまま解析に用いると、正確で信頼性のある結果を得ることができません。そのため、データ解析を行う前には、データに含まれる異常値を除去・修正したり、欠損値を適切に補完したりする作業が必要になります。このような前処理をデータクレンジングdata cleansing)またはデータクリーニングdata cleaning)と呼びます。データクレンジングは、データの品質を高め、より正確で再現性のある分析結果を導くために欠かせない作業です。誰にも気づかれない作業。けれど、やってなければ一瞬でバレる。

データクレンジングには決まった手順があるわけではありませんが、例えば次のような作業が含まれています。データの確認、欠損値や異常値の検出と処理、不要なデータの削除、表記ゆれの修正、単位や日付表記などの整形などです。データクレンジングは、分析に先立って必ず行うべき重要な準備作業です。特に、実験やフィールド調査などから得られた実データを扱う場合には、異常値や欠損値が含まれていることが多く、データの確認・整備には十分な時間と注意が必要です。本節では、Pandas を用いて、データクレンジングを行う方法について解説していきます。

4.2.1. 欠損値処理#

4.2.1.1. MCAR#

欠損値にはいくつかの種類があります。たとえば、完全にランダムに発生する欠損値は missing completely at random (MCAR) と呼ばれます。このような場合、欠損があるサンプル(行)を単純に削除しても、分析結果に大きな偏りを与えにくいため、削除による対応が一般的です。

ここでは、Pandas を使って、欠損値を含むサンプルを削除する方法を確認していきましょう。まず、サンプルデータを作成します。

data = pd.DataFrame({'tree': ['arakashi', 'arakashi', 'arakashi', 'arakashi', 'arakashi', 'shirakashi', 'shirakashi', 'shirakashi', 'shirakashi', 'shirakashi'],
                     'weight': [1.35, 1.31, 1.28, 1.24, 1.54, 1.88, 1.70, 1.78, 1.37, 1.89],
                     'height': [1.68, np.nan, np.nan, 1.34, np.nan, 2.07, 1.97, 2.09, 1.89, 2.03],
                     'diameter': [1.21, 1.29, 1.22, 1.22, 1.36, 1.24, 1.20, np.nan, 1.09, 1.26]})
data
tree weight height diameter
0 arakashi 1.35 1.68 1.21
1 arakashi 1.31 NaN 1.29
2 arakashi 1.28 NaN 1.22
3 arakashi 1.24 1.34 1.22
4 arakashi 1.54 NaN 1.36
5 shirakashi 1.88 2.07 1.24
6 shirakashi 1.70 1.97 1.20
7 shirakashi 1.78 2.09 NaN
8 shirakashi 1.37 1.89 1.09
9 shirakashi 1.89 2.03 1.26

データフレームからサンプル(行)を削除するには dropna メソッドを利用します。オプションなしで実行すると、すべての列において 1 つでも欠損値が含まれていれば、該当する行が削除されます。

data.dropna()
tree weight height diameter
0 arakashi 1.35 1.68 1.21
3 arakashi 1.24 1.34 1.22
5 shirakashi 1.88 2.07 1.24
6 shirakashi 1.70 1.97 1.20
8 shirakashi 1.37 1.89 1.09
9 shirakashi 1.89 2.03 1.26

なお、欠損値を含む列を削除したい場合は、axis=1 または axis='columns' を指定します。

# data.dropna(axis=1)
data.dropna(axis='columns')
tree weight
0 arakashi 1.35
1 arakashi 1.31
2 arakashi 1.28
3 arakashi 1.24
4 arakashi 1.54
5 shirakashi 1.88
6 shirakashi 1.70
7 shirakashi 1.78
8 shirakashi 1.37
9 shirakashi 1.89

特定の列に欠損値がある場合のみ、該当する行を削除するには dropna メソッドに subset オプションを付けて利用します。次の例は、height 列のみに着目し、欠損値が含まれていれば、該当する行を削除するようにしています。

data.dropna(subset='height')
tree weight height diameter
0 arakashi 1.35 1.68 1.21
3 arakashi 1.24 1.34 1.22
5 shirakashi 1.88 2.07 1.24
6 shirakashi 1.70 1.97 1.20
7 shirakashi 1.78 2.09 NaN
8 shirakashi 1.37 1.89 1.09
9 shirakashi 1.89 2.03 1.26

subset オプションに複数の列を指定することもできます。この場合、リスト形式で列名を渡します。

data.dropna(subset=['weight', 'height'])
tree weight height diameter
0 arakashi 1.35 1.68 1.21
3 arakashi 1.24 1.34 1.22
5 shirakashi 1.88 2.07 1.24
6 shirakashi 1.70 1.97 1.20
7 shirakashi 1.78 2.09 NaN
8 shirakashi 1.37 1.89 1.09
9 shirakashi 1.89 2.03 1.26

欠損値への対処方法として、欠損を含むサンプルを削除するのが一般的ですが、欠損を含む行を削除すると、利用可能なデータが大幅に減ってしまう可能性があります。特に、データ数が限られている場合には慎重な対応が求められます。

なお、機械学習の分野では、欠損値を他の適切な値で補完する方法もよく用いられます。たとえば、欠損している部分を、その列の平均値で埋める方法があります。次の例では、data オブジェクトの各列の平均値を mean メソッドで計算し、その値を fillna メソッドで欠損値に埋め込んでいます。

data.fillna(data.mean(numeric_only=True))
tree weight height diameter
0 arakashi 1.35 1.680000 1.210000
1 arakashi 1.31 1.867143 1.290000
2 arakashi 1.28 1.867143 1.220000
3 arakashi 1.24 1.340000 1.220000
4 arakashi 1.54 1.867143 1.360000
5 shirakashi 1.88 2.070000 1.240000
6 shirakashi 1.70 1.970000 1.200000
7 shirakashi 1.78 2.090000 1.232222
8 shirakashi 1.37 1.890000 1.090000
9 shirakashi 1.89 2.030000 1.260000

これによって、サンプルを削除せずに、すべてのデータを活用して分析やモデル構築に利用できるようになります。なお、numeric_only=True を指定することで、tree 列のような数値以外の列に対しては平均値を計算せずに処理できます。

欠損値の補完には、単に列全体の平均値を使うだけでなく、カテゴリの種類ごとに平均値を使い分ける方法もあります。たとえば、どんぐりの木の種類がアラカシとシラカシで分かれている場合、それぞれの樹種ごとに各属性(重さ、高さ、直径)の平均値を計算し、それを使って欠損値を補完することができます。このように、グループごとの傾向を活かした補完を行うことで、欠損値の処理をより実データに即したものにすることができます。特に生物学的な違いや、地理的な傾向があるデータに対しては有効です。また、データの分布の形によって、中央値なども使われることもあります。

なお、特定の値を使った補完はあくまで簡易的な方法であり、データの分布をゆがめる可能性もあります。そのため、分析の目的やデータの性質に応じて適切な補完手法を選ぶことが大切です。そうはいうものの、そんな夢のような方法は、残念ながらありません。でも安心してください。補完できなくても、来年になればまたデータを取る機会は巡ってきます。卒業さえ気にしなければ。

4.2.1.2. MAR#

欠損値の中には、観測された他のデータに依存して生じるタイプもあります。これは missing at random (MAR) と呼ばれる欠損の形式です。たとえば、「アラカシのデータに限って高さの情報が抜けやすい」といった場合、欠損は完全にランダムではなく、他の観測済みの情報に依存していることになります。

このような場合は、他の特徴量との関係を利用して欠損を補うことで、分析結果のバイアスを抑えることが可能です。たとえば、欠損のないデータを用いて、高さ(height)を目的変数、重さ(weight)と直径(diameter)を説明変数とした予測モデルを構築し、そのモデルから得られた予測値で欠損値を補うという方法が考えられます。

このように、MAR に該当する場合には、単純な平均値による補完よりも、予測モデルを用いた補完の方がより望ましい処理となることがあります。

4.2.1.3. MNAR#

missing not at random (MNAR) とは、欠損が発生している変数の本来の値に依存して欠損が生じるタイプの欠損値です。「値が大きい(あるいは小さい)ほど記録されにくい」といったように、欠損の原因が欠損そのものの値に関連している場合を指します。たとえば、「非常に小さいどんぐりは直径の測定が困難で記録されないことがある」といった状況では、どんぐりの直径が小さいという「その値」自体が欠損の原因となっています。

このような欠損は非常に厄介で、観測されていない値に欠損の原因があるため、データから直接的に補正することが難しいという特徴があります。仮定に基づいたモデル構築や感度分析など、より高度な統計手法を用いなければ、正しく対処することは困難です。

4.2.2. 重複データ除去#

データフレームに、行の値がすべて同じサンプルが複数含まれている場合、それらの重複を取り除くことができます。これには .drop_duplicates メソッドを使います。

data = pd.DataFrame({'tree': ['arakashi', 'arakashi', 'arakashi', 'arakashi', 'arakashi',],
                     'weight': [1.88, 1.54, 1.73, 1.50, 1.54],
                     'height': [1.60, 1.50, 1.57, 1.55, 1.50],
                     'diameter': [1.44, 1.36, 1.39, 1.36, 1.36]})
data
tree weight height diameter
0 arakashi 1.88 1.60 1.44
1 arakashi 1.54 1.50 1.36
2 arakashi 1.73 1.57 1.39
3 arakashi 1.50 1.55 1.36
4 arakashi 1.54 1.50 1.36
data.drop_duplicates()
tree weight height diameter
0 arakashi 1.88 1.60 1.44
1 arakashi 1.54 1.50 1.36
2 arakashi 1.73 1.57 1.39
3 arakashi 1.50 1.55 1.36

ただし、データの性質によっては、偶然にもまったく同じ値を持つサンプルが存在することがあります。そのようなデータを除去してしまうと、大切な測定データが失われ、データの分布が変わってしまう可能性があります。そのため、重複データの除去は、データの性質を十分に理解した上で行う必要があります。

なお、重複データを除去する代わりに、どの行が重複しているかを調べたいだけの場合は、.duplicated メソッドを使います。

data.duplicated()
0    False
1    False
2    False
3    False
4     True
dtype: bool

4.2.3. データ置換#

データフレームの特定の列に含まれる値を別の値に置換することができます。これは、表記のゆれを修正したい場合に便利な方法です。以下の例では、tree 列に「なら」、「nara」、「ナラ」といった異なる表記が混在していますが、これらをすべて「nara」に統一しています。

data = pd.DataFrame({
    'tree': ['なら', 'nara', '楢', 'ナラ', 'nara'],
    'weights': [1.2, 3.2, 1.3, 1.2, 1.3],
    'heights': [10, 20, 30, 40, 50]
})

spelling_variants = {
    'なら': 'nara',
    'ナラ': 'nara',
    'nara': 'nara',
}

data['tree_norm'] = data['tree'].map(spelling_variants)
data
tree weights heights tree_norm
0 なら 1.2 10 nara
1 nara 3.2 20 nara
2 1.3 30 NaN
3 ナラ 1.2 40 nara
4 nara 1.3 50 nara

map メソッドは、辞書のキーに一致する値を対応する辞書の値に置き換えます。辞書に存在しない値は NaN になってしまうため、置換したいすべての値を辞書に含める必要があります。

一方で、辞書にない値は元のまま残したい場合は、replace メソッドを使うと便利です。

data['tree_replaced'] = data['tree'].replace(spelling_variants)
data
tree weights heights tree_norm tree_replaced
0 なら 1.2 10 nara nara
1 nara 3.2 20 nara nara
2 1.3 30 NaN
3 ナラ 1.2 40 nara nara
4 nara 1.3 50 nara nara