拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Pandas与Matplotlib实战:从数据清洗到专业图表美化

Pandas与Matplotlib实战:从数据清洗到专业图表美化

先交代背景,再上手操作。我一直觉得 Pandas 和 Matplotlib 是一对黄金搭档:前者负责把乱七八糟的原始数据整理成规规矩矩的结构化表格,后者负责把这些表格变成能一眼看懂规律的图表。但很多人在实际项目里只停留在“能用”阶段,图是画出来了,离“能看”“能汇报”还有距离。这篇就把我从数据读取、清洗,到出图、美化,再到处理各种中文乱码和坐标系坑的完整流程走一遍,适合刚接触数据分析、以及已经在用但想提升图表质量的读者。

我默认你已经装好了 Python 环境,版本建议 3.8 以上。下面所有代码都用 Jupyter Notebook 写,但换成 PyCharm、VS Code 甚至命令行脚本都成立,只是展示效果略有差别。

1. 环境准备:先装对库,再谈可视化

很多人一上来就pip install pandas,结果运行时报错找不到模块。这种事我见得太多了,十有八九是环境搞混了——电脑里装了多个 Python,或者用 IDE 自动创建的虚拟环境和当前终端不是同一个。

1.1 Pandas 和 Matplotlib 的安装方式

如果你用的是 PyCharm,最稳妥的操作顺序是:

  1. 在 PyCharm 右下角查看当前解释器路径,确认用的是哪个 Python 环境。
  2. 打开左下角 Terminal(终端),输入pip install pandas matplotlib。如果终端里看到的 Python 和解释器不是同一个,就切换到解释器对应的Scripts目录再执行。
  3. 安装完成后,在终端里直接执行python -c "import pandas; import matplotlib; print(pandas.__version__, matplotlib.__version__)"。能打印出版本号,说明环境完全没问题。

如果你用 Anaconda,推荐在 Anaconda Prompt 里执行conda install pandas matplotlib,它会自动解决依赖,不会出现缺numpy、缺python-dateutil这类连锁问题。

国内网络环境下,pip下载慢可以用镜像源加速。我常用的命令是:

pip install pandas matplotlib -i https://pypi.tuna.tsinghua.edu.cn/simple

-i参数临时指定清华源,不用改全局配置,安全省事。

1.2 import 的规范写法

环境装完,进入代码环节。Pandas 和 Matplotlib 的组合已经成了业界默认配置,别名也基本统一:

import pandas as pd import numpy as np import matplotlib.pyplot as plt import matplotlib as mpl

pd、np、plt这三个别名是无数项目沉淀下来的约定俗成,没人会直接import pandas然后用pandas.DataFrame()去写,太啰嗦。matplotlib单独导入时通常是为了访问rcParams等全局配置。

1.3 笔记本环境的两个前置魔法命令

如果你在 Jupyter Notebook 里,建议先执行两条魔法命令:

%matplotlib inline plt.rcParams['figure.dpi'] = 120

%matplotlib inline让图表直接嵌入单元格输出区域,figure.dpi=120能把默认图片分辨率从 72 提高到 120,字不会发虚。这两条不写也不影响运行,但写了对观看体验影响很大,尤其是后续要调整细节时。

注意:如果你使用 VS Code 或 PyCharm 的 Python 终端,%matplotlib inline会报错,那说明你没在 Notebook 环境里运行,忽略即可。

2. 数据入口:让结构化数据变成能画图的 DataFrame

拿到手的数据十有八九是 Excel、CSV,或者几十行对着 API 文档调接口拉下来的 JSON。在画图之前,先要把这些数据变成规整的DataFrame——这是 Pandas 的核心数据结构,也是后续一切操作的基础。

2.1 读取 CSV 和 Excel 的常见坑

我先用一份农产品价格数据做演示,这种数据很典型:有日期、有品种名称、有价格,还有产地,适合做各种图形展示。

df = pd.read_csv('vegetable_prices.csv') df.head()

第一版代码很容易遇到两类报错:

  • UnicodeDecodeError: 'utf-8' codec can't decode byte 0xd6...:文件实际是 GBK 编码,但read_csv默认用 UTF-8 解码。解决办法是pd.read_csv('vegetable_prices.csv', encoding='gbk')。如果你不确定文件是什么编码,可以用记事本打开文件另存为,看右下角编码方式,或者用chardet库自动检测。
  • ParserError: Error tokenizing data:文件里混入了分隔符不一致的行,比如某些行多了逗号。解决办法是先pd.read_csv(file, sep=None, engine='python')让 Pandas 自动推断分隔符,再人工确认。

Excel 文件同理:

df = pd.read_excel('vegetable_prices.xlsx', sheet_name='Sheet1')

sheet_name参数常被忽略,但如果文件是多 Sheet 的,不指定就默认读第一个,数据往往不是你想要的。建议养成显式指定sheet_name的习惯,少踩坑。

2.2 数据类型转换:

2.2 数据类型转换:

别让"看起来是数字"骗了你

读进来的列不一定是正确的数据类型。最典型的场景:价格列在 Excel 里带了个"¥"符号,读进来变成字符串;日期列被读成 object 类型;数量列里混了空格变成 object。此时如果直接df['price'].plot(),Pandas 会尝试隐式转换,但一旦遇到无法解析的值就报错或者直接忽略。

所以画图前的必修课是显式转换:

# 去掉价格中的货币符号,并转为数值 df['price'] = df['price'].astype(str).str.replace('¥', '').astype(float) # 日期列统一成 datetime 类型,方便后续按时间筛选和排序 df['date'] = pd.to_datetime(df['date'])

这里有个容易懵的点:为什么astype(str)在前面?因为原始数据可能是 float 或 int,直接调用.str.replace会报错AttributeError。先变成字符串,再替换,再转数字,三步走是处理混合类型列的通用套路。

pd.to_numeric也是好帮手,尤其当列里混入了"约 3.5"、"3.5 kg"这类文本时,可以配合errors='coerce'把解析不了的值变成 NaN:

df['price_clean'] = pd.to_numeric(df['price'], errors='coerce')

转换完之后养成检查df.info()的习惯,看到price是float64、date是datetime64[ns],心里才踏实。这一步没做对,后面画什么图都是错的。

2.3 数据清洗:缺失值和重复值处理

结构化数据不一定干净,缺失值、重复值、异常值都有可能出现。画图之前不处理,图上的折线会出现断点,柱状图会突然缺一根,散点图会出现离谱的离群点。

我的处理顺序一般是这样:

# 1. 看缺失情况 df.isnull().sum() # 2. 按业务场景决定删还是补:时间序列数据用前后均值填充比较自然 df['price'] = df['price'].interpolate() # 3. 去重,确保同一个日期、同一种菜只保留一条记录 df.drop_duplicates(subset=['date', 'item'], keep='last', inplace=True)

interpolate()是线性插值,适合价格这类连续性数据。如果缺失量太大(超过 30%),插值就不太靠谱了,建议直接把该列或该行删掉,否则图表会给人一种虚假的平滑感。

到这里,DataFrame 里的数据已经可以信任了。接下来才是重头戏——把数据映射到图形上。

3. figure、axes、axis:这三者的关系决定了你画图时的心态

在 Matplotlib 里,figure、axes、axis是三个最核心、也最容易混淆的概念。很多人代码写得乱七八糟,就是因为没搞清楚这三个东西到底谁管谁。

3.1 用画板、画框、坐标轴来理解

我习惯用一个类比:画家的工作台。

  • figure是整个画板,相当于一块空白画布。你可以在这块画布上放一张图,也可以放四张图(即 2x2 的子图布局)。figure管的是整块画布的尺寸、背景色、分辨率。
  • axes是画板上的一块区域,相当于一个画框。每个画框里有自己的坐标系、自己的数据。一个figure可以包含多个axes,子图布局的每个格子都是一个axes。
  • axis是axes内部的具体坐标轴,包含 X 轴、Y 轴,以及对应的刻度线、刻度标签。axis负责的是"这条轴上数字怎么分布、标签写什么、刻度朝里还是朝外"。

很多教程里的plt.plot()其实隐藏了这三层关系:plt会自动在当前的axes(如果没有就创建一个)上画图,而这个axes又自动放在当前的figure上。对简单脚本来说这很方便,但当你需要控制多个子图、统一图例、精细调整单个坐标轴时,这种"隐式接口"就让人抓狂了。

3.2 Pyplot 接口和面向对象接口的区别

Matplotlib 常见两种写法:

# 写法一:pyplot 风格(快捷,但控制力弱) plt.plot(df['date'], df['price'], 'o-') plt.xlabel('日期') plt.ylabel('价格') plt.title('农产品价格走势') plt.show() # 写法二:面向对象风格(推荐,控制力强) fig, ax = plt.subplots(figsize=(10, 6)) ax.plot(df['date'], df['price'], 'o-') ax.set_xlabel('日期') ax.set_ylabel('价格') ax.set_title('农产品价格走势')

两者最终效果可能一样,但第二种写法把figure和axes都显式暴露出来了。你在ax上调各种set_*方法,可以精准控制"这个子图里的 X 轴标签是什么",而不是稀里糊涂地改到别的子图上去。

3.3 为什么我强烈建议你早点转成面向对象

因为真实项目里几乎不可能只画一张孤零零的图。多数时候要画多子图对比,要放共享图例,要调整某个子图的数据范围。面向对象写法让你能轻松创建 2x2 画布:

fig, axes = plt.subplots(2, 2, figsize=(14, 10)) axes[0, 0].plot(...) axes[0, 1].plot(...)

当你需要在同一个 figure 上叠加不同数据、在四个子图之间实现相同的轴范围时,pyplot 接口会把状态管理搞得一团糟,而面向对象接口清晰得多。

从小项目开始就用fig, ax = plt.subplots()的习惯,省得以后转项目时再花时间适应。这也是社区里从新手到进阶的分水岭。

一旦理解了这三者的关系,很多「奇奇怪怪」的报错就豁然开朗了:比如你看到'AxesSubplot' object has no attribute 'plot'之类的,往往是对象没搞清楚。

4. 画图实战:折线图、散点图、柱状图、雷达图一网打尽

理论讲完,来点实战。我用同一份模拟数据演示几张最常见的图,每张都附上关键参数。你可以在自己电脑上跑一遍,然后改参数感受变化。

演示用的数据:

import numpy as np import pandas as pd # 构造模拟数据:7天2种蔬菜的价格 dates = pd.date_range('2024-01-01', periods=7, freq='D') data = { 'date': dates, 'tomato': [5.2, 5.5, 5.8, 6.0, 5.9, 6.3, 6.1], 'cucumber': [3.1, 3.3, 3.0, 3.4, 3.6, 3.5, 3.8] } df = pd.DataFrame(data)

4.1 折线图:基础中的基础,但别只画一根线

场景:看价格随时间变化的趋势,并方便叠加对比两种蔬菜。

fig, ax = plt.subplots(figsize=(10, 6)) ax.plot(df['date'], df['tomato'], marker='o', label='番茄', linewidth=2) ax.plot(df['date'], df['cucumber'], marker='s', label='黄瓜', linewidth=2) ax.set_xlabel('日期') ax.set_ylabel('价格(元/kg)') ax.set_title('2024年1月第一周易腐价格走势') ax.legend() ax.grid(True, linestyle='--', alpha=0.6) fig.tight_layout() plt.show()

关键点:

  • marker='o'给数据点画上圆点,否则线段看起来飘忽不定。
  • label参数是给这条线一个名字,后续legend()会用到。
  • linewidth=2让线宽适中,太细会显得没质感,太粗会把点线关系压住。
  • grid(True, linestyle='--', alpha=0.6)加网格,但用虚线且半透明,避免网格线喧宾夺主。

4.2 散点图:透明度、颜色映射和网格是关键

场景:想观察价格和销量之间的关系,或者看分钟级别交易价格的分布密度。

fig, ax = plt.subplots(figsize=(8, 6)) sc = ax.scatter( df['price'], df['sales'], c=df['sales'], # 用颜色映射 cmap='viridis', alpha=0.6, s=100 ) ax.set_xlabel('价格(元/kg)') ax.set_ylabel('销量(kg)') ax.grid(True, which='major', linestyle='--', linewidth=0.5, alpha=0.7) fig.colorbar(sc, ax=ax, label='销量') plt.show()

这里的alpha=0.6很关键:当散点很多且重叠时,不设透明度,密集区域就是一团纯色,完全看不出分布密度。透明度能让重叠区域颜色更深,视觉效果更好。cmap='viridis'是色盲友好的经典色图,适合绝大多数场景。

fig.colorbar(sc, ax=ax, label='销量')是加一个颜色条,让读者知道颜色深浅对应的数值大小。如果你的散点图的c是某个有业务含义的字段,务必加色条,否则图就是无源之水,别人看不懂。

4.3 柱状图:对比组别差异时最直接

场景:不同农产品在一段时间里的平均价格对比。

fig, ax = plt.subplots(figsize=(8, 6)) avg_price = df.groupby('item')['price'].mean().sort_values() ax.barh(avg_price.index, avg_price.values, color='#4C72B0') ax.set_xlabel('平均价格(元/kg)') ax.set_title('各农产品平均价格排名') plt.show()

这里用了横向柱状图barh,当类别名字较长时比竖着放好读得多。.sort_values()让柱子按数值排序,图的信息层次更好。颜色选了一个比较稳的蓝色#4C72B0`,比默认的橙色系看起来更商务一些。

4.4 雷达图:多指标维度对比的神器

场景:比较不同产品在"价格、销量、新鲜度、口碑、供应量"五个维度的综合表现。雷达图是这类多指标可视化的经典选择。

雷达图和前面的折线图不一样,它用的是极坐标系(polar)。在 Matplotlib 里需要显式设置投影。

import numpy as np import pandas as pd import matplotlib.pyplot as plt # 模拟5个维度的评分数据(0–10分) categories = ['价格', '销量', '新鲜度', '口碑', '供应量'] values_a = [8, 7, 9, 6, 5] # 产品A values_b = [5, 8, 7, 9, 8] # 产品B # 计算角度:把圆平均分成5份,并让首尾闭合 angles = np.linspace(0, 2 * np.pi, len(categories), endpoint=False).tolist() angles += angles[:1] # 闭合 values_a += values_a[:1] values_b += values_b[:1] fig, ax = plt.subplots(figsize=(8, 8), subplot_kw={'projection': 'polar'}) # 两条雷达图曲线 ax.plot(angles, values_a, 'o-', linewidth=2, label='产品A', color='#4C72B0') ax.fill(angles, values_a, alpha=0.25, color='#4C72B0') ax.plot(angles, values_b, 'o-', linewidth=2, label='产品B', color='#DD8452') ax.fill(angles, values_b, alpha=0.25, color='#DD8452') # 设置分类标签和坐标轴范围 ax.set_xticks(angles[:-1]) ax.set_xticklabels(categories, fontsize=12) ax.set_ylim(0, 10) ax.set_title('产品多维度对比雷达图', fontsize=14) # 网格和方向调整 ax.yaxis.set_grid(True) ax.grid(True, linestyle='--', alpha=0.5) ax.legend(loc='upper right', bbox_to_anchor=(1.2, 1.0)) plt.show()

雷达图有几个容易踩的坑:

  • angles = np.linspace(0, 2 * np.pi, len(categories), endpoint=False)计算角度,endpoint=False是为了让最后一个点和第一个点不重叠。
  • 画闭环必须把数据首尾相连,angles += angles[:1]和values_a += values_a[:1]就是干这个的。忘了这一步,图就只剩一条开口线,看着不像雷达图。
  • subplot_kw={'projection': 'polar'}告诉 Matplotlib 这个子图是极坐标系,ax.set_xticks(angles[:-1])让五天份标签均匀分布在 360 度上。
  • 雷达图对量纲敏感,不同指标的数值范围如果差异太大(比如一个是 0–100,一个是 0–1),建议预先做归一化或直接映射到 0–10 的评分。

把这几张图画出来之后,你的「图库」基本覆盖了日常 80% 的需求,接下来是让它们变好看。

5. 图表美化:从能看出规律到看着专业

同样的数据,Matplotlib 默认配色下和稍加修饰下的感觉完全不同。美化的目标不是花哨,而是「一眼看过去重点突出、信息层级分明」。下面是我常用的几个提升质感的手段。

5.1 颜色体系:别再只用一种默认蓝

Matplotlib 默认会把每条线按tab10色板轮换分配颜色,但你最好主动指定。好的颜色选择应该符合业务场景和数据属性。

  • 连续型数据(如价格随时间变化),用cmap='viridis'、cmap='plasma'这对色盲友好的色图。
  • 离散型分类数据,用tab10、Set2、Pastel1等定性色板。
  • 要有「重点色」意识——比如整个图里只用一种蓝色做主色,用橙色做强调色,这种「双色法」比七个颜色堆在一起专业得多。

如果你对颜色不太敏感,推荐记住几个通用色值:

场景推荐色值
主色(蓝)#4C72B0
强调(橙)#DD8452
成功/正向(绿)#55A868
警示/负向(红)#C44E52
中性(灰)#817B72

这几个来自seaborn的配色灵感,无论折线图、柱状图、散点图都能用。#开头的十六进制色值在 Matplotlib 里直接当参数传就行。

5.2 字体设置:中文乱码是新手第一道坎

Matplotlib 默认字体是 DejaVu Sans,它对中文支持得极差。你只要在图上用了中文,就会看到一堆大小不一的方框,这就是最经典的「中文乱码」问题。

解决办法是在绘图前设置全局字体:

import matplotlib matplotlib.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'WenQuanYi Zen Hei', 'Arial Unicode MS'] matplotlib.rcParams['axes.unicode_minus'] = False # 让负号正常显示
  • SimHei是 Windows 下的中文字体,Microsoft YaHei是微软雅黑,WenQuanYi是 Linux 下的文泉驿,Arial Unicode MS是 macOS 下的通用字体。实际生效的按顺序找第一个存在的中文字体。
  • axes.unicode_minus=False是防止数字里的负号变成乱码,比如横轴标签里出现-1时,如果不设置这个参数,负号可能显示成方块。

如果你在Jupyter里执行完这两行之后发现还是乱码,大概率是字体缓存没更新。这种情况下可以运行:

import matplotlib as mpl mpl.font_manager._rebuild()

或者直接重启内核。还不行就找到你系统里除了宋体之外的某一款支持中文的字体,用下面的代码拿到字体路径再手动指定:

from matplotlib import font_manager font_path = '/path/to/your/chinese_font.ttf' font_manager.fontManager.addfont(font_path) prop = font_manager.FontProperties(fname=font_path) plt.rcParams['font.family'] = 'sans-serif' plt.rcParams['font.sans-serif'] = [prop.get_name()]

不过这个属于「救火」操作,日常办公环境直接把SimHei写进 rcParams 就够了。

5.3 细节打磨:网格、边框、刻度、注释

当图形主体画好之后,真正的差距在细节。我会按这个顺序检查:

  • 网格:默认白底网格对线图很有帮助,但要把默认的实线改成虚线并降低透明度,像我前面那样grid(True, linestyle='--', alpha=0.6)。颜色太深会扰乱视觉,太浅则失去参考作用。
  • 边框:默认图表有上、下、左、右四条框线。有时候只想突出左边和底部的轴线,可以用ax.spines['top'].set_visible(False)和ax.spines['right'].set_visible(False)去掉多余的框,图表会更干净、现代。
  • 刻度:如果某个刻度标签太长(比如日期),可以手动旋转防止重叠:ax.tick_params(axis='x', rotation=45)。
  • 注释:重要的数据点(最高点、最低点、异常点)要加箭头或文字注释。ax.annotate()是个好工具:
max_idx = df['tomato'].idxmax() ax.annotate( f'最高价 {df["tomato"][max_idx]:.1f}', xy=(df['date'][max_idx], df['tomato'][max_idx]), xytext=(df['date'][max_idx], df['tomato'][max_idx]+0.5), arrowprops=dict(arrowstyle='->', color='gray'), ha='center' )

xy是箭头指向的位置,xytext是文字放置的位置。最大值的标记能让看图的人一眼抓住故事重点,不用自己去找。

  • 文字层级:标题最大(fontsize=16),轴标签次之(fontsize=12),刻度标签最小(fontsize=10)。没有层级感的图,读者不知道先看哪。

5.4 布局与保存:finished 之前先想好输出

多子图一定要用fig.tight_layout()或者constrained_layout=True,否则子图标题和坐标轴标签会互相挤压。举个例子:

fig, axes = plt.subplots(2, 2, figsize=(12, 10), constrained_layout=True)

constrained_layout是 Matplotlib 3.1 之后引入的新布局引擎,比tight_layout更智能。它会在每次绘图时自动调整子图之间的间距,省去了手动adjust_subplots的麻烦。多子图的时候,优先用它。

保存图片时,savefig有几个参数值得养成习惯:

fig.savefig('output.png', dpi=200, bbox_inches='tight', facecolor='white')
  • dpi=200:屏幕显示一般 150–200 就够了,印刷要 300。
  • bbox_inches='tight':自动裁掉图片周围的留白,这个在保存整张图时非常实用。如果你觉得留白还不够多,可以不加这个参数。
  • facecolor='white':有些环境下保存的图背景是透明的,贴到 PPT 里会出现黑色文字看不清的问题,显式指定白色背景最稳。

如果你想给设计师或前端提供更灵活的格式,可以保存为 SVG 矢量图:fig.savefig('output.svg')。SVG 无限缩放不糊,而且可以直接用代码改颜色、线条宽度,适合需要二次处理的场景。

6. 踩坑记录:最容易让人崩溃的几个报错和解法

做数据可视化,最折磨人的不是不会画,而是画的过程中各种奇奇怪怪的报错。我挑几个高频问题,附上排查思路。

6.1 中文乱码问题:不是代码问题,是字体问题

报错形式:图片里的中文全部变成小方框,但终端不报任何错误。

原因:Matplotlib 默认的字体里没有中文字形。它只是「画不出来」,所以不会像FileNotFoundError一样明确告诉你哪里错了。

排查链路:

  1. 先执行matplotlib.rcParams['font.sans-serif'],看返回的字体列表是什么。
  2. 查看系统字体目录C:\Windows\Fonts(Windows)或/System/Library/Fonts(macOS)或/usr/share/fonts(Linux),确认中文字体是否存在。
  3. 在代码最开头加上前文的两行rcParams设置,再重启内核验证。

经验:设置中文字体这件事,最好写进项目统一的配置文件里,而不是每次复制到头部。我这里就维护了一个style.py,包含字体、默认颜色、默认 DPI 等所有全局设置,每个分析脚本 import 一下就行。

6.2 图像显示不出来:后端和显示环境的锅

报错形式:plt.show()执行后,屏幕没有任何反应;或者 Jupyter 里图片正常但脚本运行不出来。

原因:Matplotlib 有多个「后端」,有的负责生成 PNG 图片,有的通过 GUI 窗口实时交互显示。如果你的环境没有安装对应的 GUI 依赖(比如 tkinter),或者后端设置不对,show()就不会弹出窗口。

排查链路:

  1. 在脚本里加print(matplotlib.get_backend()),看当前用的什么后端。
  2. 如果是agg,它是非交互后端,只负责保存图片,plt.show()没有窗口效果是正常的。
  3. 想交互展示,可以用plt.switch_backend('TkAgg')或plt.switch_backend('QtAgg');若没有交互需求,show()可以省略,直接用savefig()保存图片也一样。

经验:我日常大部分场景在 Jupyter 里%matplotlib inline,图直接嵌在输出里;只有调试时需要放大看某个细节,才会切到交互后端,否则不会为「没弹窗」浪费时间。

6.3 Pandas 绘图报错KeyError或数据范围不对:先查数据类型

报错形式:画柱状图时报KeyError: 'sales',但明明数据表里有sales列;或者图出来了,但横轴变成 0、1、2、3… 而不是日期。

原因:前一种情况是列名大小写、空格或编码问题,比如列名是' Sales'而你在代码里写的是'sales'。后者是date列类型不是 datetime,Pandas 把日期当成普通字符串,plot 时排了序但不识别日期格式。

排查链路:

  1. print(df.columns.tolist())看真实列名列表,确认拼写和空格。
  2. df.info()看date的 dtype,如果不是datetime64[ns],用pd.to_datetime()转换。
  3. 检查是否有重复列名,如果有,df.columns会同时出现两个相同字段,Pandas 会把它们处理成sales、sales.1,也会引发 KeyError。

经验:数据清洗阶段多花 10 分钟检查 dtype 和列名,后面画图能省 1 小时。真别嫌麻烦。

6.4AttributeError: 'AxesSubplot' object has no attribute 'plot'怎么回事

这个报错本质是对象上不存在plot方法。最常见的原因是你把axes和pyplot混用了:比如ax = plt.subplot()(这是返回单一 axes 的用法),结果又写了ax.plot(...),但某个环境下plt.subplot返回的是AxesSubplot,这是支持的;如果你写成plt.subplots(1, 2)返回的是包含两个 axes 的数组,自然没有.plot方法。

正确做法:如果用了subplots(1, 2),就通过索引访问每个子图axes[0].plot(...);如果只画一张图,fig, ax = plt.subplots()是最保险的。看到AxesSubplot报错,先检查变量到底是不是单个对象。

6.5 图例位置总是不理想:掌握bbox_to_anchor

Matplotlib 的legend()有loc参数,比如loc='upper right'。但多子图或小图里,默认位置经常遮住数据。我常用的补救方法是:

ax.legend(loc='upper left', bbox_to_anchor=(1.02, 1.0))

bbox_to_anchor=(1.02, 1.0)把图例放到坐标轴外部的右侧。这样既不会挡住数据,也不会和标题挤在一起。当图例里项目比较多时,ncol=2参数还可以让图例分成两列,节省空间。

图例是图表的「说明书」,不放会让人看不懂,放不好会遮住重点。建议每次画完图,第一眼先检查图例位置。

7. 从单张图到「可视化报告」的工作流

你已经能画出一张精美的图了,但真实项目往往需要输出一份可视化报告。这时候就不只是画图,而是要考虑整体结构。我把日常最常用的工作流做一个总结。

7.1 先理清业务问题,再选图表类型

很多时候选错图不是因为不会画,而是不知道这个业务问题该用什么图表达。我的判断逻辑大致如下:

业务问题适合的图例子
随时间变化的趋势折线图价格走势、用户增长
各个类别的对比柱状图/条形图不同产品销量对比
两个变量之间的关系散点图价格与销量的相关性
多指标综合对比雷达图产品多维度评分
占比构成饼图(慎用)/堆叠柱状图各渠道流量占比
地理分布地图各城市门店数量

饼图我用得比较少,因为它不适合表达精确数值差异,尤其当份额接近时很难从扇形角度看出大小区别。优先考虑堆叠柱状图或水平条形图,更准确。

7.2 建立清晰的画布布局

一张图一个故事,一页多图则要讲究叙事顺序。我习惯先写结论,再放图佐证。比如:

  • 第一张图放整体走势,让读者看到大趋势。
  • 第二张图放分类对比,展示差异。
  • 第三张图放细节点,如异常值或最高点。

代码布局上,用constraXed_layout=True和fig, axes = plt.subplots(2, 2, ...)就能实现多数场景。

7.3 把 Pandas 和 Matplotlib 的项目代码组织好

一个常见的坏习惯是把所有分析和绘图代码堆在一个单元格或一个.py文件里,改一个参数就得翻几百行。我习惯按下面结构组织:

project/ ├── data/ │ └── prices.csv ├── src/ │ ├── load_data.py # 读取 + 清洗 │ ├── analysis.py # 数据聚合、透视 │ ├── plot_style.py # 全局样式配置 │ └── make_charts.py # 生成所有图表 └── output/ └── charts/ # 保存图表

基本思路是:数据加载和清洗归一处,样式配置归一处,画图归一处。这样当你换了新数据,只需要改load_data.py;当你想统一换配色,只需要改plot_style.py。

8. 一个完整案例:从原始数据到成品图

前面讲了不少零散的技巧,最后我用一个完整的示例串起来,帮助你把流程理顺。场景是:拿到一份过去一年某种蔬菜的日价格数据,需要做一张「年度价格走势+月度均值+异常值标注」的复合图。

import pandas as pd import numpy as np import matplotlib.pyplot as plt import matplotlib.dates as mdates # 模拟数据 np.random.seed(42) date_range = pd.date_range('2023-01-01', periods=365, freq='D') price = 3 + np.sin(np.linspace(0, 8*np.pi, 365)) * 0.8 + np.random.normal(0, 0.15, 365) df = pd.DataFrame({'date': date_range, 'price': price}) # 1. 数据处理:添加月份列,计算月均 df['month'] = df['date'].dt.to_period('M') monthly = df.groupby('month')['price'].mean() # 2. 图形布局:创建一个 figure 和一个 axes fig, ax = plt.subplots(figsize=(14, 6), constrained_layout=True) # 3. 画日价格折线(半透明细线) ax.plot(df['date'], df['price'], color='#4C72B0', alpha=0.4, linewidth=1, label='日价格') # 4. 画月度均值(粗线) ax.plot(monthly.index.to_timestamp(), monthly.values, color='#4C72B0', linewidth=2.5, label='月度均值') # 5. 标注年度最高价 max_row = df.loc[df['price'].idxmax()] ax.annotate( f"最高价 {max_row['price']:.2f}", xy=(max_row['date'], max_row['price']), xytext=(max_row['date'] + pd.Timedelta(days=25), max_row['price'] + 0.4), arrowprops=dict(arrowstyle='->', color='gray'), fontsize=11, ha='center' ) # 6. 美化细节 ax.set_title('某蔬菜年度价格走势', fontsize=16, pad=15) ax.set_xlabel('日期', fontsize=12) ax.set_ylabel('价格(元/kg)', fontsize=12) ax.legend(frameon=False, loc='upper left', bbox_to_anchor=(1.02, 1.0)) # 7. 格式化横轴:每季度一个主刻度,显示成"2023-01"的格式 ax.xaxis.set_major_locator(mdates.MonthLocator(interval=3)) ax.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m')) ax.tick_params(axis='x', rotation=45) # 8. 全局网格和边框 ax.grid(True, linestyle='--', alpha=0.5) ax.spines['top'].set_visible(False) ax.spines['right'].set_visible(False) # 9. 保存 fig.savefig('sales_annual.png', dpi=200) plt.show()

这个例子把整个流程串起来了:

  • df['date'].dt.to_period('M')在 Pandas 里生成月份周期,再用groupby算出月均。
  • monthly.index.to_timestamp()是为了让月度均值能对应到折线图的 X 轴上。
  • ax.xaxis.set_major_locator(mdates.MonthLocator(interval=3))让 X 轴每 3 个月显示一个主刻度,避免了默认每天一个刻度导致的标签密集重叠问题。DateFormatter('%Y-%m')把日期格式化成2023-01这种形态。

整张图一年 365 个点,不透明度alpha=0.4用细线画在底层,月度均值用粗线画在上面,里层的趋势一目了然,外层的波动细节也保留着。这就是「数据信息分层」的典型做法。

最后说一下我的个人体会:数据可视化的核心不是工具用得多花哨,而是能不能帮自己和读者快速理解数据。Pandas 负责把数据理顺,Matplotlib 负责把理顺的数据讲成清楚的故事,这两者配合,日常 80% 的可视化需求都能覆盖。你在画图时遇到的很多所谓「报错」和「乱码」,其实都不是算法问题,而是数据格式和字体配置的问题。按照这篇文章里的流程走一遍,把数据清洗、类型转换、rcParams 设置这几个习惯养成,输出的图表质量会稳定很多。至于配色审美和布局逻辑,是另一层功夫,多临摹优秀的图表是提高的最快路径。

返回列表