ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Pandas实战指南:从数据集加载到清洗与特征工程

Pandas实战指南:从数据集加载到清洗与特征工程 1. 数据集选型与获取别一上来就死磕FileNotFoundError网上聊 Pandas 的教程一抓一大把但绝大多数都停留在df.head()和df.info()这个层面。这就导致一个很有意思的现象很多人学会了用 Pandas 处理titanic.csv、iris.csv但一旦要处理自己业务里的真实数据顿时手足无措。我的观点很明确——第三篇不只是继续讲 API而是要把 Pandas 拉到真实的数据集场景里逼着你去处理脏数据、异构数据和大体量数据。这篇内容我准备了很久重点不是罗列函数而是把我在实际项目里折腾数据集、踩坑、重构、优化性能的过程拆给你们看。全文会以多个真实场景数据集为例从加载、清洗、特征工程、可视化到问题排查完整跑一遍流程。如果你是那种已经把merge、groupby背得滚瓜烂熟但面对一堆原始 CSV 还是会发怵的人这篇内容就是给你准备的。1.1 公共数据集的“金矿”和“雷区”先聊数据集本身。很多新手第一反应是去 Kaggle 或者 GitHub 上找现成的。但根据我这几年带项目的经验公共数据集的水很深。比如像 CCPD中国停车场车牌数据集、BDD100K伯克利驾驶数据集这种偏视觉任务的数据集动辄几十个 GB如果你只是想练 Pandas把它完整下载下来完全没必要而且容易把你本地磁盘直接塞满。更常见的坑是格式问题。很多数据集打包下发时是 JSON、XML、Parquet 或者其他乱七八糟的格式比如 ReID 数据集、HRSC2016 这种遥感数据集标注文件往往是 XML 或 TXT 格式。你想用 Pandas 分析第一步就得头疼怎么把这些格式读进来。我建议大家按“场景 格式 体量”三要素来选CSV 类适合练习read_csv、merge、groupby体量适中处理起来不伤内存。JSON 类适合练习json.loadnormalize科技类数据集的标注文件常用结构嵌套很典型。数据库导出类适合练习read_sql像电商订单、白酒销售数据、中药材流向数据这类业务数据基本都是从库里导出来的字段多且乱最能磨炼清洗能力。1.2 那些高频被搜索的 CV 数据集跟 Pandas 到底什么关系我注意到大家搜索热度很高的几个词——CCPD、HRSC2016、PHM2012、ImageNet、CrowdHuman。这里我必须泼一盆冷水这些数据集主要是给深度学习模型训练用的不是给 Pandas 练习用的。但它们在预处理阶段Pandas 可是绝对主力。拿 PHM2012 数据集举例它是 PHM Society 举办的故障预测挑战赛提供的高频振动数据专门用来做轴承剩余寿命预测的。它每一条记录都包含时间戳、转速、负载、振动加速度等多个维度的信号。这种数据拿到手后你不可能直接丢给机器学习算法必须用 Pandas 把原始的有用信号抽取出来做特征工程均值、峰值、均方根、峭度然后再切窗滑窗。这一整套流程下来你对 Pandas 的掌握才叫真正的实战级。再说 BDD100K它的标注文件是 JSON 格式里面包着图片路径、类别、边界框坐标、遮挡情况、截断情况。如果你要从里面统计“晚上有多少辆卡车被遮挡”就得先把 JSON 里的嵌套结构拍平转成DataFrame再用groupby进行多条件计数。这种嵌套 JSON 的解析恰恰是很多 Pandas 教程里讲得最少、但实际工作中遇到最多的。1.3 从零组装一个“够用”的本地数据集没有现成数据集的时候我建议自己组一个。别觉得这也麻烦其实比去网上乱下载更靠谱。比如你完全可以用pandas自带的数据模拟工具生成一份“白酒销售明细表”字段包括日期、地区、品牌、销售额、成本、销量。也可以生成“中药材进货批次表”字段包含药材名、产地、进货价、库存量、有效期。我经常用numpy.random和pandas.DataFrame直接造数。这种方法的好处是能精准控制数据的脏程度——比如让 5% 的日期字段为空让 2% 的销售额字段出现文本类型让部分地区字段故意写错大小写。然后用一个项目把这些数据串起来效果远好过去外面下载那份已经被无数人手把手清洗过一遍的完美数据集。提示练习数据集的底线是不能太大。你本地电脑如果只有 8G 内存硬去加载一个 20GB 的公开数据集第一反应不是学到知识而是风扇狂转、电脑假死。先从小体量开始性能优化那部分我们放到后面详细讲。2. 数据加载与初始探索read_csv 只是起点读文件这件事几乎所有人都觉得简单不就是df pd.read_csv(xxx.csv)嘛。但真正要面对业务数据时这一步恰恰是翻车频率最高的环节。2.1 读取阶段就该处理好的“脏数据”问题我的经验是绝对不要把清洗工作全部丢给后面。你如果在读取阶段就把大部分脏数据解决掉后面能少掉很多头发。先说最常见的编码问题。很多业务系统导出的 CSV 是 GBK 编码的尤其是一些 ERP 老系统。如果你直接用pd.read_csv()去读第一行就可能报UnicodeDecodeError。这时候别急着给文件转码直接用参数解决import pandas as pd df pd.read_csv( 白酒销售数据.csv, encodinggbk, enginepython )注意enginepython这个参数。某些文件读取报CParserError时换个引擎往往就好了。缺失值的处理也一样。业务系统里“空”的表达方式五花八门有NULL、NA、N/A、None甚至还有-1和未知。你需要在读取时告诉 Pandas 哪些值需要当作缺失值处理na_values [NULL, NA, N/A, None, null, , -1, 未知] df pd.read_csv( 白酒销售数据.csv, encodinggbk, na_valuesna_values, keep_default_naTrue )很多时候还有一个低调但致命的问题——分隔符。CSV 文件表面是逗号分隔但实际字段内容里可能包含逗号比如“北京市, 朝阳区”。字段里一出现逗号整个文件的列数就对不上了Pandas 会报ParserError。处理方式也很简单看文件里的分隔符到底是不是逗号df pd.read_csv( 数据文件.csv, sep,, # 根据实际文件调整 quotechar, # 这个参数一定要存在 skipinitialspaceTrue )2.2 数据类型转换从文本到时间再到分类读进来之后很多人立刻df.info()一看哇全是object类型。恭喜你这才是业务数据的正常打开方式。接下来要做的就是从object里把真实类型给榨出来。首先是时间列。Pandas 里的to_datetime是出镜率极高的函数但它也不是附身符。我见过不少新人把日期列传给to_datetime后报错原因是文件里混着2024-03-15和03/15/2024两种格式。to_datetime默认会尝试多种格式但如果还是失败你可以直接指定格式解析速度和稳定性都更好df[日期] pd.to_datetime( df[日期], format%Y-%m-%d, errorscoerce # 解析不了就转为 NaT )其次是category类型。这个方法很多人容易忽略但它是我在大数据处理中特别推荐的类型。比如“地区”这一列明明只有“华北”“华东”“华南”“西南”四个取值但它在内存里是以字符串形式存储的耗内存且运算慢。转成category后Pandas 内部会换成整数编码查内存能降一个量级分组运算也快很多df[地区] df[地区].astype(category)然后是数值类型。销售金额、成本、库存这些字段经常会被 Python 读成object原因往往是人眼看到的“”或者千分位逗号。清洗这类字段的时候我习惯写一个通用函数# 去掉货币符号和千分位再转数值 def clean_number(s): if isinstance(s, str): s s.replace(, ).replace(,, ).strip() try: return float(s) except ValueError: return pd.NA return s df[销售额] df[销售额].apply(clean_number) df[销售额] pd.to_numeric(df[销售额], errorscoerce)2.3 初探结构与统计量info、describe、shape类型转换完成后一定要重新对info()、describe()、shape这三个方法建立敬畏心。它们是判断一份数据能不能继续分析的三板斧。df.info()能告诉你数据总量、列数、每一列的非空数量、内存占用和数据类型。我会先看内存占用如果内存占用异常高说明类型转换没做到位。再看非空数量哪一列的非空数量明显低于其他列哪一列就要重点排查。df.describe()默认只统计数值列但如果你传入includeobject还能看到类别列的频次、唯一值数量。这一步可以快速发现极端异常值。比如“白酒销售数据”里有一个销售额是99999999一定是测试数据或者录入错误必须清洗掉。df.shape看起来简单但它和df.info()配合起来能让你立刻发现问题。比如shape显示 10 万行info()显示日期列只有 5 万非空那么这一列缺失率高达 50%基本可以判断这一列要么是后来挂接的要么就存在严重的采集缺失。3. 数据清洗与特征工程核心战力全在 Pandas 的组合拳3.1 缺失值与重复值一套组合拳带走先说缺失值。处理缺失值的核心逻辑不是“删掉”或“填上”而是“你想让模型或报表怎么解释这个空”。比如销售数据里的“折扣率”如果为空可能代表没有折扣但如果“销量”为空那就是记录缺失性质完全不同。我比较常用的处理顺序是# 第一步按列查缺失数量和比例 missing df.isna().sum() missing_percent missing / len(df) # 第二步根据业务含义决定是填充还是删除 # 对于“折扣率”空值直接填0 df[折扣率] df[折扣率].fillna(0) # 对于“城市”用“未知”填充避免影响后续 groupby df[城市] df[城市].fillna(未知) # 对于“成本”这种关键指标太多缺失就整列删除 if missing_percent[成本] 0.3: df.drop(columns[成本], inplaceTrue)重复值这边很多人以为drop_duplicates()按整行判断就完事了但业务数据往往不会给你那么干净的重复。更多的情况是“这行跟那行其他字段都一样但时间字段差了几秒”或者“同一订单编号出现两次但状态字段不同”。这时候你需要想清楚你的去重逻辑# 按订单维度去重保留最新状态 df_sorted df.sort_values(下单时间, ascendingFalse) df_deduplicated df_sorted.drop_duplicates( subset[订单编号], keepfirst )这里只说一句去重不是目的保留正确的上下文才是目的。别让drop_duplicates把你的业务语义给“去”歪了。3.2 用 Pandas 做特征抽取从“日期”到“小长假”的实战处理时间序列时光把日期转成时间类型是远远不够的得把隐藏的信息拆出来。比如分析白酒销售和节假日的关系你会用到# 提取年、月、日、星期、是否节假日 df[年份] df[日期].dt.year df[月份] df[日期].dt.month df[日] df[日期].dt.day df[星期] df[日期].dt.dayofweek # 0周一 # 定义一个简单的节假日函数 def is_holiday(dt): # 这里按实际情况维护节假日列表一般用调休安排 holidays [2024-02-10, 2024-02-11, 2024-02-12] return 1 if dt.strftime(%Y-%m-%d) in holidays else 0 df[是否节假日] df[日期].apply(is_holiday)这种特征抽取的核心逻辑是让模型或者报表工具能够看到周期性规律。日期如果没有拆开在机器学习里只是“一串无意义的字符串”拆成年月日星期是否节假日之后它就是一组有效信息了。分层聚合也是 Pandas 的强项。比如你想分析“月维度下不同品牌在不同地区的销售额占比”我的惯用手法pivot pd.pivot_table( df, index月份, columns[品牌, 地区], values销售额, aggfuncsum, fill_value0 )透视表很多时候比groupby输出更直观因为它是宽表结构直接丢给 Excel 或者 Matplotlib 都很好用。但要注意fill_value0只是为了让表好看它不代表业务上真的有“销售额为 0”如果后续要做比例计算这个 0 可能会给你埋雷。3.3 分组、聚合与透视表站在 Excel 的肩膀上如果你用过 Excel 的数据透视表那 Pandas 的pivot_table和groupby你会很熟悉。它们的最大优势是处理 10 万行以上的数据时Excel 已经卡死了而 Pandas 还能稳稳地站住。我先讲groupby的细节# 计算每个地区、每个品牌的平均折扣率、总销售额、总销量 summary df.groupby([地区, 品牌]).agg( 总销售额(销售额, sum), 平均折扣率(折扣率, mean), 总销量(销量, sum), 订单数(订单编号, nunique) ).reset_index()用reset_index()很关键。groupby默认会把分组键变成索引不重置的话后面merge会出现很奇怪的关联错误。还有一点聚合函数nunique算去重计数非常适合统计订单数比count要准确得多。再讲transform和apply的区别。这是一个特别容易让人懵的点。简单来说agg是“多对一”结果的行数减少transform是“多对多”结果的行数跟原表一致。比如你想标记每个订单是否高于同地区平均销售额region_mean df.groupby(地区)[销售额].transform(mean) df[是否高于地区均值] (df[销售额] region_mean).astype(int)这个操作如果用agg再合并会多写四五行代码。transform一行搞定而且不用考虑索引对齐的问题。4. 数据可视化疑难点排查与代码实录4.1 当 Pandas 遇到 Matplotlib绘图风格统一有人说“Pandas 画图画不漂亮”这话对但不全对。Pandas 内置的绘图函数其实跟 Matplotlib 是同源的能不能画好看关键看你的全局配置和数据处理。我一般会做一套固定的初始化配置import matplotlib.pyplot as plt import matplotlib as mpl # 解决中文乱码问题 mpl.rcParams[font.sans-serif] [SimHei, Microsoft YaHei, PingFang SC] mpl.rcParams[axes.unicode_minus] False # 统一画布风格 plt.rcParams[figure.figsize] (12, 6) plt.rcParams[figure.dpi] 150 plt.rcParams[axes.grid] True plt.rcParams[grid.linestyle] -- plt.rcParams[grid.alpha] 0.6这步配置做完你就不会在画图时看到满屏的方格子和豆腐块了。再把数据 groupby 之后一行代码画图体验是极好的df.groupby(月份)[销售额].sum().plot(kindline, markero) plt.title(白酒销售额月度趋势) plt.xlabel(月份) plt.ylabel(销售额元) plt.tight_layout() plt.show()有些情况下你需要在同一张图上叠加多根折线直接对DataFrame画就行pivot_df df.groupby([月份, 地区])[销售额].sum().unstack() pivot_df.plot(kindline, markero) plt.title(各地区月度销售趋势对比)unstack()会把“地区”从索引变成列这样plot就会自动画出多根线图例也直接出来了。这里我踩过的最大的坑是unstack()之后索引可能变成两层的 MultiIndex必须确保pivot_df的索引是唯一的日期/月份否则会出现线条交叉乱象。4.2 可视化故障排查实录中文乱码、画布重叠、数据丢失中文乱码这个问题真是老生常谈。你在rcParams里设置了SimHei但某些 Linux 服务器上根本没有这个字体画出来依然是豆腐块。治本的办法是检查服务器情况我一般会先跑那句from matplotlib.font_manager import FontManager fm FontManager() available_fonts {f.name for f in fm.ttflist} print(available_fonts)如果系统里完全没有中文字体最直接的解决方案是本地导入字体文件import matplotlib.font_manager as fm font_path /usr/share/fonts/chinese/simhei.ttf prop fm.FontProperties(fnamefont_path) plt.rcParams[font.family] prop.get_name() plt.rcParams[font.sans-serif] [prop.get_name()]画布重叠问题也经常有。当你在一张图上画了太多信息坐标轴刻度标签又很长最后全挤在一起变成一堆黑点。解决办法不是缩小字体而是给坐标轴旋转角度plt.xticks(rotation45, haright) plt.tight_layout()tight_layout()是神器。大多数画布错位、标签被裁剪的问题都是因为没用它。数据丢失这个更隐蔽。你明明画出来“销售额”折线但图上突然少了一截大概率是原始数据里有缺失值或者np.inf。Pandas 在绘图时会把缺失值当断点看起来就像数据“断层”。解决办法是在画图前统一处理plot_data df.groupby(月份)[销售额].sum() plot_data plot_data.replace([np.inf, -np.inf], np.nan).dropna() plot_data.plot(kindline)5. 结合热门数据集场景的实战演练5.1 制造业 PHM2012 数据集轴承剩余寿命预测的 Pandas 预处理PHM2012 数据集在制造业故障预测领域是很经典的基准数据。它的原始数据是一堆 CSV 文件一个文件代表一个轴承从健康到失效的全生命周期振动信号。文件里每一列是一条传感器通道不同工况下采样频率不一样有的是 25.6kHz有的是 51.2kHz。这个数据集的第一个坑就是文件体积。一个轴承的振动信号数据就有几十万行连续加载好几个文件内存直接告急。我的处理方案是“读取时削减精度 分块处理”dtype_dict { hour: int16, min: int16, sec: int16, acc1: float32, acc2: float32, acc3: float32, acc4: float32 } # 用 float32 代替 float64 能节省一半内存 # 而且振动信号本身精度很高在 0.001 级别float32 足够了 df pd.read_csv( learn_data/Bearing1_1_acc.csv, dtypedtype_dict )然后我会计算特征。振动信号数据直接做机器学习不太现实一般会切片计算统计特征。用 Pandas 的rolling方法可以很方便地做窗口特征# 在时间轴上按固定窗口计算均值、峰值、均方根、峭度 df[time] pd.to_datetime(df[[hour, min, sec]], format%H:%M:%S) df df.set_index(time) roll df[[acc1, acc2, acc3, acc4]].rolling(5s).agg( [mean, max, min, std] )这里rolling(5s)按时间窗口滑动是rolling(5000)这种行数窗口的上位替代。因为轴承运行的转速是变化的按行数滑动会让你混淆“同一段时间窗口内”的特征按时间滑动则能统一口径。这也是我在实际项目里踩过坑之后学乖的地方。5.2 车辆检测 BDD100K 数据集JSON 标注转 Pandas 结构化数据BDD100K 是伯克利发布的自动驾驶场景数据集标注文件很大如果你要分析其中各个类别的分布、边界框尺寸、夜晚/白天分布等情况JSON 解析是必过的一关。JSON 转 DataFrame最核心的 API 是json_normalize但前提你要理解嵌套结构。BDD 的标注大概长这样{ name: 0a0f3a.json, timestamp: 1561507200000, labels: [ {category: car, box2d: {x1: 100, y1: 120, x2: 200, y2: 300}}, {category: pedestrian, box2d: {x1: 50, y1: 60, x2: 100, y2: 180}} ] }如果直接用pd.json_normalize(data, record_pathlabels)你会发现“name”和“timestamp”在每条标注里都重复但“box2d”里的 x1、x2 会被拍平到顶层。真正的问题是缺失字段某些标注可能没有边界框或没有类别。这时我习惯加参数import json import pandas as pd with open(bdd100k_labels.json, r) as f: data json.load(f) # record_path 指定嵌套路径meta 指定要保留的外层字段 df pd.json_normalize( data, record_pathlabels, meta[name, timestamp], errorsignore )然后对这个df做清洗df[x1] pd.to_numeric(df[box2d.x1], errorscoerce) df[y1] pd.to_numeric(df[box2d.y1], errorscoerce) df[x2] pd.to_numeric(df[box2d.x2], errorscoerce) df[y2] pd.to_numeric(df[box2d.y2], errorscoerce) df[box_width] df[x2] - df[x1] df[box_height] df[y2] - df[y1] df[box_area] df[box_width] * df[box_height] # 统计白天夜间、不同天气下的目标数量分布 df[timestamp] pd.to_datetime(df[timestamp], unitms) df[hour] df[timestamp].dt.hour count_dist df.groupby([category, hour]).size().reset_index(namecount)这种 JSON 转表、清洗、聚合、出图的链路是很多自动驾驶数据集的通用玩法。不要怕嵌套深关键是理清结构层级。6. 常见问题与排查技巧实录6.1 内存爆炸大数据集分块读取与数据类型优化有一个真实的例子我之前处理一个网约车出行订单数据文件 3 个 GB直接read_csv死机。解决方案很简单分块读取chunk_iter pd.read_csv( order_data.csv, chunksize100000, dtype{uid: int32, price: float32} ) results [] for chunk in chunk_iter: # 对每一个 chunk 做基本的清洗和聚合 res chunk.groupby(date)[price].sum() results.append(res) # 拼接所有结果 final pd.concat(results).groupby(level0).sum()分块读取的核心思路是“分批处理、最后汇总”。注意不能直接在循环里把一个 3GB 文件逐行append到一个大DataFrame那样内存照样爆。要在循环里先聚合把结果变小再合并。数据类型优化也极其重要。举个例子int64比int32多占一半内存如果你的 id 字段最大值不超过 21 亿改成int32性价比极高。同理数值列能float32就别float64能category就别object。6.2 时间特征与索引踩坑时间索引是 Pandas 里最容易出鬼的地方。最常见的问题有两个时区不一致、时间精度不一致。时区问题经常出现在业务系统和日志数据合并时。一个库用的是UTC8另一个库用的是UTC合并起来后你会发现同一天的数据少了几小时或多了几小时。解决办法是统一转成带时区的时间df[时间] pd.to_datetime(df[时间]) df[时间] df[时间].dt.tz_localize(Asia/Shanghai) df[时间] df[时间].dt.tz_convert(UTC) # 如果不需要时区可以再转回无时区格式 df[时间] df[时间].dt.tz_localize(None)时间精度问题更常见。比如订单时间精确到秒付款时间精确到毫秒两个列merge时怎么都对不上。处理思路是统一精度比如都截断到秒df[付款时间] df[付款时间].dt.floor(s)6.3 Pandas 性能加速向量化与 apply 的取舍很多人一上来就写df.apply(lambda row: ...)但我必须说对性能要求高的场景apply是性能杀手。比如对 100 万行数据做条件判断如果写成apply可能跑好几秒但用np.where或者布尔索引几毫秒就完事。我举一个白酒销售数据案例。要根据“销售额”和“成本”计算“利润率”最简单的写法是向量化df[利润率] (df[销售额] - df[成本]) / df[销售额]这比df.apply(lambda r: (r[销售额]-r[成本])/r[销售额], axis1)快很多。但如果逻辑确实复杂到没法用向量化表达再考虑apply或者用pd.cut分桶。还有pd.merge的性能问题在大表关联时经常被忽略。一定要检查连接键是否重复。如果连接键有大量重复会产生笛卡尔积内存瞬间爆炸。我处理过一份表格合并前明明只有 20 万行合并后直接变出 800 万行原因就是连接键里“地区”字段大量重复。# 合并前先检查连接键的唯一性 df_left[地区].nunique() df_left[地区].shape[0]如果两者差异巨大就要停下来想想是不是应该先聚合再合并。最后分享一个从小白到进阶的习惯文章写到这里我突然想起一个很有意思的现象。很多初学者学 Pandas 时特别喜欢搜“秘籍”“速查表”但真正能让他们进步的反而是拿一个自己不能理解的数据集死磕它。一开始你可能要花两小时去把 JSON 结构理清再用半小时清洗掉脏数据最后只得出一个极其简单的统计结果。但这个过程才是 Pandas 能力真正内化的过程。根据我个人经验以后你看到任何一张表、任何一份数据文件脑子里会自动跳出df.info()的轮廓多少行哪些列缺多少什么类型。到那时候你就不需要再“学” Pandas 了你已经是在“用” Pandas 解决问题。如果这篇内容对你有帮助建议你立刻找个之前觉得难啃的数据集照着这个思路再过一遍比收藏多少篇教程都管用。
返回列表