前言
前面三篇分别介绍了机器学习流程、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 和 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_hours 和 attendance 可以作为候选特征;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()),否则最后会多打印一个 None。describe() 只是统计摘要,不能代替对业务含义的检查。
数据读取
读取 CSV
CSV 是机器学习项目中最常见的表格格式之一。基本读取方式是:
data = pd.read_csv("students.csv")
文件名使用相对路径时,相对的是程序当前工作目录,不一定是脚本所在目录。项目中可以使用 pathlib.Path 组合路径,并在读取前检查文件是否存在。CSV 还可能需要指定 encoding、sep、dtype 或缺失值标记,但不要在不理解文件的情况下随意设置。
读取 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 行:两条重复记录被合并,一条成绩缺失的记录被删除。学习时间和出勤率的缺失值已用各自中位数填充,因此最终 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 的选择。实践案例也展示了一个关键习惯:每次清洗都记录行数、缺失数量和最终形状,让数据变化可解释、可复现。
下一篇预告
下一篇是《机器学习完整流程:数据、训练、验证与测试》。我们会把数据准备、数据拆分、模型训练和评估串成一条完整流程,并重点说明验证集、测试集和数据泄漏。