Эффективно вычисляйте непоследовательное количество появлений элемента в кадре данных

Учитывая следующий фрейм данных

                                     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')}}}

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

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