3
votes

Puis-je effectuer des cumuls dynamiques de lignes dans les pandas?

Si j'ai le dataframe suivant, dérivé comme ceci: df = pd.DataFrame (np.random.randint (0, 10, size = (10, 1)))

   0
2  10
6  8

Existe-t-il un moyen efficace de cumuler les lignes avec une limite et à chaque fois que cette limite est atteinte, pour démarrer un nouveau cum . Une fois que chaque limite est atteinte (quel que soit le nombre de lignes), une ligne est créée avec le cumulé total.

Ci-dessous, j'ai créé un exemple de fonction qui fait cela, mais c'est très lent, surtout lorsque le dataframe devient très grand. Je n'aime pas que ma fonction soit en boucle et je cherche un moyen de la rendre plus rapide (je suppose un moyen sans boucle).

def foo(df, max_value):
    last_value = 0
    storage = []
    for index, row in df.iterrows():
        this_value = np.nansum([row[0], last_value])
        if this_value >= max_value:
            storage.append((index, this_value))
            this_value = 0
        last_value = this_value
    return storage

Si vous rhumez mon fonction comme ceci: foo (df, 5) Dans le contexte ci-dessus, il renvoie:

    0
0   0
1   2
2   8
3   1
4   0
5   0
6   7
7   0
8   2
9   2


2 commentaires

Le résultat attendu est-il 10, 8, 4? De plus, quelle est la particularité de l’indice?


Ouais 10, 8, 4 seraient un meilleur résultat que mes 10, 8. L'index devrait être basé sur la dernière valeur. car dans le cas 10, 8, 4 il devrait être respectivement 2, 6, 9. Le goulot d'étranglement que j'ai ici est la vitesse: /


3 Réponses :


5
votes

Une boucle n'est pas forcément mauvaise. L'astuce consiste à s'assurer qu'il est exécuté sur des objets de bas niveau. Dans ce cas, vous pouvez utiliser Numba ou Cython. Par exemple, en utilisant un générateur avec numba.njit:

import pandas as pd, numpy as np
from numba import njit

df = pd.DataFrame({0: [0, 2, 8, 1, 0, 0, 7, 0, 2, 2]})

@njit
def cumsum_limit_nb(A, limit=5):
    count = 0
    for i in range(A.shape[0]):
        count += A[i]
        if count > limit:
            yield i, count
            count = 0

def cumsum_limit(A, limit=5):
    count = 0
    for i in range(A.shape[0]):
        count += A[i]
        if count > limit:
            yield i, count
            count = 0

n = 10**4
df = pd.concat([df]*n, ignore_index=True)

%timeit list(cumsum_limit_nb(df[0].values))  # 4.19 ms ± 90.4 µs per loop
%timeit list(cumsum_limit(df[0].values))     # 58.3 ms ± 194 µs per loop

Pour démontrer les avantages en termes de performances de la compilation JIT avec Numba:

from numba import njit

@njit
def cumsum_limit(A, limit=5):
    count = 0
    for i in range(A.shape[0]):
        count += A[i]
        if count > limit:
            yield i, count
            count = 0

idx, vals = zip(*cumsum_limit(df[0].values))
res = pd.Series(vals, index=idx)


0 commentaires

7
votes

La boucle ne peut être évitée, mais elle peut être parallélisée en utilisant njit de numba :

perfplot.show(
    setup=lambda n: pd.DataFrame(np.random.randint(0, 10, size=(n, 1))),
    kernels=[
        lambda df: list(cumsum_limit_nb(df.iloc[:, 0].values, 5)),
        lambda df: dynamic_cumsum2(df.iloc[:, 0].values, 5)
    ],
    labels=['cumsum_limit_nb', 'dynamic_cumsum2'],
    n_range=[2**k for k in range(0, 17)],
    xlabel='N',
    logx=True,
    logy=True,
    equality_check=None # TODO - update when @jpp adds in the final `yield`
)

L'index est requis ici , en supposant que votre index n'augmente pas de manière numérique / monotone.

%timeit foo(df, 5)
1.23 ms ± 30.6 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

%timeit dynamic_cumsum2(df.iloc(axis=1)[0].values, 5)
71.4 µs ± 1.4 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)

Si l'index est de type Int64Index , vous pouvez le raccourcir en:

@njit
def dynamic_cumsum2(seq, max_value):
    cumsum = []
    running = 0
    for i in prange(len(seq)):
        if running > max_value:
            cumsum.append([i, running])
            running = 0
        running += seq[i] 
    cumsum.append([i, running])

    return cumsum

lst = dynamic_cumsum2(df.iloc(axis=1)[0].values, 5)
pd.DataFrame(lst, columns=['A', 'B']).set_index('A')

    B
A    
3  10
7   8
9   4

%timeit foo(df, 5)
1.24 ms ± 41.4 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

%timeit dynamic_cumsum(df.iloc(axis=1)[0].values, df.index.values, 5)
77.2 µs ± 4.01 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)

njit Performances des fonctions

from numba import njit, prange

@njit
def dynamic_cumsum(seq, index, max_value):
    cumsum = []
    running = 0
    for i in prange(len(seq)):
        if running > max_value:
            cumsum.append([index[i], running])
            running = 0
        running += seq[i] 
    cumsum.append([index[-1], running])

    return cumsum

Le graphique log-log montre que la fonction de générateur est plus rapide pour des entrées plus importantes:

 entrez la description de l'image ici

Une explication possible est que, à mesure que N augmente, la surcharge de l'ajout à une liste croissante dans dynamic_cumsum2 devient proéminente. Alors que cumsum_limit_nb n'a plus qu'à yield.


13 commentaires

Par intérêt, quels horaires voyez-vous pour des dataframes plus grands ... pour les données dans ma réponse, je vois le générateur 3x plus vite. Je suis en fait surpris de cela (agréablement), car je pensais que les générateurs avaient une surcharge importante.


@jpp Depuis que vous l'avez demandé, donnez-moi un peu et je verrai si je peux générer des timeits en utilisant perfplot. Je ne discuterais pas d'une manière ou d'une autre en termes de performances, les deux solutions sont très chouettes et franchement votre solution de générateur est agréable à mes yeux.


Pas de problème, pas besoin de perfplot si c'est trop de problèmes, je suis juste généralement intéressé par comment / si numba peut optimiser un générateur!


@jpp Pas du tout un problème. Dans le processus d'exécution des timeits, je remarque que votre fonction manque une déclaration de rendement finale pour le sperme le plus récemment accumulé.


Article super utile (tous les deux)! Ce type de calcul revient de temps en temps ici, il est donc bon de savoir comment le gérer efficacement: D


Quel type sont les entrées seq et index , car tout ce que j'essaye d'exécuter dans les fonctions renvoie des erreurs: /


@Newskooler est toujours des tableaux ou des listes. Ne fonctionnera pas autrement avec numba. Vous pouvez appeler .values ​​sur un objet Series pour accéder au tableau numpy sous-jacent.


Je ne recommanderais pas de paralléliser une boucle qui n'est pas pure. Dans ce cas, la variable running le rend impur. Il y a 4 résultats possibles: (1) numba décide qu'il ne peut pas le paralléliser et traite simplement la boucle comme s'il s'agissait de range au lieu de prange (2) il peut lever le variable en dehors de la boucle et utiliser la parallélisation sur le reste (3) numba insère incorrectement la synchronisation entre les exécutions parallèles et le résultat peut être faux (4) numba insère les synchronisations nécessaires autour de running ce qui peut imposer plus de frais généraux que vous gagnez en le parallélisant en premier lieu


Notez que le point est muet si vous savez réellement quelle approche numba a utilisé et déterminé qu'elle sera parallèle et correcte et que l'exécution parallèle est plus rapide qu'une non parallèle.


Bien sûr, les variables running et cumsum rendent la boucle "impure", pas seulement la variable running comme indiqué dans le commentaire précédent.


@MSeifert cela peut sembler une question idiote, mais comment puis-je déterminer laquelle des 4 choses qu'il a faites et l'améliorer? J'aimerais vraiment devenir meilleur avec numba!


@coldspeed Pas du tout une question stupide - je dois admettre que je ne sais pas quel résultat cela produit. Mais j'ai créé une question auto-répondue avec les choses que je sais (pas beaucoup, surtout des suppositions) sur ce sujet ( stackoverflow.com/questions/ 54583152 ). Peut-être que quelqu'un d'autre y ajoutera une réponse plus précise :)


@MSeifert C'est génial! Heureusement que je peux voter deux fois. Passera du temps à tout digérer. Merci beaucoup!



0
votes

approche plus simple:

x=dynamic_cumsum(df[0].values,5)
x
>>[[2, 10], [6, 8], [9, 4]]

résultat:

def dynamic_cumsum(seq,limit):
    res=[]
    cs=seq.cumsum()
    for i, e in enumerate(cs):
        if cs[i] >limit:
            res.append([i,e])
            cs[i+1:] -= e
    if res[-1][0]==i:
        return res
    res.append([i,e])
    return res


0 commentaires