Подсчет последовательного положительного значения в массиве Python

Я пытаюсь подсчитать продолжительные дни в данных возврата капитала, поэтому, если положительный день равен 1, а отрицательный - 0, список y=[0,0,1,1,1,0,0,1,0,1,1] должен возвращать z=[0,0,1,2,3,0,0,1,0,1,2].

Я пришел к решению, которое является опрятным с точки зрения количества строк кода, но очень медленно:

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)

Я предполагаю, что я слишком часто перебираю весь список. Есть ли хороший Pythonic способ достижения того, что я хочу, только когда вы просматриваете данные? Я мог бы написать цикл сам, но интересно, есть ли лучший способ.

Спасибо!

Ответ 1

почему одержимость ультра-питоническим способом делать вещи? читабельность + эффективность козырей "leet hackerz style."

Я просто сделаю так:

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

Ответ 2

>>> y = pandas.Series([0,0,1,1,1,0,0,1,0,1,1])

Нижеследующее может показаться немного волшебным, но на самом деле используются некоторые распространенные идиомы: поскольку у pandas пока нет хорошей нативной поддержки непрерывной groupby, вам часто нужно что-то подобное.

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

Некоторое объяснение: во-первых, мы сравниваем y со смещенной версией самого себя, чтобы найти, когда начинаются смежные группы:

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

Затем (так как False == 0 и True == 1) мы можем применить накопленную сумму, чтобы получить число для групп:

>>> (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

Мы можем использовать groupby и cumcount чтобы cumcount целое число в каждой группе:

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

Добавить один:

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

И, наконец, обнулите значения, где у нас был ноль для начала:

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

Ответ 3

Если что-то понятно, это "питон". Честно говоря, я даже не могу заставить ваше оригинальное решение работать. Кроме того, если это работает, мне интересно, если это быстрее, чем цикл. Ты сравнивал?

Теперь, так как мы начали обсуждать эффективность, вот некоторые идеи.

Циклы в Python по своей сути медленные, независимо от того, что вы делаете. Конечно, если вы используете панд, вы также используете нудистский низ, со всеми преимуществами производительности. Только не разрушайте их, зацикливаясь. Это не значит, что списки Python занимают намного больше памяти, чем вы думаете; потенциально намного больше, чем 8 bytes * length, так как каждое целое число может быть заключено в отдельный объект и помещено в отдельную область в памяти, на которую указывает указатель из списка.

Векторизация, предоставляемая numpy, должна быть достаточной, ЕСЛИ вы можете найти способ выразить эту функцию без зацикливания. На самом деле, мне интересно, есть ли способ представить это с помощью выражений, таких как A+B*C Если вы можете создать эту функцию из функций в Lapack, то вы даже можете превзойти обычный код C++, скомпилированный с оптимизацией.

Вы также можете использовать один из скомпилированных подходов для ускорения ваших циклов. Посмотрите решение с Numba на numy массивах ниже. Другой вариант - использовать PyPy, хотя вы, вероятно, не сможете правильно комбинировать его с пандами.

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

Фактически, большая часть времени во втором примере выше была потрачена на компиляцию Just-In-Time. Вместо этого (не забудьте скопировать, так как функция изменяет массив).

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

Так что для последующих вызовов у нас есть 25-кратное ускорение по сравнению с простой версией. Я предлагаю вам прочитать High Performance Python, если вы хотите узнать больше.

Ответ 4

Это быстрее? Он проходит только один раз...

y=[0,0,1,1,1,0,0,1,0,1,1]

def f(y):
    z = []
    i = 0
    for e in y:
        if e == 0:
            i = 0
            z.append(e)
        else:
            z.append(e + i)
            i += 1
    return z

f(y)

Ответ 5

Сохранение простых вещей, используя один массив, один цикл и один условный.

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]