关于pandas,日常数据分析工作中80%场景会用到的功能,我都帮你圈好了
我平时处理数据分析工作,跟pandas打交道的频率比跟午饭吃什么还要稳定。坦白说,很多朋友刚入门时会抱着官方文档啃一遍,或者去背那些低频冷门API,结果真到了日常工作里,翻来覆去用的也就那么二三十个函数。pandas真正厉害的地方不是它什么都能干,而是在80%的真实业务场景里,它都能用一套非常顺手的方式把"读数据、清洗、筛选、聚合、合并、变换"这几件事干完。这篇就把我自己日常最常用的功能按场景梳理一遍,顺便把我踩过的坑标出来,不管你是刚接触数据分析还是已经写了半年脚本,应该都能找到点有用的东西。
先说清楚这篇文章的定位:不是pandas大全,更不是从零开始的语法教程,面向的是"已经知道DataFrame是什么,但面对一张脏乱差的数据表不知道该用哪个函数"的这批人。我会按照实际干活时的流程来讲,先看数据,再洗数据,再查数,再汇总,再拼表,最后顺手整理成能交差的形态。每一段都有可以直接抄的代码和参数说明,照着敲就能用。
1. 先把数据搬进来:读取与第一眼探查
日常分析的第一步永远是"把数据弄进内存"。不管是接到一张Excel表、一堆CSV还是数据库里导出来的文件,最常用的就是pd.read_csv()。很多人不知道的是,这个函数看起来简单,但日常真正影响效率的是下面这几个参数。
import pandas as pd df = pd.read_csv( "d:/data/order_detail.csv", encoding="utf-8", # 文件编码,乱码时重点检查这里 sep=",", # 分隔符,制表符文件改 "\t" parse_dates=["order_time"], # 指定日期列,一次性转成 datetime 类型 usecols=["order_id", "user_id", "amount", "order_time"], # 只读需要的列 dtype={"user_id": str} # 指定列类型,防止用户ID被读成数值 )我建议你养成一个习惯:拿到文件先不要一股脑全读进来,先用nrows参数读前五千行探探路,确认列名、数据类型都符合预期再全量加载。日常场景里因为编码不对、分隔符错误导致的读取问题,比数据本身的问题还要多。如果读文件出现UnicodeDecodeError,优先试encoding="gbk",很多系统导出的中文CSV都是这个编码。
数据搬进来之后,第一眼探查通常会这么做:
df.shape # 维度,先知道多少行多少列 df.head(20) # 看前20行,别只看5行,很多问题藏在几十行之后 df.columns.tolist() # 所有列名,方便复制粘贴 df.dtypes # 各列数据类型,检查日期、数值是否被误判成object df.info() # 一次看全:行数、列数、非空计数、每列类型 df.describe() # 数值列的描述统计:计数、均值、标准差、四分位数其中df.info()是效率最高的一个,一行就能看出哪些列有缺失、哪些列类型不对。日常分析里,我见过最多的问题是:某个金额列因为混入了逗号或空格,被pandas读成了object而不是float,后面做均值、求和全部报错。这时候需要手动转类型,转到下一部分详细说。
还有一个探查习惯我想特别强调:拿到新数据集先做一次df.duplicated().sum(),看看有没有重复行。很多人一上来就做各种统计,结果最后发现数据里有重复,所有指标都得重算。数据探查阶段多花两分钟,后面能省两小时。
2. 数据清洗三板斧:缺失、重复、类型错乱
日常干活最花时间的就是清洗。真实业务数据永远不干净,要么缺字段,要么重复记录,要么类型奇葩。pandas处理这些问题的常用函数非常固定,掌握下面这三板斧就够应付绝大多数场景了。
2.1 缺失值:先弄明白是"没有"还是"不该有"
isna()和isnull()其实是一个函数,用来标记缺失值。dropna()是删除缺失,fillna()是填充缺失。关键要搞清楚业务上的处理逻辑,这不是一个技术问题,而是业务判断问题。
举个例子:某张用户订单表里,coupon_amount这一列大量为空,如果直接dropna()删行,可能把没使用优惠券的正常订单全删了;如果直接fillna(0)填充,那这些订单的优惠金额确实是0,逻辑上更合理。而user_name为空则可能是数据采集问题,这条记录本身就不可信,考虑删除。
# 统计每列缺失数量 df.isna().sum() # 删除关键字段缺失的行 df = df.dropna(subset=["order_id", "user_id"]) # 数值列统一填0 df["amount"] = df["amount"].fillna(0) # 前向填充:适合时间序列,比如某日库存值缺失,用上一个值填充 df["stock"] = df["stock"].fillna(method="ffill")日常分析里fillna用得远比dropna多,因为删数据会让后续指标基数发生变化。特别是做同比环比的时候,删掉几行可能就会导致两个月的总数对不上。填充方式的选择要能在汇报时说得出理由,当前一天的值填充、用均值填充、用0填充,背后对应的业务假设是完全不同的。
注意:
fillna(method="ffill")在新版pandas里可能要换成fillna(method="ffill")的写法已经过时,建议使用df["stock"].ffill()这种显式写法,更清晰也不容易触发废弃警告。
2.2 重复值:查重不只是"去重"这么简单
duplicated()用来判断每一行是否重复,drop_duplicates()用来删除重复行。但真正日常高频的场景是"在某一组关键字段上查重"。
比如判断一个用户是否下了多笔订单,就需要只在user_id列上查重,而不关心其他列是否一样:
# 返回重复行(默认是第一次出现的标记为False) df[df.duplicated(subset=["user_id"], keep=False)] # 按user_id去重,保留每个用户的第一条记录 df.drop_duplicates(subset=["user_id"], keep="first") # 按user_id+order_id去重,保留最后一条 df.drop_duplicates(subset=["user_id", "order_id"], keep="last")keep参数是这里最容易出错的:keep="first"保留重复出现中的第一条,keep="last"保留最后一条,keep=False则把所有重复行都标成True。平时做去重,我会先打印重复数量看看规模,再决定是删还是进一步排查原因。如果一张本该唯一的数据出现了大量重复,那大概率是上游数据同步逻辑有问题,这时候单纯去重是治标不治本。
2.3 类型转换:大部分数据问题的根源
类型转换看似简单,实际是日常分析中报错率最高的地方。最常见的需求是把字符串转数值、转日期,pandas分别提供了to_numeric和to_datetime两个函数,比astype更灵活、更好排查。
# 金额列混入了逗号和"元"字,直接astype(float)会报错 df["amount"] = pd.to_numeric(df["amount"].str.replace("元", "").str.replace(",", ""), errors="coerce") # 日期字符串转成datetime df["order_time"] = pd.to_datetime(df["order_time"], format="%Y-%m-%d %H:%M:%S") # 用户ID是数字但业务上无算术意义,转成字符串 df["user_id"] = df["user_id"].astype(str)注意errors="coerce"这个参数:它会把所有无法转换的值变成NaN,而不是直接抛出异常中止脚本。这在大规模清洗时非常有用,可以在转换之后接着用isna()筛出那些"转换失败"的值,单独检查原因。
我自己踩过的一个典型坑是:某个月的数据里日期列是2024-01-15,另一个月变成了2024/01/15,to_datetime在自动推断格式时偶尔会抽风。后来我养成了习惯,读文件时就通过parse_dates参数显式声明日期列,并在读取后打印两个值确认格式,避免后面处理到一半才发现问题。
类型转换还有个容易被忽略的场景:df["amount"].sum()突然结果不对,一查发现amount列里有字符串,字符串拼接替代了数值相加。所以每次从上游拿新数据,我都建议先执行一遍df.dtypes,看看数值列是不是真的数值类型。
3. 筛选与切片:日常查询数据的核心操作
数据到手且洗干净之后,接下来90%的时间都在做一件事:从表里挑出满足条件的行和列。pandas提供了好几套机制,新人容易混淆,我按照使用频率分别说清楚。
3.1 列选择:中括号的两种用法
选择单列,用df["列名"];选择多列,用df[["列名1", "列名2"]]。注意第二个是双重中括号,返回的是一个DataFrame,而不是Series,这在后续做groupby或者apply时会有细微差别。还有一个小技巧是用df.filter(regex="amount")按正则匹配列名,当列名很多且命名有规律时非常省事,比如一批以_amount结尾的列。
3.2 行筛选:布尔索引是绝对主流
日常用的最多的是布尔条件筛选,本质是"对每一行计算出一个布尔值,然后把True的行留下"。几个高频写法:
# 单条件 df[df["amount"] > 1000] # 多条件用 & | ~,注意每个条件都要加括号 df[(df["amount"] > 1000) & (df["city"] == "上海")] # 排除某个值 df[~df["status"].isin(["cancelled", "refunded"])] # 数值区间 df[df["amount"].between(500, 2000)] # 成员判断 df[df["city"].isin(["上海", "北京", "深圳"])]我最想提醒的一点是:多条件组合时,每个条件外面的括号不能省。df["amount"] > 1000 & df["city"] == "上海"这种写法会直接报错或者得出奇怪结果。另外,isin和between在代码可读性上比多个==或>连写要好得多,条件一多建议用这两个函数。
3.3 loc/iloc:既要位置又要标签
loc按标签(行名列名)取数据,iloc按位置(第几行第几列)取数据。日常分析里,loc的使用频率远高于iloc,因为大多数字段名比行号有意义。一个高频场景是在筛选基础上选择特定列:
# 筛选用户是男性且余额大于100的用户,只看user_id和balance两列 df.loc[df["gender"] == "M", ["user_id", "balance"]] # 按行号切片,比如看第10-20行 df.iloc[10:21, :]这里有个经常被误解的地方:df["a"]和df.loc[:, "a"]返回的结果通常一样,但df[["a"]]和df.loc[:, ["a"]]返回的都是DataFrame。建议在脚本里统一使用loc写法进行筛选加选列的操作,可读性更好,也能减少链式赋值带来的坑,后面会在避坑清单里专门讲。
3.4 query:写复杂条件时的偷懒神器
如果筛选条件特别长,连续写四五层括号确实折磨人。DataFrame.query()能把布尔表达式写成字符串,逻辑一眼就能看清:
df.query("amount > 1000 and city == '上海'") df.query("amount > 1000 or (city == '北京' and status == 'done')")query在列名没有特殊字符时会很好用,而且内部写法支持@变量引用外部变量:
threshold = 500 df.query("amount > @threshold")它的性能在大数据集上往往还略优于多次布尔索引,日常写脚本我越来越喜欢用query代替长串条件。不过要注意,列名含空格或中文带特殊符号时query容易出问题,那种情况下老老实实写布尔索引更稳。
4. 分组与聚合:日常统计报表的核心战场
做数据分析,十个任务里至少有六七个要落到"按某个维度汇总"。pandas的groupby是这一环节的绝对主角,配合agg和pivot_table基本上能覆盖全部的统计需求。
4.1 groupby基础:一张表教你拆解
假设我们要按城市统计订单数和销售额:
df.groupby("city").agg( 订单数=("order_id", "count"), 销售额=("amount", "sum"), 平均客单价=("amount", "mean") )这里有几层理解需要建立起来:groupby("city")是把数据按city分组,每个组内各自独立计算;agg传入的是一个字典,键是结果列名,值是一个二元元组,格式是(参与计算的列名, 聚合函数)。这个写法相比df.groupby("city")["amount"].sum()能一次完成多个指标,而且结果列名可以自己定义,产出的表格几乎可以直接放进报告里。
日常经常用的聚合函数无非这几个:sum求和、mean均值、count计数、nunique去重计数、max/min最大最小、median中位数。其中nunique是排查数据质量的好帮手,比如计算每个城市有多少个不同用户下单:
df.groupby("city")["user_id"].nunique()4.2 多维度分组与多重索引的处理
当你需要按两个维度分组时,直接传入一个列表即可:
df.groupby(["city", "order_month"])["amount"].sum().reset_index()这个操作的结果是一个Series,经过reset_index()之后变成DataFrame,列名默认是city、order_month、amount。如果不调用reset_index(),分组的列会成为索引,后续想再进行筛选或拼接反而麻烦。日常分析我基本都会先reset_index(),把一切保持成扁平的表格形态,这样好配合后面要讲的merge或者pivot操作。
4.3 pivot_table:当分组聚合需要"展开"时
pivot_table的典型场景是把原来长格式的数据变成宽格式。比如每个月的销售额,按行是城市,按列是月份,中间是销售额:
pd.pivot_table( df, values="amount", index="city", columns="order_month", aggfunc="sum", fill_value=0 )pivot_table和groupby的区别在于:groupby输出的是"长表"(每个组一行),pivot_table输出的是"宽表"(分组字段变成行、另一个维度变成列)。做日报、周报、月度对比时,宽表非常直观。而且fill_value=0参数可以自动把没有数据的格子填成0,省去了后续处理NaN的麻烦。
日常做月度销售对比表,我通常就是先groupby(["month", "city"])["amount"].sum(),然后pivot成index="month", columns="city",再加一行合计,这样一个简单的经营分析表格就成型了。
4.4 crosstab:快速做频次统计
如果要统计两个离散字段的交叉频次,比如"不同城市 × 不同支付方式的订单数量",crosstab是最省事的:
pd.crosstab(df["city"], df["pay_type"], margins=True, margins_name="合计")margins=True会自动在行和列上追加合计值,这在做分布分析时几乎是刚需。相比groupby加pivot两步操作,crosstab一行就能输出交叉频次表,代码干净很多。
5. 多表拼接:合并数据的核心实操
日常数据很少只在一张表里,订单表、用户表、商品表通常分开存放。分析时第一步往往就是把关联表拼起来。pandas的merge和concat承担了绝大多数拼接场景。
5.1 merge:类似SQL里的JOIN
pd.merge(df1, df2, on="user_id", how="left")是经典写法。on指定关联键,how指定连接方式,日常最常用left左连接和inner内连接。
how="left":以左表为基准,右表匹配不到的行填充NaN,订单和用户表关联时用这个,保留所有订单记录。how="inner":只保留两边都能匹配上的行,比如订单和退款表关联,只关心有退款记录的订单。how="outer":两边都保留,经常会出现很多NaN行,日常分析用得少。
举一个常见场景:订单表加用户信息。
order = pd.read_csv("order.csv") user = pd.read_csv("user.csv") merged = order.merge(user, on="user_id", how="left")如果两边列名不同,写成left_on="user_id", right_on="id"即可。如果关联键不止一个,传列表:on=["user_id", "city"]。我经常性犯的一个错误是:关联之后行数变多了。原因通常是关联键在右表中不是唯一的,比如user_id在用户表里其实重复了,结果一个订单匹配到多条用户记录,行数爆炸。所以merge之后第一步永远是检查行数:len(merged)和len(order)是否一致,不一致就得查右表有没有重复键。
5.2 concat:纵向拼表的场景
concat最常用的是把多个结构相同的DataFrame纵向堆叠。比如你手上有1月、2月、3月的订单明细,表结构完全一样,就可以:
df = pd.concat([jan_df, feb_df, mar_df], ignore_index=True)ignore_index=True会重新生成连续的行索引,否则拼接后的索引会保留各自的旧索引,后续筛选和取值容易出问题。还有横向拼接的场景axis=1,不过日常频率相对低,大多数横向需求用merge就解决了。
5.3 join方法:什么时候合适
join本质是merge的快捷方式,通常用于"按索引"关联时。比如两个DataFrame的索引都是order_time,你想把它们并列放在一起:
df_summary.join(df_detail, lsuffix="_sum", rsuffix="_detail")日常分析里我认为merge就够用了,join最大的坑是列名冲突会导致报错,需要额外传lsuffix和rsuffix。除非你是处理索引关联的数据,否则不建议优先使用join。
6. 数据变换与排序:把表格变成你要的样子
清洗和筛选解决的是"哪些行能用",而数据变换解决的是"列怎么算、怎么改、怎么排序"。这是日常分析里最后一个高频模块,搞定这一块基本就能输出成品表了。
6.1 按列/逐行应用函数:apply、map、applymap
apply是DataFrame最灵活的函数之一,按列或按行批量处理数据。
日常最常用的场景是"按已有列算新列":
# 算客单价 df["unit_price"] = df["amount"] / df["quantity"] # 对城市列做个标准化映射 df["city_group"] = df["city"].map({"上海": "一线", "北京": "一线", "杭州": "新一线"})map非常适合做离散值的映射,类似于Excel里的vlookup但更轻量。apply则适合需要写自定义函数的复杂场景。举个例子,按邮件后缀判断用户所属公司类型:
def get_email_type(email): if pd.isna(email): return "unknown" if email.endswith("qq.com"): return "personal" return "corporate" df["email_type"] = df["email"].apply(get_email_type)这里要特别注意:apply里不要写循环,也不要处理几百个不同条件——那是Python函数的正常用法,但如果数据量到了几十万行,apply的性能会明显慢于向量化写法。比如df["discount"] = df["amount"] * 0.9这种简单计算就别用apply,直接用向量化乘法,速度快几个数量级。
6.2 排序与排名:sort_values和rank
排序是出报表前的标准动作:
df.sort_values(by="amount", ascending=False, inplace=True)多列排序时按优先级传入列表即可:by=["city", "amount"]。inplace=True表示原地修改,旧pandas版本里这样用很普遍,现在更推荐写成df = df.sort_values(...),避免在函数链式调用中出现隐患。
rank()做排名时有个容易被忽视的参数method。默认的method="average"在遇到并列时会取平均排名,比如两个第一名会都显示1.5;日常业务上更常希望并列的时候都显示1,下一名跳到3,那就用method="min":
df["sales_rank"] = df["amount"].rank(method="min", ascending=False)6.3 数据分箱:cut和qcut
把连续数值分成区间是业务分析里的高频操作,比如按金额分档、按年龄分组:
# 按实际数值边界切分 df["amount_bin"] = pd.cut(df["amount"], bins=[0, 100, 500, 2000, 99999], labels=["低", "中", "高", "极高"]) # 按分位数等频切分 df["amount_qcut"] = pd.qcut(df["amount"], q=4, labels=["Q1", "Q2", "Q3", "Q4"])cut是固定区间宽度,适合金额阶梯;qcut是保证每个区间样本数量尽量一致,适合做相对分布对比。第一次用qcut可能会遇到bin edges must be unique的报错,那是数据中大量重复值导致的,可以通过duplicates="drop"参数处理,或者改用cut。
6.4 结构微调:rename、drop、unique
df.rename(columns={"amount": "销售额", "user_id": "用户ID"}, inplace=True) df.drop(columns=["unused_col"], inplace=True) df["city"].unique() # 查看城市的所有取值 df["city"].nunique() # 城市个数rename和drop是最常用的结构调整操作,unique配合nunique能快速了解离散字段的分布情况。
7. 高频杂项与避坑清单:这些坑我真的每个都踩过
最后一部分,我把自己日常遇到最频繁、最隐蔽的坑集中列一下,也给对应的排查思路。这些坑在官方文档里写得不算突出,但在实际项目中一旦踩到就会很头疼。
7.1 SettingWithCopyWarning:不是错误但必须理解
这是pandas新手最容易撞到的警告。触发场景通常是"先筛选出子集,再修改子集的某个值":
sub = df[df["city"] == "上海"] sub["amount"] = 0 # 触发SettingWithCopyWarning原因在于sub可能是原始df的一个视图,也可能是副本,pandas自己都"不确定",它就警告你"你的修改可能不会生效"。解决方式有两个:一是确认子集是独立的副本,一开始就写sub = df[df["city"] == "上海"].copy();二是尽量少用链式赋值。日常我干脆统一策略:每次做筛选切片,只要后面有改值的需求,就显式加.copy()。
7.2 链式赋值:性能差且容易出错
df[df["a"] > 1]["b"] = 2这种先筛选再赋值的写法,不仅可能触发上面说的warning,而且性能更差。推荐写法是用loc一步完成:
df.loc[df["a"] > 1, "b"] = 2如果你发现脚本里频繁出现df = df[条件]然后再df[列] = 值的模式,建议统一改成df.loc[条件, 列] = 值,一步到位,语义也更清晰。
7.3 性能:向量化永远优先于apply和循环
日常处理十万行以内的数据,apply还可以接受;一旦上到百万级,循环起来就能明显感到卡顿。前面提到过,能用向量化表达式(四则运算、比较、where)解决的,绝不用apply;能用groupby().transform()解决的,绝不写自定义循环。
还有一个容易被忽略的性能问题:频繁读取大文件。如果每天都要用同一份几GB数据做分析,建议用pyarrow或parquet格式归档,pd.read_parquet()比read_csv快好几倍。这已经超出了pandas本身的范畴,但属于pandas日常使用中"分分钟让人抓狂"的痛点。
7.4 dtype的object陷阱:字符串和数值混在一起
df.dtypes里显示object的列,运算时经常原地爆炸。排查思路很简单:先df[col].head(50)看看具体有什么值,再用pd.to_numeric(errors="coerce")批量转换,转换后的NaN单独筛出来看。常有同事跟我说"计算出了个奇怪大数",一查就是字符串拼接导致的。
7.5 内存占用:一个函数快速检查
当数据量大到内存吃紧时,用df.memory_usage(deep=True)可以查看各列内存占用。如果某些列是纯数字但精度不需要那么高,可以用df[col] = df[col].astype("float32")或int32来节省几乎一半的内存。数据高速迭代时这个优化非常实用。
写到这儿其实已经把我每天最常用的pandas功能都过了一遍。我个人在实际操作中的体会是:pandas最值得花时间研究的不是那些花哨的高级API,而是把读数据、清洗、筛选、聚合、拼接、变换这些基础功练到条件反射。很多看起来复杂的分析需求,拆到最后就是这些功能的组合。最后再分享一个小技巧:把今天提到的那些代码片段收集成一个你自己常用的工具脚本,每次接手新数据时先跑一轮探查函数,能少走很多弯路,也能省下大量重复调试的时间。