做数据分析绕不开Pandas,这句话我说了很多次,也是真心话。无论你是用Python做数据清洗、报表统计,还是给机器学习模型喂特征,Pandas几乎都是第一道关。这个系列写到第48章,我不想再重复那种照着官方文档念的入门课,而是想把日常工作中真正高频的核心操作、容易踩坑的细节、以及每个操作背后的设计逻辑,一次性讲透。
这篇内容适合两类人:一类是刚学完Python基础、想往数据分析方向走的新手,另一类是用Excel处理数据已经到瓶颈、想切换到Python流程的办公族。看完之后,你能独立完成从读取数据、清洗整理、筛选聚合到合并输出的完整链路,也能理解很多教程里没讲透的“为什么”。
1. 准备工作与Pandas安装
1.1 安装Pandas:pip和conda两手准备
网上经常有人问“pycharm怎么安装pandas包”,其实Pandas的安装路径基本就两条:pip或者conda。如果你用的是官方Python环境,直接在终端执行:
bash复制pip install pandas
如果你是Anaconda用户,推荐用conda安装,依赖关系处理得更干净:
bash复制conda install pandas
装完之后一定要验证一下,不然后面所有代码都会卡在import这一步:
bash复制python -c "import pandas as pd; print(pd.__version__)"
这里多说一句,很多新手在PyCharm里装了包却报ModuleNotFoundError,十有八九是解释器选错了。PyCharm右下角或者Settings -> Project -> Python Interpreter里,确认当前项目用的解释器路径,和你pip安装包的解释器是同一个。出现两个Python环境互相打架的情况,我见得太多,每次都是这个原因。
1.2 数据导入:Excel、CSV、各种格式的读取姿势
数据导入是整个分析流程的第一步,Pandas最常见的两个数据来源是CSV和Excel。读CSV用:
python复制import pandas as pd
df = pd.read_csv("sales_data.csv")
读Excel则要稍微注意一点,因为Pandas本身不解析xlsx文件,底层依赖openpyxl引擎。如果直接报错Missing optional dependency 'openpyxl',就先安装:
bash复制pip install openpyxl
然后再用:
python复制df = pd.read_excel("sales_data.xlsx", sheet_name="2024年订单")
这里有个隐藏痛点:很多企业导出的Excel表不是规规矩矩的二维表,第一行可能是标题合并单元格,第三行才是真正的列名。这时需要加参数跳过行:
python复制df = pd.read_excel("sales_data.xlsx", header=2)
还有编码问题,尤其是从Windows导出的CSV文件,默认可能是GBK编码。不指定编码直接读,会出现一堆乱码或者直接报UnicodeDecodeError,处理方式是:
python复制df = pd.read_csv("sales_data.csv", encoding="gbk")
遇到UTF-8和GBK都报错的情况,还可以试试encoding="gb18030",这个编码覆盖的中文字符集更全,容错率更高。读取这一步是后面所有操作的基础,文件读不进来,后面都是空谈。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 快速摸清数据底细
2.1 用head、info、describe三件套预览数据
拿到一份新数据,不要急着做清洗和可视化,先做三件事:看长什么样、看什么类型、看分布情况。
第一件事,看前几行确认数据结构:
python复制df.head()
默认显示前5行,也可以传参数df.head(10)看更多。配合df.tail()看尾部数据,能初步判断数据是按什么顺序排列的。
第二件事,用info()看每一列的数据类型和非空数量:
python复制df.info()
这个输出信息量很大,能看到每列有多少个非空值、数据类型是什么、总共占用多少内存。比如你发现一列“年龄”显示object而不是int64,说明里面有脏数据混进去了,后面清洗方向就有了。
第三件事,用describe()看数值列的统计指标:
python复制df.describe()
它会输出count、mean、std、min、25%、50%、75%、max这些指标,一组数据的分布轮廓就出来了。比如某列最大值是99999,最小值是0,明显不合常理,就可以顺藤摸瓜去找原因。
这套组合拳看起来简单,但我每次拿到新数据都老老实实先跑一遍。很多人上来直接写分析逻辑,结果后面发现列名对不上、类型不对、空值一大堆,返工成本比先花一分钟看数据结构要高得多。
2.2 理解DataFrame的结构:索引、列和轴
想用好Pandas,需要对DataFrame的结构有肌肉记忆。一个DataFrame可以理解成一张Excel表:行有索引(index),列有列名(columns),数据和行列标签组合在一起就构成整个表。
python复制df.index # 行索引
df.columns # 列名列表
df.values # 底层numpy数组
这里要特意说一下“轴”的概念。Pandas很多函数的参数里有axis=0和axis=1,新手经常搞混。我的记忆方式是:axis=0是沿着行的方向操作,表示对每一列做计算;axis=1是沿着列的方向操作,表示对每一行做计算。比如df.drop("某列", axis=1)表示删掉这一列,df.drop(0, axis=0)表示删掉这一行。
很多操作绕不开对这个结构的理解。比如你想改列名,用rename:
python复制df.rename(columns={"旧列名": "新列名"}, inplace=True)
如果你想筛出某些列,用列名列表直接取:
python复制subset = df[["客户ID", "订单金额"]]
理解了DataFrame的基本结构,后面所有操作都是在往这个“二维表”模型上叠加功能,思路会清晰很多。
3. 数据清洗:在一堆脏数据里分清主次
3.1 缺失值与重复值处理
数据清洗是整个数据分析流程里最耗时、也最见功力的环节。真实业务数据很少是干干净净的,缺值、重复、格式混乱都是家常便饭。
先看缺失值:
python复制df.isnull().sum()
这条命令会统计每一列有多少空值,是整个清洗流程的起点。拿到缺失值分布后,处理策略通常是:
- 缺失比例很小的列,直接删掉缺失行:df.dropna(subset=["列名"])
- 数值列缺失较多但不想丢数据,用均值或中位数填充:df["价格"].fillna(df["价格"].median())
- 分类列缺失,用众数填充或者单独填一个“未知”
我在实际项目中经常和业务方确认“缺失值是不是有业务含义”。比如“客户年龄”为空,可能只是没录入,但“退款时间”为空,可能代表订单还没退款,这个空值本身就是信息。直接fillna反而会污染后续分析。
再看重复值:
python复制df.duplicated().sum()
df.drop_duplicates(inplace=True)
如果只想根据某一列或者某几列判断重复,加subset参数:
python复制df.drop_duplicates(subset=["订单号"], keep="first", inplace=True)
keep="first"表示保留第一条,keep="last"表示保留最后一条,keep=False表示全部删除。实战中我建议先确认重复产生的原因,再决定去重策略,不要盲目删。
3.2 数据类型转换:从字符串到数值
“Pandas 数据类型转换”是搜索热词,因为这个问题太常见了。比如从Excel读进来的“销售额”明明是数字,type却是object;再比如“日期”列读进来是字符串,没法按时间排序。这类问题本质是Pandas在读数据时类型推断失败,需要我们手动纠正。
最常见的转换:
python复制df["销售额"] = df["销售额"].astype(float)
df["日期"] = pd.to_datetime(df["日期"])
df["上架时间"] = pd.to_datetime(df["上架时间"], format="%Y/%m/%d")
这里有两个坑要提醒。第一,astype转换遇到非法字符串会直接报错,比如一列“销售额”里混着一个“--”,整个转换就崩了。一个健壮的做法是用pd.to_numeric配合errors参数:
python复制df["销售额"] = pd.to_numeric(df["销售额"], errors="coerce")
errors="coerce"会把无法转换的非法值变成NaN,之后你再统一处理这些空值。第二个坑是日期格式多样化,纯字符串的“2024/01/01”和“2024-01-01”混在一起,直接用pd.to_datetime可能识别不了,最好的办法是先统一格式,或者按我上面写的那样显式指定format参数,速度更快也更不容易出错。
3.3 drop删除:删列删行和inplace的争议
Pandas里删除操作的核心是drop函数,这也是个高频操作。删列用axis=1,删行用axis=0:
python复制df.drop(columns=["列1", "列2"], inplace=True)
df.drop(index=[0, 1, 2], inplace=True)
新版Pandas也支持直接用columns参数指定要删的列,语义更清晰,我比较推荐这个写法。
关于inplace=True,这里值得展开说一下。早期Pandas教程喜欢用inplace=True,但社区后来逐渐倾向不用它。原因是inplace=True在部分场景下会触发SettingWithCopyWarning,而且它修改的是原对象,调试时不好追踪数据是怎么一步步变形的。我的习惯是:
python复制df = df.drop(columns=["列1", "列2"])
把结果重新赋值给变量,每个中间步骤都留一个引用,后面排查问题能省很多时间。不过话说回来,如果你在写一次性脚本,用inplace=True也不会出大事,关键是理解这个操作背后的语义——到底是在原对象上改,还是生成新对象。
4. 筛选与条件定位:精确拿到想要的行
4.1 iloc和loc:位置与标签的区别
这是Pandas新手最容易混淆的一对函数。loc用标签(行名和列名)定位,iloc用位置(第几行第几列)定位。
python复制df.loc[0, "订单金额"] # 第一行“订单金额”这一列的值
df.iloc[0, 2] # 第一行第三列的值
如果你的索引是默认的0、1、2这种,loc和iloc看起来差不多,但一旦索引被改过,区别就出来了:
python复制df.set_index("订单号", inplace=True)
df.loc["A001"] # 用订单号取值
df.iloc[0] # 仍然用位置取值
还有个很实用的切片行为。loc的切片是闭区间,包含终点;iloc的切片是左闭右开,不包含终点:
python复制df.loc[0:5, "客户": "金额"] # 包含第5行
df.iloc[0:5, 0:3] # 只取第0到第4行
很多人在这个细节上栽过跟头,写出来的结果莫名其妙少一行或者多一行,原因就是没搞清楚loc和iloc的切片规则差异。
4.2 布尔索引与query:条件筛选的两种姿势
条件筛选是数据分析里的日常操作。最常见的写法是布尔索引:
python复制df[df["订单金额"] > 1000]
df[(df["订单金额"] > 1000) & (df["地区"] == "华东")]
注意多个条件之间要用&(且)和|(或),并且每个条件都要用括号包起来,不然优先级会出错。这个语法刚看有点丑,但性能很好,而且逻辑一眼能看明白。
如果想要更清爽的写法,可以用query方法:
python复制df.query("订单金额 > 1000 and 地区 == '华东'")
query写起来像SQL,字符串条件直接传进去,复杂筛选不用套一堆括号。它的另一个优点是支持在条件里引用外部变量,用@符号:
python复制min_amount = 1000
df.query("订单金额 >= @min_amount")
从可读性角度,条件少的筛选我用布尔索引,条件特别多、或者条件字符串会动态拼接的场景我用query,这个看个人习惯,没有绝对的对错。
5. 分组聚合与数据透视:业务分析的发动机
5.1 groupby+agg组合拳
做数据报表最核心的就是分组聚合。Excel里有数据透视表,Pandas里对应的是groupby。
python复制df.groupby("地区")["订单金额"].sum()
这行代码的意思是:按“地区”分组,然后求每组“订单金额”的总和。出来的结果是一个Series,每个地区一行。
更常用的是用agg同时算多个指标:
python复制df.groupby("地区")["订单金额"].agg(["sum", "mean", "count", "max"])
agg里的列表可以传多个聚合函数,一次算出总和、均值、计数、最大值,一张汇总表就出来了。如果要对不同的列用不同的聚合函数,可以用字典:
python复制df.groupby("地区").agg({"订单金额": "sum", "客户ID": "count"})
聚合结果如果想转成规整的DataFrame,可以用reset_index:
python复制df.groupby("地区")["订单金额"].sum().reset_index()
这种方法在实际报表里几乎天天用,从原始订单表到各区域销售额汇总,一条链就搞定。
5.2 pivot_table:用代码复刻Excel透视表
如果你要用Pandas做透视,pivot_table是比groupby更接近业务思维的函数。比如想按“地区”作为行、按“季度”作为列,行列交叉处显示销售额总和:
python复制pd.pivot_table(df, values="订单金额", index="地区", columns="季度", aggfunc="sum", fill_value=0)
直接生成一个二维透视表,结构就和Excel透视表一模一样。参数说明:
- values:要聚合的数值列
- index:透视表的行维度
- columns:透视表的列维度
- aggfunc:聚合函数,默认是mean,记得改成sum或者count
- fill_value:空值填充,透视表里没有数据的格子默认是NaN,填0看着更舒服
还有个margins=True参数,会在最右和最下增加“总计”行和“总计”列,对应Excel透视表里的“行总计/列总计”,非常适合做汇报材料。
5.3 分组后的遍历:没你想得那么可怕
有时候聚合函数不够用,需要遍历每个分组做复杂自定义逻辑。groupby对象本身是可迭代的:
python复制for name, group in df.groupby("地区"):
print(name)
print(group.head())
这里name是分组键的值,group是过滤后的子DataFrame。我在做多文件汇总时经常这么用:按客户分组,对每个客户的明细数据分别计算业务指标,再拼回一个大表。对Python新手来说,这个“遍历分组”的思维是理解Pandas从“整表操作”到“子集操作”的关键一步。
6. 数据合并:多个表怎么拼在一起
6.1 merge:SQL式的表连接
数据分析经常需要把多张表的数据合在一起,比如订单表和客户表按客户ID关联。Pandas的merge对应SQL里的join操作:
python复制merged = pd.merge(订单表, 客户表, on="客户ID", how="left")
on参数指定关联键,how参数指定连接方式:
- how="inner":只保留两边都能匹配上的行
- how="left":以左边表为主,左边所有行都保留,右边没匹配上的填NaN
- how="right":以右边表为主
- how="outer":两边都保留,没匹配上的填NaN
实际业务里how="left"用得最多,比如订单表保留全部订单,客户表作为维度表补信息。还有个细节:如果两边的关联键列名不一样,用left_on和right_on分别指定:
python复制pd.merge(df1, df2, left_on="客户ID", right_on="ID", how="left")
关联之后两边都有同名列(比如都叫“金额”),Pandas会自动加上_x和_y后缀来区分。如果不需要其中一边的列,提前用drop删掉或者select出一部分列再合并,都是常规操作。
6.2 concat:上下拼接用的最多的场景
concat和merge的适用场景正好互补。merge是按照某一列关联两张表,concat更多是拼接结构一样的表——比如把1月、2月、3月的明细数据纵向堆在一起:
python复制df_all = pd.concat([df_1月, df_2月, df_3月], axis=0, ignore_index=True)
axis=0是纵向拼接,相当于把表格一行一行接起来;axis=1是横向拼接,相当于给一个表增加新的列。ignore_index=True会重新生成一套连续的行索引,避免拼接后索引全是0、0、1、1这样乱掉。
这里说一个实战场景:多个Excel文件结构一致,想合并成一个总表。用glob配合循环读取,再concat一次搞定:
python复制import glob
all_files = glob.glob("sales_*.xlsx")
df_list = []
for f in all_files:
df_list.append(pd.read_excel(f))
df_all = pd.concat(df_list, ignore_index=True)
几十张上百张表,十几行代码就并入一张总表,这种批量处理能力是Excel本身很难替代的。
7. 实战:用Pandas算一道经典算法题
7.1 向量化思维看“李白打酒”
热词里有个“李白打酒python”,这是个非常经典的递推题:李白无事街上走,提壶去买酒。遇店加一倍,见花喝一斗。三遇店和花,喝光壶中酒。试问壶中原有多少酒。
传统Python解法是倒推循环:
python复制wine = 0
for _ in range(3):
wine += 1
wine /= 2
这个思路很直接。但既然在讲Pandas,我们换个视角看这道题:如果把“每一步操作后的酒量”看成一列数据,Pandas可以批量模拟多个候选初始值,用向量化一步算出所有结果。这个思路对数据分析很重要——很多时候我们需要对一个公式在大量输入上求值,而不是靠循环一次跑一个数。
7.2 用Pandas模拟验证
假设初始酒量为x,每一步变化是:遇店翻倍,见花减一。这个模型写成Pandas代码:
python复制import pandas as pd
# 构造候选初始酒量
candidates = pd.DataFrame({"初始酒量": [i / 8 for i in range(9)]})
# 依次模拟三遇店和三见花
candidates["第1次遇店后"] = candidates["初始酒量"] * 2
candidates["第1次见花后"] = candidates["第1次遇店后"] - 1
candidates["第2次遇店后"] = candidates["第1次见花后"] * 2
candidates["第2次见花后"] = candidates["第2次遇店后"] - 1
candidates["第3次遇店后"] = candidates["第2次见花后"] * 2
candidates["第3次见花后"] = candidates["第3次遇店后"] - 1
print(candidates)
结果里“第3次见花后”为0的那一行,初始酒量就是答案,正好是0.875斗。这个例子虽然小,但很有代表性:Pandas的强项是同时对一组数据做相同运算,而不是一个一个循环处理。遇到需要批量验证数值模型、模拟参数变化的场景,这种写法比for循环清晰得多,速度也快得多。
8. 常见问题与排查心得
8.1 高频报错速查表
Pandas报错信息有时候相当抽象,这里直接整理一份高频问题速查:
| 报错现象 | 根本原因 | 解决方案 |
|---|---|---|
| ModuleNotFoundError: No module named 'pandas' | 解释器环境不对或未安装 | 切换解释器或pip install pandas |
| Missing optional dependency 'openpyxl' | 缺少Excel引擎 | pip install openpyxl |
| UnicodeDecodeError | CSV编码不是UTF-8 | 读取时指定encoding="gbk"或encoding="gb18030" |
| SettingWithCopyWarning | 链式赋值的隐患 | 改为df.loc操作或先copy() |
| KeyError: '某列名' | 列名不存在或拼写不一致 | 先df.columns查看完整列名 |
| ValueError: cannot convert float NaN to integer | 数据里有NaN却想转int | 先处理缺失值,再astype |
| PerformanceWarning | 碎片化DataFrame导致性能下降 | 检查是否有循环copy或过滤 |
这七类报错覆盖了我日常遇到的大部分情况。尤其是SettingWithCopyWarning,几乎每个Pandas用户都遇到过,它本身不一定导致错误,但会在你“觉得改了但没改到原表”的时候坑你一把。我的建议是:想改原表就老老实实用df.loc,想新建副本就明确写df.copy(),不要依赖那种模棱两可的链式操作。
8.2 关于工具选型和参考资源的几点经验
聊到“数据分析用什么软件”这个问题,我的看法是:纯表格量级小、一次性使用,Excel足够;数据量大、流程要自动化、要出可复现报表,Pandas就是主力工具。很多人在Excel和Python之间纠结,其实可以组合用——数据清洗和聚合交给Pandas,最终结果导出成Excel再放进月报里,何必二选一。
关于学习资源,热词里有“pandas教程”“pandas库”“pandas读取excel文件”等一类长尾搜索。我的建议是不要只刷教程,带着真实问题去查官方文档和源码才是最快的学习路径。遇到不懂的函数,直接在Jupyter里help(函数名),或者打开pandas的GitHub源码看实现,效果远好于死记硬背文档。
还有个实用小技巧:处理特别大的DataFrame时,可以用df.info(memory_usage="deep")查看具体内存占用,再通过astype把object类型转成category、把float64降到float32,能省下相当可观的内存。实测在某些订单明细表上,这一招能让内存占用下降一半以上。
工具链方面,Pandas经常和两个库搭配使用:数据可视化用matplotlib或者seaborn,几十行代码就能出统计图;大规模数据处理用polars,语法风格和Pandas类似但性能更好。如果数据量真的到了亿级,那已经是Spark的战场了,Pandas适合的是单机数据分析场景,这个边界要心里有数。
回到Pandas本身,很多操作第一次接触会觉得有点绕,但用多了就会形成条件反射。比如拿到新数据先head、info、describe,清洗先isnull().sum()、duplicated().sum(),聚合先想好维度再groupby,合并先确认关联键和连接方式。这套流程在不同项目里反复出现,熟练之后,你拿到任何一份表格数据都会有清晰的拆解路径。
我个人在实际使用中还有一个体会:Pandas的报错虽然偶尔吓人,但绝大多数信息都在提示你“数据类型不对”“索引对不上”“存在缺失值”这几类问题。能快速定位问题的关键,不是背报错信息,而是对数据结构足够熟悉。当你脑子里对DataFrame的行列、索引、类型有了清晰的概念,排查问题就是顺藤摸瓜的事。
