Тестирование последующих значений в 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]
Как видите, операции в стиле длины выполнения не всегда просты в пандах. Существует, однако, открытая проблема для добавления дополнительных способностей группировки / разделения, которые могли бы улучшить некоторые из них. В любом случае, ваш конкретный вариант использования имеет некоторые специфические особенности, которые немного отличают его от типичной задачи длины выполнения.