pandasのDataFrameオブジェクトの要素を選択するにはたくさんの方法があります。その中からat属性とiat属性、それからブーリアンインデクシングと呼ばれる方法を用いてアクセスする方法を紹介しましょう。
本シリーズ「Pythonデータ処理入門」は、Pythonの基礎をマスターした人を対象に以下のような、Pythonを使ってデータを処理しようというときに便利に使えるツールやライブラリ、フレームワークの使い方の基礎を説明するものです。
なお、本連載では以下のバージョンを使用しています。
第3回ではブラケット「[]」やiloc属性/loc属性を用いて行や列、特定要素を選択する方法を紹介しました。が、pandasのDataFrameオブジェクトではその他にもさまざまな方法で行や列(Seriesオブジェクト)、DataFrameオブジェクトを構成する一部のDataFrameオブジェクト、特定の要素などを選択できます。今回はそれらの方法を紹介していきます。
前述の通り、第3回ではブラケットやiloc属性/loc属性を使ってDataFrameオブジェクトの特定の行や列、要素を選択する方法を紹介しました。繰り返しになりますが、以下にその例を示しておきましょう。
なお、ここでは以下のようなDataFrameオブジェクトを定義して例として使うことにします。
df = pd.DataFrame([['kawasaki', 99, 175, 80],
['isshiki', 45, 160, 60],
['endo', 55, 165, 70]],
columns=['name', 'age', 'height', 'weight'])
print(df)
# 出力結果:
# name age height weight
#0 kawasaki 99 175 80
#1 isshiki 45 160 60
#2 endo 55 165 70
以下にブラケットに列名を指定して列を選択する例、loc属性に列ラベル(と全行を意味するコロン「:」)を指定して列を選択する例、iloc属性に行インデックスを指定して行を選択する例、loc属性に行と列を指定して特定の要素を選択する例を示します。おさらいなので詳しい解説は省略します。
# ブラケットに列を指定して選択
s = df['name']
print(s)
# 出力結果:
#0 kawasaki
#1 isshiki
#2 endo
#Name: name, dtype: object
# loc属性を使って列を選択
s = df.loc[:, 'name'] # 全ての行の'name'列を選択
print(s)
# 出力結果:
#0 kawasaki
#1 isshiki
#2 endo
#Name: name, dtype: object
# iloc属性を使って行を選択
s = df.iloc[0] # 第0行を選択
print(s)
# 出力結果:
#name kawasaki
#age 99
#height 175
#weight 80
#Name: 0, dtype: object
# loc属性を使って特定の要素を選択
item = df.loc[0, 'age'] # 第0行の'age'列の値を選択
print(item) # 99
DataFrameオブジェクトにはiloc属性とloc属性に似たiat属性とat属性があります。iat属性では整数インデックスを指定して、at属性では軸ラベルを指定するというのもiloc属性とloc属性と同様です。ただし、大きな違いがあります。それはiat属性とat属性では行と列の指定が必須であり、かつスライスは指定できない点です。簡単にいうと、特定の要素を1つだけ選択するのにこれらの属性を使用します。
以下に例を示します。
age = df.iat[0, 1] # 第0行の第1列('age'列)の値を選択
print(age) # 99
df.iat[0, 1] = 100 # 第0行の第1列('age'列)の値を変更
print(df)
# 出力結果:
# name age height weight
#0 kawasaki 100 175 80
#1 isshiki 45 160 60
#2 endo 55 165 70
height = df.at[1, 'height'] # 第1行の'height'列の値を選択
print(height) # 160
weights = df.at[1:, 'weight'] # KeyError例外
最初の例は第0行の第1列('age'列)の値をiat属性で選択しているところです。iatに続くブランケットには「[行インデックス, 列インデックス]」の順で要素を特定する値を記述します。
次の例では同じ方法で特定要素を選択し、そこに新しく値を代入しています。loc/iloc/at/iatの各属性を使って要素を選択して、そこに代入するだけで、特定要素の値を変更できるということです。
最後の例ではat属性を使って最初の例と同様な処理を行っていますが、使っているのが軸ラベルである点が異なります(行の指定では整数インデックスを使っているように見えますが、あくまでも軸ラベルとして扱われている点には覚えておきましょう)。
これまでブラケットに列ラベルを記述することで、その列を選択するコードを書いてきました。しかし、「DataFrameオブジェクト名」にドット「.」と列名を記述することでも列を選択できます。以下に例を示します。
s = df.name # 'name'列を選択
print(s)
# 出力結果:
#0 kawasaki
#1 isshiki
#2 endo
#Name: name, dtype: object
ただし、この記法ではDataFrameオブジェクトが持っている属性やメソッドと同じ名前の列は選択できないようです。
df2 = pd.DataFrame([[1, 2, 3], [4, 5, 6]], columns=['loc', 'iloc', 'info'])
print(df2.loc) # <pandas.core.indexing._LocIndexer object at 0x11745fca0>
print(df2['loc'])
# 出力結果:
#0 1
#1 4
#Name: loc, dtype: int64
この例では、わざとらしく列名を'loc'、'iloc'、'info'とDataFrameオブジェクトが持つ属性とメソッドの名前に設定しています。そして、「df2.loc」としたときに'loc'列が選択されるかどうかを試していますが、出力結果からはそうはなっていないことが分かります。
一方、「df2.loc['loc']」としたときには'loc'列が選択されました。ブラケットに列名を含める書き方であれば常に列が選択できますが、タイプする文字数が少し増えます。ドットに続けて列名を記述する方がタイプ量は減りますが、望んだ結果が得られない可能性もあります(そういう列名の付け方をするなという話ですが)。一貫した記述が求められるのであればブラケットを使うのがよいかもしれませんね。
DataFrameオブジェクトから特定の条件を満たしている行だけを選択したいということはよくあります。こうしたときに便利に使えるのが「ブーリアンインデクシング」と呼ばれる手法です。
ブーリアンインデクシングを実際に試してみる前に、サンプルのDataFrameオブジェクトの要素を確認しておきましょう。
print(df)
# 出力結果:
# name age height weight
#0 kawasaki 100 175 80
#1 isshiki 45 160 60
#2 endo 55 165 70
ブーリアンインデクシングは真偽値(True/False)を要素とするSeriesオブジェクトや配列、リストなどをブランケットの中に含めることで、その要素がTrue(真)となっている行だけを選択するものです。このとき、ブランケットに含めるSeriesオブジェクトや配列、リストの要素数は行数と一致している必要があります。
例えば、次のように[True, False, True]というリストをブラケットの中に含めるとどうなるでしょう(要素数の3はこのDataFrameオブジェクトの行数に一致している点に注目)。
result = df[[True, False, True]]
print(result)
# 出力結果:
# name age height weight
#0 kawasaki 100 175 80
#2 endo 55 165 70
DataFrameオブジェクトには[True, False, True]というリストを渡しているので、その値がTrueである第0要素と第2要素に対応する第0行と第2行が選択されていることが分かりますね。
ここで、先のDataFrameオブジェクトで体重('weight'列)が60より大きな行だけを取り出したいとします。これは次のようなコードで調べられます。
mask = df['weight'] > 60
print(mask)
# 出力結果:
#0 True
#1 False
#2 True
#Name: weight, dtype: bool
変数maskには'weight'列の要素の値が60より大きいかどうかに応じてTrueかFalseの値を格納するSeriesオブジェクトが代入されます。この場合は、出力結果を見ると分かる通り、第0行と第2行の要素の値がTrueで、第1行の要素の値がFalseとなります(上のリストの要素と同じなのをわざとらしく感じる人もいるかもしれませんね)。また、'weight'列の各要素に対してその値が60より大きいかどうかを比較しているので、得られるSeriesオブジェクトの要素数は行数に等しくなっている点も覚えておきましょう。
そして、元のDataFrameオブジェクトに対してこれをブラケットに囲んで指定することで、そこからSeriesオブジェクトの要素の値がTrueとなっているものに対応する行だけを選択できます。
result = df[mask]
print(result)
# 出力結果:
# name age height weight
#0 kawasaki 100 175 80
#2 endo 55 165 70
上の例では条件判定の結果を変数maskに保存していましたが、そうする必要は特にありません。ブラケットの中に直接記述可能です。
result = df[df['weight'] > 60]
print(result) # 上と同じ結果
また、条件を複数指定することも可能です。ただし、このときに注意する点として、それぞれの条件をかっこ「()」で囲むことと、以下の演算子でそれらをつなぐことです。
例えば、'weight'列の要素の値が60より大きくて、かつ'height'列の値が170より大きい行だけを選択するのなら次のようなコードを書きます。
result = df[(df['weight'] > 60) & (df['height'] > 170)]
print(result)
# 出力結果:
# name age height weight
#0 kawasaki 100 175 80
ここでは「(df['weight'] > 60)」と「(df['height'] > 170)」という2つの条件を&演算子でつないでいるので、両方の条件が一致したときにだけ式全体「(df['weight'] > 60) & (df['height'] > 170)」がTrueになります。よって、これに合致する第0列だけが選択されたわけです。今も述べましたが、次のように条件を囲むかっこを忘れると例外が発生するので確実にかっこで囲むようにしましょう。
result = df[df['weight'] > 60 & df['height'] > 170] # ValueError
なお、ブーリアンインデクシングはloc属性やiloc属性と組み合わせることも可能です(at属性やiat属性では使えません)。以下はloc属性と組み合わせて使用する例です。
mask = (df['height'] > 160) & (df['weight'] > 70)
print(mask)
# 出力結果:
#0 True
#1 False
#2 False
result = df.loc[mask, 'name':]
print(result)
# 出力結果:
# name age height weight
#0 kawasaki 100 175 80
ただし、iloc属性と組み合わせる場合は、ブーリアンインデクシングで使用するのはSeriesオブジェクトではなく配列(やリストなど)にしてください。配列は簡単にはSeriesオブジェクトのvalues属性で取得できます。
result = df.iloc[mask.values, 0:]
print(result) # 出力結果は上と同じ
真偽値を返す関数やラムダ式を使って条件を判定することも可能です。以下に簡単な例を示します。
result = df[lambda x: x['weight'] > 60]
print(result)
# 出力結果:
# name age height weight
#0 kawasaki 100 175 80
#2 endo 55 165 70
もちろん、この程度の簡単な例であれば、次のように書けるでしょう。
result = df[df['weight'] > 60]
print(result) # 上と同じ結果
あるいはDataFrameオブジェクトのqueryメソッドを使って次のようにも書けます(queryメソッドについては後続の回で紹介できたらいいなと思っています)。
result = df.query('weight > 60')
print(result) # 上と同じ結果
また、DataFrameオブジェクトやSeriesオブジェクトのisinメソッドを使うと、DataFrameオブジェクトの各要素の値がisinメソッドに渡した値一覧に含まれているかどうかを調べることも可能です(isinメソッドについても後続の回で紹介したいと考えています)。
mask = df['name'].isin(['isshiki', 'endo'])
print(mask)
# 出力結果:
#0 False
#1 True
#2 True
#Name: name, dtype: bool
result = df[mask]
print(result)
# 出力結果:
# name age height weight
#1 isshiki 45 160 60
#2 endo 55 165 70
最初のコード群ではisinメソッドを使って、'name'列の要素の値が'isshiki'か'endo'に合致するかどうかを調べ、その結果を変数maskに保存しています。そして、次のコード群でその結果を基にDataFrameオブジェクトで対応する行のみを選択しています。
余談ではありますが、Pythonプログラマーの心構えを説いた「Zen of Python」という文言集には「There should be one-- and preferably only one --obvious way to do it.」(何かをするのに、これしかないという方法が1つ、できれば1つだけあるはずだ)というものがあります。このことからすると、上で見たように同じことをするのに幾つも方法があるというのは、実はあまりPython的ではないと言えるかもしれません。
しかし、これは逆にpandasがどれほどの柔軟性を持っているか、プログラマーの頭に浮かんだ処理をどれだけ簡単にコードに落とし込めるかを表しているとも言えるでしょう。コードだけではなく、大量のデータも相手にしようというときに、思い付いたアイデアを「この処理はこれこれこうすればうまいことイケそう」などと考えながらPythonのプログラムに翻訳するのではなく、なるべくそのままの形でプログラムとして記述できるのであれば、その方がストレスなく自分の作業に没頭できるかもしれません。
次回はDataFrameオブジェクトが持つ要素をさまざまな形で調べたり、加工したりする方法について見ていく予定です。
Copyright© Digital Advantage Corp. All Rights Reserved.