Python读取Excel日期并绘制时间序列折线图:类型转换与刻度设置实战
简介数据处理中当横轴为日期时从Excel读取数据并用matplotlib绘制坐标图常面临日期格式识别异常、刻度间距不合理等问题。这份PDF梳理了两种解决方案一种是在Excel中将日期型数据改为带单引号的文本读取后通过datetime.strptime转换适合快速出图但刻度可能非等间距另一种是直接在Python中用mdates.DateFormatter与ticker.MultipleLocator设置日期格式和显示密度同时给出解决中文不显示的rcParams配置。此外还讨论了方法一带来的横坐标非等间距、日期取整显示等副作用便于理解方案取舍。整个资源为一份PDF文件压缩包约228KB包含问题分析、完整代码、运行结果和Excel数据示例已有4643人学习适合需要处理时间序列可视化、尤其是刚接触matplotlib日期绘图的Python学习者参考。通过对比两种处理思路读者可以快速定位日期读取与坐标轴显示异常的根源并在实际项目中直接套用代码减少摸索时间。1. 日期横轴为什么让你在 Excel 绘图上翻车拿到一张 Excel里面是某产品半年来的每日销量想用 Python 画一张横轴带日期的折线图。很多人第一次跑这段代码时画出来的横轴要么是 0、1、2、3 这样的序号要么是一堆按字符串排过序的“2024/1/5”标签跟 Excel 里看到的时间顺序完全对不上。原因很直接Excel 里的“日期”本质上可能是三种东西——序列号、字符串、或者是 pandas 读进来之后被当成 object 的文本列。而 matplotlib 只认 datetime 类型类型不对横轴就只能是乱序或数字。这篇文章讲的就是把「Excel 读进来 → 日期列转标准时间类型 → 画成横轴为日期的图」这条管线走通覆盖读取时该设什么参数、to_datetime 的边界情况、刻度怎么调、以及最常见的五个翻车现场。适合每天要出报表、做数据分析和用 Python 批量出图的人新手能照步骤跑通熟手能直接拿去排查自己画的图为什么乱。2. 把 Excel 读进来pandas 读取与列类型把关2.1 用 read_excel 拿到干净数据的第一步我一般不会拿到 Excel 就直接df pd.read_excel(data.xlsx)然后df.plot()。读文件这一步参数设对了后面能省掉大半的日期类型麻烦。最常用的做法是这样的import pandas as pd # 读取指定 sheet并把日期列在读取阶段就直接转成 datetime df pd.read_excel( 销售数据.xlsx, sheet_nameSheet1, parse_dates[日期], # 告诉 pandas这一列按日期解析 dtype{销量: float64}, # 数值列显式指定类型避免 int/float 混用 engineopenpyxl, # 处理 .xlsx 文件的官方引擎 ) print(df.dtypes)这里的parse_dates是最关键的一个参数。pandas 在读到这一列时如果值是标准日期格式比如2024-01-05、2024/1/5parse_dates会直接把它转成datetime64[ns]类型。dtype参数用来显式约束销量这一列是浮点数防止某些单元格里混入了文本导致整体被读成 object。engineopenpyxl是.xlsx文件的默认选项但当你面对.xls老格式时需要改成enginexlrd。有两点在读取阶段就要盯住。第一如果 Excel 文件第一行不是列名而是标题行比如“某某公司销售报表”占了一行需要用skiprows跳过否则列名会被顶到第二行。第二如果文件里有合并单元格pandas 会把它读成 NaN这个后面要单独处理不能指望读取参数一步到位。df pd.read_excel( 销售数据.xlsx, sheet_nameSheet1, header0, # 第一行是列名默认值显式写出来更清楚 skiprowsNone, # 如果文件第 0 行是杂项标题改成 skiprows1 parse_dates[日期], )读取阶段常见的经验是宁可多设参数把类型管住也不要事后救。一旦日期列被读成 object你后面每次画图都要面对str和datetime混战的局面且出错的地方往往不是画图本身而是排序和索引对齐那个排查成本比读取时多花两行参数高得多。不过有一类情况parse_dates救不了Excel 里日期被存成了文本格式单元格左上角带绿色小三角这种值读进来是2024/01/05字符串得靠第三章的to_datetime来兜底。2.2 先看类型再动手dtype 检查是画出日期横轴的前提很多人的代码逻辑没问题问题出在df.plot()那一刻pandas 根本不知道这一列是日期。所以读取完成后第一步不是 head 看数据而是看dtypes。我每次读完 Excel 都会跑一遍这两行print(df.info()) # 看每列类型和非空数量 print(df.head(3)) # 看前几行长什么样df.info()输出里如果“日期”这一列显示object而销量那一列显示object而不是float64就说明读取阶段没处理干净。object意味着这一列是混合类型里面装的可能是字符串、可能是数字、甚至可能有空值。想画日期横轴第一步就是把object干到datetime64[ns]。这里有一个高频场景Excel 里日期列看着是日期但里面混了几个手工录入的文本比如“-”或“待定”整列就会被 pandas 读成 objectparse_dates遇到这种脏值不会报错但那一行日期会变成 NaTNot a Time。所以读取配合后面的 to_datetime 清洗才是完整方案。若你只是画图NaT会被 matplotlib 自动跳过但如果你要做时间序列运算比如 rolling 均值NaT会导致窗口错位这个后面会专门讲。3. 把任意日期写法统一成横轴日期预处理与排序3.1 to_datetime 统一格式format 参数自动识别读取阶段没拦住的那批字符串日期统一在这儿处理。pd.to_datetime是转换的核心函数但直接用不指定 format它会猜猜对大多数情况猜错你就得到一堆 NaT 而且不知道哪儿错了。我一般会先看实际的字符串样子再定 format。# 假设“日期”列里有 2024/1/5 和 2024-1-5 两种格式 df[日期] pd.to_datetime(df[日期], format%Y-%m-%d, errorscoerce) print(df[日期].dtype) # 应该显示 datetime64[ns] print(df[df[日期].isna()]) # 专门看哪些行转换失败了代码逻辑说明format%Y-%m-%d是告诉 pandas 你期望的格式%Y是四位数年份%m是两位数月份%d是两位数日期。加了 format 之后pandas 不再猜测速度更快而且不会因为某些行格式特殊导致整列推断错误。errorscoerce的作用是转换失败的值不抛异常而是变成NaT。紧接着用isna()把那几行捞出来人工看这个习惯能让你迅速定位脏数据来自哪些行。如果 Excel 里的日期是2024年1月5日这种中文格式format就要写成format%Y年%m月%d日。还有一种比较坑的使用01/05/2024这种欧美格式pandas 默认会把前面的数字当月如果你的业务是习惯日/月/年要在to_datetime里加dayfirstTrue。这部分属于格式识别里的常见分叉新手建议直接打开 Excel 看一列到底长什么样再写 format。另外注意to_datetime 处理的是字符串列但现实中还有一种情况——Excel 里日期被存成数字比如 45234。这是 Excel 的内部日期序列号1900 年 1 月 1 日为 1。pandas 读进来后如果没被 parse_dates 干预会是 int64。这种情况的处理方式不是 to_datetime而是在读取阶段就设置parse_dates或在转换时加unitD, origin1899-12-30参数做序列号换算。但说实话与其自己换算不如直接在read_excel里用parse_dates一步到位序列号交给 pandas 内部逻辑去处理。3.2 排序、去重与缺失日期横轴不乱画的三个前提日期列转成datetime64只是第一步。Excel 里的数据往往不是按时间顺序排好的尤其是手工汇总的表格可能 1 月的数据在最后或者某一天被录了两次。如果你直接拿这个 DataFrame 去画图折线会沿着原始行序画出乱跳的线看起来像心电图。# 排序 去重 重设索引三个动作一步到位 df ( df.dropna(subset[日期]) # 丢掉日期为空的行 .sort_values(日期) # 日期升序横轴才能从左到右 .drop_duplicates(subset[日期], keeplast) # 同一天多条记录只留最后一条 .reset_index(dropTrue) # 把索引重置成 0,1,2,3... )这里的dropna(subset[日期])把转换失败的 NaT 行干掉避免后面画图时出现空洞。sort_values(日期)用的是 datetime 排序跟字符串排序完全是两回事。字符串排序会把2024/1/10排到2024/1/5前面datetime 排序则会按时间真实先后排。drop_duplicates(subset[日期], keeplast)处理同一天重复记录的情况keeplast表示保留最后一行——因为很多时候手工追加的数据是修正后的晚录入的更准。这个参数按你的数据情况选如果是同一天多条记录要画在同一个点上那就不该去重而应该做聚合。聚合也是常见的预处理。如果一天有多个门店的数据你想画的是全公司每日总销量那就不能直接画原表而应该先按日期分组求和df_daily df.groupby(日期)[销量].sum().reset_index()groupby(日期)要求日期列是 datetime 类型如果是字符串它也能聚合但聚合后的分组顺序是按字符串排的不是按时间排的后面画图还是要重新排序。所以再次强调先 to_datetime再 groupby这俩顺序不能反。日期缺失的问题则视业务而定。如果某天没有销售记录画出来的折线就是断的。如果业务上需要“当天无销售就画 0”可以用asfreq重采样把缺失日期补出来df_daily df_daily.set_index(日期).asfreq(D, fill_value0).reset_index()asfreq(D)是按天重采样fill_value0把缺失日期的销量填成 0。同样的思路可以扩展成按周W、按月MS聚合。这一步不是必须的但它是「让横轴看着完整」的关键尤其当你的图表要展示给业务方看时断点会引发“那几天怎么没数据”的误会。4. 画图让 matplotlib 认准日期横轴的三个配置4.1 直接从 DataFrame 绘图plot 的 x 轴解析规则日期数据准备干净之后画图本身反而简单。pandas 内置的df.plot()是最高效的入口它内部帮你封装了 matplotlib 的调用。但有一个规则必须知道x参数必须是 datetime 类型列或者你要先把日期列设为索引。import matplotlib.pyplot as plt # 方式 A指定 x 和 y 列 df.plot(x日期, y销量, figsize(12, 5), title每日销量趋势) plt.show() # 方式 B把日期设为索引后直接绘图 df.set_index(日期)[销量].plot(figsize(12, 5), title每日销量趋势) plt.show()两种方式我都常用。方式 A 语义更直白适合两张表、多列需要分别指定时用。方式 B 更适合时间序列运算因为 pandas 的时间序列方法都要求 datetime 索引比如rolling、resample、shift这些操作在索引是日期列而非日期索引时都会出问题。日常画图推荐方式 A数据准备和后续建模推荐方式 B。figsize(12, 5)是宽和高单位为英寸。日期图建议宽度拉长因为短宽度下日期刻度很容易挤成一团。如果你的 Excel 数据跨度超过半年宽度 12 是底线数据再多就 15 或 18。图太窄时 matplotlib 为了塞下所有刻度会旋转标签或者直接跳刻度观感很差。4.2 日期刻度与标签AutoDateLocator 和 DateFormatter 配合df.plot()能用但默认刻度有时很蠢数据跨度三个月它可能给你标出好几个“Jan”“Feb”的英文月份数据跨度半年标出来的标签可能都是“2024-01”这样的长格式挤成一团。想精确控制横轴刻度就要脱离df.plot()的便捷层直接操作 matplotlib 的日期工具。import matplotlib.dates as mdates from matplotlib import dates as mdates_alt # 只起说明作用可删 fig, ax plt.subplots(figsize(12, 5)) ax.plot(df[日期], df[销量], linewidth1.5, markero, markersize3) # 设置横轴刻度定位器自动按数据跨度选择年/月/日级别 locator mdates.AutoDateLocator(minticks5, maxticks10) ax.xaxis.set_major_locator(locator) # 设置刻度标签格式只显示年和月 formatter mdates.ConciseDateFormatter(locator, formats[%Y, %b, %d, %H:%M]) ax.xaxis.set_major_formatter(formatter) plt.xticks(rotation30) # 标签旋转 30 度重叠时视觉更清爽 plt.tight_layout() plt.show()AutoDateLocator(minticks5, maxticks10)是 matplotlib 自动选择刻度间隔的机制。它会根据横轴数据的跨度决定是每 3 天标一个刻度、每一周标一个、还是每月标一个。minticks和maxticks控制最少和最多刻度数这两个参数决定了标签密度。跨度大的数据默认刻度太少会觉得信息不足调大maxticks到 12 或 15 能展示更多时间点。跨度小的数据比如只有一周的数据默认会按小时或天标这时候直接把AutoDateLocator换成DayLocator(interval1)更明确。ConciseDateFormatter的好处是标签更短2024 年 1 月的标签会显示成 “2024 Jan” 而不是 “2024-01-01”。它省去了手动对每个刻度做字符串格式化的麻烦。但如果你需要完整的2024-01-05格式用DateFormatter(%Y-%m-%d)替换即可。这里要留意ConciseDateFormatter是相对较新的 API如果你还在用比较旧的 matplotlib 版本2.2 以下会报ImportError那就退回用DateFormatter。plt.xticks(rotation30)解决标签重叠。30 度是常见起步值数据密集时可以调 45 或 60。tight_layout()是必须的否则旋转后的标签会被裁剪掉一部分。这些配置组合在一起才能做到“横轴是日期”这件事真正肉眼可见地正确和清爽。4.3 专业级配置月份主刻度、每周副刻度与网格再往前一步就是自行指定主刻度和副刻度。很多时候你要的不是自动而是强制每月标一次每周画条淡网格线。做法是手动MonthLocator加WeekdayLocatorfrom matplotlib.ticker import FuncFormatter fig, ax plt.subplots(figsize(14, 5)) ax.plot(df[日期], df[销量], color#1f77b4, alpha0.85) # 主刻度每月 1 号 ax.xaxis.set_major_locator(mdates.MonthLocator()) ax.xaxis.set_major_formatter(mdates.DateFormatter(%Y-%m)) # 副刻度每周一 ax.xaxis.set_minor_locator(mdates.WeekdayLocator(byweekday0)) # 网格主刻度实线副刻度虚线更浅 ax.grid(whichmajor, linestyle-, linewidth0.6, alpha0.8) ax.grid(whichminor, linestyle:, linewidth0.4, alpha0.4) plt.xticks(rotation45) plt.tight_layout() plt.show()MonthLocator()没有参数默认每月初标一个主刻度。WeekdayLocator(byweekday0)中byweekday0代表周一0 是周一6 是周日——如果你要每周天标副刻度改成byweekday6。主副刻度分开设置后网格线会自动分层视觉上主刻度的实线网格与副刻度的点线网格形成层次日报看趋势时能快速对应到具体周和月。这部分配置适用于周报或月报场景是我在给业务侧交付图表时最常用的一套配置。5. 日期横轴绘图的避坑指南五个高频翻车现场5.1 现象横轴标签是按字典序排的字符串你在 Excel 里看到 1 月、2 月、3 月的数据排得整整齐齐画出来却变成了 10 月、11 月、12 月、1 月……的乱序。原因日期列没有经过to_datetime转换pandas 把它按字符串排序。字符串排序下2024-10-05 2024-9-30因为字符 1 比 9 小。所有画图前没做日期类型转换的横轴全是假的。解决回到第三章df[日期] pd.to_datetime(df[日期])再重新sort_values。若你用的是df.plot()pandas 对 datetime 类型列会自动按时间排不会按原始行序画。这个坑是五个里面最常见的只要你把df.info()的输出里日期列类型从object改成datetime64[ns]它就消失了。5.2 现象横轴变成 1900 年左右的超大序列号画的图横轴是一串五位数字比如 45234或者 x 轴的刻度全是 1900-01-01 附近。原因Excel 里日期是纯数值格式单元格显示为日期但底层是序列号。read_excel如果没指定parse_dates读进来就是int64。matplotlib 把这个当普通数字画于是五位数直接铺满横轴。解决把parse_dates[日期]加进read_excel。如果文件已经读进来了用pd.to_datetime(df[日期], unitD, origin1899-12-30)把序列号转回日期。这是 Excel 日期序列号的通用换算基准1900 年 1 月 1 日对应序列号 1所以 origin 从 1899-12-30 开始。注意 Windows 版 Excel 有 1900 闰年 bug但 pandas 用这个 origin 处理绝大多数场景不会出问题除非你的日期在 1900-03-01 之前这个概率极低知道即可。5.3 现象两条线的时间轴错位同一张图里画销量和库存两条线ax.plot(df1[日期], df1[销量])和ax.plot(df2[日期], df2[库存])看起来波形对不上。原因两个 DataFrame 的日期类型不一致。一个已经是datetime64[ns]另一个是字符串或序列号。matplotlib 在画第二种时把它当成普通数值轴两条线的横轴坐标基准完全不同。更隐蔽的情况是两边都是 datetime但一个含时区datetime64[ns, UTC]一个不含时区合并时直接报错或静默错位。解决画多序列前先统一两个人的日期格式。检查df1[日期].dtype df2[日期].dtype不一致就先转一致。另外多张表要合并时统一用pd.to_datetime再merge。这个坑我见过有人在报表上用一个多月才发现数据对不齐根源就是两个 sheet 一个用了 parse_dates一个没用。5.4 现象横轴刻度挤成一团或者标签互相覆盖数据跨度 30 天或 90 天横轴标签密密麻麻堆在一起完全没法读。原因matplotlib 默认刻度策略是尽量把所有日期都标出来或者相反——在某些版本里自动跳过太多一个月才一个刻度。这是AutoDateLocator在没有额外观测区间时表现的默认行为跟数据密度不匹配。解决显式设置刻度。跨度 30 天用DayLocator(interval5)每 5 天标一个跨度 90 天用WeekdayLocator(byweekday0)每周一标一个跨度一年以上用MonthLocator(interval1)每月初标一个。再配合ConciseDateFormatter缩短标签长度。记住规则数据点密集时加密刻度跨度大时稀释刻度这个平衡没有通用解必须按你的数据周期手动试。一个不算方法的方法AutoDateLocator加上maxticks8能强制 matplotlib 最多显示 8 个刻度这个值适合大多数看趋势的日报场景。5.5 现象中文标题和图例变方块图能画出来但标题、坐标轴标签、图例里所有中文全部变成空心的方框。原因matplotlib 默认字体里没有中文字形回退到字体替换机制后找不到可用的 CJK 字体。有人说换plt.rcParams[font.sans-serif] [SimHei]就行但这是在系统装了 SimHei 的前提下。很多服务器环境、瘦客户机和 macOS 上根本没有这个字体配了也无效。解决用matplotlib.font_manager动态查找系统中文字体。但我更建议的做法是轴标签和图例直接用英文中文只出现在标题里并且标题用plt.rcParams[font.sans-serif]指定一个确认存在的字体。Windows 下常见黑体是SimHeimacOS 常见苹方是PingFang SCLinux 看有没有Noto Sans CJK SC。写代码时先查再设import matplotlib.font_manager as fm # 列出系统里所有可用中文字体名 for font in fm.fontManager.ttflist: if CJK in font.name or Hei in font.name or PingFang in font.name: print(font.name)查到后用plt.rcParams[font.sans-serif] [查到的那一个]同时设置plt.rcParams[axes.unicode_minus] False否则负号也会乱。这个配置要在plt.show()之前、所有绘图代码最开头设置。如果你只是快速出图最稳的规避方案就是标题用英文横轴标签用数字和月份缩写完全绕开中文字体问题。6. 进阶让日期横轴的图能缩放、能联动、能看趋势6.1 用 plotly 把日期横轴做成可交互的图静态 matplotlib 图适合打印和嵌入文档但做数据分析时交互式图表的优势是鼠标悬浮能看到具体某天的数值拖动缩放能看任意时间段的趋势。用 plotly express 画日期横轴只需要改数据准备阶段图形代码更短import plotly.express as px fig px.line( df, x日期, y销量, title每日销量趋势可缩放, ) fig.update_xaxes(rangeslider_visibleTrue) # 底部加一个范围滑块 fig.show()这段代码最核心的配置是rangeslider_visibleTrue它会在图底部生成一个迷你范围选择器拖动它就能快速框选任意时间段plotly 会自动缩放横轴。px.line要求x列是 datetime 类型否则它不会按时间排序所以之前的预处理步骤在这里依然有效。如果你的数据量超过几千个点plotly 的渲染会变慢这个时候用plotly.graph_objects.Scatter配合modelines并关闭标记点性能会好一些。6.2 副刻度和滚动窗口日期图里最常用的两个叠加交互式图表适合探索但正经汇报时还是静态图多。日期横轴图上两个最常叠加的元素是「移动平均线」和「双 Y 轴副刻度」。滚动平均能消除每日波动暴露趋势双轴则能同时展示销量和累计增长率这种量级不同的数据。import matplotlib.dates as mdates fig, ax1 plt.subplots(figsize(14, 6)) # 主轴每日销量 ax1.plot(df[日期], df[销量], color#1f77b4, label每日销量, alpha0.6) ax1.set_ylabel(销量件) # 计算 7 日滚动平均 df[销量_7d] df[销量].rolling(window7, min_periods1).mean() ax1.plot(df[日期], df[销量_7d], color#d62728, linewidth2, label7日平均) # 副轴累计增长率 ax2 ax1.twinx() df[累计] df[销量].cumsum() ax2.plot(df[日期], df[累计], color#2ca02c, linestyle--, linewidth1, label累计销量) ax2.set_ylabel(累计销量件) # 时间刻度统一设置 ax1.xaxis.set_major_locator(mdates.MonthLocator()) ax1.xaxis.set_major_formatter(mdates.DateFormatter(%Y-%m)) plt.xticks(rotation45) # 合并图例 lines1, labels1 ax1.get_legend_handles_labels() lines2, labels2 ax2.get_legend_handles_labels() ax1.legend(lines1 lines2, labels1 labels2, locupper left) plt.tight_layout() plt.show()rolling(window7, min_periods1)是日期图最常用的一个滑动窗口计算window7是 7 天窗口min_periods1保证数据不足 7 天的前 6 天也能算出平均值用已有数据算否则前 6 天全是 NaN折线会从第 7 天才开始画。twinx()创建共享 x 轴的第二个 y 轴对象cumsum()算累计值。这里最容易踩的坑是副轴的数据量级和主轴差很多图例合并时要先获取两个轴的图例句柄再合并否则只显示一个轴的图例。这个结构延伸出去可以玩出很多东西把rolling(7)改成rolling(30)做月趋势线把累计销量换成同比环比增速把副轴换成另一个产品的销量做对比。日期横轴的底层逻辑都是一样的——数据准备好了剩下的就是视觉呈现的选择。我在实际工作中最常用的组合就是「每日销量 7 日平均 月度主刻度」这套图既能看出单日异常又不被单日波动迷惑。如果你画出来的日期图总让人觉得“哪里怪怪的”先检查日期列类型再看刻度设置最后才是颜色和样式的调整——顺序别反了。希望这六个场景的拆解和代码能让你把日期横轴这件事做得滚瓜烂熟少走我当年走过的弯路。遇到日期图翻车时先回来看看类型检查那一步多数时候问题就出在那儿。希望帮到你。本文还有配套的精品资源点击获取