こんにちは。データ分析部のオギワラです。最近は「NANIMONO (feat.米津玄師)」をよく聞いています。 今回はPythonのデータ分析ライブラリであるPandasについて、実践的なテクニックを「データ処理」「データ集計(Group By)」「時系列処理」の3カテゴリに分けてご紹介していきます。
Pandasに関する基本的な内容については、前エントリーで既に紹介されているので、是非こちらもご一読して頂けると幸いです。
今回、サンプルデータとしてKaggle上に公開されているFourSquareの行動ログデータを利用しています。
FourSquare - NYC and Tokyo Check-ins | Kaggle
このデータは、2012年の4月から2013年2月まで間にFourSquareを利用したユーザの、以下のようなチェックイン情報が記載されています。
- ユーザID (user_id) - 入店したお店のカテゴリID (venue_category_id) - 入店したお店のカテゴリ (venue_category) - お店の緯度・経度情報 (lat, lng) - 入店時刻 (timestamp)
このデータを利用して実際の分析内容・シチュエーションにも触れながらPandasの実践テクニックをご紹介していきます。 また、今回使用したJupyter NotebookをGitHubのリポジトリに配置したので、実際の動作等を確認したい方はぜひ利用して下さい。
データ処理
データの取り出し(query)
今回のデータには197ものお店カテゴリが存在し全カテゴリを一度に分析することは困難です。そこで対象のお店カテゴリを交通機関に絞って見ます。そのような時にquery 関数を利用することができます。 query関数はSQLのクエリのように条件を記述することでデータの取り出しを行うことができます。
>>> #venue_categoryがAirtportまたはSubwayである行を抽出する >>> df.query("venue_category in ['Airport', 'Subway']") ''' user_id venue_category timestamp 10 589 Airport 2012-04-04 04:59:06+09:00 32 1876 Subway 2012-04-04 05:59:52+09:00 34 499 Subway 2012-04-04 06:04:04+09:00 '''
条件文に基づくデータ処理の適用(where)
今回のデータでは特に必要ありませんが、データによって欠損値や異常値が含まれていることがあります。これらの値を含む行を除く場合欠損値はdropna
、外れ値はquery
関数を利用することができます。しかし行を除くのではなく何かしらの値で置換したいと考えた場合、欠損値は fillna
関数で利用することができますが、異常値に対しては利用することができません。このような時にwhere関数を利用することができます。where関数は条件文に基づく行に対してのみ処理を適用することができます。(ex. 負値は0に置換する、異常値は平均値に置換するなど)
>>>#例: user_idが10以下の行に対して, +10加算する >>>#条件を満たさない行(other)に対して処理が適用されます >>>df['test'] = df['user_id'].where(cond=lambda x: x >10, other=lambda x: x + 10) ''' user_idが10以下であるため加算処理が適用されている user_id test 9168 1 11 ''' ''' user_idが10以上であるため加算処理が適用されていない user_id test 3659 2292 2292 '''
各行への関数の適用(apply)
各行に対して関数を適用したい時、apply関数を利用することができます。また複数の列に対して適用したい時は、行和を指定(axis=1
) することで適用することができます。
注意: 大規模なデータセットを分析している場合apply関数は非常に時間がかかるため、DataFrameを一旦リスト化・numpyでベクトル化して関数を適用するなどの工夫をした方が良いです。次回以降の高速化に関する内容も執筆していきたいと思っています。
>>>#user_idをハッシュ化する関数を適用する >>>df['hashed_user_id'] = df['user_id'].apply(_hash) #user_idを引数とする_hash関数を適用、新たな列を生成 >>> 緯度・経度情報から住所を取得する >>> df['address'] = df.apply(gis_to_address, axis=1) # 関数上で複数列(緯度・経度)を指定し住所を取得、新たな列を生成
データ集計(Group By)
カラム毎に異なる集計を適用する(agg)
地点・時間別でのチェックイン数の平均・中央値などを知りたい時に、agg関数を利用することでまとめて集計することができます。 今回は例として15分ごとに各店舗(カテゴリ)のチェックイン人数をカウントし、その後店舗(カテゴリ)ごとにチェックイン人数の平均・中央値を求めてみます。
>>># Round関数を適用し、timestampを15分間隔に丸める >>>df['15min'] = od.DatetimeIndex(df['timestamp']).round('15min') >>>#15分ごとの各店舗(カテゴリ)のチェックイン人数をカウント >>> number_of_people = df.groupby(['venue_category', '15min']).agg({'user_id': 'count'}).reset_index() ''' venue_category 15min user_id 10705 Subway 2012-04-04 07:30:00+09:00 8 10706 Subway 2012-04-04 07:45:00+09:00 12 10707 Subway 2012-04-04 08:00:00+09:00 12 10708 Subway 2012-04-04 08:15:00+09:00 12 10709 Subway 2012-04-04 08:30:00+09:00 12 10710 Subway 2012-04-04 08:45:00+09:00 20 ''' >>># 各店舗(カテゴリ)ごとにチェックイン人数の平均・中央値を算出 >>> number_of_people('venue_category').agg({'user_id': ['mean', 'median']}) ''' venue_category user_id mean median 0 Airport 1.207921 1 1 American Restaurant 1.055556 1 2 Antique Shop 1.000000 1 '''
最大・最小値である行を取り出す(first)
各ユーザの最終アクションに関するデータを抽出したい時、groupby及びfirst関数等を用いることで実現することができます。 具体的な流れは(1) 対象のカラム値でソートする(最大値は降順、最小値は昇順) (2) 最大・最小値を抽出したいカラムでグループ化(3) 各グループの最初の行を抽出する 以下に各ユーザの最終地点(timestampが最後の行)を抽出する例を示します。
>>>#(1) 時間順で降順でソート (2) ユーザIDでグループ化 (3) ユーザごとに最初の行を抽出 >>>df.sort_values('timestamp', ascending=False).groupby(['user_id'], as_index=False).first() ''' user_id venue_category timestamp 0 1 Train Station 2012-04-08 12:24:53+09:00 1 2 Train Station 2012-04-13 11:03:36+09:00 2 3 Smoke Shop 2012-04-13 07:26:35+09:00 3 4 Arcade 2012-04-14 11:24:53+09:00 4 6 Train Station 2012-04-08 21:36:10+09:00 '''
標準化や正規化処理を適用する(transform)
カテゴリごとに標準化・正規化したい時、groupby及びtransform関数を利用することができます。 以下に店舗(カテゴリ)ごとにチェックイン数を標準化する例を示します。
>>>#標準化関数 >>>zscore = lambda x: (x - x.mean()) / x.std() >>>number_of_people['standarized_num_people'] = number_of_people.groupby('venue_category').transform(zscore) ''' venue_category 15min num_people standarized_num_people 10703 Subway 2012-04-04 07:00:00+09:00 3 -0.501261 10704 Subway 2012-04-04 07:15:00+09:00 4 -0.208489 10705 Subway 2012-04-04 07:30:00+09:00 8 0.962599 '''
時系列処理
時間の丸め処理(round)
既に上記でも登場していましたが、日付や1時間単位での集計だけではなく、15分や3分など独自の時間間隔で集計したいことがあります。このような時、round関数を利用することができます。 round関数はtimestampを指定した間隔に丸めることができます。
>>>df['date'] = df['timestamp'].dt.date >>>df['hour'] = df['timestamp'].dt.hour >>>df['15min'] = pd.DatetimeIndex(df['timestamp']).round('15min') #15分間隔 ''' user_id venue_category timestamp date hour 15min 0 1541 Cosmetics Shop 2012-04-04 03:17:18+09:00 2012-04-04 3 2012-04-04 03:15:00+09:00 1 868 Ramen / Noodle House 2012-04-04 03:22:04+09:00 2012-04-04 3 2012-04-04 03:15:00+09:00 2 114 Convenience Store 2012-04-04 04:12:07+09:00 2012-04-04 4 2012-04-04 04:15:00+09:00 '''
時系列データの差分計算(diff), 変化率計算(pct_change), 移動平均値の計算(rolling)
時系列データの変動を分析したい時、前時刻との差分はdiff関数, 変化率はpct_change, 移動平均値はrolling関数を用いることで簡単に算出することができます。また、店舗(カテゴリ)別やuser_id別で算出したい場合もgroupbyと組み合わせることで実現することができます。
>>>count_people_div_15min['diff'] = count_people_div_15min['num_of_people'].diff() #差分 >>>count_people_div_15min['pct_change'] = count_people_div_15min['num_of_people'].pct_change() #変化率 >>>count_people_div_15min['pct_change'] = count_people_div_15min['num_of_people'].rolling(window=3, center=False).mean() #移動平均 ''' 15min num_of_people change pct_change rolling_mean 0 2012-04-04 03:15:00+09:00 2 NaN NaN NaN 1 2012-04-04 04:15:00+09:00 5 3.0 1.5 NaN 2 2012-04-04 04:30:00+09:00 1 -4.0 -0.8 2.666667 3 2012-04-04 04:45:00+09:00 2 1.0 1.0 2.666667 4 2012-04-04 05:00:00+09:00 2 0.0 0.0 1.666667 ''' >>>#店舗カテゴリ別で算出したい場合はgroupbyと組み合わせる >>>count_people_div_15min_venue_category['change'] = count_people_div_15min_venue_category.groupby('venue_category')['user_id'].diff() #店舗カテゴリ別差分 >>>count_people_div_15min_venue_category['pct_change'] = count_people_div_15min_venue_category.groupby('venue_category')['user_id'].pct_change() #店舗カテゴリ別変化率 >>>_rolling_mean = count_people_div_15min_venue_category.groupby('venue_category')['user_id'].rolling(window=3, center=False).mean() #店舗カテゴリ別移動平均 >>>count_people_div_15min_venue_category['rolling_mean'] = _rolling_mean.reset_index(level=0, drop=True) ''' venue_category 15min user_id change pct_change rolling_mean 10701 Subway 2012-04-04 06:00:00+09:00 2 NaN NaN NaN 10702 Subway 2012-04-04 06:45:00+09:00 2 0.0 0.000000 NaN 10703 Subway 2012-04-04 07:00:00+09:00 3 1.0 0.500000 2.333333 10704 Subway 2012-04-04 07:15:00+09:00 4 1.0 0.333333 3.000000 10705 Subway 2012-04-04 07:30:00+09:00 8 4.0 1.000000 5.000000 '''
Pandasと合わせて利用したい、おすすめライブラリ
Pandas及びJupyter Notebookでデータ分析していく際に是非利用した方が良いライブラリを2つご紹介します。
for文やapply関数などの進捗をバーで表示する(tqdm)
GitHub - tqdm/tqdm: A fast, extensible progress bar for Python and CLI
Pythonのfor文などの進捗を色付きバーでリアルタイムに表示してくれます。平常心を保ちながら大規模データを分析していくためにはこのライブラリは必須ですね。
Pandas向けの進捗バーも準備されており、tqdmを有効化した後にapply → progress_apply
, map → progress_map
に変更し実行することで表示されます。
>>> from tqdm import tqdm, tqdm_notebook >>> tqdm_notebook().pandas() # pandas向けtqdmを有効化 >>> df.progress_apply(f)
DataFrameをMarkdown形式で出力する(pytablewriter)
分析結果をGithubのissueなどに転記する時に利用しています。DataFrameを自動的にMarkdown形式で出力してくれます。
>>>import pytablewriter >>>writer = pytablewriter.MarkdownTableWriter() >>>writer.from_dataframe(count_people_by_15min_category.head(5)) # Markdownに変換したいDataFrameを入力する >>>writer.write_table() ''' venue_category| 15min |user_id|change|pct_change|rolling_mean --------------|------------------------|------:|-----:|---------:|-----------: Airport |2012-04-04T05:00:00+0900| 1| NaN| NaN| NaN Airport |2012-04-04T07:00:00+0900| 1| 0| 0| NaN Airport |2012-04-04T08:15:00+0900| 1| 0| 0| 1.00 Airport |2012-04-04T10:30:00+0900| 1| 0| 0| 1.00 Airport |2012-04-04T11:45:00+0900| 2| 1| 1| 1.33 '''
さいごに
Pandasには今回紹介したメソッド以外にも、数多くのメソッドが実装されています。ぜひPandasの公式ドキュメントも読んでみて下さい! また、最近Pandasのリポジトリ上に「Pandas Cheat Sheet」という基礎内容が一つのpdfファイルにまとまったシートが公開されているので、ぜひダウンロードしておくことをお勧めします!
参考資料
- Pandas公式ドキュメント pandas: powerful Python data analysis toolkit — pandas 0.20.1 documentation Pandasの公式ドキュメントは例を交えた説明が多く、非常にわかりやすいです。