☰
Pandas数据分析常用语句全梳理:从读取清洗到聚合导出
2026/10/25 3:06:27 网站建设 项目流程

做数据分析这行,绕不开的一个工具就是 Pandas。我见过太多人,Excel 里点几下就能搞定的透视表,换到 Pandas 里要查半天文档;也见过有人对着一张几十万行的表,写了十几行嵌套循环,跑一晚上还没出结果。问题往往不在于不懂 Python,而在于没把 Pandas 那套“常用语句”变成肌肉记忆。这篇东西就是我把日常处理分析数据时反复用到的语句做一次系统梳理,从读取、清洗、筛选、聚合到导出,每一块都配上我实际踩过的坑和参数选择的理由。不管你是刚接触数据分析、还在跟SettingWithCopyWarning较劲的新手,还是已经能写几行 groupby 但总觉得自己写得不够利落的老手,下面这些内容应该都能直接拿去用。

1. 数据读取与初步体检:别急着分析,先摸清底细

很多人拿到数据文件第一反应就是pd.read_csv()一把梭,然后直接开始df.head()看两眼就动手算指标。这个习惯很危险。我吃过亏:一份看似正常的 CSV,某列里混了几个空字符串,读进来全变成 NaN,后面做求和直接少算了一大截;还有一次文件编码不是 UTF-8,读进来中文全是乱码,白折腾半小时。所以读取这一步的参数配置,值得单独拎出来讲。

1.1 read_csv 里最该记住的几个参数

pd.read_csv()的参数多到吓人,但日常真正高频的就那么几个。我按使用频率排一下:

参数作用我什么时候会用
encoding指定文件编码中文数据几乎必设,常用utf-8或gbk
sep分隔符遇到制表符分隔的文件设\t
usecols只读指定列宽表只关心几列时,省内存又省时间
dtype预设列类型防止 ID 列被读成数字丢前导零
na_values自定义缺失值标记数据里用-或NULL表示空值时
parse_dates解析日期列时间序列分析必备

举个实际例子。假设有个销售明细文件,编码是 GBK,日期列叫“下单日期”,ID 列是纯数字字符串但带前导零,还有一列用-表示缺失。稳妥的读法是这样:

import pandas as pd df = pd.read_csv( "sales.csv", encoding="gbk", usecols=["订单ID", "下单日期", "金额", "客户等级"], dtype={"订单ID": str}, na_values=["-", "NULL", ""], parse_dates=["下单日期"] )

这里每个参数都不是随便加的。dtype={"订单ID": str}是为了保住前导零——如果订单号是00123,不指定类型的话 Pandas 会自作聪明读成整数123,后面跟别的表做关联就对不上了。na_values把-和空字符串统一识别成缺失值,避免它们被当成有效字符串参与统计。parse_dates让日期列直接变成 datetime 类型,省掉后面手动pd.to_datetime()的一步。

提示:如果读文件时报UnicodeDecodeError,别急着换编码瞎试。先用chardet之类的库探测一下真实编码,或者用文本编辑器打开确认。GBK 和 UTF-8 混用是中文数据处理里最常见的翻车点。

1.2 三行代码完成数据体检

数据读进来之后,我习惯跑一套“体检三件套”,花不了十秒钟,但能提前发现大部分隐患:

df.info() # 看行列数、每列类型、非空计数 df.describe() # 看数值列的分布,均值中位数差太多要警惕 df.head(10) # 肉眼看前几行,确认列对齐、内容正常

df.info()是我最看重的。它直接告诉你每列有多少非空值,一眼就能看出哪列缺失严重。比如一列本该有 10000 行数据,info()显示只有 6000 个非空,那这列缺失率 40%,后面做任何基于它的分析都得掂量掂量。describe()则能快速暴露异常值——如果某列金额的均值是 500 但最大值是 99999999,那多半有录入错误或者单位不统一的问题,得先处理再分析。

还有一个容易被忽略的:df.shape和df.columns。前者确认行数跟你预期是否一致(防止读取时漏行或多行),后者确认列名有没有多余空格。列名带空格是个隐形杀手,df["金额"]能跑,df[" 金额"]就报 KeyError,肉眼还看不出来差别。

2. 缺失值与重复值处理:清洗环节的硬骨头

真实数据几乎没有干净的。缺失值、重复行、类型错乱,这三样是清洗阶段绕不过去的。这一块我踩的坑最多,尤其是缺失值处理,方法选错了,后面所有统计结论都是错的。

2.1 缺失值:先判断,再决定怎么填

看到 NaN 第一反应是fillna(0)的人不少,但这是最危险的操作之一。金额缺失填 0 可能合理,但“客户年龄”缺失填 0 就荒唐了,会把平均年龄严重拉低。所以处理缺失值之前,先搞清楚两件事:缺失比例是多少,缺失是不是有规律。

# 每列缺失数量和比例 missing = df.isnull().sum() missing_pct = (missing / len(df) * 100).round(2) pd.DataFrame({"缺失数": missing, "缺失比例%": missing_pct})

这段代码输出一张表,缺失比例一目了然。我的经验判断标准大致是:缺失比例低于 5%,可以考虑直接删行或者简单填充;5% 到 30% 之间,得看这列重不重要,重要的话用均值、中位数或者分组填充;超过 30%,除非这列是核心字段,否则建议直接放弃这列,硬填反而引入噪声。

填充方法的选择也有讲究。数值列我一般优先用中位数而不是均值,因为中位数抗异常值。举个例子,一列收入数据里混了几个超高值,均值被拉得很高,用它填充会把整体分布带偏,中位数就稳得多:

df["收入"] = df["收入"].fillna(df["收入"].median())

分类列则用众数填充,或者干脆填一个“未知”类别,保留“这行数据缺失过”这个信息:

df["客户等级"] = df["客户等级"].fillna("未知")

分组填充是进阶用法,也更贴近真实业务逻辑。比如不同城市的平均消费水平不一样,用全局均值填缺失就不合理,按城市分组取各自均值更靠谱:

df["消费额"] = df.groupby("城市")["消费额"].transform( lambda x: x.fillna(x.mean()) )

这里用transform而不是apply,是因为transform返回的结果和原表行数对齐,能直接赋值回去,apply在某些场景下会改变索引结构,容易出岔子。

2.2 重复值:去重前先想清楚“重复”的定义

df.drop_duplicates()谁都会用,但真正的问题在于:什么算重复?是全行完全一样才算,还是某个业务主键一样就算?这两个判断标准差别很大。

全行去重很简单:

df.drop_duplicates(inplace=True)

但更多时候,业务上的“重复”是指同一个订单号出现了多次。这时候要指定子集:

df.drop_duplicates(subset=["订单ID"], keep="first", inplace=True)

keep参数值得说一下。默认是"first",保留第一次出现的;"last"保留最后一次;False则把所有重复的整组删掉。选哪个取决于业务含义——如果是订单状态更新,通常保留last(最新状态);如果是防止重复录入,保留first就行。我见过有人不假思索用默认值,结果把最新状态的那条删了,留下旧数据,分析结论全反了。

注意:去重前务必先确认重复的原因。有些“重复”其实是正常的多条记录(比如一个客户多次下单),盲目去重会丢数据。我一般会先df[df.duplicated(subset=["订单ID"], keep=False)]把重复组全捞出来看一眼,确认是脏数据再删。

3. 筛选、排序与类型转换:把数据切成你要的形状

清洗完的数据是一整块,接下来要按分析需求切片。筛选、排序、类型转换这三样,构成了日常操作的主体。这一块语句不难,但组合起来用的时候,写法优劣差别很大。

3.1 条件筛选:loc 和布尔索引的正确姿势

最基础的筛选是单条件:

df[df["金额"] > 1000]

多条件组合,&表示与、|表示或、~表示非,每个条件必须用括号包起来。这是新手最容易犯的语法错误——不写括号,运算符优先级会让结果完全不对:

df[(df["金额"] > 1000) & (df["客户等级"] == "VIP")]

我强烈建议养成用.loc的习惯,尤其是需要同时筛选行和列的时候:

df.loc[df["金额"] > 1000, ["订单ID", "金额", "客户等级"]]

.loc的好处是语义清晰:逗号前是行条件,逗号后是列选择,一眼就能看懂在干什么。而且用.loc赋值不会触发SettingWithCopyWarning,这个警告是 Pandas 里最烦人的东西之一,根源就是链式索引(比如df[df["金额"]>1000]["等级"] = "高")到底是在原表上改还是在副本上改说不清楚。用.loc一步到位,问题就没了:

df.loc[df["金额"] > 1000, "等级"] = "高"

isin是做“多值匹配”的利器。要筛选客户等级属于某几个值之一的行,不用写一长串|:

df[df["客户等级"].isin(["VIP", "黄金", "铂金"])]

between则适合区间筛选,注意它默认是闭区间(包含两端):

df[df["金额"].between(100, 500)]

3.2 排序与排名:sort_values 的细节

排序本身简单,df.sort_values("金额", ascending=False)就是按金额降序。但有几个细节值得注意。多列排序时传入列表,排序优先级按列表顺序来:

df.sort_values(["城市", "金额"], ascending=[True, False])

上面这行的意思是:先按城市升序排,城市相同的再按金额降序排。ascending传列表可以给每列指定不同方向,这个用法很多人不知道,遇到“分组内排序”的需求时会绕很多弯路。

还有一个常被忽略的参数na_position,控制缺失值排在最前还是最后,默认"last"。如果做 Top N 分析,缺失值排最后正好不影响取头部,但如果反过来要看去尾部的数据,就得留意 NaN 会不会混进来。

排名用rank(),和排序是两回事。排序改变行顺序,排名是新增一列名次:

df["金额排名"] = df["金额"].rank(ascending=False, method="min")

method参数处理并列情况:"min"表示并列取最小名次(1、1、3),"dense"表示密集排名(1、1、2),"average"取平均名次。做业务排名时我一般用"min",符合“并列第一、下一个是第三”的直觉。

3.3 类型转换:astype 与 to_datetime 的坑

类型转换看着简单,坑却不少。astype是最常用的:

df["数量"] = df["数量"].astype(int)

但如果列里有 NaN,直接转 int 会报错,因为 int 类型不支持缺失值。解决办法是先填缺失,或者转成支持缺失的Int64(注意大写 I,这是 Pandas 的可空整数类型):

df["数量"] = df["数量"].astype("Int64")

日期转换是另一个高频操作。pd.to_datetime()比astype("datetime64")更稳,因为它有errors参数可以控制异常值的处理方式:

df["下单日期"] = pd.to_datetime(df["下单日期"], errors="coerce")

errors="coerce"的意思是:转不了的变成 NaT(缺失时间),而不是直接抛异常中断。这个设置在做批量清洗时特别有用,能让你先跑完整个流程,再回头处理那些转换失败的脏数据。转完之后,提取年月日、计算时间差都很方便:

df["年份"] = df["下单日期"].dt.year df["月份"] = df["下单日期"].dt.month df["间隔天数"] = (pd.Timestamp("2024-01-01") - df["下单日期"]).dt.days

.dt访问器是 datetime 列的专属入口,年、月、日、星期、季度都能直接取,比写 lambda 简洁得多。

4. 分组聚合:Pandas 分析能力的核心

如果说前面几块是准备工作,那分组聚合就是真正出分析结论的地方。groupby用得好不好,直接决定你的分析效率和代码质量。我见过太多人用循环加字典手动累加,其实一行groupby就搞定了。

4.1 groupby 加 agg:一次算出多个指标

最基础的用法是按某列分组求均值:

df.groupby("城市")["金额"].mean()

但实际分析往往要同时看好几个指标。agg允许一次传入多个聚合函数:

df.groupby("城市")["金额"].agg(["sum", "mean", "count", "max"])

输出是一张表,每个城市一行,各指标一列,非常直观。更进一步,可以对不同列应用不同函数,用字典指定:

df.groupby("城市").agg({ "金额": ["sum", "mean"], "订单ID": "count", "客户等级": lambda x: x.nunique() })

这里lambda x: x.nunique()算的是每个城市有多少个不同的客户等级,这种自定义聚合用 lambda 很灵活。不过要注意,用了 lambda 之后聚合结果的列名会变成<lambda>,不好看,可以用命名聚合解决:

df.groupby("城市").agg( 总金额=("金额", "sum"), 平均金额=("金额", "mean"), 订单数=("订单ID", "count"), 等级数=("客户等级", "nunique") )

这种“关键字参数 = (列名, 函数名)”的写法叫命名聚合,Pandas 0.25 之后支持,输出列名直接就是你要的名字,不用再手动 rename,代码可读性提升一大截。我现在基本都用这种写法。

4.2 transform 与 apply 的区别:什么时候用哪个

这三个函数经常被混用,但用途完全不同,搞清楚了能省很多事。

agg是“多行变一行”,分组后每组返回一个汇总值,结果行数等于分组数。transform是“多行变多行”,分组计算后把结果广播回原表的每一行,结果行数和原表一样。apply最灵活,可以对每组做任意操作,返回什么形状都行,但速度通常最慢。

举个典型场景:要算每个城市的金额占该城市总金额的比例。用transform最合适:

df["城市总金额"] = df.groupby("城市")["金额"].transform("sum") df["占比"] = df["金额"] / df["城市总金额"]

如果用agg就得先算出汇总表再 merge 回去,多好几步。transform一步到位,而且索引自动对齐,不会错位。

apply则适合复杂逻辑,比如每组要做多步计算、返回多个值:

def analyze_group(g): return pd.Series({ "总额": g["金额"].sum(), "客单价": g["金额"].mean(), "大单占比": (g["金额"] > 1000).mean() }) df.groupby("城市").apply(analyze_group)

这种写法把每组变成一个 Series,最后拼成一张表,适合做多维度的分组分析。不过apply在数据量大时确实慢,能用agg或transform解决的场景就别用它。

4.3 透视表:pivot_table 是 groupby 的进阶形态

pivot_table本质上是 groupby 的封装,但输出是二维交叉表,做多维分析时比 groupby 直观得多:

pd.pivot_table( df, values="金额", index="城市", columns="客户等级", aggfunc="sum", fill_value=0, margins=True )

这段代码生成一张“城市 × 客户等级”的金额汇总表,fill_value=0把空组合填 0(否则是 NaN),margins=True加上行列合计。做报表或者快速看多维分布时,这个比 groupby 加 unstack 省事多了。

提示:pivot_table默认对数值列取均值,如果忘了指定aggfunc,求和场景会得到错误结果。这个默认值坑过不少人,务必显式写清楚聚合函数。

5. 表连接与数据合并:多表分析的拼接逻辑

实际分析很少只靠一张表。订单表要关联客户表、商品表,这时候 merge 就上场了。merge 的参数不多,但how的选择直接决定结果对不对,值得细说。

5.1 merge 的四种连接方式

how参数有四个取值,对应 SQL 里的四种 join:

how 取值含义结果行数
inner内连接,只保留两表都有的键可能比任一表都少
left左连接,保留左表所有行至少等于左表行数
right右连接,保留右表所有行至少等于右表行数
outer外连接,保留两表所有键可能比两表之和还多

选哪个取决于业务问题。要分析“所有订单的客户信息”,用left,以订单表为左表,保证订单不丢;要分析“有下单记录的客户”,用inner,没下单的客户自然被排除。我见过有人该用 left 却用了 inner,结果把没有匹配客户信息的订单全丢了,统计出来的订单总额偏小,还查了半天原因。

result = orders.merge( customers, on="客户ID", how="left", suffixes=("_订单", "_客户") )

suffixes处理两表同名列的情况。如果两表都有“金额”列,不加 suffixes 会变成金额_x和金额_y,加了之后按你指定的后缀区分,可读性好很多。

5.2 连接后的行数校验:一个必须养成的习惯

merge 之后第一件事,永远是检查行数对不对。这是我最强调的一个习惯,因为连接出问题往往很隐蔽——不报错,但结果悄悄变多了或变少了。

行数变化有两种典型情况。一是键有重复:左表一个键对应右表多个键,连接后行数会膨胀。比如订单表按客户ID关联客户表,如果客户表里同一个客户ID有两条记录(数据质量问题),那每个订单都会变成两行。二是键类型不一致:一边是字符串"001",一边是整数1,看着一样实际匹配不上,left join 后右表的列全是 NaN。

# 连接前记录行数 before = len(orders) result = orders.merge(customers, on="客户ID", how="left") after = len(result) # 行数应该等于左表行数(left join 且右表键唯一的情况) assert after == before, f"行数异常:{before} -> {after}" # 检查右表列是否大量缺失(说明键没匹配上) print(result["客户名称"].isnull().sum())

这段校验代码建议直接抄进你的分析脚本。assert在行数不对时直接报错,逼你停下来查原因,比带着错误数据往下算强得多。检查右表列缺失数则能发现键类型不匹配的问题——如果缺失数接近总行数,基本可以断定是键对不上。

6. 常用统计与窗口计算:让分析更有深度

基础聚合之外,还有一些统计语句用得很频繁,尤其是做趋势分析和排名对比的时候。这一块内容稍微进阶一点,但掌握了能明显提升分析深度。

6.1 描述性统计与相关性

除了describe(),单列统计也有快捷方法:

df["金额"].mean() # 均值 df["金额"].median() # 中位数 df["金额"].std() # 标准差 df["金额"].quantile([0.25, 0.5, 0.75]) # 分位数 df["金额"].value_counts() # 频次统计,分类列常用

value_counts()特别适合看分类变量的分布,加normalize=True直接出占比:

df["客户等级"].value_counts(normalize=True).round(3)

相关性分析用corr(),默认皮尔逊相关系数:

df[["金额", "数量", "折扣"]].corr()

输出是相关系数矩阵。看的时候注意,相关系数只反映线性关系,且相关不等于因果。我见过有人看到两个指标相关系数 0.8 就下结论说一个导致另一个,这是分析大忌。另外,相关系数对异常值敏感,算之前最好先看看有没有极端值。

6.2 窗口函数:rolling 与 shift

做时间序列或者趋势分析,rolling移动窗口和shift位移是两把利器。移动平均是最典型的应用:

df = df.sort_values("下单日期") df["金额_7日均"] = df["金额"].rolling(window=7).mean()

rolling按行数滑动,window=7表示每 7 行算一次均值。注意它默认是向后看的(包含当前行和前面 6 行),做时间序列前一定要先按时间排序,否则窗口里的数据是乱的。如果数据不是每天都有,按行数滑动就不准了,得用时间窗口:

df.set_index("下单日期")["金额"].rolling("7D").mean()

"7D"表示 7 天的时间窗口,这样即使中间缺了几天,窗口也是按真实时间算的,更符合业务含义。

shift用来取前一行或后一行的值,算环比、同比全靠它:

df["上期金额"] = df["金额"].shift(1) df["环比增长"] = (df["金额"] - df["上期金额"]) / df["上期金额"]

shift(1)往下移一行,第一行会变成 NaN(因为没有上一行),这是正常的。算增长率时记得处理分母为 0 或 NaN 的情况,否则会得到 inf 或者报错。

7. 导出与性能:收尾环节别掉链子

分析做完要输出结果,导出这一步看似简单,但格式和编码没处理好,前面的活可能白干。另外数据量一大,性能问题就冒出来了,这一块也顺带说说。

7.1 to_csv 与 to_excel 的关键参数

导出 CSV 最需要注意的是编码和索引:

df.to_csv("result.csv", index=False, encoding="utf-8-sig")

index=False去掉行索引,否则会多出一列没用的序号。encoding="utf-8-sig"里的sig是带 BOM 的意思,这样导出的文件用 Excel 打开中文不会乱码。如果对方用的是老版本环境,可能得用gbk,具体看使用场景。

导出 Excel 用to_excel,多表导出用ExcelWriter:

with pd.ExcelWriter("report.xlsx") as writer: df_summary.to_excel(writer, sheet_name="汇总", index=False) df_detail.to_excel(writer, sheet_name="明细", index=False)

用with语句能确保文件正确关闭,避免写一半出错导致文件损坏。

7.2 大数据量下的几个实用技巧

数据量到几十万行以上,一些写法上的差异就会明显体现出来。几个我实测有效的技巧:

第一,能用向量化就别用循环。df.apply逐行处理比for循环快,但比真正的向量化操作慢得多。比如判断金额是否大于 1000,df["金额"] > 1000是向量化的,瞬间完成;写成df.apply(lambda r: r["金额"] > 1000, axis=1)就慢一个数量级。

第二,读取时用usecols和dtype限制范围,能显著降低内存占用。一个宽表如果只关心 5 列,读全部 50 列纯属浪费。

第三,分类列转成category类型能省大量内存:

df["客户等级"] = df["客户等级"].astype("category")

如果某列的不同取值很少(比如就几个等级),转成 category 后内存占用能降一个数量级,groupby 也会更快。

第四,链式操作注意顺序。先筛选再计算,别在全量数据上做重计算再筛选。比如要算 VIP 客户的总额,先df[df["等级"]=="VIP"]再求和,比先对全表求和再筛选快得多。

提示:如果数据大到单机内存放不下,Pandas 就不是最优选择了,可以考虑分块读取(chunksize参数)或者换用其他更适合大数据量的工具。但日常几十万到几百万行的分析,Pandas 配合上面这些技巧完全够用。

8. 我踩过的几个典型坑与应对经验

最后分享几个我在实际项目里踩过的坑,都是那种文档里不会写、但真遇到能折腾半天的问题。

第一个是SettingWithCopyWarning。这个警告的根源是链式索引导致的歧义。解决办法前面提过,用.loc一步到位赋值。但还有一种情况:从大表里筛出子表后修改子表,Pandas 不确定你改的是原表还是副本。稳妥做法是显式.copy():

sub = df[df["金额"] > 1000].copy() sub["等级"] = "高"

加了.copy()就明确告诉 Pandas 这是独立副本,警告消失,也不会意外改动原表。

第二个是 groupby 之后索引变了。groupby默认把分组键变成索引,后续操作如果没注意,容易出问题。加as_index=False可以让分组键保留为普通列:

df.groupby("城市", as_index=False)["金额"].sum()

这样结果就是规整的 DataFrame,不用再reset_index()。

第三个是浮点数比较。金额这类浮点列做相等判断经常出意外,因为0.1 + 0.2 != 0.3。要判断两个浮点数是否相等,用np.isclose或者设定一个容差范围,别直接用==。

第四个是中文列名带空格或特殊字符。前面提过,df["金额"]和df["金额 "]在肉眼看来几乎一样,但后者会报错。清洗列名是个好习惯:

df.columns = df.columns.str.strip()

一行代码去掉所有列名的首尾空格,能避免很多莫名其妙的 KeyError。

这些语句单独看都不复杂,但组合起来用、在真实数据上跑,细节上的差异就体现出来了。我的建议是别死记,挑几个自己手头的数据集反复练,把读取体检、清洗、筛选聚合、导出这条链路走顺,遇到报错就查文档看参数含义,比看十篇教程都管用。真正用熟了之后,你会发现 Pandas 处理日常分析任务的效率,比想象中高得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询