Movatterモバイル変換


[0]ホーム

URL:


ML_BearのKaggleな日常

元WEBマーケターのMLエンジニアがKaggleに挑戦する日々を綴ります

Kaggleで戦いたい人のためのpandas実戦入門

この記事をはてなブックマークに追加

はじめに

  • 自分は元々pandasが苦手でKaggleコンペ参加時は基本的にBigQuery上のSQLで特徴量を作り、最低限のpandas操作でデータ処理をしていました。
  • しかし、あるコードコンペティションに参加することになり、pythonで軽快にデータ処理をこなす必要が出てきたので勉強しました。
  • そこで、当時の勉強メモをもとに「これだけ知っていればKaggleでそこそこ戦えるかな」と思っているpandasの主要機能をまとめました。
注記
  • 実戦入門 のつもりがほぼ辞書 になってしまいました orz
  • pandasとはなんぞや的な内容は書いていません
    (import pandasやDataFrameとは何かなど)
  • pandas1.0系でも動くように書いたつもりですが間違ってたらすみません

目次

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: xisnot'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 ...# 時間系データをparsedf = 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')

メモリ使用量削減の工夫

ファイルを読み込んだ直後にメモリ使用量削減するクセを付けておくと色々はかどります。

型変更

# 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)

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)]# INdf[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')

結合

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=Falsedf.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()# percentiledf['col_1'].quantile([0.25,0.75])# clippingdf['col_1'].clip(-4,6)# 99パーセンタイルでclippingdf['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_notebookdf['col_1'].progress_apply(lambda x: custom_func(x))

その他の書き換え (replace / np.where)

# replacedf['animal'] = df['animal'].replace('snake','python')# np.wheredf['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 colin cols  })

集約結果の活用例

ほぼイディオムだが、最初は慣れないと処理に手間取るので例を書いておく。

# 集約agg_df = df.groupby(['key_id'])\           .agg({'col_1': ['max','min']})# カラム名が max / min になり、どのキーのものか区別できないので修正する# マルチインデックスになっているのでバラして rename するagg_df.columns = ['_'.join(col)for colin 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)

時系列データでよく使う処理

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)
  1. pandasで時系列データをリサンプリングするresample, asfreq
  2. pandasの時系列データにおける頻度(引数freq)の指定方法

カテゴリ変数エンコーディング

カテゴリ変数エンコーディングの種類についてはこの資料が詳しい

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.preprocessingimport LabelEncoder# trainとtestに分かれているデータを一括でLabelEncodingする例cat_cols = ['category_col_1','category_col_2']for colin 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 colin 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)

文字の変換にはstr.translate()が便利

日付系処理

基本

# 基本: 読み込み時に変換忘れたときとか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 jpholidayimport datetime# 指定日が祝日か判定jpholiday.is_holiday(datetime.date(2017,1,1))# Truejpholiday.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 matplotlibimport matplotlib.pyplotas pltplt.style.use('ggplot') font = {'family' :'meiryo'}matplotlib.rc('font', **font)

シンプルなグラフ

import pandasas pdimport matplotlibas mplimport matplotlib.pyplotas 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 multiprocessingimport Pool, cpu_countdefparallelize_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_namein 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)

'20/07/28 追記

おまけ: Excel読み書き

kaggleでは使わないけど実務で使う人一定数いる? (僕は使ったことない)

# writedf.to_excel('foo.xlsx', sheet_name='Sheet1')# readpd.read_excel('foo.xlsx','Sheet1', index_col=None, na_values=['NA'])

pandasを身につけるには?

まずは、おとなしく公式Tutorialに載ってるようなmaterialを以下のような順番で一通り回るのが最速かと思います。(可視化以外)

  1. 10 Minutes to pandas
  2. Pandas cookbook
  3. Cheet Sheet
  4. Cookbook

実践的な問題をやりたいときは前処理大全をやるのも良いかもですが、Kaggleコンペに参加する場合は公開Notebookを見ながら練習する程度でも十分かと思います。

リンク

おわりに

Kaggle関係の色々な記事を書いているので、良かったら読んでみてください〜。

実践的なTips集

naotaka1128.hatenadiary.jp

コンペ参戦記

naotaka1128.hatenadiary.jp

naotaka1128.hatenadiary.jp

naotaka1128.hatenadiary.jp

naotaka1128.hatenadiary.jp

プロフィール
id:naotaka1128

プロダクトマネージャー←データアナリスト←WEBディレクター←グロースハッカー←デジタルマーケター←製造業(生産技術)という変な経歴をもつWEBの人。主食は麻婆豆腐、たまにラーメン。

検索

引用をストックしました

引用するにはまずログインしてください

引用をストックできませんでした。再度お試しください

限定公開記事のため引用できません。

読者です読者をやめる読者になる読者になる

[8]ページ先頭

©2009-2025 Movatter.jp