行业资讯
📅 2026/8/4 12:38:37
Python Pandas数据处理实战:从ETL到特征工程完整指南
1. 从“脏数据”到“干净数据”为什么数据处理是分析的生命线刚接触数据分析的朋友拿到一份数据后往往最兴奋的就是直接上模型、画图表恨不得立刻得出惊天结论。我刚开始也是这么干的结果被现实狠狠教育了几次。比如一份销售数据里同一个产品名称有“iPhone 13”、“iphone13”、“苹果13”三种写法直接做统计销量被分成了三份又或者用户年龄列里混进了“-1”、“999”这样的异常值求平均年龄直接拉高到不切实际的数字。这些经历让我明白数据分析的结论质量90%取决于数据处理的质量。没有干净、规整的数据再高级的算法也只是在垃圾堆里找金子得出的结论轻则不准重则误导决策。“数据处理”听起来有点枯燥但它恰恰是数据分析中最能体现“手艺”的环节。它不像建模那样有炫酷的数学公式更像是一个数据工匠拿着各种工具对原始数据进行清洗、整理、转换最终打磨成适合分析的“标准件”。这个过程我们称之为ETLExtract, Transform, Load中的Transform转换核心阶段。今天我们就用 Python 的 Pandas 库作为主要工具深入聊聊数据处理那些你必须掌握的“硬功夫”。我会结合我踩过的坑和总结的经验手把手带你走完从数据导入到初步规整的全过程让你不仅知道怎么操作更明白为什么要这么操作。2. 数据处理的“第一印象”加载与初步审视在动手清洗之前我们必须先“认识”我们的数据。盲目操作是数据处理的大忌。2.1 选择合适的“数据搬运工”Pandas 读取函数详解Pandas 提供了丰富的读取函数选对工具事半功倍。import pandas as pd # 1. 读取 CSV 文件最常用 # 关键参数解析 # - encoding: 中文数据常遇到编码问题utf-8不行可尝试 gbk 或 gb2312 # - header: 指定哪一行作为列名表头默认为0第一行。如果数据没有表头设为 None # - sep: 分隔符CSV默认是逗号但有时可能是制表符\t或空格 df_csv pd.read_csv(sales_data.csv, encodingutf-8, sep,) # 2. 读取 Excel 文件业务数据常见 # 关键参数 # - sheet_name: 可以传工作表名字符串或索引整数传 None 则读取所有工作表返回一个字典 # - usecols: 只读取指定列例如 usecolsA:C, E 或 usecols[0, 1, 2, 4]对于列很多的大文件能显著提升加载速度 df_excel pd.read_excel(financial_report.xlsx, sheet_nameQ1, usecolsA:F) # 3. 读取 JSON 文件API接口、网络数据常见 # orient参数很重要它指定了JSON的结构。常见的有 # - records: 列表形式每个元素是一条记录字典。这是最常用的格式。 # - split: 包含index, columns, data三个键。 # - table: 遵循JSON Table Schema格式。 df_json pd.read_json(api_response.json, orientrecords)注意读取大文件几百MB以上时直接pd.read_csv可能会内存溢出。这时候可以考虑两个策略1. 使用chunksize参数分块读取迭代处理2. 在读取时通过usecols和dtype参数指定列的数据类型减少内存占用。例如dtype{user_id: int32, price: float32}。2.2 给你的数据“拍个X光”核心查看方法与信息提取数据加载进来后不要急着改先花几分钟全面了解它。# 1. 查看数据形状多少行多少列 print(f数据集形状: {df_csv.shape}) # 输出 (行数, 列数) # 2. 预览数据看头部和尾部 print(df_csv.head(10)) # 默认看前5行这里指定看10行 print(df_csv.tail()) # 查看最后5行有助于发现数据记录末尾的格式问题 # 3. 获取数据集的“体检报告” df_info df_csv.info() # 这个函数会打印 # - 列名Column # - 非空值数量Non-Null Count # - 数据类型Dtype # 它是发现缺失值和类型错误的第一道关卡。如果某列非空数量远小于总行数说明缺失严重。 # 4. 查看数值型列的统计摘要 print(df_csv.describe()) # 输出计数(count)、均值(mean)、标准差(std)、最小值(min)、四分位数(25%, 50%, 75%)、最大值(max) # 这个函数能快速发现异常值。例如年龄age列的最小值是-1最大值是200这显然不合理。 # 5. 查看所有列名和数据类型 print(df_csv.columns.tolist()) # 列名列表 print(df_csv.dtypes) # 每列的数据类型实操心得df.info()和df.describe()是我每次拿到新数据必做的两个动作。前者告诉我数据的“骨架”结构和完整性后者告诉我数据的“血肉”数值分布。曾经有一次我忽略了describe()中“客户消费金额”的标准差极大这个信号直接建模结果模型完全被几个极端富豪客户的订单带偏了。所以初步审视阶段发现的任何疑点都要记下来留到清洗阶段重点处理。3. 数据清洗的“外科手术”处理缺失、异常与重复这是数据处理最核心、最繁琐的一步。我们的目标是在尽量不损失有价值信息的前提下让数据变得完整、准确、唯一。3.1 面对“空白格”缺失值处理的策略与抉择数据中的NaN、None或空字符串就是缺失值。处理它们没有银弹只有适合场景的策略。# 1. 检测缺失值 missing_sum df.isnull().sum() # 每列缺失值总数 missing_percent (df.isnull().sum() / len(df)) * 100 # 每列缺失值百分比 print(missing_percent.sort_values(ascendingFalse)) # 按缺失比例降序排列优先处理缺失严重的列 # 2. 删除缺失值简单粗暴慎用 # - 删除任何包含缺失值的行可能损失大量数据 df_dropped_rows df.dropna(axis0) # - 删除任何包含缺失值的列如果该列缺失太严重或无关紧要 df_dropped_cols df.dropna(axis1) # - 只删除在特定列上缺失的行 df_dropped_specific df.dropna(subset[重要列1, 重要列2]) # 3. 填充缺失值更常用的方法 # - 用固定值填充适用于分类数据或编码类数据 df_filled_constant df.fillna({性别: 未知, 省份: 其他}) # - 用统计量填充适用于数值型数据 # 用均值填充对异常值敏感 df[年龄].fillna(df[年龄].mean(), inplaceTrue) # 用中位数填充更稳健不受极端值影响 - **推荐** df[薪资].fillna(df[薪资].median(), inplaceTrue) # 用众数填充适用于分类数据 df[产品类别].fillna(df[产品类别].mode()[0], inplaceTrue) # - 向前填充ffill或向后填充bfill适用于时间序列数据用前一个或后一个有效值填充 df[股价].fillna(methodffill, inplaceTrue) # 4. 高级技巧基于模型预测填充如KNN # 当缺失不是完全随机且与其他列高度相关时可以考虑。 from sklearn.impute import KNNImputer imputer KNNImputer(n_neighbors5) df_filled_knn pd.DataFrame(imputer.fit_transform(df[[身高, 体重, 年龄]]), columns[身高, 体重, 年龄])为什么这样选择删除法只适用于缺失比例极小如5%且缺失完全随机的情况。填充法中中位数优于均值因为它不受极端值干扰。对于“收入”这种右偏分布的数据均值远大于中位数用均值填充会系统性高估缺失者的收入。时间序列用前向填充是符合业务逻辑的昨天的股价和今天最接近。模型填充虽好但复杂度高且可能引入过拟合一般只在数据科学竞赛或深度挖掘中使用。3.2 揪出“害群之马”异常值的检测与处理异常值不一定是错误但会严重扭曲分析结果如求平均、回归分析。# 1. 基于标准差σ检测适用于近似正态分布的数据 # 通常认为与均值距离超过3个标准差的值可能是异常值。 mean df[销售额].mean() std df[销售额].std() lower_bound mean - 3 * std upper_bound mean 3 * std outliers_std df[(df[销售额] lower_bound) | (df[销售额] upper_bound)] # 2. 基于四分位距IQR检测更稳健推荐 # IQR Q3 (75%分位数) - Q1 (25%分位数) # 通常定义小于 Q1 - 1.5*IQR 或 大于 Q3 1.5*IQR 的值为异常值。 Q1 df[订单金额].quantile(0.25) Q3 df[订单金额].quantile(0.75) IQR Q3 - Q1 lower_bound_iqr Q1 - 1.5 * IQR upper_bound_iqr Q3 1.5 * IQR outliers_iqr df[(df[订单金额] lower_bound_iqr) | (df[订单金额] upper_bound_iqr)] # 3. 处理异常值 # - 方法A删除当异常值明确为错误录入时 df_clean df[(df[订单金额] lower_bound_iqr) (df[订单金额] upper_bound_iqr)] # - 方法B盖帽法Capping将超出边界的值替换为边界值 df[订单金额_capped] df[订单金额].clip(lowerlower_bound_iqr, upperupper_bound_iqr) # - 方法C视为缺失值然后用处理缺失值的方法处理如用中位数填充 # - **最重要的一步业务判断** # 例如在电商数据中一个金额巨大的订单可能是企业采购不是异常而是高价值客户。 # 需要结合业务知识判断是否剔除。实操心得我强烈推荐使用IQR 方法检测异常值因为它不依赖于数据服从正态分布的假设对极端值不敏感更稳健。处理异常值时千万不要自动化一刀切。务必把检测出来的异常值列表拿出来结合业务背景人工复核。曾经有次分析用户活跃度IQR 筛出一批“异常活跃”的用户差点被当成爬虫数据删除后来发现那是公司内部测试账号幸亏做了复核。3.3 消灭“双胞胎”重复数据的识别与去重完全重复的行不仅浪费存储还会在统计计数时导致结果虚高。# 1. 检测重复行基于所有列 duplicate_rows df[df.duplicated(keepFalse)] # keepFalse 标记所有重复项 print(f完全重复的行数: {df.duplicated().sum()}) # 2. 基于关键列检测重复更常见 # 例如在订单数据中“订单ID”应该是唯一的。 duplicate_order_id df[df.duplicated(subset[订单ID], keepFalse)] # 如果订单ID重复那很可能数据采集或导入环节出了问题。 # 3. 删除重复值 # - keepfirst (默认): 保留第一次出现的副本删除后续的。 # - keeplast: 保留最后一次出现的副本。 # - keepFalse: 删除所有重复的行如果一行数据出现两次这两行都会被删。 df_dedup df.drop_duplicates(subset[用户ID, 登录日期], keepfirst) # 上面代码表示对于同一个用户在同一天的登录记录只保留第一条。 # 4. 处理“业务逻辑”重复 # 有时数据格式不一致导致看起来不重复。比如同一家公司“Co.”和“Company”写法不同。 # 这需要在数据清洗的“标准化”步骤解决去重前先统一格式。注意事项drop_duplicates()默认判断所有列完全相同。在实际业务中我们更关心业务主键是否重复如订单ID、用户ID时间戳。在去重前一定要想清楚根据业务逻辑到底以哪些列为准来判断重复保留哪一条记录例如保留时间最新的还是金额最大的4. 数据转换与规整为分析铺平道路清洗干净后数据可能还不适合直接分析。我们需要将其转换成更规整、更利于计算的格式。4.1 数据类型的正确转换错误的数据类型会导致计算错误或性能下降。# 查看当前数据类型 print(df.dtypes) # 1. 转换为数值类型字符串数字 - 整数/浮点数 # errorscoerce 是关键参数它会把无法转换的值如“N/A”、“-”变成NaN而不是报错中断程序。 df[价格] pd.to_numeric(df[价格], errorscoerce) # 2. 转换为日期时间类型 df[下单时间] pd.to_datetime(df[下单时间], format%Y-%m-%d %H:%M:%S, errorscoerce) # format参数可以加速转换并避免歧义如01/02/03是月/日/年还是日/月/年。 # 转换后就可以方便地提取年、月、日、小时等特征了。 df[下单年份] df[下单时间].dt.year df[下单月份] df[下单时间].dt.month df[是否周末] df[下单时间].dt.dayofweek 5 # 5和6代表周六日 # 3. 转换为分类类型Category # 对于有限取值的字符串列如性别、省份、产品类型转换为category类型可以极大节省内存提高分组聚合速度。 df[产品类别] df[产品类别].astype(category)为什么这么做将日期字符串转换为datetime类型是时间序列分析的基础否则你无法进行时间偏移、重采样等操作。将低基数唯一值少的字符串列转为category类型在数据量较大时内存占用可能减少到原来的十分之一groupby操作也会快很多。4.2 字符串数据的清洗与标准化文本数据是混乱的重灾区需要耐心处理。# 假设有一列 city数据如下“ Beijing ”“shanghai”“SHENZHEN”“广州” # 1. 去除首尾空白字符 df[city] df[city].str.strip() # 2. 统一大小写 df[city] df[city].str.lower() # 全部小写 # 或者 df[city] df[city].str.title() # 每个单词首字母大写 # 3. 替换特定字符或词语 df[city] df[city].str.replace(shenzhen, 深圳) # 英文名转中文名 # 4. 处理不规范的缩写或全称使用映射字典 city_mapping { bj: 北京, sh: 上海, sz: 深圳, gz: 广州 } df[city] df[city].map(city_mapping).fillna(df[city]) # map不到的使用原值 # 5. 字符串分割与提取 # 例如从“姓名”列“张三_销售部”中提取名字和部门 df[[姓名, 部门]] df[姓名].str.split(_, expandTrue) # 提取字符串中的数字 df[订单号] df[订单信息].str.extract(r(\d)) # 使用正则表达式提取连续数字经验技巧字符串清洗往往需要多轮迭代。先做通用的清理如去空格、统一大小写然后针对业务中已知的不规范写法建立映射字典进行替换。对于复杂情况可以写一个自定义的清洗函数结合apply()方法使用。正则表达式 (str.extract) 在处理非结构化文本时非常强大但需要一些学习成本。4.3 特征工程初探创建新特征很多时候原始数据字段不能直接用于分析我们需要从中衍生出更有意义的特征。# 1. 从现有特征中计算新特征 df[订单总价] df[商品单价] * df[购买数量] df[BMI] df[体重_kg] / (df[身高_m] ** 2) # 2. 分箱Binning将连续数据离散化 # 例如将用户年龄分为“青年”“中年”“老年” bins [0, 30, 50, 120] # 区间为 [0,30], (30,50], (50,120] labels [青年, 中年, 老年] df[年龄分段] pd.cut(df[年龄], binsbins, labelslabels, rightTrue) # rightTrue表示区间左开右闭 # 3. 创建虚拟变量哑变量One-Hot Encoding # 将分类变量如“城市”转换为机器学习模型可识别的0/1格式 df pd.get_dummies(df, columns[城市], prefixcity) # 执行后会新增列 city_北京 city_上海等某行数据是北京则city_北京1其他city列为0。 # 4. 基于时间序列的滞后特征 # 在时间序列预测中前几期的值可能是重要的特征 df[销售额_滞后1天] df[销售额].shift(1) # 将销售额向下移动一行表示前一天的销售额为什么需要特征工程原始数据是“原材料”特征工程就是“加工”目的是让数据更能揭示问题本质。例如“年龄”是连续值但业务上可能更关心“是否是青年用户”这时分箱就创造了新特征。“城市”是文本模型无法理解转换成哑变量后就成了有效的数值输入。好的特征工程其价值往往超过模型算法的选择。5. 数据合并与连接整合多源信息现实中的数据很少躺在一个完美的表里。我们经常需要把来自不同渠道、不同表格的数据拼在一起。5.1 纵向堆叠concat当多个数据集结构相同列名一致只是行数不同时使用pd.concat进行纵向拼接。# 假设df1, df2, df3是三个月的销售数据结构完全相同 df_list [df_jan, df_feb, df_mar] df_quarter pd.concat(df_list, axis0, ignore_indexTrue) # axis0 表示按行拼接纵向。 # ignore_indexTrue 表示忽略原来的索引重建新索引。如果为False则会保留原来的索引可能导致索引重复。5.2 横向连接merge (最核心、最常用)当需要根据一个或多个关键列将两个数据集的信息关联起来时使用pd.merge。这相当于数据库的 JOIN 操作。# 假设有两个表 # df_orders (订单表): order_id, user_id, product_id, amount # df_users (用户表): user_id, name, age, city # 1. 内连接 (inner join): 只保留两个表都有的键 df_inner pd.merge(df_orders, df_users, onuser_id, howinner) # 结果只包含那些在df_orders和df_users中都有user_id的记录。 # 2. 左连接 (left join): 以左表(df_orders)为基准 df_left pd.merge(df_orders, df_users, onuser_id, howleft) # 结果包含df_orders的所有行。如果某订单的user_id在df_users中找不到则用户信息列为NaN。 # **这是业务分析中最常用的连接方式**确保主业务表订单不丢失任何记录。 # 3. 右连接 (right join): 以右表(df_users)为基准 # 4. 外连接 (outer join): 保留两个表的所有记录缺失处填NaN # 5. 基于多个键连接 df_complex pd.merge(df_A, df_B, on[date, region], howleft) # 6. 处理列名冲突 # 如果两个表有同名的列但不是连接键merge会自动加后缀 _x, _y df_merged pd.merge(df_orders, df_products, left_onproduct_id, right_onid, howleft) # 可以使用 suffixes 参数自定义后缀连接逻辑的选择是业务决定的。思考“我是否需要保留那些没有匹配上的记录” 例如分析订单行为必须用左连接以订单表为主否则会丢失部分订单数据。分析用户画像则可能需要用右连接或以用户表为主的左连接。5.3 按索引对齐joinjoin方法是merge的简化版默认按索引进行连接。# 设置索引 df_orders_indexed df_orders.set_index(user_id) df_users_indexed df_users.set_index(user_id) # 按索引进行左连接 df_joined df_orders_indexed.join(df_users_indexed, howleft)mergevsjoin功能上merge更通用强大可以指定左右连接键。join在按索引连接时写起来更简洁。我个人习惯统一使用merge因为其参数更明确代码意图更清晰尤其是在团队协作中。6. 数据重塑改变数据的布局以适应分析需求有时数据存储的格式不适合特定的分析或可视化我们需要“重塑”它。6.1 透视表pivot_table透视表可以快速对数据进行多维度的汇总统计是探索性分析EDA的利器。# 假设有销售数据Date, Region, Product, Sales # 我们想看每个Region行每个Product列的总销售额值 pivot_sales pd.pivot_table(df, valuesSales, # 要聚合的数值列 indexRegion, # 行索引 columnsProduct, # 列索引 aggfuncsum, # 聚合函数默认为均值mean fill_value0, # 填充缺失值为0 marginsTrue, # 添加总计行/列 margins_name总计) print(pivot_sales) # 输出一个二维表格行是地区列是产品交叉点是该地区该产品的销售总和。为什么用透视表它用一行代码替代了复杂的多重groupby和unstack操作能直观地展示两个维度之间的关系。aggfunc参数非常灵活可以是sum,mean,count,min,max甚至自定义函数。6.2 融合与旋转meltmelt是pivot的逆操作它将“宽格式”数据列多变为“长格式”数据行多常用于将多列指标合并便于后续用seaborn等库绘制分组图表。# 假设有宽表数据列是各个月份的销售额 # df_wide: City, Jan_Sales, Feb_Sales, Mar_Sales df_long pd.melt(df_wide, id_vars[City], # 保持不变的标识列 value_vars[Jan_Sales, Feb_Sales, Mar_Sales], # 要融合的列 var_nameMonth, # 新列名用于存放原来的列名 value_nameSales) # 新列名用于存放原来的值 # 转换后数据变为City, Month, Sales # 这样就能方便地用 sns.lineplot(datadf_long, xMonth, ySales, hueCity) 画图了。长格式 vs 宽格式宽格式便于人类阅读像Excel表长格式便于机器处理和绘图。很多统计和绘图函数如seaborn,statsmodels都要求数据是长格式。melt是进行这种转换的标准方法。7. 一个完整的数据处理流程示例让我们用一个模拟的电商订单数据串联起上述所有步骤。import pandas as pd import numpy as np # 1. 加载数据 df pd.read_csv(dirty_orders.csv, encodinggbk) # 假设文件是GBK编码 print(初始形状:, df.shape) print(df.head()) # 2. 初步审视 print(df.info()) print(df.describe(includeall)) # includeall 也显示非数值列的统计 # 3. 清洗缺失值 # 假设‘客户ID’缺失无意义删除 df.dropna(subset[客户ID], inplaceTrue) # ‘折扣’列缺失较多用0填充假设无折扣即为0 df[折扣].fillna(0, inplaceTrue) # ‘城市’列少量缺失用‘未知’填充 df[城市].fillna(未知, inplaceTrue) # 4. 清洗异常值处理‘订单金额’ Q1 df[订单金额].quantile(0.25) Q3 df[订单金额].quantile(0.75) IQR Q3 - Q1 lower_bound Q1 - 1.5 * IQR upper_bound Q3 1.5 * IQR # 这里采用盖帽法而不是直接删除避免损失潜在的大客户订单 df[订单金额_清洗后] df[订单金额].clip(lowerlower_bound, upperupper_bound) # 5. 字符串标准化 df[城市] df[城市].str.strip().str.lower() # 建立城市映射纠正常见错误 city_map {bj: 北京, sh: 上海, gz: 广州, sz: 深圳} df[城市] df[城市].replace(city_map) # 6. 类型转换与特征工程 df[订单日期] pd.to_datetime(df[订单日期], errorscoerce) df[订单月份] df[订单日期].dt.to_period(M) # 转换为“年月”周期格式便于按月度分组 df[是否周末] df[订单日期].dt.dayofweek 5 df[实际支付] df[订单金额_清洗后] * (1 - df[折扣]) # 7. 去重假设同一订单ID不应重复 df.drop_duplicates(subset[订单ID], keepfirst, inplaceTrue) # 8. 最终审视 print(\n 清洗完成 ) print(最终形状:, df.shape) print(df[[订单ID, 城市, 订单金额, 订单金额_清洗后, 实际支付, 订单月份]].head(10))这个流程覆盖了从加载到规整的主要环节。实际操作中步骤可能循环反复。比如在特征工程创建了“实际支付”列后你可能需要再次用describe()查看其分布或者用透视表按城市、月份查看汇总情况根据新发现的问题可能还需要回头调整清洗参数。数据处理没有绝对的标准答案它是在数据质量、业务逻辑和分析需求三者之间寻找最佳平衡点的艺术。每一次清洗和转换你都在为后续的分析模型奠定基础。多练、多思考、多结合业务场景你会逐渐培养出对数据的“手感”知道从哪里入手如何判断处理是否合理。记住干净的数据本身就是一份极具价值的资产。