做数据分析的人大概都经历过这种场景:别人丢给你一张几十万行的表,问“这批数据有什么特点”,你还在犹豫要不要打开matplotlib慢慢调图例位置的时候,旁边同事已经用一句df.plot()把趋势图甩出来了。没错,pandas不仅能做数据处理,它的内置可视化接口在探索性分析里非常能打。这篇文章就围绕pandas的数据可视化,把从最基础的绘图调用、高频场景、布局控制,到那些文档里不会写的坑,一次性讲清楚。适合刚接触pandas的初学者,也适合那些已经用pandas做数据处理、但每次画图都要临时查API的进阶用户。
1. 为什么我劝你用pandas做数据可视化(而不是一上来就学matplotlib)
很多人在学习路径上有个误区:一提到数据可视化,第一反应就是matplotlib、seaborn、plotly,反而把pandas自带的绘图能力放在一边。实际上,pandas的plot接口是日常数据分析里性价比最高的一层封装。它不是为了替代matplotlib,而是把数据绘图最常用的那一小部分操作,缩到了最短路径。
1.1 pandas绘图接口到底是给谁用的
DataFrame.plot()和Series.plot()是pandas内置的可视化入口,底层仍然调用matplotlib,但接口被设计成“顺着数据处理思路直接出图”的感觉。你不需要记住plt.figure()、plt.plot()、plt.xlabel()那一套,只要数据结构对了,一个plot()就能把坐标轴、图例、刻度全部带出来。
我个人的理解是,pandas绘图主要面向两个场景:
- 探索性数据分析(EDA)阶段,需要快速看列与列之间的关系、分布和趋势;
- 日常报表或分析脚本里,不追求复杂美化,但要稳定、可复现地输出图表。
如果你要做的是一张需要精细排版、自定义配色、复杂交互的展示图,那确实应该去用matplotlib或plotly,但在那之前,先用pandas画一版初步图绝对不亏。因为pandas图最大的优势是“和DataFrame的索引、列名天然对齐”,数据处理完,图也跟着出来了。
1.2 一张表看完pandas.plot支持哪些图
pandas的kind参数决定了图的类型,调用方式如下:
python复制df.plot(kind="line")
df.plot(kind="bar")
常用的图型我用一张表整理出来,后面随时可以回来查。
| kind参数 | 图型 | 典型使用场景 |
|---|---|---|
line |
折线图 | 时间序列、趋势变化 |
bar |
柱状图 | 分类对比、分组汇总 |
barh |
横向柱状图 | 类别名称较长时的对比 |
hist |
直方图 | 单列数值分布 |
box |
箱线图 | 离群点、分位数分布 |
kde / density |
密度图 | 连续变量分布平滑估计 |
area |
面积图 | 累积量、占比随时间变化 |
pie |
饼图 | 占比展示,慎用 |
scatter |
散点图 | 两个连续变量的关系 |
hexbin |
六边形分箱图 | 大数据量下两变量密度分布 |
其中scatter和hexbin只能用在DataFrame上,而且通常需要指定x和y。如果你拿一个Series去调df.plot(kind="scatter"),会直接报错,因为散点图至少需要两个维度。这也是刚上手时最容易踩的第一个坑。
1.3 底层仍然是matplotlib,这句话意味着什么
pandas绘图的返回值是一个Axes对象,明白这一点,你后续的所有自定义操作就都通了。df.plot()画完图之后,你可以继续在这个坐标轴上加标题、改刻度、加注释,甚至把图保存下来。
python复制import matplotlib.pyplot as plt
ax = df.plot(kind="line", figsize=(10, 5))
ax.set_title("销售趋势")
ax.set_xlabel("日期")
plt.tight_layout()
plt.savefig("trend.png", dpi=150)
也就是说,pandas帮你完成了80%的常规绘图,剩下20%的需要精修的地方,直接回到matplotlib层面处理。但反过来说,matplotlib的所有“坑”也会继承过来,比如中文乱码、负号显示异常、默认配色丑等。这些坑不解决,pandas画出来的一样难看。所以我会在后面专门用一节讲样式问题。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 从裸数据到第一张图:五分钟跑通pandas绘图全流程
先不谈复杂技巧,我们把一个最基础的流程走通,保证任何一台装有pandas的电脑都能跑出图。
2.1 数据准备:干净结构是绘图的起点
pandas绘图对数据格式其实有隐含要求,最常见的就是“宽表”。宽表的意思是:每一列是一个变量,每一行是一个样本。比如下面这个销售数据集:
python复制import pandas as pd
import numpy as np
df = pd.DataFrame({
"date": pd.date_range("2024-01-01", periods=30, freq="D"),
"sales": np.random.randint(1000, 5000, size=30),
"orders": np.random.randint(80, 300, size=30)
})
print(df.head())
如果你拿到的是多行多列交叉表,最好先把索引和列名整理干净。列名尽量不要带空格和特殊符号,否则图例、坐标轴标签会显示得很难看。这里我先用英文列名,后续再讲中文显示问题。
2.2 最常用的四种调用方式
pandas绘图的调用方式看起来很灵活,但真正高频的只有四种。
第一种,整表直接画折线:
python复制df.plot(x="date", y=["sales", "orders"], kind="line", figsize=(10, 5))
第二种,只画某一列:
python复制df["sales"].plot(kind="hist", bins=20, figsize=(8, 4))
第三种,指定x轴和y轴画散点:
python复制df.plot(kind="scatter", x="sales", y="orders")
第四种,先聚合再画柱状图:
python复制df.groupby(df["date"].dt.dayofweek)["sales"].mean().plot(kind="bar")
这里x参数非常关键。如果你不传x,pandas会默认使用DataFrame的索引作为横轴。很多初学者的数据索引是0、1、2这样的默认整数,画出来就会显得很“怪”,仿佛时间顺序没错,但横轴刻度完全没有业务含义。所以,只要横轴代表的是某个具体字段,就尽量通过x=显式指定。
2.3 中文乱码和负号显示问题处理
中文乱码是pandas绘图初学者最头疼的问题。明明数据里是中文,画出来的图却全是方块,或者负号变成了奇怪的符号。根源在于matplotlib默认字体不包含中文字符。
解决办法是在画图前设置字体:
python复制plt.rcParams["font.sans-serif"] = ["SimHei", "Microsoft YaHei", "PingFang SC"]
plt.rcParams["axes.unicode_minus"] = False
不同系统的可用字体不一样,Windows下常见的是SimHei和Microsoft YaHei,macOS下常见的是PingFang SC和Arial Unicode MS。如果不知道系统里有哪些中文字体,可以用下面的代码查看:
python复制from matplotlib.font_manager import fontManager
fonts = [f.name for f in fontManager.ttflist if "Hei" in f.name or "YaHei" in f.name or "PingFang" in f.name]
print(fonts)
设置完了还要注意,如果代码是在Jupyter Notebook里运行的,最好在绘图前重新设置一次,因为不同内核的字体缓存可能会有差异。这个步骤看似简单,但很容易被忽略,导致检查半天发现是字体问题。
3. 高频场景拆解:时间序列、分布、相关性、分组对比
pandas可视化真正让人舒服的地方,是把几个高频分析场景封装得很顺手。这一节我会按实际工作里最常见的四类需求拆开讲。
3.1 1950到1974年这种时间序列怎么画最省事
很多人一遇到年份数据就喜欢用pd.to_datetime转换,其实如果年份本身就是整数列,直接作为x轴画折线完全没问题。以1950到1974年的数据为例:
python复制np.random.seed(42)
df_year = pd.DataFrame({
"year": range(1950, 1975),
"value": np.random.randint(50, 200, size=25)
})
df_year.plot(x="year", y="value", kind="line", marker="o", figsize=(10, 5))
这里我会额外强调一点:画时间序列之前,一定要保证数据是按时间排序的。如果原始数据乱序,直接plot会画出乱七八糟的线条,像是有人在纸上乱涂。常见做法是:
python复制df_year = df_year.sort_values("year")
如果你需要把年份变成真正的时间索引,可以这样处理:
python复制df_year.index = pd.to_datetime(df_year["year"], format="%Y")
不过对于1950到1974这种稀疏年份数据,用普通整数列当x轴反而更直观,刻度可以自动按年份显示,不用额外格式化。
3.2 分布分析:用hist和box快速摸底
拿到一列数值数据后,第一件事通常是看分布。直方图和箱线图几乎可以同时用:
python复制fig, axes = plt.subplots(1, 2, figsize=(12, 4))
df["sales"].plot(kind="hist", bins=20, ax=axes[0], title="sales分布")
df["sales"].plot(kind="box", ax=axes[1], title="sales箱线图")
直方图能看出数据的整体形态,偏态、双峰、集中趋势都一目了然;箱线图则更关注中位数、四分位数和离群点。两张图配合起来,基本能代替一堆描述性统计指标。
如果你有多个数值列,可以一次画出所有列的直方图:
python复制df[["sales", "orders"]].plot(kind="hist", bins=20, alpha=0.6)
不过要注意,不同列数据量级差异很大时,直方图叠加在一张图上会看不清。这时候用subplots=True分成多个子图更合适。
3.3 相关性矩阵:scatter_matrix的隐藏用法
pandas里有一个专门用来画相关性矩阵散点图的函数,藏在pandas.plotting模块下,叫scatter_matrix。我在实际项目里用它检查多个连续变量的两两关系,比只看df.corr()的数字直观得多。
python复制from pandas.plotting import scatter_matrix
df_num = df[["sales", "orders"]].copy()
df_num["profit"] = df_num["sales"] * 0.3
scatter_matrix(df_num, alpha=0.5, figsize=(10, 10), diagonal="hist")
对角线默认是直方图,非对角线是两两散点图。如果你在Jupyter里运行,可以直接看到5x5的矩阵图。这个函数对列数比较敏感,列一多图就变得密密麻麻,所以通常选3到5个关键变量就够了。
3.4 分组对比:groupby之后直接plot
分组对比是数据分析最常用的动作。很多人的做法是先把groupby结果打印出来,然后手工抄到Excel里画图,这是最浪费时间的方式。实际上,groupby返回的结果可以直接调用plot:
python复制df["day_of_week"] = df["date"].dt.day_name()
grouped = df.groupby("day_of_week")[["sales", "orders"]].mean()
grouped.plot(kind="bar", figsize=(10, 5))
这样出来的是按星期分组的双系列柱状图。如果你希望每个系列单独一个子图,可以加subplots=True:
python复制grouped.plot(kind="bar", subplots=True, figsize=(10, 6))
使用groupby后是否重置索引,需要根据图的表达来定。如果直接画图,分组列会自动作为x轴标签;如果先reset_index()得到普通DataFrame,再指定x参数,效果也是一样的。我自己的习惯是分组后保留索引,让plot自动识别,除非后续还要做其他数据操作。
4. 进阶布局:子图、共享坐标轴、组合图与样式调整
当数据维度变多,单张图已经放不下的时候,就需要考虑子图和组合图。pandas在这一块提供了不少参数,但也有些限制。
4.1 subplots=True批量出图
subplots=True是pandas绘图里最实用的参数,适合把多列数据按同一时间轴分别展示。比如销售数据里的sales和orders,量级不一样,画在同一张图里容易让其中一条线被压扁,但分开展示就很清晰:
python复制df.set_index("date")[["sales", "orders"]].plot(
subplots=True,
figsize=(10, 6),
sharex=True
)
sharex=True会让所有子图共享同一个x轴,缩放或拖动时不容易错位。这个参数在查看时间序列时非常重要。如果你的列名本身就是图例,不需要额外处理,pandas会自动为每个子图加上标题。
4.2 用layout控制子图排列
subplots=True默认是按照一行一个子图的方式排,列多了会非常长。layout参数可以控制排列方式:
python复制df.set_index("date")[["sales", "orders"]].plot(
subplots=True,
layout=(2, 1),
figsize=(10, 6)
)
layout接收一个元组,格式是(行数, 列数)。pandas会尽量按这个布局填充子图,但如果子图数量和layout不匹配,会自动调整。一个经验是:宁可layout传出正好的格子,也不要多留空位。比如4个子图用layout=(2, 2),比layout=(4, 1)更紧凑。
4.3 组合图与双坐标轴
如果你需要把sales和orders画在一张图里,但两者量级差异大,双坐标轴是常见方案。pandas的plot本身没有twinx参数,但可以在拿到Axes对象后调用matplotlib的twinx():
python复制fig, ax = plt.subplots(figsize=(10, 5))
df.plot(x="date", y="sales", ax=ax, color="#2A5A8C")
ax2 = ax.twinx()
df.plot(x="date", y="orders", ax=ax2, color="#C44E52", linestyle="--")
ax.set_ylabel("sales")
ax2.set_ylabel("orders")
这里要注意的是,twinx()之后右侧坐标轴的图例会合并到同一个legend里,但有时候顺序会乱。为了保证图例清晰,我通常会在最后手动设置图例:
python复制lines, labels = ax.get_legend_handles_labels()
lines2, labels2 = ax2.get_legend_handles_labels()
ax.legend(lines + lines2, labels + labels2, loc="best")
组合图虽然看起来高级,但我还是要提醒一句:双坐标轴在汇报场景里容易误导读者,因为左右两侧的刻度起点不同,视觉上会放大或缩小趋势。能用单坐标轴就说清楚的时候,尽量不用双轴。
5. 那些年我踩过的坑:索引、数据类型、性能与版本兼容
pandas绘图表面简单,实际运行中会遇到很多奇怪的问题。这一节我把踩过的高频坑集中整理一下,每个坑都附上排查思路。
5.1 索引才是绘图灵魂
如果你画出来的折线图一塌糊涂,先别怀疑数据,先看索引。pandas画图的默认横轴就是DataFrame索引,这意味着索引重复、索引无序、索引类型不匹配,都会直接影响图的正确性。
比如下面这个例子,索引是重复的年份:
python复制df_dup = pd.DataFrame({
"year": [2000, 2000, 2001, 2001, 2002, 2002],
"value": [1, 2, 3, 4, 5, 6]
})
df_dup.set_index("year").plot()
这时候pandas会把它当成一行一条记录,横轴显示的是重复的2000、2001、2002,画出来的折线并不代表“每个年份对应一个值”。正确的做法是先聚合:
python复制df_dup.groupby("year")["value"].mean().plot()
另外,索引是字符串类型的年份时,pandas会按字符串排序,而不是按数字年份排序。比如"1950"和"1974"字符串排序没问题,但"1950"和"1999"这类也还好,一旦出现"1000"等带前导零或位数不一致的情况就要小心。最佳实践是先把索引转成整数或datetime类型。
5.2 数据类型不过关,图全是乱的
有时候你DataFrame里的数字列其实是object类型,底层存的是字符串,比如从CSV读进来时被识别成了文本。这时候plot也能画,但图就会乱掉:横轴刻度乱跳、颜色重复、图例异常。
排查方法:
python复制print(df.dtypes)
如果有列显示为object,但内容看起来是数字,需要转换:
python复制df["sales"] = pd.to_numeric(df["sales"], errors="coerce")
errors="coerce"表示无法转换的变成NaN,后面可以用dropna()处理。这个坑在Excel文件导入时特别常见,因为Excel单元格格式五花八门,有的数字混着中文单位,读进来就成了字符串。
5.3 大数据量绘图性能优化
几十万行数据直接plot,速度会明显变慢,而且画出来的图因为点太多,看不出信息量。这时候不要硬扛,先做降采样或聚合。
如果是时间序列,可以按小时、天、周做聚合:
python复制df.set_index("date")["sales"].resample("D").sum().plot()
如果只是想看分布,可以用sample()随机抽样:
python复制df["sales"].sample(10000, random_state=42).plot(kind="kde")
如果数据量实在太大,可以考虑先把中间结果存成parquet或feather格式,减少后续运行的I/O时间。parquet是跨语言通用的列式存储格式,在Spark生态里用得特别多;feather则更强调读写速度,适合单机分析。pandas里读取和写入都很简单:
python复制df.to_parquet("data.parquet", index=False)
df = pd.read_parquet("data.parquet")
df.to_feather("data.feather") # 需要安装 pyarrow
df = pd.read_feather("data.feather")
需要先安装pyarrow或fastparquet,一般建议直接装pyarrow:
bash复制pip install pyarrow
这一步看起来和可视化无关,但实际工作流里,先把几百万行数据转成parquet,再配合pandas绘图做探索,体验完全不同。我现在的习惯是“数据预处理后用parquet落盘,分析阶段直接用read_parquet加载,最后画图”。这样性能和磁盘空间都能兼顾。
5.4 pandas版本与Python版本适配
还有一个容易被忽视的问题是版本兼容。不同Python版本对pandas版本有要求,如果版本不匹配,安装或运行时都可能报错。目前比较常见的组合是:
- Python 3.9及以上:推荐pandas 2.x;
- Python 3.8:可以用pandas 1.5.x或2.0.x;
- Python 3.7:建议用pandas 1.3.x或1.5.x。
如果你是Python 3.10,直接装最新pandas基本没问题:
bash复制pip install pandas
如果在PyCharm里装不上pandas,很多情况下是因为当前项目解释器选错了,比如选了系统自带的Python而不是虚拟环境里的Python。我建议在PyCharm的Settings -> Project -> Python Interpreter里确认解释器路径,然后点击加号搜索pandas安装,或者直接用终端:
bash复制pip install pandas openpyxl
这里顺带装openpyxl是因为读取Excel文件需要它。如果你只是做DataFrame绘图,不读Excel,其实用不到openpyxl,但为了后续方便,我一般会一起装上。安装完成后可以用一行代码确认版本:
python复制import pandas as pd
print(pd.__version__)
如果出现类似于“numpy版本过旧”的提示,记得把numpy也一起升级。
6. 把pandas图变成可用产出:保存、样式与中文字体
自动生成的图,最终总要出现在报告、PPT或邮件里。这一节讲怎么把图保存得足够清晰,同时兼顾样式。
6.1 保存图片:别等plt.show之后才想起来
在Jupyter里,plt.show()会把图画出来,但如果你在脚本里运行,不调用show()图可能不会显示,自然也谈不上保存。保存图片推荐用savefig:
python复制fig = df.plot(x="date", y="sales", figsize=(10, 5)).get_figure()
fig.savefig("sales_trend.png", dpi=200, bbox_inches="tight")
dpi越高图片越清晰,200适合PPT,300适合打印。bbox_inches="tight"会自动裁掉周围多余的空白,保存出来的图片不会有大片白边。这个细节很关键,很多人保存的图四周一大圈空白,就是少了这个参数。
如果你已经用plt.subplots()创建了fig和ax,也可以直接:
python复制fig.savefig("chart.png", dpi=200, bbox_inches="tight")
6.2 配色和样式调整
pandas默认的柱状图颜色是一组循环的颜色,不算丑,但和你的PPT主题色未必搭。手动指定颜色是最快的方式:
python复制df.plot(
x="date", y="sales", kind="line",
color="#2A5A8C", linewidth=2, figsize=(10, 5)
)
多系列时可以用colormap参数:
python复制df[["sales", "orders"]].plot(kind="line", colormap="viridis", figsize=(10, 5))
colormap可以是matplotlib内置的颜色映射名称,比如viridis、plasma、Set2。我个人比较喜欢用Set2做分类对比,色彩温和,适合打印;用viridis做连续量,色觉障碍人群也能区分。
6.3 中文字体的最终方案
前面讲了中文字体设置,但实际工作中还会遇到一种情况:在Windows上设置SimHei没问题,部署到Linux服务器上画图又乱码了。解决办法是,在代码里指定一个系统中存在的字体文件路径,而不是依赖字体名称。
python复制import matplotlib.font_manager as fm
font_path = "/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc"
prop = fm.FontProperties(fname=font_path)
ax.set_title("中文标题", fontproperties=prop)
这种方式虽然麻烦点,但可移植性更好。如果你是在服务器上批量生成图表,建议一开始就检查一下系统里装了什么中文字体,没有就装一个。毕竟pandas的图默认只负责布局,不负责字体,这个锅得自己背。
7. 我的经验总结与下一步建议
这篇文章不是让你放弃matplotlib和seaborn,而是让你在数据分析和数据处理的日常循环里,少一些“为了画图而画图”的额外成本。我自己的使用习惯是:先用pandas的plot快速看数据,确认方向后再决定要不要用更专业的可视化工具细化。如果只是内部探索,pandas图完全够用;如果要交付给客户或放进大屏,我会把pandas处理好的数据导出成parquet或CSV,再用专业BI工具或前端图表库来做展示。
最后再分享一个实用小技巧:在Jupyter Notebook里,如果想让所有pandas图默认变好看一些,可以在开头统一设置plot.rcParams,比如统一figsize:
python复制import pandas as pd
pd.options.plotting.backend = "matplotlib"
不过大部分时候,我觉得最有效的提升方式不是背API,而是养成“先检查dtypes、再检查索引、最后画图”的习惯。这个习惯让我少踩了无数坑,希望你也能少走弯路。
