Контекст: у меня есть фрейм данных с индексом времени и связанными значениями. И еще один фрейм данных («фрейм данных событий»), в котором каждая строка представляет собой событие со временем начала и окончания и типом.
Вопрос: какой самый простой и самый быстрый/эффективный способ создать новый столбец в моем фрейме данных для каждой строки в моем фрейме данных событий.
Вот некоторые примеры данных и мой текущий подход.
import pandas as pd
import numpy as np
np.random.seed(0)
df_idx = pd.date_range(start='2024-03-04', end='2024-03-10', freq='h')
df = pd.DataFrame(data = {'placeholder': np.random.randint(1000,size=len(df_idx))}, index=df_idx)
df_events = pd.DataFrame([
['football', pd.Timestamp('2024-03-04 15:00:00'), pd.Timestamp('2024-03-04 16:59:59')]
,['concert', pd.Timestamp('2024-03-05 19:00:00'), pd.Timestamp('2024-03-05 21:59:59')]
, ['football', pd.Timestamp('2024-03-07 18:00:00'), pd.Timestamp('2024-03-07 19:59:59')]
, ['football', pd.Timestamp('2024-03-08 18:00:00'), pd.Timestamp('2024-03-08 19:59:59')]
, ['public_event', pd.Timestamp('2024-03-09 16:00:00'), pd.Timestamp('2024-03-09 23:59:59')]
]
, columns=['type','start','end']
)
#This is my current approach
#Get the unique events types
events = df_events['type'].unique()
#Iterate over the df_events rows per event, and create a new column in df
for event in events:
df[event]=0
for idx, row in df_events[df_events['type']==event].iterrows():
df.loc[(df.index>=row['start'])&(df.index<=row['end']),event]=1
df.head(5)
Ожидаемым результатом будет фрейм данных с исходным индексом времени и столбцом-заполнителем, а также 3 других столбца, называемых «футбол», «концерт» и публичные мероприятия с 0 и 1, где индекс времени находится между началом и концом соответствующие события.
Что-то вроде этого:

Есть ли лучший способ сделать это?
Примечание: конечно, мои фреймы данных на самом деле намного больше, и я получаю предупреждение из-за подхода iterrows.
пожалуйста, используйте воспроизводимый набор данных. Я добавил np.random.seed(0) в ваш набор данных, чтобы всегда обеспечивать одни и те же данные.






Вы можете использовать это:
ranges = pd.Series(
[
pd.date_range(start, end, freq = "H")
for start, end in zip(df_events["start"], df_events["end"])
]
)
types = (
ranges.explode()
.to_frame(name = "range")
.join(df_events["type"])
.set_index("range")["type"]
)
df = (
df.join(pd.get_dummies(types, prefix = "", prefix_sep = ""))
.fillna(0)
.astype(int)
.groupby(level=0)
.max()
)
placeholder concert football public_event
2024-03-04 00:00:00 684 0 0 0
2024-03-04 01:00:00 559 0 0 0
2024-03-04 02:00:00 629 0 0 0
2024-03-04 03:00:00 192 0 0 0
2024-03-04 04:00:00 835 0 0 0
... ... ... ... ...
2024-03-09 20:00:00 554 0 0 1
2024-03-09 21:00:00 973 0 0 1
2024-03-09 22:00:00 368 0 0 1
2024-03-09 23:00:00 999 0 0 1
2024-03-10 00:00:00 917 0 0 0
[145 rows x 4 columns]
print(df[df["concert"].eq(1) | df["football"].eq(1) | df["public_event"].eq(1)]) выведет:
placeholder concert football public_event
2024-03-04 15:00:00 472 0 1 0
2024-03-04 16:00:00 600 0 1 0
2024-03-05 19:00:00 755 1 0 0
2024-03-05 20:00:00 797 1 0 0
2024-03-05 21:00:00 659 1 0 0
2024-03-07 18:00:00 779 0 1 0
2024-03-07 19:00:00 430 0 1 0
2024-03-08 18:00:00 660 0 1 0
2024-03-08 19:00:00 227 0 1 0
2024-03-09 16:00:00 816 0 0 1
2024-03-09 17:00:00 861 0 0 1
2024-03-09 18:00:00 387 0 0 1
2024-03-09 19:00:00 610 0 0 1
2024-03-09 20:00:00 554 0 0 1
2024-03-09 21:00:00 973 0 0 1
2024-03-09 22:00:00 368 0 0 1
2024-03-09 23:00:00 999 0 0 1
Вы правы, была опечатка, исправил данные примера
@Я обновил ответ, указав результат, используя исправленные данные образца. Решение остается тем же.
Мне нравится ваш ответ, потому что он не использует другой импорт, кроме того, который я использовал. Приложение все еще существует, и мне интересно, работает ли оно по-прежнему, если события этого типа происходят одновременно (перекрываются), но это определенно хороший путь
@Adn У тебя есть серьезные опасения. Я обновил свой ответ, добавив другой способ построения диапазонов дат без apply, что должно быть намного быстрее. Кроме того, я добавил groupby в индекс, агрегирующий с max в конце, чтобы справиться с перекрывающимися событиями.
Я знаю, что образцы данных OP были созданы с помощью freq='h', но знаем ли мы, что можем предположить, что реальные данные имеют регулярно расположенные временные метки, как и образцы данных? Если нет, то этот подход не сработает. Он зависит от того, сможете ли вы точно сопоставить временные метки df_idx с помощью pd.date_range().
для меня это сработает, потому что в реальных данных я буду использовать resample('h').sum(), чтобы получить монотонный индекс с часовыми интервалами @e-motta. Я не понимаю полезности prefix = "", prefix_sep = ""; Пробовал с и без, результат не изменился
у вас есть соединение диапазона, форма соединения неравенства, когда столбец из одного фрейма данных находится между двумя столбцами из другого фрейма данных. Conditional_join от pyjanitor предлагает эффективный способ выполнить это.
Краткое описание шагов:
df и df_eventsdf и заполните, где это необходимо# pip install pyjanitor
import pandas as pd
import janitor
events = df_events['type'].unique()
# conditional_join works only with columns, not index
df = df.reset_index(names='timestamps')
# get rows that match the conditional join
merged = (df_events
.conditional_join(
df['timestamps'],
('start', 'timestamps', '<='),
('end', 'timestamps','>='),
df_columns='type')
.assign(ones=1)
)
# pivot the merged dataframe and reindex the columns
# reindexing ensures all events are accounted for
pivoted = (merged
.pivot(index='timestamps', columns='type', values='ones')
.reindex(columns=events)
)
# do a left join back to the original dataframe
# and fill nulls
(df
.merge(pivoted, on='timestamps', how='left')
.set_index('timestamps')
.fillna(0)
.astype(int)
)
placeholder football concert public_event
timestamps
2024-03-04 00:00:00 684 0 0 0
2024-03-04 01:00:00 559 0 0 0
2024-03-04 02:00:00 629 0 0 0
2024-03-04 03:00:00 192 0 0 0
2024-03-04 04:00:00 835 0 0 0
... ... ... ... ...
2024-03-09 20:00:00 554 0 0 0
2024-03-09 21:00:00 973 0 0 0
2024-03-09 22:00:00 368 0 0 0
2024-03-09 23:00:00 999 0 0 0
2024-03-10 00:00:00 917 0 0 0
Я думаю, что ваш первоначальный подход близок, но его можно очистить и сделать более эффективным с помощью всего одного цикла for:
import pandas as pd
import numpy as np
np.random.seed(0)
df_idx = pd.date_range(start='2024-03-04', end='2024-03-10', freq='h')
df = pd.DataFrame(data = {'placeholder': np.random.randint(1000,size=len(df_idx))}, index=df_idx)
events = [
['football', '2024-03-04 15:00:00', '2024-03-04 16:59:59'],
['concert', '2024-03-05 19:00:00', '2024-03-05 21:59:59'],
['football', '2024-03-07 18:00:00', '2024-03-07 19:59:59'],
['football', '2024-03-08 18:00:00', '2024-03-08 19:59:59'],
['public_event', '2024-03-09 16:00:00', '2024-03-09 23:59:59']
]
df_events = pd.DataFrame(events, columns=['type', 'start', 'end'])
df_events = df_events.astype({'start': 'datetime64[ns]', 'end': 'datetime64[ns]'})
event_types = sorted(df_events['type'].unique())
df[event_types] = 0
for event_type, start, end in df_events.itertuples(index=False):
df.loc[start:end, event_type] = 1
print(df.loc[df[event_types].any(axis=1)])
placeholder concert football public_event
2024-03-04 15:00:00 472 0 1 0
2024-03-04 16:00:00 600 0 1 0
2024-03-05 19:00:00 755 1 0 0
2024-03-05 20:00:00 797 1 0 0
2024-03-05 21:00:00 659 1 0 0
2024-03-07 18:00:00 779 0 1 0
2024-03-07 19:00:00 430 0 1 0
2024-03-08 18:00:00 660 0 1 0
2024-03-08 19:00:00 227 0 1 0
2024-03-09 16:00:00 816 0 0 1
2024-03-09 17:00:00 861 0 0 1
2024-03-09 18:00:00 387 0 0 1
2024-03-09 19:00:00 610 0 0 1
2024-03-09 20:00:00 554 0 0 1
2024-03-09 21:00:00 973 0 0 1
2024-03-09 22:00:00 368 0 0 1
2024-03-09 23:00:00 999 0 0 1
Вы не против добавить ожидаемый результат?