Суммарный временной интервал панд в группе, исключая перекрытия - PullRequest
1 голос
/ 26 октября 2019

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

Например, если у нас есть группа, которая выглядит следующим образом:

         id1:    |----|
         id2:       |-----|
         id3:                      |--------|
                 .  .  .  .  .  .  .  .  .  .  .
time ->        12:00    12:04    12:07    12:10

, тогда для всех строк, принадлежащих этой группе, будет получено суммарное время 4 + 3 мин = 420 секунд

Если они полностью перекрываются, то мы получим такой сценарий:

         id1:    |--------|
         id2:    |--------|
                 .  .  .  .  .  .  .  .  .  .  .
time ->        12:00    12:04    12:07    12:10

, который даст нам результат 4 мин = 240 секунд.

Ниже приведены некоторые фиктивные данные:


import pandas as pd

ids = [x for x in range(10)]
group = [0, 1, 1, 2, 2, 3, 4, 4, 4, 4]

start = pd.to_datetime(["2019-10-21-16:20:00", "2019-10-21-16:22:00", "2019-10-21-16:22:00", "2019-10-21-16:15:00",
         "2019-10-21-16:22:00", "2019-10-21-16:58:00", "2019-10-21-17:02:00", "2019-10-21-17:03:00",
         "2019-10-21-17:04:00", "2019-10-21-17:20:00"])

end = pd.to_datetime(["2019-10-21-16:25:00", "2019-10-21-16:24:00", "2019-10-21-16:24:00", "2019-10-21-16:18:00",
       "2019-10-21-16:26:00", "2019-10-21-17:02:00", "2019-10-21-17:06:00", "2019-10-21-17:07:00",
       "2019-10-21-17:08:00", "2019-10-21-17:22:00"])

cols = ["id", "group", "start", "end"]


df = pd.DataFrame(dict(zip(cols, [ids, group, start, end])))

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

gr = df.groupby("group").apply(lambda x : x.end.max() - x.start.min())
df['total_time'] = df.group.map(gr)

Ответы [ 4 ]

1 голос
/ 26 октября 2019

В качестве исходных данных я взял следующий DataFrame:

  group             start               end
0    G1  2019-09-01 12:00  2019-09-01 12:02
1    G1  2019-09-01 12:01  2019-09-01 12:04
2    G1  2019-09-01 12:07  2019-09-01 12:10
3    G2  2019-09-01 12:05  2019-09-01 12:12
4    G2  2019-09-01 12:10  2019-09-01 12:15

Первый шаг - определить функцию подсчета секунд в группе строк:

def getSecs(grp):
    return pd.DatetimeIndex([]).union_many([ pd.date_range(
        row.start, row.end, freq='s', closed='left')
            for _, row in grp.iterrows() ]).size

Затем применить этофункция для каждой группы, группировка по group :

secs = df.groupby('group').apply(getSecs).rename('secs')

Для моих тестовых данных результат:

group
G1    420
G2    600
Name: secs, dtype: int64

И последний шаг - создатьновый столбец в df путем слияния с сек :

df = df.merge(secs, left_on='Grp', right_index=True)

Результат:

  group             start               end  secs
0    G1  2019-09-01 12:00  2019-09-01 12:02   420
1    G1  2019-09-01 12:01  2019-09-01 12:04   420
2    G1  2019-09-01 12:07  2019-09-01 12:10   420
3    G2  2019-09-01 12:05  2019-09-01 12:12   600
4    G2  2019-09-01 12:10  2019-09-01 12:15   600

Довольно краткое решение, всего 6строк кода, значительно меньше, чем в некоторых других решениях.

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

1 голос
/ 26 октября 2019

Использование-

def merge_intervals(intervals):
    sorted_by_lower_bound = sorted(intervals, key=lambda tup: tup[0])
    merged = []

    for higher in sorted_by_lower_bound:
        if not merged:
            merged.append(higher)
        else:
            lower = merged[-1]
            # test for intersection between lower and higher:
            # we know via sorting that lower[0] <= higher[0]
            if higher[0] <= lower[1]:
                upper_bound = max(lower[1], higher[1])
                merged[-1] = (lower[0], upper_bound)  # replace by merged interval
            else:
                merged.append(higher)
    return merged

df['dt'] = df[['start', 'end']].apply(tuple, axis=1)
op = df.groupby(['group'])['dt'].apply(list)
f_op = op.apply(merge_intervals)

op_d = f_op.apply(lambda x: sum([(y[1]-y[0]).seconds for y in x]))

Выход

group
0    300
1    120
2    420
3    240
4    480
1 голос
/ 26 октября 2019

Предполагая, что ваш фрейм данных отсортирован, как насчет этого?

In [1]: import datetime 
        def calc_periods(x):
            time_delt = datetime.timedelta()
            for i in x.index:
                if (i > x.index[0]):
                    if x.loc[i].start < x.loc[i-1].end:
                        time_delt += x.loc[i].end - x.loc[i-1].end
                    else:
                        time_delt += x.loc[i].end - x.loc[i].start
                else:
                    time_delt += x.loc[i].end - x.loc[i].start
            return time_delt.seconds


In [2]: df.groupby('group')[['start', 'end']].apply(calc_periods)
Out[2]: group
        0    300
        1    120
        2    420
        3    240
        4    480
        dtype: int64
1 голос
/ 26 октября 2019

Сначала добавьте столбец, который отслеживает последнее время окончания, которое было замечено до сих пор (но только с учетом одной и той же группы):

df['notbefore'] = df.groupby('group').end.shift().cummax()

Оно сдвинуто на 1, чтобы отразить последнее время окончания, замеченное в предыдущемстроки, исключая одну и ту же строку. Важно иметь shift() до cummax(), в противном случае сдвиг «утечек» значений между группами.

Затем добавьте столбец, содержащий «эффективное» время начала:

df['effstart'] = df[['start', 'notbefore']].max(1)

Это время начала, измененное таким образом, чтобы оно не превышало какого-либо предыдущего времени окончания (во избежание наложения).

Затем вычислите общее количество пройденных секунд:

df['effsec'] = (df.end - df.effstart).clip(np.timedelta64(0))

df теперь:

   id  group               start                 end           notbefore            effstart   effsec
0   0      0 2019-10-21 16:20:00 2019-10-21 16:25:00                 NaT 2019-10-21 16:20:00 00:05:00
1   1      1 2019-10-21 16:22:00 2019-10-21 16:24:00                 NaT 2019-10-21 16:22:00 00:02:00
2   2      1 2019-10-21 16:22:00 2019-10-21 16:24:00 2019-10-21 16:24:00 2019-10-21 16:24:00 00:00:00
3   3      2 2019-10-21 16:15:00 2019-10-21 16:18:00                 NaT 2019-10-21 16:15:00 00:03:00
4   4      2 2019-10-21 16:22:00 2019-10-21 16:26:00 2019-10-21 16:24:00 2019-10-21 16:24:00 00:02:00
5   5      3 2019-10-21 16:58:00 2019-10-21 17:02:00                 NaT 2019-10-21 16:58:00 00:04:00
6   6      4 2019-10-21 17:02:00 2019-10-21 17:06:00                 NaT 2019-10-21 17:02:00 00:04:00
7   7      4 2019-10-21 17:03:00 2019-10-21 17:07:00 2019-10-21 17:06:00 2019-10-21 17:06:00 00:01:00
8   8      4 2019-10-21 17:04:00 2019-10-21 17:08:00 2019-10-21 17:07:00 2019-10-21 17:07:00 00:01:00
9   9      4 2019-10-21 17:20:00 2019-10-21 17:22:00 2019-10-21 17:08:00 2019-10-21 17:20:00 00:02:00

Чтобы получить окончательные результаты:

df.groupby('group').effsec.sum()

, что дает вам:

group
0   00:05:00
1   00:02:00
2   00:05:00
3   00:04:00
4   00:08:00
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...