Data-Science-For-Beginners 数据准备实战:用 Pandas 完成缺失值处理、去重与数据清洗
【免费下载链接】Data-Science-For-Beginners10 Weeks, 20 Lessons, Data Science for All!项目地址: https://gitcode.com/GitHub_Trending/da/Data-Science-For-Beginners
本篇文章源自微软开源课程 Data-Science-For-Beginners 第 2 单元(与数据打交道)的第 8 课,聚焦"数据准备(Data Preparation)"这一数据科学流水线中的关键环节。原始数据往往因为来源多样而包含缺失、错误或不完整的信息,本课将带你掌握基于 Python 与 Pandas 的清洗与转换技术——包括探索 DataFrame 结构、检测与处理缺失值(NaN/None)、去除重复数据,并结合本仓库的 notebook.ipynb 扩展到类别编码与真实世界数据质量检查。学完本篇,你将能独立完成一套从"脏数据"到"可建模数据"的完整清洗流程。
为什么数据清洗如此重要
根据数据来源的不同,原始数据常常包含一些不一致之处,给后续的分析与建模带来挑战。这类数据可以被归类为"脏数据(dirty)",需要通过清洗来修复。在课程(阿拉伯语版本 与 英文版本)中,清洗数据的价值被概括为三点:
- 易于使用与复用(Ease of use and reuse):数据经过正确的组织和规范化后,更容易被检索、使用和分享给他人。
- 一致性(Consistency):数据科学经常需要同时处理多个数据集,并把来自不同来源的数据拼接(join)在一起。确保每个数据集都遵循共同的标准化规范,才能保证它们在合并成一个数据集后依然可用。
- 模型准确性(Model accuracy):经过清洗的数据能够直接提升依赖它的机器学习模型的准确率。
常见的清洗目标与策略
在动手写代码之前,先建立全局视角。课程总结了四类最典型的清洗目标与对应策略:
- 探索数据集(Exploring a dataset):数据探索(本课程的 15 课"分析数据" 有更深入讲解)能帮你发现哪些数据需要清洗。通过可视化观察数据集中的值,可以建立对剩余数据的预期,或发现可以解决的问题。探索手段包括基本查询、可视化和抽样。
- 格式化(Formatting):数据可能因来源不同而呈现不一致的形式,导致搜索和表示数值时出现问题——值明明存在于数据集中,却无法在可视化或查询结果中正确展示。常见的格式化问题涉及空白字符、日期和数据类型的处理。格式化标准的确定通常取决于数据的使用者,例如日期和数字的表示标准会因国家而异。
- 重复(Duplications):出现多次的数据会产生不准确的结果,通常应当移除。这在合并两个或多个数据集时尤为常见;但也有例外——合并后数据集中"重复"的部分可能携带额外信息而需要保留。
- 缺失数据(Missing Data):缺失数据会造成结果不准确、不稳健甚至出现偏差。有时可以通过"重新加载"数据、用 Python 等代码计算并填充缺失值、或者直接删除该值与关联数据来解决。数据丢失的原因很多,采取何种措施取决于数据最初丢失的方式与原因。
探索 DataFrame 信息:先"看一眼"你的数据
数据加载进 pandas 后,通常以 DataFrame 形式存在(可参考前一课 与 Python 打交道 中对 DataFrame 的详细概述)。但如果你的 DataFrame 有 6 万行、400 列,该如何快速建立对数据的整体感知?pandas 提供了便捷工具来查看 DataFrame 的整体信息以及首尾若干行。
本课使用 scikit-learn 内置的经典Iris(鸢尾花)数据集进行演示:
import pandas as pd from sklearn.datasets import load_iris iris = load_iris() iris_df = pd.DataFrame(data=iris['data'], columns=iris['feature_names'])sepal length (cm) sepal width (cm) petal length (cm) petal width (cm) 0 5.1 3.5 1.4 0.2 1 4.9 3.0 1.4 0.2 2 4.7 3.2 1.3 0.2 3 4.6 3.1 1.5 0.2 4 5.0 3.6 1.4 0.2DataFrame.shape 与 DataFrame.columns
在 notebook.ipynb 中,除了课程正文提到的info()、head()、tail(),还补充了两个先行的探索属性:
iris_df.shape:返回(150, 4),表示 150 行、4 列,即 150 个数据点、每个数据点含 4 个特征。注意shape是属性而非方法,因此不带括号。iris_df.columns:返回列名列表(sepal length (cm)、sepal width (cm)、petal length (cm)、petal width (cm)),用于识别数据集包含的特征。
DataFrame.info():整体摘要
info()方法用于打印 DataFrame 内容的摘要:
iris_df.info()RangeIndex: 150 entries, 0 to 149 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 sepal length (cm) 150 non-null float64 1 sepal width (cm) 150 non-null float64 2 petal length (cm) 150 non-null float64 3 petal width (cm) 150 non-null float64 dtypes: float64(4) memory usage: 4.8 KB从输出可知:Iris 数据集有 150 个条目、4 个列、无空条目,全部数据以 64 位浮点数(float64)存储。这一输出同时暴露了两个关键信息:每列的数据类型(Dtype),以及每列的非空值数量(Non-Null Count)——后者正是数据准备阶段处理缺失值的重要入口。
DataFrame.describe():统计摘要
notebook 还引入了describe(),它对数据集的数值列给出单变量统计摘要,包括数据点总数、均值、标准差、最小值、下四分位数(25%)、中位数(50%)、上四分位数(75%)和最大值:
iris_df.describe()DataFrame.head() 与 DataFrame.tail():查看首尾行
head()默认返回前 5 行:
iris_df.head()sepal length (cm) sepal width (cm) petal length (cm) petal width (cm) 0 5.1 3.5 1.4 0.2 1 4.9 3.0 1.4 0.2 2 4.7 3.2 1.3 0.2 3 4.6 3.1 1.5 0.2 4 5.0 3.6 1.4 0.2tail()则相反,返回最后 5 行:
iris_df.tail()sepal length (cm) sepal width (cm) petal length (cm) petal width (cm) 145 6.7 3.0 5.2 2.3 146 6.3 2.5 5.0 1.9 147 6.5 3.0 5.2 2.0 148 6.2 3.4 5.4 2.3 149 5.9 3.0 5.1 1.8在实践中有序数据集里查找异常值时,快速查看首尾几行非常实用。notebook 还留了一个练习:head()默认返回 5 行,如何显示超过 5 行?提示是查阅文档iris_df.head?——答案即传入参数head(n)。
小结:仅通过 DataFrame 的元数据(shape、columns、info、describe)或首尾若干行的值,就能立刻了解所处理数据的规模、形状与内容。
处理缺失数据
绝大多数实际数据集都包含缺失值,而缺失值的处理方式带有微妙的权衡,会影响最终分析与真实世界的结果。
pandas 如何表示缺失值:NaN 与 None
pandas 用两种方式表示缺失值:
NaN(Not a Number):这是 IEEE 浮点数规范中的一个特殊值,仅用于表示缺失的浮点数值。任何对NaN的算术运算结果仍是NaN(如np.nan + 1、np.nan * 0都是NaN),但含NaN的数组做聚合(如sum()、min())不会报错,只是结果未必有意义(例如np.array([2, np.nan, 6, 8]).sum()返回nan)。None:Python 原生对象,用于表示浮点数以外的缺失值。None不能用于非object类型的 NumPy/pandas 数组;数组中出现None时,会触发数据类型"向上转型"(upcast)为 Python 对象,导致运算在解释型 Python 层面执行、性能下降,且对含None的数组执行sum()、min()等聚合通常会报错——整数与None的加法等运算是未定义的。
虽然NaN与None行为略有差异,但 pandas 在构造Series/DataFrame时会自动在两者之间切换,以保持数据类型同质。因此可以把它们看作 pandas 中"空值"的两种形态,而处理空值的核心方法也由此得名:isnull()、notnull()、dropna()、fillna()。关于NaN与None的更深入探讨可参见 15 课的分析 notebook。
检测缺失值:isnull() 与 notnull()
isnull()和notnull()是检测空数据的主要方法,两者都返回覆盖在数据之上的布尔掩码(Boolean mask)。使用 numpy 构造含NaN的示例:
import numpy as np example1 = pd.Series([0, np.nan, '', None]) example1.isnull()0 False 1 True 2 False 3 True dtype: bool仔细看这个输出,有两处值得留意:
0在算术上是"零",但它是完全合法的整数,pandas 会如实对待它,不会判定为空。''更微妙:虽然空字符串在概念上"什么都没有",但它仍是字符串对象,在 pandas 看来并非空值。
布尔掩码可以直接用作Series或DataFrame的索引,用于隔离缺失(或非缺失)的值;配合sum()还能统计缺失值总数(example1.isnull().sum())。
小结:
isnull()与notnull()在 DataFrame 上使用时会同时展示结果及对应的索引,这对处理数据帮助极大。
删除缺失值:dropna()
除了识别缺失值,pandas 还提供了从Series和DataFrame中移除空值的便捷方法。尤其在大型数据集中,直接删除缺失值(NA)往往比用其他方式处理更可取:
example1 = example1.dropna() example10 0 2 dtype: object这与example1[example1.notnull()]的输出相似,区别在于dropna不只是基于掩码索引,而是真正把缺失值从Series中移除。
由于 DataFrame 是二维的,删除数据的选项更丰富。先构造一个含NaN的示例 DataFrame:
example2 = pd.DataFrame([[1, np.nan, 7], [2, 5, 8], [np.nan, 6, 9]]) example20 1 2 0 1.0 NaN 7 1 2.0 5.0 8 2 NaN 6.0 9(注意到 pandas 为了容纳NaN把两个列升级成了浮点类型吗?这就是前面提到的 upcast 行为。)
无法从 DataFrame 中只删除单个值,只能删除整行或整列。在数据科学中,列通常代表变量、行代表观测样本,因此删除行更为常见;dropna()的默认设置就是删除所有含任意空值的行:
example2.dropna()0 1 2 1 2.0 5.0 8如需删除含 NA 的列,使用axis=1(或axis='columns'):
example2.dropna(axis='columns')2 0 7 1 8 2 9注意:这样可能删除大量你想保留的数据,尤其在较小的数据集中。如果只想删除含"多个"甚至"全部"空值的行或列,就需要how与thresh两个参数:
- 默认
how='any':删除含任意空值的行/列(可通过example2.dropna?在代码单元格中查看方法的全部参数)。 how='all':只删除全部为空的行/列。
给example2增加一列全为NaN的列来演示:
example2[3] = np.nan example20 1 2 3 0 1.0 NaN 7 NaN 1 2.0 5.0 8 NaN 2 NaN 6.0 9 NaNthresh参数提供更细粒度的控制:它设定一个行/列需要拥有的非空值数量阈值,达到该阈值才被保留:
example2.dropna(axis='rows', thresh=3)0 1 2 3 1 2.0 5.0 8 NaN这里第 0 行和第 2 行因为只含两个非空值而被删除。
填充缺失值:fillna()
根据数据集的特点,有时用有效值填充空值比删除更合理。虽然可以手动用isnull逐个填充,但工作量很大;pandas 为此提供了fillna(),它返回一份将缺失值替换为你所选值的副本。构造示例Series:
example3 = pd.Series([1, np.nan, 2, None, 3], index=list('abcde')) example3a 1.0 b NaN c 2.0 d NaN e 3.0 dtype: float64用单一值填充,例如用0:
example3.fillna(0)a 1.0 b 0.0 c 2.0 d 0.0 e 3.0 dtype: float64向前填充(forward-fill):用最后一个有效值填充空值:
example3.fillna(method='ffill')a 1.0 b 1.0 c 2.0 d 2.0 e 3.0 dtype: float64向后填充(back-fill):将下一个有效值向后传播以填充空值:
example3.fillna(method='bfill')a 1.0 b 2.0 c 2.0 d 3.0 e 3.0 dtype: float64DataFrame上的行为与此一致,且可以通过axis指定填充方向。沿用前面的example2:
example2.fillna(method='ffill', axis=1)0 1 2 3 0 1.0 1.0 7.0 7.0 1 2.0 5.0 8.0 8.0 2 NaN 6.0 9.0 9.0注意:当没有前值可供向前填充时(第 2 行第 0 列),空值会保留。在较新版本的 pandas 中,也可以用等价的独立方法ffill()与bfill()实现同样的效果。
按数据类型选择填充策略:众数、均值与中位数
notebook 进一步给出了针对不同数据类型的填充策略:
- 类别数据(非数值):一般用该列的**众数(mode)**填充。例如 100 个数据点中 90 个为
True、8 个为False、2 个缺失,则用True填充那 2 个缺失值,先通过value_counts()确认众数,再fillna('True', inplace=True)就地填充。也可以结合领域知识(domain knowledge)做近似填充。 - 数值数据:两种常见方式——用**中位数(median)填充(当数据存在偏斜与离群点时,中位数对离群值稳健),或用均值(mean)**填充(当数据已归一化时,均值与中位数非常接近)。例如
fill_with_mean[0].fillna(np.mean(fill_with_mean[0]), inplace=True),或用fill_with_median[1].fillna(fill_with_median[1].median(), inplace=True)。
此外还可以发挥创意:例如example4.fillna(example4.mean())用整个 DataFrame 的均值填充缺失值——注意默认按行方向填充,全空的列可能仍无值。
小结:处理缺失值有多种方式,具体策略(删除、替换,乃至如何替换)应由数据本身的特性决定。与数据集接触越多,对缺失值处理的把握就越强。
类别数据的编码:让模型"看懂"文字
机器学习模型只能处理数值,无法区分Yes与No,但能区分0与1。因此在填充缺失值之后,需要把类别数据编码为数值。notebook 介绍了两种方式:
标签编码(Label Encoding):把每个类别映射为一个数字。例如航班乘客的舱位['business class', 'economy class', 'first class']可编码为[0, 1, 2]。实现方式是构造字典映射后用replace替换:
label = pd.DataFrame([ [10,'business class'], [20,'first class'], [30, 'economy class'], [40, 'economy class'], [50, 'economy class'], [60, 'business class'] ], columns=['ID','class']) class_labels = {'business class':0, 'economy class':1, 'first class':2} label['class'] = label['class'].replace(class_labels)标签编码适用于两类场景:类别数量较多,或类别之间存在有序关系(如经济舱 < 商务舱 < 头等舱)。
独热编码(One-Hot Encoding):每个类别变成一列,每个数据点根据是否属于该类别填 0 或 1。若有 n 个类别,就新增 n 列(如class_business class、class_economy class、class_first class)。用pd.get_dummies实现:
one_hot = pd.DataFrame([ [10,'business class'], [20,'first class'], [30, 'economy class'], [40, 'economy class'], [50, 'economy class'], [60, 'business class'] ], columns=['ID','class']) one_hot_data = pd.get_dummies(one_hot, columns=['class'])独热编码适用于:类别数量少且数据集规模小,或类别之间没有顺序关系。
小结:编码用于把非数值数据转换为数值数据;标签编码与独热编码两种方式可按数据集需求选用。
去除重复数据
除了缺失数据,真实数据集里也常出现重复数据。pandas 提供了检测与移除重复条目的简便方法。
识别重复:duplicated()
duplicated()返回布尔掩码,指示 DataFrame 中的条目是否与更早的条目重复:
example4 = pd.DataFrame({'letters': ['A','B'] * 2 + ['B'], 'numbers': [1, 2, 1, 3, 3]}) example4letters numbers 0 A 1 1 B 2 2 A 1 3 B 3 4 B 3example4.duplicated()0 False 1 False 2 True 3 False 4 True dtype: bool删除重复:drop_duplicates()
drop_duplicates()返回所有duplicated值为False的数据副本:
example4.drop_duplicates()letters numbers 0 A 1 1 B 2 3 B 3duplicated与drop_duplicates默认检查所有列,但可以指定只检查 DataFrame 中的部分列:
example4.drop_duplicates(['letters'])letters numbers 0 A 1 1 B 2小结:去除重复数据几乎是每个数据科学项目的必经步骤。重复数据会改变分析结果,带来不准确的分析结论!
真实世界的数据质量检查:不止于缺失值与精确重复
notebook 在课程正文基础上扩展了一节实战内容,专门应对真实数据中更隐蔽的质量问题:类别值写法不一致、异常数值(离群点)、以及带细微差异的"近似重复"记录。下面给出其中的关键技术。
构造一个"脏"数据集
dirty_data = pd.DataFrame({ 'customer_id': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], 'name': ['John Smith', 'Jane Doe', 'John Smith', 'Bob Johnson', 'Alice Williams', 'Charlie Brown', 'John Smith', 'Eva Martinez', 'Bob Johnson', 'Diana Prince', 'Frank Castle', 'Alice Williams'], 'age': [25, 32, 25, 45, 28, 199, 25, 31, 45, 27, -5, 28], 'country': ['USA', 'UK', 'U.S.A', 'Canada', 'USA', 'United Kingdom', 'United States', 'Mexico', 'canada', 'USA', 'UK', 'usa'], 'purchase_amount': [100.50, 250.00, 105.00, 320.00, 180.00, 90.00, 102.00, 275.00, 325.00, 195.00, 410.00, 185.00] })1. 不一致的类别值
country列对同一国家有多种写法(USA、U.S.A、United States、usa……)。先用unique()、nunique()、value_counts()摸清各种写法及其频次,再用映射字典标准化:
country_mapping = { 'usa': 'USA', 'u.s.a': 'USA', 'united states': 'USA', 'uk': 'UK', 'united kingdom': 'UK', 'canada': 'Canada', 'mexico': 'Mexico' } dirty_data['country_clean'] = dirty_data['country'].str.lower().map(country_mapping)对于更复杂的场景,可以用rapidfuzz库做模糊匹配,自动找出相似度超过 70% 的字符串组合(如process.extract(country, unique_countries, scorer=fuzz.ratio))。
2. 异常数值(离群点)
age列出现199、-5这类显然异常的值。先用领域知识过滤"不可能的值"(年龄小于 0 或大于 120),再用统计方法检测离群点:
- IQR(四分位距)法:计算
Q1 = valid_ages.quantile(0.25)、Q3 = valid_ages.quantile(0.75),IQR = Q3 - Q1,上下界分别为Q1 - 1.5 * IQR与Q3 + 1.5 * IQR,落在界外的即离群点。IQR 法对极端值不敏感,更为稳健。 - Z-score 法:用 scipy 的
stats.zscore计算每个值偏离均值的标准差倍数,通常Z-score > 3视为离群点。
检测出离群点后,处理方式有四种:删除(若为录入错误)、封顶(替换为边界值)、转为NaN后按缺失值插补、保留(若为合法的极端值)。例如把不可能年龄替换为NaN:
dirty_data['age_clean'] = dirty_data['age'].apply( lambda x: np.nan if (x < 0 or x > 120) else x )3. 近似重复行
John Smith与John Smith(多一个空格)实际上是同一个人。先把名字标准化(str.strip().str.lower()),再用duplicated(subset, keep=False)找出同名组;更精细的做法是用rapidfuzz模糊匹配,定义相似度阈值(如 90%)自动聚类近似重复。处理方式包括:保留首次出现(drop_duplicates(keep='first'))、保留末次出现(keep='last')、聚合多条记录的信息、或标记出来交人工复核。
4. 整合为完整清洗流水线
notebook 最终把上述步骤封装成一个可复用的clean_dataset(df)函数:依次完成类别值标准化(country 映射)、异常年龄置为NaN、去除姓名重复,并在调用前后对比行数、唯一国家数与非法年龄数来验证效果。
最佳实践:始终保留一份原始"脏"数据副本,绝不覆盖源数据文件——清洗后的数据应使用
data_cleaned.csv之类的清晰命名另存。
综合实战:评估一份表单收集的数据
本课的 作业 提供了一个真实感极强的演练场景:客户用一个 小型表单 收集客户基础信息,请你验证所收集数据的质量。作业配套了 表单数据 CSV 与 作业 notebook(notebook 中已包含读取 CSV、以及用value_counts().plot(kind='bar')绘制州与出生月份条形图的代码)。
打开form.csv即可发现典型问题:
birth_month,state,pet January,,Cats JAN,CA,Cats Sept,Hawaii,Dog january,AK,Dog July,RI,Cats September,California,Cats April,CA,Dog January,California,Cats November,FL,Dog December,Florida,Cats- birth_month 列:同一月份存在多种写法(
January/JAN/january、Sept/September),直接做value_counts()条形图时,同一月份会被拆成多个柱子,这正是"可视化看起来不正确"的根源; - state 列:缩写(
CA、AK、RI、FL)与全称(Hawaii、California、Florida)混用,还含有缺失值(第 1 行); - pet 列:存在
Cats与Dog两种取值,表单中使用下拉选择框(见 index.html 中的<select>),是三类字段中约束最严格的。
作业要求你运用本课技巧,针对表单本身提出改进建议,使其能捕获准确且一致的信息——例如:birth_month从自由文本输入改为月份下拉选择(或日期选择器)、state提供州名/州缩写列表而非自由输入、并明确必填项校验,从而从源头避免脏数据。
挑战与延伸学习
课程正文还给出了 notebook.ipynb 作为全部讨论内容的完整载体,其中每个小节后都附有练习题(例如:如何显示超过 5 行?example4.fillna(method='bfill', axis=1)的输出是什么?向含None的int_series赋值后其 dtype 如何变化?),建议逐一尝试。此外,课程也推荐通过 Kaggle 的"数据清洗挑战"系列(日期解析、数据缩放与归一化等)探索本课未覆盖的技巧——数据清洗是高度依赖动手实践的经验型工作。
如果你希望继续深入本系列,可以接着学习 第 15 课:分析数据,该课将进一步讲解探索性数据分析(EDA)如何与数据清洗相互配合。
【免费下载链接】Data-Science-For-Beginners10 Weeks, 20 Lessons, Data Science for All!项目地址: https://gitcode.com/GitHub_Trending/da/Data-Science-For-Beginners
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考