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
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?
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.
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]
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 .
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).
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]
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
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.
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:
total
à 0
qui symbolise la somme cumuléetotal
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
x
à la nouvelle valeur de total