☰
Pandas数据分析实战:从200万行销售表到高效清洗聚合
2026/10/14 4:40:07 网站建设 项目流程

简介:《Pandas数据分析实战》面向已具备Python基础、希望深入掌握数据处理核心技能的开发者与数据分析学习者,围绕科学计算、时间序列分析与数据可视化三大方向展开。全书以近百个源自真实数据集的案例为主线,系统讲解数据清洗、分组聚合、布尔索引与多重索引、数据重构、SQL-like合并等操作,并演示如何与Matplotlib、Seaborn集成绘制具有洞察力的图表,帮助读者高效完成从数据整理到机器学习准备的完整流程。资源包内共1个PDF文件,压缩后约53.38MB,内容为完整电子书,便于在电脑或平板上随时查阅、对照练习。目前已有272人学习下载,适合需要系统梳理Pandas高级用法、提升实际项目数据处理效率的读者参考。

1. 从一份 200 万行的销售表说起:Pandas 数据分析实战到底在解决什么

如果你手上有一份几十万行、上百列的 CSV,用 Excel 打开要转三分钟圈,筛选一次卡一次,公式改一处全表重算——这就是 Pandas 数据分析实战最典型的起点。它不是一个新工具,而是一套用 Python 把「读数据、洗数据、算指标、出结论」串成可复现脚本的方法论。适合两类人:一类是天天和业务报表打交道、想摆脱手工透视表的运营和财务分析岗;另一类是有 Python 基础、但一碰到真实脏数据就不知道从哪下手的开发或数据岗。核心价值只有一个:把一次性的手工操作,变成下次换一份数据还能直接跑的代码。这篇笔记按「读进来、洗干净、算明白、跑得快」四步走,每一步都给能抄的命令和参数。

2. 把数据读进来:read_csv 的 6 个必调参数与内存估算

很多人以为pd.read_csv()就是一行的事,直到遇到中文乱码、日期变成字符串、几百万行直接把内存吃爆。读数据这一步没做对,后面所有分析都是在错误的地基上盖楼。

2.1 先估算内存,再决定怎么读

在写任何read_csv之前,我一般先看一眼文件大小和行数。一个粗略的换算:CSV 文件大小乘以 1.5 到 2 倍,大致就是读进内存后 DataFrame 占用的空间。如果文件 800MB,那内存占用可能到 1.5GB 左右。机器只有 4GB 内存的话,就得考虑分块读或者只读需要的列。

# 先看文件有多少行、多大,不用 Python 也能做 wc -l sales_2024.csv # 行数 ls -lh sales_2024.csv # 文件大小 head -3 sales_2024.csv # 看前 3 行,确认分隔符和表头

wc -l给出总行数,ls -lh看体积,head -3确认第一行是不是表头、分隔符是逗号还是制表符。这三条命令十秒钟就能跑完,比在 Python 里反复试错快得多。

2.2 read_csv 的 6 个必调参数

import pandas as pd df = pd.read_csv( "sales_2024.csv", encoding="utf-8", # 中文乱码时换 "gbk" 或 "utf-8-sig" sep=",", # 分隔符,制表符用 "\t" usecols=["order_id", "city", "amount", "order_date"], # 只读需要的列 dtype={"order_id": "str", "amount": "float32"}, # 指定类型省内存 parse_dates=["order_date"], # 直接解析成 datetime chunksize=500_000, # 大文件分块,返回迭代器 ) # chunksize 模式下 df 是 TextFileReader,需要遍历 # for chunk in df: # process(chunk)

逐个说清楚为什么这么设。encoding是最容易翻车的地方,Windows 导出的 CSV 常见gbk,带 BOM 的用utf-8-sig,报UnicodeDecodeError时先换编码而不是去改数据。usecols是省内存最狠的一招,一张 100 列的表只读 4 列,内存直接降到几十分之一。dtype里把订单号设成str很关键——订单号如果被识别成数字,前导零会丢,长数字还会变成科学计数法。amount用float32而不是默认的float64,精度对金额分析够用,内存省一半。parse_dates让日期列直接是 datetime 类型,省掉后面反复pd.to_datetime。chunksize是内存不够时的后悔药,返回的是迭代器,每次只加载 50 万行。

提示:low_memory=False能消除混合类型警告,但会一次性加载全部数据判断类型,内存紧张时反而更危险,慎用。

2.3 读进来先做三件事

数据读进来别急着分析,先跑这三行:

print(df.shape) # 行列数,确认没读少 print(df.dtypes) # 每列类型,确认日期是 datetime、金额是 float print(df.isnull().sum()) # 每列缺失值数量,心里有数

shape对不上预期行数,多半是分隔符或引号问题;dtypes里日期显示object说明解析失败;isnull().sum()让你提前知道哪些列要处理缺失。这三步花不到五秒,能省掉后面半小时的排查。

3. 数据清洗:缺失值、重复值和类型转换的取舍

清洗是 Pandas 实战里最耗时间也最见功力的环节。新手常犯的错是拿到数据就dropna(),结果把一半有效数据扔了;或者无脑fillna(0),把「没记录」和「金额为零」混为一谈。

3.1 缺失值:先判断缺失的含义再动手

缺失值分三种情况,处理方式完全不同。第一种是「真的没有」,比如新用户还没有下单记录,这种填 0 合理。第二种是「没采集到」,比如传感器断线,填 0 会污染均值,应该填中位数或前向填充。第三种是「不该有」,比如订单表里金额为空,这条记录本身有问题,该删。

# 先看缺失比例,再决定策略 missing_ratio = df.isnull().mean().sort_values(ascending=False) print(missing_ratio[missing_ratio > 0]) # 按列分别处理 df["amount"] = df["amount"].fillna(df["amount"].median()) # 数值列填中位数 df["city"] = df["city"].fillna("未知") # 类别列填占位符 df = df.dropna(subset=["order_id"]) # 主键为空直接删 df["stock"] = df["stock"].ffill() # 时序数据前向填充

isnull().mean()比isnull().sum()更直观,直接告诉你每列缺了百分之几。超过 60% 缺失的列,通常直接考虑丢弃,因为填充带来的偏差比保留的价值大。fillna用中位数而不是均值,是因为金额、销量这类数据常有极端值,均值会被拉偏。ffill()前向填充适合按时间排序的库存、价格类数据,但前提是先sort_values排好序,否则填的是错的。

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

# 看有多少完全重复的行 print(df.duplicated().sum()) # 按业务主键看重复 dup = df[df.duplicated(subset=["order_id"], keep=False)] print(dup.sort_values("order_id").head(20)) # 确认后去重,保留最后一条(通常是最新状态) df = df.drop_duplicates(subset=["order_id"], keep="last")

duplicated()不带参数看的是整行完全一致。但真实业务里更常见的是主键重复、其他字段不同——比如同一订单被系统写了两次,状态不一样。这时候要先keep=False把所有重复项捞出来看一眼,确认哪条是最新的,再用keep="last"保留。直接drop_duplicates()不带subset,很可能漏掉这种业务重复。

3.3 类型转换:astype 和 to_datetime 的边界

# 字符串转数值,遇到脏字符用 errors="coerce" 变 NaN df["amount"] = pd.to_numeric(df["amount"], errors="coerce") # 字符串转日期,指定格式比自动推断快很多 df["order_date"] = pd.to_datetime(df["order_date"], format="%Y-%m-%d", errors="coerce") # 类别列转 category,内存能省 90% df["city"] = df["city"].astype("category") # 转换后检查产生了多少 NaN print(df["amount"].isnull().sum())

errors="coerce"是关键参数,它把转不了的脏数据变成 NaN 而不是直接报错中断,让你能一次性看到所有问题数据。to_datetime指定format比让它自动推断快好几倍,数据量大时差别明显。category类型适合取值重复度高的列,比如城市、品类,几十万行里只有几十个城市,转成 category 后内存占用断崖式下降。转换完一定要再查一次 NaN 数量,因为coerce会悄悄把脏数据变成缺失值,不检查就发现不了。

4. 分组聚合与透视:groupby 的 split-apply-combine 怎么用对

分组聚合是 Pandas 最核心的能力,也是最多人用错的地方。groupby的本质是三步:按 key 把数据切成若干块(split),对每块应用函数(apply),把结果拼回一张表(combine)。理解这三步,绝大多数聚合需求都能推出来。

4.1 单列分组到多列聚合

result = ( df.groupby("city", as_index=False) .agg( 订单数=("order_id", "count"), 总金额=("amount", "sum"), 客单价=("amount", "mean"), 最大单笔=("amount", "max"), ) .sort_values("总金额", ascending=False) ) print(result.head(10))

as_index=False让分组键变回普通列,方便后续排序和导出,不加的话城市会变成索引。agg里用命名聚合的写法,左边是输出列名,右边是「源列名 + 函数名」的元组,这样出来的表列名清晰,不用事后 rename。count统计的是非空值数量,如果要统计行数(含空值)用size。这几个指标里,客单价用mean而不是sum/count,是因为mean会自动跳过 NaN,而sum/count会把 NaN 当 0 算,结果偏低。

4.2 transform 和 apply 的区别

很多人分不清transform和apply,用错了结果对不上还不知道为什么。

# transform:返回和原表等长的结果,用于新增列 df["city_avg"] = df.groupby("city")["amount"].transform("mean") df["diff_from_avg"] = df["amount"] - df["city_avg"] # apply:返回任意形状,用于复杂逻辑 def top_n(group, n=3): return group.nlargest(n, "amount") top3_per_city = df.groupby("city", group_keys=False).apply(top_n, n=3)

transform的关键是返回长度和原表一致,所以能直接赋值成新列,常用来算「每个城市平均值」「每行占组内比例」这类组内对比。apply灵活但慢,它把每个分组当独立 DataFrame 传给你的函数,返回什么形状都行。数据量大时优先用transform和内置聚合函数,apply留到实在没有向量化写法时再用。

4.3 透视表:pivot_table 一次出交叉报表

pivot = pd.pivot_table( df, values="amount", index="city", columns="category", aggfunc="sum", fill_value=0, margins=True, # 加一行一列合计 margins_name="合计", ) print(pivot)

pivot_table默认聚合函数是mean,做金额汇总一定要显式写aggfunc="sum",这是最常见的翻车点——不写的话出来的是平均值,数字看着合理但完全不是你要的。fill_value=0把没有数据的交叉格填 0,避免 NaN 影响后续计算。margins=True自动加合计行列,做报表时省事。index和columns的选择决定了报表的行列结构,一般把分类维度放columns,把主体维度放index。

5. 避坑与排查:Pandas 实战里最容易翻车的 5 个地方

这一章全是血泪经验,每条都按「现象、原因、解决」写清楚,遇到时直接对号入座。

5.1 SettingWithCopyWarning 警告

现象:对 DataFrame 切片后赋值,控制台刷出SettingWithCopyWarning,有时改了没生效,有时又生效了,非常玄学。

原因:Pandas 分不清你拿到的是视图还是副本。df[df["amount"] > 100]["status"] = "大额"这种链式索引,赋值可能作用在临时副本上,原表没变。

解决:用.loc一次性完成筛选和赋值,或者显式.copy()。

# 错误写法 df[df["amount"] > 100]["status"] = "大额" # 正确写法一:loc 一步到位 df.loc[df["amount"] > 100, "status"] = "大额" # 正确写法二:先 copy 再改 big = df[df["amount"] > 100].copy() big["status"] = "大额"

5.2 链式索引导致的性能塌方

现象:几百万行数据,一个筛选赋值跑了十几秒。

原因:链式索引每次都要重新计算布尔掩码,多次链式调用等于把全表扫了好几遍。

解决:把中间结果存成变量,或者用query一次性表达。

# 慢:多次链式 df[df["city"] == "北京"][df["amount"] > 100]["status"] = "重点" # 快:一次掩码 mask = (df["city"] == "北京") & (df["amount"] > 100) df.loc[mask, "status"] = "重点"

5.3 整数列出现 NaN 后变成 float

现象:本来order_id是整数,处理完缺失值后变成1001.0这种带小数点的浮点数。

原因:Pandas 的整数类型不支持 NaN,一旦某列出现缺失值,整列会被自动提升为 float64。

解决:用可空整数类型Int64(注意大写 I),或者处理完缺失值后再转回 int。

df["order_id"] = df["order_id"].astype("Int64") # 可空整数,支持 NaN # 或者确认无缺失后 df["order_id"] = df["order_id"].astype("int64")

5.4 merge 后行数变多

现象:两个表 merge,本来各 10 万行,结果出来 15 万行。

原因:连接键在某一侧有重复值,一对多连接会笛卡尔式膨胀。

解决:merge 前先检查连接键的唯一性,用validate参数强制校验。

# 检查右表连接键是否唯一 print(df_right["key"].duplicated().sum()) # 用 validate 强制一对一,不满足直接报错 merged = pd.merge(df_left, df_right, on="key", how="left", validate="one_to_one")

validate可选one_to_one、one_to_many、many_to_one、many_to_many,设对了能在 merge 阶段就暴露数据问题,而不是等到结果对不上才发现。

5.5 循环里逐行修改 DataFrame

现象:用for循环遍历几万行,每行df.loc[i, "col"] = ...,跑了几分钟还没完。

原因:每次.loc赋值都可能触发一次数据块重建,几万次就是几万次重建。

解决:用向量化操作、apply或np.where替代逐行循环。

# 慢:逐行循环 for i in range(len(df)): if df.loc[i, "amount"] > 100: df.loc[i, "level"] = "高" # 快:向量化 df["level"] = np.where(df["amount"] > 100, "高", "低")

6. 让分析跑得更快:dtype 优化、分块与 eval 的实战技巧

数据量上来之后,Pandas 默认配置会显得力不从心。这一章讲几个我常用的提速手段,都是改几行代码就能见效的。

6.1 dtype 优化:把内存降下来,速度自然上去

内存和速度是绑在一起的。数据占的内存越小,CPU 缓存命中率越高,运算越快。优化 dtype 是最划算的一步。

原始类型优化后适用场景内存变化
float64float32金额、比例,精度要求不高降 50%
int64int32 / int16数量、编号,范围够用降 50%~75%
objectcategory城市、品类等低基数列降 90%+
objectdatetime64日期字符串视情况
def optimize_dtypes(df): for col in df.select_dtypes(include=["float64"]).columns: df[col] = df[col].astype("float32") for col in df.select_dtypes(include=["int64"]).columns: if df[col].min() > -32768 and df[col].max() < 32767: df[col] = df[col].astype("int16") else: df[col] = df[col].astype("int32") for col in df.select_dtypes(include=["object"]).columns: if df[col].nunique() / len(df) < 0.5: # 基数低于 50% 才转 category df[col] = df[col].astype("category") return df df = optimize_dtypes(df) print(df.memory_usage(deep=True).sum() / 1024**2, "MB")

这段函数按类型批量降级。float32对金额分析足够,误差在小数点后 6 位以外。整数按范围选int16或int32,注意先看min和max别溢出。object转category前判断基数比例,唯一值占比超过 50% 的列转了反而更占内存,所以加了阈值。memory_usage(deep=True)才是真实内存,不加deep只算指针不算字符串内容。

6.2 分块处理:内存不够时的标准姿势

result_list = [] for chunk in pd.read_csv("big_file.csv", chunksize=200_000): chunk = chunk[chunk["amount"] > 0] agg = chunk.groupby("city", as_index=False)["amount"].sum() result_list.append(agg) final = pd.concat(result_list).groupby("city", as_index=False)["amount"].sum()

分块的核心思路是「每块先做能做的聚合,最后再合并」。注意这里每块算的是部分和,最后要再groupby一次把同一城市的部分和加起来。如果直接concat不重新聚合,同一个城市会出现多行。分块大小一般设 10 万到 50 万行,太小了循环次数多,太大了内存优势不明显。

6.3 query 和 eval:表达式加速

# 普通布尔索引 df[df["amount"] > 100] # query 写法,数据量大时更快,可读性也更好 df.query("amount > 100 and city == '北京'") # eval 做列间运算,避免中间临时数组 df.eval("profit = amount - cost", inplace=True)

query底层用 numexpr 引擎,大表上比普通布尔索引快,而且不用反复写df["col"],表达式更干净。eval适合多列参与的算术运算,它不生成中间临时数组,内存峰值更低。列名含空格或特殊字符时,用反引号包起来,比如df.query("`订单金额` > 100")。

6.4 一个完整的提速对比

import time # 优化前 t0 = time.time() df_raw = pd.read_csv("sales_2024.csv") r1 = df_raw.groupby("city")["amount"].sum() print("优化前:", time.time() - t0, "秒") # 优化后 t0 = time.time() df_opt = pd.read_csv( "sales_2024.csv", usecols=["city", "amount"], dtype={"city": "category", "amount": "float32"}, ) r2 = df_opt.groupby("city", observed=True)["amount"].sum() print("优化后:", time.time() - t0, "秒")

两段代码结果一样,但第二段只读两列、类型压到最小、groupby加了observed=True。observed=True在 category 分组时只保留实际出现的类别,避免生成大量空组合拖慢速度。实际项目里这套组合拳通常能把读取加聚合的时间压到原来的三分之一甚至更少,具体倍数取决于原始数据的列数和类型。

我自己的习惯是:任何要跑第二遍的分析脚本,第一遍跑完就顺手把 dtype 优化和usecols加上,别等到数据涨到跑不动了再回头改。清洗逻辑写成函数、参数抽成变量,下次换一份数据只改路径和列名就能复用。这套东西不花哨,但能让你在数据量翻倍的时候不至于推倒重来。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询