Преобразование кадра данных в новый при выполнении некоторых дополнительных операций - PullRequest
6 голосов
/ 18 июня 2019

Я работаю с фреймом данных, где каждая запись (строка) имеет время начала, длительность и другие атрибуты.Я хотел бы создать новый фрейм данных из этого, где я бы как бы преобразовывал каждую запись из оригинальной в 15-минутные интервалы, сохраняя все остальные атрибуты такими же.Количество записей в новом кадре данных на запись в старом будет зависеть от фактической продолжительности исходного.

Сначала я попытался использовать pd.resample, но он не сработал так, как я ожидал.Затем я сконструировал функцию, используя itertuples(), которая работает довольно хорошо, но это заняло около получаса с кадром данных около 3000 строк.Теперь я хочу сделать то же самое для 2 миллионов строк, поэтому я ищу другие возможности.

Допустим, у меня есть следующий фрейм данных:

testdict = {'start':['2018-01-05 11:48:00', '2018-05-04 09:05:00', '2018-08-09 07:15:00', '2018-09-27 15:00:00'], 'duration':[22,8,35,2], 'Attribute_A':['abc', 'def', 'hij', 'klm'], 'id': [1,2,3,4]}
testdf = pd.DataFrame(testdict)
testdf.loc[:,['start']] = pd.to_datetime(testdf['start'])
print(testdf)

>>>testdf
                 start  duration Attribute_A  id
0  2018-01-05 11:48:00        22         abc   1
1  2018-05-04 09:05:00         8         def   2
2  2018-08-09 07:15:00        35         hij   3
3  2018-09-27 15:00:00         2         klm   4

И я бы хотел, чтобы мой результат былкак следующее:

>>>resultdf
                start  duration Attribute_A  id
0 2018-01-05 11:45:00        12         abc   1
1 2018-01-05 12:00:00        10         abc   1
2 2018-05-04 09:00:00         8         def   2
3 2018-08-09 07:15:00        15         hij   3
4 2018-08-09 07:30:00        15         hij   3
5 2018-08-09 07:45:00         5         hij   3
6 2018-09-27 15:00:00         2         klm   4

Это функция, которую я построил с помощью itertuples, которая дала желаемый результат (тот, который я показал чуть выше):

def min15_divider(df,newdf):
for row in df.itertuples():
    orig_min = row.start.minute
    remains = orig_min % 15 # Check if it is already a multiple of 15
    if remains == 0:
        new_time = row.start.replace(second=0)
        if row.duration < 15: # if it shorter than 15 min just use that for the duration
            to_append = {'start': new_time, 'Attribute_A': row.Attribute_A,
                         'duration': row.duration, 'id':row.id}
            newdf = newdf.append(to_append, ignore_index=True)
        else: # if not, divide that in 15 min intervals until duration is exceeded
            cumu_dur = 15
            while cumu_dur < row.duration:
                to_append = {'start': new_time, 'Attribute_A': row.Attribute_A, 'id':row.id}
                if cumu_dur < 15:
                    to_append['duration'] = cumu_dur
                else:
                    to_append['duration'] = 15
                new_time = new_time + pd.Timedelta('15 minutes')
                cumu_dur = cumu_dur + 15
                newdf = newdf.append(to_append, ignore_index=True)

            else: # add the remainder in the last 15 min interval
                final_dur = row.duration - (cumu_dur - 15)
                to_append = {'start': new_time, 'Attribute_A': row.Attribute_A,'duration': final_dur, 'id':row.id}
                newdf = newdf.append(to_append, ignore_index=True)

    else: # When it is not an exact multiple of 15 min
        new_min = orig_min - remains # convert to multiple of 15
        new_time = row.start.replace(minute=new_min)
        new_time = new_time.replace(second=0)
        cumu_dur = 15 - remains # remaining minutes in the initial interval
        while cumu_dur < row.duration: # divide total in 15 min intervals until duration is exceeded
            to_append = {'start': new_time, 'Attribute_A': row.Attribute_A, 'id':row.id}
            if cumu_dur < 15:
                to_append['duration'] = cumu_dur
            else:
                to_append['duration'] = 15

            new_time = new_time + pd.Timedelta('15 minutes')
            cumu_dur = cumu_dur + 15
            newdf = newdf.append(to_append, ignore_index=True)

        else: # when we reach the last interval or the starting duration was less than the remaining minutes
            if row.duration < 15:
                final_dur = row.duration # original duration less than remaining minutes in first interval
            else:
                final_dur = row.duration - (cumu_dur - 15) # remaining duration in last interval
            to_append = {'start': new_time, 'Attribute_A': row.Attribute_A, 'duration': final_dur, 'id':row.id}
            newdf = newdf.append(to_append, ignore_index=True)
return newdf

Есть ли еще какие-нибудьспособ сделать это без использования itertuples, что может сэкономить мне время?

Заранее спасибо.

PS.Я прошу прощения за все, что может показаться немного странным в моем посте, так как я впервые задаю вопрос здесь, в stackoverflow.

EDIT

Многие записи могут иметь одинаковое началовремя, поэтому .groupby start может быть проблематичным.Однако есть столбец с уникальными значениями для каждой записи, который называется просто «id».

Ответы [ 2 ]

0 голосов
/ 18 июня 2019

Итак, начиная с вашего df:

testdict = {'start':['2018-01-05 11:48:00', '2018-05-04 09:05:00', '2018-08-09 07:15:00', '2018-09-27 15:00:00'], 'duration':[22,8,35,2], 'Attribute_A':['abc', 'def', 'hij', 'klm']}
df = pd.DataFrame(testdict)
df.loc[:,['start']] = pd.to_datetime(df['start'])
print(df)

Сначала вычислите время окончания каждой строки:

df['dur'] = pd.to_timedelta(df['duration'], unit='m')
df['end'] = df['start'] + df['dur']

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

df['start15'] = df['start'].dt.floor('15min')
df['end15'] = df['end'].dt.floor('15min')

На этом этапе датафрейм выглядит следующим образом:

  Attribute_A  duration               start      dur                 end start15               end15
0         abc        22 2018-01-05 11:48:00 00:22:00 2018-01-05 12:10:00 2018-01-05 11:45:00 2018-01-05 12:00:00  
1         def         8 2018-05-04 09:05:00 00:08:00 2018-05-04 09:13:00 2018-05-04 09:00:00 2018-05-04 09:00:00     
2         hij        35 2018-08-09 07:15:00 00:35:00 2018-08-09 07:50:00 2018-08-09 07:15:00 2018-08-09 07:45:00   
3         klm         2 2018-09-27 15:00:00 00:02:00 2018-09-27 15:02:00 2018-09-27 15:00:00 2018-09-27 15:00:00 

Столбцы start15 и end15 объединяются, чтобы иметь правильное время, но вам необходимо объединить их:

df = pd.melt(df, ['dur', 'start', 'Attribute_A', 'end'], ['start15', 'end15'], value_name='start15')
df = df.drop('variable', 1).drop_duplicates('start15').sort_values('start15').set_index('start15')

Выход:

                         dur               start Attribute_A
start15                                                     
2018-01-05 11:45:00 00:22:00 2018-01-05 11:48:00         abc
2018-01-05 12:00:00 00:22:00 2018-01-05 11:48:00         abc
2018-05-04 09:00:00 00:08:00 2018-05-04 09:05:00         def
2018-08-09 07:15:00 00:35:00 2018-08-09 07:15:00         hij
2018-08-09 07:45:00 00:35:00 2018-08-09 07:15:00         hij
2018-09-27 15:00:00 00:02:00 2018-09-27 15:00:00         klm

Хорошо выглядит, но строка 2018-08-09 07:30:00 отсутствует. Заполните эту и все остальные пропущенные строки с помощью groupby и повторите выборку:

df = df.groupby('start').resample('15min').ffill().reset_index(0, drop=True).reset_index()

Получить столбец end15 обратно, он был отброшен во время операции расплавления ранее:

df['end15'] = df['end'].dt.floor('15min')

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

df.loc[df['start15'] != df['end15'], 'duration'] = np.minimum(df['end15'] - df['start'], pd.Timedelta('15min').to_timedelta64())
df.loc[df['start15'] == df['end15'], 'duration'] = np.minimum(df['end'] - df['end15'], df['end'] - df['start'])

Тогда просто очистите, чтобы выглядело так, как вы хотели:

df['duration'] = (df['duration'].dt.seconds/60).astype(int)
print(df)
df = df[['start15', 'duration', 'Attribute_A']].copy()

Результат:

              start15  duration Attribute_A
0 2018-01-05 11:45:00        12         abc
1 2018-01-05 12:00:00        10         abc
2 2018-05-04 09:00:00         8         def
3 2018-08-09 07:15:00        15         hij
4 2018-08-09 07:30:00        15         hij
5 2018-08-09 07:45:00         5         hij
6 2018-09-27 15:00:00         2         klm

Обратите внимание, что части этого ответа были основаны на этот ответ

0 голосов
/ 18 июня 2019

Использование pd.resample - хорошая идея, но, поскольку у вас есть только время начала каждой строки, вам нужно построить конечную строку, прежде чем вы сможете ее использовать.

В приведенном ниже коде предполагается, что каждое время запуска в столбце 'start' уникально , поэтому grouby можно использовать немного необычным образом, поскольку он будет извлекать только одну строку.
Я использую groupby, поскольку он автоматически перегруппируеткадры данных, создаваемые пользовательской функцией, используемой apply.
Также обратите внимание, что столбец 'duration' преобразуется в timedelta за считанные минуты, чтобы лучше выполнить некоторые математические операции позже.

import pandas as pd

testdict = {'start':['2018-01-05 11:48:00', '2018-05-04 09:05:00', '2018-08-09 07:15:00', '2018-09-27 15:00:00'], 'duration':[22,8,35,2], 'Attribute_A':['abc', 'def', 'hij', 'klm']}
testdf = pd.DataFrame(testdict)
testdf['start'] = pd.to_datetime(testdf['start'])
testdf['duration'] = pd.to_timedelta(testdf['duration'], 'T')
print(testdf)

def calcduration(df, starttime):
    if len(df) == 1:
        return
    elif len(df) == 2:
        df['duration'].iloc[0] = pd.Timedelta(15, 'T') - (starttime - df.index[0])
        df['duration'].iloc[1] = df['duration'].iloc[1] - df['duration'].iloc[0]
    elif len(df) > 2:
        df['duration'].iloc[0] = pd.Timedelta(15, 'T') - (starttime - df.index[0])
        df['duration'].iloc[1:-1] = pd.Timedelta(15, 'T')
        df['duration'].iloc[-1] = df['duration'].iloc[-1] - df['duration'].iloc[:-1].sum()

def expandtime(x):
    frow = x.copy()
    frow['start'] = frow['start'] + frow['duration']
    gdf = pd.concat([x, frow], axis=0)
    gdf = gdf.set_index('start')
    resdf = gdf.resample('15T').nearest()
    calcduration(resdf, x['start'].iloc[0])
    return resdf

findf = testdf.groupby('start', as_index=False).apply(expandtime)
print(findf)

ЭтоКод производит:

                      duration Attribute_A
  start                                   
0 2018-01-05 11:45:00 00:12:00         abc
  2018-01-05 12:00:00 00:10:00         abc
1 2018-05-04 09:00:00 00:08:00         def
2 2018-08-09 07:15:00 00:15:00         hij
  2018-08-09 07:30:00 00:15:00         hij
  2018-08-09 07:45:00 00:05:00         hij
3 2018-09-27 15:00:00 00:02:00         klm

Немного объяснений

expandtime - первая пользовательская функция.Он принимает кадр данных из одной строки (поскольку мы предполагаем, что 'start' значения являются уникальными), создает вторую строку, чья 'start' равна 'start' первой строки + длительность, а затем использует resample для выборки ввременные интервалы 15 минут.Значения всех остальных столбцов дублируются.

calcduration используется для выполнения математических операций в столбце 'duration' для вычисления правильной продолжительности каждой строки.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...