做数据分析这行十来年,我经手过的数据集从几百行的运营报表到上千万行的日志流水都有,但真正让我每天打开频率最高的工具,既不是各种花哨的可视化平台,也不是什么重型数仓方案,而是Pandas这个看起来朴素到不行的库。很多人刚学数据分析时,总觉得写几句read_csv、groupby、describe就算会了,可真到了实际项目里,面对脏数据、缺失值、时间字段错乱、内存爆掉这些破事,才发现那些零散的“常用语句”根本串不起来一条完整的分析链路。这篇内容我想干的事很直接:把Pandas 处理分析数据时真正高频、真正好用的语句,按照一条完整的分析流程重新梳理一遍,从读数据、看数据、洗数据、算数据到出结果,每一段都讲清楚“为什么这么写”“什么场景下用哪一句”“哪里容易踩坑”。不管你是刚转行做数据分析的新手,还是已经能写几行代码但总在细节上翻车的半熟手,都能从里面找到能直接抄走用的东西。我尽量不堆术语,能用生活化的类比就用类比,但该严谨的地方绝不含糊,毕竟数据这东西,差一个字段类型,结果可能就南辕北辙。
1. 整体思路:为什么 Pandas 语句要按“链路”来学
1.1 零散记语句的坑,我踩过太多次
刚入行那会儿,我学 Pandas 的方式特别原始——网上搜“Pandas 常用语句”,复制一堆代码片段存到笔记里,用的时候翻出来改改字段名。这个方法在简单场景下能跑通,但很快就暴露问题:语句之间没有上下文,我不知道groupby之后该接agg还是apply,不知道merge完为什么行数突然翻倍,更不知道astype该在清洗的哪一步做。结果就是每次分析都像在拼一副没有参考图的拼图,拼出来的东西自己都不敢信。
后来我换了个思路:把 Pandas 的常用语句按照一条真实的数据分析链路来组织。一条完整的链路大致是“读取 → 概览 → 清洗 → 变换 → 聚合 → 输出”,每个环节有它固定的高频语句,而这些语句之间存在明确的上下游关系。比如清洗阶段处理完缺失值,变换阶段才能安全地做类型转换;聚合阶段的分组键,必须在清洗阶段就确认没有脏值。按链路学,你记住的不再是孤立的语法,而是一套“什么时候该干什么”的判断逻辑。
这个思路的价值在于,它把“记忆负担”转化成了“流程理解”。你不需要背下所有参数,只需要知道当前处在链路的哪一环、这一环的目标是什么,对应的语句自然就浮出来了。下面我就按这个链路,把每个环节的核心语句和实操细节拆开讲。
1.2 一条完整分析链路的六个环节
我把日常分析工作拆成六个环节,每个环节对应一组核心语句,先给个全局视图,后面再逐个展开。
| 环节 | 核心目标 | 高频语句 |
|---|---|---|
| 读取 | 把数据完整、正确地加载进来 | read_csv、read_excel、read_sql |
| 概览 | 快速摸清数据规模和结构 | shape、info、head、describe |
| 清洗 | 处理缺失、重复、异常、类型 | isna、dropna、fillna、drop_duplicates、astype |
| 变换 | 筛选、排序、新增列、合并 | loc、query、sort_values、assign、merge |
| 聚合 | 分组统计与透视 | groupby、agg、pivot_table |
| 输出 | 落地成文件或对接下游 | to_csv、to_excel |
这张表不是让你背,而是让你建立一个心理地图。实际工作中,你大部分时间会在“清洗”和“变换”这两个环节反复横跳,因为真实数据永远比想象中脏。读取和概览是入口,聚合和输出是出口,中间两环才是主战场。理解了这一点,你就不会在读取阶段纠结太多参数,也不会在聚合阶段才发现数据没洗干净。
提示:不要试图一次性把所有语句学完。按链路走,先把“读取 + 概览”练熟,再攻“清洗”,最后补“变换 + 聚合”,这个顺序符合真实项目的推进节奏。
2. 读取与概览:先把数据摸清楚再动手
2.1 read_csv 的那些关键参数,别只会用默认值
read_csv是绝大多数人接触 Pandas 的第一句代码,但恰恰是这句最容易被用成“默认值一把梭”。我见过太多人读进来的数据,日期是字符串、数字带千分位逗号、编码乱码,然后花大量时间在后续清洗上补救。其实很多问题在读取阶段就能解决,关键在于几个参数。
先说dtype。默认情况下 Pandas 会自己推断每列类型,但它的推断逻辑并不总是符合业务预期。比如订单号00123会被推断成整数123,前导零直接丢失;再比如某些 ID 列明明是标识符,被推断成数字后做merge时可能因为精度问题对不上。我的习惯是:对标识类字段显式指定dtype=str,对数值字段按业务含义指定int64或float64。这样读进来就是对的,省掉后面一堆astype。
import pandas as pd df = pd.read_csv( "orders.csv", dtype={"order_id": str, "user_id": str, "amount": float}, parse_dates=["created_at"], encoding="utf-8" )parse_dates也值得单独说。日期字段如果读成字符串,后面做时间序列分析、按月聚合全得先转换,不如读取时一步到位。但要注意,如果日期格式不标准(比如2024/1/5和2024-01-05混着来),parse_dates可能报错或解析出NaT,这时候要么先用dtype=str读进来再统一格式,要么用date_parser自定义解析函数。我一般遇到格式混乱的日期,宁可先读成字符串,用字符串方法统一成标准格式后再to_datetime,这样可控性更强。
还有一个常被忽略的参数是usecols。大文件读取时,如果只需要其中几列,显式指定usecols能显著降低内存占用和读取时间。我处理过一个几十列、上百万行的日志文件,只取其中五列做分析,加上usecols后读取时间从十几秒降到两三秒,内存占用也降了一大截。这个优化在数据量大时非常划算。
2.2 概览三件套:shape、info、describe 各有分工
数据读进来第一件事不是急着分析,而是先“体检”。我常用的概览三件套是shape、info、describe,它们分工明确,缺一不可。
shape给你行列数,是最粗的规模判断。看到行数远少于预期,可能是读取时nrows限制或者文件本身不完整;看到列数异常多,可能是分隔符识别错了,整行被当成一列。
info是我最看重的,它一次性告诉你每列的非空数量、数据类型和内存占用。非空数量能立刻暴露缺失情况——比如某列显示1000 non-null但总行数是1200,那就有 200 个缺失值等着处理。数据类型则能发现推断错误,比如本该是数值的列显示成object,多半是混进了非数字字符。
describe针对数值列给出均值、标准差、分位数等统计量,用来快速发现异常值。比如某列金额的max是99999999,而75%分位才几百块,那这个最大值大概率是脏数据或者单位错误。不过describe默认只统计数值列,想看字符列的分布得加include='object',会给出唯一值数量、最高频值等信息。
print(df.shape) df.info() print(df.describe()) print(df.describe(include="object"))这三句连起来跑一遍,你对数据的整体质量就有了基本判断:规模对不对、缺失多不多、类型准不准、有没有明显异常。这个习惯我建议每个人都养成,花不了一分钟,但能避免后面大量返工。
2.3 抽样看数据:head 不够,sample 更真实
head()看前几行是常规操作,但它有个天然缺陷——前几行往往是格式最规整的部分,脏数据可能藏在中间或末尾。我通常会配合sample()随机抽样看几行,尤其是数据量大的时候,随机样本更能反映真实分布。
print(df.head(10)) print(df.sample(10, random_state=42))random_state固定后每次抽到同样的样本,方便复现问题。另外tail()看末尾几行也有用,特别是怀疑文件末尾有汇总行或空行时。这三者结合,基本能覆盖数据的头、中、尾,比只看head靠谱得多。
3. 清洗环节:脏数据才是常态
3.1 缺失值处理:先判断“为什么缺”,再决定“怎么补”
缺失值处理是清洗环节的重头戏,但很多人一上来就dropna或fillna,根本没想过缺失背后的原因。我的经验是:缺失值的处理方式,取决于缺失的机制。是随机缺失(比如用户没填可选字段),还是系统性缺失(比如某个渠道的数据压根没采集),这两种情况的处理逻辑完全不同。
先看怎么定位缺失。isna()返回布尔矩阵,配合sum()能统计每列缺失数量:
print(df.isna().sum()) print(df.isna().mean().sort_values(ascending=False))第二句用均值算缺失比例,比绝对数量更有参考价值。缺失比例低于 5% 的列,通常可以放心处理;超过 30% 的列就要谨慎,可能这个字段本身质量就有问题,强行填补反而引入偏差。
处理方式上,dropna适合缺失极少且删除不影响整体分布的情况,但要注意how和subset参数。默认how='any'是只要有一列缺失就删整行,这在列多的时候非常激进,可能删掉大量本可保留的数据。我一般用subset限定只在关键列缺失时才删:
df_clean = df.dropna(subset=["order_id", "amount"])fillna适合填补。数值列常用均值、中位数或固定值,字符列常用众数或"未知"。但这里有个细节:均值填补会压缩方差,中位数填补对异常值更稳健。如果某列有极端值,用均值填补会把整体分布往极端方向拉,这时候中位数更合适。另外,填补前最好先记录填补前的分布,填补后对比一下,确认没有把数据“改歪”。
df["amount"] = df["amount"].fillna(df["amount"].median()) df["channel"] = df["channel"].fillna("未知")注意:
fillna默认返回新对象,如果确认要原地修改,记得加inplace=True或者重新赋值。我见过不少人写了df.fillna(0)却没赋值,后面发现数据根本没变,排查半天。
3.2 重复值处理:drop_duplicates 的 subset 是关键
重复值在合并多源数据后特别常见。drop_duplicates()默认按所有列判断重复,但实际业务中,“重复”往往有特定定义——比如同一个订单号出现多次才算重复,而不是所有字段都一样。这时候subset参数就派上用场了。
print(df.duplicated(subset=["order_id"]).sum()) df_clean = df.drop_duplicates(subset=["order_id"], keep="first")keep参数决定保留哪一条:first保留首次出现,last保留最后出现,False则把所有重复项全删。业务上通常保留最新记录,那就用keep="last",前提是数据已按时间排序。这里有个坑:如果没排序就keep="last",保留的“最后一条”其实是文件里的物理顺序,不一定是时间上最新的。所以正确做法是先sort_values按时间排好,再drop_duplicates。
3.3 类型转换:astype 不是万能的,to_numeric 更稳
类型转换看着简单,实则暗藏玄机。astype是最直接的方式,但它遇到无法转换的值会直接报错,比如把含"N/A"的列转成数值就会崩。这时候pd.to_numeric更稳,它的errors参数能控制异常处理方式:
df["amount"] = pd.to_numeric(df["amount"], errors="coerce")errors="coerce"会把无法转换的值变成NaN,而不是报错中断。这个策略在清洗脏数据时特别实用——先把所有“看起来像数字但实际不是”的值统一变成缺失,再按缺失值逻辑处理,比逐条排查高效得多。转换完记得再isna().sum()看一眼,确认有多少值被转成了缺失,心里有数。
日期转换同理,pd.to_datetime配合errors="coerce"能容错处理格式混乱的日期列。转换后可以用dt访问器提取年、月、日、星期等衍生特征,这在时间维度分析里非常常用。
df["created_at"] = pd.to_datetime(df["created_at"], errors="coerce") df["month"] = df["created_at"].dt.to_period("M") df["weekday"] = df["created_at"].dt.dayofweek3.4 异常值识别:describe 加分位数,比拍脑袋靠谱
异常值没有统一标准,得结合业务判断。但识别手段可以标准化:先用describe看分位数分布,再用分位数或业务阈值圈定可疑范围。常用的统计口径是 IQR(四分位距)法:低于Q1 - 1.5*IQR或高于Q3 + 1.5*IQR的值视为异常。
q1 = df["amount"].quantile(0.25) q3 = df["amount"].quantile(0.75) iqr = q3 - q1 lower = q1 - 1.5 * iqr upper = q3 + 1.5 * iqr outliers = df[(df["amount"] < lower) | (df["amount"] > upper)]识别出来之后怎么处理,取决于业务含义。金额异常可能是录入错误,可以修正或剔除;也可能是真实的大额订单,那就得保留。我一般不会无脑删异常值,而是先标记出来单独看,确认是错误数据再处理。这个判断过程没法自动化,但把识别步骤标准化,能大幅减少遗漏。
4. 变换环节:筛选、排序、新增、合并的实操细节
4.1 loc 与 query:两种筛选方式怎么选
筛选数据是变换环节最高频的操作。loc基于标签筛选,query基于表达式筛选,两者各有适用场景。
loc的写法是df.loc[行条件, 列选择],行条件可以是布尔序列,列选择可以是列名列表。它的优势是灵活,行和列能同时操作,适合复杂筛选:
result = df.loc[(df["amount"] > 100) & (df["channel"] == "线上"), ["order_id", "amount"]]注意多个条件要用&连接且每个条件加括号,用and会报错,这是新手最常见的语法坑。
query的写法更接近自然语言,适合条件较多、追求可读性的场景:
result = df.query("amount > 100 and channel == '线上'")query里可以直接用and、or,还能引用外部变量(变量名前加@),可读性明显更好。但query对列名有特殊字符时不太友好,列名含空格或中文时得用反引号包裹。我的习惯是:简单条件用loc,复杂多条件用query,两者混着用完全没问题。
4.2 sort_values 排序:多列排序和 na_position 的细节
排序用sort_values,单列排序直接传列名,多列排序传列表并配合ascending列表指定每列的升降序:
df_sorted = df.sort_values(by=["channel", "amount"], ascending=[True, False])这里ascending=[True, False]表示先按渠道升序,渠道相同再按金额降序。这个多列排序在生成报表时特别常用。
另一个容易忽略的是na_position参数,控制缺失值排在最前还是最后,默认last。如果排序后要取head看“最大的几个值”,而缺失值排在最后,那没问题;但如果缺失值排在最前,head取到的可能全是空值,结果就错了。所以排序后取头部数据时,务必确认na_position符合预期。
4.3 assign 新增列:链式调用的好搭档
新增列有两种写法:直接赋值df["new_col"] = ...和用assign。直接赋值简单直接,但会修改原对象;assign返回新对象,适合链式调用,能让代码保持流畅的管道风格。
df_result = ( df.assign( amount_with_tax=lambda x: x["amount"] * 1.06, is_large=lambda x: x["amount"] > 1000 ) .query("is_large == True") .sort_values("amount_with_tax", ascending=False) )assign里用lambda x引用当前 DataFrame,这样多个新增列可以基于前面的结果继续计算。这种链式写法把“新增 → 筛选 → 排序”串成一条流水线,读起来像在描述业务逻辑,比一堆中间变量清晰得多。不过链式太长也不好调试,我一般控制在三到五步,超过就拆开。
4.4 merge 合并:how 参数决定行数变化,必须验证
merge是变换环节最容易出问题的地方,核心在于理解how参数对行数的影响。how='inner'取交集,how='left'保留左表全部,how='outer'取并集。很多人合并后发现行数暴涨,多半是右表存在重复的关联键,导致一对多匹配。
我的习惯是合并前后都检查行数,并验证关联键的唯一性:
print(df_left.shape, df_right.shape) print(df_right["key"].duplicated().sum()) merged = df_left.merge(df_right, on="key", how="left", validate="many_to_one") print(merged.shape)validate参数是很多人不知道的利器,它能强制校验关联关系:many_to_one表示左表多、右表一,如果右表键有重复会直接报错,帮你提前发现问题。这个参数我强烈建议在关键合并时都加上,能省掉大量排查时间。
5. 聚合环节:groupby 与 pivot_table 的实战用法
5.1 groupby 加 agg:分组统计的标准姿势
分组聚合是分析的核心动作。groupby负责分组,agg负责对每组做统计,两者组合能覆盖大部分汇总需求。
summary = df.groupby("channel").agg( order_count=("order_id", "count"), total_amount=("amount", "sum"), avg_amount=("amount", "mean"), max_amount=("amount", "max") )这种命名聚合(新列名=(源列, 函数))的写法比老式的字典写法更清晰,生成的列名直接就是业务含义,不用再rename。我早期用字典写法,结果列名是amount_sum这种拼接形式,还得手动改,后来全换成命名聚合,代码可读性提升明显。
分组键可以是单列,也可以是多列,传列表即可。多列分组时结果会变成多层索引,用reset_index()展平后更方便后续处理。另外agg支持传入自定义函数,比如算极差:
summary = df.groupby("channel").agg( amount_range=("amount", lambda x: x.max() - x.min()) )自定义函数灵活但性能不如内置函数,数据量大时优先用内置的sum、mean、count等。
5.2 pivot_table:交叉表分析的利器
pivot_table本质上是groupby的二维版本,适合做交叉分析。比如按渠道和月份两个维度看金额汇总:
pivot = pd.pivot_table( df, values="amount", index="channel", columns="month", aggfunc="sum", fill_value=0 )fill_value=0把缺失的组合填成 0,避免出现NaN影响后续计算。aggfunc可以传多个函数,生成多层列索引,适合一次性看多个指标。pivot_table和groupby的选择标准很简单:一维分组用groupby,二维交叉用pivot_table,后者生成的表格直接就是报表形态,省掉重塑步骤。
5.3 窗口函数:用 rolling 和 expanding 看趋势
除了分组聚合,时间序列分析常用窗口计算。rolling做滑动窗口,比如算 7 日移动平均:
df_sorted = df.sort_values("created_at") df_sorted["ma7"] = df_sorted["amount"].rolling(window=7).mean()expanding做累积窗口,从第一行累积到当前行,适合算累计值:
df_sorted["cum_amount"] = df_sorted["amount"].expanding().sum()这两个函数都要求数据先按时间排序,否则窗口算出来没有意义。rolling的min_periods参数控制窗口内最少需要多少个非空值才计算,默认等于窗口大小,前期会出现NaN,设置min_periods=1可以让前期也有值。
6. 常见问题与排查技巧实录
6.1 那些年踩过的坑:问题速查表
实际操作中遇到的问题五花八门,我把高频问题整理成速查表,方便对照排查。
| 问题现象 | 可能原因 | 排查与解决 |
|---|---|---|
| 读取后中文乱码 | 编码不匹配 | 尝试encoding="gbk"或"utf-8-sig" |
| 数值列被识别成 object | 混入非数字字符 | pd.to_numeric(errors="coerce")后查缺失 |
| merge 后行数暴涨 | 右表关联键重复 | 检查duplicated(),加validate参数 |
| groupby 结果列名混乱 | 用了字典聚合写法 | 改用命名聚合新列名=(列, 函数) |
| 日期解析出 NaT | 格式不统一 | 先统一字符串格式再to_datetime |
| 内存占用过高 | 类型推断过大或读取全列 | 指定dtype、usecols,数值降精度 |
| 筛选条件报错 | 用了and而非& | 多条件用&并给每个条件加括号 |
| 填补后数据没变 | fillna未赋值 | 重新赋值或加inplace=True |
这张表里的每一条,我几乎都亲身踩过。尤其是 merge 行数暴涨和 fillna 没赋值这两个,排查起来特别费时间,因为代码不报错,只是结果不对,得靠对比行数和抽查数据才能发现。
6.2 性能优化:大数据量下的三条实用建议
数据量一大,Pandas 的性能问题就冒出来了。我总结三条最实用的优化建议。
第一,读取时做减法。usecols只读需要的列,dtype指定更省内存的类型(比如int32代替int64,category代替object存低基数字符列),nrows在调试阶段先读少量样本。这三招组合,能让读取速度和内存占用都有明显改善。
第二,避免在循环里逐行操作。iterrows和apply逐行处理在数据量大时非常慢,能用向量化操作就用向量化。比如判断某列是否大于阈值,直接df["col"] > threshold生成布尔列,比apply快一个数量级。确实需要复杂逻辑时,优先考虑np.where或map,实在不行再上apply。
第三,及时释放不用的对象。分析过程中会产生很多中间 DataFrame,用del删除并配合gc.collect()能释放内存。这个习惯在处理大文件时特别重要,否则内存越用越满,最后直接崩掉。
6.3 独家避坑心得:三个文档里不会写的经验
分享三个我踩坑踩出来的经验,常规教程里基本不会提。
第一个,合并前先统一关联键的类型。左表键是字符串、右表键是整数,merge时 Pandas 可能不报错但匹配不上,结果全是NaN。我现在的习惯是合并前对两边关联键都做一次astype(str),确保类型一致,这个动作花不了几秒,但能避免大量诡异问题。
第二个,groupby 前先确认分组键没有缺失值。分组键含NaN时,默认行为是把NaN单独成一组,但很多人没意识到这一点,导致结果里多出一个“空组”。如果不想要这个组,分组前先dropna(subset=[分组键]),或者用dropna=False参数显式控制。
第三个,输出前重置索引。经过筛选、分组、合并后,索引往往是乱的,直接to_csv会把旧索引也写进去,多出一列没用的数字。输出前加一句reset_index(drop=True),能保证输出文件干净整洁。这个小细节看着不起眼,但交付给别人的文件如果带着乱七八糟的索引,专业度会打折扣。
7. 输出与落地:让分析结果能被复用
7.1 to_csv 与 to_excel:输出参数别忽略
分析结果最终要落地,to_csv和to_excel是最常用的两个出口。to_csv的关键参数是index=False(不写索引)和encoding(中文用utf-8-sig避免 Excel 打开乱码):
df_result.to_csv("result.csv", index=False, encoding="utf-8-sig")to_excel适合需要格式或多 sheet 的场景,配合ExcelWriter能一次写多个表:
with pd.ExcelWriter("result.xlsx") as writer: summary.to_excel(writer, sheet_name="汇总", index=False) detail.to_excel(writer, sheet_name="明细", index=False)输出前建议再跑一遍info()和head(),确认列类型和内容符合预期。我见过输出后发现数值列变成科学计数法、日期变成时间戳数字的情况,都是输出前没检查导致的。
7.2 把常用语句封装成函数:一次编写,反复使用
分析做多了会发现,很多清洗和统计逻辑是重复的。与其每次复制粘贴,不如把高频逻辑封装成函数。比如一个通用的缺失值报告函数:
def missing_report(df): report = df.isna().sum().to_frame("missing_count") report["missing_ratio"] = report["missing_count"] / len(df) return report.sort_values("missing_ratio", ascending=False)再比如一个通用的分组汇总函数,传入分组键和指标列就能出结果。封装的好处是逻辑统一、减少重复代码、降低出错概率。我现在的习惯是,同一个操作写到第三遍,就抽成函数。这个习惯坚持下来,慢慢就攒出了自己的工具库,新项目上手速度明显加快。
7.3 从语句到流程:建立自己的分析模板
最后说个更高层的经验。Pandas 语句学到最后,真正的竞争力不在于记住多少语法,而在于能不能把语句组织成一套可复用的分析模板。我的做法是维护一个模板文件,里面按“读取 → 概览 → 清洗 → 变换 → 聚合 → 输出”的链路预置好常用代码块,新项目开始时复制模板,按实际情况改字段名和参数,能省掉大量重复劳动。
这个模板不需要多复杂,关键是覆盖高频场景。比如读取部分预置好带dtype和parse_dates的read_csv,概览部分预置好三件套加缺失报告,清洗部分预置好类型转换和去重,聚合部分预置好命名聚合的示例。用的时候按需删减,比从零开始写快得多。这个习惯我坚持了好几年,现在处理一个新数据集,从读取到出第一版汇总,通常十几分钟就能搞定,靠的就是这套模板打底。
说到底,Pandas 的常用语句就那么些,真正拉开差距的是对“什么时候用哪句、为什么这么用”的理解。把语句放进链路里学,把链路固化成模板,把模板沉淀成习惯,这套方法我在实际项目里反复验证过,确实能让人从“会写几句代码”变成“能稳定交付分析结果”。如果你现在还在零散记语句的阶段,不妨从今天开始,按链路重新整理一遍你手头的代码,效果会比想象中明显。