web-dev-qa-db-fra.com

Calculer la somme cumulée d'une liste jusqu'à ce qu'un zéro apparaisse

J'ai une (longue) liste dans laquelle les zéros et les uns apparaissent au hasard:

list_a = [1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1]

Je veux obtenir la liste_b

  • somme de la liste jusqu'à l'endroit où 0 apparaît
  • où 0 apparaît, conservez 0 dans la liste

    list_b = [1, 2, 3, 0, 1, 2, 0, 1, 0, 1, 2, 3]
    

Je peux implémenter cela comme suit:

list_b = []
for i, x in enumerate(list_a):
    if x == 0:
        list_b.append(x)
    else:
        sum_value = 0
        for j in list_a[i::-1]:
            if j != 0:
                sum_value += j
            else:
                break
        list_b.append(sum_value)
print(list_b)

mais la longueur réelle de la liste est très longue.

Donc, je veux améliorer le code pour la haute vitesse. (s'il n'est pas lisible)

Je change le code comme ceci:

from itertools import takewhile
list_c = [sum(takewhile(lambda x: x != 0, list_a[i::-1])) for i, d in enumerate(list_a)]
print(list_c)

Mais ce n'est pas assez rapide. Comment puis-je le faire de manière plus efficace?

22
Hiroyuki Taniichi

Vous y pensez trop.

Option 1
Vous pouvez simplement parcourir les indices et les mettre à jour en conséquence (en calculant la somme cumulée), selon que la valeur actuelle est 0 Ou non.

data = [1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1]

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

Autrement dit, si l'élément actuel est différent de zéro, mettez à jour l'élément à l'index actuel en tant que somme de la valeur actuelle, plus la valeur de l'index précédent.

print(data)
[1, 2, 3, 0, 1, 2, 0, 1, 0, 1, 2, 3]

Notez que cela met à jour votre liste en place. Vous pouvez créer une copie à l'avance si vous ne voulez pas que - new_data = data.copy() et itérer sur new_data De la même manière.


Option 2
Vous pouvez utiliser l'API pandas si vous avez besoin de performances. Recherchez des groupes en fonction de l'emplacement des 0 S, et utilisez groupby + cumsum pour calculer les sommes cumulées par groupe, comme ci-dessus:

import pandas as pd

s = pd.Series(data)    
data = s.groupby(s.eq(0).cumsum()).cumsum().tolist()
print(data)
[1, 2, 3, 0, 1, 2, 0, 1, 0, 1, 2, 3]

Performance

Tout d'abord, la configuration -

data = data * 100000
s = pd.Series(data)

Prochain,

%%timeit
new_data = data.copy()
for i in range(1, len(data)):
    if new_data[i]:  
        new_data[i] += new_data[i - 1]

328 ms ± 4.09 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

Et, en chronométrant la copie séparément,

%timeit data.copy()
8.49 ms ± 17.2 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

Donc, la copie ne prend pas vraiment beaucoup de temps. Finalement,

%timeit s.groupby(s.eq(0).cumsum()).cumsum().tolist()
122 ms ± 1.69 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

L'approche pandas est conceptuellement linéaire (tout comme les autres approches) mais plus rapide à un degré constant en raison de l'implémentation de la bibliothèque.

35
cs95

Si vous voulez une solution native compacte Python qui est probablement la plus efficace en mémoire, mais pas la plus rapide (voir les commentaires), vous pouvez tirer largement de itertools:

>>> from itertools import groupby, accumulate, chain
>>> list(chain.from_iterable(accumulate(g) for _, g in groupby(list_a, bool)))
[1, 2, 3, 0, 1, 2, 0, 1, 0, 1, 2, 3]

Les étapes sont les suivantes: regrouper la liste en sous-listes en fonction de la présence de 0 (Ce qui est faux), prendre la somme cumulée des valeurs dans chaque sous-liste, aplatir les sous-listes.

Comme Stefan Pochmann commentaires, si votre liste est binaire dans le contenu (comme ne comprenant que 1 S et 0 S uniquement) alors vous n'avez pas besoin de passer un clé à groupby() du tout et il retombera sur la fonction d'identité. C'est ~ 30% plus rapide que l'utilisation de bool dans ce cas:

>>> list(chain.from_iterable(accumulate(g) for _, g in groupby(list_a)))
[1, 2, 3, 0, 1, 2, 0, 1, 0, 1, 2, 3]
16
Chris_Rands

Personnellement, je préférerais un générateur simple comme celui-ci:

def gen(lst):
    cumulative = 0
    for item in lst:
        if item:
            cumulative += item
        else:
            cumulative = 0
        yield cumulative

Rien de magique (quand on sait comment yield fonctionne), facile à lire et devrait être plutôt rapide.

Si vous avez besoin de plus de performances, vous pouvez même en faire un type d'extension Cython (j'utilise IPython ici). Vous perdez ainsi la partie "facile à comprendre" et cela nécessite des "dépendances importantes":

%load_ext cython

%%cython

cdef class Cumulative(object):
    cdef object it
    cdef object cumulative
    def __init__(self, it):
        self.it = iter(it)
        self.cumulative = 0

    def __iter__(self):
        return self

    def __next__(self):
        cdef object nxt = next(self.it)
        if nxt:
            self.cumulative += nxt
        else:
            self.cumulative = 0
        return self.cumulative

Les deux doivent être consommés, par exemple en utilisant list pour donner la sortie souhaitée:

>>> list_a = [1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1]
>>> list(gen(list_a))
[1, 2, 3, 0, 1, 2, 0, 1, 0, 1, 2, 3]
>>> list(Cumulative(list_a))
[1, 2, 3, 0, 1, 2, 0, 1, 0, 1, 2, 3]

Cependant, puisque vous avez posé des questions sur la vitesse, je voulais partager les résultats de mes horaires:

import pandas as pd
import numpy as np
import random

import pandas as pd
from itertools import takewhile
from itertools import groupby, accumulate, chain

def MSeifert(lst):
    return list(MSeifert_inner(lst))

def MSeifert_inner(lst):
    cumulative = 0
    for item in lst:
        if item:
            cumulative += item
        else:
            cumulative = 0
        yield cumulative

def MSeifert2(lst):
    return list(Cumulative(lst))

def original1(list_a):
    list_b = []
    for i, x in enumerate(list_a):
        if x == 0:
            list_b.append(x)
        else:
            sum_value = 0
            for j in list_a[i::-1]:
                if j != 0:
                    sum_value += j
                else:
                    break
            list_b.append(sum_value)

def original2(list_a):
    return [sum(takewhile(lambda x: x != 0, list_a[i::-1])) for i, d in enumerate(list_a)]

def Coldspeed1(data):
    data = data.copy()
    for i in range(1, len(data)):
        if data[i]:  
            data[i] += data[i - 1] 
    return data

def Coldspeed2(data):
    s = pd.Series(data)    
    return s.groupby(s.eq(0).cumsum()).cumsum().tolist()

def Chris_Rands(list_a):
    return list(chain.from_iterable(accumulate(g) for _, g in groupby(list_a, bool)))

def EvKounis(list_a):
    cum_sum = 0
    list_b = []
    for item in list_a:
        if not item:            # if our item is 0
            cum_sum = 0         # the cumulative sum is reset (set back to 0)
        else:
            cum_sum += item     # otherwise it sums further
        list_b.append(cum_sum)  # and no matter what it gets appended to the result

def schumich(list_a):
    list_b = []
    s = 0
    for a in list_a:
        s = a+s if a !=0 else 0
        list_b.append(s)
    return list_b

def jbch(seq):
    return list(jbch_inner(seq))

def jbch_inner(seq):
    s = 0
    for n in seq:
        s = 0 if n == 0 else s + n
        yield s


# Timing setup
timings = {MSeifert: [], 
           MSeifert2: [],
           original1: [], 
           original2: [],
           Coldspeed1: [],
           Coldspeed2: [],
           Chris_Rands: [],
           EvKounis: [],
           schumich: [],
           jbch: []}
sizes = [2**i for i in range(1, 20, 2)]

# Timing
for size in sizes:
    print(size)
    func_input = [int(random.random() < 0.75) for _ in range(size)]
    for func in timings:
        if size > 10000 and (func is original1 or func is original2):
            continue
        res = %timeit -o func(func_input)   # if you use IPython, otherwise use the "timeit" module
        timings[func].append(res)


%matplotlib notebook

import matplotlib.pyplot as plt
import numpy as np

fig = plt.figure(1)
ax = plt.subplot(111)

baseline = MSeifert2 # choose one function as baseline
for func in timings:
    ax.plot(sizes[:len(timings[func])], 
            [time.best / ref.best for time, ref in Zip(timings[func], timings[baseline])], 
            label=func.__name__)  # you could also use "func.__name__" here instead
ax.set_ylim(0.8, 1e4)
ax.set_yscale('log')
ax.set_xscale('log')
ax.set_xlabel('size')
ax.set_ylabel('time relative to {}'.format(baseline)) # you could also use "func.__name__" here instead
ax.grid(which='both')
ax.legend()
plt.tight_layout()

Dans le cas où vous êtes intéressé par les résultats exacts, je les ai mis dans ce Gist .

enter image description here

C'est un tracé log-log et relatif à la réponse Cython. En bref: plus le plus rapide est faible et la plage entre deux graduations majeures représente un ordre de grandeur.

Ainsi, toutes les solutions ont tendance à être dans un ordre de grandeur (au moins lorsque la liste est longue) à l'exception des solutions que vous aviez. Étrangement, la solution pandas est assez lente par rapport aux approches pures Python. Cependant, la solution Cython bat toutes les autres approches d'un facteur 2).

10
MSeifert

Vous jouez trop avec les indices dans le code que vous avez publié alors que vous n'en avez pas vraiment besoin. Vous pouvez simplement garder une trace d'un somme cumulée et le réinitialiser à 0 chaque fois que vous rencontrez un 0.

list_a = [1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1]

cum_sum = 0
list_b = []
for item in list_a:
    if not item:            # if our item is 0
        cum_sum = 0         # the cumulative sum is reset (set back to 0)
    else:
        cum_sum += item     # otherwise it sums further
    list_b.append(cum_sum)  # and no matter what it gets appended to the result
print(list_b)  # -> [1, 2, 3, 0, 1, 2, 0, 1, 0, 1, 2, 3]
7
Ev. Kounis

Cela ne doit pas être aussi compliqué que dans la question posée, une approche très simple pourrait être la suivante.

list_a = [1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1]
list_b = []
s = 0
for a in list_a:
    s = a+s if a !=0 else 0
    list_b.append(s)

print list_b
4
Shubham Mishra

J'utiliserais un générateur si vous voulez des performances (et c'est simple aussi).

def weird_cumulative_sum(seq):
    s = 0
    for n in seq:
        s = 0 if n == 0 else s + n
        yield s

list_b = list(weird_cumulative_sum(list_a_))

Je ne pense pas que vous irez mieux que cela, dans tous les cas, vous devrez répéter la liste_a au moins une fois.

Notez que j'ai appelé list () sur le résultat pour obtenir une liste comme dans votre code mais si le code utilisant list_b ne l'itère qu'une seule fois avec une boucle for ou quelque chose ne sert à rien de convertir le résultat en une liste, il suffit de le passer le générateur.

1
jbch

Démarrage Python 3.8, et l'introduction de expressions d'affectation (PEP 572) (:= opérateur), on peut utiliser et incrémenter une variable dans une liste de compréhension:

# items = [1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1]
total = 0
[total := (total + x if x else x) for x in items]
# [1, 2, 3, 0, 1, 2, 0, 1, 0, 1, 2, 3]

Cette:

  • Initialise une variable total à 0 qui symbolise la somme cumulée
  • Pour chaque article, les deux:
    • soit incrémente total avec l'élément en boucle actuel (total := total + x) via une expression d'affectation ou redéfinissez-la sur 0 si l'élément est 0
    • et en même temps, mappe x à la nouvelle valeur de total
0
Xavier Guihot