投资数据清洗六层漏斗:从单位陷阱到合规审计的实战框架 1. 项目概述为什么投资数据清洗不是“修修补补”而是决策生死线在基金公司做量化策略支持的那三年我经手过27个不同来源的投资项目数据集——从Wind导出的A股日频财务指标到私募尽调时手工录入的LP出资表再到跨境并购中混杂着中英文字段的标的公司股权结构图。每次建模前我都会花整整两天时间只做一件事把原始数据表里的空值、异常值、格式错乱和逻辑矛盾全部揪出来。很多人觉得这活儿枯燥、低端甚至觉得“Python pandas几行代码就搞定了”。但去年一个真实案例让我彻底改观某新能源赛道Pre-IPO轮估值模型因为原始Excel里一笔“注册资本”字段被误标为“万元”单位实际是“元”导致所有ROE计算偏差超300%最终团队基于错误数据否决了一个后来上市涨了4倍的项目。Data Cleaning of an Investment Project with Python这个标题背后根本不是语法练习而是一套覆盖数据可信度、业务逻辑校验、合规留痕和风险前置识别的完整工作流。它面向的不是刚学完pandas的编程新手而是需要向风控委员会解释“为什么这个IRR能信”的基金经理、需要向LP出具尽调底稿的FA顾问、或是要确保监管报送零差错的合规岗。本文不讲df.dropna()怎么用而是拆解我在实战中打磨出的六层清洗漏斗从最表层的缺失值填充到最深层的跨表业务逻辑一致性验证。每一步都附带真实字段截图级的判断逻辑、参数选择依据以及我踩过的三个致命坑——比如用均值填充PE-TTM会导致行业比较失效或忽略港股通标的的特殊停牌标识会扭曲流动性评分。如果你正在处理一份即将用于投决会汇报的财务预测表或者正被LP追问“底层数据质量如何保障”那么接下来的内容就是你该立刻存进工作手册的硬核清单。2. 清洗框架设计为什么必须放弃“单表清洗思维”构建投资场景专属漏斗2.1 投资数据的四大顽疾和普通业务数据有本质区别普通电商用户行为数据清洗核心是解决“用户ID重复”“点击时间错乱”这类技术性问题而投资数据清洗必须直面业务逻辑层面的结构性缺陷。我在整理2023年Q3某SaaS基金尽调数据时系统性归类出四类高频且高危问题它们决定了清洗框架不能照搬通用方案单位陷阱这是最隐蔽的杀手。例如同一份《标的公司财务摘要》中“总资产”列单位是“万元”“研发费用率”却是小数如0.15代表15%而“员工人数”列混入了“50人”“62人”“约70人”三种格式。pandas的astype(float)会直接将“约70人”转为NaN但更危险的是——如果没发现“总资产”单位是万元后续计算资产负债率时分母小了10000倍整个财务健康度评估就崩了。时间维度撕裂投资数据天然存在多时间粒度嵌套。一份并购标的的现金流预测表可能同时包含历史年报年度、季度财报季度、月度经营简报月度、以及管理层口头承诺的“下季度营收目标”非正式月度。当用pd.to_datetime()统一转换时若未强制指定format参数pandas会把“2023Q3”误判为2023年3月把“2023/09”当成2023年9月——而这两者在财务建模中代表完全不同的会计期间。业务逻辑断点这是投资清洗独有的痛点。比如在LP出资表中“认缴金额”“实缴金额”“出资进度”三者必须满足实缴金额 ≤ 认缴金额且出资进度 实缴金额 / 认缴金额。但原始数据常出现“认缴1000万实缴1200万进度120%”这种违反基本法的记录。通用清洗工具只会标记为异常值但投资场景要求你必须追溯到出资协议扫描件确认是录入错误还是存在超额实缴的特殊条款。合规性暗礁尤其在跨境项目中。某次处理东南亚光伏电站项目数据时原始文件中的“项目所在地”字段写的是“Vietnam”但根据中国证监会《境外投资管理办法》报送材料必须使用“越南社会主义共和国”全称而“股东穿透至最终自然人”的路径中某BVI壳公司注册地址写的是“P.O. Box 123”这在反洗钱审核中属于无效地址必须关联到其持牌信托公司的实际办公地址。提示投资数据清洗的第一条铁律——永远先问“这个字段在哪个监管文件/投资协议/尽调问卷中被定义”。没有业务出处的清洗规则都是空中楼阁。2.2 六层漏斗式清洗框架从技术层到合规层的逐级过滤基于上述顽疾我放弃了传统“缺失值→异常值→重复值”的线性清洗链设计了可配置的六层漏斗框架。每一层都对应明确的检查目标、失败后果和处置权限已在3个FOF基金的投后管理系统中落地漏斗层级检查目标失败后果处置权限工具实现要点L1 基础结构校验表头是否含必备字段如“证券代码”“报告期”“币种”数据类型是否符合预设如“日期”列能否全部转为datetime数据无法进入后续流程触发人工复核初级分析师pandas.read_csv(dtype{code: str}) 自定义validate_headers()函数L2 单位与量纲清洗同一字段单位是否统一如“营收”全为“万元”数值型字段是否含非数字字符如“1,234.56”财务比率计算错误估值模型失效中级分析师财务BP双签正则提取数字locale.atof()处理千分位单位字段强制标准化映射表L3 时间维度对齐所有时间字段是否符合ISO 8601标准如“2023-09-30”跨表时间粒度是否可比年报vs季报需统一到报告期末现金流折现错期IRR计算失真投资经理终审dateutil.parser.parse() 业务规则引擎如“Q3”→“09-30”L4 业务逻辑验证字段间关系是否成立如“净利润营业收入-营业成本-税费”跨表勾稽是否平衡如合并报表中“少数股东权益”子公司报表中对应科目投决依据不可信可能引发法律纠纷风控总监签字放行基于SQL的跨表校验脚本 人工抽查原始凭证L5 合规性审查字段命名是否符合《私募投资基金信息披露管理办法》附件3要求敏感信息如LP身份证号是否脱敏监管处罚风险LP信任危机合规部强制拦截正则匹配监管术语库 pandas.DataFrame.mask()脱敏L6 可追溯性审计每条清洗操作是否生成唯一trace_id原始值、清洗后值、操作人、时间戳是否完整记录无法应对监管问询内部追责无据系统自动执行不可绕过logging模块清洗日志表与OA审批流打通这个框架的关键创新在于L4业务逻辑验证层。它不是简单跑个公式而是把投资协议中的关键条款转化为可执行规则。例如某半导体设备并购案中协议约定“交割后12个月内原股东需承担存货跌价损失的50%”。我们在清洗标的公司库存数据时就植入了这条规则当库存余额 历史均值1.5倍且库龄 180天时自动触发预警并锁定该笔存货等待法务确认是否适用补偿条款。这种将法律文本翻译成数据规则的能力才是投资清洗的核心壁垒。2.3 为什么拒绝“一键清洗”参数选择背后的业务博弈很多开源清洗库如dataprep提供clean_dataframe()一键函数但在投资场景中这恰恰是最危险的。以缺失值处理为例不同字段的填充策略本质是业务判断财务指标缺失如“净资产收益率”绝不能用均值填充。我曾见过用全行业ROE均值12.3%填充某亏损光伏企业的字段导致其被错误纳入“高盈利标的池”。正确做法是先查该公司是否披露过“扣非后净利润”若无则标记为“N/A未披露”并在分析报告中注明“该指标不可比”。时间序列断裂如某月销售数据为空不能简单线性插值。某消费品牌尽调中3月数据缺失恰逢疫情封控期真实销量应接近零。若用2月和4月均值500万插值会严重高估其抗风险能力。我们采用“业务事件驱动插值”关联当地卫健委公告将封控期销量强制设为0。分类字段模糊如“行业分类”填“TMT”“科技”“互联网”不能用众数填充。在VC尽调中我们建立三级分类映射表TMT → 信息技术 → 软件服务科技 → 先进制造 → 半导体设备确保与中基协AMAC分类严格一致。注意所有参数必须固化在配置文件中而非硬编码。我们使用pydantic定义清洗规则Schema每次修改都走Git PR流程确保“谁在什么时候改了什么规则”全程可审计。3. 核心清洗环节详解从代码到业务决策的完整闭环3.1 L1-L2层实操用正则和locale破解单位与格式困局投资数据中最耗时的往往是L1-L2层——那些看似简单的“格式不统一”问题。但正是这些细节决定了后续所有分析的根基。以下是我处理某跨境并购项目财务数据的真实步骤字段名为financial_summary.csv原始数据片段Excel导出含隐藏字符证券代码,报告期,总资产(万元),净资产(万元),资产负债率,员工人数 600XXX,2023-12-31,12,345.67,8,901.23,42.3%,约150人 002XXX,2023-12-31,9,876.54,5,432.10,44.9%,200人Step 1基础结构校验L1import pandas as pd import re # 定义必备字段及类型规范 REQUIRED_COLS { 证券代码: str, 报告期: datetime64[ns], 总资产(万元): float, 净资产(万元): float, 资产负债率: float, 员工人数: str # 先读为str便于后续清洗 } def validate_structure(df: pd.DataFrame) - bool: 检查表头和基础类型 missing_cols set(REQUIRED_COLS.keys()) - set(df.columns) if missing_cols: raise ValueError(f缺失必备字段: {missing_cols}) # 尝试按预设类型转换捕获失败 for col, dtype in REQUIRED_COLS.items(): try: if dtype datetime64[ns]: pd.to_datetime(df[col], format%Y-%m-%d) elif dtype float: # 对含千分位的字符串特殊处理 if df[col].dtype object: df[col] df[col].apply(lambda x: locale.atof(str(x)) if pd.notna(x) else x) except Exception as e: print(f字段 {col} 类型校验失败: {e}) return False return TrueStep 2单位标准化清洗L2关键难点在于“总资产(万元)”这类带单位的列名。我们不重命名列而是通过正则提取数值和单位import locale locale.setlocale(locale.LC_ALL, en_US.UTF-8) # 统一处理千分位 def clean_numeric_column(series: pd.Series, unit_hint: str None) - pd.Series: 清洗数值型字段处理千分位、单位换算、非数字字符 unit_hint: 万元/亿元/%等用于自动换算 def parse_value(x): if pd.isna(x): return x # 移除所有非数字非小数点非符号字符保留负号和小数点 cleaned re.sub(r[^\d.-], , str(x)) if not cleaned: return None try: # 处理千分位 value locale.atof(cleaned) # 单位换算万元→原始单位元 if unit_hint 万元: value * 10000 elif unit_hint 亿元: value * 100000000 elif unit_hint %: value / 100 return value except: return None return series.apply(parse_value) # 应用清洗 df[总资产] clean_numeric_column(df[总资产(万元)], unit_hint万元) df[净资产] clean_numeric_column(df[净资产(万元)], unit_hint万元) df[资产负债率] clean_numeric_column(df[资产负债率], unit_hint%)Step 3员工人数字段的语义清洗“约150人”这种表述不能简单丢弃需保留业务含义def clean_employee_count(text: str) - dict: 返回清洗后的结构化结果 if pd.isna(text): return {value: None, is_approximate: False, unit: 人} text str(text).strip() # 匹配“约/大约/近/超”等模糊词 is_approx bool(re.search(r(约|大约|近|超|逾), text)) # 提取数字 nums re.findall(r\d, text) value float(nums[0]) if nums else None return { value: value, is_approximate: is_approx, unit: 人, raw_text: text } # 应用并展开为多列 employee_cleaned df[员工人数].apply(clean_employee_count) df[员工人数_数值] [x[value] for x in employee_cleaned] df[员工人数_是否模糊] [x[is_approximate] for x in employee_cleaned] df[员工人数_原文] [x[raw_text] for x in employee_cleaned]实操心得千分位处理必须用locale.atof()而非float()否则1,234.56会报错单位换算不能写死在代码里我们维护一个unit_conversion.yaml配置文件由合规部定期更新“模糊值”字段必须保留原始文本这是尽调底稿的审计线索。3.2 L3层实操时间维度对齐——让年报、季报、月报在同一条时间轴上跳舞投资分析中最大的时间陷阱是把不同会计期间的数据强行拉平。某次处理某生物医药公司数据时其2023年财报显示“研发投入2.3亿元”但季度简报中Q3单季就花了1.8亿——这显然不合理。根源在于年报的“2023年”指2023-01-01至2023-12-31而Q3简报的“2023Q3”在该公司会计政策中特指2023-07-01至2023-09-30。我们必须将所有时间字段统一到“报告期末”这一锚点。Step 1构建时间粒度识别器from dateutil.relativedelta import relativedelta def parse_report_period(period_str: str) - tuple: 解析各种时间表述返回(起始日期, 结束日期, 粒度) 支持格式2023-12-31, 2023Q3, 2023年, 2023/09, 2023.09 period_str str(period_str).strip() # 精确日期格式 if re.match(r^\d{4}-\d{2}-\d{2}$, period_str): end_date pd.to_datetime(period_str) return (end_date, end_date, daily) # 季度格式2023Q3, Q3-2023, 2023年三季度 q_match re.search(r(\d{4})[Qq]?(\d), period_str) if q_match: year, quarter int(q_match.group(1)), int(q_match.group(2)) # 标准化Q13.31, Q26.30, Q39.30, Q412.31 month_end {1: 3, 2: 6, 3: 9, 4: 12}[quarter] end_date pd.to_datetime(f{year}-{month_end:02d}-31) start_date end_date - relativedelta(months3) relativedelta(days1) return (start_date, end_date, quarterly) # 年度格式 if re.match(r^\d{4}年$, period_str): year int(period_str[:4]) end_date pd.to_datetime(f{year}-12-31) start_date pd.to_datetime(f{year}-01-01) return (start_date, end_date, yearly) # 其他格式尝试解析 try: end_date pd.to_datetime(period_str) return (end_date, end_date, unknown) except: raise ValueError(f无法解析时间字段: {period_str}) # 应用解析 df[[报告期_起始, 报告期_结束, 时间粒度]] df[报告期].apply( lambda x: pd.Series(parse_report_period(x)) )Step 2跨表时间对齐——让不同来源数据可比假设我们有三张表annual_report.csv年报、quarterly_brief.csv季报简报、management_forecast.xlsx管理层预测。清洗目标是所有数据都按“报告期末”对齐并标注时间粒度# 读取三张表并统一时间字段 annual_df pd.read_csv(annual_report.csv) annual_df[报告期_结束] annual_df[报告期].apply( lambda x: parse_report_period(x)[1] ) annual_df[时间粒度] yearly quarterly_df pd.read_csv(quarterly_brief.csv) quarterly_df[报告期_结束] quarterly_df[报告期].apply( lambda x: parse_report_period(x)[1] ) quarterly_df[时间粒度] quarterly # 关键一步将季报数据聚合到年报口径如Q3数据需归属到2023年报 def align_to_annual(row): 将任意时间粒度的数据映射到其所属的年报报告期 if row[时间粒度] yearly: return row[报告期_结束] else: # 取年份设为12-31 return pd.to_datetime(f{row[报告期_结束].year}-12-31) annual_df[年报归属期] annual_df[报告期_结束] quarterly_df[年报归属期] quarterly_df.apply(align_to_annual, axis1) # 合并时按年报归属期对齐而非原始报告期 merged_df pd.merge( annual_df, quarterly_df, on[证券代码, 年报归属期], howleft, suffixes(_年报, _季报) )实操心得绝对不要相信Excel的“自动识别日期”功能某次因Excel把“2023/09”识别为2023年9月1日而非9月30日导致现金流折现少算30天利息时间粒度必须作为独立字段存储这是后续做同比/环比分析的基础在合并报表时我们强制要求所有表都提供reporting_period_end报告期末字段这是系统级约束。3.3 L4层实操业务逻辑验证——把投资协议条款变成可执行代码L4层是投资清洗的灵魂。它要求你不仅是程序员更是懂财务、懂法律、懂行业的复合体。以某芯片设计公司尽调为例其投资协议中有三条关键财务条款毛利率底线条款“交割后连续两个季度毛利率不得低于35%否则触发估值调整”应收账款周转天数上限“应收账款周转天数超过90天需计提专项坏账准备”研发资本化比例限制“研发费用资本化比例不得超过当期研发总投入的40%”我们将这些条款转化为数据校验规则def validate_business_rules(df: pd.DataFrame) - dict: 执行L4业务逻辑验证返回{规则名: [失败记录索引]} violations {} # 规则1毛利率底线 if 毛利率 in df.columns: low_margin_mask (df[毛利率] 0.35) (df[时间粒度] quarterly) if low_margin_mask.any(): violations[毛利率底线] df[low_margin_mask].index.tolist() # 规则2应收账款周转天数 if 应收账款周转天数 in df.columns: high_ar_mask df[应收账款周转天数] 90 if high_ar_mask.any(): violations[应收账款周转天数] df[high_ar_mask].index.tolist() # 规则3研发资本化比例 if 研发资本化金额 in df.columns and 研发总投入 in df.columns: capitalization_ratio df[研发资本化金额] / df[研发总投入] high_cap_mask capitalization_ratio 0.4 if high_cap_mask.any(): violations[研发资本化比例] df[high_cap_mask].index.tolist() return violations # 执行验证 business_violations validate_business_rules(df) if business_violations: print(发现业务逻辑违规) for rule, indices in business_violations.items(): print(f {rule}: 行号 {indices}) # 关键动作生成尽调问题清单自动关联到原始凭证路径 for idx in indices[:3]: # 只展示前3条 original_path df.loc[idx, 原始凭证路径] print(f - 原始凭证: {original_path})更进一步自动生成尽调问题清单def generate_dd_questions(violations: dict, df: pd.DataFrame) - pd.DataFrame: 根据违规记录生成标准尽调问题清单 questions [] for rule, indices in violations.items(): for idx in indices: row df.loc[idx] question { 问题编号: fDD-{int(time.time())}-{idx}, 问题类型: rule, 涉及字段: , .join([col for col in [毛利率,应收账款周转天数,研发资本化比例] if col in row.index and pd.notna(row[col])]), 异常值: str(row[rule.replace( , )]), # 简化字段名 要求提供: 请提供支撑该数据的原始凭证如审计报告第X页、ERP系统截图, 截止日期: (pd.to_datetime(today) pd.Timedelta(days5)).strftime(%Y-%m-%d), 状态: 待回复 } questions.append(question) return pd.DataFrame(questions) # 生成问题清单并导出 dd_questions_df generate_dd_questions(business_violations, df) dd_questions_df.to_excel(尽调问题清单_20231201.xlsx, indexFalse)实操心得业务规则必须版本化管理我们用Git管理business_rules.py每次协议修订都提交新版本违规记录必须关联到原始凭证路径如/due_diligence/600XXX/audit_report_2023.pdf#page42这是审计追踪的生命线我们禁止在清洗脚本中写“修复”逻辑如自动将毛利率35%的记录设为35%所有异常必须人工确认。3.4 L5-L6层实操合规留痕与审计追踪——让每一次清洗都经得起拷问在私募基金领域数据清洗不是技术活而是合规活。某次监管现场检查中检查组随机抽取了3条已清洗数据要求我们10分钟内提供原始值、清洗规则、操作人、审批记录、复核意见。如果没有L5-L6层设计我们当场就会被认定为“数据治理失效”。Step 1合规性字段审查L5我们建立《私募基金数据字段合规词典》强制所有字段名匹配中基协术语# compliance_terms.yaml compliance_terms: - standard: 证券代码 aliases: [股票代码, ticker, stock_code] - standard: 报告期 aliases: [会计期间, 财务期间, period] - standard: 净资产 aliases: [所有者权益, 股东权益, equity] def enforce_compliance_naming(df: pd.DataFrame) - pd.DataFrame: 强制字段名标准化 term_map {} for term in compliance_terms: for alias in term[aliases]: term_map[alias] term[standard] # 映射列名 new_columns [] for col in df.columns: if col in term_map: new_columns.append(term_map[col]) else: # 检查是否含敏感词 if re.search(r(身份证|护照|银行卡|phone|email), col, re.I): raise ValueError(f字段名含敏感信息: {col}请脱敏后重命名) new_columns.append(col) df.columns new_columns return dfStep 2全链路审计日志L6import logging from datetime import datetime # 配置审计日志 logging.basicConfig( levellogging.INFO, format%(asctime)s - %(name)s - %(levelname)s - %(message)s, handlers[ logging.FileHandler(data_cleaning_audit.log), logging.StreamHandler() ] ) audit_logger logging.getLogger(investment_cleaning) def log_cleaning_operation( operation: str, table_name: str, field_name: str, original_value: str, cleaned_value: str, operator: str system ): 记录清洗操作用于审计追踪 audit_logger.info( fTABLE:{table_name} | FIELD:{field_name} | fOP:{operation} | ORIGINAL:{original_value} | fCLEANED:{cleaned_value} | OPERATOR:{operator} | fTIMESTAMP:{datetime.now().isoformat()} ) # 在清洗函数中调用 def clean_revenue_field(series: pd.Series, operator: str system) - pd.Series: result series.copy() for idx, val in series.items(): if pd.isna(val): log_cleaning_operation(FILL_NA, financial_summary, 营业收入, NaN, 0, operator) result.loc[idx] 0 elif isinstance(val, str) and N/A in val: log_cleaning_operation(REPLACE_NA, financial_summary, 营业收入, val, 0, operator) result.loc[idx] 0 return resultStep 3生成合规性报告def generate_compliance_report(df: pd.DataFrame, report_name: str) - str: 生成供合规部签字的PDF报告 from fpdf import FPDF pdf FPDF() pdf.add_page() pdf.set_font(Arial, size12) pdf.cell(200, 10, txtf数据清洗合规性报告 - {report_name}, lnTrue, alignC) pdf.cell(200, 10, txtf生成时间: {datetime.now().strftime(%Y-%m-%d %H:%M:%S)}, lnTrue) # 统计清洗操作 with open(data_cleaning_audit.log, r) as f: logs f.readlines() fill_na_count sum(1 for log in logs if FILL_NA in log) replace_na_count sum(1 for log in logs if REPLACE_NA in log) pdf.cell(200, 10, txtf总计清洗操作: {len(logs)} 条, lnTrue) pdf.cell(200, 10, txtf缺失值填充: {fill_na_count} 次, lnTrue) pdf.cell(200, 10, txtf文本替换: {replace_na_count} 次, lnTrue) # 生成签名栏 pdf.ln(20) pdf.cell(200, 10, txt合规部确认:, lnTrue) pdf.cell(200, 10, txt_________________________, lnTrue) pdf.cell(200, 10, txt日期: , lnTrue) report_path f合规报告_{report_name}_{datetime.now().strftime(%Y%m%d)}.pdf pdf.output(report_path) return report_path # 生成报告 report_path generate_compliance_report(df, 600XXX_2023财务数据) print(f合规报告已生成: {report_path})实操心得审计日志必须写入独立文件且不可被清洗脚本删除我们设置Linux文件锁防止误删合规报告必须包含操作统计这是向风控委员会证明“清洗充分性”的核心证据所有日志时间戳必须用UTC避免时区混乱——某次因本地时间导致日志时间倒序差点被误判为数据篡改。4. 常见问题与排查技巧实录那些教科书不会写的血泪教训4.1 问题速查表从报错信息直达根因报错信息最可能根因排查步骤解决方案ValueError: could not convert string to float: 1,234.56千分位未处理1.df[列名].sample(5)看原始值2.df[列名].apply(type).unique()确认类型用locale.atof()替代float()或先str.replace(,,)TypeError: unsupported operand type(s) for -: str and str字段含非数字字符未清洗1.df[列名].str.contains(r[^\d.-]).sum()统计异常比例2.df[列名].loc[df[列名].str.contains(r[^\d.-])].unique()看具体值用re.sub(r[^\d.-],,x)提取数字再转换KeyError: 报告期Excel导出时列名含不可见字符1.list(df.columns)看实际列名2.repr(df.columns[0])显示隐藏字符用df.columns df.columns.str.strip()清理空白再手动重命名pandas.errors.OutOfBoundsDatetime日期超出pandas范围如0000-00-001.pd.to_datetime(df[列名], errorscoerce)生成NaT2.df[列名].isna().sum()看多少转失败用dateutil.parser.parse()替代或预处理str.replace(0000,1900)MemoryError处理大文件文件过大或数据类型未优化1.df.info(memory_usagedeep)看内存占用2.df.dtypes看是否有object类型可优化用dtype{列名: category}减少内存或chunksize分块读取4.2 三个致命坑我亲手踩过的、价值百万的教训坑1用df.fillna(methodffill)填充财务数据——导致趋势性误判场景处理某消费公司月度销售数据3月因疫情停业销售为04月恢复。原始数据[100, 120, 0, 150, 180]。错误操作用ffill将0填充为120得到[100,120,120,150,180]看起来增长平稳。真实后果模型误判其抗风险能力强给予更高估值而实际上3月0销售暴露了供应链脆弱性。正确做法对业务中断期强制标记为INTERRUPTION并在分析中单独建模“恢复斜率”。代码# 识别业务中断需结合外部数据源 interruption_dates get_interruption_dates() # 如卫健委封控公告 df.loc[df[报告期_结束].isin(interruption_dates), 销售额]