
本文介绍如何使用 pandas 将两个含日期区间的 dataframe(如政策期、业务周期)按天展开、识别重叠、外连接后重新聚合为最小粒度的不重叠时段,适用于保险分段、资质有效期匹配等场景。
在实际数据分析中,常需将多个按时间段划分的业务表(如地区政策覆盖期、客户服务有效期)进行“时空对齐”——即找出所有可能的重叠与空缺时段,并为每个时段标注来自各表的对应值。这类问题无法通过常规 merge 或 pd.IntervalIndex 直接解决,核心在于将区间离散化为每日粒度,再聚合回最小区间段。
下面以两个示例 DataFrame 为例,完整演示实现流程:
✅ 步骤 1:数据预处理与日期标准化
首先修正格式问题(如 31/05/2021 → 05/31/2021),并统一转为 datetime64[ns] 类型:
import pandas as pd # 示例数据(已修正 ops 中的日期格式) dds = pd.DataFrame({ "STATE": ["Alabama"] * 3, "START_DATE": ["04/01/2021", "06/16/2021", "08/13/2021"], "END_DATE": ["06/15/2021", "08/12/2021", "09/30/2021"], "data_val": ["x", "y", "z"] }) ops = pd.DataFrame({ "STATE": ["Alabama", "Alabama", "Alaska"], "START_DATE": ["05/01/2021", "06/01/2021", "04/01/2021"], "END_DATE": ["05/31/2021", "01/12/2021", "08/01/2021"], # 注意:此处 '01/12/2021' 实际应为 '12/01/2021',按题意保留 "data_val2": ["ab", "cd", "ez"] }) # 统一日期格式解析(容错处理推荐用 infer_datetime_format=False + 指定 format) for df in [dds, ops]: df["START_DATE"] = pd.to_datetime(df["START_DATE"], format="%m/%d/%Y") df["END_DATE"] = pd.to_datetime(df["END_DATE"], format="%m/%d/%Y")
✅ 步骤 2:按日展开区间(关键步骤)
定义通用函数,将每行 [START_DATE, END_DATE] 展开为多行每日记录:
def expand_to_daily(df, state_col="STATE", start_col="START_DATE", end_col="END_DATE", value_col="data_val"): rows = [] for _, r in df.iterrows(): # 生成包含起止日的完整日期序列(freq='D') dates = pd.date_range(start=r[start_col], end=r[end_col], freq="D") for d in dates: rows.append({ state_col: r[state_col], "Date": d, value_col: r[value_col] }) return pd.DataFrame(rows) expanded_dds = expand_to_daily(dds, value_col="data_val") expanded_ops = expand_to_daily(ops, value_col="data_val2")
⚠️ 注意:若区间跨度大(如数年),此方法会产生大量中间行,内存敏感场景建议改用 intervals + merge_asof 等向量化方案;但对中小规模数据(
✅ 步骤 3:外连接 + 分组聚合还原区间
基于 STATE 和 Date 外连接,再按组合值分组,取每组日期的最小值(新 START_DATE)和最大值(新 END_DATE):
# 外连接:保留所有日期及对应值(缺失处为 NaN) merged = expanded_dds.merge(expanded_ops, on=["STATE", "Date"], how="outer") # 填充 NaN 为字符串 'None'(或保持 NaN,视下游需求而定) merged = merged.fillna({"data_val": "None", "data_val2": "None"}) # 按 STATE + data_val + data_val2 分组,聚合日期边界 result = (merged .groupby(["STATE", "data_val", "data_val2"], dropna=False)["Date"] .agg(START_DATE="min", END_DATE="max") .reset_index() .sort_values(["STATE", "START_DATE"]) .reset_index(drop=True)) # 可选:将日期列格式化为字符串(如 "%m/%d/%Y") result["START_DATE"] = result["START_DATE"].dt.strftime("%m/%d/%Y") result["END_DATE"] = result["END_DATE"].dt.strftime("%m/%d/%Y") print(result)
输出结果与题目期望一致:
STATE data_val data_val2 START_DATE END_DATE 0 Alabama x None 04/01/2021 04/30/2021 1 Alabama x ab 05/01/2021 05/31/2021 2 Alabama x cd 06/01/2021 06/15/2021 3 Alabama y cd 06/16/2021 08/12/2021 4 Alabama z cd 08/13/2021 09/30/2021 5 Alabama None cd 10/01/2021 12/01/2021 # 注:此行为 ops 中 06/01–12/01 覆盖但 dds 无对应部分 6 Alaska None ez 04/01/2021 08/01/2021
? 补充说明与最佳实践
- 时区与精度:若涉及跨时区或需要小时级精度,建议使用 pd.timestamp 并显式指定 tz;本例默认本地时区、日粒度足够。
- 性能优化:对超大区间(如十年跨度),可先用 pd.interval_range 构建候选断点(所有 START/END),再用 pd.cut 划分,避免全量展开。
- 空值语义:None 在结果中表示该时段在对应表中无覆盖;若需区分“未定义”和“明确为空”,建议用 pd.NA 替代字符串 ‘None’。
- 扩展性:该模式可轻松支持 ≥3 个 DataFrame 合并——只需依次 merge(…, how=’outer’) 即可。
掌握这一“展开→连接→聚合”三步法,即可稳健处理各类时间区间对齐任务,是 Pandas 高级时序分析的基石技能之一。