86
90

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

R ユーザーへの pandas 実践ガイド

Last updated at Posted at 2019-07-24

概要

R で tidyverse (dplyr+tidyr) に使い慣れているが, Python に乗り換えると pandas がどうも使いにくい, と感じている人の視点で, Rの dplyr などとの比較を通して, pandas の効率的な使い方について書いています. そのため, 「R ユーザーへの」と書きましたが, R経験のない pandas ユーザーであってもなんらかの役に立つと思います. また, 自社インターン学生に対する教材も兼ねています. どちらかというと, 初歩を覚えたての初心者向けの記事となっています.

データ分析は一発で終わることはまずなく, 集計・前処理を探索的に行う必要があります. よって, プログラムを頻繁に書き直す必要があり, 普段以上に保守性のある書き方, 例えば参照透過性を考慮した書き方をしたほうが便利です. R の tidyverse の強みとして, 再帰代入をする必要がほとんどなく, 複雑な処理であってもかなりをワンライナーで書ける, というものがあります. 関数が, 集計, グループ化, 列の追加, というように, 機能の分け方が SQL 風になっているのもとっつきやすさにつながっていると思います. 一方で, Python のpandas は, メソッドチェーンで dplyr のパイプに近い機能を持たせるなど, tidyverse に影響された機能も多いですが, 普通に書こうとすると再帰代入を頻繁に要求されます.

結果として, pandas を効率的に書くテクニックは存在しますが, 公式ドキュメントではあまり強調されていません. そこで, ここでは, pandas で実現可能な, 効率的なデータ分析作業をする方法について紹介します.

導入

この記事の記述の多くは, 以下を参考にしています.

  1. Pandas 公式チートシートを翻訳しました
  2. Python/pandasのデータ処理で再帰代入撲滅委員会
  3. dplyr のアレを Pandas でやる
  4. dplyr使いのためのpandas dfplyすごい編

最低限, [1] を読んでいて, pandas の基本的な操作を知っていることを前提としています. そして今回は, 公式チートシートを踏まえて, より効率的なやり方をいろいろと紹介します. よって, ilocloc の違いなど, 公式チートシートに書いてある話はあえて繰り返すことはありません. また, 教材という目的のため, 上記 [2]-[4] で紹介されている内容の繰り返しになっている箇所も多いですが, ご容赦ください.

なお, 動作確認は python 3.6, pandas 0.25, dfply 0.3.3でやっています.

今回紹介するものは, 以下のようにして作成したデータフレームを使っています.

import pandas as pd
import numpy as np
from dfply import *

np.random.seed(42)

df = pd.DataFrame(
    {
        'x': np.random.normal(size=50*3),
        'y': np.random.normal(size=50*3),
        'col': list('ABCDE') * 10 * 3,
    })
pd.options.display.max_rows = 10  # 記事上の見やすさのため表示行数を制限
pd.options.display.precision = 2  # 記事上の見やすさのため表示桁数を制限

参考記事のおさらい

データフレームの再帰代入をやめる

このセクションは, 参考記事 [2] の内容と被る箇所がほとんどですが, いくつか異なる点もあります. 冗長ですが今一度確認します.

行の選択・列の作成

上記に挙げた記事では lambda 演算子を使う方法が紹介されていました.
これがなぜ必要なのか. 例えば, 以下の2つはどちらも同じです.

df.assign(z=lambda d: d['x']**2)
df.assign(z=df['x']**2)
x y col z
0 0.50 0.25 A 0.25
1 -0.14 0.35 B 0.02
2 0.65 -0.68 C 0.42
3 1.52 0.23 D 2.32
4 -0.23 0.29 E 0.05
... ... ... ... ...
145 0.78 -0.69 A 0.61
146 -1.24 0.90 B 1.53
147 -1.32 0.31 C 1.74
148 0.52 0.81 D 0.27
149 0.30 0.63 E 0.09

これだけだと何がいいのかわかりませんが, メソッドチェーンを連結していくと lambda が便利であることがわかります. 例えば,

  1. col の値ごとにグループ別合計をとり
  2. x の値が正の行だけを取り出し
  3. y の数値を2乗した z を新たに作成する

という処理を, lambda を使う場合と使わない場合とで考えてみます. 以下の1行目が lambda を使った場合, 2行目が使わない場合です. いずれも同じ結果を返します.

df.groupby('col').sum().loc[lambda d: d['x'] > 0].assign(z=lambda d: d['y']**2)
x y z
col
A 1.6 -1.64 2.68
B 1.9 3.28 10.77
df.groupby('col').sum().loc[df.groupby('col').sum()['x'] > 0].assign(
    z=df.groupby('col').sum().loc[df.groupby('col').sum()['x'] > 0]['y'] ** 2)
x y z
col
A 1.6 -1.64 2.68
B 1.9 3.28 10.77

メソッドチェーンはデータフレームを書き換えるわけではないので, 集計後の値を参照する場合は, このように同一の集計処理を何度も書く必要があり, 非常に冗長となります. 何度もやり直すことが普通なデータ分析の作業で, 同じ処理を何度も書き直すのは効率が悪く, 変更時に書き間違える可能性が増します.
このように, 処理が複雑になり, メソッドチェーンが深くなるほど lambda を使った参照透過性は重要になっていきます.

参考記事 [2] では, df.pipe を使って SQL の select-where 構文の処理を実行していますが, 参考記事 [3] のここ にあるように, このような処理は実際には df.loc で済むことが多いです. pipe メソッドは, 以下にあるようにデータフレーム全体に関数を適用したい際に使う (lambda も関数ですが) ものです.

Python pandas データのイテレーションと関数適用、pipe - StatsFragrments

すべての列 or 行に適用する mutate_all 相当の処理は, apply を使います. こちらでも lambda を使えます.

# mutate_all()
# s は DataFrame ではなく Series であることに注意
df.select_dtypes(exclude='object').apply(lambda s: s - [10] * s.shape[0], axis=0)
# row-wise に適用したい場合
df.select_dtypes(exclude='object').apply(lambda s: s - [10] * s.shape[0], axis=1)
x y
0 -9.50 -9.75
1 -10.14 -9.65
2 -9.35 -10.68
3 -8.48 -9.77
4 -10.23 -9.71
... ... ...
145 -9.22 -10.69
146 -11.24 -9.10
147 -11.32 -9.69
148 -9.48 -9.19
149 -9.70 -9.37

処理が多い場合は, dict で与える手もあります. リスト内包表記と組み合わせれば複雑な処理も比較的シンプルに書けるかもしれません.

df.assign(**{'a': lambda d: d['x']**2, 'col2': lambda d: d['col'] + '_2'})
x y col a col2
0 0.50 0.25 A 0.25 A_2
1 -0.14 0.35 B 0.02 B_2
2 0.65 -0.68 C 0.42 C_2
3 1.52 0.23 D 2.32 D_2
4 -0.23 0.29 E 0.05 E_2
... ... ... ... ... ...
145 0.78 -0.69 A 0.61 A_2
146 -1.24 0.90 B 1.53 B_2
147 -1.32 0.31 C 1.74 C_2
148 0.52 0.81 D 0.27 D_2
149 0.30 0.63 E 0.09 E_2

補足: assign と eval

dplyr使いのためのpandas 基本編 にあるように, eval メソッドは文字列を評価して, assign と同様のことができます. 参照透過性も担保されているため, 再帰代入も必要ありません. 最初の例で eval を使うなら, 以下のようになります.

df.groupby('col').sum().loc[lambda d: d['x'] > 0].eval('z = y**2')
x y z
col
A 1.6 -1.64 2.68
B 1.9 3.28 10.77

pandas でメモリに乗らない 大容量ファイルを上手に扱う - Stats Fragments』 の「eval による処理の最適化」のセクションに書かれているように, evalassign と違いメモリを消費しません. よって, 大容量のデータを扱うときは eval のほうが高速になります. しかし, これも引用記事に書かれていることですが, 評価できる演算子が限定されているため, 汎用性はありません(引用した記事は少し古いですが, 最新の 0.25 でもこの状況は変わっていません). 書きやすさの観点から, 私も引用記事の著者と同じ用に, 普段は assign を使い, 大容量なデータを扱う際にどうしてもネックになる処理だけ eval を使う, というやり方にしています.

参考: 公式リファレンス "Enhancing performance" 中の 『Expression evaluation via eval()

補足: ドットか括弧か

参考記事ではいずれも, 列を参照するとき, d.x という書き方をしていますが, この記事では d['x'] という書き方をしています. . で列にアクセスする場合は, 列名とデータフレームのメソッドと名前が重複していると, 列ではなくメソッドが呼び出されてしまいます1.
そこで, 名前の衝突回避のため lambda d: d['col'] のような書き方をすると安全です. ただし, 括弧の多用により, 少し見づらくなります. どちらを使うかは好みの問題でしょう.

補足: inplace を使うべきか

これも必ずこうしたほうがいい, というより好みの問題です. pandas のメソッドのほとんどはデータフレームを返すので, 結果を自身に代入するオプションとして, inplace 引数が多くのメソッドに用意されています. しかし, メソッドチェーンの中で inplace=True するとエラーになりますが, 末尾に inplace=True しても何もエラーが発生しません. また, assign など inplace がないメソッドもあります. そのため, 何度も処理の組み合わせを変更しているうちにおかしなコードになってしまう恐れがあるので, 私は inplace=True を使わず, なるべく = 代入の形で統一するようにしています.

列選択

上記の例では, assign.loc での行選択で lambda を使っていますが, 列選択にも使うことができます.

dplyr::select() に対応する操作は, df[['A', 'B']]df.loc[] です. 前者は列名のリストによる指定ですが, 後者は .loc を使うことでインデックスまたはブーリアンによる指定になっています.

しかし, もう少し複雑な条件で取り出したいときはもあります. そのような場合, 以下のようなメソッドが使えます.

  1. 正規表現で列名にマッチする df.filter
  2. 列の型で条件付けて取り出す df.select_dtypes
  3. dplyrstarts_with, ends_with, contains に対応する df.columns.str.startswith/endswith/contains

などがあります.

df.loc[lambda d: d['x'] > 0, lambda d: d.filter(regex='x').columns]
x
0 0.50
2 0.65
3 1.52
6 1.58
7 0.77
... ...
143 0.18
144 0.26
145 0.78
148 0.52
149 0.30
df.filter(regex='x').loc[lambda d: d['x'] > 0]
x
0 0.50
2 0.65
3 1.52
6 1.58
7 0.77
... ...
143 0.18
144 0.26
145 0.78
148 0.52
149 0.30

この例では, メソッドチェーンが浅いため, filter を先に持ってきて, lambda を使わないほうがむしろシンプルになります. しかし, もっと複雑な処理でも, このように lambda 内で .filter() など, pandas.DataFrame の持つ便利なメソッドを呼び出せることを覚えておいてください.

long to wide

wide (いわゆる横持ち) から long (いわゆる縦持ち) への変換は, pd.DataFrame.melt2tidyr::gather とほぼ同じなので省略します. 問題は long から wide への変換の場合です. 公式チートシートでは pivot を使えばできることになっていますが...

df.pivot(columns='col', values=['x', 'y'])
x y
col A B C D E A B C D E
0 0.50 NaN NaN NaN NaN 0.25 NaN NaN NaN NaN
1 NaN -0.14 NaN NaN NaN NaN 0.35 NaN NaN NaN
2 NaN NaN 0.65 NaN NaN NaN NaN -0.68 NaN NaN
3 NaN NaN NaN 1.52 NaN NaN NaN NaN 0.23 NaN
4 NaN NaN NaN NaN -0.23 NaN NaN NaN NaN 0.29
... ... ... ... ... ... ... ... ... ... ...
145 0.78 NaN NaN NaN NaN -0.69 NaN NaN NaN NaN
146 NaN -1.24 NaN NaN NaN NaN 0.90 NaN NaN NaN
147 NaN NaN -1.32 NaN NaN NaN NaN 0.31 NaN NaN
148 NaN NaN NaN 0.52 NaN NaN NaN NaN 0.81 NaN
149 NaN NaN NaN NaN 0.30 NaN NaN NaN NaN 0.63

この通り大失敗です.

そもそもこれは, pivot の名前の通り, 本来ピボットテーブルを作るのが目的のメソッドです. 行に対応する軸を指定しなければ, 意図したとおりに変形してくれません. これは unstack でも同様です.

今回は, A-E の5要素で1グループとしたいので, 変換後の行に対応する行インデックスを新たに作る必要があります. 以下のように

df.assign(
    row=lambda x: sorted(list(range(x.shape[0] // x.col.nunique())) * x.col.nunique())
).head(10)
x y col row
0 0.50 0.25 A 0
1 -0.14 0.35 B 0
2 0.65 -0.68 C 0
3 1.52 0.23 D 0
4 -0.23 0.29 E 0
5 -0.23 -0.71 A 1
6 1.58 1.87 B 1
7 0.77 0.47 C 1
8 -0.47 -1.19 D 1
9 0.54 0.66 E 1

列を作ってから pivot を呼び出します.

df.assign(
    row=lambda x: sorted(list(range(x.shape[0] // x.col.nunique())) * x.col.nunique())
).pivot(index='row', columns='col', values=['x', 'y'])
x y
col A B C D E A B C D E
row
0 0.50 -0.14 0.65 1.52 -0.23 0.25 0.35 -0.68 0.23 0.29
1 -0.23 1.58 0.77 -0.47 0.54 -0.71 1.87 0.47 -1.19 0.66
2 -0.46 -0.47 0.24 -1.91 -1.72 -0.97 0.79 1.16 -0.82 0.96
3 -0.56 -1.01 0.31 -0.91 -1.41 0.41 0.82 1.90 -0.25 -0.75
4 1.47 -0.23 0.07 -1.42 -0.54 -0.89 -0.82 -0.08 0.34 0.28
... ... ... ... ... ... ... ... ... ... ...
25 2.19 -0.99 -0.57 0.10 -0.50 0.46 0.20 -0.60 0.07 -0.39
26 -1.55 0.07 -1.06 0.47 -0.92 0.11 0.66 1.59 -1.24 2.13
27 1.55 -0.78 -0.32 0.81 -1.23 -1.95 -0.15 0.59 0.28 -0.62
28 0.23 1.31 -1.61 0.18 0.26 -0.21 -0.49 -0.59 0.85 0.36
29 0.78 -1.24 -1.32 0.52 0.30 -0.69 0.90 0.31 0.81 0.63

unstack でもほぼ同じです

df.assign(
    row=lambda x: sorted(list(range(x.shape[0] // x.col.nunique())) * x.col.nunique())
).set_index(['row', 'col']).unstack(1)
x y
col A B C D E A B C D E
row
0 0.50 -0.14 0.65 1.52 -0.23 0.25 0.35 -0.68 0.23 0.29
1 -0.23 1.58 0.77 -0.47 0.54 -0.71 1.87 0.47 -1.19 0.66
2 -0.46 -0.47 0.24 -1.91 -1.72 -0.97 0.79 1.16 -0.82 0.96
3 -0.56 -1.01 0.31 -0.91 -1.41 0.41 0.82 1.90 -0.25 -0.75
4 1.47 -0.23 0.07 -1.42 -0.54 -0.89 -0.82 -0.08 0.34 0.28
... ... ... ... ... ... ... ... ... ... ...
25 2.19 -0.99 -0.57 0.10 -0.50 0.46 0.20 -0.60 0.07 -0.39
26 -1.55 0.07 -1.06 0.47 -0.92 0.11 0.66 1.59 -1.24 2.13
27 1.55 -0.78 -0.32 0.81 -1.23 -1.95 -0.15 0.59 0.28 -0.62
28 0.23 1.31 -1.61 0.18 0.26 -0.21 -0.49 -0.59 0.85 0.36
29 0.78 -1.24 -1.32 0.52 0.30 -0.69 0.90 0.31 0.81 0.63

pandas.DataFrame.pivottidyr::spread と同じという説明はかなりミスリードです. pivot は複数列を同時に処理できますが, 行インデックスも指定しなければうまくいきません. 一方で spread は1列づつしか処理できませんが, 当然行インデックスは存在しないので不要です.

なお, tidyr 1.0 ではまた新たな構文が追加される予定らしい3 4 ですが, これに対応する機能は当然まだありません. 個人的には1関数1機能のほうが直感的でわかりやすいです.

unstackpivot の詳しい挙動は以下も参考にしてください.

データの要約

[3] の 『グループ化 dplyr::group_by と集約 dplyr::summarise』セクションにあるように, pandas で各列を要約する agg メソッドは, 任意の列に任意の要約関数 (sumcount など, 列の全要素から計算する関数群のこと) を適用する dplyr::summarise とは機能が異なります. すべての列に要約関数を適用する summarise_atsummarise_all に近いです.

基本的な要約関数ならば, agg を使わなくとも呼び出せます. 使える関数は, [1] などを参考にしてください.

df.groupby('col').mean()
x y
col
A 0.05 -0.05
B 0.06 0.11
C -0.11 0.16
D -0.07 -0.07
E -0.35 0.21
df.groupby('col').agg({'x': [np.min, np.max]})
x
amin amax
col
A -1.92 2.19
B -1.24 1.89
C -1.96 1.48
D -1.91 2.46
E -2.62 1.03

0.25 からの新機能

以下では, 最近追加された機能で特に便利なものをピックアップします

要約処理の改善

pandas 0.25 からは, pd.NamedAgg という新しい構文が追加され, NEW_COLUMN=pd.NamedAgg(column='COL', aggfunc=FUNCTION) という構文で, dplyr::summarise に近いことができるようになりました. 詳細は以下の公式ドキュメントを確認してください.

例えば, x 列に対して最大, 最小, 中央値をグループ別に集計し, それぞれ x_min, x_max, x_med という列名にしたい場合は, 以下のように,

df.groupby('col').agg(
    x_min=pd.NamedAgg('x', 'min'),
    x_max=pd.NamedAgg('x', 'max'),
    x_med=pd.NamedAgg('x', 'median')
)
x_min x_max x_med
col
A -1.92 2.19 0.17
B -1.24 1.89 -0.05
C -1.96 1.48 -0.02
D -1.91 2.46 0.05
E -2.62 1.03 -0.23

と書けます. さらに省略して, (列名, 関数) のタプルだけで表現できる構文も用意されています. これはかなり便利になりました.

df.groupby('col').agg(
    x_min=('x', 'min'),
    x_max=('x', 'max'),
    x_med=('x', 'median')
)
x_min x_max x_med
col
A -1.92 2.19 0.17
B -1.24 1.89 -0.05
C -1.96 1.48 -0.02
D -1.91 2.46 0.05
E -2.62 1.03 -0.23

ネスト/アンネスト

python で使う機会はあまりなさそうな気もしますが, R の tibble のように, データフレームの要素にさらにテーブル構造のデータが入れ子になっている場合を考えます.
pandastibble のようにデータフレームの要素にデータフレームを入れることもできますが, nest/unnest はサポートされていません.
ただし, 要素がリストやタプルであるなら, 0.25 以降は行に展開する explode メソッドが使えます.

以下では, それ以外の方法も紹介されています.

df_nested = pd.DataFrame({'x': [1, 2], 'y': [df, df]})

iloc で要素 (=データフレーム) を取り出せます.

df_nested.iloc[0, 1]
x y col
0 0.50 0.25 A
1 -0.14 0.35 B
2 0.65 -0.68 C
3 1.52 0.23 D
4 -0.23 0.29 E
... ... ... ...
145 0.78 -0.69 A
146 -1.24 0.90 B
147 -1.32 0.31 C
148 0.52 0.81 D
149 0.30 0.63 E

explode は正確には unnest と対応する処理ではありませんが, 場合によっては代用になります.

df_nested = pd.DataFrame({'a': [1, 2], 'y': [['A', 1], ['B', 2]]})
df_nested
a y
0 1 [A, 1]
1 2 [B, 2]
df_nested.explode('y')
a y
0 1 A
0 1 1
1 2 B
1 2 2

メソッドチェーンでできないことをできるように

しかし, 頻繁に必要になるもののメソッドチェーンで完結できない処理というものが pandas ではまだいくつかあります. ここでは, いくつか解消方法を紹介します.

列に MultiIndex がある場合

インデックスは tidyverse にはない機能でした. pandas に乗り換えたユーザーにとって一番戸惑うのが index の扱いだと思います. grouoby で集計したら index, データフレーム同士を結合したら index, pivot したら index, というように何かにつけて index が出てきます. とはいえ, 基本的には reset_index メソッドを使えば列に戻すことができるので, さほど問題にはなりません.

厄介なのは, 列名が MultiIndex になっている場合です. これは reset_insex で解消できません. これはまさに, 先ほど紹介した pivotunstack をしたとき, あるいは agg で複数の要約関数を適用した場合にも発生します.

df_test = df.assign(
    row=lambda x: sorted(list(range(x.shape[0] // x.col.nunique())) * x.col.nunique())
).set_index(['row', 'col']).unstack(1).reset_index()

列名が MultiIndex で入れ子になっている場合, タプルを使って列にアクセスします.

df_test.columns
MultiIndex([('row',  ''),
            (  'x', 'A'),
            (  'x', 'B'),
            (  'x', 'C'),
            (  'x', 'D'),
            (  'x', 'E'),
            (  'y', 'A'),
            (  'y', 'B'),
            (  'y', 'C'),
            (  'y', 'D'),
            (  'y', 'E')],
           names=[None, 'col'])
df_test[[('row', ''), ('x', 'A')]]
row x
col A
0 0 0.50
1 1 -0.23
2 2 -0.46
3 3 -0.56
4 4 1.47
... ... ...
25 25 2.19
26 26 -1.55
27 27 1.55
28 28 0.23
29 29 0.78

すると, 3点の問題が発生します:

  1. df.x のようにドットで列にアクセスできなくなる
  2. df.filter での列名マッチがしづらくなる
  3. 列を str で指定できなくなる

(1) はともかくとして, (2, 3) が問題です. seabornplotnine は便利ですが, タプルでは列を指定できません. これ以外にも文字列だけで列名を指定できたほうが便利な場面は多いです. そこで, タプルを文字列に置き換える必要がでてきますが, この機能をもつビルトインメソッドは存在しません.
この問題に対して, 以下では新しくメソッドを定義する方法が提案されています.

"Method chaining solution to drop column level in pandas DataFrame"

ここで提案されている方法を少し修正して, メソッドチェーン内で列インデックスの変換をする方法を紹介します.

def reset_column_index(self, inplace=False,  sep='_'):
    if inplace:
        self.columns = [sep.join(filter(None, tup)) for tup in self.columns]
    else:
        c = self.copy()
        c.columns = [sep.join(filter(None, tup)) for tup in c.columns]
        return c

# 横持ちに変換して multiindex 列を作成
df_wide = df.assign(
    row=lambda x: sorted(list(range(x.shape[0] // x.col.nunique())) * x.col.nunique())
).pivot(index='row', columns='col', values=['x', 'y'])

df_wide.pipe(reset_column_index)
x_A x_B x_C x_D x_E y_A y_B y_C y_D y_E
row
0 0.50 -0.14 0.65 1.52 -0.23 0.25 0.35 -0.68 0.23 0.29
1 -0.23 1.58 0.77 -0.47 0.54 -0.71 1.87 0.47 -1.19 0.66
2 -0.46 -0.47 0.24 -1.91 -1.72 -0.97 0.79 1.16 -0.82 0.96
3 -0.56 -1.01 0.31 -0.91 -1.41 0.41 0.82 1.90 -0.25 -0.75
4 1.47 -0.23 0.07 -1.42 -0.54 -0.89 -0.82 -0.08 0.34 0.28
... ... ... ... ... ... ... ... ... ... ...
25 2.19 -0.99 -0.57 0.10 -0.50 0.46 0.20 -0.60 0.07 -0.39
26 -1.55 0.07 -1.06 0.47 -0.92 0.11 0.66 1.59 -1.24 2.13
27 1.55 -0.78 -0.32 0.81 -1.23 -1.95 -0.15 0.59 0.28 -0.62
28 0.23 1.31 -1.61 0.18 0.26 -0.21 -0.49 -0.59 0.85 0.36
29 0.78 -1.24 -1.32 0.52 0.30 -0.69 0.90 0.31 0.81 0.63

修正点は2つです. 1つは, 今回では row 列のように, 列インデックスが1層しかないものはセパレータ "_" がつかないようになっています. もう1つは, メソッドとして与えるのではなく, .pipe で与えているという点です. この修正もほとんど個人的な好みの問題なので, 上記のリンクそのままの使い方でもかまいません.

再帰的な計算処理

assign, apply でできるのは, element/row/column-wise な処理です. また, groupby メソッドならばグループ別集計ができます. 1つ前の要素も参照して処理するようなことはできません. つまり, ラグ演算や累積的な集計処理です. いちおう, pandas にはラグ計算専用のメソッドがいくつか用意されています

  • cumsum (累積和)
  • cumprod (累積積)
  • cummax (累積最大値)
  • cummin (累積最小値)

なぜか累積平均がありませんが, チートシート[1] の expanding メソッドを使えば実現できます.

df.expanding().mean()
x y
0 0.50 0.25
1 0.18 0.30
2 0.34 -0.03
3 0.63 0.04
4 0.46 0.09
... ... ...
145 -0.07 0.06
146 -0.08 0.06
147 -0.09 0.06
148 -0.08 0.07
149 -0.08 0.07

グループ化にも適用できます.

df.groupby('col').expanding().mean()
x y
col
A 0 0.50 0.25
5 0.13 -0.23
10 -0.07 -0.48
15 -0.19 -0.26
20 0.14 -0.38
... ... ... ...
E 129 -0.34 0.15
134 -0.36 0.22
139 -0.39 0.19
144 -0.37 0.20
149 -0.35 0.21

移動平均を取りたいならば, rolling や, 指数加重移動平均を取る ewma メソッドがあります.

"Exponentially-weighted moving window functions"

しかし, より一般的な再帰式で表現されるような処理はこれらでは処理できません. つまり,
$$
x_{t+1} = f(x_t)\
t=0, 1, 2, ...
$$

のような, 行ごとの再帰的な計算をしたい場合です. これは R の dplyr でもできない処理ですが, tidyverse に属する purrr::accumulate を組み合わせるとできます.

pandas の場合, $f$ に対応する関数を与えるだけで計算してくれるメソッドは存在しません5. そこで, 簡易的な代替方法として, 以下のような関数を用意します. 以下ならば, 1階差分方程式で表現できる計算ができるはずです.

def recurrence(x, func):
    x = x.copy()
    for i in range(1, x.shape[0]):
        x[i] = func(x[i-1])
    return x

例として, $f(x) = 2(x+1)$ を適用します.

df.assign(x2=lambda d: recurrence(d['x'], lambda x: (x+1)*2))
x y col x2
0 0.50 0.25 A 4.97e-01
1 -0.14 0.35 B 2.99e+00
2 0.65 -0.68 C 7.99e+00
3 1.52 0.23 D 1.80e+01
4 -0.23 0.29 E 3.79e+01
... ... ... ... ...
145 0.78 -0.69 A 1.11e+44
146 -1.24 0.90 B 2.23e+44
147 -1.32 0.31 C 4.45e+44
148 0.52 0.81 D 8.91e+44
149 0.30 0.63 E 1.78e+45

ラムダが多すぎる

ここまで, 参照透過性のため lambda を多用してきましたが, そもそも dplyr では lambda すら必要ないため, pandas ではコードが冗長になりがちです. そこで, dplyr 風の構文を python 上で再現することを目指した dfply を紹介します.

日本語での使用法の解説は, 最初に挙げた参考記事 [4] があります.

見てわかるように, メソッド名が dplyr とそっくりで, パイプ演算子も >> として再現しています. 一方で, 現時点での dfply の問題点として,

  1. pandas.DataFrame ではない独自のクラスを返すため, dfply のメソッドで処理を完結しなければならない
  2. すべて python で書かれているため, (pandas に比べ) さほど早くない6.
  3. Python の構文上, >> の直後に改行することができない (コードが見づらくなる)

という点が挙げられます.

具体的な操作方法は, dplyr をすでに使っているのなら [4] を読めばあとは直感でできるので, ここでは詳しい説明を省略します.
以前 dfply を見つけたときは使いやすそうだと思いましたが, pandas のほうも lambda の利用や 0.25 以降の新機能によりだいぶ使いやすくなったので, 個人的にはそこまでして使う必要はないかな, という考えに移りつつあります. 特に大きなデータを扱う場合は.

  1. 典型例は .reset_index()index という列が作られたときです. df.index では通常列名ではなくメンバの index が呼び出されてしまいます.

  2. インターネット上のスニペットでは pd.melt を使っている例も多いですが, 0.20 以降はデータフレームのメソッドとしても呼び出せます.

  3. https://speakerdeck.com/yutannihilation/tidyr-pivot

  4. https://blog.atusy.net/2019/06/29/pivoting-tidyr-1-0-0/

  5. Issues では「そのうち追加したい」機能という程度の優先順位に位置づけられています. https://github.com/pandas-dev/pandas/issues/4567

  6. すべて確認したわけではないですが, たとえば pandas の sort_values と dfply の arrange とでは, 後者のほうが顕著に時間がかかります. 余計なコピーが発生しているため?

86
90
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
86
90

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?