web-dev-qa-db-fra.com

Comptage de valeurs positives consécutives dans un tableau Python

J'essaie de compter les jours consécutifs dans les données de retour sur actions. Ainsi, si un jour positif est 1 et un négatif est 0, une liste y=[0,0,1,1,1,0,0,1,0,1,1] devrait renvoyer z=[0,0,1,2,3,0,0,1,0,1,2].

Je suis arrivé à une solution qui est soignée en termes de nombre de lignes de code, mais est très lent:

import pandas
y=pandas.Series([0,0,1,1,1,0,0,1,0,1,1])
def f(x):
    return reduce(lambda a,b:reduce((a+b)*b,x)
z=pandas.expanding_apply(y,f)

J'imagine que je suis en train de parcourir la liste entière y trop de fois. Existe-t-il un moyen agréable pour Pythonic de réaliser ce que je veux tout en ne parcourant les données qu'une seule fois? Je pourrais écrire une boucle moi-même mais je me demandais s'il y avait un meilleur moyen.

Merci!

16
alex314159

pourquoi l'obsession de la manière ultra-pythonique de faire les choses? lisibilité + efficacité l'emporte sur le "style leet hackerz".

Je ferais juste comme ça:

a = [0,0,1,1,1,0,0,1,0,1,1]
b = [0,0,0,0,0,0,0,0,0,0,0]

for i in range(len(a)):
    if a[i] == 1:
        b[i] = b[i-1] + 1
    else:
        b[i] = 0
1
Coding Orange

Cela peut sembler un peu magique, mais utilise en réalité quelques idiomes communs: comme pandas n’a pas encore de support natif Nice pour une groupby contiguë, vous avez souvent besoin de quelque chose comme ça.

>>> y * (y.groupby((y != y.shift()).cumsum()).cumcount() + 1)
0     0
1     0
2     1
3     2
4     3
5     0
6     0
7     1
8     0
9     1
10    2
dtype: int64

Quelques explications: premièrement, nous comparons y à une version décalée de lui-même pour déterminer quand les groupes contigus commencent:

>>> y != y.shift()
0      True
1     False
2      True
3     False
4     False
5      True
6     False
7      True
8      True
9      True
10    False
dtype: bool

Ensuite (puisque False == 0 et True == 1), nous pouvons appliquer une somme cumulative pour obtenir un nombre pour les groupes:

>>> (y != y.shift()).cumsum()
0     1
1     1
2     2
3     2
4     2
5     3
6     3
7     4
8     5
9     6
10    6
dtype: int32

Nous pouvons utiliser groupby et cumcount pour nous obtenir un nombre entier comptant dans chaque groupe:

>>> y.groupby((y != y.shift()).cumsum()).cumcount()
0     0
1     1
2     0
3     1
4     2
5     0
6     1
7     0
8     0
9     0
10    1
dtype: int64

Ajoute un:

>>> y.groupby((y != y.shift()).cumsum()).cumcount() + 1
0     1
1     2
2     1
3     2
4     3
5     1
6     2
7     1
8     1
9     1
10    2
dtype: int64

Et enfin zéro les valeurs où nous avions zéro pour commencer:

>>> y * (y.groupby((y != y.shift()).cumsum()).cumcount() + 1)
0     0
1     0
2     1
3     2
4     3
5     0
6     0
7     1
8     0
9     1
10    2
dtype: int64
66
DSM

Si quelque chose est clair, c'est "Pythonic". Franchement, je ne peux même pas faire fonctionner votre solution originale. De plus, si cela fonctionne, je suis curieux de savoir si c'est plus rapide qu'une boucle. Avez-vous comparé?

Maintenant, depuis que nous avons commencé à discuter d’efficacité, voici quelques idées. 

Les boucles en Python sont intrinsèquement lentes, peu importe ce que vous faites. Bien sûr, si vous utilisez des pandas, vous utilisez également Numpy dessous, avec tous les avantages en termes de performances. Juste ne les détruisez pas en boucle. Cela ne veut pas dire que les listes Python utilisent beaucoup plus de mémoire que vous ne le pensez; potentiellement beaucoup plus que 8 bytes * length, car chaque entier peut être encapsulé dans un objet séparé et placé dans une zone distincte en mémoire, et pointé par un pointeur de la liste.

La vectorisation fournie par numpy devrait suffire SI vous pouvez trouver un moyen d’exprimer cette fonction sans effectuer de boucle. En fait, je me demande s’il existe un moyen de le représenter en utilisant des expressions telles que A+B*C. Si vous pouvez construire cette fonction à partir de Lapack , vous pouvez même potentiellement battre le code C++ ordinaire compilé avec optimisation.

Vous pouvez également utiliser l'une des approches compilées pour accélérer vos boucles. Voir une solution avec Numba sur les tableaux numpy ci-dessous. Une autre option consiste à utiliser PyPy , bien que vous ne puissiez probablement pas le combiner correctement avec des pandas. 

In [140]: import pandas as pd
In [141]: import numpy as np
In [143]: a=np.random.randint(2,size=1000000)

# Try the simple approach
In [147]: def simple(L):
              for i in range(len(L)):
                  if L[i]==1:
                      L[i] += L[i-1]


In [148]: %time simple(L)
CPU times: user 255 ms, sys: 20.8 ms, total: 275 ms
Wall time: 248 ms


# Just-In-Time compilation
In[149]: from numba import jit
@jit          
def faster(z):
    prev=0
    for i in range(len(z)):
        cur=z[i]
        if cur==0:
             prev=0
        else:
             prev=prev+cur
             z[i]=prev

In [151]: %time faster(a)
CPU times: user 51.9 ms, sys: 1.12 ms, total: 53 ms
Wall time: 51.9 ms


In [159]: list(L)==list(a)
Out[159]: True

En fait, dans le deuxième exemple ci-dessus, la plupart du temps a été consacrée à la compilation Just-In-Time. Au lieu de cela (rappelez-vous de copier, car la fonction change le tableau).

b=a.copy()
In [38]: %time faster(b)
CPU times: user 55.1 ms, sys: 1.56 ms, total: 56.7 ms
Wall time: 56.3 ms

In [39]: %time faster(c)
CPU times: user 10.8 ms, sys: 42 µs, total: 10.9 ms
Wall time: 10.9 ms

Donc, pour les appels suivants, nous avons un 25x-speedup par rapport à la version simple. Je vous suggère de lire Python Haute Performance si vous voulez en savoir plus.

5
osa

Garder les choses simples, en utilisant un tableau, une boucle et une conditionnelle.

a = [0,0,1,1,1,0,0,1,0,1,1]

for i in range(1, len(a)):
    if a[i] == 1:
        a[i] += a[i - 1]
1
Dan