Coursera『Introduction to Data Science in Python – by University of Michigan』の学習メモ
Pandas Idioms
Pandasのお作法について学ぶ。高いパフォーマンスかつ読みやすいPandasコード(= Pandorableなコード)を目指す。
先週も使ったセンサスデータを用いる。
import pandas as pd
df = pd.read_csv('census.csv')
df
クエリの書き方
Chain Indexingは良くない
Chain Indexingは、一般的に悪い書き方とされている。その理由は、後ろで動くnumpyライブラリの影響で、copyを返す時もあれば、単にviewを返す時もあり、挙動が一定しないから。したがって、”[]” を書いた時は、自分がどのような挙動を期待しているか注意深く検討するべきである。
Method chainingが良い
クエリメソッドを繋げる書き方。一つ一つのメソッドは、そのオブジェクトへのリファレンスを返すので、いくつもの異なった処理をDataframeに対して実行することが可能となる。
以下のコード例の一つ目は、Pandorableな書き方。ポイントは2点:
- 見やすくするために全体を “()” で囲っている
- “)” までは、見やすいタイミングで改行している
二つ目のコード例は、伝統的な書き方。Pandorableではないが、処理速度はこちらの方が早い。これはよくあるトレードオフ(読みやすさ vs 処理速度)である。
# Pandorable
(df.where(df['SUMLEV']==50)
.dropna()
.set_index(['STNAME','CTYNAME'])
.rename(columns={'ESTIMATESBASE2010': 'Estimates Base 2010'}))
# Not pandorable
df = df[df['SUMLEV']==50]
df.set_index(['STNAME','CTYNAME'], inplace=True)
df.rename(columns={'ESTIMATESBASE2010': 'Estimates Base 2010'})
applymap
applyファンクションは、カラムを追加して計算した値を入れる時などに便利(例えば、最大値、最小値など)。
最大値と最小値を持った新しいデータフレームを、applyで生成する。applyの引数に “axis=1” を渡すことで、行全体に値を適用することができる。
import numpy as np
def min_max(row):
data = row[['POPESTIMATE2010',
'POPESTIMATE2011',
'POPESTIMATE2012',
'POPESTIMATE2013',
'POPESTIMATE2014',
'POPESTIMATE2015']]
return pd.Series({'min': np.min(data), 'max': np.max(data)})
df.apply(min_max, axis=1)
既存のデータフレームにmaxとmin列を追加したい場合は、以下の通りになる。npの max() と min() で簡単に計算することができる。
import numpy as np
def min_max(row):
data = row[['POPESTIMATE2010',
'POPESTIMATE2011',
'POPESTIMATE2012',
'POPESTIMATE2013',
'POPESTIMATE2014',
'POPESTIMATE2015']]
row['max'] = np.max(data)
row['min'] = np.min(data)
return row
df.apply(min_max, axis=1)
lambdas
実務的には、上のように大きな関数の定義をあまり見ることはなく、代わりにlambdaを使って描かれることが多い。
rows = ['POPESTIMATE2010',
'POPESTIMATE2011',
'POPESTIMATE2012',
'POPESTIMATE2013',
'POPESTIMATE2014',
'POPESTIMATE2015']
df.apply(lambda x: np.max(x[rows]), axis=1)
Group by
pandasを使った集計方法。センサスデータを用いる。
import pandas as pd
import numpy as np
df = pd.read_csv('census.csv')
df = df[df['SUMLEV']==50]
df
groupby
ステイトごとの平均値をループを使って計算してみる。
unique()を使って以下の1つ目のコードのようにも書けるが、groupbyを使った2つ目のコードの方が処理速度は格段に速い。したがって、ほぼ毎回groupbyを使うことになるだろう。
# unique
for state in df['STNAME'].unique():
avg = np.average(df.where(df['STNAME']==state).dropna()['CENSUS2010POP'])
print('Counties in state ' + state + ' have an average population of ' + str(avg))
# groupby
for group, frame in df.groupby('STNAME'):
avg = np.average(frame['CENSUS2010POP'])
print('Counties in state ' + group + ' have an average population of ' + str(avg))
また、大量のバッチ処理のように限られた時間内で、かつグルーピングパターンが複雑でない場合は、以下のように関数として書くこともできる。このような light weight hashing と言われるテクニックは、実務的に用いられることが多い。
df = df.set_index('STNAME')
def fun(item):
if item[0]<'M':
return 0
if item[0]<'Q':
return 1
return 2
for group, frame in df.groupby(fun):
print('There are ' + str(len(frame)) + ' records in group ' + str(group) + ' for processing.')
agg()
groupbyしたものにapplyメソッド(イテレーション)を適用する方法として、"agg({キー: 適用する処理})"(aggregateの省略)というメソッドがある。
df = pd.read_csv('census.csv')
df = df[df['SUMLEV']==50]
df.groupby('STNAME').agg({'CENSUS2010POP': np.average})
aggを使う際の注意点は、groupbyのオブジェクトが dataframeになる時と、seriesになる時がある点で、これらの挙動には少し違いがある。
print(type(df.groupby(level=0)['POPESTIMATE2010','POPESTIMATE2011']))
#
print(type(df.groupby(level=0)['POPESTIMATE2010']))
#
例えば、groupbyのオブジェクトがseriesの場合、CENSUS2010POPの平均値と合計値を表示させようとすると以下のようになる。
(df.set_index('STNAME').groupby(level=0)['CENSUS2010POP']
.agg({'avg': np.average, 'sum': np.sum}))
groupbyのオブジェクトがdataframeの同じことをやろうとすると以下のようになる。
(df.set_index('STNAME').groupby(level=0)['POPESTIMATE2010','POPESTIMATE2011']
.agg({'avg': np.average, 'sum': np.sum}))
しかし、以下のように前のカラム名をaggで指定した場合、思っていないような挙動になる。(POPESTIMATE2010に平均値が、POPESTIMATE2011に合計値が入ってしまう。)
(df.set_index('STNAME').groupby(level=0)['POPESTIMATE2010','POPESTIMATE2011']
.agg({'POPESTIMATE2010': np.average, 'POPESTIMATE2011': np.sum}))