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

前言

前面三篇分别介绍了机器学习流程、Python 环境和 NumPy 数组。真正进入项目后,我们通常不会马上调用模型的 fit(),而是先花时间理解数据:文件能否正确读取,每一列代表什么,有没有缺失、重复或格式错误,哪些字段可以作为特征。

模型效果差,原因不一定是算法不够复杂。训练数据中把字符串误当成数字、把重复记录算了两次、用错误方式填补缺失标签,都可能让结果失真。算法只能根据收到的数据学习;输入的含义有问题,换一个更复杂的模型也不会自动修复。

因此,真实机器学习流程更接近"获取数据 → 理解数据 → 清洗数据 → 准备特征 → 训练模型"。Pandas 主要负责模型训练之前的几步。本文用一份模拟学生成绩表,完整走一遍从表格到特征矩阵的过程。

Pandas 是什么

Pandas 常被解释为 Python Data Analysis Library,即 Python 数据分析库。它擅长处理带有行列结构的数据,例如 CSV 文件、Excel 工作表、数据库查询结果和日志汇总表。

Pandas 的主要用途包括:

  • 把不同来源的数据读取为统一的表格结构;
  • 按行、按列查看和筛选数据;
  • 处理缺失值、重复记录和错误类型;
  • 完成分组统计、排序、合并与格式转换;
  • 选择机器学习需要的特征和目标。

它有两个核心对象:SeriesDataFrameSeries 可以理解为带索引的一列数据,DataFrame 则是由多列组成的二维表。与普通 Excel 表格相比,DataFrame 不仅能显示数据,还能通过代码重复执行同一套清洗规则,并记录每一步做了什么。

这种"可重复"很重要。手动在表格里删除几行也许很快,但当数据更新后,我们很难保证再次做出完全相同的处理。Pandas 代码可以重新运行,更适合机器学习实验和长期项目。

为什么机器学习需要 Pandas

机器学习数据可能来自 CSV 文件、Excel 表格、数据库或日志文件。它们的格式和质量通常并不统一:CSV 的分隔符或编码可能不同;Excel 中同一列可能同时出现数字和文字;数据库字段可能包含空值;日志里还可能混入重复事件。

模型通常希望得到规则的数值矩阵,但原始数据往往包含姓名、日期、备注、类别和各种业务字段。把数据交给模型前,至少需要完成四件事:读取、查看、整理和转换。

Pandas 处在数据来源与机器学习模型之间。它负责把原始内容组织成 DataFrame,帮助我们检查列名、类型、范围和缺失情况,再选择合适字段。处理完成后,可以把 DataFrame 直接交给许多 scikit-learn 接口,也可以通过 to_numpy() 转为 NumPy 数组。

需要注意,Pandas 只提供工具,不替我们决定清洗规则。年龄缺失能否用中位数填充、异常高分是否真是录入错误,都要结合数据来源和任务目标判断。

Series 和 DataFrame

Series:一列数据

下面创建一列学生成绩:

复制代码
import pandas as pd

scores = pd.Series([85, 92, 78], name="score")
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_hoursattendance 可以作为候选特征;score 可以作为希望预测的目标。字段在表里相邻,不代表它们在机器学习中扮演相同角色。

创建第一个 DataFrame

pd.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()),否则最后会多打印一个 Nonedescribe() 只是统计摘要,不能代替对业务含义的检查。

数据读取

读取 CSV

CSV 是机器学习项目中最常见的表格格式之一。基本读取方式是:

复制代码
data = pd.read_csv("students.csv")

文件名使用相对路径时,相对的是程序当前工作目录,不一定是脚本所在目录。项目中可以使用 pathlib.Path 组合路径,并在读取前检查文件是否存在。CSV 还可能需要指定 encodingsepdtype 或缺失值标记,但不要在不理解文件的情况下随意设置。

读取 Excel

Excel 文件可以使用:

复制代码
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"], keep="first")

保留第一条还是最后一条,取决于哪一条代表最新或最可信记录。

数据类型转换

CSV 中的 "90" 看起来像数字,但可能被读取为字符串。可以使用:

复制代码
data["score"] = pd.to_numeric(data["score"], errors="coerce")

无法转换的内容在 errors="coerce" 下会变成缺失值,之后必须继续检查。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.py

复制代码
"""Pandas 入门示例:构造、检查并清洗学生成绩数据。"""

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(f"NumPy 或 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, size=student_count).round(1)
    attendance = rng.integers(70, 101, size=student_count).astype(object)
    scores = (
        30.0
        + study_hours * 5.0
        + np.asarray(attendance, dtype=float) * 0.25
        + rng.normal(0.0, 3.0, size=student_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": [f"S{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_index=True)


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], errors="coerce")

    # 特征缺失用中位数填充;标签缺失不猜测,直接移除对应行。
    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"], keep="first")
    return cleaned.reset_index(drop=True)


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(f"Pandas: {pd.__version__}")
    print("\n原始 DataFrame 前 5 行:")
    print(raw_data.head().to_string(index=False))
    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, errors="coerce"
    )
    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(index=False))
    print(f"X shape: {features.shape}")
    print("目标 y 前 5 个值:", target.head().tolist())
    print(f"y shape: {target.shape}")


if __name__ == "__main__":
    try:
        main()
    except (RuntimeError, ValueError) as exc:
        print(f"程序无法继续:{exc}", file=sys.stderr)
        sys.exit(1)

在项目根目录运行:

复制代码
python 04_pandas_demo.py

代码已在 Conda base 环境中实际运行,使用 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 行:两条重复记录被合并,一条成绩缺失的记录被删除。学习时间和出勤率的缺失值已用各自中位数填充,因此最终 Xy 都没有缺失值。

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 读取原始表格并准备 Xy,这些数据在需要时转换为 NumPy 数组,然后交给 scikit-learn 模型。许多 scikit-learn 接口也能直接接收 DataFrame,并利用清晰的列顺序。无论采用哪种形式,都要确保训练和预测时特征列一致。

可以把它们理解为一条协作链:Pandas 解决"数据是否干净、字段是否明确",NumPy 解决"数字如何高效组织和计算",scikit-learn 解决"如何训练并评估模型"。

总结

机器学习不是拿到文件就训练模型。更可靠的顺序是:获取数据、理解数据、清洗数据、准备特征,再进入模型训练。Pandas 是数据准备阶段的重要工具。

本文认识了 Series 和 DataFrame,练习了 head()info()describe()、缺失值处理、重复记录删除、类型转换以及 Xy 的选择。实践案例也展示了一个关键习惯:每次清洗都记录行数、缺失数量和最终形状,让数据变化可解释、可复现。

下一篇预告

下一篇是《机器学习完整流程:数据、训练、验证与测试》。我们会把数据准备、数据拆分、模型训练和评估串成一条完整流程,并重点说明验证集、测试集和数据泄漏。

相关推荐
科技圈观察1 小时前
选哪家服务商做跨境业务KYC数字身份认证和合规支持?2026出海选型参考
大数据·人工智能
手写码匠1 小时前
华为云Flexus+DeepSeek征文|Dify 多智能体协同编排实战:R1 规划 + V3 执行,构建企业 Agent 团队
人工智能·深度学习·算法·aigc
舒一笑1 小时前
Windows + WSL2 完整复现 Avernet WAIC 6 Bot 协作演示
人工智能·git·开源
犀利豆2 小时前
我和 Claude Code 一起写了一本介绍 Claude Code 原理的书
人工智能·ai编程·claude
墨_浅-2 小时前
20260805金融科技动向:《知识产权保护和运用“十五五”规划》金融机遇
人工智能·科技·金融
微硬创新2 小时前
耐达讯自动化16路0-20mA转PROFINET协议转换模块技术说明
人工智能·网络协议·自动化·信息与通信
IT_陈寒3 小时前
我又被JavaScript的隐式类型转换坑了
前端·人工智能·后端
工业设备方案笔记3 小时前
RK3588 vs RK3568:AI边缘计算项目到底应该如何选择芯片平台?
arm开发·人工智能·目标跟踪·架构·边缘计算
SomeB1oody4 小时前
【RustyML入门】2.6. 线性判别分析
开发语言·后端·机器学习·rust·教程