简介一份面向Python初学者的时间序列分析实战教程PDF聚焦Pandas在日期型数据上的处理与可视化。以美国西雅图费利蒙桥自动自行车计数器的逐小时流量数据为案例完整演示从CSV导入、设置日期索引、重命名列、清洗缺失值与重复项到describe统计、按周/月重采样、30天滚动平均及Matplotlib/Seaborn绘图等关键操作帮助读者理解季节性与天气对骑行量的影响。资源为单个PDF文件压缩包约391KB内容紧凑、步骤清晰当前已有1113人学习参考适合正在学习Pandas时间序列模块或需要完成类似数据项目的数据分析入门者可直接对照代码思路迁移到其他时序数据集。1. 时间序列分析入门费利蒙桥自行车数据为什么值得跑一遍Python 数据分析里时间序列是最容易“一看就会、一跑就废”的模块——不是 API 记不住而是拿到不规则的真实数据时根本不知道先干什么。这份《Python数据分析实践时间序列实例》PDF 选了一个特别适合练手的数据集美国西雅图费利蒙桥的自动自行车计数器从 2012 年末起按小时记录东西双向的自行车数量公开 CSV 直接下载。教程每一步都有完整的 Python 代码块从 Pandas 读取 CSV、清洗缺失值和重复项到按周按月重采样、滚动平均、GroupBy 分组最后画出能说明通勤规律的双峰图。它适合刚学完 Pandas 基础、想找真实数据练手的新手也适合写过不少脚本但没深究过 resample 边界和 rolling 窗口语义的熟手。整份资料不碰复杂模型却把时间序列的常用 API 几乎全用了一遍。2. 数据读取与清洗read_csv 的 parse_dates、列名重命名和去重顺序2.1 拿数据curl 下载与本地备选西雅图政府数据门户把费利蒙桥计数器公布成了一个开放数据集接口标识是65db-xm6k。教程里在 Jupyter 中用一行命令把它拉下来curl -o FremontBridge.csv https://data.seattle.gov/api/views/65db-xm6k/rows.csv?accessTypeDOWNLOAD-o FremontBridge.csv指定保存文件名URL 末尾的?accessTypeDOWNLOAD是让接口返回纯 CSV 而不是网页的关键参数漏掉它你会拿到一个 HTML 文件后面read_csv直接报错。这段在 Jupyter 里要写成!curl ...在终端里则是去掉感叹号直接跑。下载完成后的文件是一张典型的时序宽表第一列Date是整点时间戳后面几列分别记录总计、东向、西向的自行车计数。字段名很长这是教程后续要重命名列名的直接原因。导入方面教程用了标准四件套import pandas as pd import numpy as np import matplotlib as mpl import matplotlib.pyplot as plt import seabornseaborn 需要单独pip install seaborn它在这套代码里不是用来画统计图的而是用来设置绘图风格。导入后跟了一段字体配置rc {font.sans-serif: SimHei, axes.unicode_minus: False} seaborn.set(rcrc)font.sans-serif指定无衬线中文字体解决图里中文变方框的问题axes.unicode_minus设为 False否则坐标轴的负号会显示成方块。这两项是 matplotlib 中文绘图的常规双保险换了字体不换它照样翻车。2.2 指定 Date 列做索引parse_dates 做了什么data pd.read_csv(FremontBridge.csv, index_colDate, parse_datesTrue) data.head() data.info()index_colDate告诉 Pandas 用 Date 列作为行索引parse_datesTrue则要求 Pandas 在读取时把所有能解析成日期的列自动转成datetime64[ns]。这两步是整条链路的地基索引不是时间类型的话后面resample(W)、data19.index.time这些操作会直接报“没有周期属性”的错。data.info()的输出有两块值得细看第一块看索引行是否变成datetime64[ns]第二块看每列的非空计数。教程里这份数据在清洗前几乎没有缺失和重复所以接下来的检查看起来像走过场——但换任何真实数据这两步都是必须的。parse_datesTrue有个隐含行为它会对所有能解析的列做转换如果表里恰好有一列 ID 长得像日期Pandas 也会顺手把它转掉更可控的写法是parse_dates[Date]只指定这一列。当你的数据里有“日期”和“时间”两列要拼成一个时间戳时可以写parse_dates{记录时间: [日期, 时间]}Pandas 会自动拼接再解析。2.3 列名重命名与列顺序调整data.columns [总计, 东向, 西向] data.index.name 记录时间 data data.iloc[:,[0,2,1]]原始列名很长逐列改太麻烦教程用了整体赋值只要列表长度和原 DataFrame 的列数一致data.columns [...]就会把列名整套换掉。data.index.name 记录时间是给索引起名打印时表头更清楚后续画图写 ylabel 也方便。data.iloc[:,[0,2,1]]这行代码第一次看容易懵iloc 后面跟的下标数组[0,2,1]表示按顺序取出第 0 列、第 2 列、第 1 列也就是把「总计、东向、西向」重排成「总计、西向、东向」。这不是数学需要纯粹是绘图时让三条线不容易互相遮挡视觉上更顺。类似的技巧还有data.sort_index()它保证索引按时间升序排列这是后面 truncate 和 resample 的前提。2.4 检查缺失和重复去重的顺序是隐性坑data.isnull().sum() data.duplicated().sum() data.drop_duplicates(inplaceTrue) data.dropna(inplaceTrue) data.info() data.describe()教程的顺序是先查缺失数量、查重复数量然后drop_duplicates最后dropna。这个顺序不是随手写的——如果先删缺失行某些原本因为缺失位置不同而不完全重复的行会在缺失行被抽掉后变成完全重复导致你漏删重复反过来先按原始行判断重复并删除再处理缺失重复判断更干净。drop_duplicates()默认用整行所有列判断是否重复只有所有字段都相同才删。想只看某几列传subset[记录时间]。dropna()默认删掉含任意缺失值的行想更保守就加howall只删整行全空的。注意inplaceTrue是在原对象上修改新版 Pandas 更推荐写成data data.drop_duplicates()避免在链式操作时出现SettingWithCopyWarning一类的警告。清洗完成后执行data.describe()能看到各列的均值、标准差、极值。放在时间序列上我会额外关注 min 是不是 0——如果某个小时计数恒为 0很可能计数器在那个时间段没在工作而不是真的没人骑车。3. 截取与重采样truncate 边界、resample 窗口和月度差值对齐3.1 按年份截取排序在前边界是闭区间data19 data.sort_index().truncate(before1/2019).truncate(after1/2020) data21 data.sort_index().truncate(before1/2021).truncate(after1/2022) data19 data21教程选了 2019 和 2021 两个年份做对比。truncate是 Pandas 针对排序索引的切片方法before1/2019和after1/2020的含义是取从 2019 年 1 月到 2020 年 1 月之间的记录。要注意 before 和 after 都是闭区间所以 data19 实际包含 2020-01-01 00:00 这条记录严格说是“2019 年加 2020 年第一小时”。提示truncate 的闭区间边界在跨年对比时会引入额外一小时如果要做精确自然年切片优先考虑data[2019]写法它按年份边界精确切分不带多余数据。truncate另一个硬性要求是索引必须已排序所以前面必须先sort_index()。忘记排序时 Pandas 会抛 “index must be sorted” 的异常这是新手经常卡住的地方。为什么教程跳过 2020 年因为那一年桥面计数有明显的异常形态不适合当正常年份做对比。教程最后也提到19 年的计数普遍比 21 年高——如果把 2020 也拉进来三条线会混在一起趋势反而不清楚。3.2 按周和按月重采样频率码与聚合函数weekly data.resample(W).sum() weekly.head() month19 data19.resample(M).sum() month21 data21.resample(M).sum() month19resample是时间序列降频的核心接口。第一个参数是频率码W表示按周默认窗口是周一到周日聚合结果以周日为标签M表示按自然月结果以月末为标签。聚合函数的选择要看业务含义——自行车流量是累计量窗口内相加sum才有意义如果是温度、水位这类瞬时测量值应该用mean()否则算出来的“月均值”会被采样密度带偏。resample常用的还有混合频率码比如W-MON表示以周一为一周起点MS表示月份起点而不是月末Q表示季度。需要对齐财年或自定义周期的场景这些码比手写循环高效得多。对这份数据按周聚合后每一行代表一周的流量总和再画图就能看出季节性轮廓。这是时间序列可视化的通用经验样本点太多时先降频再看趋势眼睛才能分辨出结构。3.3 月度差值set_index 硬对齐的风险month21.set_index(month19.index, inplaceTrue) monthdelta month19 - month21 monthdelta.plot(style[:, --, -]) plt.ylabel(19年和21年自行车数差值/月)这段代码的思路很直接把 2021 年的月索引强行换成 2019 年的月索引然后相减得到每个月两个年份的自行车计数差值。教程里这个方案能跑通是因为两个年份都是整年逐小时数据月份数完全一致。但set_index(month19.index)是有隐患的写法它是整体替换索引不做任何对齐。一旦某个月份数据缺失序列长度对不上后面的差值全部错位。更稳的做法是month21.reindex(month19.index)——它按 2019 的月份顺序重新排列 2021 的数据缺失位置自动填 NaN或者干脆用pd.concat([month19, month21], axis1)把两列放进同一张表再做列运算索引对齐交给 Pandas 处理。画差值图时style[:, --, -]表示三条线依次用点线、虚线、实线对应「总计、西向、东向」三个列。这个线型列表与列顺序一一对应如果调整了列顺序线型也要跟着换否则会张冠李戴。4. 可视化与滚动平均rolling 的 window、center 和高斯窗口调参4.1 原始数据为什么必须降频再画seaborn.set(rcrc) data.plot() plt.ylabel(自行车数/小时)这组代码画的是全量数据的原始曲线。2012 年末到 2022 年逐小时数据点接近十万直接 plot 出来就是一团密集的波形几乎看不到任何结构性信息。Python 数据分析与可视化里有一条经验当一条折线的数据点超过几千个人眼就分不清局部波动和噪声了这时正确步骤是重采样降频或者滚动平均或者两者结合。教程里紧接着画了data19.plot()和data21.plot()分别展示两个年份的原始曲线同样只用于建立直觉。真正有用的图是从weekly开始的——按周聚合后季节性趋势才浮出水面夏季高、冬季低同一季节内周间波动明显这部分波动主要来自天气。这个结论不是从原始图里看出来的是降频之后才浮现的。4.2 rolling 窗口参数window、center 和 min_periodsdaily data19.resample(D).sum() daily.rolling(30, centerTrue).mean().plot(style[:, --, -]) plt.ylabel(30天自行车数的均值)rolling(30, centerTrue)是整套代码里最值得抠细节的一行。先看参数表参数示例取值作用window30 或 30D窗口宽度int 表示观测值个数offset 表示时间长度min_periods默认等于 window窗口内最少非空值数少于它结果为 NaNcenterFalse / TrueFalse 用窗口右端做标签True 用窗口中间点win_typeNone / gaussian窗口权重函数None 是等权平均closed默认 right窗口左右边界是否闭合centerTrue是最容易忽略的参数。默认情况下第 t 个点的滚动值由“过去 window-1 个点 当前点”算出整条曲线等于向右平移了约 window/2 个周期。30 天窗口会滞后 15 天肉眼可见地右偏。centerTrue把标签移到窗口正中间消除相位滞后适合做趋势观察做预测特征时反而不能用 center因为未来数据会泄漏进历史点。min_periods默认等于 window意味着窗口内出现一个缺失值整个结果就是 NaN。数据有缺口时可以把 min_periods 降到 7 或 14 保住曲线但要清楚代价不同窗口的有效宽度不一样曲线在缺口附近会变毛糙。表格里closed参数一般用不到默认值但如果你发现窗口两端的值比预期多算或少算一小时就该检查它了。4.3 高斯窗口锯齿的根源和调参手感daily.rolling(50, centerTrue, win_typegaussian).mean(std10).plot(style[:, --, -])教程里对锯齿的解释是“窗口边缘的硬切割”。等权窗口滑动时新点进入窗口的权重从 0 瞬间跳到 1窗口末尾点权重从 1 瞬间掉到 0这两个突变量叠加在曲线上形成肉眼可见的锯齿。win_typegaussian把等权换成高斯权重窗口中间的数据点权重最大向两边平滑衰减硬边缘变成软边缘。std10控制高斯函数的宽度——std 越小权重越集中在中心平滑程度越低保留的细节越多std 越大权重分布越平坦曲线越接近等权平均。教程里 50 天窗口配 std10大约是窗口宽度的五分之一这个比例可以作为起点。win_type不是越高档越好。Pandas 支持triang、blackman、bartlett、hanning等好几种窗口函数选哪个取决于你要抑制的噪声形态。高斯窗口是通用性最好的代价是计算量比等权窗口略大——对几万行数据无感到百万级数据就要掂量了。调滚动平均参数带一点玄学我的习惯是同时画三条线原始序列、等权 30 天滚动、高斯 50 天滚动放在同一张图里对比。哪条把季节趋势表达得最清楚就选哪条而不是凭感觉定 window。5. 时间序列实例常见问题五个翻车点与对应解法5.1 resample(M) 后索引对不上日历月现象data.resample(M).sum()跑完索引里的时间戳不是月末看起来像是随机漂移的日期。原因原始小时数据里有缺失时间戳Pandas 的 M 锚点是按实际存在的索引计算的缺了几小时或几天窗口边界就会偏移。解决先data.asfreq(h)把小时频率补齐再决定填充方式——计数类数据用fillna(0)或ffill()均值类数据用interpolate()。补完频率再重采样索引就规整了。5.2 中文标签全部变成方框现象plt.ylabel(自行车数/小时)之后 y 轴标题显示为方框坐标轴负号也变成方块。原因matplotlib 默认字体 DejaVu Sans 不包含中文字形负号则受axes.unicode_minus影响。解决绘图前执行rc {font.sans-serif: SimHei, axes.unicode_minus: False}再seaborn.set(rcrc)。注意 SimHei 在 Windows 常见Linux 服务器没有这个字体需要改成 Noto Sans CJK 或文泉驿否则还是方框。5.3 dropna 之后周汇总突然偏低现象清洗阶段dropna(inplaceTrue)删除缺失行后某几周的resample(W).sum()结果明显小于相邻周。原因对小时计数数据dropna 是整行删除缺失的那一小时不是“0 辆自行车”而是“没有记录”。Pandas 重采样时跳过缺失时段周汇总自然少算了一段流量。解决计数类时序数据不要用 dropna改用data.fillna(0)。自动计数器没回传数据不等于没人骑车补 0 比删行更接近物理真实。如果确认是传感器故障且缺口很大再考虑插值。5.4 两个年份月度相减全是 NaN现象monthdelta month19 - month21执行后 DataFrame 里全是 NaN绘图失败。原因Pandas 做 DataFrame 相减时按索引对齐。2019 和 2021 的月索引虽然都落在月末附近但具体时间戳和顺序不完全一致对不上的索引位置直接变成 NaN。解决先统一索引再相减——month21.reindex(month19.index)按 2019 的月份顺序重排 2021 数据或者用pd.concat([month19, month21], axis1)让两列数据共享同一套索引再在列之间做差值。5.5 plot 的 style 线型顺序和列顺序对不上现象data.plot(style[:, --, -])画出来三条线的线型和自己想的不一样点线那条其实是东向而不是总计。原因style 列表是按 DataFrame 的当前列顺序一一对应的。前面章节里我们用data.iloc[:,[0,2,1]]调整过列顺序如果后面又改了列顺序线型自然跟着错位。解决绘图前先print(data.columns)把 style 列表顺序调整到和列顺序一致。更保险的做法是显式指定列data[[总计, 东向, 西向]].plot(...)列顺序自己控制线型不会乱。6. 分组挖掘GroupBy 小时、星期与工作日/休息日双峰对比6.1 按小时分组看日内周期by_time data19.groupby(data19.index.time).mean() hourly_ticks 4 * 60 * 60 * np.arange(6) by_time.plot(xtickshourly_ticks, style[:, --, -])data19.index.time把时间戳截成纯时间对象去掉日期按它分组再 mean就能得到一天 24 小时的平均流量。hourly_ticks 4 * 60 * 60 * np.arange(6)生成 0、4、8、12、16、20 点对应的总秒数作为 x 轴刻度位置比默认显示的 00:00:00 长格式清爽得多。从图里能看到 8 点和 17 点两个高峰这个双峰构造就是通勤的直接证据。6.2 按星期分组看工作日效应by_weekday data19.groupby(data19.index.dayofweek).mean() by_weekday.index [Mon, Tues, Wed, Thurs, Fri, Sat, Sun] by_weekday.plot(style[:, --, -])dayofweek返回 0 到 60 代表周一。分组取均值后重命名索引是纯展示需求。图上工作日流量约为周末的两倍说明这座桥主要服务通勤。6.3 工作日与休息日分开画weekend np.where(data19.index.weekday 5, Weekday, Weekend) by_time data19.groupby([weekend, data19.index.time]).mean() fig, ax plt.subplots(1, 2, figsize(14, 5)) by_time.loc[Weekday].plot(axax[0], titleWeekdays, xtickshourly_ticks, style[:, --, -]) by_time.loc[Weekend].plot(axax[1], titleWeekends, xtickshourly_ticks, style[:, --, -])np.where(条件, Weekday, Weekend)生成一个和索引等长的标签数组groupby 的列表参数支持按「工作日/休息日 小时」两级分组。两张子图对比工作日是清晰的早 8 晚 5 双峰休息日只有一个平缓峰。再细看东西双向的峰值差早高峰东向西流量更大晚高峰西向东更大说明通勤主流方向是早上进市区、晚上出市区。注意np.where 生成的标签数组顺序必须和 data19 的索引顺序一致不要单独排序后再传入 groupby否则分组会错位。这套思路稍加扩展就能用于时间序列预测任务从 index 提取 hour、dayofweek、is_month_end 这类特征把流量序列转成监督学习样本滚动平均后的趋势列可以作为特征工程的底子。教程止步于分析但正好给了预测一个干净的起点。从那以后我每次拿到带时间戳的数据集都会强制自己走一遍这套流程read_csv 带 parse_dates、sort_index、查缺查重、resample 降频、groupby 分组。这套组合拳打完数据的基本结构就清楚了后续建模才能把精力放在真正的问题上。希望帮到你。本文还有配套的精品资源点击获取
