如何在Pandas时间范围交集计算中处理重复事件?
问题:重复事件导致Pandas时间交集计算返回空DataFrame
我使用Python的Pandas库编写事件数据分析脚本,目标是计算活跃事件的时间交集。当前代码在事件无重复时运行正常,但当同一事件重复出现时,会返回空DataFrame。
原问题代码
import pandas as pd def is_active(df, event, start_date, end_date): """Filters events that are active within a time range.""" filter = (df['event_name'] == event) & ( (df['start_date'] <= end_date) & (df['end_date'] >= start_date) ) return df[filter].shape[0] > 0 def is_not_active(df, event, start_date, end_date): """Filters events that are inactive within a time range.""" filter = (df['event_name'] == event) & ( (df['start_date'] <= end_date) & (df['end_date'] >= start_date) ) return df[filter].empty def generate_active_intersection(df_events, active_events, inactive_events, combination): """Generates a DataFrame with active events and filters inactive ones.""" # Define initial intersection range as the maximum start_date and minimum end_date among active events df_filtered = df_events[df_events['event_name'].isin(active_events)] if df_filtered.empty: return pd.DataFrame() # No common active events max_start_date = df_filtered['start_date'].max() min_end_date = df_filtered['end_date'].min() # Ensure the time range is valid if max_start_date > min_end_date: return pd.DataFrame() # No overlap of all active events # Verify that there is a temporal intersection among all active events for event in active_events: if not is_active(df_events, event, max_start_date, min_end_date): return pd.DataFrame() # No overlap of all active events # Verify that inactive events are NOT active in the same time range for inactive_event in inactive_events: event_without_no = inactive_event.replace("NO_", "") if not is_not_active(df_events, event_without_no, max_start_date, min_end_date): return pd.DataFrame() # Some event that should be inactive is active # Calculate active time in seconds active_time_seconds = (min_end_date - max_start_date).total_seconds() # If all conditions are met, return the common time range with the catalog format return pd.DataFrame({ 'start_date': [max_start_date], 'end_date': [min_end_date], 'catalog': [combination], # Use the original catalog combination 'active_time_seconds': [active_time_seconds] }) def process_catalog(df_events, df_catalog): """Processes each catalog combination and generates the corresponding DataFrames.""" results = [] for index, row in df_catalog.iterrows(): combination = row['catalog'] events = combination.split(', ') active_events = [e for e in events if not e.startswith('NO_')] inactive_events = [e for e in events if e.startswith('NO_')] df_result = generate_active_intersection(df_events, active_events, inactive_events, combination) if not df_result.empty: results.append(df_result) if results: return pd.concat(results, ignore_index=True) else: return pd.DataFrame(columns=['start_date', 'end_date', 'catalog', 'active_time_seconds']) # No valid results
触发问题的示例数据
# Example DataFrames: events = pd.DataFrame({ 'event_name': ['C', 'A', 'B', 'D', 'E', 'A'], # 最后一个F改为A,触发问题 'start_date': pd.to_datetime([ '2023-10-01 09:45:00', '2023-10-01 12:00:00', '2023-10-02 14:30:00', '2023-10-04 16:00:00', '2023-10-05 18:15:00', '2023-10-05 18:20:00' ]), 'end_date': pd.to_datetime([ '2023-10-03 11:30:00', '2023-10-05 18:00:00', '2023-10-06 23:59:59', '2023-10-07 08:45:00', '2023-10-08 20:00:00', '2023-10-08 10:00:00', ]) }) candidates = pd.DataFrame({ 'catalog': ['A, B, C', 'B, C, D', 'A, NO_E, C'] }) # Process and get the results df_results = process_catalog(events, candidates) print(f"Catalog results with start_time, end_time, name and active time in seconds: \n", df_results, "\n")
问题原因
- 初始交集范围计算逻辑错误:当同一事件存在多个时间段时,原代码直接取所有活跃事件的
start_date最大值和end_date最小值,会导致范围无效。比如重复的A事件有一个晚开始时间(2023-10-05 18:20),而C事件的结束时间是2023-10-03 11:30,此时max_start_date > min_end_date,直接返回空DataFrame。 - 未处理同一事件的多时间段合并:原逻辑没有考虑同一事件多个时间段的并集,导致后续验证逻辑无法正确判断事件是否覆盖目标区间。
修复后的代码
import pandas as pd def merge_event_time_ranges(df, event): """Merge overlapping or adjacent time ranges for a single event.""" event_df = df[df['event_name'] == event].sort_values('start_date').reset_index(drop=True) if event_df.empty: return pd.DataFrame(columns=['start_date', 'end_date']) merged = [{'start_date': event_df.iloc[0]['start_date'], 'end_date': event_df.iloc[0]['end_date']}] for _, row in event_df.iloc[1:].iterrows(): last = merged[-1] if row['start_date'] <= last['end_date']: # Overlapping or adjacent, merge them last['end_date'] = max(last['end_date'], row['end_date']) else: merged.append({'start_date': row['start_date'], 'end_date': row['end_date']}) return pd.DataFrame(merged) def get_common_intersection(active_events_ranges): """Calculate the common intersection across all active events' merged time ranges.""" if not active_events_ranges: return None, None # Initialize with the first event's ranges common_starts = [r['start_date'] for r in active_events_ranges[0]] common_ends = [r['end_date'] for r in active_events_ranges[0]] for event_ranges in active_events_ranges[1:]: new_common = [] for cs, ce in zip(common_starts, common_ends): for er_start, er_end in zip(event_ranges['start_date'], event_ranges['end_date']): # Calculate overlap between [cs, ce] and [er_start, er_end] overlap_start = max(cs, er_start) overlap_end = min(ce, er_end) if overlap_start <= overlap_end: new_common.append((overlap_start, overlap_end)) if not new_common: return None, None # Update common ranges for next iteration common_starts, common_ends = zip(*new_common) # Find the overall common intersection (if multiple overlaps exist, take the one that covers all) final_start = max(common_starts) final_end = min(common_ends) return final_start if final_start <= final_end else None, final_end if final_start <= final_end else None def is_event_active_in_range(df, event, start_date, end_date): """Check if the event has any overlap with the target range.""" merged_ranges = merge_event_time_ranges(df, event) for _, row in merged_ranges.iterrows(): if row['start_date'] <= end_date and row['end_date'] >= start_date: return True return False def generate_active_intersection(df_events, active_events, inactive_events, combination): """Generates a DataFrame with active events and filters inactive ones.""" # Get merged time ranges for each active event active_ranges = [] for event in active_events: ranges = merge_event_time_ranges(df_events, event) if ranges.empty: return pd.DataFrame() # Event has no time ranges active_ranges.append(ranges) # Calculate common intersection across all active events common_start, common_end = get_common_intersection(active_ranges) if common_start is None or common_end is None: return pd.DataFrame() # No common intersection # Verify inactive events are not active in the common range for inactive_event in inactive_events: event_name = inactive_event.replace("NO_", "") if is_event_active_in_range(df_events, event_name, common_start, common_end): return pd.DataFrame() # Calculate active time active_time_seconds = (common_end - common_start).total_seconds() return pd.DataFrame({ 'start_date': [common_start], 'end_date': [common_end], 'catalog': [combination], 'active_time_seconds': [active_time_seconds] }) def process_catalog(df_events, df_catalog): """Processes each catalog combination and generates the corresponding DataFrames.""" results = [] for index, row in df_catalog.iterrows(): combination = row['catalog'] events = combination.split(', ') active_events = [e for e in events if not e.startswith('NO_')] inactive_events = [e for e in events if e.startswith('NO_')] df_result = generate_active_intersection(df_events, active_events, inactive_events, combination) if not df_result.empty: results.append(df_result) return pd.concat(results, ignore_index=True) if results else pd.DataFrame(columns=['start_date', 'end_date', 'catalog', 'active_time_seconds'])
修复说明
- 新增
merge_event_time_ranges函数:合并同一事件的重叠或相邻时间段,得到该事件的有效覆盖区间。 - 新增
get_common_intersection函数:计算所有活跃事件合并后区间的公共交集,确保这个区间是所有活跃事件都覆盖的。 - 优化
is_event_active_in_range函数:基于合并后的区间判断事件是否在目标范围内,避免重复计算。 - 调整
generate_active_intersection逻辑:先处理每个事件的多时间段,再求公共交集,最后验证禁用事件条件。
运行修改后的示例数据,会正确返回符合条件的交集结果,比如对于A, B, C组合,会返回2023-10-02 14:30:00到2023-10-03 11:30:00的区间及对应活跃时长。
内容的提问来源于stack exchange,提问作者slow_learner
相关产品推荐
相关产品推荐

