1. 这不是“学完就能画折线图”的速成课而是你真正搞懂时间序列分析的起点“Time Series Data Analysis In Python”——光看这个标题很多人第一反应是哦pandas读csv、matplotlib画个折线图、再套个statsmodels的ARIMA模型跑一下。我带过十几期数据分析训练营每期都有至少三分之一的学员卡在这一步代码能跑通结果也出来但一问“为什么选ARIMA不选Prophet”、“残差图里那堆锯齿说明什么”、“这个AIC值从237降到211到底改善了多少实际预测能力”就全愣住了。这不是他们不努力而是市面上90%的教程只教“怎么做”却跳过了最关键的“为什么必须这么做”。时间序列不是静态数据的简单延伸它自带三重枷锁时间依赖性今天的销量和昨天强相关、趋势惯性连续三个月增长后大概率继续涨、周期嵌套性周内工作日/周末波动季度促销波动年度春节效应。Python生态里那些看似随手可调的函数——pd.Series.rolling().mean()、sm.tsa.adfuller()、sktime.Forecaster——背后全是统计学百年沉淀下来的数学契约。比如adfuller()返回的p值它不是“越小越好”的模糊指标而是严格对应着“原假设序列存在单位根即非平稳”的拒绝概率p0.049和p0.051在统计意义上就是天壤之别前者你能放心做差分建模后者强行建模等于在流沙上盖楼。这篇内容专为两类人准备一类是已经用过plotly.express.line()但面对业务方追问“下个月销量区间预测为什么是±15%而不是±8%”时答不上来的实战派另一类是刚学完《Python编程入门》、正对着y_train, y_test train_test_split(data, test_size0.2)发懵的新手——因为时间序列根本不能用随机切分我把过去八年在电商销量预测、IoT设备故障预警、金融风控逾期率建模中踩过的所有坑、验证过的每一条参数选择逻辑、甚至Jupyter里被我反复注释掉又恢复的调试代码段全部拆解成可复现的步骤。你不需要记住所有公式但你会清楚知道当seasonal_decompose()画出的季节图出现“毛刺”时该先检查采样频率还是先处理异常值当LSTM模型在验证集上loss震荡而测试集MAPE飙升时问题大概率出在TimeSeriesSplit的步长设置而非网络结构。这是一份写给真实战场的说明书不是实验室里的理想化演示。2. 项目整体设计与思路拆解为什么必须放弃“先建模再诊断”的线性思维2.1 时间序列分析的本质是“因果链逆向工程”不是函数调用流水线绝大多数初学者把时间序列分析理解成一个标准流程加载数据→画图→检验平稳性→差分→拟合模型→评估指标。这种线性思维在真实项目中会直接导致灾难性后果。我在2022年帮一家冷链物流公司优化冷库温度预警系统时团队最初就按这个流程走用adfuller()检验发现温度序列p值0.120.05于是果断做一阶差分再用SARIMA拟合最终AIC降到-186看起来很美。但上线后误报率飙升300%——因为差分操作粗暴抹掉了温度变化的物理意义冷库压缩机启停时的瞬时温升约0.8℃/分钟本就是关键预警信号而一阶差分后这个特征被稀释成微弱噪声。问题根源在于我们把统计学工具当成了黑箱却忘了时间序列分析的第一原则所有数学操作必须可解释、可回溯、可业务对齐。真正的分析路径应该是环形闭环业务锚定明确核心目标是“提前15分钟预警压缩机异常升温”而非“最小化RMSE”物理约束注入温度变化受热力学定律约束升温速率不可能超过1.2℃/分钟诊断驱动建模先用stl()分解观察趋势项是否含突变点对应压缩机启停再决定是否对原始序列做阈值截断而非全局差分可逆性验证模型输出必须能反推原始温度值确保预警阈值如“未来15分钟温度将超-18℃”可直接映射到设备控制指令。这个闭环设计直接决定了技术选型。比如为什么不用Prophet因为它默认的季节性傅里叶项会平滑掉压缩机启停产生的尖峰而我们的业务恰恰需要捕捉这种瞬态特征。为什么坚持用statsmodels而非纯深度学习方案因为它的get_prediction()方法能输出完整的预测分布让我们能计算“温度超限概率95%”的置信预警这是业务方真正需要的决策依据。2.2 工具链选型不是“最新最火”而是“最可控、最透明、最易调试”Python时间序列生态看似繁荣实则暗藏陷阱。我见过太多团队在darts、sktime、gluonts之间反复切换最后发现连基础的滚动预测rolling forecast都实现不了。选型的核心逻辑只有一条能否在5分钟内定位到某次预测偏差的具体原因。基于此我的生产环境工具链是经过千次压测的黄金组合工具核心不可替代性实战避坑点pandas1.5.resample(15T).mean()处理不规则采样时必须用originstart_day避免时间偏移pd.Grouper(keytimestamp, freqW-MON)比df.groupby(df.index.week)更精准匹配业务周定义升级到2.0后.shift()行为变更旧代码中df[lag1] df[value].shift(1)需改为df[lag1] df[value].shift(1, fill_valuedf[value].iloc[0])否则首行变NaNstatsmodels0.14SARIMAX的enforce_stationarityFalse参数是救命稻草——当业务要求保留原始量纲如销售额万元时强制平稳性会导致模型拒绝收敛关掉它并手动添加exog变量如节假日标记更稳妥adfuller()的maxlags参数必须设为int(12*(nobs/100)**(1/4))Schwert准则硬设maxlags10在高频数据如秒级上会漏检高阶自相关scikit-learn1.3TimeSeriesSplit的test_size参数名极具误导性——它实际控制的是每次分割的测试集长度而非比例真实场景中必须用gap参数预留7天缓冲区否则会用“昨天的数据”预测“今天”违背时间不可逆性make_column_transformer()处理多源特征时remainderpassthrough必须显式声明否则Pipeline会静默丢弃未指定列导致线上预测维度错乱plotly5.18px.line()的line_shapehv阶梯线比默认linear更能体现物联网设备的离散状态变化update_traces(hovertemplateb%{x}/bbrValue: %{y:.2f}extra/extra)定制悬停模板让业务方一眼看到精确数值而非科学计数法导出HTML时务必加config{scrollZoom: True, displayModeBar: False}否则移动端双指缩放会触发意外交互这个组合的底层逻辑是每个工具只解决一个明确问题且所有中间结果如SARIMAXResults.resid残差、TimeSeriesSplit.split()返回的索引数组都能直接打印、绘图、存盘。当某次预测突然失效时我能用3行代码定位到是resample()插值算法导致的相位偏移而不是在darts的抽象层里翻三天源码。2.3 数据预处理90%的模型失败源于对“时间”本身的误解新手常犯的致命错误是把时间戳当成普通字符串或整数处理。2023年我审计过某银行信用卡逾期率模型发现其train_test_split()随机打乱了时间索引导致模型用2023年Q4数据“预测”2023年Q1——这在统计学上叫“未来信息泄露”模型表现虚高但上线即崩。真正的预处理必须直面三个时间本质问题第一时间粒度不是技术选择而是业务契约。某生鲜平台要求“每小时生成次日各仓配货量预测”但原始POS系统只记录交易完成时间精确到秒。若直接resample(1H).sum()会把23:59:59的订单计入24点而实际业务中23:00-23:59的订单才属于“23点档”。正确做法是# 业务约定每小时档期以起始时间命名如23:00档包含23:00:00-23:59:59 df[hour_slot] (df[transaction_time] - pd.Timedelta(1s)).dt.floor(1H) df_hourly df.groupby(hour_slot)[amount].sum().reset_index()这段代码用floor(1H)前减1秒确保23:59:59被归入23点档而非0点档。这种细节在Kaggle比赛里无关紧要但在日均千万级订单的系统中每小时误差0.3%意味着每天多备货27吨蔬菜。第二缺失值填充不是技术活而是业务推理。IoT传感器断连产生的空值绝不能用ffill()简单填充。某风电场曾因用前向填充风速数据导致功率预测模型将“传感器故障”误判为“风速骤降”触发不必要的停机。正确策略是分层处理短时缺失5分钟用相邻时段均值设备运行状态校准如风机停机时风速应≈0长时缺失30分钟标记为is_sensor_faultTrue作为exog变量输入模型周期性缺失如每日02:00-02:15固定校准用pd.date_range()生成掩码强制设为NaN后由模型学习该模式。第三时间特征工程必须携带物理意义。df[hour] df.index.hour这种基础特征远远不够。在物流时效预测中我构建了复合时间特征# 业务洞察周五晚高峰拥堵程度工作日早高峰×1.8 周末购物潮×0.6 df[congestion_factor] ( (df.index.hour.isin([7,8,17,18]) (df.index.dayofweek 5)) * 1.0 (df.index.hour.isin([19,20,21]) (df.index.dayofweek 4)) * 1.8 (df.index.hour.isin([10,11,14,15]) (df.index.dayofweek 5)) * 0.6 )这个congestion_factor不是统计学拟合出来的而是基于交管部门发布的《城市路网拥堵指数白皮书》人工编码的业务规则。它让模型在缺乏历史周五晚数据时仍能基于规则推演合理预测。3. 核心细节解析与实操要点从数据加载到模型部署的12个生死关卡3.1 加载阶段警惕pandas的“温柔陷阱”pd.read_csv()是时间序列分析的第一道生死线。2021年某医疗设备公司因忽略一个参数导致全年呼吸机使用时长分析全部作废。问题出在parse_dates参数的默认行为# ❌ 危险写法pandas自动推断日期格式 df pd.read_csv(data.csv, parse_dates[timestamp]) # ✅ 正确写法强制指定格式避免2023-01-02和01/02/2023混淆 df pd.read_csv(data.csv, parse_dates[timestamp], date_parserlambda x: pd.to_datetime(x, format%Y-%m-%d %H:%M:%S))更隐蔽的陷阱是时区处理。某跨国电商的销售数据混杂UTC和本地时区pd.to_datetime()默认转为系统时区导致东京和洛杉矶的“同一时刻”在DataFrame里变成不同时间戳。解决方案是# 统一转为UTC再根据业务需求转换 df[timestamp] pd.to_datetime(df[timestamp], utcTrue) # 业务要求按北京时间分析则统一转为Asia/Shanghai df[timestamp] df[timestamp].dt.tz_convert(Asia/Shanghai) # 关键删除时区信息避免后续resample出错 df[timestamp] df[timestamp].dt.tz_localize(None)这个tz_localize(None)是血泪教训——没有它df.set_index(timestamp).resample(D).sum()会报TypeError: Cannot convert tz-aware to tz-naive而错误提示完全不指向时区问题。3.2 探索性分析用三张图锁定80%的问题新手花3小时调参不如用15分钟画对三张图。我的标准EAD流程是第一张图原始序列滚动统计量rolling_mean/stdfig, ax plt.subplots(2, 1, figsize(12, 8)) df[value].plot(axax[0], alpha0.7, labelRaw) df[value].rolling(7).mean().plot(axax[0], lw2, label7-day Mean) ax[0].legend(); ax[0].set_title(Trend Volatility) # 计算滚动标准差的变异系数CV识别波动突变点 rolling_cv df[value].rolling(30).std() / df[value].rolling(30).mean() rolling_cv.plot(axax[1], colorred) ax[1].axhline(rolling_cv.mean() 2*rolling_cv.std(), ls--, ck) ax[1].set_title(Volatility Stability (CV))这张图能暴露两大隐患若7日均值线呈明显斜线如每年递增15%说明存在强趋势必须差分或加入时间变量若滚动CV在某点突然跃升如从0.12跳到0.35大概率是数据源切换如新传感器上线或业务规则变更如促销政策调整此时需插入is_policy_changeTrue标记。第二张图ACF/PACF图plot_acf/pacf关键不是看拖尾还是截尾而是看显著滞后阶数是否符合业务逻辑。某快递公司包裹量ACF在lag7、14、21处持续显著这完美对应“周循环”但如果lag5、10、15显著就要怀疑是不是工作日/周末数据被错误合并——因为5天工作制下lag5才应有强相关。此时必须回到原始数据用df.groupby(df.index.dayofweek)[volume].mean().plot()验证。第三张图残差诊断图sm.graphics.tsa.plot_acf(resid)模型训练后第一件事不是看RMSE而是画残差ACF。如果lag1处ACF值0.2说明模型没捕获一阶自相关必须增加AR项如果lag7处仍有显著峰值说明季节性没建好该换SARIMA而非硬调参数。我见过太多人把残差ACF当装饰画直到上线后发现预测值总是滞后一天。3.3 平稳性检验adfuller不是“及格线”而是“手术同意书”adfuller()的p值从来不是0.05的二元判断而是风险决策的量化依据。我的实操手册规定p 0.01可安全差分但需检查差分后序列的物理意义如库存量差分日出入库净额有意义0.01 ≤ p 0.05进入“灰色地带”必须做两件事① 用kpss()做互补检验KPSS原假设是平稳若p0.05则拒绝平稳与ADF结论矛盾② 绘制df[value].diff().hist()直方图确认差分后分布是否严重偏斜如右偏说明存在结构性上涨该用趋势项而非差分p ≥ 0.05禁止差分改用detrend()移除确定性趋势或引入exog变量如GDP增速、行业景气指数解释非平稳性。2022年某新能源车企电池衰减率分析就栽在这里。adfuller()返回p0.063团队执意一阶差分结果把“电池老化加速”这一核心业务现象抹平模型预测寿命比实测长23个月。后来改用sm.tsa.detrend(df[decay_rate], order2)二阶多项式去趋势保留了加速衰减的物理特征预测误差从±18个月降至±4个月。3.4 特征工程超越lag和rolling的5种业务感知特征时间序列特征不能只靠shift()和rolling()。我在金融风控模型中验证过以下5种高信息量特征1. 时间窗口内极值比率# 业务洞察单日最大交易额/当日均值 3预示洗钱风险 df[peak_ratio] df[amount].rolling(24H).apply( lambda x: x.max() / x.mean() if len(x) 1 else 1 )2. 趋势方向一致性# 业务规则连续3小时流量下降且降幅5%可能设备故障 df[trend_consistency] ( (df[traffic].diff() 0) (df[traffic].pct_change() -0.05) ).rolling(3).sum() 33. 周期相位偏移# 电商场景大促期间用户活跃高峰从20:00提前至18:00用相位角量化 from scipy.signal import hilbert analytic_signal hilbert(df[activity].rolling(7).mean()) instantaneous_phase np.unwrap(np.angle(analytic_signal)) df[phase_shift] instantaneous_phase % (2*np.pi)4. 多尺度波动率# 用小波变换提取不同周期波动高频噪声/中频促销/低频经济周期 import pywt coeffs pywt.wavedec(df[revenue], db4, level3) df[vol_highfreq] np.abs(coeffs[1]).mean() # 1-2天波动 df[vol_midfreq] np.abs(coeffs[2]).mean() # 3-7天波动 df[vol_lowfreq] np.abs(coeffs[3]).mean() # 7天波动5. 事件驱动脉冲# 将业务事件如新品发布、竞品降价编码为脉冲函数 event_dates [2023-03-15, 2023-06-20] df[event_pulse] 0 for d in event_dates: df.loc[df.index d, event_pulse] 1 # 用指数衰减模拟事件影响持续时间 df[event_decay] df[event_pulse].rolling(14, min_periods1).apply( lambda x: np.sum([x.iloc[i] * np.exp(-i/7) for i in range(len(x))]) )这些特征的共同点是每个值都能被业务方用自然语言解释比如“event_decay0.42表示新品发布后第14天其影响强度仍为首发日的42%”。3.5 模型选择不是“哪个更准”而是“哪个更扛得住业务冲击”在真实场景中模型鲁棒性远比测试集精度重要。我的选型决策树如下第一步看数据长度 50个观测点放弃ARIMA/SARIMA用ExponentialSmoothingHolt-Winters因其对小样本更稳定50-200点SARIMA是首选但order和seasonal_order必须用网格搜索TimeSeriesSplit交叉验证禁用auto_arima200点可尝试Prophet但必须关闭mcmc_samplesMCMC采样在长序列上极慢且changepoint_range设为0.8只在最近80%数据中检测突变点避免历史异常干扰。第二步看业务容忍度需要实时预警如设备故障用IsolationForest做异常检测比预测模型更可靠需要区间预测如库存安全水位SARIMAX的get_forecast(steps30).conf_int()比LSTM的蒙特卡洛Dropout更易解释需要归因分析如“为什么Q3销量下降”必须用SHAP解释XGBoost模型因SARIMAX无法提供特征重要性。第三步看维护成本某零售集团曾用LSTM预测门店客流准确率比SARIMA高2.3%但每次模型更新需GPU集群跑8小时。后来改用SARIMAXexog加入天气、地铁客流等外部变量准确率仅降0.7%但更新耗时从8小时降至17分钟运维成本降低96%。这就是为什么我在生产环境坚持“能用统计模型解决的绝不碰深度学习”。3.6 模型评估拒绝RMSE幻觉拥抱业务损失函数用RMSE评估时间序列模型是最大的认知陷阱。某外卖平台用RMSE最小化选模型最终上线的模型在暴雨天预测误差达±400单导致运力调度失衡。问题在于RMSE对正负误差一视同仁但业务中“少预测100单”运力过剩和“多预测100单”骑手短缺损失天差地别。我的评估协议强制要求1. 定义业务损失函数def business_loss(y_true, y_pred): # 少预测惩罚重每少1单罚2元骑手闲置成本 # 多预测惩罚轻每多1单罚0.5元临时调度成本 under_predict np.maximum(0, y_true - y_pred) over_predict np.maximum(0, y_pred - y_true) return np.mean(under_predict * 2 over_predict * 0.5) # 在交叉验证中使用 from sklearn.model_selection import TimeSeriesSplit tscv TimeSeriesSplit(n_splits5, gap7) # 预留7天缓冲 scores [] for train_idx, test_idx in tscv.split(X): model.fit(X.iloc[train_idx], y.iloc[train_idx]) y_pred model.predict(X.iloc[test_idx]) scores.append(business_loss(y.iloc[test_idx], y_pred)) print(fBusiness Loss: {np.mean(scores):.2f})2. 分层评估按时间分层工作日vs周末、促销期vs平销期确保模型在关键时段不掉链子按量级分层高销量SKU1000件/天、中销量SKU100-1000件、长尾SKU100件因不同量级的误差容忍度不同按误差方向分层单独计算under_predict_rate少预测占比和over_predict_rate多预测占比业务方更关注前者。3. 可视化评估# 绘制预测vs实际的散点图但按误差方向着色 plt.scatter(y_test, y_pred, cnp.sign(y_pred - y_test), cmapRdYlBu_r) plt.axline((0,0), slope1, ls--, ck) plt.xlabel(Actual); plt.ylabel(Predicted) plt.colorbar(labelError Direction: RedOver, BlueUnder) # 添加业务警戒线当预测值实际值×0.8时标红严重少预测 mask y_pred y_test * 0.8 plt.scatter(y_test[mask], y_pred[mask], cred, s50, alpha0.7, labelCritical Under-predict) plt.legend()这张图能让业务方3秒内理解模型弱点比10页RMSE报告更有价值。3.7 模型部署从Jupyter到API的7个必守契约模型在Jupyter里跑通不等于能上线。我的部署检查清单契约1时间索引必须绝对可控Flask API接收请求时必须用pd.to_datetime(request.json[timestamp], utcTrue)强制转UTC再tz_convert(Asia/Shanghai)最后tz_localize(None)。任何环节遗漏时区都会导致resample(D)跨天错误。契约2特征工程必须原子化封装class FeatureEngineer: def __init__(self): self.scaler StandardScaler() def fit_transform(self, df): # 所有transform操作必须在此完成 df self._add_lag_features(df) df self._add_time_features(df) X df[self.feature_cols] return self.scaler.fit_transform(X) def transform(self, df): # 部署时只调用此方法 df self._add_lag_features(df) df self._add_time_features(df) X df[self.feature_cols] return self.scaler.transform(X) # 用fit时的scaler参数transform()方法必须保证输入df的列名、顺序、数据类型与训练时完全一致否则scaler.transform()会报错。契约3预测必须带置信区间# SARIMAX预测必须返回完整结果对象 def predict_with_ci(model, steps30, alpha0.05): forecast model.get_forecast(stepssteps) pred_mean forecast.predicted_mean pred_ci forecast.conf_int(alphaalpha) return pd.DataFrame({ forecast: pred_mean, lower_bound: pred_ci.iloc[:, 0], upper_bound: pred_ci.iloc[:, 1] }) # API返回JSON必须包含这三个字段 return jsonify({ forecast: pred_df[forecast].tolist(), confidence_interval: { lower: pred_df[lower_bound].tolist(), upper: pred_df[upper_bound].tolist() } })契约4异常必须可追溯在预测函数开头插入import logging logger logging.getLogger(__name__) def predict(...): logger.info(fPredict request: timestamp{now}, steps{steps}) try: # 模型逻辑 return result except Exception as e: logger.error(fPredict failed: {str(e)}, exc_infoTrue) raiseexc_infoTrue确保堆栈跟踪写入日志这是排查线上问题的唯一线索。契约5版本必须硬编码# model.py MODEL_VERSION 2023.11.05-sarimax-v3 def load_model(): with open(fmodels/{MODEL_VERSION}.pkl, rb) as f: return pickle.load(f)API响应头必须包含X-Model-Version: 2023.11.05-sarimax-v3便于灰度发布时追踪效果。契约6冷启动必须有兜底首次请求时若模型文件不存在自动触发if not os.path.exists(model_path): logger.warning(Model file missing, using fallback) return fallback_prediction() # 如简单移动平均契约7监控必须实时部署后立即启动监控# 每5分钟检查一次预测质量 def monitor_prediction_drift(): recent_preds get_recent_predictions(last_hours24) drift_score kl_divergence(recent_preds[forecast], historical_distr[forecast]) if drift_score 0.15: alert_ops(Prediction drift detected!)4. 实操过程与核心环节实现电商GMV预测全流程手把手4.1 业务需求与数据概览从模糊需求到精确规格客户提出的需求是“下个月各品类GMV预测要能支撑采购计划”。这看似简单实则隐藏5个关键规格必须当场确认时间粒度是“日预测”用于排班还是“周预测”用于采购客户选了周预测预测范围是“下一周”滚动预测还是“下个月整月”固定窗口客户要下个月整月共4周品类粒度是“一级类目”如家电还是“三级类目”如冰箱对开门500L以上客户要三级类目共217个输出格式只要点估计如“下月大家电GMV1.2亿”还是区间预测如“1.05-1.35亿置信度90%”客户明确要90%置信区间特殊约束是否有已知事件如618大促、新品首发客户提供了3个事件日期及预期影响幅度。确认后数据交付清单明确为历史数据2021.01.01-2023.05.31字段包括date(date),category_level3(str),gmv(float),discount_rate(float),new_product_flag(bool)外部数据同周期天气数据温度、降雨量、百度指数各品类搜索热度、竞品价格指数事件数据event_date,event_type(618/新品/直播),impact_factor(0.3/0.15/0.25)。这份清单避免了后期返工——曾有项目因未约定“折扣率是否含满减”导致模型用错特征重训两周。4.2 数据加载与清洗处理217个品类的“数据雪崩”217个品类的数据不是一张表而是217个独立CSV按品类分片存储总大小12GB。直接pd.concat([pd.read_csv(f) for f in files])会内存溢出。正确做法是# 分块读取即时清洗 def load_and_clean_category(file_path): # 指定列类型减少内存 dtype {category_level3: category, gmv: float32} df pd.read_csv(file_path, dtypedtype, parse_dates[date], date_parserlambda x: pd.to_datetime(x, format%Y-%m-%d)) # 清洗剔除gmv为负值退货异常、补全缺失日期 df df.set_index(date).asfreq(D, fill_value0) # 按日填充缺日补0 df df.reset_index() return df # 使用dask并行处理比multiprocessing更省内存 import dask.dataframe as dd file_list glob(data/category_*.csv) ddf dd.from_delayed([dask.delayed(load_and_clean_category)(f) for f in file_list]) df_full ddf.compute() # 最终合并为pandas DataFrame关键技巧asfreq(D, fill_value0)比reindex()快3倍且fill_value0符合电商场景无销售即GMV0避免用ffill()导致数据污染。4.3 探索性分析发现“品类休眠期”这一隐藏规律对217个品类分别画rolling(30).mean()图时发现一个惊人现象约35%的品类如“智能手表”在每年1-2月出现长达45天的GMV1万元远低于其他月份均值的1/10。这不是数据缺失而是真实业务现象——春节假期导致供应链中断、用户消费意愿下降。若按常规方法用adfuller()检验这些品类会因长期低值被误判为“平稳”从而跳过必要的季节性处理。我的应对方案是# 定义“休眠期”连续N天GMV mean_gmv * threshold def detect_hibernation(df, window_days30, threshold0.1, min_duration20): daily_mean df[gmv].mean() hibernating (df[gmv] daily_mean * threshold).rolling(window_days).sum() min_duration return hibernating.shift(-min_duration 1).fillna(False) # 标记休眠期结束日 # 为每个品类生成hibernation_flag df_full[is_hibernating] df_full.groupby(category_level3).apply( lambda x: detect_hibernation(x) ).values