Эффективно вычисляйте непоследовательное количество появлений элемента в кадре данных
Учитывая следующий фрейм данных
Value
time
2020-02-14 14:16:10.769999872+00:00 74
2020-02-14 14:16:11.360999936+00:00 74
2020-02-14 14:16:11.970000128+00:00 72
2020-02-14 14:16:12.637000192+00:00 72
2020-02-14 14:16:13.210000128+00:00 74
... ...
2020-02-28 08:15:20.340000+00:00 71
2020-02-28 08:15:20.890000128+00:00 71
2020-02-28 08:15:21.424000+00:00 71
2020-02-28 08:15:22.032999936+00:00 72
2020-02-28 08:15:22.594000128+00:00 72
Я хотел бы, чтобы мой код прошел через значения, нашел начальный и конечный индексы каждого значения и сохранил эту информацию в словаре.
results = {74: {start:2020-02-14 14:16:10.769999872+00:00, end:2020-02-14 14:16:11.360999936+00:00},
72: {start: ..., end: ...},
...}
Поскольку это было бы слишком просто, сложность заключается в том, что одно или несколько значений могут появляться несколько раз непоследовательно:
74, 74, 72, 72, 72, 74, 74, 74, 71, 71, 71, 72, 72, 71, 71
.
В этом случае для каждого значения должна быть сгенерирована новая последовательность, содержащая начальный и конечный индексы.
results = {74:
{Sequence1: {start:2020-02-14 14:16:10.769999872+00:00, end:2020-02-14 14:16:11.360999936+00:00},
Sequence2: {start: ... , end: ...}},
72:
{Sequence1: {start: ..., end: ...},
Seqeunce2: {start: ..., end: ...},
Sequence3: {start: ..., end: ...}},
71: ...,
}
Естественно, я могу закодировать это с помощью множества циклов for, но мне было интересно, может ли быть более аккуратное и умное решение, которое избавило бы меня от pfaff. И, может быть, самое главное, очень важно, чтобы код работал быстро. Фрейм данных содержит около 300000 строк.
2 ответа
Это можно сделать в двух частях. Первый состоит в выводах последовательных групп. Второй заключается в нахождении минимального / максимального времени для каждой группы.
Для поиска групп вы можете использовать решение, описанное здесь. Вот решение, примененное в вашем случае:
groups = (df.Value != df.Value.shift()).cumsum()
Тогда вы можете просто применить несколько groupby
чтобы найти даты начала и окончания. Однако есть более эффективный и простой способ сделать это, используяagg
:
result = df.groupby(groups).agg(Value=('Value',min), startTime=('time',min), endTime=('time',max))
Наконец, если вам нужен dict, вы можете просто перебрать получившийся фрейм данных.
Вот протестированный ввод:
time Value
0 2020-02-14 14:16:10.769999872+00:00 74
1 2020-02-14 14:16:11.360999936+00:00 74
2 2020-02-14 14:16:11.970000128+00:00 72
3 2020-02-14 14:16:12.637000192+00:00 72
4 2020-02-14 14:16:13.210000128+00:00 74
5 2020-02-28 08:15:20.340000+00:00 71
6 2020-02-28 08:15:20.890000128+00:00 71
7 2020-02-28 08:15:21.424000+00:00 71
8 2020-02-28 08:15:22.032999936+00:00 72
9 2020-02-28 08:15:22.594000128+00:00 72
Вот результат:
Value startTime endTime
Value
1 74 2020-02-14 14:16:10.769999872+00:00 2020-02-14 14:16:11.360999936+00:00
2 72 2020-02-14 14:16:11.970000128+00:00 2020-02-14 14:16:12.637000192+00:00
3 74 2020-02-14 14:16:13.210000128+00:00 2020-02-14 14:16:13.210000128+00:00
4 71 2020-02-28 08:15:20.340000+00:00 2020-02-28 08:15:21.424000+00:00
5 72 2020-02-28 08:15:22.032999936+00:00 2020-02-28 08:15:22.594000128+00:00
Обратите внимание, что я тестировал входные даты, закодированные в виде строк, что должно быть нормально, поскольку они выражены в соответствии с ISO 8601.
Я предполагаю, что индекс на самом деле является DatetimeIndex. Если это не так, преобразуйте его.
Чтобы выполнить свою задачу, начните с определения функции, которая будет применяться к каждой группе строк:
def fn(grp):
tMin = grp.index.min()
tMax = grp.index.max()
v = grp.Value.iloc[0]
return pd.Series([v, tMin, tMax], index=['val', 'start', 'end'])
Затем примените его к каждой группе строк с одинаковым значением Value (изменение Value открывает новую группу):
df2 = df.groupby([(df.Value != df.Value.shift()).cumsum()])\
.apply(fn).reset_index(drop=True)
Следующим шагом будет создание столбца с содержимым Sequence... (сначала только число, а затем преобразовать его в строку):
df2['Seq'] = df2.groupby('val').cumcount() + 1
df2['Seq'] = 'Sequence' + df2['Seq'].astype(str)
И чтобы вычислить окончательный результат, запустите:
result = {}
for key, grp in gr:
result[key] = grp.set_index('Seq')[['start', 'end']].to_dict(orient='index')
Для ваших данных образца результат будет следующим:
{71: {'Sequence1': {'start': Timestamp('2020-02-28 08:15:20.340000+0000', tz='UTC'),
'end': Timestamp('2020-02-28 08:15:21.424000+0000', tz='UTC')}},
72: {'Sequence1': {'start': Timestamp('2020-02-14 14:16:11.970000128+0000', tz='UTC'),
'end': Timestamp('2020-02-14 14:16:12.637000192+0000', tz='UTC')},
'Sequence2': {'start': Timestamp('2020-02-28 08:15:22.032999936+0000', tz='UTC'),
'end': Timestamp('2020-02-28 08:15:22.594000128+0000', tz='UTC')}},
74: {'Sequence1': {'start': Timestamp('2020-02-14 14:16:10.769999872+0000', tz='UTC'),
'end': Timestamp('2020-02-14 14:16:11.360999936+0000', tz='UTC')},
'Sequence2': {'start': Timestamp('2020-02-14 14:16:13.210000128+0000', tz='UTC'),
'end': Timestamp('2020-02-14 14:16:13.210000128+0000', tz='UTC')}}}
Обратите внимание, что каждое значение, сохраненное под ключом начала или конца, является фактической меткой времени. Это может быть и простая строка, но я думаю, что это содержимое легче для дальнейшей обработки.