资讯动态

Python 数据处理入门(十)时间序列:按天、按周、按月自动汇总

发布时间:2026/10/21 17:34:33 来源:尧图企业网站定制
写在前面上一篇我们做了分组聚合但有个东西一直被当成普通文本处理——日期。表里的日期长这样2024-01-05。这种列能排序、能筛选但没法回答1 月每周的销售额是多少“这周比上周涨了还是跌了”因为 Python 根本不知道它是个日期。这篇解决三件事把字符串变成时间类型、按天/周/月自动汇总、算趋势和环比同比。学会之后日报周报月报就是同一行代码换个参数的事股票、日志、打卡记录、订单流水都是同一套处理方式。示例数据用stock.csv30 个交易日的收盘价和成交量和sales.csv订单日期正好是字符串用来演示转换。一、pd.to_datetime把字符串变成时间先看没转换之前的样子——订单日期的 dtype 是str这在读 CSV 时是默认结果。转换之后.dt就成了通往各种日期属性的入口先确认示例数据在手边importosimportmkdata# 第 1 篇里的 mkdata.pyifnotos.path.exists(data/sales.csv):mkdata.main()# 没生成过就先跑一次print(示例数据就绪,os.path.exists(data/sales.csv))这段会检查第 1 篇生成的data/目录在不在不在就自动补一次已经有数据时只会打印一行示例数据就绪 True不影响后面的结果。parse_dates[订单日期]让 pandas 在读文件时就把这一列转成时间类型省掉一次pd.to_datetime。手动转换的写法是df[订单日期] pd.to_datetime(df[订单日期])两种效果一样——pd.to_datetime会自动识别2024-01-05、2024/01/05、20240105这些常见格式一次转一整列。转换失败会怎样pandas 默认直接报错Could not infer format之类这是好事说明列里混了脏值。格式实在不统一时加errorscoerce把转换不了的值变成NaT时间版的 NaN再df[订单日期].isna().sum()看看丢了多少行——和第 6 篇处理缺失值是一个思路。dt里两个容易混的属性dayofweek是周一 0、周日 6day_name()直接给英文星期名Friday。想要中文星期就用map映射一行搞定dt.month给的是数字 112。想要2024-01这种年月字符串用dt.to_period(M)再astype(str)直接astype(str)会得到2024-01-05这种带日的完整日期。月份、季度取出来就能直接分组等于给上一篇的 groupby 加了个时间维度——df.groupby(df[订单日期].dt.quarter)[金额].sum()就是一份季度汇总连dt.to_period(Q)都不用。二、set_index时间必须是索引而且要排好序同一份股价数据的三条曲线原始日线、rolling(5) 的 5 日均线、resample(“W”) 的周均值线。时间序列的标准姿势是把时间列设为索引并排好序。索引是时间类型后loc切片、resample、rolling才能正常用importpandasaspd stockpd.read_csv(data/stock.csv,encodingutf-8-sig,parse_dates[日期])print(原始形状,stock.shape, 日期类型,stock[日期].dtype)stockstock.set_index(日期).sort_index()print(索引类型,type(stock.index).__name__)print(stock.head(3).to_string())原始形状 (30, 3) 日期类型 datetime64[us] 索引类型 DatetimeIndex 收盘价 成交量 日期 2024-01-02 100.00 12792 2024-01-03 100.48 16943 2024-01-04 100.04 14828sort_index()不能省。股票数据、日志数据从数据库里捞出来常常是乱序的时间索引没排序的话rolling会按错误顺序算窗口resample的结果也可能错位。先排序再看结果这是我踩过的坑里最冤的一个。resample则可以理解成给时间做的 groupby不用手动造周这一列只要给一个频率字符串它自己按时间窗口分组。常用的频率字符串记住这几个就够频率含义说明D按自然日没有数据的日子也会出现值为 0 或 NaNW按周默认周日为一周的最后一天ME按月M 表示月末标签落在当月最后一天QE按季度同理年末用YE三、resample按天、按周、按月自动汇总把订单日期转成 datetime 之后按月汇总只需要一行代码得到的月度销售额柱状图可以直接放进月报。resample可以理解成给时间做的 groupby不用手动造周这一列只要给一个频率字符串它自己按时间窗口分组。常用的频率字符串记住这几个就够频率含义说明D按自然日没有数据的日子也会出现值为 0 或 NaNW按周默认周日为一周的最后一天ME按月M 表示月末标签落在当月最后一天QE按季度同理年末用YEimportpandasaspd stockpd.read_csv(data/stock.csv,encodingutf-8-sig,parse_dates[日期]).set_index(日期).sort_index()print(--- 按周汇总)print(stock.resample(W).agg(周均价(收盘价,mean),周成交量(成交量,sum),交易天数(收盘价,count)).round(2).to_string())print(--- 按月汇总)print(stock.resample(ME).agg(月均价(收盘价,mean),最高价(收盘价,max),月成交量(成交量,sum)).round(2).to_string())print(--- 按天汇总D)print(stock.resample(D).agg(收盘价(收盘价,sum),交易笔数(收盘价,count)).head(6).to_string())--- 按周汇总 周均价 周成交量 交易天数 日期 2024-01-07 99.78 57018 4 2024-01-14 97.38 80744 5 2024-01-21 97.50 91567 5 2024-01-28 95.45 86870 5 2024-02-04 86.57 90417 5 2024-02-11 82.19 97433 5 2024-02-18 80.12 19514 1 --- 按月汇总 月均价 最高价 月成交量 日期 2024-01-31 96.10 100.48 357403 2024-02-29 82.57 85.21 166160 --- 按天汇总D 收盘价 交易笔数 日期 2024-01-02 100.00 1 2024-01-03 100.48 1 2024-01-04 100.04 1 2024-01-05 98.62 1 2024-01-06 0.00 0 2024-01-07 0.00 0按周汇总时一周里的 5 个交易日被合并成一行按月汇总时30 个交易日压成 12 月两行按天汇总时周末没有交易那两天的收盘价合计是 0、交易笔数是 0。三个细节最后一行只有 1 个交易日数据到 2 月 12 日为止这一周不是数据错了而是样本不完整——做周报时最后一周的残缺数据要么标注、要么剔除别直接拿去和完整周比较sum用在收盘价上其实没有业务含义价格不能相加这里只是演示写法均价用mean、最高用max、成交量用sum、交易天数用count聚合函数选错比代码写错更隐蔽起始日期前、结束日期后没有数据的日子resample不会凭空造出来——它只按数据覆盖的区间开窗口。label 和 closed 是 resample 唯一有点绕的地方。resample(W)的默认参数是labelright这一行的标签用窗口的结束时间周日closedright区间是(上周日, 本周日]也就是本周日包含在内。importpandasaspd stockpd.read_csv(data/stock.csv,encodingutf-8-sig,parse_dates[日期]).set_index(日期).sort_index()weekstock.resample(W).agg(周成交量(成交量,sum))print(W 的默认 label/closed,stock.resample(W).label,stock.resample(W).closed)print(D 的默认 label/closed,stock.resample(D).label,stock.resample(D).closed)print(--- 标签用右边界)print(week.head(3).to_string())print(--- 标签改用左边界labelleft, closedleft)print(stock.resample(W,labelleft,closedleft).agg(周成交量(成交量,sum)).head(3).to_string())W 的默认 label/closed right right D 的默认 label/closed left left --- 标签用右边界 周成交量 日期 2024-01-07 57018 2024-01-14 80744 2024-01-21 91567 --- 标签改用左边界labelleft, closedleft 周成交量 日期 2023-12-31 57018 2024-01-07 80744 2024-01-14 91567两段输出的成交量数字完全一样只是行标签不同默认的labelright把 1 月 2 日1 月 7 日这一周的成交量记在2024-01-07周日那一行改成labelleft后同一批数据记在2023-12-31那一行因为左边界是上一个周日。想让标签落在周一写resample(W-MON, labelleft, closedleft)就行。关键不是记默认值而是知道结果里那个日期到底是窗口的左边界还是右边界——和别的部门对周报时口径必须说清不然一定有一周的差异。顺便说一个频率别名的坑resample(M)现在是会直接报错的必须写成resample(ME)。importpandasaspd stockpd.read_csv(data/stock.csv,encodingutf-8-sig,parse_dates[日期]).set_index(日期).sort_index()try:stock.resample(M).sum()exceptValueErrorasexc:print(ValueError:,exc)ValueError: Invalid frequency: M. Failed to parse with error message: ValueError(M is no longer supported for offsets. Please use ME instead.)四、rolling滚动平均看趋势而不是看噪声rolling 窗口长度的影响3 日线贴合原始波动10 日线更平滑窗口越大越能看清趋势、越滞后于拐点。单日的收盘价上下乱跳看不出走势。**滚动平均移动平均**的做法是每次取最近 N 天算一个平均值把噪声抹平importpandasaspd stockpd.read_csv(data/stock.csv,encodingutf-8-sig,parse_dates[日期]).set_index(日期).sort_index()stock[MA5]stock[收盘价].rolling(5).mean().round(2)stock[MA3成交量]stock[成交量].rolling(3).mean().round(0)print(stock[[收盘价,MA5,成交量,MA3成交量]].head(8).to_string())收盘价 MA5 成交量 MA3成交量 日期 2024-01-02 100.00 NaN 12792 NaN 2024-01-03 100.48 NaN 16943 NaN 2024-01-04 100.04 NaN 14828 14854.0 2024-01-05 98.62 NaN 12455 14742.0 2024-01-08 97.89 99.41 25878 17720.0 2024-01-09 96.30 98.67 8212 15515.0 2024-01-10 96.40 97.85 9749 14613.0 2024-01-11 98.54 97.55 11463 9808.0前 4 行的 MA5 是NaN这是正常的窗口是 5 天1 月 2 日只攒到 1 个数据点凑不满就出 NaN。想让前几行也有值加min_periods1rolling(5, min_periods1).mean()它表示最少 1 个数据点就算。rolling后面跟的不止mean还能直接agg一次算多个统计量。std算的是滚动标准差默认自由度ddof1值越大说明这几天成交量越不稳定stock[成交量].rolling(5).agg([mean, std, max])就能拿到最近 5 天的均值、标准差和最大值。滚动窗口的方向是向前的第 5 行用的是第 15 行绝不会用到第 6 行的数据所以它不会偷看未来可以直接用来做趋势判断。五、shift环比、同比和区间筛选shift(n)把整列数据往下挪 n 行配合减法/除法就是环比importpandasaspd stockpd.read_csv(data/stock.csv,encodingutf-8-sig,parse_dates[日期]).set_index(日期).sort_index()stock[昨日收盘]stock[收盘价].shift(1)stock[日环比%](stock[收盘价]/stock[昨日收盘]*100-100).round(2)stock[5日涨跌]stock[收盘价].diff(5).round(2)print(stock[[收盘价,昨日收盘,日环比%,5日涨跌]].head(7).to_string())收盘价 昨日收盘 日环比% 5日涨跌 日期 2024-01-02 100.00 NaN NaN NaN 2024-01-03 100.48 100.00 0.48 NaN 2024-01-04 100.04 100.48 -0.44 NaN 2024-01-05 98.62 100.04 -1.42 NaN 2024-01-08 97.89 98.62 -0.74 NaN 2024-01-09 96.30 97.89 -1.62 -3.70 2024-01-10 96.40 96.30 0.10 -4.08第 1 行的日环比是NaN因为昨天不存在——这是 shift 的固定特征不是缺数据pct_change()是环比百分比的简写stock[收盘价].pct_change() * 100和上面的写法等价diff(n)就是自身 - shift(n)看5 个交易日前到现在涨跌了多少很方便。放到月报维度也一样。下面用sales.csv算出每月销售额加上上月和环比再用一份去年同期的数据算同比importpandasaspd dfpd.read_csv(data/sales.csv,encodingutf-8-sig,parse_dates[订单日期])monthdf.set_index(订单日期)[金额].resample(ME).sum()month.indexmonth.index.to_period(M)reportmonth.to_frame(本月销售额)report[上月销售额]report[本月销售额].shift(1)report[环比%](report[本月销售额]/report[上月销售额]*100-100).round(1)# 去年同期真实项目里来自去年同期的汇总表这里用一组示例数字演示对齐方式last_yearpd.Series([532.70,322.15,1101.52,178.08,2339.20],indexreport.index,name去年同期)report[去年同期]last_year report[同比%](report[本月销售额]/report[去年同期]*100-100).round(1)print(report.to_string())本月销售额 上月销售额 环比% 去年同期 同比% 订单日期 2024-01 626.7 NaN NaN 532.70 17.6 2024-02 379.0 626.7 -39.5 322.15 17.6 2024-03 1295.9 379.0 241.9 1101.52 17.6 2024-04 209.5 1295.9 -83.8 178.08 17.6 2024-05 2752.0 209.5 1213.6 2339.20 17.6同比、环比的区别就一句话环比和上一个月比同比和去年同一个月比。技术实现上都是 shift区别只在错位多少期以及数据源是哪一张表。4 月环比 -83.8% 但同比 17.6%这在业务上是很常见的现象——环比受季节性影响大同比更能看出真实增长。六、按区间筛选和导出结果最后是按日期区间筛选和导出结果。时间索引可以直接用字符串切片这是把时间设成索引的最大福利importpandasaspd stockpd.read_csv(data/stock.csv,encodingutf-8-sig,parse_dates[日期]).set_index(日期).sort_index()print(--- 2024 年 1 月 15 日到 1 月 19 日)print(stock.loc[2024-01-15:2024-01-19,[收盘价,成交量]].to_string())print(--- 只要 2 月的数据)print(stock.loc[2024-02].to_string())weekly(stock.resample(W).agg(周均价(收盘价,mean),周最高(收盘价,max),周最低(收盘价,min),周成交量(成交量,sum)).round(2).reset_index())print(--- 每周汇总并导出)print(weekly.to_string(indexFalse))weekly.to_csv(stock_weekly.csv,indexFalse,encodingutf-8-sig)backpd.read_csv(stock_weekly.csv,encodingutf-8-sig)print(导出后读回,back.shape,list(back.columns))--- 2024 年 1 月 15 日到 1 月 19 日 收盘价 成交量 日期 2024-01-15 96.76 20456 2024-01-16 97.55 23873 2024-01-17 98.12 11610 2024-01-18 98.29 20977 2024-01-19 96.80 14651 --- 只要 2 月的数据 收盘价 成交量 日期 2024-02-01 84.52 25368 2024-02-02 84.96 23845 2024-02-05 85.21 11352 2024-02-06 84.91 17176 2024-02-07 80.88 24915 2024-02-08 80.02 23248 2024-02-09 79.94 20742 2024-02-12 80.12 19514 --- 每周汇总并导出 日期 周均价 周最高 周最低 周成交量 2024-01-07 99.78 100.48 98.62 57018 2024-01-14 97.38 98.54 96.30 80744 2024-01-21 97.50 98.29 96.76 91567 2024-01-28 95.45 97.86 91.94 86870 2024-02-04 86.57 89.87 84.52 90417 2024-02-11 82.19 85.21 79.94 97433 2024-02-18 80.12 80.12 80.12 19514 导出后读回 (7, 5) [日期, 周均价, 周最高, 周最低, 周成交量]loc[2024-01-15:2024-01-19]的左右两端都包含不需要写、loc[2024-02]这种只给年月的写法会自动匹配整个 2 月。前提还是那一个索引必须是时间类型如果索引还是字符串loc[2024-01-15:2024-01-19]会按字符串字典序切结果很容易莫名其妙。导出部分的三个习惯导出前一定reset_index()把时间索引还原成日期列否则打开 CSV 会发现时间列不见了indexFalse防止多出一列无名的行号encodingutf-8-sig让 Excel 双击打开不乱码这是本系列从第 3 篇起的统一约定。常见问题1. 报错ValueError: Invalid frequency: M是怎么回事月频别名变了把resample(M)改成resample(ME)月末年末用YE季末用QE。D、W、H这些不受影响。2. resample 之后报TypeError: Only valid with DatetimeIndex说明索引不是时间类型。检查df.index.dtype如果不是datetime64就先df[日期] pd.to_datetime(df[日期])再set_index(日期)。时间还在普通列里没设索引时也可以用df.resample(ME, on日期)。3. rolling 的结果前几行全是 NaN可以删掉吗可以但建议别急着删。前几行 NaN 说明窗口还没攒满这是客观事实。真要保留数值就用rolling(5, min_periods1)要交报表就dropna()丢掉前 4 行。小结这一篇的主线是pd.to_datetime把字符串变成时间 →set_indexsort_index建立时间索引 →resample按天/周/月汇总 →rolling看趋势 →shift算环比同比 →loc按区间筛选 →reset_index导出。三个最值得记住的点时间索引必须排序resample的label/closed决定窗口标签落在左边界还是右边界rolling和shift产生的 NaN 都是数据不够的正常表现不是 bug。下一篇我们聊可视化把这一篇算出来的周均价、月度环比直接画成折线图和柱状图5 行代码出图顺便解决中文乱码、样式和图片导出的问题。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑