先说个我最近遇到的事。一位做区域销售的朋友,手里压着今年上半年近万条订单明细,想快速算出每个大区的回款率、环比增幅,再用图表把趋势讲清楚。他第一反应是打开表格软件,结果文件大到拖拽都卡。我告诉他,这种活计,用 Python 的 Pandas、NumPy、Matplotlib 三件套,半小时内全部搞定,还能把数据做成可复用的脚本,下次再拿到新数据,跑一遍就出报表。
这个组合,就是目前最主流、也最亲民的 Python 数据分析全栈方案。NumPy 负责底层的高性能数值计算,Pandas 提供类似表格的数据结构和清洗加工能力,Matplotlib 负责把数字变成直观的图表。三者各管一段,配合起来能覆盖从数据读取、清洗、计算到可视化的完整链条。这篇文章,我会按实际工作流的顺序,把三个库的核心用法、关键坑点和一套可直接套用的实战模板完整拆给你。无论你是刚接触 Python 的职场人,还是准备转行做数据分析,这篇文章都能帮你少走很多弯路。
1. 先认清三件套的分工:为什么要这么组合
很多初学者上来就 Pandas、Matplotlib 一顿乱学,结果学完发现还是不会做分析。问题就出在没搞明白这三个库的"人设"。
1.1 NumPy:数据分析的地基与加速器
NumPy 提供的核心东西是 ndarray,也就是高性能的多维数组对象。它和 Python 原生的 list 最本质的区别,不只是"快一点",而是整个计算模型的不同。
举个例子,你想把一组数字每个都乘以 2。用 Python 原生的列表推导式,你得写[x*2 for x in data],解释器要逐个元素执行循环。但用 NumPy,你直接写data * 2,它的底层是 C 语言实现,并且借助 CPU 的向量化指令,一次性把整个数组的元素都处理完。数据量小的时候,Python 列表可能感觉不出差异,但当数据量达到百万级,NumPy 的速度优势能拉出几十倍的差距。
更关键的在于,Pandas 的底层索引和数据结构,本身就是基于 NumPy 数组构建的。你学会了 NumPy 的切片、广播、布尔索引这些操作,再去理解 Pandas 的 Series 和 DataFrame,会觉得非常顺畅,因为它们的很多行为模式是相通的。所以我把话说在前面:想跳过 NumPy 直接学 Pandas,你在很多概念上会一直觉得"隔着一层雾"。
1.2 Pandas:把脏活累活包圆的表格处理神器
Pandas 做的事,就是帮你把 Excel 的体验搬到 Python 里,并且做得更快、更自动化。它的两个核心对象是 Series(一列数据)和 DataFrame(整个表格)。
日常分析工作里,我们有 60% 以上的时间其实不是在建模,而是在做数据清洗和准备。表头有乱码、日期格式不统一、空值一列一列的、同一列中数值和字符串混在一起,这些才是真实世界数据的常态。Pandas 中read_excel()、dropna()、fillna()、astype()、groupby()这些方法和函数组合在一起,就是一把把的刀,专门对付这种脏乱差的数据。
很多人喜欢拿 Pandas 和 Excel 做比较,我的看法是:Excel 在即点即得的手工操作上依然有优势,但 Pandas 的一切操作都是"可复现的脚本"。
这意味着你处理数据的每一步都有记录,不会出现手滑把单元格覆盖了还得靠撤销往回找的尴尬。而且当你手里有几百个结构相同的表格要处理时,Pandas 的能力就体现出价值了:Excel 你得一个一个打开操作,Pandas 只需要一个for循环批量处理。
1.3 Matplotlib:把分析结果变成人话
前面两个库帮你算出了结果,但如果结果只是一堆数字,老板看不懂,同事不直观,你的报告就失败了一大半。Matplotlib 解决的就是这个问题。
Matplotlib 的设计逻辑仿照了 MATLAB。你可以用plt.figure()建一块画布,用plt.plot()或plt.bar()在上面画折线、柱状图,用plt.scatter()画散点,再用plt.xlabel()、plt.ylabel()标注轴名,最后用plt.title()写上标题。学它的时候,脑海里要始终有一个"画布-坐标轴-图形元素"的层次感,抓住这个主干,其余都是细节。
这里要提个醒,Matplotlib 的用法有很强的惯性。它既支持类似 MATLAB 的 pyplot 过程式接口,也支持面向对象式接口。我推荐初学者从pyplot方式入手,因为它更符合直觉,代码短小精悍。但如果你想画非常复杂、精细定制的那种出版级图表,迟早也要接触面向对象的方式,比如fig, ax = plt.subplots()然后只通过ax来操作,这样能避免多个图形互相干扰的诡异问题。
1.4 一个完整数据分析项目的执行流程
把三者的角色放在一个真实流程里看会更清晰。一个典型的分析任务包含:获取数据、理解和清洗数据、核心计算分析、可视化呈现、得出结论。
- 获取数据:通常用 Pandas 的
read_csv()、read_excel()加载本地文件,或者连数据库后用read_sql()。 - 清洗与处理:用 Pandas 搞定缺失值、重复值、异常值,把日期、字符串、数值类型都校正过来。
- 计算分析:用 Pandas 的
groupby()做分组聚合,用merge()做表格关联,期间涉及到的数组级运算交给 NumPy,比如计算均值、方差、分位数、同比环比,或者做更复杂的条件筛选。 - 可视化:把分析结论放到 Matplotlib 中,绘制直方图、箱线图、折线图、堆叠柱状图等,直观呈现数据背后的含义。
就这么一条线,没有大家想象中那么高深莫测。接下来,我逐个部分带你把最关键的操作过一遍。
2. NumPy 实操:数组、广播与性能优势
2.1 从列表到 ndarray:三步建好你的数据仓库
第一步永远是导入库。注意社区约定俗成的别名,不要随意换名字,不然你的代码别人读不懂。
import numpy as np创建数组最直白的方式,就是从 Python 列表转换:
data_list = [10, 20, 30, 40, 50] arr = np.array(data_list) print(arr, arr.shape, arr.dtype)shape属性告诉你这是一个几行几列的数组,这里是(5,),表示一个含有 5 个元素的一维数组。dtype告诉你里面的元素是什么类型的,通常是int64或float64。如果在同一个数组里混入整数和浮点数,NumPy 会自动统一成浮点数类型,这叫类型提升,是为了不丢精度。
二维数组的创建方法也很常用,特别适合表达类似"每家门店每天的销售额"这样的数据表:
matrix = np.array([ [100, 120, 90], [130, 80, 110], [95, 105, 115] ]) print(matrix.shape) # (3, 3)还有几个创建特殊数组的函数要记住:np.zeros((3, 4))生成全 0 数组,np.ones((2, 5))生成全 1 数组,np.arange(0, 10, 2)生成从 0 开始到 10 结束、步长为 2 的等差数组,np.linspace(0, 1, 5)生成从 0 到 1 之间均匀分布的 5 个数。
这些函数在初始化参数或者生成用于测试的模拟数据时非常实用。
2.2 numpy 和 list 比快在哪:向量化操作的真相
刚才说了 NumPy 快,但快在哪、为什么快,得讲清楚。这里要打破一个误解:NumPy 本身并非所有场景都一定比 list 快。在数据量很小的时候,NumPy 创建数组的开销反而比 list 直接操作还要慢一点。它的绝对优势体现在大规模数值计算上,有两大支柱。
支柱一是底层用 C 语言实现数组运算逻辑,避免了 Python 解释器逐行逐条执行的解释开销。
支柱二是 SIMD(单指令多数据)技术。CPU 能同时对数组中的多个数据执行同一条指令,相当于一条流水线上同时处理多个工位,而不是一个接一个处理。
举个具体例子来看速度差异:
import time big_list = list(range(1_000_000)) big_arr = np.array(big_list) # Python list 求和 start = time.time() total = sum(big_list) print("list 耗时:", time.time() - start) # NumPy 求和 start = time.time() total = big_arr.sum() print("NumPy 耗时:", time.time() - start)在我自己的笔记本上,NumPy 的耗时大约是 list 方式的几十分之一。这种性能差距在真实项目中意味着,一份几百万行的销售数据,你用 Pandas 做分组计算时,底层如果有一百个 NumPy 运算在进行,累积起来的加速效果是极其可观的。
2.3 切片、布尔索引与广播:数据分析高频动作
切片是取数组子集的最基础操作。和 Python 列表相似,但支持多维切片:
arr = np.arange(1, 13).reshape(3, 4) # array([[ 1, 2, 3, 4], # [ 5, 6, 7, 8], # [ 9, 10, 11, 12]]) sub = arr[1:, 2:] # 取从第1行到最后、第2列到最后 # array([[ 7, 8], # [11, 12]])布尔索引是数据分析里用得最多的筛选方式。它的思路是构造一个"条件为 True/False"的掩码数组,然后直接把布尔数组放进方括号里:想挑出大于 5 的元素,像这样:
mask = arr > 5 print(arr[mask]) # 返回所有大于5的元素的一维数组你也可以组合多个条件,比如(arr > 3) & (arr < 10)。注意这里必须用&而不要用 Python 逻辑运算符and,因为 NumPy 的运算需要逐元素处理。
广播机制是 NumPy 最优雅的设计之一,用一句话概括就是:不同维度的数组做算术运算时,NumPy 自动把较小数组的维度扩展,使两者形状匹配。举个最常见的例子:
a = np.array([1, 2, 3]) b = 10 print(a + b) # [11 12 13]这里b这个标量自动广播成了[10, 10, 10]。再比如你想对每个销售区域统一上调 5% 单价,一行代码就能完成:
price_array = np.array([98.0, 120.0, 88.0, 56.0]) adjusted = price_array * 1.05广播让代码更简洁,也免去了手写循环的麻烦。但要留意一点,广播虽然强大,却也有严格的维度规则:两个数组从尾部维度开始比较,只有维度大小相等或其中一个为 1 时才算兼容。如果不符合规则,NumPy 会直接抛异常,这是好事,因为它帮你提前发现了数据形状的问题。
2.4 常用统计函数:聚合计算的起手式
NumPy 内置的统计函数用起来非常顺手,它们是数据分析高频操作的"原子能力"。np.mean()求均值,np.median()求中位数,np.std()求标准差,np.var()求方差,np.sum()求和,np.min/max()求最值,np.percentile()求分位数。
有个细节要注意,np.mean()和arr.mean()这两种写法都能用。它们几乎等价,但arr.mean()是数组对象的方法,写起来更短;np.mean(arr)是全局函数,当你需要对某个轴(axis)求均值时,两种写法都能传axis=0或axis=1参数。
axis参数是新人最容易懵的地方。简单记法:axis=0表示沿着行的方向移动,也就是对每一列做聚合;axis=1表示沿着列的方向移动,也就是对每一行做聚合。举个例子:
sales = np.array([ [10, 20, 30], [40, 50, 60], [70, 80, 90] ]) col_mean = sales.mean(axis=0) # 每列均值 -> [40. 50. 60.] row_mean = sales.mean(axis=1) # 每行均值 -> [20. 50. 80.]这两个结果就分别对应"按列求平均"和"按行求平均"。实际工作中,把一张表看成"行是记录、列是字段",你要按字段求统计量就用axis=0,按记录求统计量就用axis=1,记这个场景比硬背定义管用得多。
3. Pandas 核心细节:数据结构、数据类型与清洗
3.1 DataFrame 和 Series:像操作 Excel 一样操作数据
Pandas 的 DataFrame 是一个带行索引和列名的二维表格,能装下不同类型的数据。创建方式非常灵活:
import pandas as pd # 通过字典创建,键为列名,值为列表 df = pd.DataFrame({ "区域": ["华东", "华南", "华北", "西南"], "销售额": [12000, 15000, 9800, 8600], "成本": [8000, 9500, 7000, 6500] }) print(df)每一列抽取出来都是一个 Series。Series 可以理解为"带索引的一维数组"。df["销售额"]返回的就是 Series,它保留了 DataFrame 中的行索引。Series 和 NumPy 数组在运算上兼容,这意味着你可以对 Series 做sales * 1.1这种向量化操作,结果会保留原来的索引。
DataFrame 最值得称道的地方,是loc和iloc这两个索引器。df.loc[行标签, 列名]按标签取值,df.iloc[行位置, 列位置]按位置取值。很多新手在这里会混乱,给你一个口诀:有字母用loc,是数字位置用iloc。
# 取第一行区域字段的值 print(df.loc[0, "区域"]) # 取第一行第一列的值 print(df.iloc[0, 0])loc还可以做很优雅的筛选。比如找出销售额超过 10000 的所有行:
filtered = df.loc[df["销售额"] > 10000]3.2 pandas 数据类型转换:astype 与 to_numeric 的正确姿势
数据分析中最烦人的问题之一,就是"表面上看是数字,实际是字符串"。比如从 CSV 里读出的销售额列,里面混入了一些带逗号的文本"12,000",或者从数据库导出的百分数带了 "%" 符号,Pandas 会默认把这些列读成object类型(本质上就是字符串)。
处理这个问题要分两步走。第一步,先看清当前列的 dtype:
print(df["销售额"].dtype)如果是object,可以用astype()尝试转换:
df["销售额"] = df["销售额"].astype(float)但astype()非常"死板",如果列里有任何一个非数字字符,它就直接报错。这时候更稳妥的办法是用pd.to_numeric(),这个函数有errors参数,可以设置为'coerce',表示遇到无法解析的值时,不报错而是置为 NaN:
df["销售额"] = pd.to_numeric(df["销售额"], errors="coerce")接下来你再回过头用fillna()或者dropna()把缺失值处理掉。这两个场景要分清:数据规整、格式统一时用astype(),应对脏数据、不确定能否转换时用to_numeric(errors="coerce")。
日期列的转换也同样常见:
df["日期"] = pd.to_datetime(df["日期"], errors="coerce")转成 datetime 类型之后,你才能方便地提取年月日、计算时间差,或者按月份分组。
3.3 数据清洗:处理缺失值、重复值和异常值
清洗这一步是最能体现数据分析工程师功夫的地方。缺失值处理的核心决策是"删还是填"。删,就是dropna();填,就是fillna()。
- 如果某一行只有日期和 ID,其他关键字段全是空的,这种行留着没有分析价值,直接删除。
- 如果某列的缺失值是少量零星出现,比如偶发的录入遗漏,用平均值、中位数或众数填充,可以尽量保留样本量。
- 如果缺失是时间序列数据里常见的情况,像每日监测的传感器数据缺了一个小时的记录,用
ffill()(前向填充)或bfill()(后向填充)更符合业务逻辑。
举个例子:
df_clean = df.dropna(subset=["销售额"]) df["备注"] = df["备注"].fillna("无") df["库存"] = df["库存"].fillna(df["库存"].mean())重复值用duplicated()判断,用drop_duplicates()删除:
# 查看重复数量 print(df.duplicated().sum()) # 按"订单号"判断重复,保留第一条 df = df.drop_duplicates(subset=["订单号"], keep="first")异常值的处理则讲究"先看再动"。推荐先画个箱线图,或者用describe()看描述统计:
print(df["销售额"].describe())describe()会输出 count、mean、std、min、25% 分位、50% 分位、75% 分位和 max。这些数字能帮你快速判断数据分布是否有异常,比如最大值的量级是平均值的几十倍,就要考虑是不是录入了错误数据。
对于明显的异常值,可以做截尾或者替换,比如把超出 99% 分位的数据替换为 99% 分位对应的值。不过注意,这类操作必须有业务逻辑支撑,不要为了"清洗"而清洗,把真实的业务波动当成脏数据删掉了。
3.4 分组聚合初步:groupby 替代透视表
Pandas 的groupby()是对应 Excel 透视表的概念,但更灵活。基本语法就是:df.groupby("分组列")["聚合列"].聚合方法()。看个例子:
order_df = pd.read_excel("orders.xlsx") result = order_df.groupby("区域")["销售额"].sum()这行代码就按区域分组,统计每个区域的销售额总和。你还可以一次算多个指标,像这样:
result = order_df.groupby("区域").agg( 销售额总和=("销售额", "sum"), 订单数=("订单号", "count"), 平均客单价=("销售额", "mean") )agg()这个函数很强大,你在括号里重命名了输出列名,也指定了聚合规则。对于初学者来说,吃到groupby() + agg()这一套组合,就已经能应付日常 80% 的分组统计需求。
4. Matplotlib 可视化:从基础图形到高级定制
4.1 快速上手:解决"画不出来"的问题
Matplotlib 安装很简单,还记得前面说吗?
import matplotlib.pyplot as plt import numpy as np x = np.linspace(0, 10, 100) y = np.sin(x) plt.plot(x, y) plt.title("正弦曲线") plt.show()这段代码会弹出一个窗口展示你画出的正弦曲线。做数据分析时,通常我们是在 Jupyter Notebook 或 VS Code 的 Python 交互式窗口中运行代码。如果在 Jupyter 中,加上一行魔法命令%matplotlib inline,图形就直接显示在单元格下方,不用单独弹窗。
入门阶段,你最需要记住的就是三张图的画法:折线图看趋势,柱状图看对比,饼图看占比。三个函数分别是plt.plot()、plt.bar()、plt.pie()。记得画完一定要plt.show(),尤其当你在脚本里运行时,没有这行,图形窗口不会弹出。
4.2 matplotlib颜色配置:不要再用默认配色了
要我说,Matplotlib 默认的配色确实有点"上个世纪"。好消息是,你不需要另外学一个绘图库,只需掌握几个关键参数,就能让图表的高级感提升不少。
color参数可以接受多种写法:英文单词'red',单字母'r',十六进制'#FF5733',RGB 元组(0.1, 0.2, 0.5),甚至是灰度值。在实际展示中,我更推荐使用十六进制颜色,因为它能精确控制色值,并且方便设计师或老板直接查看。
有一组颜色值值得记下来,视觉柔和又不辣眼睛:
| 用途 | 色值 |
|---|---|
| 主色(蓝) | #2E86AB |
| 强调色(橙) | #F18F01 |
| 成功色(绿) | #43B02A |
| 警示色(红) | #C73E1D |
| 中性色(灰) | #6C7A89 |
不过你以为这就完了吗?真正让图表拉开档次的是全局颜色风格。Matplotlib 自带了seaborn风格的配色。在绘图前加一行:
plt.style.use("seaborn-v0_8-whitegrid")或者使用内置的ggplot、fivethirtyeight风格,图表瞬间会变得专业起来。我个人偏爱seaborn-v0_8-whitegrid,网格线清晰的图在数据分析报告里非常友好,读者的目光能很自然地沿着参考线落到数据点上。
4.3 中文显示与字体问题:一个必须提前处理的坑
刚入门用 Matplotlib 画图,基本都会遇到一个灵魂问题:图上中文全变成了一个个小方块。这是因为 Matplotlib 默认的英文字体里根本没有中文字形。
这个问题不是我一个人遇到过,很多开发者在初始化环境时都要处理一到两次。最简单的解决方案是先看系统中有哪些可用字体:
from matplotlib import font_manager font_manager.fontManager.addfont("/System/Library/Fonts/PingFang.ttc") # macOS示例但在 Windows 上,中文字体一般是SimHei,你可以这样设置:
plt.rcParams["font.sans-serif"] = ["SimHei"] plt.rcParams["axes.unicode_minus"] = False # 解决负号显示为方块的问题如果你在服务器上运行脚本,系统里不一定有中文字体,这时可以手动下载一款开源中文字体(比如思源黑体)放到指定目录,然后用font_manager.fontManager.addfont()注册。记得axes.unicode_minus那里也要设置为False,不然坐标轴的负号也会显示成乱码或方框。
4.4 坐标轴问题:横坐标太密集怎么办
真实数据的时间序列,横轴可能会挤成一团,比如你要画一年 365 天的销售趋势,如果不处理刻度,横轴上的日期标签会叠成一根黑色的"粗线"。这就是热搜里频繁出现的"matplotlib画图横坐标太密集"问题。
碰到这种情况,可以限制横轴刻度的数量:
import matplotlib.dates as mdates # 假设 x 是日期序列 fig, ax = plt.subplots(figsize=(10, 5)) ax.plot(x, y) ax.xaxis.set_major_locator(mdates.MonthLocator()) ax.xaxis.set_major_formatter(mdates.DateFormatter("%Y-%m")) plt.xticks(rotation=45)这里的MonthLocator()表示只显示每个月的刻度,DateFormatter控制显示格式为"年-月",rotation=45让标签倾斜 45 度防止重叠。如果你是普通数字横轴,也可以手动指定要显示的位置:
plt.xticks([0, 50, 100, 150, 200])或者更简单地每隔 N 个显示一个:
plt.xticks(range(len(x))[::30], rotation=45)4.5 画布与子图:一张图里放多个信息
在分析报告中,经常需要在一张画布里放多个子图。这里就凸显了面向对象接口的价值。用plt.subplots(nrows=1, ncols=2)一次创建一行两列的子图:
fig, axes = plt.subplots(1, 2, figsize=(12, 4)) # 左边画柱状图 axes[0].bar(categories, values) axes[0].set_title("各区域销售额") # 右边画折线图 axes[1].plot(months, sales_trend) axes[1].set_title("月度销售趋势") plt.tight_layout() plt.show()注意这里fig是整个画布,axes是坐标轴对象的数组。调用axes[i].plot()就是在对应的子图里绘图。plt.tight_layout()也很重要,它自动调整子图之间的间距,避免标题和坐标轴标签互相重叠。
5. 综合实战:白酒销售的完整分析流程
光讲 API 是记不住知识的,我们来做一个完整的实战项目。这个案例规模不大,但五脏俱全,能帮你把所有知识点串在一起。场景是:某白酒经销商有连续 12 个月的销售明细,包含订单号、销售日期、区域、品牌、数量、单价,要通过数据分析找出各区域的销售表现和趋势。
5.1 数据模拟与格式准备
真实项目里你可能是从公司数据库导出数据,我这里先构造一份模拟数据,方便你直接跑通流程:
import pandas as pd import numpy as np import matplotlib.pyplot as plt # 固定随机种子,让结果可复现 np.random.seed(42) dates = pd.date_range("2024-01-01", "2024-12-31", freq="D") regions = ["华东", "华南", "华北", "西南"] brands = ["青花郎", "国窖1573", "茅台王子", "五粮液"] data = { "日期": np.random.choice(dates, size=1000), "区域": np.random.choice(regions, size=1000), "品牌": np.random.choice(brands, size=1000), "数量": np.random.randint(1, 20, size=1000), "单价": np.random.choice([399, 599, 899, 1099], size=1000), } df = pd.DataFrame(data) df["销售额"] = df["数量"] * df["单价"]写这段代码时,我故意用了np.random.seed(42),这样每次生成的随机数据都一样,方便你对照结果是否有误。
5.2 清洗与计算:从明细到指标
先看看整体数据结构:
print(df.head()) print(df.info()) print(df.isnull().sum())info()方法会显示每一列的名称、非空数量、数据类型,这是了解数据的基本功。
接下来做日期处理,加上"月份"列,方便后续做月度聚合:
df["月份"] = df["日期"].dt.to_period("M")先按区域汇总销售额,再按月份汇总:
region_summary = df.groupby("区域")["销售额"].sum() monthly_summary = df.groupby("月份")["销售额"].sum()这里可能有细致的读者会问,dt.to_period("M")和dt.month有什么区别?dt.month返回的是整数 1 到 12,但它把年份信息丢掉了。如果你有两年的数据,1月会被混在一起算。to_period("M")生成的是形如2024-01的周期对象,年份和月份都在,跨年数据不会串。
前面给np.random.choice加了一个size=1000,如果有两条记录日期和区域都相同,这时去重代码就派上用场了:
df_clean = df.drop_duplicates()实际情况中,你可能还需要把单价里可能出现的"¥399"这样的脏数据清洗掉。用str.replace()再转数值:
df["单价"] = pd.to_numeric( df["单价"].astype(str).str.replace("¥", "").str.strip(), errors="coerce" )5.3 可视化:把分析结果讲清楚
算出了结果,下一步画图。先画各区域销售额对比柱状图:
plt.style.use("seaborn-v0_8-whitegrid") plt.rcParams["font.sans-serif"] = ["SimHei"] plt.rcParams["axes.unicode_minus"] = False colors = ["#2E86AB", "#F18F01", "#43B02A", "#C73E1D"] plt.figure(figsize=(8, 5)) plt.bar(region_summary.index, region_summary.values, color=colors) plt.title("2024年各区域白酒销售额对比", fontsize=15) plt.xlabel("区域") plt.ylabel("销售额(元)") for i, v in enumerate(region_summary.values): plt.text(i, v * 1.02, f"{v:,.0f}", ha="center", va="bottom") plt.tight_layout() plt.show()上面的plt.text()在柱顶加上了具体数值,这很符合很多业务汇报的场景需求,不用低头看坐标轴去找数值。格式字符串f"{v:,.0f}"还会自动加上千分位分隔符,显示成类似1,234,567的形式,阅读友好。
再画月度趋势折线图:
plt.figure(figsize=(12, 5)) plt.plot( monthly_summary.index.astype(str), monthly_summary.values, marker="o", linewidth=2, color="#2E86AB" ) plt.title("2024年各月白酒销售额趋势", fontsize=15) plt.xlabel("月份") plt.ylabel("销售额(元)") plt.xticks(rotation=45) plt.grid(alpha=0.3) plt.tight_layout() plt.show()这里多了marker="o",让每个月份的数据点都用圆点标出来,趋势感更强。网格线的透明度也能设置,alpha=0.3能让网格不至于喧宾夺主。
更进一步,还可以分析各品牌的区域分布,用堆叠柱状图表达:
brand_region_pivot = pd.pivot_table( df, values="销售额", index="品牌", columns="区域", aggfunc="sum", fill_value=0 ) brand_region_pivot.plot(kind="bar", figsize=(10, 6), colormap="Set2") plt.title("各品牌分区域销售额", fontsize=15) plt.ylabel("销售额(元)") plt.xticks(rotation=0) plt.legend(title="区域") plt.tight_layout() plt.show()pivot_table的作用是把"长表"转成"宽表",品牌作为行、区域作为列、销售额作为值,这是多维分析里的常用手法。
5.4 从数据到结论:一份完整报告的思路
图形画完不是终点,你要能从数据里提炼出有价值的业务结论。比如:
- 华东区域全年销售额最高,但华东的增长率是否垫底?如果是,说明这个市场趋于饱和。
- 某品牌在华南的销量最好,但在西南几乎没有存在感,这是渠道覆盖问题还是品牌定位问题?
- 月度趋势图上如果出现"春节前"和"中秋前"两个明显峰值,就能对应白酒消费的节庆效应。
这些结论,才是数据分析真正交付的价值,而不仅仅是几张漂亮的图。你的报告,一半靠数据计算,另一半靠业务理解,两者缺一不可。
6. 常见问题排查与避坑速查表
6.1 环境安装与库版本问题
很多人第一步就卡在安装上。打开终端或命令提示符,执行:
pip install numpy pandas matplotlib国内网络环境下,为了速度更快,可以指定清华源:
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple numpy pandas matplotlib有一个问题值得单独提醒:如果你用的是 PyCharm,记得检查当前解释器是不是你安装库的那个 Python 环境。常见场景是命令行里pip install装好了,但 PyCharm 里import pandas还是报错,因为 PyCharm 可能指向了虚拟环境而不是全局环境。解决办法是在 PyCharm 的 Settings > Project > Python Interpreter 中手动确认解释器路径,或者直接在 PyCharm 的 Terminal 里执行安装命令。
另外,numpy 版本不匹配是另一个高频报错。很多第三方库对 NumPy 有最低版本要求,装新库的时候,pip可能会自动升级 NumPy,而升级后又导致另一个库出问题。遇到这种情况,我把这个约束先写下来:
pip install "numpy<2.0"或者先把所有包升级到最新稳定版:
pip install --upgrade numpy pandas matplotlib同时对 NumPy 版本做一次体检:
print(np.__version__)6.2 数据读取和类型推断的常见坑
使用read_csv()时,一个很隐蔽的问题是 Pandas 对大整数列做类型推断,可能会自动把"订单号"这样的 ID 判断成int64,但某些订单号开头有 0,它又会被读成字符串。这种时候,可以在读取时直接指定 dtype:
df = pd.read_csv("orders.csv", dtype={"订单号": str})另一个典型问题是空值:CSV 文件里的空单元格在 Pandas 中被读成NaN(属于 float),这会导致整列变成 float 类型,看起来非常突兀。这是正常现象,不要慌,用fillna()处理就好。
6.3 Matplotlib 绘制失败的常见情况排查
- 画出的图是空的,不显示任何内容。原因多半是没调用
plt.show(),或者代码中先调用了plt.show()然后又调用了plt.figure(),导致新图覆盖了旧图。 - 中文全部显示成方框。参考 4.3 节内容,设置
rcParams里的中文字体。 - 图形模糊不清。在保存图片时指定
dpi:
plt.savefig("chart.png", dpi=150, bbox_inches="tight")- 多个图形互相重叠。如果是子图布局问题,用
plt.tight_layout();如果是在同一个画布重复绘制了多次,考虑用plt.figure()创建新画布,或者plt.clf()清空当前画布。
6.4 性能问题:数据大了就卡顿怎么办
当你的数据有几十万行甚至上百万行,Pandas 的某些操作会明显变慢。这时候有一个顺序要记牢:先筛选,后计算;先清洗,后合并。
推荐优化顺序:
- 读取时只选择需要的列:
pd.read_csv("big.csv", usecols=["列1", "列2", "列3"]),这能大幅减少内存占用。 - 选用合适的数据类型:如果某列只有少数几个类别,比如"区域",用
category类型能省很多内存:
df["区域"] = df["区域"].astype("category")- 避免链式赋值。不要写
df[df["销售额"] > 1000]["新列"] = 1这种代码,Pandas 的链式赋值容易出现SettingWithCopyWarning警告,行为也不可控。改为:
filtered = df[df["销售额"] > 1000].copy() filtered["新列"] = 1copy()的作用是显式复制一份数据,避免修改视图可能引发的不可预期行为。
7. 写在最后:几个我踩过的坑
来总结几条常规文档不会说的经验吧。
第一,数据分析的成败,很大程度在数据清洗阶段,而不在算法阶段。我见过太多人急着跑模型、画图,结果数据里有一列空值没处理,画出来的图完全失真。用df.info()多看一眼,用df.describe()多读两行,这三十秒能帮你省下一个小时的返工。
第二,画图之前,先想清楚这张图给谁看、要传达什么结论。老板看的是趋势和对比,业务同事看的是明细和数据口径。同样的数据,面向不同的人,可能需要完全不同的图形和标注。
第三,不要排斥把三件套和openpyxl、sqlalchemy这些库搭配使用。pd.read_excel()底层依赖openpyxl,pd.read_sql()依赖数据库驱动。你不需要背下每个依赖库的用法,但遇到报错时能顺着这个思路排查,会快很多。
我个人最深的体会是:这套技术栈最大的学习门槛不是语法的复杂度,而是"数据思维"的建立。所谓数据思维,就是拿到任何问题,第一反应是想我有哪些字段、这些字段之间是什么关系、据什么样的问题可以用什么操作来回答。等你想清楚这个问题,Python 代码只是把你脑子里的想法变成机器能执行的步骤而已。
如果你按这篇文章的路线走完一遍,你会发现数据分析没有想象中神秘。它不过是一套有逻辑的工具链。工具链可以模仿,经验要自己积累。赶紧把你手头最乱的那份数据拿出来试一次,跑通一次,比你看十篇教程都管用。