PandasのDataFrameやSeriesにPandas以外の関数を適用させる方法はいくつか存在します。
1つは適用させたい関数の引数として直接DataFrame(Series)を指定する方法で、
もう1つはapply関数やapplymap関数を使用する方法です。
NumPyの関数を適用させたい場合は直接指定する方法でほとんど問題ありませんが、他のモジュールの関数や自作の関数ですとapplyやapplymapを使わないと思うような動作をしないことがあるのでこちらの関数を使って計算することをオススメします。
本記事では以下の方法を一つ一つ解説していきます。
- 関数を直接適用
- 
applyでDataFrameやSeriesの列や行ごと
- 
applymapでDataFrameの各要素ごと
- 
mapでSeriesの各要素ごと
- 
groupbyメソッドと併用できるのはapply
NumPyの関数を直接適用させる
DataFrameやSeriesの中身のデータを配列と見立ててNumPyの関数を適用させることができます。 使える引数の種類がPandas用に変換されており(例えばkeepdims引数が無効になっているなど)、NumPyの配列を対象にした時ほど自由度は高くありませんが、実用の範囲内ならほぼ問題ないでしょう。
PandasとNumPyの親和性は非常に高く、手軽に適用ができます。
NumPyの関数のタイプでも
- 
np.sqrt関数のように各要素ごとに処理を実行する関数(ufuncと呼びます)
- 
np.sum関数のように列全体や行全体を対象とする関数
の2種類に大きく分けることができます。
各要素に処理を実行する関数はDataFrameやSeriesの形状に関係なく1つ1つ処理を実行するだけなのであまり混乱はありませんが、 列や行全体(もしくはデータ全体)を処理対象とする関数についてはNumPyのndarrayを対象とするときと若干挙動が異なります。
ufuncは要素ごと
In [1]: import pandas as pd
In [2]: import numpy as np
In [3]: df = pd.DataFrame({'A': np.random.rand(10),
   ...:                    'B': np.random.rand(10)})
   ...:                    
In [4]: df
Out[4]:
          A         B
0  0.006706  0.686428
1  0.748844  0.642340
2  0.903863  0.631214
3  0.399030  0.075003
4  0.769379  0.206487
5  0.269700  0.034937
6  0.779630  0.979609
7  0.168005  0.434582
8  0.156968  0.874474
9  0.597533  0.508777
上のDataFrameを考えます。これに対してそれぞれ指数関数の値を計算するnp.exp関数を適用させてみます。
In [5]: np.exp(df)
Out[5]:
          A         B
0  1.006728  1.986607
1  2.114553  1.900924
2  2.469122  1.879892
3  1.490379  1.077887
4  2.158426  1.229352
5  1.309572  1.035554
6  2.180664  2.663414
7  1.182943  1.544318
8  1.169959  2.397615
9  1.817630  1.663256
デフォルトでは列ごとに処理を行う
デフォルトではSeriesに対してならデータ全体、DataFrameに対してなら列全体が処理の対象となっており、DataFrameの構造に沿った関数の適用のされかたをしてくれます。
先ほどのDataFrameに対してnp.sum関数を適用させてみましょう。
In [6]: np.sum(df) # 各列ごとの合計を表示する
Out[6]:
A    4.799659
B    5.073851
dtype: float64
In [7]: np.sum(df['A']) # 列全体
Out[7]: 4.7996585197314054
このようにデフォルトでは列ごとの合計を表示します。基本的に処理の実行する方向は行方向(縦方向)に進んで行くのがデフォルトです。
要素の足し合わせを行うcumsum関数も使ってみます。
In [8]: np.cumsum(df)
Out[8]:
          A         B
0  0.006706  0.686428
1  0.755549  1.328768
2  1.659412  1.959982
3  2.058442  2.034985
4  2.827822  2.241473
5  3.097522  2.276409
6  3.877152  3.256018
7  4.045157  3.690600
8  4.202125  4.565074
9  4.799659  5.073851
In [9]: np.cumsum(df['B']) # Seriesは全体になる
Out[9]:
0    0.686428
1    1.328768
2    1.959982
3    2.034985
4    2.241473
5    2.276409
6    3.256018
7    3.690600
8    4.565074
9    5.073851
Name: B, dtype: float64
これもA列とB列とで別々に処理を行なっています。
行ごとに処理を行いたい場合はaxis=1, ‘columns’
列方向に処理を行いたい場合は引数としてaxis=1を指定すれば良いです。axis='columns'とすることも可能です。先ほどの2つの関数に対してaxis引数を変更します。
In [10]: np.sum(df, axis=1) # axis=1
Out[10]:
0    0.693134
1    1.391183
2    1.535077
3    0.474033
4    0.975867
5    0.304637
6    1.759238
7    0.602587
8    1.031443
9    1.106310
dtype: float64
In [11]: np.cumsum(df, axis=1)
Out[11]:
          A         B
0  0.006706  0.693134
1  0.748844  1.391183
2  0.903863  1.535077
3  0.399030  0.474033
4  0.769379  0.975867
5  0.269700  0.304637
6  0.779630  1.759238
7  0.168005  0.602587
8  0.156968  1.031443
9  0.597533  1.106310
欠損値があっても対応可能
欠損値があっても関数の適用は可能です。
In [20]: df.iloc[4, :] = np.nan # 欠損値
In [21]: np.sum(df)
Out[21]:
A    4.030279
B    4.867364
dtype: float64
関数全体に適用させることはできないようです。
自作の関数を適用する
自作の関数を適用する場合、要素ごとの処理ならば関数の引数としてDataFrameを指定しても問題ありません。
In [30]: def func(x):
    ...:     return x * 2
    ...:
In [31]: func(df)
Out[31]:
          A         B
0  0.013412  1.372857
1  1.497687  1.284680
2  1.807725  1.262428
3  0.798060  0.150006
4       NaN       NaN
5  0.539401  0.069874
6  1.559259  1.959217
7  0.336010  0.869164
8  0.313937  1.748949
9  1.195067  1.017554
In [32]: def func2(x):
    ...:     return x**2 + 4*x - 1
    ...:
In [33]: func2(df)
Out[33]:
          A         B
0 -0.973132  2.216897
1  2.556141  1.981960
2  3.432419  1.923288
3  0.755346 -0.694363
4       NaN       NaN
5  0.151540 -0.859032
6  2.726340  3.878067
7 -0.299754  0.927190
8 -0.347488  3.262603
9  1.747180  1.293962
文字列データの操作はSeries.str.(関数)でPython組み込みの文字列関数を呼び出すことができるので。これを使って処理するとよいでしょう。
In [34]: df_str = pd.DataFrame({'A': ["I don't have a cake.", "I'm hungry."],
    ...:                        'B': ["He doesn't have a cake.", "He is hungry."
    ...: ]})
In [46]: df_str['A'].str.replace("'", " B")
Out[46]:
0    I don Bt have a cake.
1             I Bm hungry.
Name: A, dtype: object
apply関数を使って列ごとや行ごとに関数を適用する
次はPandasの関数であるapply関数を使って関数を適用させていきます。無名関数も適用可能です。
apply関数によって指定された関数に渡される値は各々の列のSeriesとなっており、axis='columns'もしくはaxis=1と指定された時は行ごとのSeriesが渡されることになっています。
In [46]: df
Out[46]:
          A         B
0  0.006706  0.686428
1  0.748844  0.642340
2  0.903863  0.631214
3  0.399030  0.075003
4       NaN       NaN
5  0.269700  0.034937
6  0.779630  0.979609
7  0.168005  0.434582
8  0.156968  0.874474
9  0.597533  0.508777
In [47]: f = lambda x: (x - x.min())/(x.max() - x.min())
In [48]: df.apply(f)
Out[48]:
          A         B
0  0.000000  0.689649
1  0.827211  0.642978
2  1.000000  0.631200
3  0.437297  0.042413
4       NaN       NaN
5  0.293142  0.000000
6  0.861526  1.000000
7  0.179789  0.423052
8  0.167487  0.888708
9  0.658555  0.501592
In [49]: def function(dataframe):
    ...:     return dataframe.max() - dataframe.min()
    ...:
In [50]: df.apply(function)
Out[50]:
A    0.897157
B    0.944672
dtype: float64
行ごとに処理させる場合はaxis=1,’columns’を指定
行ごとに処理を実行させたい場合はaxis=1またはaxis='columns'と指定します。
In [52]: df.apply(function, axis='columns')
Out[52]:
0    0.679723
1    0.106504
2    0.272649
3    0.324027
4         NaN
5    0.234764
6    0.199979
7    0.266577
8    0.717506
9    0.088756
dtype: float64
DataFrame,Seriesの各要素にはapplymap, map関数を使う
DataFrameの各要素に対してはapplymap関数を、Seriesの各要素に対してはmap関数を用います。
In [54]: df_2 = pd.DataFrame({'data1' : np.random.randn(10),
    ...:                      'data2' : np.random.randn(10)})
    ...:                      
In [55]: f = lambda x: "大" if x > 0 else "小"
In [56]: df_2.applymap(f)
Out[56]:
  data1 data2
0     小     小
1     小     小
2     大     大
3     小     大
4     大     小
5     大     大
6     小     大
7     小     大
8     小     大
9     大     小
In [57]: df_2['data1'].map(f) # Seriesに対してはmap関数
Out[57]:
0    小
1    小
2    大
3    小
4    大
5    大
6    小
7    小
8    小
9    大
Name: data1, dtype: object
groupby関数との併用にはapplyを使う
SeriesやDataFrameをgroupby関数を使ってグループ分けしたのち、グループごとに関数を適用させたい時はapply関数との相性が良いです。
groupby関数の詳しい使い方については別途解説します。
ここではKaggleのタイタニックのデータを使います。会員登録が必要ですが、以下の公式サイトからダウンロード可能です。
ここのtrain.csvという訓練データを使います。
In [58]: train = pd.read_csv("train.csv")
In [59]: train.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 891 entries, 0 to 890
Data columns (total 12 columns):
PassengerId    891 non-null int64
Survived       891 non-null int64
Pclass         891 non-null int64
Name           891 non-null object
Sex            891 non-null object
Age            714 non-null float64
SibSp          891 non-null int64
Parch          891 non-null int64
Ticket         891 non-null object
Fare           891 non-null float64
Cabin          204 non-null object
Embarked       889 non-null object
dtypes: float64(2), int64(5), object(5)
memory usage: 83.6+ KB
In [60]: def stats(s): # グループごとの概要を取得する。
    ...:     return { "mean" : s.mean(), "max" : s.max(), "min" : s.min(), "count" : s.count()}
    ...:
このように、辞書形式で関数を指定するとSeriesが返されるのでデータの見通しがよくなります。
性別(Sex)と経済的階級(Pclass)でグループ分けをし、この関数を特定の列(今回は同乗した親族の数SibSp)に適用させてみます。
In [62]: train.groupby(["Sex","Pclass"])['SibSp'].apply(stats)
Out[62]:
Sex     Pclass       
female  1       count     94.000000
                max        3.000000
                mean       0.553191
                min        0.000000
        2       count     76.000000
                max        3.000000
                mean       0.486842
                min        0.000000
        3       count    144.000000
                max        8.000000
                mean       0.895833
                min        0.000000
male    1       count    122.000000
                max        3.000000
                mean       0.311475
                min        0.000000
        2       count    108.000000
                max        2.000000
                mean       0.342593
                min        0.000000
        3       count    347.000000
                max        8.000000
                mean       0.498559
                min        0.000000
Name: SibSp, dtype: float64
みやすくするためにunstack関数をつけます。すると、一番内側のIndexが列のラベルに移動します。
In [63]: train.groupby(["Sex","Pclass"])['SibSp'].apply(stats).unstack()
Out[63]:
               count  max      mean  min
Sex    Pclass                           
female 1        94.0  3.0  0.553191  0.0
       2        76.0  3.0  0.486842  0.0
       3       144.0  8.0  0.895833  0.0
male   1       122.0  3.0  0.311475  0.0
       2       108.0  2.0  0.342593  0.0
       3       347.0  8.0  0.498559  0.0
無名関数でも適用できます。
In [69]: f = lambda x: x.std()
In [70]: train.groupby(["Sex","Pclass"])['SibSp'].apply(f)
Out[70]:
Sex     Pclass
female  1         0.665865
        2         0.642774
        3         1.531573
male    1         0.546695
        2         0.566380
        3         1.288846
Name: SibSp, dtype: float64
まとめ
今回はPandasのDataFrameやSeriesに関数を適用させる方法をまとめました。
applyやapplymap、map関数と、似たような関数名が並び混乱するかもしれませんがそれぞれの関数で扱う関数の種類が異なってくるのでこの辺りの違いはざっくりとでいいので押さえておくと処理の際にエラーにならず、スムーズに実装できるかと思います。
また、groupbyメソッドにおいてapply関数は真価を発揮すると言ってもよく、是非色々と使ってみてください。
参考
- Python for Data Analysis 2nd edition –Wes McKinney(書籍)
- pandas.DataFrame.apply — pandas 0.23.4 documentation
- pandas.DataFrame.applymap — pandas 0.23.4 documentation
- pandas.Series.map — pandas 0.23.4 documentation