こんにちは、技術開発ユニットの三浦です。
仕事や調べ物、家事など色々やることはあるのですが、空いた時間でちょこちょことゲームをすることが好きです。 半年くらいかけて進めていたRPGのゲームがようやく終盤戦に差し掛かったようで、終わったら次のゲームができる楽しみと、これまで冒険してきた(ゲームの中の)仲間たちとの別れが寂しいなと感じる今日このごろです。
さて、最近データ集計を色々やる機会があるのですが、そこではPythonのpandas
というライブラリを使っています。このライブラリの代表的な2つのクラスDataFrame
とSeries
のオブジェクトにアクセスする時に使用する[]
の挙動について、ちょっと気になったので調べてみました。
pandas
と私
pandas
はテーブル形式のデータを扱うためのPythonのライブラリです。データ分析や機械学習のためにPythonを始めた人が、たぶん入門最初期に触れることになるライブラリがpandas
ではないでしょうか。少なくとも私はそうでした。
pandas
からPythonに入って、その後色々なアプリをPythonで作る経験をして、またpandas
に触れると、勉強し始めた頃には特に不思議に思わなかったことが色々気になるようになりました。その中でも特に気になったのが冒頭に触れた[]
でpandas
のデータオブジェクトにアクセスするときの挙動です。
[]
の私にとって分かりやすい使い方
次のようなテーブルデータをDataFrame
オブジェクトsample_data
で扱っているとします。
ここから"formula"の列を取ってきて何か操作したり検索をしたい時は、sample_data['formula']
のように[]
に取りたい列のカラム名を渡してあげます。そうするとpandas
で一次元配列を扱うSeries
クラスのオブジェクトが得られます。
print(type(sample_data['formula'])) print(sample_data['formula'])
結果
DataFrame
はSeries
オブジェクトを、カラム名をキーに格納した辞書型、というイメージです。実際にpandas
のAPI referenceには、DataFrame
について以下のような記述があります。
Can be thought of as a dict-like container for Series objects.
pandas API reference pd.DataFrame https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.html
このように考えると、[]
にカラム名を入れるとそれに該当する列が返ってくる、という動作は自然に思えます。
[]
の私にとって少し分かりにくい使い方
[]
には他にも色々なタイプの入力が可能で、その1つにスライスがあります。sample_data[:5]
のような使い方です。この時、sample_data
の先頭5行がDataFrame
で返ってきます。カラム名を指定する時と異なり、行方向の操作になります。
print(type(sample_data[:5])) print(sample_data[:5])
結果
このように、同じ[]
なのに入力によって色々な挙動を取るところが私にとってなかなか分かりにくいな・・・と感じたところです。先程から[]
と記載していますが、Pythonではdata[key]
のように、key
に紐付いた値を取りに行こうとすると、__getitem__
という特殊メソッドが呼ばれます。また、data[key]=value
のように値をセットしようとすると__setitem__
という特殊メソッドが呼ばれます。
話は逸れますが自分で作ったclassにも実装して使うことができます。
class Person(): name = None equip = {} def __init__(self, name): self.name = name self.equip['hand'] = None self.equip['body'] = None def __getitem__(self, key): if key in self.equip.keys(): print(f'{self.name}は{key}に{self.equip[key]}をもっている') return self.equip[key] else: return None def __setitem__(self, key, value): self.equip[key] = value print(f'{self.name}は{key}に{value}をそうびした') #Test test_person = Person('Test') test_person['hand'] = 'pen' print(test_person['hand'])
結果
pandas
の[]
の挙動を知りたければ__getitem__
の実装を直接見るのが確実です。ただ今回は実際に色々なタイプの入力を試してみて、どんな挙動をするのかを見ていきたいと思います。
入力タイプごとの挙動を見てみる
色々なパターンを以下見ていきます。
スカラー(値)を入れた場合
これはさっき見たように、DataFrame
の場合は列方向に一致する列名を探しに行き、見つかれば該当する列をSeries
で返します。Series
の場合は一致するインデックスを探してスカラーを返します。
DataFrame
print(type(sample_data['formula'])) print(sample_data['formula'])
結果
Series
print(type(sample_data['formula'][10])) print(sample_data['formula'][10])
結果
入力した値が列名に無い(Series
はインデックスにない)場合はKeyError
でエラーになります。
sample_data[10] #KeyError: 10 sample_data['formula'][9] #KeyError: 9
list
(boolean
配列以外)を入れた場合
DataFrame
の場合はlist
に含まれる要素を列名に持つDataFrame
が、Series
の場合はインデックスを持つSeries
が返ってきます。
DataFrame
print(type(sample_data[['x', 'x**2']])) print(sample_data[['x', 'x**2']].head())
結果
Series
print(type(sample_data['formula'][[0, 2, 4]])) print(sample_data['formula'][[0, 2, 4]])
結果
list
の中に1つでも列名(Series
ではインデックス)に存在しない要素が含まれているとKeyError
です。
sample_data[['x','x**4']] #KeyError: "['x**4'] not in index"
後で触れるのですが、list
の要素がboolean
で構成されている場合は違った挙動になります。
スライスを入れた場合
DataFrame
の場合はスライスで指定した範囲に含まれる位置の行をDataFrame
で返し、Series
の場合はSeries
で返します。上から数えた行の位置を評価しています。
DataFrame
print(type(sample_data[1:5])) print(sample_data[1:5])
Series
print(type(sample_data['formula'][1:5])) print(sample_data['formula'][1:5])
結果
スライスの範囲が行番号の範囲と被っていなくてもエラーにはなりません。
sample_data[100:200]
結果
True
/False
で構成されたboolean
配列を入れた場合
さっきlist
を入力した場合の挙動を見ましたが、要素がboolean
だと違った挙動になります。列方向ではなく行方向へ作用し、True
に該当する行のオブジェクトが返ってきます。よくpandas
オブジェクトから条件に該当する部分だけを抽出するときに使います。
boolean_vector = [True for _ in range(0, 10, 2)] \ + [False for _ in range(10, 20, 2)] print(type(sample_data[boolean_vector])) print(sample_data[boolean_vector])
結果
pandas
のオブジェクトに対して比較演算を行うと各要素に対して比較演算を行った結果がboolean
で格納されたオブジェクトが返ってきます。例えばSeries
に対して比較演算を行うと同じサイズのboolean
で構成されたSeries
が得られます。それをDataFrame
の[]
に入力しても、boolean
配列と同様の動作になります。
boolean_vector = sample_data['x**3'] > 40 print(type(boolean_vector)) print(boolean_vector.shape) print(sample_data[boolean_vector])
結果
ちなみにboolean
を要素に持つpandas
オブジェクト同士は論理演算子and
やor
ではなく、ビット演算子&
や|
を使って計算します。
boolean
配列を使う場合はその長さが対象のオブジェクトの行数と一致していないとValueError
になります。
boolean_vector = [True, False, True] sample_data[boolean_vector] #ValueError: Item wrong length 3 instead of 10.
・・・だいたいこれくらいを試しておけば、OKかな、と思います。
まとめ
ということで、今回はpandas
ライブラリのDataFrame
とSeries
に[]
でアクセスしたときの挙動について調べてみました。これまでその場その場でなんとなく使ってきたのですが、実際に動作を確認しながら結果を記録していくと、とても勉強になりました。pandas
はとても機能の多いライブラリなので、これからもちょくちょく使って気づいたことをまとめていきたいなと思います。