Тестирование последующих значений в DataFrame

У меня есть DataFrame с одним столбцом с положительными и отрицательными целыми числами. Для каждой строки я хотел бы видеть, сколько последовательных строк (начиная с текущей строки и включая ее) имеют отрицательные значения.

Так что если последовательность была 2, -1, -3, 1, -1 результат будет 0, 2, 1, 0, 1,

Я могу сделать это, перебирая все индексы, используя .iloc разбить столбец и next() чтобы узнать, где находится следующее положительное значение. Но я чувствую, что это не использует возможности панды, и я думаю, что есть лучший способ сделать это. Я экспериментировал с использованием .shift() а также expanding_window но без успеха.

Есть ли более "пандастический" способ узнать, сколько последовательных строк после текущей удовлетворяет некоторому логическому условию?

Вот что работает сейчас:

import pandas as pd

df = pd.DataFrame({"a": [2, -1, -3, -1, 1, 1, -1, 1, -1]})

df["b"] = 0
for i in df.index:
    sub = df.iloc[i:].a.tolist()
    df.b.iloc[i] = next((sub.index(n) for n in sub if n >= 0), 1)

Изменить: я понимаю, что даже мой собственный пример не работает, когда в конце более одного отрицательного значения. Так что это делает лучшее решение еще более необходимым.

Редактировать 2: я изложил проблему в терминах целых чисел, но первоначально только поставить 1 а также -1 в моем примере. Мне нужно решить для положительных и отрицательных целых чисел в целом.

2 ответа

Решение

FWIW, вот довольно пандастический ответ, который не требует никаких функций или применений. Заимствует отсюда (среди других ответов, я уверен) и спасибо @DSM за упоминание параметра ascending=False:

df = pd.DataFrame({"a": [2, -1, -3, -1, 1, 1, -1, 1, -1, -2]})

df['pos'] = df.a > 0
df['grp'] = ( df['pos'] != df['pos'].shift()).cumsum()
dfg = df.groupby('grp')
df['c'] = np.where( df['a'] < 0, dfg.cumcount(ascending=False)+1, 0 )

   a  b    pos  grp  c
0  2  0   True    1  0
1 -1  3  False    2  3
2 -3  2  False    2  2
3 -1  1  False    2  1
4  1  0   True    3  0
5  1  0   True    3  0
6 -1  1  False    4  1
7  1  0   True    5  0
8 -1  1  False    6  2
9 -2  1  False    6  1

Я думаю, что хорошая вещь в этом методе состоит в том, что после того, как вы установите переменную 'grp', вы можете очень легко сделать много вещей стандартными методами groupby.

Это была интересная головоломка. Я нашел способ сделать это, используя инструменты панд, но я думаю, вы согласитесь, что это намного более непрозрачно:-). Вот пример:

data = pandas.Series([1, -1, -1, -1, 1, -1, -1, 1, 1, -1, 1])
x = data[::-1] # reverse the data

print(x.groupby(((x<0) != (x<0).shift()).cumsum()).apply(lambda x: pandas.Series(
    np.arange(len(x))+1 if (x<0).all() else np.zeros(len(x)),
    index=x.index))[::-1])

Вывод правильный:

0     0
1     3
2     2
3     1
4     0
5     2
6     1
7     0
8     0
9     1
10    0
dtype: float64

Основная идея аналогична той, что я описал в своем ответе на этот вопрос, и вы можете найти тот же подход, который использовался в различных ответах, которые спрашивают, как использовать информацию между рядами в пандах. Ваш вопрос немного сложнее, потому что ваш критерий идет в обратном направлении (спрашивая количество следующих негативов, а не количество предшествующих негативов), и потому, что вы хотите только одну сторону группировки (то есть, вы хотите только количество последовательных негативов, не количество последовательных номеров с одинаковым знаком).

Вот более подробная версия того же кода с некоторыми пояснениями, которые могут облегчить понимание:

def getNegativeCounts(x):
    # This function takes as input a sequence of numbers, all the same sign.
    # If they're negative, it returns an increasing count of how many there are.
    # If they're positive, it just returns the same number of zeros.
    # [-1, -2, -3] -> [1, 2, 3]
    # [1, 2, 3] -> [0, 0, 0]
    if (x<0).all():
        return pandas.Series(np.arange(len(x))+1, index=x.index)
    else:
        return pandas.Series(np.zeros(len(x)), index=x.index)

# we have to reverse the data because cumsum only works in the forward direction
x = data[::-1]

# compute for each number whether it has the same sign as the previous one
sameSignAsPrevious = (x<0) != (x<0).shift()
# cumsum this to get an "ID" for each block of consecutive same-sign numbers
sameSignBlocks = sameSignAsPrevious.cumsum()
# group on these block IDs
g = x.groupby(sameSignBlocks)
# for each block, apply getNegativeCounts
# this will either give us the running total of negatives in the block,
# or a stretch of zeros if the block was positive
# the [::-1] at the end reverses the result
# (to compensate for our reversing the data initially)
g.apply(getNegativeCounts)[::-1]

Как видите, операции в стиле длины выполнения не всегда просты в пандах. Существует, однако, открытая проблема для добавления дополнительных способностей группировки / разделения, которые могли бы улучшить некоторые из них. В любом случае, ваш конкретный вариант использования имеет некоторые специфические особенности, которые немного отличают его от типичной задачи длины выполнения.

Другие вопросы по тегам