pandasで指数平滑移動平均

昨日の記事が移動平均だったので、今日は指数平滑移動平均を扱います。
初めて知った日は衝撃だったのですが、pandasには指数平滑移動平均を計算する専用の関数が用意されています。
(pythonを使い始める前はExcel VBAでいちいち実装していたので非常にありがたいです。)

馴染みがない人もいると思いますので軽く紹介しておきます。
元のデータを${x_t}$とし、期間$n$に対して指数平滑移動平均${EWMA_t}$は次のように算出されます。
$$
\begin{align}\alpha &= \frac{2}{1+n}\\
EWMA_0 &= x_0\\
EWMA_t &= (1-\alpha)*EWMA_{t-1} + \alpha * x_t
\end{align}
$$

3番目の式を自分自身に逐次的に代入するとわかるのですが、
$EWMA_t$は、$x_t$から次のように算出されます。
$$
EWMA_t = \alpha\sum_{k=0}^{\infty}(1-\alpha)^k x_{t-k}
$$
$(1-\alpha)$の絶対値は1より小さいので、この無限級数の後ろの方の項は無視できるほど小さくなります。
結果的に、過程${x_t}$の最近の値に重みを置いた加重平均と見做せます。

さて、早速ですが計算してみましょう。

pandasのDataFrameおよび、Seriesに定義されているewm関数を使います。
pandas.DataFrame.ewm


import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
# データ作成
data = pd.Series(np.random.normal(0, 100, 200).cumsum() + 20000)
# 指数平滑移動平均の計算
data_ewm = data.ewm(span=10).mean()
# 可視化
plt.rcParams["font.size"] = 14
fig = plt.figure(figsize=(12, 7))
ax = fig.add_subplot(1, 1, 1)
ax.plot(data, label="元データ")
ax.plot(data_ewm, label="指数平滑移動平均")
plt.legend()
plt.show()

出力がこちら。

ここで一つ注意する点があります。
data_ewm = data.ewm(span=10).mean()
という風に、spanという変数名で期間$10$を渡しています。
ドキュメントを読んでいただくとわかるのですが、span=をつけないと、
comという別の変数に値が渡され、$\alpha$の計算が、
$\alpha=1/(1+com)$となり、結果が変わります。

また、spanやcomを使う以外にも、alpha=で$\alpha$のあたいを直接指定することも可能です。

pandasでカテゴリー変数を数値に変換する

pandasの関数を使ってカテゴリー変数を数値に変換する方法のメモです。
one-hot表現とは異なり、[“a”, “b”, “c”] みたいなデータを、 “a”は0,”b”は1,”c”は2みたいな感じで数字に変換します。

scikit-learnのデータセットにおける、正解ラベルのようなデータ型にする、と言ったほうがわかりやすいかもしれません。
Rにはカテゴリカル変数の型があるのでそちらの方がイメージしやすい人もいるかも。

使う関数は、pandas.factorizeです。

この関数に変換したいリストを渡すと、数値に変換したリストと、何番めの数がもともと何の値だったのかを示す配列を返してくれます。

実際にやってみましょう。
numpyでランダムに10個のデータを作って、それを数値化します。


import numpy as np
import pandas as pd

data = np.random.choice(["a", "b", "c"], 10)
print(data)
# ['b' 'c' 'a' 'c' 'c' 'c' 'c' 'a' 'b' 'a']
labels, uniques = pd.factorize(data)
print(labels)
# [0 1 2 1 1 1 1 2 0 2]
print(uniques)
# ['b' 'c' 'a']

うまくいきました。
uniques の値をアルファベット順にしたい時は、
factorize する時に sort=Trueを指定するとできます。


labels, uniques = pd.factorize(data, sort=True)
print(labels)
# [1 2 0 2 2 2 2 0 1 0]
print(uniques)
# ['a' 'b' 'c']

できました。

pandasでクロス集計

テーブルデータを分析する時、2つの列の値の関係を調べたいことが良くあります。
対象の列がどちらも連続値をとるのであれば、散布図を書きますが、
これが連続値ではなく性別や都道府県、世代、アンケートのn段階評価などの時は、散布図はあまり適しません。

このような場合は、クロス集計を使うと便利です。
(wikipediaでは分割表と呼んでいるようです。)

pandasにクロス集計を行う専用の関数があるのでそれを紹介します。
(職場ではcsvに書き出してtableauに読み込ませてやることも多いのですが、pythonで完結できればそれはそれ便利です。)

ドキュメントはこちら。
pandas.crosstab

aggfuncに値を渡せば、数え上げ以外にも色々できることがあり、pivot_table的な使い方もできるのですが、
とりあえずダミーデータを用意してデータの数え上げでクロス表を作ってみましょう。
ABテストなどで頻繁に使いますからね。


import pandas as pd
import numpy as np
# ダミーデータ生成
df = pd.DataFrame(
        {
            "data1": np.random.randint(1, 6, 100),
            "data2": np.random.choice(["foo", "bar"], 100),
        }
    )
# サンプル(5件)
print(df.sample(5))
# 出力
'''
    data1 data2
71      2   bar
13      1   foo
7       5   bar
16      1   bar
56      2   foo
'''

# クロス集計
cross_df = pd.crosstab(df.data2, df.data1)
print(cross_df)
# 出力
'''
data1   1   2   3   4  5
data2                   
bar    10  13   8  12  8
foo    12   9  11   9  8
'''

とても楽にクロス集計ができました。
SQLでこれを実装しようとするとかなりの手間なので、地味に重宝しています。

完全に蛇足ですが、crosstabを知らなかった頃はこういうコードを書いていました。
indexとcolumnsだけ指定して値が全部0のデータフレームを作って、
元のデータの値を順番に数え上げています。
今思えばcrosstab知らなくても、もう少しまともな書き方がありそうです。


cross_df_2 = pd.DataFrame(
        0,
        index=set(df.data2),
        columns=set(df.data1),
    )
for _, row in df.iterrows():
    cross_df_2.loc[row.data2, row.data1] += 1    
print(cross_df_2)
# 出力
'''
     1   2   3   4  5
foo  12   9  11   9  8
bar  10  13   8  12  8
'''

pandasで散布図行列を書く

何かしらのデータについて調べる時、各データごとの関係をまとめて可視化するために
散布図行列(matrix of scatter plots)を描きたくなることがあります。

matplotlibでゴリゴリ実装するのは手間なので、seabornが良く使われていると思うのですが、
実はpandasでも作成できるのでその紹介です。
(seabornを使えばいいじゃん、というのはごもっともなのですが、
なぜか僕の自宅環境ではwarningが発生し、その原因調査中なのであまり使いたくないのです。)

ドキュメントはこちら。
pandas.plotting.scatter_matrix

早速ですが、irisでやってみましょう。


import pandas as pd
import matplotlib.pyplot as plt
from sklearn.datasets import load_iris

iris = load_iris()
df = pd.DataFrame(
        iris.data,
        columns=iris.feature_names,
    )
pd.plotting.scatter_matrix(df, figsize=(15, 15), c=iris.target)
plt.show()

そして表示される結果がこちらです。

オプションで、 引数cにクラス情報を渡して色もつけました。
このコーディング量でかける図としては十分なクオリティではないでしょうか。

pythonで累積和

数列に対して、最初の項からn番目の項まで足したものの数列が必要になることがあります。
日々の売上データからその日までの累計の売上データを出すような計算ですね。

イメージとしては、
1,2,3,4,5,…に対して、1,3,6,10,15,… を得るような操作です。

スライスと内包表記を組み合わせてもいいし、足し算くらいならfor文を回しても問題ないのですが、
numpyやpandas に専用の関数が用意されているのでその紹介です。

ドキュメントはこの辺
numpy.cumsum
pandas.Series.cumsum

早速実行してみました。


import numpy as np
import pandas as pd

ary = np.arange(1, 11)
print(ary)
# [ 1  2  3  4  5  6  7  8  9 10]

print(ary.cumsum())
# [ 1  3  6 10 15 21 28 36 45 55]

series = pd.Series(np.arange(1, 11))
print(series)
'''
0     1
1     2
2     3
3     4
4     5
5     6
6     7
7     8
8     9
9    10
dtype: int64
'''

print(series.cumsum())
'''
0     1
1     3
2     6
3    10
4    15
5    21
6    28
7    36
8    45
9    55
dtype: int64
'''

よく似た関数として、
numpyには累積の積を返すcumprodが用意されており、
さらにpandasの方にはcumprodに加えて、そこまでの最大値と最小値を得られるcummax, cumminが用意されています。

pandasで数値データを区間ごとに区切って数える

今日偶然知って便利だった関数を紹介します。

まずやりたいこと。
配列でもDatafarameの列でも、数値データを区間(bins)ごとに区切って数えたい場面は多々あると思います。
例えば、
[455,133,666,111,645,236]
みたいなデータを、
100以上,200未満が2個、200以上,300未満が1個、、、といった具合です。
ヒストグラム書くためにやる操作ですね。

これまで僕がこれをやる時100単位でやるのであれば、
各データを100で割り算して小数点以下を切り捨てて、再度100倍するといった面倒なことをやってました。

これを pandas.cut という関数を使うと、非常に容易に行えます。
ドキュメントはこちら。
pandas.cut

早速これを使ってみましょう。
次のコードは、乱数で生成した10個の数値を、100ごとに区切って数えるものです。
結果に出てくる “[300, 400)”といった出力は、文字列に見えますが、Intervalというオブジェクトです。
right=False は指定しないと、n以上m未満ではなく、nより大きいm以下、って区切り方になります。
引数のbinsには区切りたい点のリストを渡していますが、arrange関数で生成して渡しています。


import pandas as pd
import numpy as np  # ダミーデータ生成用

# 300〜999 の整数を10個生成
data = np.random.randint(300, 1000, 10)
# 100 ごとに区切る。
category = pd.cut(data, bins=np.arange(300, 1100, 100), right=False)

print("データとそれが所属する区間")
for d, c in zip(data, category):
    print(d, "・・・", c)

print("\n区間ごとのデータ件数")
print(category.value_counts())

# 以下出力
データとそれが所属する区間
309 ・・・ [300, 400)
305 ・・・ [300, 400)
874 ・・・ [800, 900)
953 ・・・ [900, 1000)
727 ・・・ [700, 800)
950 ・・・ [900, 1000)
384 ・・・ [300, 400)
789 ・・・ [700, 800)
486 ・・・ [400, 500)
501 ・・・ [500, 600)

区間ごとのデータ件数
[300, 400)     3
[400, 500)     1
[500, 600)     1
[600, 700)     0
[700, 800)     2
[800, 900)     1
[900, 1000)    2
dtype: int64

pandasで時系列データの自己相関係数算出

時系列データとそれをn時間分シフトさせたデータ間の相関係数を自己相関係数と呼びます。
データに周期性があるかどうかを確認するときなどに調査します。

単に配列をスライドさせてnumpyか何かで相関係数を出してもいいのですが、
pandasに専用の関数が定義されているのでそれを使って求めることもできます。

pandas.Series.autocorr

試しにやってみましょう。


import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

# 7点ごとに周期性のあるデータを準備
series = pd.Series([5, 4, 5, 6, 5, 3, 1]*10)
# 乱数追加
series += np.random.randn(70) * 0.5

# lag=0から29までの自己相関係数
auto_series = [series.autocorr(lag=i) for i in range(30)]

# 可視化
fig = plt.figure(figsize=(10, 10))
ax = fig.add_subplot(2, 1, 1, title="周期性のあるデータ")
ax.plot(series.index, series)
ax = fig.add_subplot(2, 1, 2, title="自己相関係数")
ax.bar(range(30), auto_series)
plt.show()

出力されるグラフがこちら。

7点間隔の周期が綺麗に現れているのがわかります。

DataFrameを特定の列の値によって分割する

python(pandas)で巨大なデータフレームを扱っている時、
ある列の値によって、分割したいことが良くあります。

pandasのgroupbyのユーザーガイドを見ていて、良い方法を見つけたのでそれを紹介します。
Iterating through groups
のところに書かれていますが、groupbyした後そのままsumなどの関数を適用するだけでなく、イテレーターとして使うことで、
各グループに対して順番に処理を行えます。

実際にやってみましょう。
とりあえずサンプルのデータフレームを作ります。


import pandas as pd
import numpy as np
df = pd.DataFrame(
            np.random.randn(10000, 10),
            columns=['x'+str(i) for i in range(10)],
        )
df['y'] = np.random.randint(20, size=10000)

これで、長さが1万行、列が11個あるデータフレーム(df)ができました。
これをy列の値(0〜19)によって、20個のデータフレームに分割します。


df_dict = {}
for name, group in df.groupby('y'):
    df_dict[name] = group

これで、20個のデータフレームに分割できました。

ちなみに、この方法を知る前は次のように書いていました。


df_dict = {}
for name in set(df.y):
    df_dict[name] = df[df.y == name]

コードの量はほとんど同じなのですが、この方法は無駄な比較を何度も行うことになります。

実際、実行時刻を測ってみるとgroupbyを使う方法は、
CPU times: user 7.47 ms, sys: 3.28 ms, total: 10.8 ms
Wall time: 7.71 ms

なのに対して、使わない方法は
CPU times: user 16.4 ms, sys: 1.81 ms, total: 18.2 ms
Wall time: 16.5 ms
となり、倍以上の時間を要しています。

pandasでgroupbyした時に複数の集計関数を同時に適用する

前の記事の続きです。
pandasでデータフレームをgroupbyした時に使える集計関数
ドキュメントのこの記事で参照した部分のすぐ下に、
Applying multiple functions at once
という段落があります。
実はこれ初めて知りました。
今までグルプごとに個数と、平均と、標準偏差を計算したい、みたいな時は、
groupbyして集計を個別に実施して、その結果をmergeするという非常に面倒なことをずっとやっていました。

それが、aggというのを使うと一発でできるようです。


import pandas as pd
from sklearn.datasets import load_iris

# データフレームの準備
iris = load_iris()
df = pd.DataFrame(iris.data, columns=iris.feature_names)
df["target"] = iris.target
df["target_name"] = df.target.apply(lambda x:iris.target_names[x])
del df["target"]

df.groupby("target_name").agg(["count", "mean", "std"])

出力されるのが次です。(ブログのレイアウトの都合上画像で貼り付けます。)

これは便利です。
また、DataFrameのカラム名が2段になっています。
これをみて、indexだけではなく実はcolumnsでも、MultiIndexが使えることを知りました。

pandasでデータフレームをgroupbyした時に使える集計関数

データの集計や分析をpandasで行う時、平均や合計を求めるために、
groupbyを使って集計することがよくあると思います。

非常に手軽に使え流のでなんとなく .sum()や .mean()と書いていたのですが、
そういえば他にどんな関数が使えるのか調べたことがなかったと思ったのでドキュメントを見てみました。
まずここ。
pandas.DataFrame.groupby
平均をとるサンプルコードがありますが求めていた関数の一覧がないですね。

よく読むと、See the user guide for more.とあります。
そのuser guideがこちらです。

Group By: split-apply-combine

この下の方に一覧がありました。

Function

Description

mean()

Compute mean of groups

sum()

Compute sum of group values

size()

Compute group sizes

count()

Compute count of group

std()

Standard deviation of groups

var()

Compute variance of groups

sem()

Standard error of the mean of groups

describe()

Generates descriptive statistics

first()

Compute first of group values

last()

Compute last of group values

nth()

Take nth value, or a subset if n is a list

min()

Compute min of group values

max()

Compute max of group values

グループ化した後に、describe()なんてできたんですね。
少し試してみたのですがこれ便利そうです。
他にもSeriesをスカラーに変換するlambda式なども使えるようです。