Pandas数据分析常用语句全梳理:从读取清洗到聚合导出 做数据分析这行绕不开的一个工具就是 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或gbksep分隔符遇到制表符分隔的文件设\tusecols只读指定列宽表只关心几列时省内存又省时间dtype预设列类型防止 ID 列被读成数字丢前导零na_values自定义缺失值标记数据里用-或NULL表示空值时parse_dates解析日期列时间序列分析必备举个实际例子。假设有个销售明细文件编码是 GBK日期列叫“下单日期”ID 列是纯数字字符串但带前导零还有一列用-表示缺失。稳妥的读法是这样import pandas as pd df pd.read_csv( sales.csv, encodinggbk, 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(inplaceTrue)但更多时候业务上的“重复”是指同一个订单号出现了多次。这时候要指定子集df.drop_duplicates(subset[订单ID], keepfirst, inplaceTrue)keep参数值得说一下。默认是first保留第一次出现的last保留最后一次False则把所有重复的整组删掉。选哪个取决于业务含义——如果是订单状态更新通常保留last最新状态如果是防止重复录入保留first就行。我见过有人不假思索用默认值结果把最新状态的那条删了留下旧数据分析结论全反了。注意去重前务必先确认重复的原因。有些“重复”其实是正常的多条记录比如一个客户多次下单盲目去重会丢数据。我一般会先df[df.duplicated(subset[订单ID], keepFalse)]把重复组全捞出来看一眼确认是脏数据再删。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(金额, ascendingFalse)就是按金额降序。但有几个细节值得注意。多列排序时传入列表排序优先级按列表顺序来df.sort_values([城市, 金额], ascending[True, False])上面这行的意思是先按城市升序排城市相同的再按金额降序排。ascending传列表可以给每列指定不同方向这个用法很多人不知道遇到“分组内排序”的需求时会绕很多弯路。还有一个常被忽略的参数na_position控制缺失值排在最前还是最后默认last。如果做 Top N 分析缺失值排最后正好不影响取头部但如果反过来要看去尾部的数据就得留意 NaN 会不会混进来。排名用rank()和排序是两回事。排序改变行顺序排名是新增一列名次df[金额排名] df[金额].rank(ascendingFalse, methodmin)method参数处理并列情况min表示并列取最小名次1、1、3dense表示密集排名1、1、2average取平均名次。做业务排名时我一般用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[下单日期], errorscoerce)errorscoerce的意思是转不了的变成 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客户等级, aggfuncsum, fill_value0, marginsTrue )这段代码生成一张“城市 × 客户等级”的金额汇总表fill_value0把空组合填 0否则是 NaNmarginsTrue加上行列合计。做报表或者快速看多维分布时这个比 groupby 加 unstack 省事多了。提示pivot_table默认对数值列取均值如果忘了指定aggfunc求和场景会得到错误结果。这个默认值坑过不少人务必显式写清楚聚合函数。5. 表连接与数据合并多表分析的拼接逻辑实际分析很少只靠一张表。订单表要关联客户表、商品表这时候 merge 就上场了。merge 的参数不多但how的选择直接决定结果对不对值得细说。5.1 merge 的四种连接方式how参数有四个取值对应 SQL 里的四种 joinhow 取值含义结果行数inner内连接只保留两表都有的键可能比任一表都少left左连接保留左表所有行至少等于左表行数right右连接保留右表所有行至少等于右表行数outer外连接保留两表所有键可能比两表之和还多选哪个取决于业务问题。要分析“所有订单的客户信息”用left以订单表为左表保证订单不丢要分析“有下单记录的客户”用inner没下单的客户自然被排除。我见过有人该用 left 却用了 inner结果把没有匹配客户信息的订单全丢了统计出来的订单总额偏小还查了半天原因。result orders.merge( customers, on客户ID, howleft, suffixes(_订单, _客户) )suffixes处理两表同名列的情况。如果两表都有“金额”列不加 suffixes 会变成金额_x和金额_y加了之后按你指定的后缀区分可读性好很多。5.2 连接后的行数校验一个必须养成的习惯merge 之后第一件事永远是检查行数对不对。这是我最强调的一个习惯因为连接出问题往往很隐蔽——不报错但结果悄悄变多了或变少了。行数变化有两种典型情况。一是键有重复左表一个键对应右表多个键连接后行数会膨胀。比如订单表按客户ID关联客户表如果客户表里同一个客户ID有两条记录数据质量问题那每个订单都会变成两行。二是键类型不一致一边是字符串001一边是整数1看着一样实际匹配不上left join 后右表的列全是 NaN。# 连接前记录行数 before len(orders) result orders.merge(customers, on客户ID, howleft) 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()特别适合看分类变量的分布加normalizeTrue直接出占比df[客户等级].value_counts(normalizeTrue).round(3)相关性分析用corr()默认皮尔逊相关系数df[[金额, 数量, 折扣]].corr()输出是相关系数矩阵。看的时候注意相关系数只反映线性关系且相关不等于因果。我见过有人看到两个指标相关系数 0.8 就下结论说一个导致另一个这是分析大忌。另外相关系数对异常值敏感算之前最好先看看有没有极端值。6.2 窗口函数rolling 与 shift做时间序列或者趋势分析rolling移动窗口和shift位移是两把利器。移动平均是最典型的应用df df.sort_values(下单日期) df[金额_7日均] df[金额].rolling(window7).mean()rolling按行数滑动window7表示每 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, indexFalse, encodingutf-8-sig)indexFalse去掉行索引否则会多出一列没用的序号。encodingutf-8-sig里的sig是带 BOM 的意思这样导出的文件用 Excel 打开中文不会乱码。如果对方用的是老版本环境可能得用gbk具体看使用场景。导出 Excel 用to_excel多表导出用ExcelWriterwith pd.ExcelWriter(report.xlsx) as writer: df_summary.to_excel(writer, sheet_name汇总, indexFalse) df_detail.to_excel(writer, sheet_name明细, indexFalse)用with语句能确保文件正确关闭避免写一半出错导致文件损坏。7.2 大数据量下的几个实用技巧数据量到几十万行以上一些写法上的差异就会明显体现出来。几个我实测有效的技巧第一能用向量化就别用循环。df.apply逐行处理比for循环快但比真正的向量化操作慢得多。比如判断金额是否大于 1000df[金额] 1000是向量化的瞬间完成写成df.apply(lambda r: r[金额] 1000, axis1)就慢一个数量级。第二读取时用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_indexFalse可以让分组键保留为普通列df.groupby(城市, as_indexFalse)[金额].sum()这样结果就是规整的 DataFrame不用再reset_index()。第三个是浮点数比较。金额这类浮点列做相等判断经常出意外因为0.1 0.2 ! 0.3。要判断两个浮点数是否相等用np.isclose或者设定一个容差范围别直接用。第四个是中文列名带空格或特殊字符。前面提过df[金额]和df[金额 ]在肉眼看来几乎一样但后者会报错。清洗列名是个好习惯df.columns df.columns.str.strip()一行代码去掉所有列名的首尾空格能避免很多莫名其妙的 KeyError。这些语句单独看都不复杂但组合起来用、在真实数据上跑细节上的差异就体现出来了。我的建议是别死记挑几个自己手头的数据集反复练把读取体检、清洗、筛选聚合、导出这条链路走顺遇到报错就查文档看参数含义比看十篇教程都管用。真正用熟了之后你会发现 Pandas 处理日常分析任务的效率比想象中高得多。