ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

pandas数据分析入门:从数据读取到清洗、分组与实战技巧

pandas数据分析入门:从数据读取到清洗、分组与实战技巧

1. 为什么数据分析入门绕不开pandas

很多人第一次接触数据分析,脑子里想的都是"我要学机器学习""我要搞深度学习模型",结果打开教程第一课就是pandas,心里多少有点落差。我当初也是这样,觉得这玩意儿不就是个高级Excel吗,拖拖拽拽的事情,有什么好学的。直到真正上手处理一份几十万行的业务数据,Excel直接卡死,我才明白pandas的价值到底在哪。

pandas本质上是一套建立在Python之上的表格数据处理工具库,它把数据分成两种核心结构:Series(一维带标签数组)和DataFrame(二维表格)。你可以把它理解成一个"能用代码操控的Excel",但它的能力边界比Excel大得多——处理百万行数据毫无压力,支持复杂的条件筛选、分组聚合、时间序列操作,还能和NumPy、Matplotlib、scikit-learn这些库无缝衔接。

这篇文章适合谁看?如果你是刚转行做数据分析的新人,或者做运营、产品、财务想自己动手处理数据的职场人,又或者是写后端想补一补数据处理能力的开发者,pandas都是性价比最高的切入点。它不需要你有多深的数学功底,也不需要你懂算法原理,只要你会基本的Python语法,就能在几天内上手干活。

我写这篇内容的思路是:不堆API文档,而是按照一个真实数据分析项目的推进顺序,把pandas最核心的操作串起来讲。每一步我都会说清楚"为什么要这么做""不这么做会怎样",以及我自己踩过的坑。你跟着走一遍,基本就能独立处理一份中等复杂度的数据集了。

2. 环境搭建与数据读取的第一道坎

2.1 装库这件事,别想得太简单

安装pandas本身一条命令就够:

pip install pandas numpy matplotlib openpyxl

但我建议你顺手把numpy、matplotlib、openpyxl一起装上。原因很实际:pandas底层依赖NumPy做数值计算,画图要靠Matplotlib,读Excel文件需要openpyxl引擎。很多人只装pandas,结果读xlsx报错、画图报错,又回头一个个补,浪费时间。

如果你用Anaconda,这些库基本都预装了,省事。但我个人更推荐用虚拟环境管理,比如venv或者conda env,避免不同项目之间的库版本打架。我遇到过最坑的一次是,一个老项目依赖pandas 1.x,新项目用pandas 2.x,全局环境里两个版本互相覆盖,调了半天才发现是版本问题。

提示:pandas 2.x之后对数据类型和性能做了不少优化,新项目直接用最新稳定版即可,但接手老项目时一定要先看requirements.txt里锁定的版本。

2.2 读取数据:路径、编码、引擎三个坑

读取数据是分析的第一步,也是最容易出问题的一步。最常见的三种数据源是CSV、Excel和数据库。

读CSV用pd.read_csv(),看起来简单,但坑最多:

import pandas as pd df = pd.read_csv( "sales_data.csv", encoding="utf-8", sep=",", header=0, dtype={"order_id": str} )

这里每个参数都有讲究。encoding不指定的话,遇到中文经常报UnicodeDecodeError,国内数据常见的是utf-8、gbk、gb18030几种,实在不确定可以用chardet库先探测一下。sep默认是逗号,但有些系统导出的文件用制表符或分号分隔,读进来会变成一整列,这时候要手动指定。header指定哪一行是列名,有些文件前几行是说明文字,就得设header=2之类跳过。

dtype这个参数我要重点说。pandas默认会自动推断类型,但订单号、身份证号、手机号这类"看起来像数字"的字段,如果被推断成int或float,前导零会丢失,长数字还会变成科学计数法。我见过最典型的事故是订单号0012345读进来变成12345,对账的时候怎么都对不上。所以关键ID字段一定要显式指定成字符串。

读Excel用pd.read_excel(),需要openpyxl或xlrd引擎。大文件读取会慢,如果只是看数据结构,可以先读前几行:

df = pd.read_excel("report.xlsx", sheet_name="Sheet1", nrows=100)

sheet_name不指定默认读第一个工作表,多表文件一定要明确指定,否则读错表自己还不知道。

2.3 第一眼该看什么

数据读进来之后,别急着分析,先做一轮"体检"。我习惯用这几个方法快速摸底:

df.shape # 行列数 df.info() # 字段类型、非空数量、内存占用 df.head(10) # 前10行 df.describe() # 数值字段的统计摘要 df.isnull().sum() # 每个字段的缺失值数量

df.info()是我用得最多的,它能一眼看出哪些字段有缺失、类型对不对。比如一个本该是数值的"金额"字段显示成object,那多半是里面混了文字或者特殊符号,后面计算肯定出问题。

df.describe()默认只统计数值列,如果加上include="all"可以看所有列,包括字符串列的唯一值数量、出现频率最高的值等,对快速了解数据分布很有帮助。

这一步花不了几分钟,但能帮你避开后面一大堆莫名其妙的错误。我养成的习惯是,任何一份新数据到手,先跑一遍这套体检,心里有底了再往下做。

3. 数据清洗:把脏数据变成能用的数据

真实业务数据几乎没有干净的,缺失值、重复值、格式不统一、异常值,这些都是家常便饭。清洗这一步做得好不好,直接决定后面分析结论可不可信。我见过太多人跳过清洗直接跑分析,最后得出一个离谱的结论,回头查半天才发现是数据本身有问题。

3.1 缺失值:删还是填,要看业务含义

缺失值处理没有标准答案,关键看这个字段缺失意味着什么。

df.isnull().sum() # 统计缺失 df.dropna(subset=["amount"]) # 删除关键字段缺失的行 df["amount"].fillna(0) # 用0填充 df["amount"].fillna(df["amount"].mean()) # 用均值填充

我的经验是:关键字段缺失且占比很小(比如低于5%),直接删;占比大或者字段重要,就得想办法填。填充方式也有讲究,数值字段用均值、中位数,类别字段用众数,时间序列字段用前向填充ffill或后向填充bfill。

但这里有个反直觉的点:有时候"缺失"本身就是一种信息。比如用户资料里"收入"字段大量缺失,可能恰恰说明这部分用户不愿意透露收入,这个特征本身就有分析价值。所以别一上来就无脑填充,先想清楚缺失背后的业务逻辑。

注意:fillna默认返回新对象,不会修改原DataFrame。想原地修改要加inplace=True,但我不推荐这么做,容易在链式操作里出问题,还是显式赋值更安全。

3.2 重复值:去重前先搞清楚为什么重复

df.duplicated().sum() # 统计完全重复的行 df.drop_duplicates() # 删除完全重复 df.drop_duplicates(subset=["user_id"]) # 按某字段去重

完全重复的行通常是数据采集或导入时出的错,直接删没问题。但按某个字段去重就要小心了,比如按user_id去重,意味着每个用户只保留一条记录,那保留哪一条?默认保留第一条,但你可能需要保留最新的一条,这时候要先按时间排序再去重:

df.sort_values("update_time", ascending=False).drop_duplicates(subset=["user_id"])

我踩过的坑是:有一次做用户分析,没注意去重逻辑,把同一用户的多次行为记录当成多个用户,导致用户数虚高了一倍多。所以去重之前,一定要想清楚"什么算重复"。

3.3 类型转换与格式统一

类型不对是很多计算错误的根源。常见的有:

df["amount"] = pd.to_numeric(df["amount"], errors="coerce") df["date"] = pd.to_datetime(df["date"], errors="coerce") df["category"] = df["category"].astype("category")

pd.to_numeric的errors="coerce"参数很关键,它会把无法转换的值变成NaN而不是直接报错,方便你后续定位问题数据。pd.to_datetime同理。

字符串字段的格式统一也常被忽略。比如"北京""北京市""北京 "(带空格)在分组统计时会被当成三个不同的值。处理方法是先去除首尾空格,再统一替换:

df["city"] = df["city"].str.strip().str.replace("市", "", regex=False)

.str访问器是pandas处理字符串的利器,配合正则表达式能搞定大部分文本清洗。但要注意,.str方法遇到NaN会返回NaN,不会报错,这既是优点也是坑——你可能不知不觉漏掉了一批缺失值。

3.4 异常值:别急着删,先看看是不是真的异常

异常值检测常用的是四分位距法(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. 筛选、排序与分组:数据分析的核心动作

数据清洗完,就进入真正的分析环节。pandas在这一块的能力非常强,掌握几个核心操作,80%的分析需求都能覆盖。

4.1 条件筛选:loc和布尔索引

最常用的筛选方式是布尔索引:

df[df["amount"] > 1000] df[(df["city"] == "北京") & (df["amount"] > 1000)] df[df["category"].isin(["电子", "服装"])]

多个条件用&(与)、|(或)、~(非)连接,每个条件必须用括号包起来,这是新手最容易犯的语法错误。忘了括号,pandas会报运算符优先级相关的错,或者得到完全错误的结果。

loc和iloc是另外两个核心方法。loc按标签索引,iloc按位置索引:

df.loc[df["amount"] > 1000, ["order_id", "amount"]] df.iloc[0:10, 0:3]

loc的妙处在于可以同时筛选行和列,写起来很清晰。我处理复杂筛选时基本都用loc,可读性比纯布尔索引好很多。

4.2 排序:多字段排序的实际场景

df.sort_values("amount", ascending=False) df.sort_values(["city", "amount"], ascending=[True, False])

多字段排序在报表场景很常见,比如先按城市升序,同城市内按金额降序。ascending参数传列表时,长度要和排序字段数量一致,否则报错。

排序时如果字段有NaN,默认排在最后。想控制NaN的位置可以用na_position="first"。这个细节在做TopN榜单时要注意,否则NaN可能混进排名里。

4.3 分组聚合:groupby是pandas的灵魂

如果只能保留一个pandas功能,我选groupby。它的逻辑是"拆分-应用-合并":按某个字段把数据拆成若干组,对每组做计算,再把结果合并回来。

df.groupby("city")["amount"].sum() df.groupby("city").agg({ "amount": ["sum", "mean", "count"], "order_id": "nunique" })

agg方法支持对不同字段应用不同聚合函数,还能一次算多个指标。上面这段代码的意思是:按城市分组,对amount字段算总和、均值、计数,对order_id算去重计数。

分组后如果想把结果变回普通DataFrame,用reset_index():

result = df.groupby("city")["amount"].sum().reset_index()

不reset的话,分组字段会变成索引,后续操作容易出问题。我习惯每次groupby之后都reset一下,保持数据结构统一。

transform是另一个容易被忽略但很有用的方法,它把聚合结果广播回原数据的每一行:

df["city_avg"] = df.groupby("city")["amount"].transform("mean")

这样每一行都能拿到所属城市的平均值,方便做"和平均值对比"这类分析。这个技巧在做用户分层、异常检测时特别实用。

4.4 透视表:交叉分析的利器

pivot_table可以快速做出类似Excel透视表的效果:

pd.pivot_table( df, values="amount", index="city", columns="category", aggfunc="sum", fill_value=0 )

这段代码生成一个"城市×品类"的金额交叉表,行是城市,列是品类,单元格是汇总金额。fill_value=0把空值填成0,避免出现NaN影响后续计算。

透视表在分析多维度关系时效率极高,比手动groupby再拼接方便得多。但要注意,aggfunc默认是mean,做金额汇总时一定要显式指定sum,否则算出来的是平均值,结论完全不一样。

5. 数据合并与时间序列处理

5.1 merge:多表关联的正确姿势

真实项目的数据往往分散在多张表里,需要关联起来分析。merge是pandas的关联利器,类似SQL的join:

pd.merge(orders, users, on="user_id", how="left") pd.merge(orders, users, left_on="uid", right_on="user_id", how="inner")

how参数决定关联方式:inner取交集,left保留左表全部,right保留右表全部,outer取并集。最常用的是left,比如以订单表为主,关联用户信息,没有匹配上的用户信息显示为NaN。

关联时最容易出的问题是字段类型不一致。左表user_id是int,右表是str,merge会匹配不上,结果全是NaN。所以关联前一定要检查两边关联字段的类型是否一致。

另一个坑是关联后行数暴增。如果右表关联字段有重复值,一对多关联会让结果行数膨胀。比如订单表关联用户表,如果用户表里同一个user_id有多条记录,订单就会被复制多份。关联后一定要检查行数变化,确认符合预期。

5.2 concat:纵向拼接与横向拼接

pd.concat([df1, df2], axis=0, ignore_index=True) # 纵向堆叠 pd.concat([df1, df2], axis=1) # 横向拼接

纵向拼接常用于合并多个月份、多个渠道的数据。ignore_index=True会重新生成索引,避免索引重复。横向拼接则是按列拼,要求两边行数一致,否则会用NaN补齐。

我处理月度数据时,习惯写个循环把每个月的文件读进来放进列表,最后一次性concat,比逐个merge高效得多。

5.3 时间序列:日期字段的威力

时间字段处理好了,能挖出很多有价值的信息。先把日期转成datetime类型:

df["date"] = pd.to_datetime(df["date"]) df["year"] = df["date"].dt.year df["month"] = df["date"].dt.month df["weekday"] = df["date"].dt.dayofweek df["hour"] = df["date"].dt.hour

.dt访问器能提取年月日、星期、小时等各种时间维度。有了这些字段,就能做"按月趋势""周末vs工作日""高峰时段"这类分析。

按时间重采样用resample:

df.set_index("date").resample("M")["amount"].sum()

M表示按月,W按周,D按天。重采样在分析趋势时非常方便,比手动groupby年月简洁得多。

时间序列还有个常见需求是计算同比环比,用shift:

df["last_month"] = df["amount"].shift(1) df["growth"] = (df["amount"] - df["last_month"]) / df["last_month"]

shift(1)把数据往下移一行,就能拿到上一期的值,做环比计算。这个技巧在报表自动化里用得很多。

6. 那些文档里不会写的实战经验

前面讲的都是"标准动作",但真正让分析效率拉开差距的,往往是这些细节经验。

6.1 链式操作与SettingWithCopyWarning

pandas最烦人的警告之一就是SettingWithCopyWarning。它出现的原因是pandas分不清你操作的是原始数据的视图还是副本。比如:

df[df["amount"] > 1000]["level"] = "high" # 会报警告,且可能不生效

正确写法是用loc:

df.loc[df["amount"] > 1000, "level"] = "high"

或者先显式复制:

subset = df[df["amount"] > 1000].copy() subset["level"] = "high"

我的习惯是,任何筛选后的子集要修改,先.copy()一下,彻底避免这个警告。虽然多占点内存,但省心。

6.2 性能优化:大数据量下的几个技巧

数据量上到百万行,pandas的性能问题就显现了。几个实用的优化手段:

第一,用对数据类型。字符串列如果唯一值不多,转成category类型能大幅省内存:

df["city"] = df["city"].astype("category")

第二,避免循环。能用向量化操作就别用for循环,两者性能差几十倍甚至上百倍。比如要判断金额等级,用np.where或pd.cut比逐行判断快得多:

df["level"] = pd.cut(df["amount"], bins=[0, 100, 500, 1000, np.inf], labels=["低", "中", "高", "超高"])

第三,分块读取大文件。文件太大内存放不下时,用chunksize分块处理:

for chunk in pd.read_csv("big_file.csv", chunksize=100000): process(chunk)

第四,用query方法简化筛选。df.query("amount > 1000 and city == '北京'")写起来比布尔索引简洁,大数据量下性能也略好。

6.3 输出结果:别让最后一步掉链子

分析完要导出结果,to_csv和to_excel是常用方法:

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

index=False避免把索引也写进去,这个几乎每次都要加。encoding="utf-8-sig"是给Excel看的,带BOM头,否则Excel打开中文会乱码。这个细节坑过很多人,导出的CSV自己用代码读没问题,发给同事用Excel打开全是乱码。

导出Excel时如果数据量大,openpyxl会比较慢,可以考虑先导CSV再手动转,或者用xlsxwriter引擎。

6.4 调试技巧:出错了怎么快速定位

分析过程中报错是常态,关键是快速定位。我的几个习惯:

先看df.shape和df.dtypes,确认数据结构符合预期。很多错误其实是数据本身的问题,不是代码逻辑的问题。

用df.head()和df.sample(5)看实际数据长什么样,比盯着报错信息猜有效得多。

遇到类型错误,用df["col"].apply(type).value_counts()看看这一列到底混了哪些类型,一目了然。

分组聚合结果不对,先df.groupby("key").size()看看每组的数量,确认分组是否正确,再检查聚合函数。

这些方法看起来简单,但能解决大部分日常问题。真正复杂的bug,往往也是从这些基础检查里发现线索的。

7. 从pandas到完整分析流程的衔接

pandas本身只是数据处理工具,真正产生价值的是把它嵌入到完整的分析流程里。一个典型的流程是:数据读取→清洗→探索性分析→特征构造→可视化→建模或输出报告。

pandas在探索性分析和特征构造环节是绝对主力。比如做用户分群,先用groupby算出每个用户的消费频次、金额、最近购买时间(RFM三个维度),再用pandas做分位数切分,就能快速给用户打上标签。这个过程用pandas写下来也就几十行代码,但业务价值很高。

可视化环节,pandas自带的plot方法能快速出图:

df.groupby("month")["amount"].sum().plot(kind="line") df["category"].value_counts().plot(kind="bar")

虽然样式不如专业可视化库精致,但胜在快,探索阶段够用了。正式报告再换Matplotlib或Seaborn精修。

和scikit-learn衔接也很顺,pandas的DataFrame可以直接喂给模型:

from sklearn.linear_model import LinearRegression model = LinearRegression() model.fit(df[["feature1", "feature2"]], df["target"])

特征工程阶段用pandas做编码、标准化、缺失填充,然后转成模型能吃的格式,整个链路很流畅。

我个人的体会是,pandas学到最后,难点不在API本身,而在于对业务的理解。同样一份数据,懂业务的人能构造出有价值的特征,不懂业务的人只能做做基础统计。所以学pandas的同时,一定要结合自己的业务场景去练,拿真实数据反复折腾,比刷一百个教程都管用。

最后分享一个我常用的练习方法:找一份自己感兴趣的公开数据集,比如电商订单、电影评分、城市天气,然后给自己提三个业务问题,用pandas从头到尾分析一遍,把过程和结论写成笔记。坚持做几份,pandas就真正变成你自己的工具了。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进