Comment fonctionne pandas Rolling objects?

Edit: j'ai condensé cette question étant donné qu'elle était probablement trop impliquée au départ. La viande de la question est en gras ci-dessous.

j'aimerais en savoir plus sur l'objet qui est réellement créé en utilisant DataFrame.rolling ou Series.rolling:

print(type(df.rolling))
<class 'pandas.core.window.Rolling'>

un peu de background: considérez l'alternative souvent utilisée avec np.as_strided. Cet extrait de code lui-même n'est pas important, mais son résultat est mon point de référence en demandant cette question.

def rwindows(a, window):
    if a.ndim == 1:
        a = a.reshape(-1, 1)
    shape = a.shape[0] - window + 1, window, a.shape[-1]
    strides = (a.strides[0],) + a.strides
    windows = np.lib.stride_tricks.as_strided(a, shape=shape, strides=strides)
    return np.squeeze(windows)

Ici rwindows va prendre un 1d ou 2d ndarray et construire des "blocs" mobiles égaux à la taille de fenêtre spécifiée (comme ci-dessous). Comment .rolling comparer l'objet avec le ndarray sortie ci-dessous? Est-il un itérateur, avec certains attributs stockés pour chaque bloc? Ou tout autre chose? J'ai essayé de jouer avec l'achèvement d'onglet sur l'objet avec des attributs / méthodes telles que __dict__ et _get_index() et ils ne me disent pas grand chose. J'ai aussi vu un _create_blocks méthode dans pandas -- est-ce que cela ressemble à la strided méthode?

# as_strided version

a = np.arange(5)
print(rwindows(a, 3))           # 1d input
[[0 1 2]
 [1 2 3]
 [2 3 4]]

b = np.arange(10).reshape(5,2)
print(rwindows(b, 4))           # 2d input
[[[0 1]
  [2 3]
  [4 5]
  [6 7]]

 [[2 3]
  [4 5]
  [6 7]
  [8 9]]]

Partie 2, crédit supplémentaire

en utilisant l'approche NumPy ci-dessus (mise en oeuvre de la SLO ici) est rendue nécessaire par le fait que func à l'intérieur de pandas.core.fenêtre.Roulant.appliquer

produire une seule valeur à partir d'une entrée ndarray * args et * * kwargs sont passé à la la fonction

Donc l'argument ne peut pas être un autre objet roulant. I. e.

def prod(a, b):
    return a * b
df.rolling(3).apply(prod, args=((df + 2).rolling(3),))
-----------------------------------------------------------------------
...
TypeError: unsupported operand type(s) for *: 'float' and 'Rolling'

C'est donc bien de là que vient ma question ci-dessus. Pourquoi la fonction passée doit-elle utiliser un NumPy array et produire une seule valeur scalaire, et qu'est-ce que cela a à voir avec la disposition d'un .rolling objet?

24
demandé sur denfromufa 2017-07-22 14:45:29

1 réponses

je vous suggère de regarder le code source afin d'obtenir dans le nitty gritty de ce roulement. En particulier, je vous suggère de jeter un oeil à la rolling fonctions generic.py et window.py. De là, vous pouvez jeter un oeil à l' Window classe qui est utilisé si vous spécifiez un type de fenêtre ou la valeur par défaut Rolling classe. Le dernier hérite de _Rolling_and_Expanding et en fin de compte _Rolling et _Window.

cela dit, je vais donner à mes deux cents: les Pandas de " l'ensemble du mécanisme de rotation, s'appuie sur la fonction numpy apply_along_axis. En particulier, il est utilisé ici à pandas. Il est utilisé en conjonction avec le windows.pyx cython module. Dans votre série, sort la fenêtre de roulement agrégé. Pour les fonctions d'agrégation typiques, il les gère efficacement pour vous, mais pour les fonctions personnalisées (en utilisant apply()), il utilise un roll_generic()windows.pyx.

la fonction de laminage dans pandas fonctionne indépendamment sur les colonnes de base de données pandas. Ce n'est pas un Python iterator, et est chargé paresseusement, ce qui signifie que rien n'est calculé jusqu'à ce que vous lui appliquiez une fonction d'agrégation. Les fonctions qui appliquent réellement la fenêtre mobile des données ne sont utilisées que juste avant qu'une agrégation soit faite.

une source de confusion pourrait être que vous pensez à l'objet roulant comme un dataframe. (Vous avez nommé les rolling objet df dans votre dernier extrait de code). Vraiment pas. C'est un objet qui peut produire des images de données en appliquant des agrégations sur la logique de fenêtre qu'il héberge.

Le lambda que vous fournissez est appliquée pour chaque cellule de votre nouveau dataframe. Il prend une fenêtre à l'envers (le long de chaque colonne) dans votre ancienne base de données, et il l'agrège à une seule cellule dans la nouvelle base de données. L'agrégation peut être des choses comme sum,mean, quelque chose que vous avez fait sur mesure, etc. par-dessus une fenêtre, disons 3. Voici quelques exemples:

a = np.arange(5)
df = pd.DataFrame(a, columns=['a'])
df.rolling(3).mean().dropna()

... qui peut aussi être fait par:

df.rolling(3).apply(np.mean).dropna()

... et produit:

     a
2  3.0
3  6.0
4  9.0

(La première colonne est la valeur de l'indice et peut être ignoré ici, et pour les exemples suivants.)

notez comment nous avons fourni une fonction existante d'agrégation de numpy. C'est l'idée. Nous sommes censés être en mesure de fournir tout ce que nous voulons tant qu'il est conforme à ce que fonctions d'agrégation faire, c'est à dire, prendre un vecteur de valeurs et de produire une valeur unique. Voici une autre où nous créons une fonction d'agrégation personnalisée, dans ce cas la norme L2 de la fenêtre:

df.rolling(3).apply(lambda x: np.sqrt(x.dot(x))).dropna()

si vous n'êtes pas familier avec les lambda fonctions c'est la même chose:

def euclidean_dist(x):
    return np.sqrt(x.dot(x))

df.rolling(3).apply(euclidean_dist).dropna()

... cédant:

          a
2  2.236068
3  3.741657
4  5.385165

Juste pour être sûr, nous pouvons vérifier manuellement que np.sqrt(0**2 + 1**2 + 2**2) en effet 2.236068.

[vos originaux De modifier, dans la dernière code snippet, votre code tombe probablement en panne plus tôt que prévu. Elle échoue avant l'invocation de df.apply(...) Vous essayez d'ajouter un roulement objet nommé df au numéro 2 avant de passer à df.apply(...). L'objet roulant n'est pas quelque chose sur lequel vous opérez. La fonction d'agrégation que vous avez fourni n'est pas conforme à une fonction d'agrégation en général. a une liste avec les valeurs d'une fenêtre, b serait un paramètre supplémentaire constant. Il peut être un objet roulant si vous voulez, mais il ne serait généralement pas quelque chose que vous aimeriez faire. Pour être plus clair, voici quelque chose qui est similaire à ce que vous faisiez dans votre édition originale mais fonctionne:

a = np.arange(8)
df = pd.DataFrame(a, columns=['a'])
n = 4
rol = df.rolling(n)

def prod(window_list, constant_rol):
    return window_list.dot(constant_rol.sum().dropna().head(n))

rol.apply(prod, args=(rol,)).dropna()

# [92.0, 140.0, 188.0, 236.0, 284.0]

c'est un exemple artificiel, mais je le montre pour faire le point que vous pouvez passer dans tout ce que vous voulez comme une constante, même l'objet roulant que vous utilisez lui-même. La partie dynamique est le premier argument a dans votre cas ou window_list dans mon cas. Tout les fenêtres définies, sous la forme de listes individuelles, sont passées dans cette fonction une à une.

D'après vos commentaires de suivi, c'est peut-être ce que vous cherchez:

import numpy as np
import pandas as pd

n = 3
a = np.arange(5)
df = pd.DataFrame(a, columns=['a'])

def keep(window, windows):
    windows.append(window.copy())
    return window[-1]

windows = list()
df['a'].rolling(n).apply(keep, args=(windows,))
df = df.tail(n)
df['a_window'] = windows

qui ajoute des matrices / vecteurs à chaque bloc roulant produisant ainsi:

   a         a_window
2  2  [0.0, 1.0, 2.0]
3  3  [1.0, 2.0, 3.0]
4  4  [2.0, 3.0, 4.0]

Notez que cela ne fonctionne que si vous le faites sur une colonne à la fois. Si vous voulez faire quelques calculs sur la fenêtre avant de le stocker dans keep c'est très bien aussi.

cela dit, sans plus de commentaires sur exactement ce que vous essayez d'atteindre, il est difficile de construire un exemple qui répond à vos besoins.

si votre but ultime est de créer une base de données de variables en retard, alors j'utiliserais des colonnes réelles en utilisant shift():

import numpy as np
import pandas as pd

a = np.arange(5)

df = pd.DataFrame(a, columns=['a'])
for i in range(1,3):
    df['a-%s' % i] = df['a'].shift(i)

df.dropna()

... donner:

   a  a-1  a-2
2  2  1.0  0.0
3  3  2.0  1.0
4  4  3.0  2.0

(il y a peut-être une autre façon plus belle de le faire, mais cela permet de faire le travail.)

concernant votre variable b dans votre premier extrait de code, rappelez-vous que les images de données dans les pandas ne sont généralement pas traitées comme des tenseurs de dimensions/objets arbitraires. Vous pouvez probablement y mettre tout ce que vous voulez, mais en fin de compte, les chaînes, les objets temporels, les ints et les floats sont ce que l'on attend. C'est peut-être pour cette raison que les concepteurs de pandas n'ont pas pris la peine de permettre l'agrégation mobile à des valeurs non scalaires. Il ne semble même pas qu'une simple chaîne de caractères soit autorisée comme sortie de la fonction d'agrégation.

en tout cas, j'espère que cette réponse question. Si non, laissez-moi savoir, et je vais essayer de vous aider dans les commentaires, ou une mise à jour.


note finale sur le _create_blocks() fonction des objets mobiles.

_create_blocks() gère la fonction reindexing et binning lorsque vous utilisez le freq argument de rolling.

Si vous utilisez freq avec, disons, des semaines, tels que freq=W:

import pandas as pd

a = np.arange(50)
df = pd.DataFrame(a, columns=['a'])
df.index = pd.to_datetime('2016-01-01') + pd.to_timedelta(df['a'], 'D')
blocks, obj, index = df.rolling(4, freq='W')._create_blocks(how=None)
for b in blocks:
    print(b)

... ensuite, nous obtenons les données originales binned (not rolling) semaine par semaine:

               a
a               
2016-01-03   2.0
2016-01-10   9.0
2016-01-17  16.0
2016-01-24  23.0
2016-01-31  30.0
2016-02-07  37.0
2016-02-14  44.0
2016-02-21   NaN

notez que ce n'est pas la sortie du roulement agrégé. C'est tout simplement la nouvelle blocs. Après cette. Nous faisons une agrégation comme sum et obtenir:

                a
a                
2016-01-03    NaN
2016-01-10    NaN
2016-01-17    NaN
2016-01-24   50.0
2016-01-31   78.0
2016-02-07  106.0
2016-02-14  134.0
2016-02-21    NaN

... qui est vérifié par une sommation d'essai: 50 = 2 + 9 + 16 + 23.

Si vous n'utilisez pas freq comme argument, il renvoie simplement la structure de données originale:

import pandas as pd
a = np.arange(5)
df = pd.DataFrame(a, columns=['a'])
blocks, obj, index = df.rolling(3)._create_blocks(how=None)

for b in blocks:
    print(b)

... qui produit ...

            a
a            
2016-01-01  0
2016-01-02  1
2016-01-03  2
2016-01-04  3
2016-01-05  4

... et est utilisé pour l'agrégation de fenêtres mobiles.

24
répondu André C. Andersen 2017-08-24 21:00:24