Kaggleで戦いたい人のためのpandas実戦入門
はじめに
- 自分は元々pandasが苦手でKaggleコンペ参加時は基本的にBigQuery上のSQLで特徴量を作り、最低限のpandas操作でデータ処理をしていました。
- しかし、あるコードコンペティションに参加することになり、pythonで軽快にデータ処理をこなす必要が出てきたので勉強しました。
- そこで、当時の勉強メモをもとに「これだけ知っていればKaggleでそこそこ戦えるかな」と思っているpandasの主要機能をまとめました。
注記
実戦入門
のつもりがほぼ辞書
になってしまいました orz- pandasとはなんぞや的な内容は書いていません
(import pandas
やDataFrameとは何かなど) - pandas1.0系でも動くように書いたつもりですが間違ってたらすみません
目次
- はじめに
- 目次
- Options
- DaraFrame 読み書き
- データクリーニング
- DataFrame操作
- 各種演算
- カテゴリ変数エンコーディング
- 文字列操作
- 日付系処理
- 可視化
- 並列処理
- おまけ: Excel読み書き
- pandasを身につけるには?
- おわりに
Options
jupyter notebook で DataFrame の表示が省略されないようにする。 なんだかんだ書き方をよく忘れる。
pd.set_option('display.max_columns', None) pd.set_option('display.max_rows', None)
DaraFrame 読み書き
CSVファイル
読み込み
read_csv
は意外とオプションが多いのでなかなか覚えきれません。
# 基本 df = pd.read_csv('train.csv') # headerがないとき (列名は連番になる) df = pd.read_csv('train.csv', header=None) # headerがなくて自分で列名指定したいとき df = pd.read_csv('train.csv', names=('col_1', 'col_2')) # 利用する列を指定したいとき df = pd.read_csv('train.csv', usecols=['col_1', 'col_3']) # lamda式も利用可能 df = pd.read_csv('train.csv', usecols=lambda x: x is not 'col_2') # 列名: 読み込んだあとの変更 df = df.rename(columns={'c': 'col_1'}) # 型指定で読み込み (指定した列以外は自動推定) ## メモリ逼迫しているとき以外は、型指定せず read_csv して、 ## 後述の `reduce_mem_usage` を使うことも多い df = pd.read_csv('train.csv', dtype={'col_1': str, 'col_3': str}) ## 型: 読み込んだあとの変更 df = df['col_1'].astype(int) # float / str / np.int8 ... # 時間系データをparse df = pd.read_csv('train.csv', parse_dates=['created_at', 'updated_at'])
書き出し
# 基本 df.to_csv('file_name.csv') # index不要のとき (kaggle submission fileは不要なので忘れがち) submission.to_csv('submission.csv', index=False)
Pickleファイル
# 基本 df = pd.read_pickle('df.pickle') df.to_pickle('df.pickle') # データが重いときはzip化できる (が遅くて実用に耐えないらしい) ## 書き出し: 拡張子を zip や gzip にするだけでよい df.to_pickle('df.pickle.zip') ## 読み込み: read_pickle は拡張子を見て自動的に解凍処理をしてくれる df = pd.read_pickle('df.pickle.zip')
- pandas.DataFrame, Seriesをpickleで保存、読み込み
- pandas の永続化フォーマットについて調べた
- pickle / feather / parquet の比較をしている記事
- 保存時のファイルサイズ以外はあらゆる面でpickleが優れているとのこと
- ファイルサイズはparquetが小さいとのこと
メモリ使用量削減の工夫
ファイルを読み込んだ直後にメモリ使用量削減するクセを付けておくと色々はかどります。
型変更
# kaggleでよく使われる `reduce_mem_usage` でメモリ使用量削減 ## 内部では各カラムの値域に合わせて型変更を行っている ## `reduce_mem_usage` 実装は ref 参照 df = reduce_mem_usage(df) # 実践的には read_csv した直後にメモリ使用量削減を行うことも多い df = df.read_csv('train.csv')\ .pipe(reduce_mem_usage) # 余談だが、pipeを使うと可読性向上することが多い # f(g(h(df), arg1=1), arg2=2, arg3=3) df.pipe(h) \ .pipe(g, arg1=1) \ .pipe(f, arg2=2, arg3=3)
不要カラム削除
import gc # dropでも良い: df.drop('col_1', axis=1, inplace=True) del df['col_1']; gc.collect();
データクリーニング
欠損データ処理
# 欠損がある行を削除 df1.dropna(how='any') # 特定の列で欠損している行を無視 df = df[~df['col_1'].isnull()] # 埋める df1.fillna(value=0)
重複排除
# 基本 df2.drop_duplicates() # 重複しているカラムの指定 df2.drop_duplicates(['col_1']) # 残す列の指定 df2.drop_duplicates(['col_1'], keep='last') # keep='first' / False(drop all)
補間 (interpolate)
- Kaggleではあまり使わないかもだが、実務とかで役に立ちそう
DataFrame操作
DataFrame 情報表示
# 行数,列数,メモリ使用量,データ型,非欠損要素数の表示 df.info() # 行数 x 列数 取得 df.shape # 行数取得 len(df) # 最初 / 最後のN行表示 df.head(5) df.tail(5) # カラム名一覧を取得 df.columns # 各要素の要約統計量を取得 ## 数値型要素の min/max/mean/stdなどを取得 df.describe() ## カテゴリ型要素の count/unique/freq/stdなどを取得 df.describe(exclude='number') ## 表示するパーセンタイルを指定 df.describe(percentiles=[0.01, 0.25, 0.5, 0.75, 0.99])
Slice (iloc / loc / (ix))
# 基本 df.iloc[3:5, 0:2] df.loc[:, ['col_1', 'col_2']] # 行は数値で指定して、列は名前で指定する # (バージョンによっては ix でもできるが廃止された) df.loc[df.index[[3, 4, 8]], ['col_3', 'col_5']]
型による列選択
# 除外もできる df.select_dtypes( include=['number', 'bool'], exclude=['object'])
条件指定による行選択
# 基本 df[df.age >= 25] # OR条件 df[(df.age <= 19) | (df.age >= 30)] # AND条件 df[(df.age >= 25) & (df.age <= 34)] ## betweenでも書ける (あまり見ないが) df[df['age'].between(25, 34)] # IN df[df.user_id.isin(target_user_list)] # query記法: 賛否両論あるが個人的には好き df.query('age >= 25') \ .query('gender == "male"')
indexリセット
# 基本 df = df.reset_index() # 破壊的変更 df.reset_index(inplace=True) # drop=Falseにするとindexが列として追加される df.reset_index(drop=False, inplace=True)
列削除
# 基本 df = df.drop(['col_1'], axis=1) # 破壊的変更 df = df.drop(['col_1'], axis=1, inplace=True)
Numpy Array 化
# df['col_1'] のままだと index が付いてきて # 他のdfにくっつけるときにバグを引き落とすようなこともあるので # numpy array にして後続の処理を行うことも多々ある df['col_1'].values
連結・結合
連結
# concat ## 基本 (縦に積む: カラムは各DataFrameの和集合 df = pd.concat([df_1, df_2, df_3]) ## 横につなげる df = pd.concat([df_1, df_2], axis=1) ## 各DataFrameに共通のカラムのみで積む df = pd.concat([df_1, df_2, df_3], join='inner')
- pandas cheetsheet
- Reshaping Data の部分に色付きのわかりやすい図があります
- pandas.DataFrame, Seriesを連結するconcat
- concat 苦手すぎてこの記事をたぶん100回以上見てる気がします笑
結合
merge
: キーを指定しての結合
# 基本 (内部結合) df = pd.merge(df, df_sub, on='key') # 複数のカラムをキーとする df = pd.merge(df, df_sub, on=['key_1', 'key_2']) # 左結合 df = pd.merge(df, df_sub, on='key', how='left') # 左右でカラム名が違うとき df = pd.merge(df, df_sub, left_on='key_left', right_on='key_right') \ .drop('key_left', axis=1) # キーが両方残るのでどちらか消す
join
: indexを利用した結合
# 基本 (左結合: mergeと違うので注意) df_1.join(df_2) # 内部結合 df_1.join(df_2, how='inner')
ランダムサンプリング
# 100行抽出 df.sample(n=100) # 25%抽出 df.sample(frac=0.25) # seed固定 df.sample(frac=0.25, random_state=42) # 重複許可: デフォルトはreplace=False df.sample(frac=0.25, replace=True) # 列をサンプリング df.sample(frac=0.25, axis=1)
ソート
# 基本 df.sort_values(by='col_1') # indexでソート df.sort_index(axis=1, ascending=False) # キーを複数 & 降昇順指定 df.sort_values(by=['col_1', 'col_2'], ascending=[False, True])
argmax / TOP-N 系の処理
# 最も値が小さな行/列を見つける df['col1'].idxmax() # 最も和が小さな列を見つける df.sum().idxmin() # TOP-N: col_1で上位5件を出す → 同一順位であればcol_2を見る df.nlargest(5, ['col_1', 'col_2']) # .smallest: 下位N件
各種演算
よく使う関数基礎
# 集計 df['col_1'].sum() # mean / max / min / count / ... # ユニーク値取得 df['col_1'].unique() # ユニーク要素個数 (count distinct) df['col_1'].nunique() # percentile df['col_1'].quantile([0.25, 0.75]) # clipping df['col_1'].clip(-4, 6) # 99パーセンタイルでclipping df['col_1'].clip(0, df['col_1'].quantile(0.99))
出現頻度カウント (value_counts)
# (NaN除く) df['col_1'].value_counts() # 出現頻度カウント(NaN含む) df['col_1'].value_counts(dropna=False) # 出現頻度カウント (合計を1に正規化) df['col_1'].value_counts(normalize=True)
値の書き換え (apply / map)
Series各要素の書き換え: map
# 各要素に特定の処理 f_brackets = lambda x: '[{}]'.format(x) df['col_1'].map(f_brackets) # 0 [11] # 1 [21] # 2 [31] # Name: col_1, dtype: object # dictを渡して値の置換 df['priority'] = df['priority'].map({'yes': True, 'no': False})
DataFrameの各行・各列の書き換え: apply
# 基本 df['col_1'].apply(lambda x: max(x)) # もちろん自身で定義した関数でも良い df['col_1'].apply(lambda x: custom_func(x)) # 進捗を表示するときは # from tqdm._tqdm_notebook import tqdm_notebook df['col_1'].progress_apply(lambda x: custom_func(x))
その他の書き換え (replace / np.where)
# replace df['animal'] = df['animal'].replace('snake', 'python') # np.where df['logic'] = np.where(df['AAA'] > 5, 'high', 'low') # np.where: 複雑ver. condition_1 = ( (df.title == 'Bird Measurer (Assessment)') & \ (df.event_code == 4110) ) condition_2 = ( (df.title != 'Bird Measurer (Assessment)') & \ (df.type == 'Assessment') & \ (df.event_code == 4100) ) df['win_code'] = np.where(condition_1 | condition_2, 1, 0)
集約 (agg)
# 基本 df.groupby(['key_id'])\ .agg({ 'col_1': ['max', 'mean', 'sum', 'std', 'nunique'], 'col_2': [np.ptp, np.median] # np.ptp: max - min }) # 全ての列を一律で集約したいときはリスト内包表記で書いてしまっても良い df.groupby(['key_id_1', 'key_id_2'])\ .agg({ col: ['max', 'mean', 'sum', 'std'] for col in cols })
集約結果の活用例
ほぼイディオムだが、最初は慣れないと処理に手間取るので例を書いておく。
# 集約 agg_df = df.groupby(['key_id']) \ .agg({'col_1': ['max', 'min']}) # カラム名が max / min になり、どのキーのものか区別できないので修正する # マルチインデックスになっているのでバラして rename する agg_df.columns = [ '_'.join(col) for col in agg_df.columns.values] # 集約結果はindexにkey_idが入っているのでreset_indexで出す agg_df.reset_index(inplace=True) # key_idをキーとして元のDataFrameと結合 df = pd.merge(df, agg_df, on='key_id', how='left')
ピボットテーブルによる集計
pd.pivot_table(df, values=['D', 'E'], index=['A', 'C'], aggfunc={'D': np.mean, 'E': [min, max, np.mean]}) # D E # mean max mean min # A C # bar large 5.500000 9.0 7.500000 6.0 # small 5.500000 9.0 8.500000 8.0 # foo large 2.000000 5.0 4.500000 4.0 # small 2.333333 6.0 4.333333 2.0
ループを回さず配列同士の演算
列方向の平均値との差分を算出する時に便利です
# `df['{col}_diff_to_col_mean] = df['{col}'] - df['{col}'].mean()` 的な処理を一括でやる時 df.sub(df.mean(axis=0), axis=1) # sub 以外にも add / div / mul (掛け算) もある # 以下は `df['{col}_div_by_col_max] = df['{col}'] / df['{col}'].max()` の一括処理 df.div(df.max(axis=0), axis=1)
ビン詰め (cut / qcut)
# df['col_1']の最小値と最大値の間を4分割 → その境界を使ってビン詰め # つまり、各ビンに含まれる個数がバラける pd.cut(df['col_1'], 4) # df['col_1']の要素数を4等分してビンを作る → その後に境界を求める # つまり、ビンの間隔がバラける pd.qcut(df['col_1'], 4)
- ヒストグラム可視化については詳しく 後述 した
- pandasのcut, qcut関数でビニング処理(ビン分割)
時系列データでよく使う処理
shift
: 行・列方向に値をずらす
# 2行下にずらす df.shift(periods=2) # 1行上にずらす df.shift(periods=-1) # 2列ずらす (あまり使わない) df.shift(periods=2, axis='columns')
rolling
: 移動平均などの算出
# window幅=3の窓関数により合計値を算出 df['col_1'].rolling(3).sum() # 複数の df['col_1'].rolling(3) \ .agg([sum, min, max, 'mean'])
cumsum
: 累積和
同様の関数に cummax
, cummin
もある
# df # A B # 0 2.0 1.0 # 1 3.0 NaN # 2 1.0 0.0 # 上記のdfの累計和を算出 df.cumsum() # A B # 0 2.0 1.0 # 1 5.0 NaN # 2 6.0 1.0
diff
, pct_change
: 行・列の差分・変化率を取得
# 例で使うdataframe # col_1 col_2 # 0 1 2 # 1 2 4 # 2 3 8 # 3 4 16 # 基本: 1行前との差分を算出 df.diff() # col_1 col_2 # 0 NaN NaN # 1 1.0 2.0 # 2 1.0 4.0 # 3 1.0 8.0 # 2行前との差分算出 df.diff(2) # col_1 col_2 # 0 NaN NaN # 1 NaN NaN # 2 2.0 6.0 # 3 2.0 12.0 # 負の数も指定可能 df.diff(-1) # col_1 col_2 # 0 -1.0 -2.0 # 1 -1.0 -4.0 # 2 -1.0 -8.0 # 3 NaN NaN # 変化率を取得するときは `pct_change` df.pct_change() # col_1 col_2 # 0 NaN NaN # 1 1.000000 1.0 # 2 0.500000 1.0 # 3 0.333333 1.0 # 計算対象がdatetimeの場合は頻度コードで指定可能 # 以下の例では `2日前` のデータとの変化率を算出 df.pct_change(freq='2D')
時間単位での集約
# 5分おきに平均、最大値を集計 # 頻度コード `min` `H` などの詳細は ref.2 に非常に詳しいので参照のこと funcs = {'Mean': np.mean, 'Max': np.max} df['col_1'].resample("5min").apply(funcs)
カテゴリ変数エンコーディング
カテゴリ変数エンコーディングの種類についてはこの資料が詳しい
One-Hot Encoding
# この DataFrame を処理する # name gender # 0 hoge male # 1 fuga NaN # 2 hage female # prefixを付けることでなんのカラムのOne-Hotかわかりやすくなる tmp = pd.get_dummies(df['gender'], prefix='gender') # gender_female gender_male # 0 0 1 # 1 0 0 # 2 1 0 # 結合したあと元のカラムを削除する df = df.join(tmp).drop('gender', axis=1) # name gender_female gender_male # 0 hoge 0 1 # 1 fuga 0 0 # 2 hage 1 0
Label Encoding
from sklearn.preprocessing import LabelEncoder # trainとtestに分かれているデータを一括でLabelEncodingする例 cat_cols = ['category_col_1', 'category_col_2'] for col in cat_cols: # 慣例的に `le` と略すことが多い気がする le = LabelEncoder().fit(list( # train & test のラベルの和集合を取る set(train[col].unique()).union( set(test[col].unique())) )) train[f'{col}'] = le.transform(train[col]) test[f'{col}'] = le.transform(test[col]) # label encoding したらメモリ使用量も減らせるので忘れずに train = reduce_mem_usage(train) test = reduce_mem_usage(test)
- 注記
- 上記方法だとtestにのみ含まれるラベルもencodingされてしまう
- 気持ち悪い場合は、trainにないものは一括で
-1
とかに書き換えてしまう (個人的にはあまり気にしていないので正しいやり方かどうか不安…。)
- kaggle本実装
- kaggle本ではtrainに出てくるものだけでLabelEnconding
Frequency Encoding
for col in cat_cols: freq_encoding = train[col].value_counts() # ラベルの出現回数で置換 train[col] = train[col].map(freq_encoding) test[col] = test[col].map(freq_encoding)
Target Encoding
# 超雑にやるとき (非推奨) ## col_1の各ラベルに対して target(correct) の平均値とカウントを算出 ## 一定のカウント未満(仮に1000件)のラベルは無視して集計する、という例 target_encoding = df.groupby('col_1') \ .agg({'correct': ['mean', 'count']}) \ .reset_index() \ # 少数ラベルはリークの原因になるので消す .query('count >= 1000') \ .rename(columns={ 'correct': 'target_encoded_col_1', }) \ # カウントは足切りに使っただけなので消す .drop('count', axis=1) train = pd.merge( train, target_encoding, on='col_1', how='left') test = pd.merge( test, target_encoding, on='col_1', how='left')
- 上記の例は非常に雑な実装です。真面目にやるときはKaggle本の実装を読んでFoldごとに計算しましょう
文字列操作
pandas official method list にたくさん載っているので一度目を通すことをおすすめします。
基本
# 文字数 series.str.len() # 置換 series.str.replace(' ', '_') # 'm' から始まる(終わる)かどうか series.str.starswith('m') # endswith # 表現を含んでいるかどうか pattern = r'[0-9][a-z]' series.str.contains(pattern)
クリーニング
# 大文字/小文字 series.str.lower() # .upper() # capitalize (male → Male) series.str.capitalize() # 英数字抽出: 最初の適合部分だけだけ ## マッチが複数の場合はDFが返ってくる ## extractall: すべての適合部分がマルチインデックスで返ってくる series.str.extract('([a-zA-Z\s]+)', expand=False) # 前後の空白削除 series.str.strip() # 文字の変換 ## 変換前: Qiitaは、プログラミングに関する知識を記録・共有するためのサービスです。 ## 変換後: Qiitaは,プログラミングに関する知識を記録共有するためのサービスです. table = str.maketrans({ '、': ',', '。': '.', '・': '', }) result = text.translate(table)
日付系処理
基本
# 基本: 読み込み時に変換忘れたときとか df['timestamp'] = pd.to_datetime(df['timestamp']) # 日付のリストを作成 dates = pd.date_range('20130101', periods=6) # 日付のリストを作成: 秒単位で100個 pd.date_range('20120101', periods=100, freq='S') # 日付でフィルタ df['20130102':'20130104'] # unixtime にする df['timestamp'].astype('int64')
高度な日付抽出
- pandasにはとても複雑な日付抽出の仕組みが実装されており、
毎月の第4土曜日
や月初第一営業日
といった抽出も一瞬です。(日本の祝日が対応していないので後述のjpholiday
などで多少変更は必要ですが。) - pandasの時系列データにおける頻度(引数freq)の指定方法 に詳しいので、日付関係の実装が必要な際はぜひ一読されることをおすすめします。
# 月の最終日を抽出する pd.date_range('2020-01-01', '2020-12-31', freq='M') # DatetimeIndex(['2020-01-31', '2020-02-29', '2020-03-31', '2020-04-30', # '2020-05-31', '2020-06-30', '2020-07-31', '2020-08-31', # '2020-09-30', '2020-10-31', '2020-11-30', '2020-12-31'], # dtype='datetime64[ns]', freq='M') # 2020年の第4土曜日を抽出する pd.date_range('2020-01-01', '2020-12-31', freq='WOM-4SAT') # DatetimeIndex(['2020-01-25', '2020-02-22', '2020-03-28', '2020-04-25', # '2020-05-23', '2020-06-27', '2020-07-25', '2020-08-22', # '2020-09-26', '2020-10-24', '2020-11-28', '2020-12-26'], # dtype='datetime64[ns]', freq='WOM-4SAT')
祝日判定
- pandasではないしkaggleでも使うことも(たぶん)ありませんが、実務上便利なので掲載しておきます。
- jpholiday official
import jpholiday import datetime # 指定日が祝日か判定 jpholiday.is_holiday(datetime.date(2017, 1, 1)) # True jpholiday.is_holiday(datetime.date(2017, 1, 3)) # False # 指定月の祝日を取得 jpholiday.month_holidays(2017, 5) # [(datetime.date(2017, 5, 3), '憲法記念日'), # (datetime.date(2017, 5, 4), 'みどりの日'), # (datetime.date(2017, 5, 5), 'こどもの日')]
可視化
デザインを綺麗にするおまじない
このQiita記事に載っているおまじないを書いておくと、グラフがとても綺麗になるのでとてもおすすめです。
import matplotlib import matplotlib.pyplot as plt plt.style.use('ggplot') font = {'family' : 'meiryo'} matplotlib.rc('font', **font)
シンプルなグラフ
import pandas as pd import matplotlib as mpl import matplotlib.pyplot as plt # 基本 df['col_1'].plot() # 複数のカラムのプロットを 2x2 のタイル状に表示 # (カラム数がタイル数を超えていると怒られる) df.plot(subplots=True, layout=(2, 2)) # 上記でX軸,Y軸の共通化 df.plot(subplots=True, layout=(2, 2), sharex=True, sharey=True)
ヒストグラム
# ヒストグラム df['col_1'].plot.hist() # binを20に増やす / バーの幅を細くして間を開ける df['col_1'].plot.hist(bins=20, rwidth=.8) # X軸のレンジを指定 ## 0-100歳を5歳刻みで表示するイメージ df['col_1'].plot.hist(bins=range(0, 101, 5), rwidth=.8) # ヒストグラムが重なる時に透過させる df['col_1'].plot.hist(alpha=0.5) # Y軸の最小値・最大値を固定 df['col_1'].plot.hist(ylim=(0, 0.25))
箱ひげ図
df['col_1'].plot.box()
分布図
df.plot.scatter(x='col_1', y='col_2')
並列処理
- pandasでの処理は残念ながら速くはないと思います。BigQuery等と比較すると残念なレベルです。(まぁ処理の速さそのものを比較するのはアンフェアですが…。)
- 大量の特徴量を全て正規化するときや、大量の要素にmapをかける時とかは並列処理を駆使すると便利だと思います。
from multiprocessing import Pool, cpu_count def parallelize_dataframe(df, func, columnwise=False): num_partitions = cpu_count() num_cores = cpu_count() pool = Pool(num_cores) if columnwise: # 列方向に分割して並列処理 df_split = [df[col_name] for col_name in df.columns] df = pd.concat(pool.map(func, df_split), axis=1) else: # 行方向に分割して並列処理 df_split = np.array_split(df, num_partitions) df = pd.concat(pool.map(func, df_split)) pool.close() pool.join() return df # 適当な関数にDataFrameを突っ込んで列方向に並列処理する df = parallelize_dataframe(df, custom_func, columnwise=True)
- Make your Pandas apply functions faster using Parallel Processing (初出は別の記事だったと思いますが見当たらなかった…。)
'20/07/28 追記
- pandaparallelやswifterという並行処理を行ってくれるライブラリが充実してきているようです。
- 今後はこちらを使うのが良いかもしれません。
- 参考記事: たった数行でpandasを高速化する2つのライブラリ(pandarallel/swifter)
おまけ: Excel読み書き
kaggleでは使わないけど実務で使う人一定数いる? (僕は使ったことない)
# write df.to_excel('foo.xlsx', sheet_name='Sheet1') # read pd.read_excel('foo.xlsx', 'Sheet1', index_col=None, na_values=['NA'])
pandasを身につけるには?
まずは、おとなしく公式Tutorialに載ってるようなmaterialを以下のような順番で一通り回るのが最速かと思います。(可視化以外)
実践的な問題をやりたいときは前処理大全をやるのも良いかもですが、Kaggleコンペに参加する場合は公開Notebookを見ながら練習する程度でも十分かと思います。
リンク
おわりに
Kaggle関係の色々な記事を書いているので、良かったら読んでみてください〜。