You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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")

问题原因

  1. 初始交集范围计算逻辑错误:当同一事件存在多个时间段时,原代码直接取所有活跃事件的start_date最大值和end_date最小值,会导致范围无效。比如重复的A事件有一个晚开始时间(2023-10-05 18:20),而C事件的结束时间是2023-10-03 11:30,此时max_start_date > min_end_date,直接返回空DataFrame。
  2. 未处理同一事件的多时间段合并:原逻辑没有考虑同一事件多个时间段的并集,导致后续验证逻辑无法正确判断事件是否覆盖目标区间。

修复后的代码

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

修复说明

  1. 新增merge_event_time_ranges函数:合并同一事件的重叠或相邻时间段,得到该事件的有效覆盖区间。
  2. 新增get_common_intersection函数:计算所有活跃事件合并后区间的公共交集,确保这个区间是所有活跃事件都覆盖的。
  3. 优化is_event_active_in_range函数:基于合并后的区间判断事件是否在目标范围内,避免重复计算。
  4. 调整generate_active_intersection逻辑:先处理每个事件的多时间段,再求公共交集,最后验证禁用事件条件。

运行修改后的示例数据,会正确返回符合条件的交集结果,比如对于A, B, C组合,会返回2023-10-02 14:30:00到2023-10-03 11:30:00的区间及对应活跃时长。

内容的提问来源于stack exchange,提问作者slow_learner

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 11:24:54