Pandas与Matplotlib入门实战:从数据清洗到可视化完整指南
1. 项目整体思路为什么选 Pandas 和 Matplotlib 当入门组合如果你刚接触 Python 数据分析网上随便一搜就是一堆名词NumPy、Pandas、SciPy、Seaborn、Plotly……初学者很容易被术语劝退。但如果你只打算学两个库我一定推荐 Pandas 和 Matplotlib。理由很简单一个负责“玩数据”一个负责“给数据做解释”。Pandas 的核心是 DataFrame你可以把它理解成一张“Excel 表格”但它比 Excel 强大得多。平时你用 Excel 做的筛选、求和、排序、透视表DataFrame 几行代码就能搞定Excel 做不了的大批量清洗、多文件合并、复杂条件运算DataFrame 同样能处理。而 Matplotlib 就是 Python 生态里最经典、最底层的绘图库几乎所有图表库的底层逻辑都跟它沾边。数据整理清楚之后用 Matplotlib 画一张折线图、柱状图或饼图老板和用户才能直观看到你的分析结果。写这篇实战指南的初衷是帮零基础的朋友用一次完整的小项目把这两个库串起来。我不会只讲 API 怎么用而是带着大家模拟一个非常常见的真实场景拿到一份销售明细表先做数据清洗再做分组统计最后画成可视化图表。整个过程大概 200 行代码但每行都是日常工作中高频使用的操作。适合谁来读呢三类人最合适刚学完 Python 基础语法循环、函数、列表但不知道下一步学什么的人日常工作里被 Excel 折磨想用代码解放双手的人需要快速产出数据分析图表但不想碰那些复杂的商业智能工具的人读完这篇文章你会对“数据从哪来、怎么清洗、怎么分析、怎么展示”有一个完整的感知。不需要数学背景不需要机器学习基础你会用列表和字典就已经够用了。2. 环境准备与第一个 DataFrame五步跑通基础环境2.1 安装与导入这套组合怎么装最快先解决环境问题。如果你之前装过 Anaconda那 Pandas 和 Matplotlib 通常已经带在环境里了不用额外安装。如果用的是原生 Python我建议直接用 pip 一次装齐命令如下pip install pandas matplotlib装完之后打开你习惯的开发环境Jupyter Notebook、VS Code、PyCharm 都行先写两行导入语句确认环境可用import pandas as pd import matplotlib.pyplot as plt print(pd.__version__) print(matplotlib.__version__)如果输出了版本号比如2.1.4和3.8.0说明一切正常。我自己的经验是Windows 用户如果安装卡顿可以换个国内镜像源会快非常多pip install pandas matplotlib -i https://pypi.tuna.tsinghua.edu.cn/simple还有一个细节容易被新手忽略在 Jupyter Notebook 中为了让图标直接显示在单元格下方通常需要加一行魔法指令%matplotlib inline这行代码只对 Jupyter 有效作用是让图表以内嵌的方式渲染而不是弹出一个独立窗口。如果你在 VS Code 或 PyCharm 的脚本文件里写就不需要这行运行后图表会正常显示在屏幕绘图窗口中。2.2 创建 DataFrame 的三种常用方式以及选型思路环境跑通之后先来认识 DataFrame 本身。你可以把它当作“带行索引和列名的表格”。最直观的创建方式是传入一个字典键是列名值是列表data { 城市: [北京, 上海, 广州, 深圳], 1月销量: [120, 138, 90, 156], 2月销量: [128, 142, 95, 168], } df pd.DataFrame(data) print(df)输出结果城市 1月销量 2月销量 0 北京 120 128 1 上海 138 142 2 广州 90 95 3 深圳 156 168除了字典方式还有两种常见创建方式。第一种是列表套字典每条记录是一行rows [ {城市: 北京, 月份: 1月, 销量: 120}, {城市: 上海, 月份: 1月, 销量: 138}, ] df pd.DataFrame(rows)第二种是从 CSV 文件读入这也是实战中最常用的方式df pd.read_csv(sales_data.csv)三种方式适用的场景完全不同字典方式适合快速构建测试数据列表套字典适合在代码里手动拼数据read_csv 则用于真正的项目文件读取。初学者常犯的一个错误是忘记了列表里每个字典的键顺序可能不一致Pandas 会按照第一次出现的列顺序来排版如果键不一致缺失的地方会自动变成“空值”后面会细说。提示创建 DataFrame 后建议养成先看一眼.shape行数、列数和.info()类型与缺失值的习惯。拿到任何数据表第一件事不是分析而是“体检”。3. 数据导入与清洗拿到真实数据以后90% 时间都在干这个3.1 用 read_csv 读取数据并处理残缺值真实场景里数据很少像教程示例那样干净。我记得有一次拿到一份几百行的运营数据表面上看着挺整齐一读进来才发现问题一堆有空行、有残缺字段、金额列里混着“N/A”这种文本、日期格式不统一。大部分初学者会在这一步直接卡住其实只要掌握几个核心方法就能有条不紊地处理。先用 read_csv 把文件读进来。如果文件和脚本在同一目录直接写文件名就行如果不在需要写完整路径或相对路径df pd.read_csv(sales_data.csv, encodingutf-8)这里有两个参数值得说一下。第一个是encoding常见的 CSV 文件编码有 utf-8、gbk、gb2312Excel 导出的文件经常不是 utf-8。遇到乱码就把编码换成gbk试试。第二个是sep默认按逗号分隔但如果文件是制表符或分号分隔就需要手动指定。数据读进来后第一件事可以使用df.info()看缺失值情况df.info()输出长这样部分截取class pandas.core.frame.DataFrame RangeIndex: 1000 entries, 0 to 999 Data columns (total 7 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 订单编号 1000 non-null object 1 城市 998 non-null object 2 商品类别 995 non-null object 3 销售额 970 non-null float64 4 成本 985 non-null float64 5 利润 960 non-null float64 6 订单日期 1000 non-null objectNon-Null Count 表示非空值的数量。如果数量不等于总行数就说明有缺失值。处理缺失值有两个思路要么删要么填。删除用dropnadf_clear df.dropna()但直接删往往太粗暴因为可能把大量有效信息一起丢掉。比如城市字段缺了几行但其他字段都有值删掉虽然省事若之后还要按城市统计也可能导致误差。一般我会先按列来看缺失比例高于 50% 的列直接弃掉低于 5% 的列直接删掉对应行介于中间的就用填充策略处理。填充的常见方式是“用数值列的平均值填充缺失值”比如# 用销售额平均值填充缺失值 df[销售额].fillna(df[销售额].mean(), inplaceTrue)填充之前建议现看一下分布确认“是不是有特别大的值影响了均值”。如果销售额很偏用中位数更稳妥df[销售额].fillna(df[销售额].median(), inplaceTrue)这里要为初学者解释一下inplaceTrue的含义意为直接在原数据上修改不返回新对象。如果不加Pandas 会默认返回一个新对象原数据不变。很多人以为自己调用了方法就等于改了数据结果后面打印出来发现还是老样子根源就在这里。我个人的习惯是不太用inplaceTrue因为 Python 的函数式风格更推荐“赋值式写回”也就是df[销售额] df[销售额].fillna(...)这样代码更清晰也不容易有歧义。3.2 类型转换、筛选、排序与去重清洗的核心四板斧缺失值处理完之后还要检查类型是否正确。常见陷阱是“销售额”这一列被读成了文本对象明明能做加减法却显示字符串。这时要手动转成数值类型df[销售额] pd.to_numeric(df[销售额], errorscoerce)errorscoerce的意思是转换不了的非数字自动变成 NaN而不是直接报错。这一步特别实用因为真实数据里经常混进“约123”“1.2万元”这种带格式的文本。清洗环节还有两个高频操作筛选和排序。筛选可以按条件筛比如只看利润大于 0 的订单df_profit df[df[利润] 0]也可以用.query()方法写更接近直觉的条件尤其适合多条件组合df_profit df.query(利润 0 and 城市 上海)排序就用sort_valuesdf_sorted df.sort_values(by销售额, ascendingFalse)去重则用drop_duplicates。这个方法的坑在于默认会对整行进行去重如果只想根据“订单编号”这个字段去重需要指定 subsetdf_unique df.drop_duplicates(subset[订单编号])说一句题外话我见过不少人在这一步翻了车。有个同学去重后行数没变化原因就是数据里每条订单号本身就不重复但同一订单号对应了多行商品明细——它该去重的根本不是订单编号而是订单编号和商品类别的组合。所以用drop_duplicates之前先想一想你“重复”的定义是什么这个定义决定了subset怎么填。为了让大家有完整的体验我把这段清洗流程合在一起写一下假设现在拿到了 sales_data.csvimport pandas as pd df pd.read_csv(sales_data.csv, encodingutf-8) # 1. 查看结构 print(df.shape) print(df.info()) # 2. 删除订单号完全重复的行 df df.drop_duplicates(subset[订单编号]) # 3. 清洗销售额和利润的类型 df[销售额] pd.to_numeric(df[销售额], errorscoerce) df[利润] pd.to_numeric(df[利润], errorscoerce) # 4. 缺失值处理城市缺失的按“未知”填充数值列缺失的平均值填充 df[城市] df[城市].fillna(未知) df[销售额] df[销售额].fillna(df[销售额].median()) df[利润] df[利润].fillna(0) # 5. 筛选有效订单 df_valid df[df[利润] 0] # 6. 排序查看 df_valid df_valid.sort_values(by销售额, ascendingFalse) print(df_valid.head())到这一步数据已经具备可分析的基础形态了。4. 分组统计与透视用 groupby 把明细表变成决策表4.1 单字段和多字段分组聚合的操作思路清洗完数据后下一步就是统计。分组统计在 Pandas 里用groupby实现它做的事情可以用一句话概括按某些列把数据切分成小组再对每个小组做相同的计算。举个例子我要按“城市”统计每个城市的平均销售额、总销售额和订单数grouped df_valid.groupby(城市) result grouped.agg({ 销售额: [sum, mean], 订单编号: count, }) print(result)结果大概长这样销售额 订单编号 sum mean count 城市 北京 389000.0 1496.15 260 上海 456000.0 1520.01 300 广州 218000.0 984.26 221 深圳 445000.0 1816.33 245这里有个小技巧agg()可以同时传入不同列的多种聚合方式只要用字典指定列名和算子的对应关系即可。常用算子除了sum、mean、count还有max、min、median、std。多字段分组也很简单就是在 groupby 里传入一个列表grouped2 df_valid.groupby([城市, 商品类别])[销售额].sum().reset_index()reset_index()的作用是把分组索引还原成普通列这样结果就变成一张平铺的表格后续画图、写 CSV、做拼接都更顺手。这一步经常被忽略但非常重要——不加reset_index()时结果是带层级索引的 DataFrame与很多函数配合时不方便。复杂一点的需求比如“每个城市销售额最高的商品类别”怎么写一种比较直观的方法是使用排序 去重的组合拳先按城市、销售额排序再做drop_duplicates拿到每组最大值的记录。这种写法的好处是好懂、不容易出错对小规模数据性能也完全够用df_sorted df_valid.sort_values(by[城市, 销售额], ascending[True, False]) df_top_by_city df_sorted.drop_duplicates(subset[城市])4.2 用 pivot_table 做透视表五分钟上手明细汇总如果你用过 Excel 的透视表Pandas 的pivot_table几乎就是你熟悉的那套逻辑。它的核心思想是把“行、列、值”三要素用代码表达出来。比如我想看看每个城市、每类商品的总销售额。城市作为行索引商品类别作为列索引销售额作为值用pivot_table这样写pivot pd.pivot_table( df_valid, values销售额, index城市, columns商品类别, aggfuncsum, fill_value0 ) print(pivot)这个写法的工作过程是先按“城市”和“商品类别”把数据分成若干小组然后对每组销售额求和最后把结果填充为一个二维表格。fill_value0的意思是把没有数据的交叉位置填成 0避免生成 NaN 影响阅读和后续绘图。pivot_table的aggfunc参数可以换成mean平均值、count计数或者更复杂的函数。它还有一个实用参数叫marginsTrue会在结果底部和右侧增加合计行和合计列和 Excel 里的“总计”一个意思。这个参数我第一次用时以为没生效后来才明白它只在指定aggfunc之后才能真正生效而且合计算的也是原始数据而不是聚合后的平均值。题目再延伸一下用什么场景适合groupby、什么场景适合pivot_table我的经验是目标是纵向汇总结果比如“每个城市总销量是多少”用 groupby目标是交叉维度观察比如“城市 × 商品类别”矩阵用 pivot_table。大多数初学者会把两者混用但其实只要明确了维度关系就不会纠结。4.3 统计结果导出代码结束后的交付姿势统计结果最终要给同事或领导看导出文件是很常见的一步。Pandas 导出 CSV 和 Excel 都非常简单result.to_csv(sales_summary.csv, indexFalse, encodingutf-8-sig) pivot.to_excel(sales_pivot.xlsx, sheet_name按城市统计)这里有两个细节。第一个是encodingutf-8-sig如果只写utf-8用 Excel 打开 CSV 时很容易出现中文乱码加个 BOM 头即 utf-8-sig就能有效解决。第二个是导出 Excel 需要安装对应引擎如果没装可以先运行pip install openpyxl。5. Matplotlib 可视化实战从统计数字到一张看得懂的信息图5.1 绘图基本流程与三个核心类图表分析的价值在数字表格里往往要被多“看”几眼才能感受到但一张好的图能把规律直接摆在眼前。Matplotlib 的基本概念可以拆成三层Figure整张画布相当于一块白纸Axes画布中的一个子图区域相当于白纸上框出的一个区域plot等绘图函数在 Axes 上根据数据画出具体图形最简单、最常用的绘图流程是三步准备数据、调用绘图函数、调整展示信息并显示。一个经典的柱状图例子import matplotlib.pyplot as plt # 数据准备 cities [北京, 上海, 广州, 深圳] sales [389000, 456000, 218000, 445000] # 创建画布和子图 fig, ax plt.subplots(figsize(8, 5)) # 绘制柱状图 ax.bar(cities, sales) # 调整标题和坐标轴 ax.set_title(各城市总销售额对比) ax.set_xlabel(城市) ax.set_ylabel(销售额元) plt.show()在 Jupyter Notebook 里运行时plt.show()可以缺省不写但在脚本模式里务必保留否则图片不一定显示。很多人把show()的作用和“保存图片”混淆这里说明一下show()是把图显示到屏幕上存图则用plt.savefig()两者不冲突。写图表代码时我喜欢先创建fig, ax再在ax上操作而不是直接使用plt.plot。原因是用ax的方式更明确后续如果要在同一张画布上画多个子图或者添加多个图例、坐标轴调整都会更方便。直接plt.plot适合非常快速的临时绘图但代码一多就会混乱。5.2 四种最常用的图表以及各自适合的场景实际做数据分析时下面四种图几乎覆盖了 80% 的需求。折线图适合看趋势比如看几个月的销售变化months [1月, 2月, 3月, 4月, 5月] sales_by_month [1200, 1350, 1290, 1520, 1680] fig, ax plt.subplots(figsize(8, 4)) ax.plot(months, sales_by_month, markero, linestyle-, colorsteelblue) ax.set_title(月度销售额趋势) ax.set_xlabel(月份) ax.set_ylabel(销售额万元) plt.show()markero会让数据点变成实心圆圈折线上有标记点数据变化看起来更清楚。如果想在同一张图上画两条线做对比直接在同一ax上再调用一次ax.plot就行Matplotlib 会自动用不同颜色区分。柱状图适合做横向对比比如按城市对比销售额。具体代码参考 5.1 节的示例。如果想做“城市 × 商品类别”的对比一种方式是把柱状图变成并列柱状图这需要自己设定每个柱子的横坐标偏移稍微复杂一点但效果非常直观。初学者如果觉得并列柱状图繁琐可以直接用后面提到的分组柱状图代替思路把分组结果放在同一个ax上依次画柱子。直方图适合看数值分布比如“订单金额的分布形态”import numpy as np # 模拟一组订单金额数据 order_values df_valid[销售额] fig, ax plt.subplots(figsize(8, 4)) ax.hist(order_values, bins20, edgecolorwhite) ax.set_title(订单销售额分布) ax.set_xlabel(销售额区间元) ax.set_ylabel(订单数) plt.show()bins参数控制柱子数量。bins 越大柱子越细分布细节越碎bins 越小柱子越粗越容易忽略细节。建议先用默认值再根据实际分布微调。数据量小的时候 bins 设成 10 到 20 就够。饼图适合看占比但大家普遍的建议是“能不用就不用”因为人眼对角度大小的感知不如长度对比准确。真要用的话务必突出最大的一项并且把不需要区分的细小项合并category_sales df_valid.groupby(商品类别)[销售额].sum() category_sales_sorted category_sales.sort_values(ascendingFalse) fig, ax plt.subplots(figsize(6, 6)) ax.pie( category_sales_sorted.values, labelscategory_sales_sorted.index, autopct%1.1f%%, ) ax.set_title(商品类别销售额占比) plt.show()autopct会显示每部分百分比%1.1f%%表示保留一位小数再加百分号。5.3 中文字体与图表的“颜值”问题画图时最让人崩溃的一个问题就是图表出来了中文全变成方块。原因是 Matplotlib 默认字体不支持汉字。解决思路有二要么改全局字体配置要么在代码里临时指定字体。我比较推荐在代码开头临时指定不污染全局设置plt.rcParams[font.sans-serif] [SimHei] # Windows 黑体 plt.rcParams[axes.unicode_minus] False # 解决负号显示问题如果是在 macOS 上SimHei不适用可以换成Arial Unicode MS或PingFang SC。另一个偏极端的做法是安装中文字体并把字体路径传给 Matplotlib但对零基础同学来说没有必要先记住上面这两行配置即可。还有个小技巧坐标轴标签如果过长可以用ax.set_xticklabels(labels, rotation45)让文字斜着摆放避免重叠图会干净很多。这个细节在很多人第一次画城市时都踩过坑。5.4 把多个小图拼到一张画布subplots 的高级用法实际报告中经常需要在一张画布里放多个子图做对比。用subplots指定行和列即可fig, axes plt.subplots(1, 2, figsize(12, 4)) # 左边是折线图 axes[0].plot(months, sales_by_month, markero) axes[0].set_title(月度销售趋势) # 右边是柱状图 axes[1].bar(cities, sales) axes[1].set_title(城市销售对比) plt.tight_layout() plt.show()axes是一个数组分别对应两个子图。plt.tight_layout()会自动调整子图间距和留白避免标题重叠。这个函数我每次画多子图都会带一句不然排版会非常难看。6. 常见问题与排查技巧这三个问题几乎每个人都会遇到6.1 中文乱码与负号变成方块中文乱码是零基础阶段最高频的问题。一句话总结如果图里的中文方块化了就用 5.3 节讲的plt.rcParams两行配置。如果 CSV 读进来中文乱码先检查encoding参数改成gbk或utf-8-sig。负号显示成方块则要设置axes.unicode_minusFalse。这两类问题虽然不大但每次遇到都会让人卡很久提前写进模板脚本里最省事。6.2 NaN 带来的隐形报错数据分析中最容易忽略的“隐形炸弹”就是 NaN。虽然 Pandas 在很多计算中会自动忽略 NaN但在某些情况下它会在图表上留下缺口或者让计算结果变得莫名其妙。比如分组统计时如果某类商品在某个城市的销售额全是缺失值求sum会返回 0但求mean会直接返回 NaN。这类问题排查起来很费劲。一个实用技巧在清洗之后、统计之前用一行代码主动检查一遍print(df_valid.isna().sum())如果任何一列的数量不是 0就先处理再继续。养成这个习惯之后很多“为什么结果不对”的问题会在源头被切断。6.3 DataFrame 的链式操作陷阱A value is trying to be set on a copy还有一个我见过无数次的问题如果你用“筛选出来的子 DataFrame”尝试修改原数据有时会碰到SettingWithCopyWarning警告。这个警告的意思是你改的可能是副本不是原始 DataFrame最终结果可能和你想象的不一样。比如下面这种写法df_high df[df[销售额] 1000] df_high[等级] A第 1 行得到的df_high是原始 df 的“视图”还是“拷贝”Pandas 自己有时都说不清所以它干脆用警告提醒你。解决方式很简单想修改子集时要么明确复制一份df_high df[df[销售额] 1000].copy()要么直接对原数据操作不要试图通过筛出来的对象去改原对象。这个警告不影响代码运行但会让人焦虑而且确实藏着潜在风险。有过一次踩坑经历后我现在凡是“从某个 DataFrame 切片出来的新表”都会习惯性加.copy()。写在最后的小经验整套 Pandas Matplotlib 的入门流程如果每天抽出一两个小时大概一周就能跑通。我在实际教学中发现最容易让人坚持不下去的并不是语法难而是“不知道自己写的代码在解决什么问题”。所以我强烈建议你找一个真实的数据集从这个文章里的销量例子入手然后换成你自己的数据比如“商场客流数据”“课程成绩表”“个人收支明细”什么都行。把清洗、分组、画图三步走完你会发现后面学任何数据分析工具底层逻辑都差不多。另外一个小技巧给到大家在 Jupyter Notebook 里写分析时用“分单元格写代码”的方式代替“一个大脚本跑到底”每写完一步就运行一次看看中间输出。这样排查问题会快好几倍也更容易理解每一段代码做了什么。Pandas 的报错信息虽然有时候长得很吓人但绝大部分一眼就能定位到具体列名和行号翻译一下无非是“你这个东西是字符串不能做求和”或者“索引对不上”。把这类错误当成字典来查遇到一个记一个两三个项目下来你就很难被同样的坑绊住了。