web-dev-qa-db-fra.com

Comment fonctionnent les pandas)?

Edit: J'ai condensé cette question étant donné qu'elle était probablement trop complexe pour commencer. La viande de la question est en gras ci-dessous.

J'aimerais en savoir plus sur l'objet créé lors de l'utilisation de DataFrame.rolling ou Series.rolling :

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

Un peu d’arrière-plan: considérez l’alternative souvent utilisée avec np.as_strided. Cet extrait de code n'est pas important en soi, mais son résultat est mon point de référence pour poser 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 prendra 1d ou 2d ndarray et construira des "blocs" roulants égaux à la taille de la fenêtre spécifiée (comme ci-dessous). Comment un objet .rolling Se compare-t-il à la sortie ndarray ci-dessous? S'agit-il d'un itérateur, avec certains attributs stockés pour chaque bloc? Ou quelque chose d'autre entièrement? J'ai essayé de jouer avec la complétion de tabulation sur l'objet avec des attributs/méthodes tels que __dict__ Et _get_index() et ils ne me disent pas grand chose. J'ai aussi vu une méthode _create_blocks dans les pandas - ressemble-t-elle à la méthode strided?

# 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

L'utilisation de l'approche NumPy ci-dessus (implémentation OLS ici ) est rendue nécessaire par le fait que func au sein de pandas.core.window.Rolling.apply must

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

Donc, l'argument ne peut pas être un autre objet roulant. C'est à dire.

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'

Donc, c’est d’où vient ma question ci-dessus. Pourquoi la fonction transmise doit-elle utiliser un tableau NumPy et générer une seule valeur scalaire, et en quoi cela a-t-il à voir avec la présentation d'un objet .rolling?

32
Brad Solomon

Je vous suggère de jeter un coup d'œil au code source pour entrer dans le vif du sujet. En particulier, je vous suggère de regarder les fonctions rolling dans generic.py et window.py . À partir de là, vous pouvez jeter un oeil sur Window class , utilisé si vous spécifiez un type de fenêtre ou la valeur par défaut Rolling class . Le dernier hérite de _Rolling_and_Expanding Et finalement _Rolling Et _Window.

Cela dit, je vais donner mes deux centimes: tout le mécanisme de roulement des pandas repose sur la fonction numpy apply_along_axis . En particulier, il est utilisé ici dans les pandas. Il est utilisé conjointement avec le module windows.pyx cython. Dans votre série, sort la fenêtre déroulante agrégée. 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() dans windows.pyx.

La fonction de roulement dans pandas opère sur pandas colonnes de trame de données indépendamment. Ce n'est pas un itérateur python) , et est chargé paresseux, cela 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 défilante des données ne sont utilisées que juste avant la réalisation de l'agrégation.

Une source de confusion peut être que vous envisagez l’objet roulant comme un cadre de données. (Vous avez nommé l'objet glissant df dans votre dernier extrait de code). Ce n'est vraiment pas. C'est un objet qui peut produire des trames 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é à chaque cellule de votre nouveau cadre de données. Il ouvre une fenêtre vers l'arrière (le long de chaque colonne) dans votre ancien cadre de données et l'agrège dans une seule cellule du nouveau cadre de données. L'agrégation peut comprendre des éléments tels que sum, mean, quelque chose de personnalisé que vous avez créé, etc., sur une taille de fenêtre donnée, par exemple 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'index et peut être ignorée ici et pour les exemples suivants.)

Remarquez comment nous avons fourni une fonction d'agrégation numpy existante. C'est l'idée. Nous sommes censés être en mesure de fournir tout ce que nous voulons tant que cela est conforme à ce que font les fonctions d’agrégation, c’est-à-dire prendre un vecteur de valeurs et en produire une seule valeur. 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 fonctions lambda, c’est la même chose que:

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) est bien 2.236068.

[Dans votre édition originale, dans] le dernier extrait de code, votre code échoue probablement plus tôt que prévu. Il échoue avant l'appel de df.apply(...). Vous essayez d'ajouter un objet roulant nommé df au nombre 2 avant de le transmettre à df.apply(...). L'objet roulant n'est pas quelque chose sur lequel vous effectuez des opérations. La fonction d'agrégation que vous avez fournie ne correspond pas non plus à une fonction d'agrégation en général. Le a est une liste contenant les valeurs d'une fenêtre. b est un paramètre supplémentaire constant que vous transmettez. Il peut s'agir d'un objet roulant si vous le souhaitez, mais ce n'est généralement pas le cas. quelque chose que vous aimeriez faire. Pour que ce soit plus clair, voici quelque chose qui ressemble à ce que vous faisiez dans votre montage original mais qui 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 comprendre que vous pouvez transmettre ce que vous voulez comme 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. Toutes les fenêtres définies, sous forme de listes individuelles, sont passées à cette fonction une par une.

En fonction de vos commentaires, cela pourrait être ce que vous recherchez:

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 tableaux/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 des calculs sur la fenêtre avant de les stocker dans keep c'est très bien aussi.

Cela dit, sans plus d'informations sur ce que vous essayez de réaliser, il est difficile de construire un exemple qui réponde à vos besoins.

Si votre objectif ultime est de créer une trame de données de variables en retard, utilisez plutôt 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()

... donnant:

   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 façon plus belle de le faire, mais le travail est fait.)

En ce qui concerne votre variable b dans votre premier extrait de code, rappelez-vous que les DataFrames dans pandas ne sont généralement pas traités comme des tenseurs de dimensions/objets arbitraires. Vous pouvez probablement y insérer ce que vous voulez. C'est finalement ce que l'on attend de chaînes, d'objets temporels, d'ints et de flotteurs, ce qui pourrait expliquer pourquoi les concepteurs de pandas ne se sont pas souciés de permettre l'agrégation par roulement à des valeurs non scalaires. Il ne semble même pas qu'une chaîne simple soit autorisée en sortie de la fonction d'agrégation.

Quoi qu'il en soit, j'espère que cela répond à certaines de vos questions. Si ce n'est pas le cas, laissez-moi savoir et je vais essayer de vous aider dans les commentaires ou une mise à jour.


Note finale sur la fonction _create_blocks() des objets roulants.

La fonction _create_blocks() gère la réindexation et le binning lorsque vous utilisez l'argument freq de rolling.

Si vous utilisez freq avec, disons, des semaines telles 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)

... puis nous obtenons les données originales regroupées (et non cumulables) semaine après 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é. Ce sont simplement les nouveaux blocs sur lesquels il fonctionne. Après ça. Nous faisons une agrégation comme sum et obtenons:

                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 vérifie avec une sommation de tests: 50 = 2 + 9 + 16 + 23.

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

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 roulantes.

36
André C. Andersen