ウィンドウ操作#

pandasには、ウィンドウ操作を実行するためのコンパクトなAPIセットが含まれています。ウィンドウ操作とは、値のスライディングパーティションに対して集計を実行する操作です。APIはgroupby APIと同様に機能し、SeriesDataFrameは必要なパラメーターでウィンドウメソッドを呼び出し、その後集計関数を呼び出します。

In [1]: s = pd.Series(range(5))

In [2]: s.rolling(window=2).sum()
Out[2]: 
0    NaN
1    1.0
2    3.0
3    5.0
4    7.0
dtype: float64

ウィンドウは、現在の観測値からウィンドウの長さを遡って構成されます。上記の計算結果は、データに対して以下のウィンドウ化されたパーティションの合計を取ることで導き出すことができます。

In [3]: for window in s.rolling(window=2):
   ...:     print(window)
   ...: 
0    0
dtype: int64
0    0
1    1
dtype: int64
1    1
2    2
dtype: int64
2    2
3    3
dtype: int64
3    3
4    4
dtype: int64

概要#

pandasは4種類のウィンドウ操作をサポートしています。

  1. ローリングウィンドウ: 値に対する一般的な固定または可変のスライディングウィンドウ。

  2. 重み付けウィンドウ: scipy.signalライブラリが提供する、重み付けされた非矩形ウィンドウ。

  3. 拡張ウィンドウ: 値に対する累積ウィンドウ。

  4. 指数加重ウィンドウ: 値に対する累積され、指数的に重み付けされたウィンドウ。

概念

メソッド

返されるオブジェクト

時間ベースのウィンドウをサポート

チェインされたgroupbyをサポート

テーブルメソッドをサポート

オンライン操作をサポート

ローリングウィンドウ

rolling

pandas.typing.api.Rolling

はい

はい

はい (バージョン1.3以降)

いいえ

重み付けウィンドウ

rolling

pandas.typing.api.Window

いいえ

いいえ

いいえ

いいえ

拡張ウィンドウ

expanding

pandas.typing.api.Expanding

いいえ

はい

はい (バージョン1.3以降)

いいえ

指数加重ウィンドウ

ewm

pandas.typing.api.ExponentialMovingWindow

いいえ

はい (バージョン1.2以降)

いいえ

はい (バージョン1.3以降)

上記のように、一部の操作では時間オフセットに基づいてウィンドウを指定できます。

In [4]: s = pd.Series(range(5), index=pd.date_range('2020-01-01', periods=5, freq='1D'))

In [5]: s.rolling(window='2D').sum()
Out[5]: 
2020-01-01    0.0
2020-01-02    1.0
2020-01-03    3.0
2020-01-04    5.0
2020-01-05    7.0
Freq: D, dtype: float64

さらに、一部のメソッドではgroupby操作をウィンドウ操作と連結させることができます。これにより、まず指定されたキーでデータをグループ化し、次にグループごとにウィンドウ操作を実行します。

In [6]: df = pd.DataFrame({'A': ['a', 'b', 'a', 'b', 'a'], 'B': range(5)})

In [7]: df.groupby('A').expanding().sum()
Out[7]: 
       B
A       
a 0  0.0
  2  2.0
  4  6.0
b 1  1.0
  3  4.0

ウィンドウ操作は現在、数値データ(整数および浮動小数点)のみをサポートしており、常にfloat64値を返します。

警告

一部のウィンドウ集計、meansumvarstdメソッドは、基となるウィンドウアルゴリズムが合計を累積するため、数値の不正確さに悩まされる可能性があります。値が\(1/np.finfo(np.double).eps\)の桁で異なる場合、切り捨てが発生します。注意すべき点は、大きな値がそれらの値を含まないウィンドウに影響を与える可能性があることです。Kahan summationが、可能な限り精度を維持するためにローリング合計の計算に使用されます。

バージョン 1.3.0 で追加。

一部のウィンドウ操作では、コンストラクターでmethod='table'オプションもサポートされています。これは、一度に単一の列または行ではなく、DataFrame全体に対してウィンドウ操作を実行します。これは、多数の列または行を持つDataFrame(対応するaxis引数を使用)の場合に有用なパフォーマンス上の利点を提供したり、ウィンドウ操作中に他の列を利用する機能を提供したりできます。method='table'オプションは、対応するメソッド呼び出しでengine='numba'が指定されている場合にのみ使用できます。

例えば、加重平均の計算は、重みの別々の列を指定することでapply()を使用して計算できます。

In [8]: def weighted_mean(x):
   ...:     arr = np.ones((1, x.shape[1]))
   ...:     arr[:, :2] = (x[:, :2] * x[:, 2]).sum(axis=0) / x[:, 2].sum()
   ...:     return arr
   ...: 

In [9]: df = pd.DataFrame([[1, 2, 0.6], [2, 3, 0.4], [3, 4, 0.2], [4, 5, 0.7]])

In [10]: df.rolling(2, method="table", min_periods=0).apply(weighted_mean, raw=True, engine="numba")  # noqa: E501
Out[10]: 
          0         1    2
0  1.000000  2.000000  1.0
1  1.800000  2.000000  1.0
2  3.333333  2.333333  1.0
3  1.555556  7.000000  1.0

バージョン1.3で追加されました。

一部のウィンドウ操作では、ウィンドウオブジェクトを構築した後にonlineメソッドもサポートされています。これは、新しいDataFrameまたはSeriesオブジェクトを渡して、新しい値でウィンドウ計算を継続できる新しいオブジェクト(つまりオンライン計算)を返します。

この新しいウィンドウオブジェクトのメソッドは、オンライン計算の初期状態を「準備」するために、まず集計メソッドを呼び出す必要があります。次に、新しいDataFrameまたはSeriesオブジェクトをupdate引数に渡すことで、ウィンドウ計算を継続できます。

In [11]: df = pd.DataFrame([[1, 2, 0.6], [2, 3, 0.4], [3, 4, 0.2], [4, 5, 0.7]])

In [12]: df.ewm(0.5).mean()
Out[12]: 
          0         1         2
0  1.000000  2.000000  0.600000
1  1.750000  2.750000  0.450000
2  2.615385  3.615385  0.276923
3  3.550000  4.550000  0.562500
In [13]: online_ewm = df.head(2).ewm(0.5).online()

In [14]: online_ewm.mean()
Out[14]: 
      0     1     2
0  1.00  2.00  0.60
1  1.75  2.75  0.45

In [15]: online_ewm.mean(update=df.tail(1))
Out[15]: 
          0         1         2
3  3.307692  4.307692  0.623077

すべてのウィンドウ操作は、ウィンドウが持つべき非np.nan値の最小量を決定するmin_periods引数をサポートしています。そうでない場合、結果の値はnp.nanです。min_periodsは時間ベースのウィンドウでは1に、固定ウィンドウではwindowにデフォルト設定されます。

In [16]: s = pd.Series([np.nan, 1, 2, np.nan, np.nan, 3])

In [17]: s.rolling(window=3, min_periods=1).sum()
Out[17]: 
0    NaN
1    1.0
2    3.0
3    3.0
4    2.0
5    3.0
dtype: float64

In [18]: s.rolling(window=3, min_periods=2).sum()
Out[18]: 
0    NaN
1    NaN
2    3.0
3    3.0
4    NaN
5    NaN
dtype: float64

# Equivalent to min_periods=3
In [19]: s.rolling(window=3, min_periods=None).sum()
Out[19]: 
0   NaN
1   NaN
2   NaN
3   NaN
4   NaN
5   NaN
dtype: float64

さらに、すべてのウィンドウ操作は、ウィンドウに適用された複数の集計の結果を返すaggregateメソッドをサポートしています。

In [20]: df = pd.DataFrame({"A": range(5), "B": range(10, 15)})

In [21]: df.expanding().agg(["sum", "mean", "std"])
Out[21]: 
      A                    B                
    sum mean       std   sum  mean       std
0   0.0  0.0       NaN  10.0  10.0       NaN
1   1.0  0.5  0.707107  21.0  10.5  0.707107
2   3.0  1.0  1.000000  33.0  11.0  1.000000
3   6.0  1.5  1.290994  46.0  11.5  1.290994
4  10.0  2.0  1.581139  60.0  12.0  1.581139

ローリングウィンドウ#

汎用ローリングウィンドウは、ウィンドウを固定された観測数またはオフセットに基づく可変観測数として指定することをサポートしています。時間ベースのオフセットが提供される場合、対応する時間ベースのインデックスは単調でなければなりません。

In [22]: times = ['2020-01-01', '2020-01-03', '2020-01-04', '2020-01-05', '2020-01-29']

In [23]: s = pd.Series(range(5), index=pd.DatetimeIndex(times))

In [24]: s
Out[24]: 
2020-01-01    0
2020-01-03    1
2020-01-04    2
2020-01-05    3
2020-01-29    4
dtype: int64

# Window with 2 observations
In [25]: s.rolling(window=2).sum()
Out[25]: 
2020-01-01    NaN
2020-01-03    1.0
2020-01-04    3.0
2020-01-05    5.0
2020-01-29    7.0
dtype: float64

# Window with 2 days worth of observations
In [26]: s.rolling(window='2D').sum()
Out[26]: 
2020-01-01    0.0
2020-01-03    1.0
2020-01-04    3.0
2020-01-05    5.0
2020-01-29    4.0
dtype: float64

サポートされているすべての集計関数については、ローリングウィンドウ関数を参照してください。

ウィンドウのセンタリング#

デフォルトでは、ラベルはウィンドウの右端に設定されますが、centerキーワードを使用すると、ラベルを中央に設定できます。

In [27]: s = pd.Series(range(10))

In [28]: s.rolling(window=5).mean()
Out[28]: 
0    NaN
1    NaN
2    NaN
3    NaN
4    2.0
5    3.0
6    4.0
7    5.0
8    6.0
9    7.0
dtype: float64

In [29]: s.rolling(window=5, center=True).mean()
Out[29]: 
0    NaN
1    NaN
2    2.0
3    3.0
4    4.0
5    5.0
6    6.0
7    7.0
8    NaN
9    NaN
dtype: float64

これは、日時のようなインデックスにも適用できます。

バージョン 1.3.0 で追加。

In [30]: df = pd.DataFrame(
   ....:     {"A": [0, 1, 2, 3, 4]}, index=pd.date_range("2020", periods=5, freq="1D")
   ....: )
   ....: 

In [31]: df
Out[31]: 
            A
2020-01-01  0
2020-01-02  1
2020-01-03  2
2020-01-04  3
2020-01-05  4

In [32]: df.rolling("2D", center=False).mean()
Out[32]: 
              A
2020-01-01  0.0
2020-01-02  0.5
2020-01-03  1.5
2020-01-04  2.5
2020-01-05  3.5

In [33]: df.rolling("2D", center=True).mean()
Out[33]: 
              A
2020-01-01  0.5
2020-01-02  1.5
2020-01-03  2.5
2020-01-04  3.5
2020-01-05  4.0

ローリングウィンドウの終点#

ローリングウィンドウ計算における区間終点の含みは、closedパラメータで指定できます。

動作

'right'

右端を閉じる

'left'

左端を閉じる

'both'

両端を閉じる

'neither'

両端を開く

例えば、右端を開くことは、現在の情報から過去の情報への汚染がないことを必要とする多くの問題で役立ちます。これにより、ローリングウィンドウは「その時点までの」統計を計算できますが、その時点自体は含まれません。

In [34]: df = pd.DataFrame(
   ....:     {"x": 1},
   ....:     index=[
   ....:         pd.Timestamp("20130101 09:00:01"),
   ....:         pd.Timestamp("20130101 09:00:02"),
   ....:         pd.Timestamp("20130101 09:00:03"),
   ....:         pd.Timestamp("20130101 09:00:04"),
   ....:         pd.Timestamp("20130101 09:00:06"),
   ....:     ],
   ....: )
   ....: 

In [35]: df["right"] = df.rolling("2s", closed="right").x.sum()  # default

In [36]: df["both"] = df.rolling("2s", closed="both").x.sum()

In [37]: df["left"] = df.rolling("2s", closed="left").x.sum()

In [38]: df["neither"] = df.rolling("2s", closed="neither").x.sum()

In [39]: df
Out[39]: 
                     x  right  both  left  neither
2013-01-01 09:00:01  1    1.0   1.0   NaN      NaN
2013-01-01 09:00:02  1    2.0   2.0   1.0      1.0
2013-01-01 09:00:03  1    2.0   3.0   2.0      1.0
2013-01-01 09:00:04  1    2.0   3.0   2.0      1.0
2013-01-01 09:00:06  1    1.0   2.0   1.0      NaN

カスタムウィンドウローリング#

rollingは、window引数として整数またはオフセットを受け入れるだけでなく、ユーザーがウィンドウ境界を計算するためのカスタムメソッドを定義できるBaseIndexerサブクラスも受け入れます。BaseIndexerサブクラスは、ウィンドウの開始インデックスと終了インデックスの2つの配列のタプルを返すget_window_boundsメソッドを定義する必要があります。さらに、num_valuesmin_periodscenterclosedstepは自動的にget_window_boundsに渡され、定義されたメソッドは常にこれらの引数を受け入れる必要があります。

たとえば、次のDataFrameがある場合

In [40]: use_expanding = [True, False, True, False, True]

In [41]: use_expanding
Out[41]: [True, False, True, False, True]

In [42]: df = pd.DataFrame({"values": range(5)})

In [43]: df
Out[43]: 
   values
0       0
1       1
2       2
3       3
4       4

そして、use_expandingTrueの場合に拡張ウィンドウを使用し、それ以外の場合はサイズ1のウィンドウを使用したい場合、以下のBaseIndexerサブクラスを作成できます。

In [44]: from pandas.api.indexers import BaseIndexer

In [45]: class CustomIndexer(BaseIndexer):
   ....:      def get_window_bounds(self, num_values, min_periods, center, closed, step):
   ....:          start = np.empty(num_values, dtype=np.int64)
   ....:          end = np.empty(num_values, dtype=np.int64)
   ....:          for i in range(num_values):
   ....:              if self.use_expanding[i]:
   ....:                  start[i] = 0
   ....:                  end[i] = i + 1
   ....:              else:
   ....:                  start[i] = i
   ....:                  end[i] = i + self.window_size
   ....:          return start, end
   ....: 

In [46]: indexer = CustomIndexer(window_size=1, use_expanding=use_expanding)

In [47]: df.rolling(indexer).sum()
Out[47]: 
   values
0     0.0
1     1.0
2     3.0
3     3.0
4    10.0

BaseIndexerサブクラスの他の例はこちらで確認できます。

これらの例の中で注目すべきサブクラスの1つは、BusinessDayのような非固定オフセットに対するローリング操作を可能にするVariableOffsetWindowIndexerです。

In [48]: from pandas.api.indexers import VariableOffsetWindowIndexer

In [49]: df = pd.DataFrame(range(10), index=pd.date_range("2020", periods=10))

In [50]: offset = pd.offsets.BDay(1)

In [51]: indexer = VariableOffsetWindowIndexer(index=df.index, offset=offset)

In [52]: df
Out[52]: 
            0
2020-01-01  0
2020-01-02  1
2020-01-03  2
2020-01-04  3
2020-01-05  4
2020-01-06  5
2020-01-07  6
2020-01-08  7
2020-01-09  8
2020-01-10  9

In [53]: df.rolling(indexer).sum()
Out[53]: 
               0
2020-01-01   0.0
2020-01-02   1.0
2020-01-03   2.0
2020-01-04   3.0
2020-01-05   7.0
2020-01-06  12.0
2020-01-07   6.0
2020-01-08   7.0
2020-01-09   8.0
2020-01-10   9.0

一部の問題では、将来の知識を分析に利用できます。たとえば、各データポイントが実験から読み取られた完全な時系列であり、タスクが基礎となる条件を抽出することである場合にこれが発生します。このような場合、先読みローリングウィンドウ計算を実行すると役立つことがあります。この目的のために、FixedForwardWindowIndexerクラスが利用可能です。このBaseIndexerサブクラスは、閉じた固定幅の先読みローリングウィンドウを実装しており、次のように使用できます。

In [54]: from pandas.api.indexers import FixedForwardWindowIndexer

In [55]: indexer = FixedForwardWindowIndexer(window_size=2)

In [56]: df.rolling(indexer, min_periods=1).sum()
Out[56]: 
               0
2020-01-01   1.0
2020-01-02   3.0
2020-01-03   5.0
2020-01-04   7.0
2020-01-05   9.0
2020-01-06  11.0
2020-01-07  13.0
2020-01-08  15.0
2020-01-09  17.0
2020-01-10   9.0

スライス、ローリング集計の適用、そして結果の反転を用いることでも、これを達成できます(以下の例を参照)。

In [57]: df = pd.DataFrame(
   ....:     data=[
   ....:         [pd.Timestamp("2018-01-01 00:00:00"), 100],
   ....:         [pd.Timestamp("2018-01-01 00:00:01"), 101],
   ....:         [pd.Timestamp("2018-01-01 00:00:03"), 103],
   ....:         [pd.Timestamp("2018-01-01 00:00:04"), 111],
   ....:     ],
   ....:     columns=["time", "value"],
   ....: ).set_index("time")
   ....: 

In [58]: df
Out[58]: 
                     value
time                      
2018-01-01 00:00:00    100
2018-01-01 00:00:01    101
2018-01-01 00:00:03    103
2018-01-01 00:00:04    111

In [59]: reversed_df = df[::-1].rolling("2s").sum()[::-1]

In [60]: reversed_df
Out[60]: 
                     value
time                      
2018-01-01 00:00:00  201.0
2018-01-01 00:00:01  101.0
2018-01-01 00:00:03  214.0
2018-01-01 00:00:04  111.0

ローリング適用#

apply()関数は追加のfunc引数を取り、一般的なローリング計算を実行します。func引数は、ndarray入力から単一の値を生成する単一の関数である必要があります。rawは、ウィンドウがSeriesオブジェクトとしてキャストされるか(raw=False)、ndarrayオブジェクトとしてキャストされるか(raw=True)を指定します。

In [61]: def mad(x):
   ....:     return np.fabs(x - x.mean()).mean()
   ....: 

In [62]: s = pd.Series(range(10))

In [63]: s.rolling(window=4).apply(mad, raw=True)
Out[63]: 
0    NaN
1    NaN
2    NaN
3    1.0
4    1.0
5    1.0
6    1.0
7    1.0
8    1.0
9    1.0
dtype: float64

Numbaエンジン#

さらに、apply()は、オプションの依存関係としてNumbaがインストールされている場合、それを活用できます。apply集計は、engine='numba'およびengine_kwargs引数を指定することでNumbaを使用して実行できます(rawTrueに設定する必要があります)。引数の一般的な使用法とパフォーマンスに関する考慮事項については、Numbaによるパフォーマンス向上を参照してください。

Numbaは、潜在的に2つのルーチンで適用されます。

  1. funcが標準のPython関数である場合、エンジンは渡された関数をJITします。funcが既にJITされた関数である場合、エンジンは関数を再度JITしません。

  2. エンジンは、apply関数が各ウィンドウに適用されるforループをJITします。

engine_kwargs引数は、numba.jitデコレーターに渡されるキーワード引数の辞書です。これらのキーワード引数は、渡された関数(標準のPython関数である場合)と、各ウィンドウに対するapply forループの両方に適用されます。

バージョン 1.3.0 で追加。

meanmedianmaxmin、およびsumも、engineおよびengine_kwargs引数をサポートしています。

二項ウィンドウ関数#

cov()corr()は、2つのSeries、またはDataFrame/Series、あるいはDataFrame/DataFrameの任意の組み合わせについて、移動ウィンドウ統計量を計算できます。それぞれのケースでの動作は次のとおりです。

  • 2つのSeries: ペアリングの統計量を計算します。

  • DataFrame/Series: DataFrameの各列と渡されたSeriesの統計量を計算し、DataFrameを返します。

  • DataFrame/DataFrame: デフォルトでは、一致する列名について統計量を計算し、DataFrameを返します。pairwise=Trueというキーワード引数が渡されると、各列のペアについて統計量を計算し、問題の日付が値となるMultiIndexを持つDataFrameを返します(次のセクションを参照)。

例えば

In [64]: df = pd.DataFrame(
   ....:     np.random.randn(10, 4),
   ....:     index=pd.date_range("2020-01-01", periods=10),
   ....:     columns=["A", "B", "C", "D"],
   ....: )
   ....: 

In [65]: df = df.cumsum()

In [66]: df2 = df[:4]

In [67]: df2.rolling(window=2).corr(df2["B"])
Out[67]: 
              A    B    C    D
2020-01-01  NaN  NaN  NaN  NaN
2020-01-02 -1.0  1.0 -1.0  1.0
2020-01-03  1.0  1.0  1.0 -1.0
2020-01-04 -1.0  1.0  1.0 -1.0

ローリングペアワイズ共分散と相関の計算#

金融データ分析やその他の分野では、時系列の集合に対して共分散行列や相関行列を計算することが一般的です。多くの場合、移動ウィンドウ共分散行列や相関行列にも関心があります。DataFrame入力の場合、pairwiseキーワード引数を渡すことでこれを行うことができ、これによりindexが問題の日付であるMultiIndexedなDataFrameが生成されます。単一のDataFrame引数の場合、pairwise引数は省略することもできます。

欠損値は無視され、各エントリはペアワイズの完全な観測値を使用して計算されます。

欠損データがランダムに欠損していると仮定すると、これにより共分散行列の推定値は不偏となります。しかし、多くのアプリケーションでは、この推定値は正の半定値であることが保証されないため、許容できない場合があります。これにより、推定された相関の絶対値が1を超える、および/または反転不能な共分散行列になる可能性があります。詳細については、共分散行列の推定を参照してください。

In [68]: covs = (
   ....:     df[["B", "C", "D"]]
   ....:     .rolling(window=4)
   ....:     .cov(df[["A", "B", "C"]], pairwise=True)
   ....: )
   ....: 

In [69]: covs
Out[69]: 
                     B         C         D
2020-01-01 A       NaN       NaN       NaN
           B       NaN       NaN       NaN
           C       NaN       NaN       NaN
2020-01-02 A       NaN       NaN       NaN
           B       NaN       NaN       NaN
...                ...       ...       ...
2020-01-09 B  0.342006  0.230190  0.052849
           C  0.230190  1.575251  0.082901
2020-01-10 A -0.333945  0.006871 -0.655514
           B  0.649711  0.430860  0.469271
           C  0.430860  0.829721  0.055300

[30 rows x 3 columns]

重み付けウィンドウ#

.rollingwin_type引数は、フィルタリングやスペクトル推定で一般的に使用される重み付きウィンドウを生成します。win_typescipy.signalウィンドウ関数に対応する文字列でなければなりません。これらのウィンドウを使用するにはScipyがインストールされている必要があり、Scipyウィンドウメソッドが取る補助引数は集計関数で指定する必要があります。

In [70]: s = pd.Series(range(10))

In [71]: s.rolling(window=5).mean()
Out[71]: 
0    NaN
1    NaN
2    NaN
3    NaN
4    2.0
5    3.0
6    4.0
7    5.0
8    6.0
9    7.0
dtype: float64

In [72]: s.rolling(window=5, win_type="triang").mean()
Out[72]: 
0    NaN
1    NaN
2    NaN
3    NaN
4    2.0
5    3.0
6    4.0
7    5.0
8    6.0
9    7.0
dtype: float64

# Supplementary Scipy arguments passed in the aggregation function
In [73]: s.rolling(window=5, win_type="gaussian").mean(std=0.1)
Out[73]: 
0    NaN
1    NaN
2    NaN
3    NaN
4    2.0
5    3.0
6    4.0
7    5.0
8    6.0
9    7.0
dtype: float64

サポートされているすべての集計関数については、重み付けウィンドウ関数を参照してください。

拡張ウィンドウ#

拡張ウィンドウは、その時点までに利用可能なすべてのデータを使用した集計統計量​​の値を生成します。これらの計算はローリング統計量の特殊なケースであるため、pandasでは以下の2つの呼び出しが同等になるように実装されています。

In [74]: df = pd.DataFrame(range(5))

In [75]: df.rolling(window=len(df), min_periods=1).mean()
Out[75]: 
     0
0  0.0
1  0.5
2  1.0
3  1.5
4  2.0

In [76]: df.expanding(min_periods=1).mean()
Out[76]: 
     0
0  0.0
1  0.5
2  1.0
3  1.5
4  2.0

サポートされているすべての集計関数については、拡張ウィンドウ関数を参照してください。

指数加重ウィンドウ#

指数加重ウィンドウは拡張ウィンドウに似ていますが、各以前の点が現在の点に対して指数関数的に重み付けされます。

一般的に、加重移動平均は次のように計算されます。

\[y_t = \frac{\sum_{i=0}^t w_i x_{t-i}}{\sum_{i=0}^t w_i},\]

ここで、\(x_t\)は入力、\(y_t\)は結果、\(w_i\)は重みです。

サポートされているすべての集計関数については、指数加重ウィンドウ関数を参照してください。

EW関数は、指数重みの2つのバリアントをサポートしています。デフォルトのadjust=Trueは、重み\(w_i = (1 - \alpha)^i\)を使用し、次のように計算します。

\[y_t = \frac{x_t + (1 - \alpha)x_{t-1} + (1 - \alpha)^2 x_{t-2} + ... + (1 - \alpha)^t x_{0}}{1 + (1 - \alpha) + (1 - \alpha)^2 + ... + (1 - \alpha)^t}\]

adjust=Falseが指定されている場合、移動平均は次のように計算されます。

\[\begin{split}y_0 &= x_0 \\ y_t &= (1 - \alpha) y_{t-1} + \alpha x_t,\end{split}\]

これは、重みを使用することと同等です。

\[\begin{split}w_i = \begin{cases} \alpha (1 - \alpha)^i & \text{if } i < t \\ (1 - \alpha)^i & \text{if } i = t. \end{cases}\end{split}\]

これらの式は、\(\alpha' = 1 - \alpha\)を使って書かれることもあります。例えば、

\[y_t = \alpha' y_{t-1} + (1 - \alpha') x_t.\]

上記2つのバリアントの違いは、有限の履歴を持つ系列を扱っていることに起因します。adjust=Trueの場合の無限履歴の系列を考えてみましょう。

\[y_t = \frac{x_t + (1 - \alpha)x_{t-1} + (1 - \alpha)^2 x_{t-2} + ...} {1 + (1 - \alpha) + (1 - \alpha)^2 + ...}\]

分母が初項1、公比\(1 - \alpha\)の等比級数であることに注目すると、次のようになります。

\[\begin{split}y_t &= \frac{x_t + (1 - \alpha)x_{t-1} + (1 - \alpha)^2 x_{t-2} + ...} {\frac{1}{1 - (1 - \alpha)}}\\ &= [x_t + (1 - \alpha)x_{t-1} + (1 - \alpha)^2 x_{t-2} + ...] \alpha \\ &= \alpha x_t + [(1-\alpha)x_{t-1} + (1 - \alpha)^2 x_{t-2} + ...]\alpha \\ &= \alpha x_t + (1 - \alpha)[x_{t-1} + (1 - \alpha) x_{t-2} + ...]\alpha\\ &= \alpha x_t + (1 - \alpha) y_{t-1}\end{split}\]

これは上記のadjust=Falseと同じ式であり、無限系列の場合の両バリアントの等価性を示しています。adjust=Falseの場合、\(y_0 = x_0\)および\(y_t = \alpha x_t + (1 - \alpha) y_{t-1}\)となります。したがって、\(x_0\)は通常の値ではなく、その時点までの無限系列の指数加重モーメントであるという仮定があります。

\(0 < \alpha \leq 1\)である必要があり、\(\alpha\)を直接渡すことも可能ですが、EWモーメントのスパン (span)重心 (center of mass, com)、または半減期 (half-life)のいずれかを考える方が簡単な場合が多いです。

\[\begin{split}\alpha = \begin{cases} \frac{2}{s + 1}, & \text{for span}\ s \geq 1\\ \frac{1}{1 + c}, & \text{for center of mass}\ c \geq 0\\ 1 - \exp^{\frac{\log 0.5}{h}}, & \text{for half-life}\ h > 0 \end{cases}\end{split}\]

EW関数には、スパン重心半減期アルファのうち、正確に1つを指定する必要があります。

  • スパンは、一般的に「N日指数移動平均」と呼ばれるものに対応します。

  • 重心はより物理的な解釈を持ち、スパンの観点から考えることができます: \(c = (s - 1) / 2\)

  • 半減期は、指数重みが半分に減少するまでの時間です。

  • アルファは、平滑化係数を直接指定します。

timesのシーケンスも指定する場合、観測値がその値の半分に減衰するのにかかる時間を指定するために、timedeltaに変換可能な単位でhalflifeを指定することもできます。

In [77]: df = pd.DataFrame({"B": [0, 1, 2, np.nan, 4]})

In [78]: df
Out[78]: 
     B
0  0.0
1  1.0
2  2.0
3  NaN
4  4.0

In [79]: times = ["2020-01-01", "2020-01-03", "2020-01-10", "2020-01-15", "2020-01-17"]

In [80]: df.ewm(halflife="4 days", times=pd.DatetimeIndex(times)).mean()
Out[80]: 
          B
0  0.000000
1  0.585786
2  1.523889
3  1.523889
4  3.233686

時間の入力ベクトルを用いた指数加重平均の計算には、以下の式が使用されます。

\[y_t = \frac{\sum_{i=0}^t 0.5^\frac{t_{t} - t_{i}}{\lambda} x_{t-i}}{\sum_{i=0}^t 0.5^\frac{t_{t} - t_{i}}{\lambda}},\]

ExponentialMovingWindowにはignore_na引数もあり、これは中間にある欠損値が重みの計算にどのように影響するかを決定します。ignore_na=False(デフォルト)の場合、重みは絶対位置に基づいて計算されるため、中間にある欠損値が結果に影響します。ignore_na=Trueの場合、重みは中間にある欠損値を無視して計算されます。例えば、adjust=Trueと仮定して、ignore_na=Falseの場合、3, NaN, 5の加重平均は次のように計算されます。

\[\frac{(1-\alpha)^2 \cdot 3 + 1 \cdot 5}{(1-\alpha)^2 + 1}.\]

一方、ignore_na=Trueの場合、加重平均は次のように計算されます。

\[\frac{(1-\alpha) \cdot 3 + 1 \cdot 5}{(1-\alpha) + 1}.\]

var()std()、およびcov()関数にはbias引数があり、結果にバイアスのある統計量を含めるか、バイアスのない統計量を含めるかを指定します。例えば、bias=Trueの場合、ewmvar(x)ewmvar(x) = ewma(x**2) - ewma(x)**2として計算されます。一方、bias=False(デフォルト)の場合、バイアスのある分散統計量はデバイアス係数でスケーリングされます。

\[\frac{\left(\sum_{i=0}^t w_i\right)^2}{\left(\sum_{i=0}^t w_i\right)^2 - \sum_{i=0}^t w_i^2}.\]

(\(w_i = 1\)の場合、これは通常の\(N / (N - 1)\)係数に帰着し、\(N = t + 1\)です。)詳細については、Wikipediaの加重標本分散を参照してください。