🐼

【Python】【pandas入門】引数がfunc系のアイツラ ~オモテのDataFrame/Series編~

2025/01/19に公開

0. はじめに

map()apply()agg()など、引数に関数(func)を取るメソッドについて調査し、その内容をまとめた。これらのメソッドは、データを変換したり計算したりする際に非常に便利だが、その使い方や違いについては、少し混乱しがちな部分もあるため、整理することにした。

本記事では、pandas.DataFrameおよびpandas.Seriesにて定義されているメソッドを中心に扱うが、pandas.core.groupby.generic.DataFrameGroupBypandas.core.groupby.generic.SeriesGroupByについては、次回以降の「ウラのGroupBy編」で取り上げる予定である。

この記事の内容や動作確認は、pandas 2.2.3で行っている。

タイトルに「pandas入門」と記載しているが、入門の客観的・一般的な基準と個人的な基準に乖離があるかもしれない。世界がおかしいのか、私がおかしいのか。

1. 目的

pandasの「引数がfunc系のアイツラ」についてあやふやなので、まとめておこうと思い一筆したたためた次第。一筆の限度にも程がある。

"アイツラ" は同じような引数を持ち、我が物顔でpandas便利機能の顔を有しているが、結局のところお前らの違い、使い分けってなんなん?ってのを、駄文を書き連ねながら妄想したりすることで私の長期記憶の礎にしてやろうという魂胆であり、全ては自分の知識の為のエゴでこんなしょうもない事を書き連ねているんですよ、本当クズ野郎ですよね、昔っから私利私欲でしか動かない。

2. アイツラとは?

2-1. アイツラを名指しする

  • 本記事の対象

    • Seriesの対象メソッド
      map(),apply(),agg(),aggregate(),transform(), pipe()

    • DataFrameの対象メソッド
      applymap(),map(),apply(),agg(),aggregate(),transform(), pipe()

  • 次回以降の記事の対象

    • SeriesGroupByの対象メソッド
      apply(),agg(),aggregate(),transform(), pipe()

    • DataFrameGroupByの対象メソッド
      apply(),agg(),aggregate(),transform(), pipe()

GroupBy系のfilter()やDataFrameのassign()など、引数にlambdaを使用することがあるメソッドは他にも存在するが、これらは用途が明確に異なるため、ここでは取り上げない。

2-2. アイツラの継承関係

誰の子なのか認知することは長期記憶的にもライブラリ理解においても大切なことなので、まずは親子関係を整理する。クラス関係の話です。

今回のメソッドの話に関係のない継承関係は割愛。

3. 本記事のまとめ

記事が長くなってしまったため、結論先形式にする。詳細な内容は各項を参照。

3-1. 用途・特徴

メソッド名 用途・特徴
applymap() pandas 2.1.0以降では非推奨(Deprecated)。前方互換性以外の理由では使用しないべき。現在は内部的にmap()を呼び出しているだけなので、新規でコーディングする場合はmap()を使用する。DataFrameでのみ実装されている。
map() 引数として渡したfunc、辞書、pandas.Seriesを用いたマッピングによる値変換を行う。引数funcにはpandasオブジェクトの要素が1つずつ渡されるため、行や列単位での集計や計算には向かない。
apply() 引数として渡したfuncを適用する。funcを実行するために位置引数やキーワード引数をサポートしている。使用時には、by_rowrawなどのパラメータの要否の検討や、numbaや並列化を検討する。
agg() 引数として渡したfuncを使用して集計を行う。配列や辞書にfuncを格納して複数の集計を一度に実行できる。aggregate()のエイリアスであり、処理上の差異はない。
transform() 引数として渡したfuncを使用してデータを変換する。agg()と同様に、複数の処理を一度に実行できるようにfuncを配列などに設定して実行することが可能。返却される値は呼び出し元オブジェクトと同じサイズ(同じlen())になるため、集計用ではなく、正則化やNumPyufunc適用による計算に適している。
pipe() 引数として渡したfuncでパイプラインチェーンを構築する。funcにはselfが渡されるため、DataFrame内の特定の複数の列に対する処理が可能。その他のfunc系メソッドよりも圧倒的な処理性能を発揮するポテンシャルがあるが、本来の使い方とは異なる気がする。

3-2. funcに渡される値

ユーザー定義関数やlambdaを使用する場合、パラメーターに何が渡されるかを考慮する必要がある。例えば、「要素の値」が渡されるケースで、funclambda x: np.sum(x)である場合、funcxを返すが、この場合は意図した動作になっていない可能性があり、エラーも発生しない。
ユーザー定義関数やlambdaを使用しない場合は、特に考慮する必要はない。

クラス メソッド名 funcに渡される値
Series map() 要素の値
Series apply() by_row=Falseの場合、pandas.Series(self)
それ以外の場合、要素の値
Series agg() 要素の値。ValueErrorAttributeErrorTypeErrorのいずれかが発生した場合、pandas.Series
Series transform() 要素の値。Exceptionが発生した場合、pandas.Series
Series pipe() pandas.Series
DataFrame applymap() 要素の値
DataFrame map() 要素の値
DataFrame apply() raw=Trueの場合、numpy.ndarray
それ以外の場合、pandas.Series
DataFrame agg() pandas.Series
DataFrame transform() pandas.Series
DataFrame pipe() pandas.DataFrame

基本的に、Seriesオブジェクトでは要素の値が、DataFrameオブジェクトではpandas.Seriesfuncに渡されるが、例外パターンは以下の通り。

  • map()funcにはどのような状況下でも要素の値が渡される。
  • pipe()funcにはself(自分自身)のオブジェクトが渡される。
  • apply()は、パラメーターに応じてfuncに渡されるオブジェクトが異なる。
  • Series.agg()Series.transform()は、最初に要素の値をfuncに渡して計算を行い、エラーが発生した場合は、Series(self)自体をfuncに渡して再度計算を行う。

3-3. 性能差

  • 値の計算での用途について、apply()が最も速い可能性がある。
  • 値の計算での用途について、map()は最も性能が悪い可能性がある。
  • 値のマッピング変換用途では、map()で変換用の辞書をパラメータに使うのが最も速い可能性がある。
  • 環境依存なので、性能確認の結果を全ての環境で保証することはできない

4. applymap()メソッド(Deprecated)

https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.applymap.html

applymap()はpandas 2.1.0にてDeprecatedとなり、現在はmap()の使用が推奨されている。
なので、基本的に前方互換の目的以外では使う必要はなさそう。ただし、書籍によっては登場したりするので一応まとめておく。

applymap()DataFrameでのみ定義されている。
なお、現在のapplymap()はDeprecatedである旨のWarningを表示してmap()メソッドを呼び出しているだけなので、DataFrame.map()の利用と等価。

https://github.com/pandas-dev/pandas/blob/v2.2.3/pandas/core/frame.py#L10470-L10522

4-1. インターフェース

DataFrame.applymap(func, na_action=None, **kwargs) -> DataFrame

パラメータ デフォルト 説明(Google翻訳)
func callable - Python 関数。単一の値から単一の値を返します。
na_action {None, ‘ignore’} None "ignore"の場合は、NaN 値を func に渡さずに伝播します。
**kwargs - - func に渡す追加のキーワード引数。

4-2. サンプルコード

na_actionのGoogle翻訳が不穏だから、そのあたりくわしく。

import pandas as pd

data = {
    "hoge1": [100000, 20000, 3000],
    "hoge2": [40000, 5000, 600],
    "hoge3": [7000, 800, 90],
}
df = pd.DataFrame(data)

# NAを仕込む
df.iloc[0, 0] = pd.NA

df
hoge1 hoge2 hoge3
0 NaN 40000 7000
1 20000.0 5000 800
2 3000.0 600 90

まずは、na_actionはデフォルトのまま、そのままの風味を味わおう。

df.applymap(lambda x: len(str(x)))
hoge1 hoge2 hoge3
0 3 5 4
1 7 4 3
2 6 3 2

次は少し味変で、na_action="ignore"を指定して味や香り、素材のハーモニーを楽しむ。

df.applymap(lambda x: len(str(x)), na_action="ignore")
hoge1 hoge2 hoge3
0 NaN 5 4
1 7.0 4 3
2 6.0 3 2

なるほど。味については多く語れることはないが、納得感のある結果だと言える。
NaNが関数に渡されない、処理されないまま、産まれたままの自然な姿で顕現した。

さいごにキーワード引数も使ったしょーもない例もひとつ。

df.applymap(lambda x, kw1: kw1 + str(x), na_action="ignore", kw1="くわしく")
hoge1 hoge2 hoge3
0 NaN くわしく40000 くわしく7000
1 くわしく20000.0 くわしく5000 くわしく800
2 くわしく3000.0 くわしく600 くわしく90

くわしくなりすぎだっつうの(笑)

5. map()メソッド - 写像を取る

map()はもともとSeriesでのみ定義されていたが、バージョン2.1.0でapplymap()がDeprecatedされ、DataFrameでもmap()が定義された。
map()は、値のマッピングを通じた変換を利用目的としている。DataFrame側でapplymap()という異なる名前でメソッドが実装されていたのは、2つのメソッドの用途が異なることを設計者は強調したかったのか?

5-1. pandas.Series.map()

5-1-1. インターフェース

https://pandas.pydata.org/docs/reference/api/pandas.Series.map.html#pandas.Series.map

Series.map(arg, na_action=None) -> Series

パラメータ デフォルト 説明(Google翻訳)
arg function,
collections.abc.Mapping subclass or Series
- マッピング対応。
na_action {None, ‘ignore’} None "ignore"の場合は、NaN 値を func に渡さずに伝播します。

5-1-2. サンプルコード

import numpy as np
import pandas as pd

ser = pd.Series(["北海道", "秋田", "沖縄", "四国", "ペルシャ", "ベンガル", "ノルウェージャンフォレストキャット"])

map()メソッドを使い、Seriesの要素が犬 or 猫 or 芋にマッピングする。

ser.map(
    {
        "ノルウェージャンフォレストキャット": "猫",
        "アメリカンショートヘア": "猫",
        "ロシアンブルー": "猫",
        "ペルシャ": "猫",
        "ベンガル": "猫",
        "アンデスレッド": "芋",
        "インカの目覚め": "芋",
        "柴": "犬",
        "四国": "犬",
        "北海道": "犬",
        "紀州": "犬",
        "秋田": "犬",
        "甲斐": "犬",
    }
)
0      犬
1      犬
2    NaN
3      犬
4      猫
5      猫
6      猫
dtype: object

残念ながら芋に該当する要素は存在しなかった。

このようなマッピングによる値の変換についてmap()は最適化されており、apply()よりも良いパフォーマンスを発揮するらしい。

もちろん、lambdaをはじめとした関数のたぐいも対応しているので、以下のようなゴミカスコードでも当然動作する。

ser.map(lambda x: True if len(x) > 3 else not (lambda x: True if len(x) < 5 else False) or not not not not not (lambda x: True if len(x) > 4 else False))
0    False
1    False
2    False
3    False
4     True
5     True
6     True
dtype: bool

5-2. pandas.DataFrame.map()

https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.map.html#pandas.DataFrame.map

前述の通り、applymap()が内部的に呼び出しているのはこのmap()メソッドなので、メソッドの仕様はapplymap()の項目を参照。

6. apply()メソッド - 適用する

apply()メソッドはSeries、DataFrameどちらにも定義されている。

以下は「apply()を使用する必要があるかどうか」についてStackOverflowのポスト。

https://stackoverflow.com/questions/54432583/when-should-i-not-want-to-use-pandas-apply-in-my-code

英語弱者なのでざっくりとしか内容を読むことはできなかったが、高い汎用性であるが故に、適用する関数の性質を何も定めていないため、必要に応じて関数を各行や列に繰り返し適用、その処理毎にオーバーヘッドが発生するため性能やメモリの消費が最適化されていないとのこと。
処理対象となるデータ件数、適用する処理が要素単位で処理を行うのか、ベクトル単位で処理を行うのか、などを考慮すべき事項が色々あって、上級者向きかな?
上級者ではない私には触れずらい、まさに腫物のような存在のメソッドである。
上記StackOverflowのポストも古い記事なので、現在のバージョンのpandasでどうかは確認する必要があるだろう。

numba(JITコンパイラ)とか並列化とか色々、apply()を高速化する手法は存在するようだが、諸々の処理速度検証はいつか別途行うとして(しなさそう)、まずは基本的な機能を抑えたい。

6-1. pandas.Series.apply()

6-1-1. インターフェース

https://pandas.pydata.org/docs/reference/api/pandas.Series.apply.html#pandas.Series.apply

Series.apply(func, convert_dtype=<no_default>, args=(), *, by_row='compat', **kwargs) -> DataFrame | Series

パラメータ デフォルト 説明(Google翻訳)
func function - 適用するPython関数、またはNumPyユニバーサル関数。[1]
convert_dtype bool True version 2.1.0でDeprecated。
要素ごとの関数の結果に対して、より適切な dtype を探します。False の場合は、dtype=object のままにします。バージョン 2.1.0 以降では convert_dtype=False にしたい場合は、代わりに ser.astype(object).apply() を実行してください。Categorical などの一部の拡張配列 dtype では、dtype が常に保持されることに注意してください。
args tuple - Seriesの値の後に関数に渡される位置引数。
by_row False or "compat" "compat" "compat" かつ func が呼び出し可能であれば、Series.map のように、Series の各要素が func に渡されます。func が呼び出し可能のリストまたは辞書である場合、最初に各 func を pandas メソッドに変換しようとします。それが機能しない場合は、by_row="compat" で再度 apply を呼び出し、それが失敗した場合は、by_row=False (下位互換性あり) で再度 apply を呼び出します。False の場合、func には Series 全体が一度に渡されます。

func が文字列の場合、by_row は効果がありません。
**kwargs - - func に渡す追加のキーワード引数。

**kwargsによるキーワード引数渡しだけでなく、argsによる位置引数にも対応している点は、関数を適用(apply)することに重点を置いたメソッドであり、map()と違いを垣間見ることができるインターフェースだというのが所感。

6-1-2. サンプルコード

by_rowパラメータの挙動による差異がわかりやすい例が良い。

import pandas as pd
import numpy as np

# サンプルデータ, 0 ~ 2πのデータを10000件等間隔で作成
start, end = 0, 2 * np.pi
samlpe_size = 10000
ser = pd.Series(np.arange(start, end, (end - start) / samlpe_size))

# applyから呼ばれるヤツ
def apply_kara_yobareru_yatsu():
    first_call = True

    def closure_func(x):
        nonlocal first_call
        
        # 初回呼び出し時のみ行う処理
        if first_call:
            print(f'Type of x: {type(x)}')
            first_call = False
        
        return np.sin(x) ** 2
    
    return closure_func

app1 = apply_kara_yobareru_yatsu()
app2 = apply_kara_yobareru_yatsu()

%timeit ser.apply(app1)
%timeit ser.apply(app2, by_row=False)

# 台形法による数値積分
print(np.trapz(ser.apply(app1), ser))
print(np.trapz(ser.apply(app2, by_row=False), ser))
Type of x: <class 'float'>
23.3 ms ± 1.55 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
Type of x: <class 'pandas.core.series.Series'>
404 μs ± 22.7 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
3.1415926534657683
3.1415926534657683

applyから呼ばれるヤツ()という関数を定義して、by_row="compat"(デフォルト)、by_row=Falseそれぞれでの呼び出しによる動作の違い、処理時間の違い、ついでに本項末尾の数式の数値計算シミュレーション結果を確認するサンプルコード。
applyから呼ばれるヤツ()は呼び出し初回時のみパラメータの型を表示している。クロージャー久々に使った。

  • by_row="compat"(デフォルト)

    • apply()を介してapp1に渡されるのはserの各要素のため、<class 'float'>と表示される。
    • serの各要素に対してnp.sin(x)**2を計算しているため、処理時間が20 msぐらいかかっている。
  • by_row=False

    • apply()を介してapp2に渡されるのはser自体のため、<class 'pandas.core.series.Series'>と表示される。
    • ser自体にnp.sin(x)**2を計算しているため、処理時間がはええ。
\begin{aligned} \int^{2 \pi}_{0} \sin^{2} x dx &= \int^{2 \pi}_{0} \frac{1 - \cos(2x)}{2} dx \\ &= \frac{1}{2} \int^{2 \pi}_{0} 1 dx - \frac{1}{2} \int^{2 \pi}_{0} \cos(2x) dx \\ &= \frac{1}{2} \cdot 2 \pi - \frac{1}{2} \cdot 0 \\ &= \pi \end{aligned}

6-2. pandas.DataFrame.apply()

6-2-1. インターフェース

https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.apply.html#pandas.DataFrame.apply

DataFrame.apply(func, axis=0, raw=False, result_type=None, args=(), by_row='compat', engine='python', engine_kwargs=None, **kwargs) -> DataFrame | Series

パラメータ デフォルト 説明(Google翻訳)
func function - 各列または行に適用する関数。
axis {0 or 'index', 1 or 'columns'} 0 関数が適用される軸:
0 または'index': 各列に関数を適用します。
1 または'columns': 各行に関数を適用します。
raw bool False 行または列が Series または ndarray オブジェクトとして渡されるかどうかを決定します:
False: 各行または列を関数に Series として渡します。
True: 渡された関数は代わりに ndarray オブジェクトを受け取ります。NumPy reduction function[^2]を適用するだけの場合は、これによりパフォーマンスが大幅に向上します。
result_type {'expand', 'reduce’, 'broadcast', None} None これらは axis=1 (列) の場合にのみ機能します:
'expand': リストのような結果が列に変換されます。
'reduce': リストのような結果を展開するのではなく、可能な場合は Series を返します。これは 'expand' の反対です。
'broadcast': 結果は DataFrame の元の形状にブロードキャストされ、元のインデックスと列は保持されます。
デフォルトの動作 (なし) は、適用された関数の戻り値によって異なります。リストのような結果は、それらの Series として返されます。ただし、apply 関数が Series を返す場合、これらは列に展開されます。
args tuple () 配列/シリーズに加えて func に渡す位置引数。
by_row False or 'compat' 'compat' func が関数のリストのような、または関数の辞書のようなものであり、関数が文字列でない場合にのみ効果があります。「compat」の場合、可能であれば最初に関数を pandas メソッドに変換します (例: Series().apply(np.sum) は Series().sum() に変換されます)。それが機能しない場合は、by_row=True で再度 apply を呼び出し、それが失敗した場合は、by_row=False (下位互換性あり) で再度 apply を呼び出します。False の場合、関数には Series 全体が一度に渡されます。
engine {'python', 'numba'} 'python' 適用時に Python (デフォルト) エンジンまたは numba エンジンを選択します。
numba エンジンは渡された関数を JIT コンパイルしようとします。これにより、大きな DataFrame の速度が向上する可能性があります。また、次の engine_kwargs もサポートされています:
nopython (関数を nopython モードでコンパイルします)
nogil (JIT コンパイルされた関数内で GIL を解放します)
parallel (DataFrame に関数を並列に適用します)
注: numba 内の制限、または pandas と numba のインターフェイス方法により、raw=True の場合にのみこれを使用する必要があります
注: numba コンパイラは、有効な Python/numpy 操作のサブセットのみをサポートします。
渡された関数で何が使用できるか、何が使用できないかを確認するには、numba でサポートされている Python 機能とサポートされている numpy 機能の詳細をお読みください。
engine_kwargs dict None キーワード引数をエンジンに渡します。これは現在 numba エンジンでのみ使用されます。詳細については、エンジン引数のドキュメントを参照してください。
**kwargs - - キーワード引数として func に渡す追加のキーワード引数。

基本的には、Series.apply()メソッドに適用軸に関するパラメータ、numbaサポートに関するパラメータが追加されたイメージ。細かい部分で違いがあり、Series.apply()ではby_rowによってfuncに渡されるのがSeriesの要素かSeries自身かが変わっていたが、DataFrame.apply()ではrawパラメータによってそのあたりが決まってくるようだ。

全体的に型に応じた処理分岐などのオーバーヘッドを短縮するためのパラメーターであったり、高速化のためのnumbaサポートやby_rowなどなど・・・「遅い」と言われ続けコンプレックスを感じてしまっているが、そんな黒歴史を塗りつぶさんとするが如く、「諦めない、きっと見返してやるんだから!」という感じで努力を重ねている様子を垣間見ることができるインターフェースである。非常に好感度が高い。建設的な努力には敬意を感じる。

6-2-2. サンプルコード

個人的に確認したい事項

  • axis=0axis=1での性能差があるらしい。この目で確かめてえんだ。
  • rawby_rowに応じた性能差。
  • 処理の種類に応じた各パラメータの性能差。
    • NumPy ufunc, NumPy reducing function, pandas集計関数

まずは、NumPy ufunc(np.sin())に対する各パラメータの性能差を確認する。
1万×1万の単位行列に対して、axis=0axis=1方向の演算を各by_row,rawパラメータで行う。

import pandas as pd
import numpy as np

# サンプルデータ
samlpe_size = 10000
mat_i = pd.DataFrame(np.eye(samlpe_size))

# applyから呼ばれるヤツ
def apply_np_sin():
    first_call = True

    def closure_func(x):
        nonlocal first_call
        
        # 初回呼び出し時のみ行う処理
        if first_call:
            print(f'Type of x: {type(x)}')
            first_call = False
        
        return np.sin(x)
    
    return closure_func


# 性能計測 : numpy ufuncに対するapply - axis, by_row, rawに応じた性能の確認
print("-------------------- by_row='compat' raw=False / axis=0 vs axis=1 --------------------")
print("=== axis=0 by_row='compat' raw=False")
func = apply_np_sin()
%timeit mat_i.apply(func, axis=0, by_row="compat", raw=False)
print("=== axis=1 by_row='compat' raw=False")
func = apply_np_sin()
%timeit mat_i.apply(func, axis=1, by_row="compat", raw=False)
print()
print("-------------------- by_row=False raw=False / axis=0 vs axis=1 --------------------")
print("=== axis=0 by_row=False raw=False")
func = apply_np_sin()
%timeit mat_i.apply(func, axis=0, by_row=False, raw=False)
print("=== axis=1 by_row=False raw=False")
func = apply_np_sin()
%timeit mat_i.apply(func, axis=1, by_row=False, raw=False)
print()
print("-------------------- by_row='compat' raw=True / axis=0 vs axis=1 --------------------")
print("=== axis=0 by_row='compat' raw=False")
func = apply_np_sin()
%timeit mat_i.apply(func, axis=0, by_row="compat", raw=True)
print("=== axis=1 by_row='compat' raw=False")
func = apply_np_sin()
%timeit mat_i.apply(func, axis=1, by_row="compat", raw=True)
print()
print("-------------------- by_row=False raw=True / axis=0 vs axis=1 --------------------")
print("=== axis=0 by_row=False raw=False")
func = apply_np_sin()
%timeit mat_i.apply(func, axis=0, by_row=False, raw=True)
print("=== axis=1 by_row=False raw=False")
func = apply_np_sin()
%timeit mat_i.apply(func, axis=1, by_row=False, raw=True)
-------------------- by_row='compat' raw=False / axis=0 vs axis=1 --------------------
=== axis=0 by_row='compat' raw=False
Type of x: <class 'pandas.core.series.Series'>
5.11 s ± 156 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
=== axis=1 by_row='compat' raw=False
Type of x: <class 'pandas.core.series.Series'>
3.2 s ± 40.9 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

-------------------- by_row=False raw=False / axis=0 vs axis=1 --------------------
=== axis=0 by_row=False raw=False
Type of x: <class 'pandas.core.series.Series'>
5.3 s ± 203 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
=== axis=1 by_row=False raw=False
Type of x: <class 'pandas.core.series.Series'>
3.18 s ± 102 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

-------------------- by_row='compat' raw=True / axis=0 vs axis=1 --------------------
=== axis=0 by_row='compat' raw=False
Type of x: <class 'numpy.ndarray'>
1.55 s ± 33.3 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
=== axis=1 by_row='compat' raw=False
Type of x: <class 'numpy.ndarray'>
502 ms ± 14.3 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

-------------------- by_row=False raw=True / axis=0 vs axis=1 --------------------
=== axis=0 by_row=False raw=False
Type of x: <class 'numpy.ndarray'>
1.57 s ± 45.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
=== axis=1 by_row=False raw=False
Type of x: <class 'numpy.ndarray'>
496 ms ± 8.51 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

わかること

  • axis=0axis=1の性能差について、axis=0の方が遅かった。
  • raw=Trueにしたほうが性能が良い。

続いて、NumPy reducing fanction (np.sum())に対する性能を確認。

# applyから呼ばれるヤツ2
def apply_np_sum():
    first_call = True

    def closure_func(x):
        nonlocal first_call
        
        # 初回呼び出し時のみ行う処理
        if first_call:
            print(f'Type of x: {type(x)}')
            first_call = False
        
        return np.sum(x)
    
    return closure_func


# 性能計測 : numpy reducing functionに対するapply - axis, by_row, rawに応じた性能の確認
print("-------------------- by_row='compat' raw=False / axis=0 vs axis=1 --------------------")
print("=== axis=0 by_row='compat' raw=False")
func = apply_np_sum()
%timeit mat_i.apply(func, axis=0, by_row="compat", raw=False)
print("=== axis=1 by_row='compat' raw=False")
func = apply_np_sum()
%timeit mat_i.apply(func, axis=1, by_row="compat", raw=False)
print()
print("-------------------- by_row=False raw=False / axis=0 vs axis=1 --------------------")
print("=== axis=0 by_row=False raw=False")
func = apply_np_sum()
%timeit mat_i.apply(func, axis=0, by_row=False, raw=False)
print("=== axis=1 by_row=False raw=False")
func = apply_np_sum()
%timeit mat_i.apply(func, axis=1, by_row=False, raw=False)
print()
print("-------------------- by_row='compat' raw=True / axis=0 vs axis=1 --------------------")
print("=== axis=0 by_row='compat' raw=False")
func = apply_np_sum()
%timeit mat_i.apply(func, axis=0, by_row="compat", raw=True)
print("=== axis=1 by_row='compat' raw=False")
func = apply_np_sum()
%timeit mat_i.apply(func, axis=1, by_row="compat", raw=True)
print()
print("-------------------- by_row=False raw=True / axis=0 vs axis=1 --------------------")
print("=== axis=0 by_row=False raw=False")
func = apply_np_sum()
%timeit mat_i.apply(func, axis=0, by_row=False, raw=True)
print("=== axis=1 by_row=False raw=False")
func = apply_np_sum()
%timeit mat_i.apply(func, axis=1, by_row=False, raw=True)
-------------------- by_row='compat' raw=False / axis=0 vs axis=1 --------------------
=== axis=0 by_row='compat' raw=False
Type of x: <class 'pandas.core.series.Series'>
3.9 s ± 181 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
=== axis=1 by_row='compat' raw=False
Type of x: <class 'pandas.core.series.Series'>
970 ms ± 24.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

-------------------- by_row=False raw=False / axis=0 vs axis=1 --------------------
=== axis=0 by_row=False raw=False
Type of x: <class 'pandas.core.series.Series'>
3.87 s ± 149 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
=== axis=1 by_row=False raw=False
Type of x: <class 'pandas.core.series.Series'>
986 ms ± 27 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

-------------------- by_row='compat' raw=True / axis=0 vs axis=1 --------------------
=== axis=0 by_row='compat' raw=False
Type of x: <class 'numpy.ndarray'>
1.26 s ± 18 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
=== axis=1 by_row='compat' raw=False
Type of x: <class 'numpy.ndarray'>
271 ms ± 9.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

-------------------- by_row=False raw=True / axis=0 vs axis=1 --------------------
=== axis=0 by_row=False raw=False
Type of x: <class 'numpy.ndarray'>
1.26 s ± 18.3 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
=== axis=1 by_row=False raw=False
Type of x: <class 'numpy.ndarray'>
263 ms ± 5.33 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
  • NumPy ufunc同様、axis=0の方が遅かった。
  • NumPy ufunc同様、raw=Trueにしたほうが性能が良い。

続いて、pands集計関数(pd.sum())での性能を計測。

# applyから呼ばれるヤツ3
def apply_pd_sum():
    first_call = True

    def closure_func(x):
        nonlocal first_call
        
        # 初回呼び出し時のみ行う処理
        if first_call:
            print(f'Type of x: {type(x)}')
            first_call = False
        
        return x.sum()
    
    return closure_func


# 性能計測 : pandas sum に対するapply - axis, by_row, rawに応じた性能の確認
print("-------------------- by_row='compat' raw=False / axis=0 vs axis=1 --------------------")
print("=== axis=0 by_row='compat' raw=False")
func = apply_pd_sum()
%timeit mat_i.apply(func, axis=0, by_row="compat", raw=False)
print("=== axis=1 by_row='compat' raw=False")
func = apply_pd_sum()
%timeit mat_i.apply(func, axis=1, by_row="compat", raw=False)
print()
print("-------------------- by_row=False raw=False / axis=0 vs axis=1 --------------------")
print("=== axis=0 by_row=False raw=False")
func = apply_pd_sum()
%timeit mat_i.apply(func, axis=0, by_row=False, raw=False)
print("=== axis=1 by_row=False raw=False")
func = apply_pd_sum()
%timeit mat_i.apply(func, axis=1, by_row=False, raw=False)
print()
print("-------------------- by_row='compat' raw=True / axis=0 vs axis=1 --------------------")
print("=== axis=0 by_row='compat' raw=False")
func = apply_pd_sum()
%timeit mat_i.apply(func, axis=0, by_row="compat", raw=True)
print("=== axis=1 by_row='compat' raw=False")
func = apply_pd_sum()
%timeit mat_i.apply(func, axis=1, by_row="compat", raw=True)
print()
print("-------------------- by_row=False raw=True / axis=0 vs axis=1 --------------------")
print("=== axis=0 by_row=False raw=False")
func = apply_pd_sum()
%timeit mat_i.apply(func, axis=0, by_row=False, raw=True)
print("=== axis=1 by_row=False raw=False")
func = apply_pd_sum()
%timeit mat_i.apply(func, axis=1, by_row=False, raw=True)
-------------------- by_row='compat' raw=False / axis=0 vs axis=1 --------------------
=== axis=0 by_row='compat' raw=False
Type of x: <class 'pandas.core.series.Series'>
3.45 s ± 40.6 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
=== axis=1 by_row='compat' raw=False
Type of x: <class 'pandas.core.series.Series'>
758 ms ± 10.6 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

-------------------- by_row=False raw=False / axis=0 vs axis=1 --------------------
=== axis=0 by_row=False raw=False
Type of x: <class 'pandas.core.series.Series'>
3.58 s ± 121 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
=== axis=1 by_row=False raw=False
Type of x: <class 'pandas.core.series.Series'>
782 ms ± 31.9 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

-------------------- by_row='compat' raw=True / axis=0 vs axis=1 --------------------
=== axis=0 by_row='compat' raw=False
Type of x: <class 'numpy.ndarray'>
1.17 s ± 16 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
=== axis=1 by_row='compat' raw=False
Type of x: <class 'numpy.ndarray'>
214 ms ± 9.61 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

-------------------- by_row=False raw=True / axis=0 vs axis=1 --------------------
=== axis=0 by_row=False raw=False
Type of x: <class 'numpy.ndarray'>
1.18 s ± 36.6 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
=== axis=1 by_row=False raw=False
Type of x: <class 'numpy.ndarray'>
209 ms ± 4.87 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
  • NumPy系同様、axis=0の方が性能が悪い。
  • NumPy系同様、raw=Trueの方が性能が良い。

私の環境ではこのような結果となったが、あくまでも参考情報ということはご理解頂きたい。

  • 私がpandasの内部実装に関しては無知。性能確認として適切な処理を選定できていない可能性。
  • 性能は環境依存の割合が高く、あらゆる環境・処理で同一の結果を保証するべきではない。

apply()に渡すfuncがpandasのインデックスを活用する処理の場合、raw=Falseのほうが性能が出る可能性もある。計測していないからわからないけど。

7. agg

モチベーションや識字率など、様々な理由によりaggregateと書くことを諦めた層向けに、大幅に字数を削減している。 実体はただのエイリアスで、実体はaggregate()である。ドキュメント的にもエイリアスの使用をオススメしている。メソッドの詳細はaggregate()の項で。

8. aggregate()メソッド - 集計する

GroupBy系のオブジェクトを介して呼び出されることが多い。groupby()によるグルーピング操作に対してsum()min()mean()などの集計の計算を行う際によく使われる。

グルーピングと集計の関係について、常識的なことを少し考えてみよう。

Q1. 集計を行う際は、必ずグルーピング操作が必要か?
A1. (メガネクイッ)データによれば84.3%の確率で必要ですね。

Q2. グルーピング操作を行う際は、必ず集計が必要か?
A2. (メガネクイッ)諸説ありますが66.8%の確率で必要です。

頭の悪いデータ系メガネキャラなら模範的な回答だが、いずれも100%不要である。回りくどくなったが「グルーピングしていなくても集計することはある」ということを書きたかっただけである。そして、この要件があるため、aggregate()メソッドはGroupBy系オブジェクト以外にも実装されているのである。

8-1. pandas.Series.aggregate()

8-1-1. インターフェース

https://pandas.pydata.org/docs/reference/api/pandas.Series.aggregate.html#pandas.Series.aggregate

Series.aggregate(func=None, axis=0, *args, **kwargs)

パラメータ デフォルト 説明(Google翻訳)
func function, str, list or dict - データの集計に使用する関数。関数の場合は、Series に渡されたとき、または Series.apply に渡されたときに機能する必要があります。
受け入れられる組み合わせは次のとおりです:
・関数
・文字列関数名
・関数または関数名のリスト (例: [np.sum, 'mean'])
・軸ラベルの辞書 -> 関数、関数名、またはそれらのリスト。
axis {0 or ‘index’} 0 未使用。DataFrame との互換性に必要なパラメータ。
*args - - func に渡す位置引数。
**kwargs - - func に渡すキーワード引数。

集計を行う時、「合計だけ求めたい」というケースもあれば、「平均と標準偏差を求めたい」のように複数の集計が必要なケースもあろう。そのため、aggregate()は複数のfuncを同時に計算することが可能なインターフェース設計となっている。

8-1-2. サンプルコード

単体で集計を行う。

import pandas as pd
import numpy as np

s = pd.Series([0, 2, 1, 6, 2, 8, 1, 2, 4, 2, 1])
s.agg("max")
8

callableなfuncオブジェクトを渡す。

s.agg(pd.Series.min)
0

Seriesの平均、標準偏差、最大値、最小値を求める

s.agg(["mean", "std", "max", "min"])
mean    2.636364
std     2.419617
max     8.000000
min     0.000000
dtype: float64

辞書でもOK。

s.agg(
    {
        "平均": "mean",
        "標準偏差": "std",
        "最大": "max",
        "最小": "min",
    }
)
平均      2.636364
標準偏差    2.419617
最大      8.000000
最小      0.000000
dtype: float64

キーワード引数も使える・・・?数値しかないSeriesなのでnumeric_only=Trueがあってもなくても結果は変わらず、動作確認ができていない。

s.agg(
    {
        "平均": "mean",
        "標準偏差": "std",
        "最大": "max",
        "最小": "min",
    },
    numeric_only=True,
)
平均      2.636364
標準偏差    2.419617
最大      8.000000
最小      0.000000
dtype: float64

lambdaを使って無名関数を定義して呼び出すことも可能。

s.agg(
    {
        "売上":lambda x: str(x) + "千億万円",
        "費用":lambda x: str(x) + "兆万円",
    }
)
FutureWarning: using <function <lambda> at 0x00000212EF1937F0> in Series.agg cannot aggregate and has been deprecated. Use Series.transform to keep behavior unchanged.
FutureWarning: using <function <lambda> at 0x00000212EF1932E0> in Series.agg cannot aggregate and has been deprecated. Use Series.transform to keep behavior unchanged.

売上  0     0千億万円
    1     2千億万円
    2     1千億万円
    3     6千億万円
    4     2千億万円
    5     8千億万円
    6     1千億万円
    7     2千億万円
    8     4千億万円
    9     2千億万円
    10    1千億万円
費用  0      0兆万円
    1      2兆万円
    2      1兆万円
    3      6兆万円
    4      2兆万円
    5      8兆万円
    6      1兆万円
    7      2兆万円
    8      4兆万円
    9      2兆万円
    10     1兆万円
dtype: object

Warningが出た。「aggregate()にてlambdaを使用すると集計できないため、Deprecatedになったから、動作を変えたくなかったらtransform()を使ってね。」とのこと。
pandasで定義されているmax()median()などの集計関数を呼び出す、というのが基本的な使い方になるだろう。

8-2. pandas.DataFrame.aggregate()

基本的にはSeriesに軸が追加され2次元となったバージョンであり、特記事項は特に無し。

8-2-1. インターフェース

https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.aggregate.html#pandas.DataFrame.aggregate

DataFrame.aggregate(func=None, axis=0, *args, **kwargs)

パラメータ デフォルト 説明(Google翻訳)
func function, str, list or dict - データの集計に使用する関数。関数の場合は、Series に渡されたとき、または Series.apply に渡されたときに機能する必要があります。
受け入れられる組み合わせは次のとおりです:
・関数
・文字列関数名
・関数または関数名のリスト (例: [np.sum, 'mean'])
・軸ラベルの辞書 -> 関数、関数名、またはそれらのリスト。
axis {0 or ‘index’, 1 or ‘columns’} 0 0 または「index」の場合: 各列に関数を適用します。1 または「columns」の場合: 各行に関数を適用します。
*args - - func に渡す位置引数。
**kwargs - - func に渡すキーワード引数。

8-2-2. サンプルコード

Seriesで正しく確認ができていなかった、キーワード引数を確認する。

data = {
    "A": [1, 2, 3, 4, None],
    "B": [5, 4, None, 2, 1],
    "C": [2, 3, 4, 5, 6]
}
df = pd.DataFrame(data)

result = df.agg({
    "A": ["min", "max", "mean", "std"],
    "B": ["min", "max", "mean", "std"],
    "C": ["min", "max", "mean", "std"],
}, skipna=False)
print(result)
             A         B         C
min   1.000000  1.000000  2.000000
max   4.000000  5.000000  6.000000
mean  2.500000  3.000000  4.000000
std   1.290994  1.825742  1.581139
result = df.agg({
    "A": ["min", "max", "mean", "std"],
    "B": ["min", "max", "mean", "std"],
    "C": ["min", "max", "mean", "std"],
}, skipna=True)
print(result)
             A         B         C
min   1.000000  1.000000  2.000000
max   4.000000  5.000000  6.000000
mean  2.500000  3.000000  4.000000
std   1.290994  1.825742  1.581139

結果がどちらも一緒になるので、skipnaパラメータがうまく動作していないように見受けられる。
集計関数が1つしかないケースではどうだろうか。

df.agg("min", skipna=True)
A    1.0
B    1.0
C    2.0
dtype: float64
df.agg("min", skipna=False)
A    NaN
B    NaN
C    2.0
dtype: float64

集計関数が単体であればキーワード引数はうまく動作する。

9. transform()メソッド - 変換する

transform()の特徴としては、呼び出し元オブジェクトのサイズと同じオブジェクトを返すところである。なので、使い道としては各値に対して正則化を行ったり、SQLのOVER句でPARTITION BYするような使い方ができる。後者は次回以降にGroupBy系オブジェクトの話を交えてすることになると思う。

9-1. pandas.Series.transform()

9-1-1. インターフェース

https://pandas.pydata.org/docs/reference/api/pandas.Series.transform.html#pandas.Series.transform

Series.transform(func=None, axis=0, *args, **kwargs) -> DataFrame | Series

パラメータ デフォルト 説明(Google翻訳)
func function, str, list or dict - データの変換に使用する関数。関数の場合は、Series を渡したとき、または Series.apply に渡されたときに機能する必要があります。func がリスト形式と辞書形式の両方である場合、辞書形式の動作が優先されます。
受け入れられる組み合わせは次のとおりです:
・関数
・文字列関数名
・関数または関数名のリスト (例: [np.sum, 'mean'])
・軸ラベルの辞書 -> 関数、関数名、またはそれらのリスト。
axis {0 or ‘index’} 0 未使用。DataFrame との互換性に必要なパラメータ。
*args - - func に渡す位置引数。
**kwargs - - func に渡すキーワード引数。

パラメータはagg()と一緒なので、セットで覚えてしまおう。

9-1-2. サンプルコード

まずは軽めの動作検証から。

import pandas as pd
import numpy as np

s = pd.Series([0, 1, 2])
s.transform(lambda x: x * 2)
0    0
1    2
2    4
dtype: int64

サイズが同一ならDataFrameを返すこともできる。

print(s.transform([np.sin, np.cos, np.exp, np.log]))
        sin       cos       exp       log
0  0.000000  1.000000  1.000000      -inf
1  0.841471  0.540302  2.718282  0.000000
2  0.909297 -0.416147  7.389056  0.693147

同じサイズのデータにはならない集計処理はエラーになる。

s.transform(["sum", "min", "max"])
ValueError: Function did not transform

pandasの集計処理ではなく、NumPyの集計処理もエラーになる。

s.transform([np.sum, np.min, np.max])
ValueError: Function did not transform

lambdaを介すると、エラーにならない。

print(
    s.transform(
        [
            lambda x: np.sum(x),
            lambda x: np.min(x),
            lambda x: np.max(x),
        ]
    )
)
   <lambda>
0         0
1         1
2         2

3回の集計計算でいずれも項目<lambda>を上書きしているので、辞書形式で項目名を指定する。

print(
    s.transform(
        {
            "SUM": lambda x: np.sum(x),
            "MIN": lambda x: np.min(x),
            "MAX": lambda x: np.max(x),
        }
    )
)
   SUM  MIN  MAX
0    0    0    0
1    1    1    1
2    2    2    2

NumPy集計関数を渡すとエラーは発生しないが、結果が想定と異なる。要素の値が1つ1つlambdaに渡っているようだ。

s.transform(lambda x: type(x))
0    <class 'int'>
1    <class 'int'>
2    <class 'int'>
dtype: object

そのため、以下のように標準化を行うと、np.std(x)は必ず0となり、0除算が発生してNaNとなる。

s.transform(lambda x: (x - np.mean(x))/ np.std(x, ddof=0))
0   NaN
1   NaN
2   NaN
dtype: float64

標準化を行う場合、NumPy関数ではなくpandas関数を使う。

s.transform(lambda x: (x - x.mean())/ x.std(ddof=0))
0   -1.224745
1    0.000000
2    1.224745
dtype: float64

しかし直感とは違う動作とである。lambdaには要素ごとの値、つまり今回のケースではint型が渡っているはずだが、なぜpandasオブジェクトのメソッドが呼び出せるのだろうか?「11. funcに渡される値」にて詳細な調査を行おう。

9-2. pandas.DataFrame.transform()

9-2-1. インターフェース

https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.transform.html#pandas.DataFrame.transform

DataFrame.transform(func=None, axis=0, *args, **kwargs) -> DataFrame | Series

パラメータ デフォルト 説明(Google翻訳)
func function, str, list or dict - データの変換に使用する関数。関数の場合は、DataFrame を渡したとき、または DataFrame.apply に渡されたときに機能する必要があります。func がリスト形式と辞書形式の両方である場合、辞書形式の動作が優先されます。
受け入れられる組み合わせは次のとおりです:
・関数
・文字列関数名
・関数または関数名のリスト (例: [np.sum, 'mean'])
・軸ラベルの辞書 -> 関数、関数名、またはそれらのリスト。
axis {0 or ‘index’, 1 or ‘columns’} 0 0 または「index」の場合: 各列に関数を適用します。1 または「columns」の場合: 各行に関数を適用します。
*args - - func に渡す位置引数。
**kwargs - - func に渡すキーワード引数。

9-2-2. サンプルコード

簡単なデータフレームでサンプルデータを作成する。

df = pd.DataFrame([[1, 4], [6, 7], [3, 5]])
print(df)
   0  1
0  1  4
1  6  7
2  3  5

簡単な式で動作確認。

print(df.transform(lambda x: x * 2))
    0   1
0   2   8
1  12  14
2   6  10

配列で渡すと、各項目に対してマルチインデックスが作成される。

print(df.transform([np.sin, np.cos, np.exp]))
          0                               1                       
        sin       cos         exp       sin       cos          exp
0  0.841471  0.540302    2.718282 -0.756802 -0.653644    54.598150
1 -0.279415  0.960170  403.428793  0.656987  0.753902  1096.633158
2  0.141120 -0.989992   20.085537 -0.958924  0.283662   148.413159

配列でlambdaを渡すと、項目が上書きされ、最後のnp.exp()しか値が残らない。

print(
    df.transform(
        [
            lambda x: np.sin(x),
            lambda x: np.cos(x),
            lambda x: np.exp(x),
        ]
    )
)
            0            1
     <lambda>     <lambda>
0    2.718282    54.598150
1  403.428793  1096.633158
2   20.085537   148.413159

配列ではなく、辞書でlambdaを渡すと、辞書のキー名はlambdaを適用する列名と解釈されるようで、Series.transform()の動作と異なりエラーとなってしまう。

print(
    df.transform(
        {
            "np.sin()": lambda x: np.sin(x),
            "np.cos()": lambda x: np.cos(x),
            "np.exp()": lambda x: np.exp(x),
        }
    )
)
KeyError: "Column(s) ['np.cos()', 'np.exp()', 'np.sin()'] do not exist"

辞書のキーに列名を指定すれば、特定の列に特定の処理を行うようにできる。

print(
    df.transform(
        {
            0: lambda x: np.sin(x),
            1: lambda x: np.cos(x)
        }
    )
)
          0         1
0  0.841471 -0.653644
1 -0.279415  0.753902
2  0.141120  0.283662

各項目に対して複数の異なるlambdaを適用するのは不得意のようだ。この場合もlambdaは配列の最後の要素であるnp.cos(x)np.log(x)で上書きされる。

print(
    df.transform(
        {
            0: [np.sinh ,lambda x: np.sin(x), lambda x: np.cos(x)],
            1: [np.cosh ,lambda x: np.exp(x), lambda x: np.log(x)],
        }
    )
)
            0                     1          
         sinh  <lambda>        cosh  <lambda>
0    1.175201  0.540302   27.308233  1.386294
1  201.713157  0.960170  548.317035  1.945910
2   10.017875 -0.989992   74.209949  1.609438

以下のように辞書をネストさせることはサポートされていないようだ。

print(
    df.transform(
        {
            0: {
                "sinh": np.sinh ,
                "sin": lambda x: np.sin(x),
                "cos": lambda x: np.cos(x)
            },
            1: {
                "cosh": np.cosh ,
                "exp": lambda x: np.exp(x),
                "log": lambda x: np.log(x)
            },
        }
    )
)
SpecificationError: nested renamer is not supported

DataFrame.transform()で高い汎用性を持たせる場合、lambdaではなく面倒でも関数を定義する方法を取るしかなさそうだ。

def my_sin(x):
    return np.sin(x)

def my_cos(x):
    return np.cos(x)

def my_exp(x):
    return np.exp(x)

def my_log(x):
    return np.log(x)

print(
    df.transform(
        {
            0: [np.sinh ,my_sin, my_cos],
            1: [np.cosh ,my_exp, my_log],
        }
    )
)
            0                               1                       
         sinh    my_sin    my_cos        cosh       my_exp    my_log
0    1.175201  0.841471  0.540302   27.308233    54.598150  1.386294
1  201.713157 -0.279415  0.960170  548.317035  1096.633158  1.945910
2   10.017875  0.141120 -0.989992   74.209949   148.413159  1.609438

最後に、agg()と同様にキーワード引数の動作について確認する。

df[0][0] = pd.NA
df[1][1] = pd.NA
print(df)
     0    1
0  NaN  4.0
1  6.0  NaN
2  3.0  5.0

このようなサンプルデータに対してskipnaの指定が効くかどうか確認する。

print(
    df.transform(
        {
            0: "cumsum",
            1: "cumprod",
        },
    )
)
     0     1
0  NaN   4.0
1  6.0   NaN
2  9.0  20.0

skipna=Falseを指定すると、cumsum()cumprod()いずれもNAをskipせず、累積和、累積積の計算を行うため、以降の計算は全てNaNになる。

print(
    df.transform(
        {
            0: "cumsum",
            1: "cumprod",
        },
        skipna=False
    )
)
    0    1
0 NaN  4.0
1 NaN  NaN
2 NaN  NaN

cumsum()skipna=Truecumprod()skipna=Falseのようにfunc毎に異なるキーワード引数を使いたい場合は、残念ながら関数を定義するしかないようだ。

10. pipe()メソッド - パイプラインチェーンを構築する

pipe()メソッドはSeries/DataFrameを期待する複数の処理を連結したパイプラインチェーンを構築するためのメソッドである。

df.pipe(func1).pipe(func2).pipe(func3)

このように、処理1から処理3までを順番に実行していくことができる。
「Series/DataFrameを期待する複数の処理」の具体的な意味は、funcに渡される値がselfである、ということを意味している。

10-1. pandas.core.generic.NDFrame.pipe()

冒頭のクラス図でも示したが、pipe()メソッドはSeriesDataFrameの親クラスであるNDFrameに定義されている。

10-1-1. インターフェース

https://pandas.pydata.org/docs/reference/api/pandas.Series.pipe.html#pandas.Series.pipe

https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.pipe.html#pandas.DataFrame.pipe

pipe(func, *args, **kwargs) -> T

パラメータ デフォルト 説明(Google翻訳)
func Callable[..., T] | tuple[Callable[..., T], str] - Series/DataFrame に適用する関数。args と kwargs が func に渡されます。あるいは、(callable、data_keyword) タプル。ここで、data_keyword は、Series/DataFrame を期待する callable のキーワードを示す文字列です。
*args - - func に渡す位置引数。
**kwargs - - func に渡すキーワード引数の辞書。

10-1-2. サンプルコード

pipe()メソッドについて、「Series/DataFrame を期待する」とは、funcに渡される値がselfである、ということである。

import pandas as pd

df = pd.DataFrame([[1, 2], [3, 4], [5, 6]])
print(df.pipe(lambda x: type(x)))
print(df[0].pipe(lambda x: type(x)))
<class 'pandas.core.frame.DataFrame'>
<class 'pandas.core.series.Series'>

そのため、DataFrameの複数の項目を使用する処理や、複数のDataFrame/Seriesに対する処理をfuncに記述できるのが他のfunc系メソッドと大きく異なる点である。

複数のDataFrameを使用する場合はpipe()メソッドのargsパラメーターを使う。

df1 = pd.DataFrame([[1, 2], [3, 4], [5, 6]])
df2 = pd.DataFrame([[1, 2], [3, 4], [5, 6]])
print(df1.pipe(lambda x, y: (type(x), type(y)), df2))
print(df1[0].pipe(lambda x, y: (type(x), type(y)), df2))
(<class 'pandas.core.frame.DataFrame'>, <class 'pandas.core.frame.DataFrame'>)
(<class 'pandas.core.series.Series'>, <class 'pandas.core.frame.DataFrame'>)

以上を踏まえて、とあるケーキ屋さんの日次損益を計算するシナリオを考えてみよう。まずはサンプルデータである。

# 原材料単価
df_material = pd.DataFrame(
    {
        "単価": [300, 180, 30, 250, 80, 20, 200, 400, 220, 12000],
    },
    index=["小麦粉", "ミルク", "アンモニア", "生クリーム", "砂糖", "過酸化水素水", "チョコレート", "イチゴ", "バナナ", "ウラン"]
)
# 商品単価
df_product = pd.DataFrame(
    {
        "単価" : [1000, 960, 1120, 500000],
    },
    index=["ショートケーキ", "フルーツタルト", "チョコレートケーキ", "イエローケーキ"]
)
# 日次販売個数
df_sell = pd.DataFrame(
    {
        "ショートケーキ": [18, 22, 20, 14],
        "フルーツタルト": [20, 18, 14, 20],
        "チョコレートケーキ": [21, 24, 95, 23],
        "イエローケーキ": [0, 5, 0, 4],
    },
    index=["2024-12-01", "2024-12-02", "2024-12-03", "2024-12-04"]
)
# 日次仕入個数
df_buy = pd.DataFrame(
    {
        "小麦粉": [3, 14, 4, 3],
        "ミルク": [5, 31, 7, 7],
        "アンモニア": [0, 0, 0, 0],
        "生クリーム": [5, 34, 5, 6],
        "砂糖": [10, 64, 13, 15],
        "過酸化水素水": [0, 0, 2, 0],
        "チョコレート": [13, 70, 14, 15],
        "イチゴ": [3, 4, 3, 3],
        "バナナ": [3, 4, 3, 3],
        "ウラン": [0, 0, 2, 0],
    },
    index=["2024-12-01", "2024-12-02", "2024-12-03", "2024-12-04"]
)

# 日次固定費
df_kotei = pd.DataFrame(
    {
        "人件費": [4000, 4000, 8000, 4000],
        "家賃": [5000, 5000, 5000, 5000],
        "水道費": [800, 800, 800, 800],
        "光熱費": [1200, 1200, 1200, 1200],
        "広告費": [0, 20000, 0, 0],
        "減価償却費": [1000, 1000, 1000, 1000],
    },
    index=["2024-12-01", "2024-12-02", "2024-12-03", "2024-12-04"]
)

損益の計算は適当に超ざっくりと以下のようにしよう。

  1. 売上計算:sum(各商品単価 × 販売個数)
  2. 仕入計算:sum(各原材料単価 × 仕入個数)
  3. 粗利の計算:売上 - 仕入
  4. 固定費の計算 : sum(各固定費)
  5. 損益計算: 粗利 - 固定費

各種計算を行う関数を定義する。

def calc_daily_sell(
        df_return : pd.DataFrame,
        df_sell : pd.DataFrame,
        df_product : pd.DataFrame
    ):
    """日次の売上価格を計算"""
    df_return["売上"] = (df_sell * df_product["単価"]).sum(axis=1)
    return df_return

def calc_daily_buy(
        df_return : pd.DataFrame,
        df_buy : pd.DataFrame,
        df_material : pd.DataFrame
    ):
    """日次の仕入価格を計算"""
    df_return["仕入"] = (df_buy * df_material["単価"]).sum(axis=1)
    return df_return

def calc_daily_arari(df_return : pd.DataFrame):
    """日次の粗利を計算"""
    df_return["粗利"] = df_return["売上"] - df_return["仕入"]
    return df_return

def calc_daily_kotei(
        df_return : pd.DataFrame,
        df_kotei : pd.DataFrame
    ):
    """日次の固定費を計算"""
    df_return["固定費"] = df_kotei.sum(axis=1)
    return df_return

def cal_daily_pl(df_return : pd.DataFrame):
    """日次の損益を計算"""
    df_return["損益"] = df_return["粗利"] - df_return["固定費"]
    return df_return

pipe()を使って各種計算を行う。

df_calc = pd.DataFrame()
(
    df_calc.pipe(calc_daily_sell, df_sell, df_product)
        .pipe(calc_daily_buy, df_buy, df_material)
        .pipe(calc_daily_arari)
        .pipe(calc_daily_kotei, df_kotei)
        .pipe(cal_daily_pl)
)
print(df_calc)
                 売上     仕入       粗利    固定費       損益
2024-12-01    60720   8310    52410  12000    40410
2024-12-02  2566160  39880  2526280  32000  2494280
2024-12-03   139840  33450   106390  16000    90390
2024-12-04  2058960   9720  2049240  12000  2037240

関数1, 関数2, 関数3 があるとして、

df.pipe(関数1).pipe(関数2).pipe(関数3)

と書くか、

関数1(df)
関数2(df)
関数3(df)

と書くかの違いなので、記述量や可読性にそこまで差異があるかと言われるとそう思わず、利用を推奨する程劇的に変わるような機能でもないという所感。いずれにせよ、funcに渡す値がselfであるという点から、他のfunc系メソッドと明確に異なるシーンで利用することになるだろう。

11. funcに渡される値

apply()pipe()については、funcに渡される値の型を確認した。しかし、transform()のサンプルコードでは想定とは異なる動作が確認されている。func呼び出しの初回以外も含めて、網羅的に確認することにする。

11-1. 何を確認するか

funcで行う処理に応じて、想定通りの結果が得られるかを確認する。以下に、funcで適用する処理と、それぞれの想定結果を示す。

  • lambda x: x**2 : NumPy/pandasで定義された関数を使用しない。

    • 要素の値が渡される場合 : 要素の値の2乗が算出される。
    • Seriesが渡される場合 : 要素の値の2乗が算出される。
  • lambda x: np.sin(x) : NumPy ufuncを単体で使用する。

    • 要素の値が渡される場合 : 要素の値のsin()が算出される。
    • Seriesが渡される場合 : 要素の値のsin()が算出される。
  • lambda x: np.std(x) : NumPyのreducing functionを単体で使用する。

    • 要素の値が渡される場合 : 0になる。
    • Seriesが渡される場合 : Seriesの標準偏差が算出される。
  • lambda x: x.std() : pandasの集約関数を単体で使用する。

    • 要素の値が渡される場合 : エラーが発生する。
    • Seriesが渡される場合 : Seriesの標準偏差が算出される。
  • lambda x: (x - np.mean(x)) / np.std(x) : NumPyのreducing function + ブロードキャスト。

    • 要素の値が渡される場合 : 0除算が発生し、NaNになる。
    • Seriesが渡される場合 : xを標準化した値が算出される。
  • lambda x: (x - x.mean()) / x.std(ddof=0) : pandasの集約関数 + ブロードキャスト。

    • 要素の値が渡される場合 : エラーが発生する。
    • Seriesが渡される場合 : xを標準化した値が算出される。

サンプルデータは以下のように適当なものを利用する。

import pandas as pd
import numpy as np

df = pd.DataFrame(
    [[1, 4], [6, 7], [3, 5]]
)

また、apply()の時に作成したクロージャーを用いた関数を改造して、funcに渡された値が何かを確認する関数を定義する。

def my_func(func):

    def closure_func(x):
        print(f'Type of x: {type(x)}')
        return func(x)
    
    return closure_func

11-2. 確認

11-2-1. apply()

まずはpandas.Seriesについて確認する。

print("--- x**2")
func = my_func(lambda x: x**2)
try:
    print(df[0].apply(func))
except Exception as e:
    print("Exception")
    print(e)
print()
print("--- np.sin(x)")
func = my_func(lambda x: np.sin(x))
try:
    print(df[0].apply(func))
except Exception as e:
    print("Exception")
    print(e)
print()
print("--- np.std(x)")
func = my_func(lambda x: np.std(x))
try:
    print(df[0].apply(func))
except Exception as e:
    print("Exception")
    print(e)
print()
print("--- x.std()")
func = my_func(lambda x: x.std(ddof=0))
try:
    print(df[0].apply(func))
except Exception as e:
    print("Exception")
    print(e)
print()
print("--- np 標準化")
func = my_func(lambda x: (x - np.mean(x)) / np.std(x))
try:
    print(df[0].apply(func))
except Exception as e:
    print("Exception")
    print(e)
print()
print("--- pd 標準化")
func = my_func(lambda x: (x - x.mean()) / x.std(ddof=0))
try:
    print(df[0].apply(func))
except Exception as e:
    print("Exception")
    print(e)
--- x**2
Type of x: <class 'int'>
Type of x: <class 'int'>
Type of x: <class 'int'>
0     1
1    36
2     9
Name: 0, dtype: int64

--- np.sin(x)
Type of x: <class 'int'>
Type of x: <class 'int'>
Type of x: <class 'int'>
0    0.841471
1   -0.279415
2    0.141120
Name: 0, dtype: float64

--- np.std(x)
Type of x: <class 'int'>
Type of x: <class 'int'>
Type of x: <class 'int'>
0    0.0
1    0.0
2    0.0
Name: 0, dtype: float64

--- x.std()
Type of x: <class 'int'>
Exception
'int' object has no attribute 'std'

--- np 標準化
Type of x: <class 'int'>
Type of x: <class 'int'>
Type of x: <class 'int'>
0   NaN
1   NaN
2   NaN
Name: 0, dtype: float64

--- pd 標準化
Type of x: <class 'int'>
Exception
'int' object has no attribute 'mean'
RuntimeWarning: invalid value encountered in scalar divide
  func = my_func(lambda x: (x - np.mean(x)) / np.std(x))

各処理のケースでもint型の値、つまり要素の値がfuncに渡っており、計算結果についても想定通りの結果となっている。

Series.apply()には、by_row=Falseを指定するとfuncにはSeriesが渡される。そちらも同様に確認する。

print("--- x**2")
func = my_func(lambda x: x**2)
try:
    print(df[0].apply(func, by_row=False))
except Exception as e:
    print("Exception")
    print(e)
print()
print("--- np.sin(x)")
func = my_func(lambda x: np.sin(x))
try:
    print(df[0].apply(func, by_row=False))
except Exception as e:
    print("Exception")
    print(e)
print()
print("--- np.std(x)")
func = my_func(lambda x: np.std(x))
try:
    print(df[0].apply(func, by_row=False))
except Exception as e:
    print("Exception")
    print(e)
print()
print("--- x.std()")
func = my_func(lambda x: x.std(ddof=0))
try:
    print(df[0].apply(func, by_row=False))
except Exception as e:
    print("Exception")
    print(e)
print()
print("--- np 標準化")
func = my_func(lambda x: (x - np.mean(x)) / np.std(x))
try:
    print(df[0].apply(func, by_row=False))
except Exception as e:
    print("Exception")
    print(e)
print()
print("--- pd 標準化")
func = my_func(lambda x: (x - x.mean()) / x.std(ddof=0))
try:
    print(df[0].apply(func, by_row=False))
except Exception as e:
    print("Exception")
    print(e)
--- x**2
Type of x: <class 'pandas.core.series.Series'>
0     1
1    36
2     9
Name: 0, dtype: int64

--- np.sin(x)
Type of x: <class 'pandas.core.series.Series'>
0    0.841471
1   -0.279415
2    0.141120
Name: 0, dtype: float64

--- np.std(x)
Type of x: <class 'pandas.core.series.Series'>
2.0548046676563256

--- x.std()
Type of x: <class 'pandas.core.series.Series'>
2.0548046676563256

--- np 標準化
Type of x: <class 'pandas.core.series.Series'>
0   -1.135550
1    1.297771
2   -0.162221
Name: 0, dtype: float64

--- pd 標準化
Type of x: <class 'pandas.core.series.Series'>
0   -1.135550
1    1.297771
2   -0.162221
Name: 0, dtype: float64

各処理のケースでもSeriesfuncに渡っており、計算結果についても想定通りの結果となっている。

続いて、DaraFrame.apply()の確認を行う。

print("--- x**2")
func = my_func(lambda x: x**2)
try:
    print(df.apply(func))
except Exception as e:
    print("Exception")
    print(e)
print()
print("--- np.sin(x)")
func = my_func(lambda x: np.sin(x))
try:
    print(df.apply(func))
except Exception as e:
    print("Exception")
    print(e)
print()
print("--- np.std(x)")
func = my_func(lambda x: np.std(x))
try:
    print(df.apply(func))
except Exception as e:
    print("Exception")
    print(e)
print()
print("--- x.std()")
func = my_func(lambda x: x.std(ddof=0))
try:
    print(df.apply(func))
except Exception as e:
    print("Exception")
    print(e)
print()
print("--- np 標準化")
func = my_func(lambda x: (x - np.mean(x)) / np.std(x))
try:
    print(df.apply(func))
except Exception as e:
    print("Exception")
    print(e)
print()
print("--- pd 標準化")
func = my_func(lambda x: (x - x.mean()) / x.std(ddof=0))
try:
    print(df.apply(func))
except Exception as e:
    print("Exception")
    print(e)
--- x**2
Type of x: <class 'pandas.core.series.Series'>
Type of x: <class 'pandas.core.series.Series'>
    0   1
0   1  16
1  36  49
2   9  25

--- np.sin(x)
Type of x: <class 'pandas.core.series.Series'>
Type of x: <class 'pandas.core.series.Series'>
          0         1
0  0.841471 -0.756802
1 -0.279415  0.656987
2  0.141120 -0.958924

--- np.std(x)
Type of x: <class 'pandas.core.series.Series'>
Type of x: <class 'pandas.core.series.Series'>
0    2.054805
1    1.247219
dtype: float64

--- x.std()
Type of x: <class 'pandas.core.series.Series'>
Type of x: <class 'pandas.core.series.Series'>
0    2.054805
1    1.247219
dtype: float64

--- np 標準化
Type of x: <class 'pandas.core.series.Series'>
Type of x: <class 'pandas.core.series.Series'>
          0         1
0 -1.135550 -1.069045
1  1.297771  1.336306
2 -0.162221 -0.267261

--- pd 標準化
Type of x: <class 'pandas.core.series.Series'>
Type of x: <class 'pandas.core.series.Series'>
          0         1
0 -1.135550 -1.069045
1  1.297771  1.336306
2 -0.162221 -0.267261

各処理のケースでもSeriesfuncに渡っており、計算結果についても想定通りの結果となっている。

DaraFrame.apply()raw=Trueパラメータを渡すと、funcにはndarrayが渡される。

print("--- x**2")
func = my_func(lambda x: x**2)
try:
    print(df.apply(func, raw=True))
except Exception as e:
    print("Exception")
    print(e)
print()
print("--- np.sin(x)")
func = my_func(lambda x: np.sin(x))
try:
    print(df.apply(func, raw=True))
except Exception as e:
    print("Exception")
    print(e)
print()
print("--- np.std(x)")
func = my_func(lambda x: np.std(x))
try:
    print(df.apply(func, raw=True))
except Exception as e:
    print("Exception")
    print(e)
print()
print("--- x.std()")
func = my_func(lambda x: x.std(ddof=0))
try:
    print(df.apply(func, raw=True))
except Exception as e:
    print("Exception")
    print(e)
print()
print("--- np 標準化")
func = my_func(lambda x: (x - np.mean(x)) / np.std(x))
try:
    print(df.apply(func, raw=True))
except Exception as e:
    print("Exception")
    print(e)
print()
print("--- pd 標準化")
func = my_func(lambda x: (x - x.mean()) / x.std(ddof=0))
try:
    print(df.apply(func, raw=True))
except Exception as e:
    print("Exception")
    print(e)
--- x**2
Type of x: <class 'numpy.ndarray'>
Type of x: <class 'numpy.ndarray'>
    0   1
0   1  16
1  36  49
2   9  25

--- np.sin(x)
Type of x: <class 'numpy.ndarray'>
Type of x: <class 'numpy.ndarray'>
          0         1
0  0.841471 -0.756802
1 -0.279415  0.656987
2  0.141120 -0.958924

--- np.std(x)
Type of x: <class 'numpy.ndarray'>
Type of x: <class 'numpy.ndarray'>
0    2.054805
1    1.247219
dtype: float64

--- x.std()
Type of x: <class 'numpy.ndarray'>
Type of x: <class 'numpy.ndarray'>
0    2.054805
1    1.247219
dtype: float64

--- np 標準化
Type of x: <class 'numpy.ndarray'>
Type of x: <class 'numpy.ndarray'>
          0         1
0 -1.135550 -1.069045
1  1.297771  1.336306
2 -0.162221 -0.267261

--- pd 標準化
Type of x: <class 'numpy.ndarray'>
Type of x: <class 'numpy.ndarray'>
          0         1
0 -1.135550 -1.069045
1  1.297771  1.336306
2 -0.162221 -0.267261

各処理のケースでもndarrayfuncに渡っている。計算結果について特に想定を記載していないが、違和感はない結果となっている。

11-2-2. map()

Series.map()の確認する。

print("--- x**2")
func = my_func(lambda x: x**2)
try:
    print(df[0].map(func))
except Exception as e:
    print("Exception")
    print(e)
print()
print("--- np.sin(x)")
func = my_func(lambda x: np.sin(x))
try:
    print(df[0].map(func))
except Exception as e:
    print("Exception")
    print(e)
print()
print("--- np.std(x)")
func = my_func(lambda x: np.std(x))
try:
    print(df[0].map(func))
except Exception as e:
    print("Exception")
    print(e)
print()
print("--- x.std()")
func = my_func(lambda x: x.std(ddof=0))
try:
    print(df[0].map(func))
except Exception as e:
    print("Exception")
    print(e)
print()
print("--- np 標準化")
func = my_func(lambda x: (x - np.mean(x)) / np.std(x))
try:
    print(df[0].map(func))
except Exception as e:
    print("Exception")
    print(e)
print()
print("--- pd 標準化")
func = my_func(lambda x: (x - x.mean()) / x.std(ddof=0))
try:
    print(df[0].map(func))
except Exception as e:
    print("Exception")
    print(e)
--- x**2
Type of x: <class 'int'>
Type of x: <class 'int'>
Type of x: <class 'int'>
0     1
1    36
2     9
Name: 0, dtype: int64

--- np.sin(x)
Type of x: <class 'int'>
Type of x: <class 'int'>
Type of x: <class 'int'>
0    0.841471
1   -0.279415
2    0.141120
Name: 0, dtype: float64

--- np.std(x)
Type of x: <class 'int'>
Type of x: <class 'int'>
Type of x: <class 'int'>
0    0.0
1    0.0
2    0.0
Name: 0, dtype: float64

--- x.std()
Type of x: <class 'int'>
Exception
'int' object has no attribute 'std'

--- np 標準化
Type of x: <class 'int'>
Type of x: <class 'int'>
Type of x: <class 'int'>
0   NaN
1   NaN
2   NaN
Name: 0, dtype: float64

--- pd 標準化
Type of x: <class 'int'>
Exception
'int' object has no attribute 'mean'
RuntimeWarning: invalid value encountered in scalar divide
  func = my_func(lambda x: (x - np.mean(x)) / np.std(x))

Series.map()では、funcには要素の値が渡される。各処理の結果については想定通りの結果である。

続いてDataFrame.map()を確認する。

print("--- x**2")
func = my_func(lambda x: x**2)
try:
    print(df.map(func))
except Exception as e:
    print("Exception")
    print(e)
print()
print("--- np.sin(x)")
func = my_func(lambda x: np.sin(x))
try:
    print(df.map(func))
except Exception as e:
    print("Exception")
    print(e)
print()
print("--- np.std(x)")
func = my_func(lambda x: np.std(x))
try:
    print(df.map(func))
except Exception as e:
    print("Exception")
    print(e)
print()
print("--- x.std()")
func = my_func(lambda x: x.std(ddof=0))
try:
    print(df.map(func))
except Exception as e:
    print("Exception")
    print(e)
print()
print("--- np 標準化")
func = my_func(lambda x: (x - np.mean(x)) / np.std(x))
try:
    print(df.map(func))
except Exception as e:
    print("Exception")
    print(e)
print()
print("--- pd 標準化")
func = my_func(lambda x: (x - x.mean()) / x.std(ddof=0))
try:
    print(df.map(func))
except Exception as e:
    print("Exception")
    print(e)
--- x**2
Type of x: <class 'int'>
Type of x: <class 'int'>
Type of x: <class 'int'>
Type of x: <class 'int'>
Type of x: <class 'int'>
Type of x: <class 'int'>
    0   1
0   1  16
1  36  49
2   9  25

--- np.sin(x)
Type of x: <class 'int'>
Type of x: <class 'int'>
Type of x: <class 'int'>
Type of x: <class 'int'>
Type of x: <class 'int'>
Type of x: <class 'int'>
          0         1
0  0.841471 -0.756802
1 -0.279415  0.656987
2  0.141120 -0.958924

--- np.std(x)
Type of x: <class 'int'>
Type of x: <class 'int'>
Type of x: <class 'int'>
Type of x: <class 'int'>
Type of x: <class 'int'>
Type of x: <class 'int'>
     0    1
0  0.0  0.0
1  0.0  0.0
2  0.0  0.0

--- x.std()
Type of x: <class 'int'>
Exception
'int' object has no attribute 'std'

--- np 標準化
Type of x: <class 'int'>
Type of x: <class 'int'>
Type of x: <class 'int'>
Type of x: <class 'int'>
Type of x: <class 'int'>
Type of x: <class 'int'>
    0   1
0 NaN NaN
1 NaN NaN
2 NaN NaN

--- pd 標準化
Type of x: <class 'int'>
Exception
'int' object has no attribute 'mean'
RuntimeWarning: invalid value encountered in scalar divide
  func = my_func(lambda x: (x - np.mean(x)) / np.std(x))

DataFrame.map()でも、funcには要素の値が渡される。各処理の結果については想定通りの結果である。

map()メソッドは、どのような状況でもfuncには要素の値が渡されるため、集約系の処理は意図した計算結果とならない。また、DataFrameSeriesのメソッドであるsum()などのpandas集約関数を使うことはできない。
要素の値が1つ1つfuncに渡されるので、処理性能の点で優位性のあるベクトル単位の計算は行うことはできないため、計算を行う用途には向いていない。map()の名前が示す通り、シンプルな値の変換を行う用途に留めよう。

11-2-3. agg()

Series.agg()の確認を行う。

print("--- x**2")
func = my_func(lambda x: x**2)
try:
    print(df[0].agg(func))
except Exception as e:
    print("Exception")
    print(e)
print()
print("--- np.sin(x)")
func = my_func(lambda x: np.sin(x))
try:
    print(df[0].agg(func))
except Exception as e:
    print("Exception")
    print(e)
print()
print("--- np.std(x)")
func = my_func(lambda x: np.std(x))
try:
    print(df[0].agg(func))
except Exception as e:
    print("Exception")
    print(e)
print()
print("--- x.std()")
func = my_func(lambda x: x.std(ddof=0))
try:
    print(df[0].agg(func))
except Exception as e:
    print("Exception")
    print(e)
print()
print("--- np 標準化")
func = my_func(lambda x: (x - np.mean(x)) / np.std(x))
try:
    print(df[0].agg(func))
except Exception as e:
    print("Exception")
    print(e)
print()
print("--- pd 標準化")
func = my_func(lambda x: (x - x.mean()) / x.std(ddof=0))
try:
    print(df[0].agg(func))
except Exception as e:
    print("Exception")
    print(e)
--- x**2
Type of x: <class 'int'>
Type of x: <class 'int'>
Type of x: <class 'int'>
0     1
1    36
2     9
Name: 0, dtype: int64

--- np.sin(x)
Type of x: <class 'int'>
Type of x: <class 'int'>
Type of x: <class 'int'>
0    0.841471
1   -0.279415
2    0.141120
Name: 0, dtype: float64

--- np.std(x)
Type of x: <class 'int'>
Type of x: <class 'int'>
Type of x: <class 'int'>
0    0.0
1    0.0
2    0.0
Name: 0, dtype: float64

--- x.std()
Type of x: <class 'int'>
Type of x: <class 'pandas.core.series.Series'>
2.0548046676563256

--- np 標準化
Type of x: <class 'int'>
Type of x: <class 'int'>
Type of x: <class 'int'>
0   NaN
1   NaN
2   NaN
Name: 0, dtype: float64

--- pd 標準化
Type of x: <class 'int'>
Type of x: <class 'pandas.core.series.Series'>
0   -1.135550
1    1.297771
2   -0.162221
Name: 0, dtype: float64
FutureWarning: using <function my_func.<locals>.closure_func at 0x00000212EE11A950> in Series.agg cannot aggregate and has been deprecated. Use Series.transform to keep behavior unchanged.
  print(df[0].agg(func))
FutureWarning: using <function my_func.<locals>.closure_func at 0x00000212EE118550> in Series.agg cannot aggregate and has been deprecated. Use Series.transform to keep behavior unchanged.
  print(df[0].agg(func))
FutureWarning: using <function my_func.<locals>.closure_func at 0x00000212EE11BEB0> in Series.agg cannot aggregate and has been deprecated. Use Series.transform to keep behavior unchanged.
  print(df[0].agg(func))
RuntimeWarning: invalid value encountered in scalar divide
  func = my_func(lambda x: (x - np.mean(x)) / np.std(x))
FutureWarning: using <function my_func.<locals>.closure_func at 0x00000212EE11A950> in Series.agg cannot aggregate and has been deprecated. Use Series.transform to keep behavior unchanged.
  print(df[0].agg(func))

ここで想定とは異なる挙動となった。以下のpandasの集計関数を使ったときの挙動である。

--- x.std()
Type of x: <class 'int'>
Type of x: <class 'pandas.core.series.Series'>
2.0548046676563256

--- pd 標準化
Type of x: <class 'int'>
Type of x: <class 'pandas.core.series.Series'>
0   -1.135550
1    1.297771
2   -0.162221
Name: 0, dtype: float64

最初は要素の値であるint型がfuncに渡されたが、その後Seriesが渡されている。Seriesが渡るためpandasの集計関数が使えるので、値が正しく算出される。

実はSeries.agg()は、以下のソースコード1441行目近辺の通り、funcへ要素の値を渡して実行した際にValueError, AttributeError, TypeErrorのいずれかが発生した場合、Seriesを渡して改めてfuncを実行するという作りになっている。

https://github.com/pandas-dev/pandas/blob/v2.2.3/pandas/core/apply.py#L1429-L1453

このような作りのため、上記サンプルコードの「np 標準化」のようにfunc内でNumPyの集計関数を利用すると、要素ごとの値で集計するため意図した値にならず、「pd 標準化」のようにpandasの集計関数を使っていれば意図した通りに集計される。
agg()から呼び出すfuncでは、NumPyの集計関数は使用しないほうが良いし、使う場合は細心の注意を払おう。使う必要がある場合、あまり直感的に理解できる可読性の高いコードとは言えないかもしれない。

続いて、DataFrame.agg()を確認する。

print("--- x**2")
func = my_func(lambda x: x**2)
try:
    print(df.agg(func))
except Exception as e:
    print("Exception")
    print(e)
print()
print("--- np.sin(x)")
func = my_func(lambda x: np.sin(x))
try:
    print(df.agg(func))
except Exception as e:
    print("Exception")
    print(e)
print()
print("--- np.std(x)")
func = my_func(lambda x: np.std(x))
try:
    print(df.agg(func))
except Exception as e:
    print("Exception")
    print(e)
print()
print("--- x.std()")
func = my_func(lambda x: x.std(ddof=0))
try:
    print(df.agg(func))
except Exception as e:
    print("Exception")
    print(e)
print()
print("--- np 標準化")
func = my_func(lambda x: (x - np.mean(x)) / np.std(x))
try:
    print(df.agg(func))
except Exception as e:
    print("Exception")
    print(e)
print()
print("--- pd 標準化")
func = my_func(lambda x: (x - x.mean()) / x.std(ddof=0))
try:
    print(df.agg(func))
except Exception as e:
    print("Exception")
    print(e)
--- x**2
Type of x: <class 'pandas.core.series.Series'>
Type of x: <class 'pandas.core.series.Series'>
    0   1
0   1  16
1  36  49
2   9  25

--- np.sin(x)
Type of x: <class 'pandas.core.series.Series'>
Type of x: <class 'pandas.core.series.Series'>
          0         1
0  0.841471 -0.756802
1 -0.279415  0.656987
2  0.141120 -0.958924

--- np.std(x)
Type of x: <class 'pandas.core.series.Series'>
Type of x: <class 'pandas.core.series.Series'>
0    2.054805
1    1.247219
dtype: float64

--- x.std()
Type of x: <class 'pandas.core.series.Series'>
Type of x: <class 'pandas.core.series.Series'>
0    2.054805
1    1.247219
dtype: float64

--- np 標準化
Type of x: <class 'pandas.core.series.Series'>
Type of x: <class 'pandas.core.series.Series'>
          0         1
0 -1.135550 -1.069045
1  1.297771  1.336306
2 -0.162221 -0.267261

--- pd 標準化
Type of x: <class 'pandas.core.series.Series'>
Type of x: <class 'pandas.core.series.Series'>
          0         1
0 -1.135550 -1.069045
1  1.297771  1.336306
2 -0.162221 -0.267261

いずれのケースでも、Seriesfuncに渡されており、各結果も想定通りである。

11-2-4. transform()

Series.transform()の確認を行う。

print("--- x**2")
func = my_func(lambda x: x**2)
try:
    print(df[0].transform(func))
except Exception as e:
    print("Exception")
    print(e)
print()
print("--- np.sin(x)")
func = my_func(lambda x: np.sin(x))
try:
    print(df[0].transform(func))
except Exception as e:
    print("Exception")
    print(e)
print()
print("--- np.std(x)")
func = my_func(lambda x: np.std(x))
try:
    print(df[0].transform(func))
except Exception as e:
    print("Exception")
    print(e)
print()
print("--- x.std()")
func = my_func(lambda x: x.std(ddof=0))
try:
    print(df[0].transform(func))
except Exception as e:
    print("Exception")
    print(e)
print()
print("--- np 標準化")
func = my_func(lambda x: (x - np.mean(x)) / np.std(x))
try:
    print(df[0].transform(func))
except Exception as e:
    print("Exception")
    print(e)
print()
print("--- pd 標準化")
func = my_func(lambda x: (x - x.mean()) / x.std(ddof=0))
try:
    print(df[0].transform(func))
except Exception as e:
    print("Exception")
    print(e)
--- x**2
Type of x: <class 'int'>
Type of x: <class 'int'>
Type of x: <class 'int'>
0     1
1    36
2     9
Name: 0, dtype: int64

--- np.sin(x)
Type of x: <class 'int'>
Type of x: <class 'int'>
Type of x: <class 'int'>
0    0.841471
1   -0.279415
2    0.141120
Name: 0, dtype: float64

--- np.std(x)
Type of x: <class 'int'>
Type of x: <class 'int'>
Type of x: <class 'int'>
0    0.0
1    0.0
2    0.0
Name: 0, dtype: float64

--- x.std()
Type of x: <class 'int'>
Type of x: <class 'pandas.core.series.Series'>
Exception
Function did not transform

--- np 標準化
Type of x: <class 'int'>
Type of x: <class 'int'>
Type of x: <class 'int'>
0   NaN
1   NaN
2   NaN
Name: 0, dtype: float64

--- pd 標準化
Type of x: <class 'int'>
Type of x: <class 'pandas.core.series.Series'>
0   -1.135550
1    1.297771
2   -0.162221
Name: 0, dtype: float64
RuntimeWarning: invalid value encountered in scalar divide
  func = my_func(lambda x: (x - np.mean(x)) / np.std(x))

Series.agg()と同様、基本的には要素の値が渡されてfuncを実行するが、funcにpandasの集約関数が存在する場合、Exceptionが発生し、Seriesを渡して改めてfuncを実行している。ソースコードで該当するのは以下の314行目辺りである。

https://github.com/pandas-dev/pandas/blob/v2.2.3/pandas/core/apply.py#L295-L317

x.std()の処理では、Seriesで改めて計算した結果transform()の「呼び出し元のSeries/DataFrameと同じサイズのデータを返却する」の仕様にひっかかるため、Function did not transformのエラーとなる。一方、「pd 標準化」については、集計した値が適したサイズにブロードキャストされるので正常に処理が完了する。

Series.agg()と同様、Series.transform()で利用するfuncではNumPyの集計関数は使用しないほうが良いだろう。

続いて、DataFrame.transform()の確認をする。

print("--- x**2")
func = my_func(lambda x: x**2)
try:
    print(df.transform(func))
except Exception as e:
    print("Exception")
    print(e)
print()
print("--- np.sin(x)")
func = my_func(lambda x: np.sin(x))
try:
    print(df.transform(func))
except Exception as e:
    print("Exception")
    print(e)
print()
print("--- np.std(x)")
func = my_func(lambda x: np.std(x))
try:
    print(df.transform(func))
except Exception as e:
    print("Exception")
    print(e)
print()
print("--- x.std()")
func = my_func(lambda x: x.std(ddof=0))
try:
    print(df.transform(func))
except Exception as e:
    print("Exception")
    print(e)
print()
print("--- np 標準化")
func = my_func(lambda x: (x - np.mean(x)) / np.std(x))
try:
    print(df.transform(func))
except Exception as e:
    print("Exception")
    print(e)
print()
print("--- pd 標準化")
func = my_func(lambda x: (x - x.mean()) / x.std(ddof=0))
try:
    print(df.transform(func))
except Exception as e:
    print("Exception")
    print(e)
--- x**2
Type of x: <class 'pandas.core.series.Series'>
Type of x: <class 'pandas.core.series.Series'>
    0   1
0   1  16
1  36  49
2   9  25

--- np.sin(x)
Type of x: <class 'pandas.core.series.Series'>
Type of x: <class 'pandas.core.series.Series'>
          0         1
0  0.841471 -0.756802
1 -0.279415  0.656987
2  0.141120 -0.958924

--- np.std(x)
Type of x: <class 'pandas.core.series.Series'>
Type of x: <class 'pandas.core.series.Series'>
Exception
Function did not transform

--- x.std()
Type of x: <class 'pandas.core.series.Series'>
Type of x: <class 'pandas.core.series.Series'>
Exception
Function did not transform

--- np 標準化
Type of x: <class 'pandas.core.series.Series'>
Type of x: <class 'pandas.core.series.Series'>
          0         1
0 -1.135550 -1.069045
1  1.297771  1.336306
2 -0.162221 -0.267261

--- pd 標準化
Type of x: <class 'pandas.core.series.Series'>
Type of x: <class 'pandas.core.series.Series'>
          0         1
0 -1.135550 -1.069045
1  1.297771  1.336306
2 -0.162221 -0.267261

いずれのケースでも、Seriesfuncに渡されており、各結果も想定通りである。
np.std(x)x.std()いずれも集計した結果、データサイズが減ってしまうため、Function did not transformとなり動作しない。

11-2-5. pipe()

ついでにpipe()も確認する。まずはSeries.pipe()から。

print("--- x**2")
func = my_func(lambda x: x**2)
try:
    print(df[0].pipe(func))
except Exception as e:
    print("Exception")
    print(e)
print()
print("--- np.sin(x)")
func = my_func(lambda x: np.sin(x))
try:
    print(df[0].pipe(func))
except Exception as e:
    print("Exception")
    print(e)
print()
print("--- np.std(x)")
func = my_func(lambda x: np.std(x))
try:
    print(df[0].pipe(func))
except Exception as e:
    print("Exception")
    print(e)
print()
print("--- x.std()")
func = my_func(lambda x: x.std(ddof=0))
try:
    print(df[0].pipe(func))
except Exception as e:
    print("Exception")
    print(e)
print()
print("--- np 標準化")
func = my_func(lambda x: (x - np.mean(x)) / np.std(x))
try:
    print(df[0].pipe(func))
except Exception as e:
    print("Exception")
    print(e)
print()
print("--- pd 標準化")
func = my_func(lambda x: (x - x.mean()) / x.std(ddof=0))
try:
    print(df[0].pipe(func))
except Exception as e:
    print("Exception")
    print(e)
--- x**2
Type of x: <class 'pandas.core.series.Series'>
0     1
1    36
2     9
Name: 0, dtype: int64

--- np.sin(x)
Type of x: <class 'pandas.core.series.Series'>
0    0.841471
1   -0.279415
2    0.141120
Name: 0, dtype: float64

--- np.std(x)
Type of x: <class 'pandas.core.series.Series'>
2.0548046676563256

--- x.std()
Type of x: <class 'pandas.core.series.Series'>
2.0548046676563256

--- np 標準化
Type of x: <class 'pandas.core.series.Series'>
0   -1.135550
1    1.297771
2   -0.162221
Name: 0, dtype: float64

--- pd 標準化
Type of x: <class 'pandas.core.series.Series'>
0   -1.135550
1    1.297771
2   -0.162221
Name: 0, dtype: float64

もう、完全に想定通り。

print("--- x**2")
func = my_func(lambda x: x**2)
try:
    print(df.pipe(func))
except Exception as e:
    print("Exception")
    print(e)
print()
print("--- np.sin(x)")
func = my_func(lambda x: np.sin(x))
try:
    print(df.pipe(func))
except Exception as e:
    print("Exception")
    print(e)
print()
print("--- np.std(x)")
func = my_func(lambda x: np.std(x))
try:
    print(df.pipe(func))
except Exception as e:
    print("Exception")
    print(e)
print()
print("--- x.std()")
func = my_func(lambda x: x.std(ddof=0))
try:
    print(df.pipe(func))
except Exception as e:
    print("Exception")
    print(e)
print()
print("--- np 標準化")
func = my_func(lambda x: (x - np.mean(x)) / np.std(x))
try:
    print(df.pipe(func))
except Exception as e:
    print("Exception")
    print(e)
print()
print("--- pd 標準化")
func = my_func(lambda x: (x - x.mean()) / x.std(ddof=0))
try:
    print(df.pipe(func))
except Exception as e:
    print("Exception")
    print(e)
--- x**2
Type of x: <class 'pandas.core.frame.DataFrame'>
    0   1
0   1  16
1  36  49
2   9  25

--- np.sin(x)
Type of x: <class 'pandas.core.frame.DataFrame'>
          0         1
0  0.841471 -0.756802
1 -0.279415  0.656987
2  0.141120 -0.958924

--- np.std(x)
Type of x: <class 'pandas.core.frame.DataFrame'>
0    2.054805
1    1.247219
dtype: float64

--- x.std()
Type of x: <class 'pandas.core.frame.DataFrame'>
0    2.054805
1    1.247219
dtype: float64

--- np 標準化
Type of x: <class 'pandas.core.frame.DataFrame'>
          0         1
0 -1.622214 -0.267261
1  0.811107  2.138090
2 -0.648886  0.534522

--- pd 標準化
Type of x: <class 'pandas.core.frame.DataFrame'>
          0         1
0 -1.135550 -1.069045
1  1.297771  1.336306
2 -0.162221 -0.267261
FutureWarning: The behavior of DataFrame.std with axis=None is deprecated, in a future version this will reduce over both axes and return a scalar. To retain the old behavior, pass axis=0 (or do not pass axis)
  return std(axis=axis, dtype=dtype, out=out, ddof=ddof, **kwargs)

どちらかというとNumPyについて知っているかどうかに関係するが、「np 標準化」で得られた数値と「pd 標準化」で得られた数値が全く違う値となっている。これはNumPyとpandasのmean()の動作の違いに起因する差異である。

print("np.mean()")
func = my_func(lambda x: np.mean(x))
print(df.pipe(func))
print()
print("pd.mean()")
func = my_func(lambda x: x.mean())
print(df.pipe(func))
np.mean()
Type of x: <class 'pandas.core.frame.DataFrame'>
4.333333333333333

pd.mean()
Type of x: <class 'pandas.core.frame.DataFrame'>
0    3.333333
1    5.333333
dtype: float64

DataFrameに対してNumPyのmean()は、デフォルトでは行列単位での平均ではなく全ての値の平均を算出する。一方pandasのDataFrame.mean()は、行または列単位での平均を算出する。知っていればこういったことは避けられるが、必ずしも全てを知っている人間は存在しないので、知らなくても対応できるようにすると良い。maen()に限らずaxisに関するパラメーターは基本的には明示して、デフォルトを使用するときは意図を持ってデフォルトを使用する方が不具合を避けられるだろう。

print("np.mean()")
func = my_func(lambda x: np.mean(x, axis=0))
print(df.pipe(func))
print()
print("pd.mean()")
func = my_func(lambda x: x.mean(axis=0))
print(df.pipe(func))
np.mean()
Type of x: <class 'pandas.core.frame.DataFrame'>
0    3.333333
1    5.333333
dtype: float64

pd.mean()
Type of x: <class 'pandas.core.frame.DataFrame'>
0    3.333333
1    5.333333
dtype: float64

11-3. まとめ

  • 基本的に、Series.〇〇()メソッドには要素の値が渡され、DataFrame.〇〇()メソッドには列ごとのSeriesが渡される。
  • 例外1 : pipe()では、self(オブジェクト自身)がfuncに渡される。
  • 例外2 : map()では、要素の値がfuncに渡される。
  • 例外3 : apply()では、パラメータに応じてfuncに渡される値が変わる。
  • 例外4 : Series.agg()Series.transform()では、func内で例外が発生した場合、Seriesが再度funcに渡されて計算される。

その他の注意事項

  • 要素の値が渡される場合、NumPyの集約関数を使用すると、意図した計算が行われないことがある。
  • pandasの関数を使用する方が意図した計算結果を得やすい。ただし、要素の値がfuncに渡される場合は、一部の特殊なメソッド(Series.agg()Series.transform())を除き、pandasの集約関数を使うことはできない。
  • axisなど、NumPyやpandasの様々な関数やメソッドで定義されているパラメーターは、できる限り明示的に指定した方が動作の勘違いによる不具合を避けられる。

12. 各メソッドの性能差簡易計測

apply()は性能が遅いという事前情報があるため、軽めの性能確認を行っていた。しかし、apply()transform()のどちらが速いかなど、横断的に比較した際の性能差はわからない。わからないなら、実際に試してみよう。

12-1. 計測

サンプルデータを作成する。

import numpy as np
import pandas as pd

data1 = np.eye(1000)
data2 = data1.reshape(1000000)
df = pd.DataFrame(data1)
s = pd.Series(data2)

まず、funcに要素の値が渡されるケースで値の変換を確認する。

%timeit s.apply(lambda x: "Yes" if x == 1 else "Nooooooop!!!!!!")
%timeit s.map(lambda x: "Yes" if x == 1 else "Nooooooop!!!!!!")
%timeit s.map({1: "Yes", 0:"Nooooooop!!!!!!"})
%timeit s.agg(lambda x: "Yes" if x == 1 else "Nooooooop!!!!!!")
%timeit s.transform(lambda x: "Yes" if x == 1 else "Nooooooop!!!!!!")
912 ms ± 16.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
921 ms ± 15.3 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
15.7 ms ± 45.7 μs per loop (mean ± std. dev. of 7 runs, 100 loops each)
<magic-timeit>:1: FutureWarning: using <function inner.<locals>.<lambda> at 0x0000021301736EF0> in Series.agg cannot aggregate and has been deprecated. Use Series.transform to keep behavior unchanged.
897 ms ± 6.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
899 ms ± 12.2 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

lambdaで作成した変換処理において、ほとんど性能差は見られない。map()メソッドにマッピング用の辞書を渡すと、lambdaによる処理よりも約1/60に速くなる。値変換を行う場合は、まずmap()メソッドを選択するのが良さそうである。

次に、funcに要素の値が渡されるケースで、特にNumPyやpandasの関数を利用せずに計算を行う場合を確認する。

%timeit s.apply(lambda x: x*2)
%timeit s.map(lambda x: x*2)
%timeit s.agg(lambda x: x*2)
%timeit s.transform(lambda x: x*2)
889 ms ± 18.3 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
919 ms ± 46.8 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
<magic-timeit>:1: FutureWarning: using <function inner.<locals>.<lambda> at 0x00000213017376D0> in Series.agg cannot aggregate and has been deprecated. Use Series.transform to keep behavior unchanged.
867 ms ± 13.6 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
831 ms ± 13.8 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

誤差の範囲内に収まった結果となった。funcに要素の値が渡される場合、優位性のあるメソッドは特にない。

続いて、funcSeriesndarrayなど1次元のデータが渡されるケースで、NumPyやpandasの関数を利用せずに計算を行う場合を確認する。

%timeit df.apply(lambda x: x*2)
%timeit df.apply(lambda x: x*2, raw=True)
%timeit df.map(lambda x: x*2)
%timeit df.agg(lambda x: x*2)
%timeit df.transform(lambda x: x*2)
312 ms ± 4.69 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
16.1 ms ± 141 μs per loop (mean ± std. dev. of 7 runs, 100 loops each)
994 ms ± 32.6 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
306 ms ± 3.11 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
306 ms ± 2.03 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

うっかりDataFrame.map()も計測している。要素ごとの値をfuncに渡すmap()での計算は圧倒的に遅いため、使用すべきではない。このケースでは、apply()raw=Trueを指定してndarrayで計算を行う方法が最も高速だ。

本来の使い方と異なるかもしれないが、おそらく最速なのはfuncDataFrameが渡されるpipe()ではないかと思われる。

%timeit df.pipe(lambda x: x*2)
2.55 ms ± 283 μs per loop (mean ± std. dev. of 7 runs, 100 loops each)

最速だった。

最後にSeriesndarrayが渡されるケースで標準化処理を行った場合の計測を行う。

%timeit df.apply(lambda x: (x - x.mean(axis=0)) / x.std(ddof=0, axis=0))
%timeit df.apply(lambda x: (x - np.mean(x, axis=0)) / np.std(x, ddof=0, axis=0, raw=True)
%timeit df.agg(lambda x: (x - x.mean(axis=0)) / x.std(ddof=0, axis=0))
%timeit df.transform(lambda x: (x - x.mean(axis=0)) / x.std(ddof=0, axis=0))
622 ms ± 17.8 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
85.9 ms ± 1.13 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
615 ms ± 11.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
607 ms ± 7.84 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

やはり、apply()raw=Trueが最も早い。他のメソッドは誤差の範囲内だろう。apply()が遅いと言われがちだが、funcを引数にとる系のメソッドを使う場合には、最も適した選択肢であると言える。

興味本位でpipe()も試してみる。

%timeit df.pipe(x - x.mean(axis=0)) / x.std(ddof=0, axis=0))
13.6 ms ± 153 μs per loop (mean ± std. dev. of 7 runs, 100 loops each)

絶対に使い方が違う気がするが・・・最速なんだなぁ。

前の項でも記載したが、性能は環境依存であり、すべての環境で「この手法が最善」と断言することはできない。本項はあくまで参考程度にとどめて頂けると幸いである。

13. さいごに

最後までお読みいただき、ありがとうございます。

脚注
  1. NumPyのユニバーサル関数とは、numpy.ufuncクラスのインスタンスで、要素ごとにndarrayを操作する関数で、配列ブロードキャスト、型キャストなどをサポートする。
    https://numpy.org/doc/stable/user/basics.ufuncs.html#ufuncs-basics
    https://numpy.org/doc/stable/reference/ufuncs.html ↩︎

Discussion