Pandasでデータを処理する際には避けては通れない欠損値。その概要と欠損値かどうかの判定方法、欠損値が行や列に含まれているかを確認する方法、それら数をカウントする方法、欠損値を含む行や列を削除したり置き換えたりする方法を紹介します。
本シリーズ「Pythonデータ処理入門」は、Pythonの基礎をマスターした人を対象に以下のような、Pythonを使ってデータを処理しようというときに便利に使えるツールやライブラリ、フレームワークの使い方の基礎を説明するものです。
なお、本連載では以下のバージョンを使用しています。
前回はpandasのDataFrameオブジェクトをNumPyの多次元配列やPythonのリスト、辞書、JSON形式のオブジェクトに変換する方法を紹介しました。今回は、pandasでデータを処理する際には必ずと言っていいほど目にする「欠損値」とその扱いについて見ていきます。
欠損値とは、取得したデータに含まれる「何らかの理由で存在していないデータ」のことです。何かのデータを計測しているときに本来は得られるはずの値がセンサーの誤動作により1つだけ取得できなかったとか、データの伝送時にノイズが入ったためにデータがきちんと読み取れなかったとか、理由はいろいろと考えられますが、あるべきデータがないという状況はよくあり、データを処理する際にはそれらについても考慮する必要があります。
ここでは簡単な例として、次のコードでCSVファイルを作成し、それをDataFrameオブジェクトに読み込んで、欠損値の基本的な扱いを見てみます。
l = [['0', '1', '2'], ['3', '', '5'], ['', '7', '8']]
with open('test.csv', 'w') as f:
for row in l:
f.write(','.join(row) + '\n')
with open('test.csv') as f:
print(f.read())
# 出力結果:
#0,1,2
#3,,5
#,7,8
出力結果を見ると、第1行の第1列(カンマ「,」が連続している部分)と第2行の第0列(「,7」とあるカンマの左側)にはデータが存在していないようです。このCSVファイルをpandasのread_csv関数で読み込んでみるとどうなるでしょうか。
import pandas as pd
df = pd.read_csv('test.csv', header=None) # ヘッダー行がないのでheader=Noneを指定
print(df)
# 出力結果:
# 0 1 2
#0 0.0 1.0 2
#1 3.0 NaN 5
#2 NaN 7.0 8
上で述べた通り、第1行の第1列と第2行の第0列には「NaN」と表示されました。「NaN」とは「Not a Number」(非数)を表す表記のことで、DataFrameオブジェクトの要素としてこれが現れた場合、一般的にはそこにはデータが存在していないことを示すマーカーとして機能します。ここで作成したDataFrameオブジェクトの概要をinfoメソッドで調べてみましょう。
df.info()
# 出力結果:
#<class 'pandas.core.frame.DataFrame'>
#RangeIndex: 3 entries, 0 to 2
#Data columns (total 3 columns):
# # Column Non-Null Count Dtype
#--- ------ -------------- -----
# 0 0 2 non-null float64
# 1 1 2 non-null float64
# 2 2 3 non-null int64
#dtypes: float64(2), int64(1)
#memory usage: 204.0 bytes
上記の出力結果の「Non-Null Count」という部分(強調書体で表示)を見ると、NaN値を含んでいる第0列と第1列は非Null値の数が「2」になっています。このことからも、これらの列には欠損値が含まれていることが確認できます。
ここでNaN値のデータ型を調べてみましょう。
print(type(df.iloc[1, 1])) # <class 'numpy.float64'>
NumPyが提供するnumpy.float64型であることが分かりました。しかし、Pythonでは「float('nan')」などとすることでもNaN値を求められます。実際のところ、pandasでは以下を欠損値として扱えます。
最初の2つについては既に述べた通りです。また、pandas.NAの「NA」は「Not Available」(使えない)を意味していると思われます。pandasが標準で提供しているという点では、これを欠損値として使うのが一番適切かもしれません。次のpandas.NaTの「NaT」は「Not a Time」つまり「時間や日付ではない」ことを意味し、日付や時刻を表すデータにおける欠損値として使います。最後にPythonに組み込みのNone値も欠損値として扱えます。
実際に試してみましょう。pandasにはisna関数とisnull関数があります。これらの関数にスカラー値(単一の値)やSeriesオブジェクト、DataFrameオブジェクトを渡すと、その値または一次元配列や二次元配列の要素が欠損値かどうかを判断できます。
pandas.isna(obj)
pandas.isnull(obj)
引数として与えたobj(スカラー値)またはobj(配列)の要素が欠損値であるかどうかを判定する。なお、pandas.isnull関数はpandas.isna関数の別名である(「pd.isna is pd.isnull」の結果がTrueとなる)。
では、上で紹介した欠損値として扱えるオブジェクトと空文字列、文字列の'NaN'、それから偽値であるFalseをここではpandas.isna関数に渡してみましょう。
import numpy as np
print(pd.isna(float('nan'))) # True
print(pd.isna(np.nan)) # True
print(pd.isna(pd.NA)) # True
print(pd.isna(pd.NaT)) # True
print(pd.isna(None)) # True
print(pd.isna('')) # False
print(pd.isna('NaN')) # False
print(pd.isna(False)) # False
すると、上で述べた値についてはisna関数の戻り値はTrueになりますが、NaN値を文字列として表現した'NaN'や、条件式では偽値として取り扱われる空文字列やFalseを渡したときにはその戻り値はFalseになりました。後者の3つは欠損値ではないことには注意してください(同様にfloat('inf')で生成できるinf値も欠損値としては扱われません)。
なお、if文などの条件式で何かの値が欠損値であるかどうかを調べるときには==演算子などを使ってもうまく比較できません。
x = np.nan
if x == pd.NA: # TypeError: boolean value of NA is ambiguous
print('x == pd.NA')
else:
print('x != pd.NA')
if x == np.nan: # TypeError: boolean value of NA is ambiguous
print('x == np.nan')
else:
print('x != np.nan')
# 出力結果:
# x != np.nan
最初の例ではTypeError例外が発生しています。次の例ではnp.nanとnp.nanを==演算子で比較しているのにelse節が実行されています。欠損値かどうかの判定にはpd.isna関数などを使うようにしましょう。
isna関数とは逆の振る舞いをするpandas.notna関数とpandas.notnull関数もあります(後者は前者の別名)。
pandas.notna(obj)
pandas.notnull(obj)
引数として与えたobj(スカラー値)またはobj(配列)の要素が欠損値でないかどうかを判定する。スカラー値が欠損値でなければTrueが返され、配列については欠損値に対応する要素の値がFalseで、それ以外の要素の値がTrueなDataFrameオブジェクトが返される。
以下に使用例を示します。
print(df)
# 出力結果:
# 0 1 2
#0 0.0 1.0 2
#1 3.0 NaN 5
#2 NaN 7.0 8
result = pd.notna(df)
print(result)
# 出力結果:
# 0 1 2
#0 True True True
#1 True False True
#2 False True True
ここでは先ほど作成したDataFrameオブジェクトをpandas.notna関数に渡しています。そのため、NaN値が格納されている要素に対応する第1行第1列と第2行第0列の要素がFalse、それら以外の要素がTrueとなるDataFrameオブジェクトが返されました。
ここまではpandasモジュールが提供するisna関数などを見てきましたが、DataFrameオブジェクト(およびSeriesオブジェクト)には同様な処理を行うisnaメソッド/isnullメソッドがあります(以下ではDataFrameオブジェクトのメソッドを紹介します)。
pandas.DataFrame.isna()
pandas.DataFrame.isnull()
メソッドの呼び出しに使用したDataFrameオブジェクトの要素と同じ形状のDataFrameオブジェクトを返す。ただし、その要素の値は全てブーリアン値であり、元のDataFrameオブジェクトの要素のうち欠損値と判定されたものに対応する要素の値はTrue、それ以外の要素の値はFalseとなる。pandas.DataFrame.isnullメソッドはpandas.DataFrame.isnaメソッドの別名。
以下に使用例を示します。先ほどと同様、最初に作成したDataFrameオブジェクトを使って、欠損値があるかどうかを調べてみましょう。
print(df)
# 出力結果:
# 0 1 2
#0 0.0 1.0 2
#1 3.0 NaN 5
#2 NaN 7.0 8
result = df.isna()
print(result)
# 出力結果:
# 0 1 2
#0 False False False
#1 False True False
#2 True False False
ご覧の通り、NaN値が格納されている第1行第1列と第2行第0列の要素がTrueになり、それら以外はFalseとなりました。
もちろん、pandas.notna関数やpandas.notnull関数に対応するpandas.DataFrame.notnaメソッドやpandas.DataFrame.notnullメソッドもあります。以下に例を示します。
result = df.notna()
print(result)
# 出力結果:
# 0 1 2
#0 True True True
#1 True False True
#2 False True True
DataFrameオブジェクトにはallメソッドとanyメソッドがあります。これらはPythonに組み込みのall関数やany関数と似た振る舞いをします。
print(all([0, 1, 2])) # False
print(any([0, 1, 2])) # True
all関数は与えられた反復可能オブジェクトの要素が全て真ならTrueを、そうでなければFalseを返し、any関数は与えられた反復可能オブジェクトの要素が1つでも真ならTrueを、そうでなければFalseを返します。allメソッドとanyメソッドはこれをDataFrameオブジェクト(2次元の配列)に拡張したものだと考えられます。よって、呼び出し時には行方向に見るか列方向に見るか、その軸を指定する必要がある点には注意が必要です(デフォルトでは各列の要素が全て真かどうか、あるいはいずれかが真かどうかを判定します)。
pandas.DataFrame.all(*, axis=0)
pandas.DataFrame.any(*, axis=0)
allメソッドは、指定された軸に沿って、その列または行の要素が全て真であればTrueを、そうでなければFalseを格納するSeriesオブジェクトを返す。anyメソッドは、指定された軸に沿って、その列または行のいずれかの要素が真であればTrueを、全ての要素がFalseであればFalseを格納するSeriesオブジェクトを返す。以下ではaxisパラメーターだけを紹介する。全パラメーターについてはpandasのドキュメント「pandas.DataFrame.all」および「pandas.DataFrame.any」を参照のこと。
allメソッドの簡単な使い方の例を以下に示します。
test = pd.DataFrame([[0, 1, 2], [0, 4, 5], [6, 7, 8]])
test.columns = ['A', 'B', 'C']
test.index = ['X', 'Y', 'Z']
result = test.all()
print(result)
# 出力結果:
#A False
#B True
#C True
#dtype: bool
result = test.all(axis=1)
print(result)
# 出力結果:
#X False
#Y False
#Z True
#dtype: bool
ここでは第0行('X'行)と第1行('Y'行)の第0列('A'列)が0となっている(その他の要素は0以外の)DataFrameオブジェクトを作成しています。そして、それに対してallメソッドを呼び出しています。最初の「test.all()」では軸を指定していないので列方向に各要素を調べるので、第0列('A'列)については全ての要素が真とはいえません(Pythonでは0は偽)。よって、戻り値であるSeriesオブジェクトでは、この列に関していえば結果はFalseで、他の列については結果はTrueになります。axis=1を指定した場合は同様なことを行方向に調べます。そのため、第0行('X'行)と第1行('Y'行)についての結果を格納する要素の値はFalseに、第2行('Z'行)に対応する要素の値はTrueになります。
anyメソッドも同様なので、ここでは例は省略します。
ここで重要なのは、上で見たisna/isnull/notna/notnullなどの関数/メソッドは真偽値を含むDataFrameオブジェクトを返すことです。つまり、これらの関数/メソッドの戻り値に対してallメソッドを使えば、特定の行や列に欠損値が含まれているかどうかを確認できるということです。
df.columns = ['col0', 'col1', 'col2']
df.index = ['row0', 'row1', 'row2']
print(df)
# 出力結果:
# col0 col1 col2
#row0 0.0 1.0 2
#row1 3.0 NaN 5
#row2 NaN 7.0 8
test = df.notna()
print(test)
# 出力結果:
# col0 col1 col2
#row0 True True True
#row1 True False True
#row2 False True True
result = test.all() # result = df.notna().all()
print(result)
# 出力結果:
#col0 False
#col1 False
#col2 True
#dtype: bool
この例ではDataFrame.notnaメソッドを呼び出して、欠損値ではない要素についてはTrue、欠損値である要素についてはFalseとなるようなDataFrameオブジェクトを得た後、allメソッドを呼び出しています。結果、欠損値を全く含んでいない'col2'列に対応する要素がTrueとなるSeriesオブジェクトが得られました。
逆に欠損値を含んでいる列に対応する要素がTrueとなるようなSeriesオブジェクトが欲しければ次のようなコードになるでしょう。
test = df.isna()
print(test)
# 出力結果:
# col0 col1 col2
#row0 False False False
#row1 False True False
#row2 True False False
result = test.any() # result = df.isna().any()
print(result)
# 出力結果:
#col0 True
#col1 True
#col2 False
#dtype: bool
上で見たように、allメソッドやanyメソッドは行や列に欠損値があるかどうかを調べるのに使えますが、その数を知りたいときにはDataFrameオブジェクトのsumメソッドを使えます。
pandas.DataFrame.sum(axis=0)
指定された軸に沿って、行または列の全要素の値を合計した結果を格納するSeriesオブジェクトを返す。以下ではaxisパラメーターのみを紹介する。全てのパラメーターについてはpandasのドキュメント「pandas.DataFrame.sum」を参照のこと。
以下に使用例を示します。
test = pd.DataFrame([[0, 1, 2], [3, 4, 5], [6, 7, 8]])
test.columns = ['A', 'B', 'C']
test.index = ['X', 'Y', 'Z']
result = test.sum()
print(result)
# 出力結果:
#A 9
#B 12
#C 15
#dtype: int64
ここでは3行3列のDataFrameオブジェクトを作成し、各列の要素を合計した値を要素とするSeriesオブジェクトを求めています。
そして、Pythonにおける真偽値が0と1として扱えることを使い、isnaなどのメソッドで元のDataFrameオブジェクトの要素の値を基に真偽値を格納するDataFrameオブジェクトを作成して、sumメソッドを呼び出すことで、欠損値がある行や列の数をカウントします。以下はその例です。
print(df)
# 出力結果:
# col0 col1 col2
#row0 0.0 1.0 2
#row1 3.0 NaN 5
#row2 NaN 7.0 8
result = df.isna()
print(result)
# 出力結果:
# col0 col1 col2
#row0 False False False
#row1 False True False
#row2 True False False
cnt = result.sum() # cnt = df.isna().sum()
print(cnt)
# 出力結果:
#col0 1
#col1 1
#col2 0
#dtype: int64
ここで使用しているDataFrameオブジェクトは第0行と第1列にNaN値が含まれています(最初の出力結果を参照)。このDataFrameオブジェクトでisnaメソッドを呼び出すと、対応する要素がTrueとなるDataFrameオブジェクトが得られます。後はこれに対してsumメソッドを呼び出すだけです。ここでは軸の指定を省略しているので列方向に合計が計算され、その結果、第0列('col0'列)と第1列('col1'列)の合計が1となります。これが意味するのは、これらの列には欠損値を含む行が1つずつ存在するということです。
もちろん、これは最初の方で見たinfoメソッドでも調べられます。が、infoメソッドはコンソールやノートブックに概要を出力するためのものです。得られた行数や列数を使って何らかの条件分岐や処理を行いたいのであれば、こちらのやり方が便利かもしれませんね。
ここまでは欠損値があるかどうかの判定や、その行数や列数をカウントする方法などについて見てきました。しかし、実際に欠損値がDataFrameオブジェクトに含まれていたとしたらどうすればよいのでしょう。
実は欠損値といってもMCAR(Missing Completely At Random)やMAR(Missing At Random)、MNAR(Missing Not At Random)のように幾つかの種類があります。ただし、これらの種類については、実際にデータ処理を行ってみる段階でまたお話しすることにしましょう。いずれにせよ、pandasで欠損値を含む行や列を扱うときには次のような方法を採ることになるでしょう。
欠損値を含む行や列(主に行でしょう)を削除するのに使えるのがDataFrameオブジェクトのdropnaメソッドです。
pandas.DataFrame.dropna(*, axis=0, how='any', ignore_index=False)
欠損値を(含む行や列を)削除する。以下では一部のパラメーターだけを紹介する。全てのパラメーターについてはpandasのドキュメント「pandas.DataFrame.dropna」を参照のこと。
ここではサンプルとして以下のようなDataFrameオブジェクトを作成しましょう。
kawasaki = {'name': 'kawasaki', 'age': 98, 'height': 162, 'weight': np.nan}
isshiki = {'name': 'isshiki', 'age': 45, 'height': 168, 'weight': 65}
endo = {'name': 'endo', 'age': 54, 'height': 175, 'weight': 70}
ogawa = {'name': 'ogawa', 'age': np.nan, 'height': 180, 'weight': 75}
df = pd.DataFrame([kawasaki, isshiki, endo, ogawa])
print(df)
# 出力結果:
# name age height weight
#0 kawasaki 98.0 162 NaN
#1 isshiki 45.0 168 65.0
#2 endo 54.0 175 70.0
#3 ogawa NaN 180 75.0
簡単なdropnaメソッドの使用例を以下に示します。
result = df.dropna()
print(result)
# 出力結果:
# name age height weight
#1 isshiki 45.0 168 65.0
#2 endo 54.0 175 70.0
result = df.dropna(axis=1)
print(result)
# 出力結果:
# name height
#0 kawasaki 162
#1 isshiki 168
#2 endo 175
#3 ogawa 180
最初の例では軸を指定していません。そのため、欠損値を含む2つの行(インデックス0と3)が削除されました。次の例ではaxisパラメーターに1を指定しているので、今度は欠損値を含む2つの列(列ラベルが'age'と'weight')が削除されています。
上記のデータは列には1件のデータが含むさまざまなデータが並べられ、行が1件のデータを表すようになっています。このようなときに、列を削除することはあまりないでしょう(行と列が入れ替わっている場合には、axisパラメーターの指定が必要になるでしょう)。
howパラメーターには行または列の全ての要素が欠損値であるときに削除するか、それともどれか1つでも欠損があれば削除するか('any')を指定します。省略すると'any'が指定されたものと見なされます。以下に例を示します。
tmp_df = pd.DataFrame({'name': np.nan, 'age': np.nan,
'height': np.nan, 'weight': np.nan}, index=[4])
tmp = pd.concat([df, tmp_df])
print(tmp)
# 出力結果:
# name age height weight
#0 kawasaki 98.0 162.0 NaN
#1 isshiki 45.0 168.0 65.0
#2 endo 54.0 175.0 70.0
#3 ogawa NaN 180.0 75.0
#4 NaN NaN NaN NaN
result = tmp.dropna(how='all')
print(result)
# 出力結果:
# name age height weight
#0 kawasaki 98.0 162.0 NaN
#1 isshiki 45.0 168.0 65.0
#2 endo 54.0 175.0 70.0
#3 ogawa NaN 180.0 75.0
result = tmp.dropna(how='any')
print(result)
# 出力結果:
# name age height weight
#1 isshiki 45.0 168.0 65.0
#2 endo 54.0 175.0 70.0
ここではhowパラメーターに'all'を指定したときの振る舞いを確認するために、全ての値が欠損している行を元のDataFrameオブジェクトに追加したものを一時的に使用しています。「result = tmp.dropna(how='all')」行を実行することで、その行が削除されたことを確認してください。また、「how='any'」を指定した場合は、このパラメーターを指定しなかったときと同じ結果になっています。
最後のignore_indexパラメーターにTrueを指定した場合の例は省略します(予想通りの結果になるはずです)。
上で見たdropnaメソッドは欠損値を含む行や列を削除するものでした。欠損値を別の値で補完するには、DataFrameオブジェクトのfillnaメソッドを使います。
pandas.DataFrame.fillna(value=None)
欠損値を別の値に置き換える。以下ではvalueパラメーターのみを紹介する。全パラメーターについてはpandasのドキュメント「pandas.DataFrame.fillna」を参照のこと。
ここでも先ほどと同様なDataFrameオブジェクトを作成して、それを使ってfillnaメソッドを試してみましょう。
kawasaki = {'name': 'kawasaki', 'age': 98, 'height': 162, 'weight': np.nan}
isshiki = {'name': 'isshiki', 'age': 45, 'height': 168, 'weight': 65}
endo = {'name': 'endo', 'age': 54, 'height': 175, 'weight': 70}
ogawa = {'name': 'ogawa', 'age': np.nan, 'height': 180, 'weight': 75}
df = pd.DataFrame([kawasaki, isshiki, endo, ogawa])
print(df)
# 出力結果:
# name age height weight
#0 kawasaki 98.0 162 NaN
#1 isshiki 45.0 168 65.0
#2 endo 54.0 175 70.0
#3 ogawa NaN 180 75.0
まずはvalueパラメーターにスカラー値を指定する例です。
result = df.fillna(-1)
print(result)
# 出力結果:
# name age height weight
#0 kawasaki 98.0 162 -1.0
#1 isshiki 45.0 168 65.0
#2 endo 54.0 175 70.0
#3 ogawa -1.0 180 75.0
valueパラメーターに-1を指定したので、2つの欠損値がその値に置き換えられています(浮動小数点数値に変換されている点にも注意)。ただし、これは欠損値があったことのマーカーとしては機能するでしょうが、実際にデータを分析する際には明らかに場違いな値になりそうです。
そこで、 'age'列についてはその列の全要素の平均値を、'weight'列についても全要素の平均値を計算して、それらを使って置き換えてみましょう。このとき、valueパラメーターには「{'age': xxx, 'weight': yyy}」のように置き換える列のラベルと置き換え後の値を辞書として渡すのが簡単です。
age_mean = df['age'].mean()
print(age_mean) # 65.66666666666667
weight_mean = df['weight'].mean()
print(weight_mean) # 70.0
result = df.fillna({'age': age_mean, 'weight': weight_mean})
print(result)
# 出力結果:
# name age height weight
#0 kawasaki 98.000000 162 70.0
#1 isshiki 45.000000 168 65.0
#2 endo 54.000000 175 70.0
#3 ogawa 65.666667 180 75.0
それらしいデータになったように見えます。が、これはあくまでも平均値で穴埋めをしただけで、実際のデータとは乖離(かいり)している可能性もあります。この点には注意が必要です。もっとよい置き換え方法があれば、それを検討すべきでしょう。
最後にDataFrameオブジェクトをvalueパラメーターに渡す例です。
shape = df.shape
tmp = pd.DataFrame(np.zeros(shape), columns=df.columns)
print(tmp)
# 出力結果:
# name age height weight
#0 0.0 0.0 0.0 0.0
#1 0.0 0.0 0.0 0.0
#2 0.0 0.0 0.0 0.0
#3 0.0 0.0 0.0 0.0
result = df.fillna(tmp)
print(result)
# 出力結果:
# name age height weight
#0 kawasaki 98.0 162 0.0
#1 isshiki 45.0 168 65.0
#2 endo 54.0 175 70.0
#3 ogawa 0.0 180 75.0
tmp = pd.DataFrame(np.zeros((4, 3)), columns=['name', 'age', 'height'])
result = df.fillna(tmp)
print(result)
# 出力結果:
# name age height weight
#0 kawasaki 98.0 162 NaN
#1 isshiki 45.0 168 65.0
#2 endo 54.0 175 70.0
#3 ogawa 0.0 180 75.0
最初にnumpy.zeros関数を使って、元のDataFrameオブジェクトと同じ形状の多次元配列(要素の値は全て0.0)を作成して、それを基に置き換え後の値を含んだDataFrameオブジェクトを作成しています(列ラベルも基と同じものにしている点に注意)。
そして、それをfillnaメソッドに渡してやれば、欠損値があれば、それに対応する要素の値で置き換えられます。なお、最後の例ではわざと1列少ないDataFrameオブジェクトを作成して、それをfillnaメソッドに渡しています。このときには、欠損値があっても、対応する要素が置き換え後の値を格納しているDataFrameオブジェクトにないので置き換えが行われません。
少し長くなりましたが、今回は欠損値の概要とDataFrameオブジェクトに欠損値が含まれているかどうかを確認したり、その数をカウントしたり、欠損値を含む行や列を削除したり、欠損値を別の値で置き換えたりする方法を見てきました。次回からはもうちょっと高度な話題に触れていく予定です。
Copyright© Digital Advantage Corp. All Rights Reserved.