StatsFragments

Python, R, Rust, 統計, 機械学習とか

Python pandas の算術演算 / 集約関数 / 統計関数まとめ

概要

恒例の pandas 記事。今回は 基本的な算術演算についてまとめた。このあたりの挙動は numpy と一緒で直感的だと思うが、知っていないとハマるポイントがいくつかあるので。

準備

サンプルは DataFrame のみ。だが内容は Series でも同じ ( 行/列 2次元のデータに関するに記載は除く)。

import numpy as np
import pandas as pd

df = pd.DataFrame({'C1': [1, 1, 1],
                   'C2': [1, 1, 1],
                   'C3': [1, 1, 1]})
df
#    C1  C2  C3
# 0   1   1   1
# 1   1   1   1
# 2   1   1   1

四則演算

まずは基本的な例。

# スカラーの加算はデータ全体に対して適用 ( ブロードキャスト ) される
df + 1
#    C1  C2  C3
# 0   2   2   2
# 1   2   2   2
# 2   2   2   2

# np.array の加算は 各行に対する加算
df + np.array([1, 2, 3])
#    C1  C2  C3
# 0   2   3   4
# 1   2   3   4
# 2   2   3   4

# 要素の長さが違う場合は ValueError
df + np.array([1, 2])
# ValueError: Wrong number of items passed 2, place

とはいえ、演算をデータ全体へブロードキャストさせたいことはマレだと思う。特定の列 あるいは 特定セルに対して演算を行う場合は、以下の記事でまとめたデータ選択記法を使って計算結果を代入する。

補足 データへの演算自体は非破壊的な処理だが、代入によって元のデータが変更されることに注意。

# 1列目に [1, 2, 3] を足す
df['C1'] = df['C1'] + np.array([1, 2, 3])
df
#    C1  C2  C3
# 0   2   1   1
# 1   3   1   1
# 2   4   1   1

# 3行目 / 3列目の値に 5 を足す
df.iloc[2, 2] += 5
df
#    C1  C2  C3
# 0   2   1   1
# 1   3   1   1
# 2   4   1   6

# 複数列に対する演算も OK
# (裏側では DataFrame 選択 -> 計算 -> 代入 をしているだけなので)
df[['C1', 'C2']] -= 5
df
#    C1  C2  C3
# 0  -3  -4   1
# 1  -2  -4   1
# 2  -1  -4   6

算術演算メソッド

演算の挙動をもうすこし制御したい場合は、DataFrame.addDataFrameSeries は、add のほかにも Python 標準モジュールの operators と対応する算術演算メソッド / 論理演算メソッドを持つ。利用できるメソッドの一覧はこちら。オプションはどれも重要なので順番に。

列に対するブロードキャスト (axis=0)

上記のような1列の加算ではなく、列全体に対してブロードキャストをしたい場合は axis=0 を指定。

  • axis=0 もしくは axis='index' で列に対する演算
  • axis=1 もしくは axis='columns'で行に対する演算 (デフォルト)
# データは元に戻す
df = pd.DataFrame({'C1': [1, 1, 1],
                   'C2': [1, 1, 1],
                   'C3': [1, 1, 1]})
df
#    C1  C2  C3
# 0   1   1   1
# 1   1   1   1
# 2   1   1   1

df.add(np.array([1, 2, 3]), axis=0)
#    C1  C2  C3
# 0   2   2   2
# 1   3   3   3
# 2   4   4   4

df.add(np.array([1, 2, 3]), axis='index')
# 略

欠測値 ( NaN ) 要素へのパディング (fill_value)

データに 欠測値 ( NaN ) が含まれるとき、その要素への演算の結果も NaN になる。これは numpy の挙動と同じ。

# numpy の挙動
np.nan + 1
# nan

# NaN を含む DataFrame を定義する
df_nan = pd.DataFrame({'C1': [1, np.nan, np.nan],
                       'C2': [np.nan, 1, np.nan],
                       'C3': [np.nan, np.nan, 1]})
df_nan
#    C1  C2  C3
# 0   1 NaN NaN
# 1 NaN   1 NaN
# 2 NaN NaN   1

# NaN を含むセルの演算結果は NaN 
df.add(df_nan)
#    C1  C2  C3
# 0   2 NaN NaN
# 1 NaN   2 NaN
# 2 NaN NaN   2

これに気づかず四則演算していると、なんか合計があわないな?ということになってしまう。この挙動を変えたい場合は fill_value。演算の実行前に fill_value で指定した値がパディングされる。ただし、演算対象の要素が 両方とも NaN の場合は NaN のまま。

df.add(df_nan, fill_value=0)
#    C1  C2  C3
# 0   2   1   1
# 1   1   2   1
# 2   1   1   2

# データの順序が変わっても有効 ( fill_value は演算対象 両方のデータに適用される)
df_nan.add(df, fill_value=0)
#    C1  C2  C3
# 0   2   1   1
# 1   1   2   1
# 2   1   1   2

# 要素が 両方とも NaN の場合はパディングされない
df_nan.add(df_nan, fill_value=0)
#    C1  C2  C3
# 0   2 NaN NaN
# 1 NaN   2 NaN
# 2 NaN NaN   2

# DataFrame について、対象データが array, Series の場合の処理は未実装 (NotImplementedError) なので注意
df.add(np.array([1, np.nan, 1]), fill_value=0)
# NotImplementedError: fill_value 0 not supported

最後のオプション、 levelindex複数の階層 (レベル) 持つ場合 ( MultiIndex を持つ場合 ) の制御を行う。このオプションの挙動は近日公開予定 (?) の MultiIndex 編で。

行列積

*DataFrame 同士の積をとった場合は、各要素同士の積になる。

df2 = pd.DataFrame({'C1': [2, 3],
                    'C2': [4, 5]})
df2
#    C1  C2
# 0   2   4
# 1   3   5

df3 = pd.DataFrame({'C1': [1, -2],
                    'C2': [-3, 4]})
df3
#    C1  C2
# 0   1  -3
# 1  -2   4

df2 * df3
#    C1  C2
# 0   2 -12
# 1  -6  20

行列の積をとりたい場合は DataFrame.dot。ただし、行列の積をとるためには元データの columns と 引数の index のラベルが一致している必要がある (説明 次節)。そのため、上のように columns / index が一致しないデータから直接 行列の積をとることはできない。そんなときは DataFrame.values プロパティで引数側の内部データを numpy.array として取り出せばよい。

# NG!
df2.dot(df3)
# ValueError: matrices are not aligned

# values プロパティで内部データを numpy.array として取り出せる
df3.values
# array([[ 1, -3],
#        [-2,  4]], dtype=int64)

# OK!
df2.dot(df3.values)
#    0   1
# 0 -6  10
# 1 -7  11

引数側を転置すれば、元データの columns と 引数の index のラベルが一致する。このようなデータはそのまま DataFrame.dot で積をとれる。

df3.T
#     0  1
# C1  1 -2
# C2 -3  4

df2.dot(df3.T)
#     0   1
# 0 -10  12
# 1 -12  14

補足 そもそも Python には行列積の演算子がない。が、現在 PEP-465 で行列積として @ 演算子が提案されている。

ラベルによる演算の制御

ここまでは index, columns が一致するデータのみを対象にしてきた。実際には演算を行うデータの index, columns が異なることもある。その場合の挙動には少し注意が必要。

Series, DataFrame 同士の演算は、 index, columns のラベルが一致する要素同士で行われる。例えば以下のように indexcolumns がずれているとき、対応しない要素は NaN となる。

df4 = pd.DataFrame({'C1': [1, 1, 1],
                    'C2': [1, 1, 1],
                    'C3': [1, 1, 1]})
df4
#    C1  C2  C3
# 0   1   1   1
# 1   1   1   1
# 2   1   1   1

df5 = pd.DataFrame({'C2': [1, 1, 1],
                    'C3': [1, 1, 1],
                    'C4': [1, 1, 1]},
                   index=[1, 2, 3])

df5
#    C2  C3  C4
# 1   1   1   1
# 2   1   1   1
# 3   1   1   1

df4 + df5
#    C1  C2  C3  C4
# 0 NaN NaN NaN NaN
# 1 NaN   2   2 NaN
# 2 NaN   2   2 NaN
# 3 NaN NaN NaN NaN

どう直せばよいかは状況による。index, columns のずれ自体は OK で、 NaN でのパディングが気に入らない場合は fill_value

df4.add(df5, fill_value=0)
#    C1  C2  C3  C4
# 0   1   1   1 NaN
# 1   1   2   2   1
# 2   1   2   2   1
# 3 NaN   1   1   1

index, columnsに関係なく、対応する位置の要素同士で演算したい場合は DataFrame.values を使って取得した numpy.array のみを渡せばよい。

df4 + df5.values
#    C1  C2  C3
# 0   2   2   2
# 1   2   2   2
# 2   2   2   2

もしくは、以下のようにして データの index もしくは columns を揃えてから演算する。

  • df4indexdf5 にそろえる場合は、df4.index に代入。
  • df5indexdf4 にそろえる場合は、df5.index に代入。もしくは、DataFrame.reset_index(drop=True)index を 0 から振りなおし
# df4 の index を df5 にそろえる
df4.index = [1, 2, 3]
df4
#    C1  C2  C3
# 1   1   1   1
# 2   1   1   1
# 3   1   1   1

# df5 の index を df4 にそろえる 
df5.reset_index(drop=True)
#    C2  C3  C4
# 0   1   1   1
# 1   1   1   1
# 2   1   1   1

# reset_index デフォルトでは、元の index をカラムとして残す
df5.reset_index()
#    index  C2  C3  C4
# 0      1   1   1   1
# 1      2   1   1   1
# 2      3   1   1   1

集約関数

Series, DataFrame は 合計を取得する sum, 平均を計算する mean などの一連の集約関数に対応するメソッドを持つ。一覧は こちら

df6 = pd.DataFrame({'C1': ['A', 'B', 'C'],
                    'C2': [1, 2, 3],
                    'C3': [4, 5, 6]})
df6
#   C1  C2  C3
# 0  A   1   4
# 1  B   2   5
# 2  C   3   6

集約系の関数は既定では列方向に対して適用される。基本的には 数値型のみが集約対象になる。が、一部の関数では数値以外の型に対してもがんばってくれるものもある。

# mean は数値型のみ集約
df6.mean()
# C2    2
# C3    5
# dtype: float64

# sum は 文字列も集約
df6.sum()
# C1    ABC
# C2      6
# C3     15
# dtype: object

# 数値以外が不要な場合は numeric_only=True
df6.sum(numeric_only=True)
# C2     6
# C3    15
# dtype: int64

# 行方向へ適用したい場合は axis = 1
# このとき、数値型以外は除外して関数適用される
df6.sum(axis=1)
# 0    5
# 1    7
# 2    9
# dtype: int64

# 明示的に含めようとするとエラー
df6.sum(axis=1, numeric_only=False)
# TypeError: cannot concatenate 'str' and 'long' objects

複数列のデータをまとめて関数適用したい、という場合もたまーにある。pandas には直接対応するメソッドはないが、以下のようにすればできる。

# values.flatten() で 2列分の値を ひとつの numpy.array として取得
df6[['C2', 'C3']].values.flatten()
# array([1, 4, 2, 5, 3, 6], dtype=int64)

# 集約関数適用
np.mean(df6[['C2', 'C3']].values.flatten())
# 3.5

補足 numpy.array.flatten() は1次元化した array のコピーを返す。

統計関数

また、分散を計算する var, 標準偏差を計算する std などの統計関数に対応するメソッドもある。一覧は上と同じく こちら。関数の適用方向など、挙動は集約関数と一緒。

1点 覚えておくべき箇所は、pandas では分散 / 標準偏差について不偏推定量の計算がデフォルトになっている。これは numpy の挙動 ( 標本統計量を返す ) とは異なる。この挙動は pandas, numpy ともに ddof オプションで制御できる。

  • pandas : 不偏推定量の計算 ( ddof=True ) がデフォルト。
  • numpy : 標本統計量の計算 ( ddof=False ) がデフォルト。

補足 ddof = Delta Degrees of Freedom

df7 = pd.DataFrame({'C1': [1, 2, 3, 4],
                    'C2': [4, 5, 6, 7],
                    'C3': [2, 3, 3, 2]})
#    C1  C2  C3
# 0   1   4   2
# 1   2   5   3
# 2   3   6   3
# 3   4   7   2

# 不偏分散
df7.var()
# C1    1.666667
# C2    1.666667
# C3    0.333333
# dtype: float64

# 標本分散
df7.var(ddof=False)
# C1    1.25
# C2    1.25
# C3    0.25
# dtype: float64

# 標本分散 (numpy)
np.var(df7)
# C1    1.25
# C2    1.25
# C3    0.25
# dtype: float64

# 不偏標準偏差
df7.std()
# C1    1.290994
# C2    1.290994
# C3    0.577350
# dtype: float64

# 標本標準偏差
df7.std(ddof=False)
# C1    1.118034
# C2    1.118034
# C3    0.500000
# dtype: float64

# 標本標準偏差 (numpy)
np.std(df7)
# C1    1.118034
# C2    1.118034
# C3    0.500000
# dtype: float64

基本統計量の表示 ( describe )

最後。基本統計量をまとめて計算したい場合は DataFrame.describe()

df7.describe()
#              C1        C2       C3
# count  4.000000  4.000000  4.00000
# mean   2.500000  5.500000  2.50000
# std    1.290994  1.290994  0.57735
# min    1.000000  4.000000  2.00000
# 25%    1.750000  4.750000  2.00000
# 50%    2.500000  5.500000  2.50000
# 75%    3.250000  6.250000  3.00000
# max    4.000000  7.000000  3.00000

まとめ

pandas での算術演算、集約関数、統計関数の挙動をまとめた。ポイントは、

  • DataFrame への演算は適用される方向に注意。演算方向を指定する場合、列方向なら axis=0, 行方向は axis=1
  • 演算したい要素に NaN が含まれる場合、必要に応じて fill_value
  • 演算したい要素同士のラベルは一致させること。

Pythonによるデータ分析入門 ―NumPy、pandasを使ったデータ処理

Pythonによるデータ分析入門 ―NumPy、pandasを使ったデータ処理