公司动态

04 Pandas 数据清洗实战:从表格读取到机器学习特征准备

📅 2026/8/7 9:32:22
04 Pandas 数据清洗实战:从表格读取到机器学习特征准备
前言前面三篇分别介绍了机器学习流程、Python 环境和 NumPy 数组。真正进入项目后我们通常不会马上调用模型的fit()而是先花时间理解数据文件能否正确读取每一列代表什么有没有缺失、重复或格式错误哪些字段可以作为特征。模型效果差原因不一定是算法不够复杂。训练数据中把字符串误当成数字、把重复记录算了两次、用错误方式填补缺失标签都可能让结果失真。算法只能根据收到的数据学习输入的含义有问题换一个更复杂的模型也不会自动修复。因此真实机器学习流程更接近“获取数据 → 理解数据 → 清洗数据 → 准备特征 → 训练模型”。Pandas 主要负责模型训练之前的几步。本文用一份模拟学生成绩表完整走一遍从表格到特征矩阵的过程。Pandas 是什么Pandas 常被解释为 Python Data Analysis Library即 Python 数据分析库。它擅长处理带有行列结构的数据例如 CSV 文件、Excel 工作表、数据库查询结果和日志汇总表。Pandas 的主要用途包括把不同来源的数据读取为统一的表格结构按行、按列查看和筛选数据处理缺失值、重复记录和错误类型完成分组统计、排序、合并与格式转换选择机器学习需要的特征和目标。它有两个核心对象Series和DataFrame。Series可以理解为带索引的一列数据DataFrame则是由多列组成的二维表。与普通 Excel 表格相比DataFrame 不仅能显示数据还能通过代码重复执行同一套清洗规则并记录每一步做了什么。这种“可重复”很重要。手动在表格里删除几行也许很快但当数据更新后我们很难保证再次做出完全相同的处理。Pandas 代码可以重新运行更适合机器学习实验和长期项目。为什么机器学习需要 Pandas机器学习数据可能来自 CSV 文件、Excel 表格、数据库或日志文件。它们的格式和质量通常并不统一CSV 的分隔符或编码可能不同Excel 中同一列可能同时出现数字和文字数据库字段可能包含空值日志里还可能混入重复事件。模型通常希望得到规则的数值矩阵但原始数据往往包含姓名、日期、备注、类别和各种业务字段。把数据交给模型前至少需要完成四件事读取、查看、整理和转换。Pandas 处在数据来源与机器学习模型之间。它负责把原始内容组织成 DataFrame帮助我们检查列名、类型、范围和缺失情况再选择合适字段。处理完成后可以把 DataFrame 直接交给许多 scikit-learn 接口也可以通过to_numpy()转为 NumPy 数组。需要注意Pandas 只提供工具不替我们决定清洗规则。年龄缺失能否用中位数填充、异常高分是否真是录入错误都要结合数据来源和任务目标判断。Series 和 DataFrameSeries一列数据下面创建一列学生成绩import pandas as pd scores pd.Series([85, 92, 78], namescore) print(scores)Series 既保存值也保存索引。默认索引是0、1、2也可以换成学生编号。索引用于定位和对齐数据不等同于模型特征。scores pd.Series([85, 92, 78], index[S001, S002, S003]) print(scores[S002])DataFrame二维表DataFrame 由多列 Series 组成。下面的每一行是一名学生每一列是一项字段students pd.DataFrame( { student: [小林, 小周, 小陈], study_hours: [3.5, 5.0, 2.0], attendance: [92, 98, 85], score: [78, 91, 70], } )students.columns返回列名students.index返回行索引students.values返回底层二维数组。实际代码中更推荐写students.to_numpy()它明确表达“转换为 NumPy 数组”。转换后会失去列名和索引所以应在确认字段顺序后再做。在这个表中student适合识别记录但姓名本身不应直接作为数值模型输入study_hours和attendance可以作为候选特征score可以作为希望预测的目标。字段在表里相邻不代表它们在机器学习中扮演相同角色。创建第一个 DataFramepd.DataFrame()可以接收字典、二维列表等数据。字典的键通常成为列名每个列表提供一列data pd.DataFrame( { student: [S001, S002, S003], study_hours: [2.5, 4.0, 6.5], attendance: [88, 95, 97], score: [70, 82, 93], } )创建后不要只盯着完整表格看。三个常用入口分别回答不同问题print(data.head()) data.info() print(data.describe())head()默认显示前 5 行适合快速检查列是否错位info()输出行数、非空数量、列类型和内存概况describe()对数值列给出数量、平均值、标准差、四分位数和最大最小值。info()会直接把结果写到终端返回值是None所以一般不写print(data.info())否则最后会多打印一个None。describe()只是统计摘要不能代替对业务含义的检查。数据读取读取 CSVCSV 是机器学习项目中最常见的表格格式之一。基本读取方式是data pd.read_csv(students.csv)文件名使用相对路径时相对的是程序当前工作目录不一定是脚本所在目录。项目中可以使用pathlib.Path组合路径并在读取前检查文件是否存在。CSV 还可能需要指定encoding、sep、dtype或缺失值标记但不要在不理解文件的情况下随意设置。读取 ExcelExcel 文件可以使用data pd.read_excel(students.xlsx, sheet_name成绩)读取.xlsx往往需要openpyxl等额外依赖。缺少时应在当前项目环境中安装匹配版本而不是大范围升级全部包。一个工作簿可能包含多个工作表还要确认sheet_name是否正确。读取数据库Pandas 可以通过pd.read_sql()读取 SQL 查询结果。它需要有效的数据库连接、驱动和查询语句。真实项目不应把账号密码直接写进文章或源码也不要默认一次读取整张大表应先确定字段、时间范围和数据量。数据查看与探索拿到数据后直接训练常会把早期问题带到流程末端。可以先查看print(data.head()) print(data.tail()) print(data.shape) print(data.columns) print(data.dtypes)head()和tail()检查文件开头与结尾shape给出行数和列数columns帮助发现多余空格、拼写差异或意外列dtypes显示每列的数据类型。如果预期有 10000 条记录实际shape却只有(100, 12)可能读错文件或过滤条件。若成绩列的类型是object说明它可能含有字符串。还应检查唯一值、取值范围和缺失比例判断“0”究竟是真实数值还是另一种缺失标记。探索的目的不是追求漂亮统计而是建立数据契约每行代表什么、每列单位是什么、哪些范围合理、哪些字段在预测时能够获得。没有这些信息后面的特征工程很容易产生数据泄漏。数据清洗真实数据常见四类问题缺失值、错误格式、重复记录和异常值。清洗不是把“不好看的值”全部删除而是依据任务规则处理并记录清洗前后的行数变化。实际项目还应保留一份未经修改的原始数据把清洗结果写到新的变量或文件中。这样发现规则有误时可以重新处理也能对比某一步究竟删除或改变了哪些记录。不要反复覆盖唯一的原始文件。异常值尤其需要谨慎。出勤率 150% 很可能是录入错误考试成绩 100 分却可能完全合理。可以用统计方法发现异常候选但是否修正或删除仍要回到数据定义。缺失值处理isnull()和isna()都可判断缺失值print(data.isnull().sum())dropna()删除含缺失值的行或列fillna()用指定值填充。例如数值特征缺失时可以在有依据的情况下用中位数填充data[age] data[age].fillna(data[age].median())缺失标签需要区别对待。如果目标score缺失随意用平均成绩填充会制造一个并不存在的训练答案。本文示例选择删除标签缺失行而对学习时间和出勤率使用中位数填充。真实项目还应只用训练集计算填充值避免测试集信息泄漏到训练过程。重复数据处理duplicated()返回每行是否重复的布尔结果drop_duplicates()删除重复项print(data.duplicated().sum()) data data.drop_duplicates()判断重复要结合业务主键。两名学生学习时间相同不代表记录重复如果student_id应当唯一可以写data data.drop_duplicates(subset[student_id], keepfirst)保留第一条还是最后一条取决于哪一条代表最新或最可信记录。数据类型转换CSV 中的90看起来像数字但可能被读取为字符串。可以使用data[score] pd.to_numeric(data[score], errorscoerce)无法转换的内容在errorscoerce下会变成缺失值之后必须继续检查。astype(float)适合数据已经干净、转换失败应立即报错的情况。日期则通常使用pd.to_datetime()不要把所有类型问题都交给astype()。特征选择和数据转换清洗后的表格仍可能包含模型不需要的字段。假设原始数据有姓名、学习时间、成绩和备注本文只选择学习时间与出勤率作为输入把成绩作为目标X data[[study_hours, attendance]].copy() y data[score].copy()双方括号很重要data[[study_hours]]返回二维 DataFrame形状类似(样本数, 1)data[study_hours]返回一维 Series形状是(样本数,)。许多 scikit-learn 模型要求特征X为二维结构。学生编号用于追踪数据但通常不应作为连续数字特征备注是自由文本也不能未经编码直接进入常规模型。特征选择不仅是语法问题还要考虑预测时是否能获得该字段、字段是否泄漏答案、使用它是否合理。第一个 Pandas 实践案例完整示例先用固定随机种子构造 20 条基础学生记录字段包括学生编号、学习时间、出勤率和成绩。随后有意加入一项字符串数字、三处缺失值并追加两条重复记录使清洗前共有 22 行。程序先查看前 5 行、缺失数量、重复数量和数值统计。清洗时把数值列统一转换使用中位数填补两个特征删除缺失成绩的行再按学生编号删除重复记录。最终得到 19 行干净数据并选择两列特征组成X成绩组成y。这些成绩和权重关系由代码构造只用于演示 Pandas 流程不代表真实教学数据也不适合评价学生。完整可运行代码将下面代码保存为04_pandas_demo.pyPandas 入门示例构造、检查并清洗学生成绩数据。 from __future__ import annotations import sys def load_dependencies(): 导入示例依赖并在环境有问题时给出清晰提示。 try: import numpy as np import pandas as pd except ModuleNotFoundError as exc: raise RuntimeError( 当前 Python 环境缺少 NumPy 或 Pandas请先检查解释器和 Conda 环境。 ) from exc except Exception as exc: raise RuntimeError(fNumPy 或 Pandas 已被找到但导入失败{exc}) from exc return np, pd def build_raw_data(np, pd): 创建 20 条基础记录并有意加入缺失值、字符串数字和重复行。 rng np.random.default_rng(42) student_count 20 study_hours rng.uniform(1.0, 10.0, sizestudent_count).round(1) attendance rng.integers(70, 101, sizestudent_count).astype(object) scores ( 30.0 study_hours * 5.0 np.asarray(attendance, dtypefloat) * 0.25 rng.normal(0.0, 3.0, sizestudent_count) ).clip(0.0, 100.0).round(1) # 人为制造真实表格中常见的问题便于演示清洗流程。 study_hours[3] np.nan attendance[5] None attendance[14] 91 scores[8] np.nan data pd.DataFrame( { student_id: [fS{i:03d} for i in range(1, student_count 1)], study_hours: study_hours, attendance: attendance, score: scores, } ) # 追加两条完整重复记录原始数据由 20 行变为 22 行。 return pd.concat([data, data.iloc[[2, 7]]], ignore_indexTrue) def clean_data(data, pd): 转换类型、处理缺失值并删除重复学生记录。 cleaned data.copy() numeric_columns [study_hours, attendance, score] for column in numeric_columns: cleaned[column] pd.to_numeric(cleaned[column], errorscoerce) # 特征缺失用中位数填充标签缺失不猜测直接移除对应行。 cleaned[study_hours] cleaned[study_hours].fillna( cleaned[study_hours].median() ) cleaned[attendance] cleaned[attendance].fillna( cleaned[attendance].median() ) cleaned cleaned.dropna(subset[score]) cleaned cleaned.drop_duplicates(subset[student_id], keepfirst) return cleaned.reset_index(dropTrue) def main() - None: 运行完整的数据构造、探索、清洗和特征选择流程。 if sys.version_info (3, 8): raise RuntimeError(本示例需要 Python 3.8 或更高版本。) np, pd load_dependencies() raw_data build_raw_data(np, pd) required_columns {student_id, study_hours, attendance, score} if set(raw_data.columns) ! required_columns: raise ValueError(原始数据列与示例约定不一致。) print(fPandas: {pd.__version__}) print(\n原始 DataFrame 前 5 行) print(raw_data.head().to_string(indexFalse)) print(f\n清洗前 shape: {raw_data.shape}) print(清洗前各列缺失值数量) print(raw_data.isna().sum().to_string()) duplicate_count int(raw_data.duplicated(subset[student_id]).sum()) print(f重复学生记录数: {duplicate_count}) print(\n原始数值列统计) numeric_view raw_data[[study_hours, attendance, score]].apply( pd.to_numeric, errorscoerce ) print(numeric_view.describe().round(2).to_string()) cleaned_data clean_data(raw_data, pd) features cleaned_data[[study_hours, attendance]].copy() target cleaned_data[score].copy() if cleaned_data[student_id].duplicated().any(): raise ValueError(清洗后仍存在重复的学生编号。) if features.isna().any().any() or target.isna().any(): raise ValueError(最终特征或标签中仍存在缺失值。) if features.shape ! (19, 2) or target.shape ! (19,): raise ValueError(最终特征或标签的形状与预期不一致。) print(f\n清洗后 shape: {cleaned_data.shape}) print(f删除或合并的行数: {len(raw_data) - len(cleaned_data)}) print(清洗后各列缺失值数量) print(cleaned_data.isna().sum().to_string()) print(\n最终机器学习特征 X 前 5 行) print(features.head().to_string(indexFalse)) print(fX shape: {features.shape}) print(目标 y 前 5 个值:, target.head().tolist()) print(fy shape: {target.shape}) if __name__ __main__: try: main() except (RuntimeError, ValueError) as exc: print(f程序无法继续{exc}, filesys.stderr) sys.exit(1)在项目根目录运行python 04_pandas_demo.py代码已在 Condabase环境中实际运行使用 Python 3.11.4、Pandas 1.5.3 和 NumPy 1.24.3程序正常结束。关键输出如下Pandas: 1.5.3 原始 DataFrame 前 5 行 student_id study_hours attendance score S001 8.0 75 95.2 S002 4.9 93 76.5 S003 8.7 91 94.7 S004 NaN 80 84.1 S005 1.8 72 58.8 清洗前 shape: (22, 4) 清洗前各列缺失值数量 student_id 0 study_hours 1 attendance 1 score 1 重复学生记录数: 2 原始数值列统计 study_hours attendance score count 21.00 21.00 21.0 mean 6.32 85.90 84.5 std 2.58 8.48 12.6 min 1.60 71.00 57.2 25% 4.90 80.00 77.0 50% 6.80 91.00 86.5 75% 8.40 93.00 94.7 max 9.80 97.00 100.0 清洗后 shape: (19, 4) 删除或合并的行数: 3 清洗后各列缺失值数量 student_id 0 study_hours 0 attendance 0 score 0 最终机器学习特征 X 前 5 行 study_hours attendance 8.0 75.0 4.9 93.0 8.7 91.0 6.8 80.0 1.8 72.0 X shape: (19, 2) 目标 y 前 5 个值: [95.2, 76.5, 94.7, 84.1, 58.8] y shape: (19,)原始 22 行最终变为 19 行两条重复记录被合并一条成绩缺失的记录被删除。学习时间和出勤率的缺失值已用各自中位数填充因此最终X与y都没有缺失值。Pandas 常见问题1. 为什么读取 CSV 后中文乱码文件编码与read_csv()使用的编码不一致时会乱码或解码失败。常见编码有 UTF-8、UTF-8 with BOM 和 GBK。应先确认文件实际编码再明确传入encoding不要把多种编码挨个尝试后就忽略异常。Excel 导出的 CSV 在不同系统中也可能不同。2. 为什么数字变成字符串同一列中只要混入单位、空格、特殊缺失标记或其他文本Pandas 就可能把整列识别为object。先用unique()或字符串检查找到异常内容再用pd.to_numeric()转换并核对新增缺失值不能只把报错隐藏起来。3. 为什么修改 DataFrame 没有生效有时对筛选结果直接赋值会触发SettingWithCopyWarning因为 Pandas 无法确定操作的是原表视图还是副本。可以先.copy()或使用.loc[行条件, 列名] 值明确修改目标。修改后立即检查几行和类型不要等到训练阶段才发现。4. 为什么机器学习前需要标准化数据不同特征的量纲可能差别很大例如学习小时在 0 到 10累计点击数可能达到几万。依赖距离或梯度的模型可能受尺度影响。标准化通常由 scikit-learn 的StandardScaler完成并且只能在训练集上拟合再应用到验证集和测试集。并非所有模型都必须标准化树模型通常不敏感。5. Pandas 和 NumPy 有什么区别NumPy 适合规则的多维数值数组和底层计算Pandas 在其上提供列名、索引、不同列类型和缺失值处理更适合表格数据。清洗阶段保留 DataFrame 的列名更容易检查模型计算阶段再根据接口需要转为数组。Pandas、NumPy 和 scikit-learn 的关系三者经常共同出现但分工不同。NumPy 提供底层数组与数值计算Pandas 负责结构化数据读取、探索和清洗scikit-learn 提供数据拆分、预处理、模型训练与评估工具。常见流程是Pandas 读取原始表格并准备X、y这些数据在需要时转换为 NumPy 数组然后交给 scikit-learn 模型。许多 scikit-learn 接口也能直接接收 DataFrame并利用清晰的列顺序。无论采用哪种形式都要确保训练和预测时特征列一致。可以把它们理解为一条协作链Pandas 解决“数据是否干净、字段是否明确”NumPy 解决“数字如何高效组织和计算”scikit-learn 解决“如何训练并评估模型”。总结机器学习不是拿到文件就训练模型。更可靠的顺序是获取数据、理解数据、清洗数据、准备特征再进入模型训练。Pandas 是数据准备阶段的重要工具。本文认识了 Series 和 DataFrame练习了head()、info()、describe()、缺失值处理、重复记录删除、类型转换以及X、y的选择。实践案例也展示了一个关键习惯每次清洗都记录行数、缺失数量和最终形状让数据变化可解释、可复现。下一篇预告下一篇是《机器学习完整流程数据、训练、验证与测试》。我们会把数据准备、数据拆分、模型训练和评估串成一条完整流程并重点说明验证集、测试集和数据泄漏。