Pandas: média móvel por intervalo de tempo


87

Eu sou novo no Pandas ... Eu tenho um monte de dados de pesquisas; Quero calcular uma média móvel para obter uma estimativa para cada dia com base em uma janela de três dias. Pelo que entendi com essa pergunta , as funções rolling_ * calculam a janela com base em um número especificado de valores, e não em um intervalo de data e hora específico.

Existe uma função diferente que implementa essa funcionalidade? Ou estou preso a escrever o meu próprio?

EDITAR:

Dados de entrada de amostra:

polls_subset.tail(20)
Out[185]: 
            favorable  unfavorable  other

enddate                                  
2012-10-25       0.48         0.49   0.03
2012-10-25       0.51         0.48   0.02
2012-10-27       0.51         0.47   0.02
2012-10-26       0.56         0.40   0.04
2012-10-28       0.48         0.49   0.04
2012-10-28       0.46         0.46   0.09
2012-10-28       0.48         0.49   0.03
2012-10-28       0.49         0.48   0.03
2012-10-30       0.53         0.45   0.02
2012-11-01       0.49         0.49   0.03
2012-11-01       0.47         0.47   0.05
2012-11-01       0.51         0.45   0.04
2012-11-03       0.49         0.45   0.06
2012-11-04       0.53         0.39   0.00
2012-11-04       0.47         0.44   0.08
2012-11-04       0.49         0.48   0.03
2012-11-04       0.52         0.46   0.01
2012-11-04       0.50         0.47   0.03
2012-11-05       0.51         0.46   0.02
2012-11-07       0.51         0.41   0.00

A saída teria apenas uma linha para cada data.

EDIT x2: erro de digitação corrigido


2
Há um problema aberto no bug tracker do Pandas solicitando esta funcionalidade: github.com/pydata/pandas/issues/936 . A funcionalidade ainda não existe. As respostas a esta pergunta descrevem uma maneira de obter o efeito desejado, mas normalmente será bem lento em comparação com as rolling_*funções integradas.
BrenBarn,

Respostas:


75

Nesse ínterim, um recurso de janela de tempo foi adicionado. Veja este link .

In [1]: df = DataFrame({'B': range(5)})

In [2]: df.index = [Timestamp('20130101 09:00:00'),
   ...:             Timestamp('20130101 09:00:02'),
   ...:             Timestamp('20130101 09:00:03'),
   ...:             Timestamp('20130101 09:00:05'),
   ...:             Timestamp('20130101 09:00:06')]

In [3]: df
Out[3]: 
                     B
2013-01-01 09:00:00  0
2013-01-01 09:00:02  1
2013-01-01 09:00:03  2
2013-01-01 09:00:05  3
2013-01-01 09:00:06  4

In [4]: df.rolling(2, min_periods=1).sum()
Out[4]: 
                       B
2013-01-01 09:00:00  0.0
2013-01-01 09:00:02  1.0
2013-01-01 09:00:03  3.0
2013-01-01 09:00:05  5.0
2013-01-01 09:00:06  7.0

In [5]: df.rolling('2s', min_periods=1).sum()
Out[5]: 
                       B
2013-01-01 09:00:00  0.0
2013-01-01 09:00:02  1.0
2013-01-01 09:00:03  3.0
2013-01-01 09:00:05  3.0
2013-01-01 09:00:06  7.0

Esta deve ser a melhor resposta.
Ivan

6
A documentação para os argumentos de compensação (como '2s') rollingpode ser usada
Guilherme Salomé

2
E se houver várias colunas no dataframe; como especificamos colunas específicas?
Brain_overflowed

@Brain_overflowed definido como índice
jamfie

O min_period não parece confiável com este método. Para min_periods> 1, você pode obter NaNs onde não os espera devido à precisão do carimbo de data / hora / taxa de amostragem variável
Albert James Teddy

50

Que tal algo assim:

Primeiro refaça a amostra do quadro de dados em intervalos 1D. Isso leva a média dos valores para todos os dias duplicados. Use a fill_methodopção para preencher os valores de data ausentes. Em seguida, passe o quadro reamostrado pd.rolling_meancom uma janela de 3 e min_periods = 1:

pd.rolling_mean(df.resample("1D", fill_method="ffill"), window=3, min_periods=1)

            favorable  unfavorable     other
enddate
2012-10-25   0.495000     0.485000  0.025000
2012-10-26   0.527500     0.442500  0.032500
2012-10-27   0.521667     0.451667  0.028333
2012-10-28   0.515833     0.450000  0.035833
2012-10-29   0.488333     0.476667  0.038333
2012-10-30   0.495000     0.470000  0.038333
2012-10-31   0.512500     0.460000  0.029167
2012-11-01   0.516667     0.456667  0.026667
2012-11-02   0.503333     0.463333  0.033333
2012-11-03   0.490000     0.463333  0.046667
2012-11-04   0.494000     0.456000  0.043333
2012-11-05   0.500667     0.452667  0.036667
2012-11-06   0.507333     0.456000  0.023333
2012-11-07   0.510000     0.443333  0.013333

ATUALIZAÇÃO : como Ben aponta nos comentários, com o pandas 0.18.0 a sintaxe mudou . Com a nova sintaxe, isso seria:

df.resample("1d").sum().fillna(0).rolling(window=3, min_periods=1).mean()

desculpe, Pandas newb, o que exatamente ffill usa como regra para fornecer valores ausentes?
Anov

1
Existem algumas opções de preenchimento. ffillsignifica forward fill e simplesmente propaga o valor não ausente mais recente. Da mesma forma, bfillpara preenchimento reverso, faz o mesmo na ordem inversa.
Zelazny7,

9
Talvez eu esteja enganado aqui, mas você está ignorando várias leituras do mesmo dia (ao fazer o rolamento significa que você esperaria que duas leituras tivessem mais peso do que uma ...)
Andy Hayden

4
Ótima resposta. Apenas observando que no pandas 0.18.0 a sintaxe mudou . A nova sintaxe é:df.resample("1D").ffill(limit=0).rolling(window=3, min_periods=1).mean()
Ben

1
Para replicar os resultados da resposta original no pandas versão 0.18.1 Estou usando: df.resample("1d").mean().rolling(window=3, min_periods=1).mean()
JohnE

33

Eu só tinha a mesma pergunta, mas com pontos de dados com espaçamento irregular. Reamostrar não é realmente uma opção aqui. Então criei minha própria função. Talvez seja útil para outros também:

from pandas import Series, DataFrame
import pandas as pd
from datetime import datetime, timedelta
import numpy as np

def rolling_mean(data, window, min_periods=1, center=False):
    ''' Function that computes a rolling mean

    Parameters
    ----------
    data : DataFrame or Series
           If a DataFrame is passed, the rolling_mean is computed for all columns.
    window : int or string
             If int is passed, window is the number of observations used for calculating 
             the statistic, as defined by the function pd.rolling_mean()
             If a string is passed, it must be a frequency string, e.g. '90S'. This is
             internally converted into a DateOffset object, representing the window size.
    min_periods : int
                  Minimum number of observations in window required to have a value.

    Returns
    -------
    Series or DataFrame, if more than one column    
    '''
    def f(x):
        '''Function to apply that actually computes the rolling mean'''
        if center == False:
            dslice = col[x-pd.datetools.to_offset(window).delta+timedelta(0,0,1):x]
                # adding a microsecond because when slicing with labels start and endpoint
                # are inclusive
        else:
            dslice = col[x-pd.datetools.to_offset(window).delta/2+timedelta(0,0,1):
                         x+pd.datetools.to_offset(window).delta/2]
        if dslice.size < min_periods:
            return np.nan
        else:
            return dslice.mean()

    data = DataFrame(data.copy())
    dfout = DataFrame()
    if isinstance(window, int):
        dfout = pd.rolling_mean(data, window, min_periods=min_periods, center=center)
    elif isinstance(window, basestring):
        idx = Series(data.index.to_pydatetime(), index=data.index)
        for colname, col in data.iterkv():
            result = idx.apply(f)
            result.name = colname
            dfout = dfout.join(result, how='outer')
    if dfout.columns.size == 1:
        dfout = dfout.ix[:,0]
    return dfout


# Example
idx = [datetime(2011, 2, 7, 0, 0),
       datetime(2011, 2, 7, 0, 1),
       datetime(2011, 2, 7, 0, 1, 30),
       datetime(2011, 2, 7, 0, 2),
       datetime(2011, 2, 7, 0, 4),
       datetime(2011, 2, 7, 0, 5),
       datetime(2011, 2, 7, 0, 5, 10),
       datetime(2011, 2, 7, 0, 6),
       datetime(2011, 2, 7, 0, 8),
       datetime(2011, 2, 7, 0, 9)]
idx = pd.Index(idx)
vals = np.arange(len(idx)).astype(float)
s = Series(vals, index=idx)
rm = rolling_mean(s, window='2min')

Você poderia incluir as importações relevantes?
Bryce Drennan

Você pode fornecer um exemplo de dataframe de entrada que funcionaria se computasse uma janela deslizante de intervalo de tempo, obrigado
Joshlk

Adicionado um exemplo ao post original.
user2689410

5
O mesmo agora pode ser feito usandos.rolling('2min', min_periods=1).mean()
kampta

8

O código do user2689410 era exatamente o que eu precisava. Fornecendo minha versão (créditos para o usuário2689410), que é mais rápido devido ao cálculo da média de uma vez para linhas inteiras no DataFrame.

Espero que minhas convenções de sufixo sejam legíveis: _s: string, _i: int, _b: bool, _ser: Series e _df: DataFrame. Onde você encontrar vários sufixos, o tipo pode ser ambos.

import pandas as pd
from datetime import datetime, timedelta
import numpy as np

def time_offset_rolling_mean_df_ser(data_df_ser, window_i_s, min_periods_i=1, center_b=False):
    """ Function that computes a rolling mean

    Credit goes to user2689410 at http://stackoverflow.com/questions/15771472/pandas-rolling-mean-by-time-interval

    Parameters
    ----------
    data_df_ser : DataFrame or Series
         If a DataFrame is passed, the time_offset_rolling_mean_df_ser is computed for all columns.
    window_i_s : int or string
         If int is passed, window_i_s is the number of observations used for calculating
         the statistic, as defined by the function pd.time_offset_rolling_mean_df_ser()
         If a string is passed, it must be a frequency string, e.g. '90S'. This is
         internally converted into a DateOffset object, representing the window_i_s size.
    min_periods_i : int
         Minimum number of observations in window_i_s required to have a value.

    Returns
    -------
    Series or DataFrame, if more than one column

    >>> idx = [
    ...     datetime(2011, 2, 7, 0, 0),
    ...     datetime(2011, 2, 7, 0, 1),
    ...     datetime(2011, 2, 7, 0, 1, 30),
    ...     datetime(2011, 2, 7, 0, 2),
    ...     datetime(2011, 2, 7, 0, 4),
    ...     datetime(2011, 2, 7, 0, 5),
    ...     datetime(2011, 2, 7, 0, 5, 10),
    ...     datetime(2011, 2, 7, 0, 6),
    ...     datetime(2011, 2, 7, 0, 8),
    ...     datetime(2011, 2, 7, 0, 9)]
    >>> idx = pd.Index(idx)
    >>> vals = np.arange(len(idx)).astype(float)
    >>> ser = pd.Series(vals, index=idx)
    >>> df = pd.DataFrame({'s1':ser, 's2':ser+1})
    >>> time_offset_rolling_mean_df_ser(df, window_i_s='2min')
                          s1   s2
    2011-02-07 00:00:00  0.0  1.0
    2011-02-07 00:01:00  0.5  1.5
    2011-02-07 00:01:30  1.0  2.0
    2011-02-07 00:02:00  2.0  3.0
    2011-02-07 00:04:00  4.0  5.0
    2011-02-07 00:05:00  4.5  5.5
    2011-02-07 00:05:10  5.0  6.0
    2011-02-07 00:06:00  6.0  7.0
    2011-02-07 00:08:00  8.0  9.0
    2011-02-07 00:09:00  8.5  9.5
    """

    def calculate_mean_at_ts(ts):
        """Function (closure) to apply that actually computes the rolling mean"""
        if center_b == False:
            dslice_df_ser = data_df_ser[
                ts-pd.datetools.to_offset(window_i_s).delta+timedelta(0,0,1):
                ts
            ]
            # adding a microsecond because when slicing with labels start and endpoint
            # are inclusive
        else:
            dslice_df_ser = data_df_ser[
                ts-pd.datetools.to_offset(window_i_s).delta/2+timedelta(0,0,1):
                ts+pd.datetools.to_offset(window_i_s).delta/2
            ]
        if  (isinstance(dslice_df_ser, pd.DataFrame) and dslice_df_ser.shape[0] < min_periods_i) or \
            (isinstance(dslice_df_ser, pd.Series) and dslice_df_ser.size < min_periods_i):
            return dslice_df_ser.mean()*np.nan   # keeps number format and whether Series or DataFrame
        else:
            return dslice_df_ser.mean()

    if isinstance(window_i_s, int):
        mean_df_ser = pd.rolling_mean(data_df_ser, window=window_i_s, min_periods=min_periods_i, center=center_b)
    elif isinstance(window_i_s, basestring):
        idx_ser = pd.Series(data_df_ser.index.to_pydatetime(), index=data_df_ser.index)
        mean_df_ser = idx_ser.apply(calculate_mean_at_ts)

    return mean_df_ser

3

Este exemplo parece exigir uma média ponderada, conforme sugerido no comentário de @andyhayden. Por exemplo, há duas pesquisas em 25/10 e uma em 26/10 e 27/10. Se você simplesmente reamostrar e depois tirar a média, isso efetivamente dará o dobro de peso para as pesquisas em 26/10 e 27/10 em comparação com as de 25/10.

Para atribuir peso igual a cada pesquisa em vez de peso igual a cada dia , você poderia fazer algo como o seguinte.

>>> wt = df.resample('D',limit=5).count()

            favorable  unfavorable  other
enddate                                  
2012-10-25          2            2      2
2012-10-26          1            1      1
2012-10-27          1            1      1

>>> df2 = df.resample('D').mean()

            favorable  unfavorable  other
enddate                                  
2012-10-25      0.495        0.485  0.025
2012-10-26      0.560        0.400  0.040
2012-10-27      0.510        0.470  0.020

Isso fornece os ingredientes básicos para fazer uma média baseada em pesquisas em vez de uma média baseada em dias. Como antes, as pesquisas são em média em 25/10, mas o peso para 25/10 também é armazenado e é o dobro do peso em 26/10 ou 27/10 para refletir que duas pesquisas foram realizadas em 25/10.

>>> df3 = df2 * wt
>>> df3 = df3.rolling(3,min_periods=1).sum()
>>> wt3 = wt.rolling(3,min_periods=1).sum()

>>> df3 = df3 / wt3  

            favorable  unfavorable     other
enddate                                     
2012-10-25   0.495000     0.485000  0.025000
2012-10-26   0.516667     0.456667  0.030000
2012-10-27   0.515000     0.460000  0.027500
2012-10-28   0.496667     0.465000  0.041667
2012-10-29   0.484000     0.478000  0.042000
2012-10-30   0.488000     0.474000  0.042000
2012-10-31   0.530000     0.450000  0.020000
2012-11-01   0.500000     0.465000  0.035000
2012-11-02   0.490000     0.470000  0.040000
2012-11-03   0.490000     0.465000  0.045000
2012-11-04   0.500000     0.448333  0.035000
2012-11-05   0.501429     0.450000  0.032857
2012-11-06   0.503333     0.450000  0.028333
2012-11-07   0.510000     0.435000  0.010000

Observe que a média móvel para 27/10 é agora 0,51500 (ponderada por pesquisa) em vez de 52,1667 (ponderada por dia).

Observe também que houve alterações nas APIs para resamplee a rollingpartir da versão 0.18.0.

rolando (o que há de novo no pandas 0.18.0)

reamostrar (o que há de novo no pandas 0.18.0)


3

Para mantê-lo básico, usei um loop e algo assim para você começar (meu índice são datetimes):

import pandas as pd
import datetime as dt

#populate your dataframe: "df"
#...

df[df.index<(df.index[0]+dt.timedelta(hours=1))] #gives you a slice. you can then take .sum() .mean(), whatever

e então você pode executar funções nessa fatia. Você pode ver como adicionar um iterador para tornar o início da janela diferente do primeiro valor em seu índice de dataframes rolaria a janela (você também poderia usar uma regra> para o início, por exemplo).

Observe, isso pode ser menos eficiente para dados SUPER grandes ou incrementos muito pequenos, pois seu fatiamento pode se tornar mais árduo (funciona bem para mim para centenas de milhares de linhas de dados e várias colunas, embora para janelas por hora em algumas semanas)


2

Descobri que o código user2689410 quebrou quando tentei com window = '1M', pois o delta no mês útil gerou este erro:

AttributeError: 'MonthEnd' object has no attribute 'delta'

Eu adicionei a opção de passar diretamente um delta de tempo relativo, para que você possa fazer coisas semelhantes para períodos definidos pelo usuário.

Obrigado pelas dicas, aqui está minha tentativa - espero que seja útil.

def rolling_mean(data, window, min_periods=1, center=False):
""" Function that computes a rolling mean
Reference:
    http://stackoverflow.com/questions/15771472/pandas-rolling-mean-by-time-interval

Parameters
----------
data : DataFrame or Series
       If a DataFrame is passed, the rolling_mean is computed for all columns.
window : int, string, Timedelta or Relativedelta
         int - number of observations used for calculating the statistic,
               as defined by the function pd.rolling_mean()
         string - must be a frequency string, e.g. '90S'. This is
                  internally converted into a DateOffset object, and then
                  Timedelta representing the window size.
         Timedelta / Relativedelta - Can directly pass a timedeltas.
min_periods : int
              Minimum number of observations in window required to have a value.
center : bool
         Point around which to 'center' the slicing.

Returns
-------
Series or DataFrame, if more than one column
"""
def f(x, time_increment):
    """Function to apply that actually computes the rolling mean
    :param x:
    :return:
    """
    if not center:
        # adding a microsecond because when slicing with labels start
        # and endpoint are inclusive
        start_date = x - time_increment + timedelta(0, 0, 1)
        end_date = x
    else:
        start_date = x - time_increment/2 + timedelta(0, 0, 1)
        end_date = x + time_increment/2
    # Select the date index from the
    dslice = col[start_date:end_date]

    if dslice.size < min_periods:
        return np.nan
    else:
        return dslice.mean()

data = DataFrame(data.copy())
dfout = DataFrame()
if isinstance(window, int):
    dfout = pd.rolling_mean(data, window, min_periods=min_periods, center=center)

elif isinstance(window, basestring):
    time_delta = pd.datetools.to_offset(window).delta
    idx = Series(data.index.to_pydatetime(), index=data.index)
    for colname, col in data.iteritems():
        result = idx.apply(lambda x: f(x, time_delta))
        result.name = colname
        dfout = dfout.join(result, how='outer')

elif isinstance(window, (timedelta, relativedelta)):
    time_delta = window
    idx = Series(data.index.to_pydatetime(), index=data.index)
    for colname, col in data.iteritems():
        result = idx.apply(lambda x: f(x, time_delta))
        result.name = colname
        dfout = dfout.join(result, how='outer')

if dfout.columns.size == 1:
    dfout = dfout.ix[:, 0]
return dfout

E o exemplo com uma janela de tempo de 3 dias para calcular a média:

from pandas import Series, DataFrame
import pandas as pd
from datetime import datetime, timedelta
import numpy as np
from dateutil.relativedelta import relativedelta

idx = [datetime(2011, 2, 7, 0, 0),
           datetime(2011, 2, 7, 0, 1),
           datetime(2011, 2, 8, 0, 1, 30),
           datetime(2011, 2, 9, 0, 2),
           datetime(2011, 2, 10, 0, 4),
           datetime(2011, 2, 11, 0, 5),
           datetime(2011, 2, 12, 0, 5, 10),
           datetime(2011, 2, 12, 0, 6),
           datetime(2011, 2, 13, 0, 8),
           datetime(2011, 2, 14, 0, 9)]
idx = pd.Index(idx)
vals = np.arange(len(idx)).astype(float)
s = Series(vals, index=idx)
# Now try by passing the 3 days as a relative time delta directly.
rm = rolling_mean(s, window=relativedelta(days=3))
>>> rm
Out[2]: 
2011-02-07 00:00:00    0.0
2011-02-07 00:01:00    0.5
2011-02-08 00:01:30    1.0
2011-02-09 00:02:00    1.5
2011-02-10 00:04:00    3.0
2011-02-11 00:05:00    4.0
2011-02-12 00:05:10    5.0
2011-02-12 00:06:00    5.5
2011-02-13 00:08:00    6.5
2011-02-14 00:09:00    7.5
Name: 0, dtype: float64

0

Verifique se o seu índice realmente datetimenão str pode ser útil:

data.index = pd.to_datetime(data['Index']).values
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.