【Python】【pandas入門】引数がfunc系のアイツラ ~オモテのDataFrame/Series編~
0. はじめに
map()
、apply()
、agg()
など、引数に関数(func
)を取るメソッドについて調査し、その内容をまとめた。これらのメソッドは、データを変換したり計算したりする際に非常に便利だが、その使い方や違いについては、少し混乱しがちな部分もあるため、整理することにした。
本記事では、pandas.DataFrame
およびpandas.Series
にて定義されているメソッドを中心に扱うが、pandas.core.groupby.generic.DataFrameGroupBy
やpandas.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_row やraw などのパラメータの要否の検討や、numba や並列化を検討する。 |
agg() |
引数として渡したfunc を使用して集計を行う。配列や辞書にfunc を格納して複数の集計を一度に実行できる。aggregate() のエイリアスであり、処理上の差異はない。 |
transform() |
引数として渡したfunc を使用してデータを変換する。agg() と同様に、複数の処理を一度に実行できるようにfunc を配列などに設定して実行することが可能。返却される値は呼び出し元オブジェクトと同じサイズ(同じlen() )になるため、集計用ではなく、正則化やNumPy のufunc 適用による計算に適している。 |
pipe() |
引数として渡したfunc でパイプラインチェーンを構築する。func にはself が渡されるため、DataFrame 内の特定の複数の列に対する処理が可能。その他のfunc 系メソッドよりも圧倒的な処理性能を発揮するポテンシャルがあるが、本来の使い方とは異なる気がする。 |
3-2. funcに渡される値
ユーザー定義関数やlambda
を使用する場合、パラメーターに何が渡されるかを考慮する必要がある。例えば、「要素の値」が渡されるケースで、func
がlambda x: np.sum(x)
である場合、func
はx
を返すが、この場合は意図した動作になっていない可能性があり、エラーも発生しない。
ユーザー定義関数やlambda
を使用しない場合は、特に考慮する必要はない。
クラス | メソッド名 |
func に渡される値 |
---|---|---|
Series |
map() |
要素の値 |
Series |
apply() |
by_row=False の場合、pandas.Series (self)それ以外の場合、要素の値 |
Series |
agg() |
要素の値。ValueError 、AttributeError 、TypeError のいずれかが発生した場合、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.Series
がfunc
に渡されるが、例外パターンは以下の通り。
-
map()
はfunc
にはどのような状況下でも要素の値が渡される。 -
pipe()
はfunc
にはself
(自分自身)のオブジェクトが渡される。 -
apply()
は、パラメーターに応じてfunc
に渡されるオブジェクトが異なる。 -
Series.agg()
とSeries.transform()
は、最初に要素の値をfunc
に渡して計算を行い、エラーが発生した場合は、Series
(self
)自体をfunc
に渡して再度計算を行う。
3-3. 性能差
- 値の計算での用途について、
apply()
が最も速い可能性がある。 - 値の計算での用途について、
map()
は最も性能が悪い可能性がある。 - 値のマッピング変換用途では、
map()
で変換用の辞書をパラメータに使うのが最も速い可能性がある。 - 環境依存なので、性能確認の結果を全ての環境で保証することはできない
4. applymap()メソッド(Deprecated)
applymap()
はpandas 2.1.0にてDeprecatedとなり、現在はmap()
の使用が推奨されている。
なので、基本的に前方互換の目的以外では使う必要はなさそう。ただし、書籍によっては登場したりするので一応まとめておく。
applymap()
はDataFrame
でのみ定義されている。
なお、現在のapplymap()
はDeprecatedである旨のWarningを表示してmap()
メソッドを呼び出しているだけなので、DataFrame.map()
の利用と等価。
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. インターフェース
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()
前述の通り、applymap()
が内部的に呼び出しているのはこのmap()
メソッドなので、メソッドの仕様はapplymap()
の項目を参照。
6. apply()メソッド - 適用する
apply()
メソッドはSeries、DataFrameどちらにも定義されている。
以下は「apply()
を使用する必要があるかどうか」についてStackOverflowのポスト。
英語弱者なのでざっくりとしか内容を読むことはできなかったが、高い汎用性であるが故に、適用する関数の性質を何も定めていないため、必要に応じて関数を各行や列に繰り返し適用、その処理毎にオーバーヘッドが発生するため性能やメモリの消費が最適化されていないとのこと。
処理対象となるデータ件数、適用する処理が要素単位で処理を行うのか、ベクトル単位で処理を行うのか、などを考慮すべき事項が色々あって、上級者向きかな?
上級者ではない私には触れずらい、まさに腫物のような存在のメソッドである。
上記StackOverflowのポストも古い記事なので、現在のバージョンのpandasでどうかは確認する必要があるだろう。
numba(JITコンパイラ)とか並列化とか色々、apply()
を高速化する手法は存在するようだが、諸々の処理速度検証はいつか別途行うとして(しなさそう)、まずは基本的な機能を抑えたい。
6-1. pandas.Series.apply()
6-1-1. インターフェース
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
を計算しているため、処理時間がはええ。
-
6-2. pandas.DataFrame.apply()
6-2-1. インターフェース
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=0
、axis=1
での性能差があるらしい。この目で確かめてえんだ。 -
raw
、by_row
に応じた性能差。 - 処理の種類に応じた各パラメータの性能差。
- NumPy ufunc, NumPy reducing function, pandas集計関数
まずは、NumPy ufunc(np.sin()
)に対する各パラメータの性能差を確認する。
1万×1万の単位行列に対して、axis=0
、axis=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=0
、axis=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. インターフェース
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. インターフェース
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. インターフェース
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. インターフェース
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=True
、cumprod()
は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()
メソッドはSeries
とDataFrame
の親クラスであるNDFrame
に定義されている。
10-1-1. インターフェース
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"]
)
損益の計算は適当に超ざっくりと以下のようにしよう。
- 売上計算:sum(各商品単価 × 販売個数)
- 仕入計算:sum(各原材料単価 × 仕入個数)
- 粗利の計算:売上 - 仕入
- 固定費の計算 : sum(各固定費)
- 損益計算: 粗利 - 固定費
各種計算を行う関数を定義する。
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系メソッドと明確に異なるシーンで利用することになるだろう。
func
に渡される値
11. 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
を標準化した値が算出される。
-
要素の値が渡される場合 : 0除算が発生し、
-
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
各処理のケースでもSeries
がfunc
に渡っており、計算結果についても想定通りの結果となっている。
続いて、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
各処理のケースでもSeries
がfunc
に渡っており、計算結果についても想定通りの結果となっている。
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
各処理のケースでもndarray
がfunc
に渡っている。計算結果について特に想定を記載していないが、違和感はない結果となっている。
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
には要素の値が渡されるため、集約系の処理は意図した計算結果とならない。また、DataFrame
やSeries
のメソッドである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
を実行するという作りになっている。
このような作りのため、上記サンプルコードの「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
いずれのケースでも、Series
がfunc
に渡されており、各結果も想定通りである。
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行目辺りである。
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
いずれのケースでも、Series
がfunc
に渡されており、各結果も想定通りである。
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
に要素の値が渡される場合、優位性のあるメソッドは特にない。
続いて、func
にSeries
やndarray
など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
で計算を行う方法が最も高速だ。
本来の使い方と異なるかもしれないが、おそらく最速なのはfunc
にDataFrame
が渡されるpipe()
ではないかと思われる。
%timeit df.pipe(lambda x: x*2)
2.55 ms ± 283 μs per loop (mean ± std. dev. of 7 runs, 100 loops each)
最速だった。
最後にSeries
やndarray
が渡されるケースで標準化処理を行った場合の計測を行う。
%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. さいごに
最後までお読みいただき、ありがとうございます。
-
NumPyのユニバーサル関数とは、
numpy.ufunc
クラスのインスタンスで、要素ごとにndarrayを操作する関数で、配列ブロードキャスト、型キャストなどをサポートする。
https://numpy.org/doc/stable/user/basics.ufuncs.html#ufuncs-basics
https://numpy.org/doc/stable/reference/ufuncs.html ↩︎
Discussion