☰
数据清洗数据源实战:从脏数据到可建模数据的完整指南
2026/10/14 6:05:56 网站建设 项目流程

简介:面向大数据应用人才的数据清洗练习数据源,汇集课程信息、租房信息、学生信息等多类典型业务场景下的原始文件,覆盖数据质量评估、缺失值填充、异常值识别、一致性检查及格式转换等关键步骤,适合初学者入门,也便于有一定基础的学习者系统梳理清洗流程。压缩包共11个文件,含3个SQL脚本、2个CSV与2个TXT文本、1个XLS和1个XLSX表格,另有JSON与XML数据各1个,覆盖关系型数据库、表格、纯文本及半结构化数据多种形态,整体约96KB,便于快速下载和课堂分发。目前已有528人学习使用。文件除提供课程、学生、租房等可直接操作的数据集外,还包含SQL建表脚本及Kettle、TxtExtract相关素材,适合配合Pandas、dplyr及ETL工具进行实操,从中练习关系表创建、多源导入、缺失值补全、异常值修正与重复项剔除。通过跨格式数据间的对比与整合,能够直观理解清洗规则对后续分析和建模质量的影响,为真实业务中的数据预处理积累实战经验。

1. 数据清洗数据源.zip:一个被低估的脏数据起点

做数据分析和建模的人迟早会遇到一个尴尬时刻:手里有一批数据源文件,可能是导出的 CSV、接口拉下来的 JSON、别人打包发来的 Excel,但打开一看全是“惊喜”——字段对不上、日期格式五花八门、缺失值用不同符号标记、同一列里混着数值和文本。大部分人在这一步就开始手工改 Excel,改到一半发现数据量根本不允许,最后要么放弃这批数据,要么硬着头皮把脏数据喂进模型,然后被结果坑得怀疑人生。这个数据清洗数据源.zip,本质上就是把这类“脏数据样本 + 清洗思路 + 可复用脚本”打成一个包,让你在动手清洗之前先看清楚问题长什么样、常见的坑埋在哪里、以及一套不用从零摸索的清洗路径。它不是某个特定项目的产物,而是一个面向数据准备环节的通用起点,适合正在做数据分析、特征工程、机器学习建模的从业者和想系统学习数据清洗的新手。别小看这个压缩包,数据清洗往往占整个数据项目 60% 以上的时间,而这里面的数据源,就是让你提前踩完坑的演练场。

2. 数据清洗数据源包里的典型构成:为什么需要为清洗单独建一个数据源

2.1 清洗数据源和普通数据集的本质区别

很多人在网上下过各种数据集,下载下来之后发现字段干净、类型统一、注释齐全,基本不需要清洗就能直接跑模型。但真实业务里的数据源完全不是这个状态。真实的数据源往往是从多个业务系统里导出来的原始记录,带着各自的存储习惯和导出格式,甚至还会有导出时的截断、乱码、字段错位这类问题。数据清洗数据源.zip 里的文件,模拟的正是这种“真实脏数据”的形态,而不是那种已经处理好的干净数据集。

我一般会把这个包里的数据按脏的程度分成三个层次:第一层是结构性问题,比如列名不一致、文件编码混乱、同一份数据里混着多种日期格式;第二层是内容性问题,比如缺失值用空字符串、NULL、'N/A'、'unknown' 多种方式表示,数值列里混入文本注释,分组字段出现大小写不一致;第三层是逻辑性问题,比如订单金额出现负数、年龄字段出现 200 这种明显越界的值、外键关联不上的孤立记录。能意识到“清洗数据源”和“干净数据集”是两种完全不同的资源,是做好清洗工作的第一步,也是这个 zip 包最大的价值所在。

2.2 一个能直接落地的数据源目录结构与选型理由

拿到这个 zip 包之后,你大概率会看到类似下面的目录组织方式。这个结构不是随便拍的,而是按照“原始层 → 中间层 → 结果层”的思路设计的,方便你在清洗过程中随时回溯每一步的改动。我自己的项目里也一直沿袭这个约定,清洗不是一次性操作,而是要能回去查到底改了什么、为什么改。

data/ ├── raw/ # 原始数据,永远不改动 │ ├── customers.csv # 客户信息,含缺失与重复 │ ├── orders.json # 订单数据,嵌套结构 │ └── logs/ # 日志文件,多格式混合 ├── interim/ # 清洗中间结果 │ ├── customers_cleaned.csv │ └── orders_flattened.csv ├── processed/ # 最终建模可用数据 │ └── master_dataset.csv ├── scripts/ # 清洗脚本和规则配置 │ ├── clean_customers.py │ ├── clean_orders.py │ └── validate.py └── README.md # 数据字典和清洗说明

这个目录结构的核心逻辑是分层隔离。raw 目录里的文件一旦放进去就不要再手动改动,所有清洗动作都通过脚本生成新文件到 interim 和 processed 目录,这样就算清洗规则写错了,也不会破坏原始数据,重新跑一遍脚本就行。README.md 在这里不是摆设,它是数据字典,记录每个字段的业务含义、取值范围、缺失率统计信息,是后续写清洗规则的依据。

在动手处理数据源之前,强烈建议先把 README 里的字段说明和实际文件里的字段做一遍交叉核对。我在某公司做一个跨平台系统数据整合时,就吃过这个亏——文档里写着用户 ID 是纯数字,结果实际数据里有 30% 的记录带了前缀字母,导致第一版清洗脚本处理到一半就报错。先核对再动手,能省掉大量返工时间。

3. 把脏数据洗出可建模口径:清洗流程与脚本拆解

3.1 缺失值与默认值的处理:先摸底再动手

处理缺失值最忌讳的就是上手就 fillna,因为你根本不知道缺失值在原始数据源里长什么样。在清洗数据源里,缺失值通常有 N 种伪装:空字符串、空格字符串、全角空格、'NULL'、'N/A'、'NaN'、'-'、'unknown'。如果不先把这些统一识别出来,fillna 只会制造更多脏数据。

import pandas as pd import numpy as np # 读取原始数据源,注意先保持 dtype 为 str,避免类型转换掩盖问题 df = pd.read_csv('data/raw/customers.csv', dtype=str, keep_default_na=False) # 自定义缺失值识别集合,覆盖常见伪装 missing_values = ['', ' ', 'NULL', 'N/A', 'NA', 'NaN', '-', 'unknown', 'Unknown', 'null'] # 对每一列统计缺失率 for col in df.columns: miss_mask = df[col].isin(missing_values) | df[col].isna() miss_rate = miss_mask.sum() / len(df) print(f'{col}: missing rate = {miss_rate:.2%}')

这段代码有两个关键设定。dtype=str 是为了在读入阶段把所有字段都当作字符串处理,防止 pandas 提前把某些空值标记为 NaN 或者把带特殊字符的数字列转成 float,掩盖原始问题。keep_default_na=False 则是让 pandas 不要自作主张把空字符串自动识别为 NaN,这样才能看清数据源里缺失值到底以什么形式存在。跑完统计之后,再根据每一列的缺失率和业务含义决定策略:超过 70% 缺失的列直接丢弃,低于 5% 的可以直接填充或删除对应行,中间的就需要单独评估。

3.2 重复数据的识别:完全重复与关键字段重复要区别对待

重复数据在数据源包里几乎必然出现,但重复的定义本身就有讲究。完全重复是指所有字段都相同,这种数据通常是导出时的重复记录,直接去重即可。但更隐蔽的是“关键字段重复”——比如同一个客户 ID 出现多条记录,但邮箱、电话、地址字段稍有不同,这种不是简单的重复,而是数据合并时产生的多版本记录,需要根据业务规则决定保留哪一条。

# 完全重复检测:所有列都相同 duplicate_rows = df[df.duplicated(keep=False)] print(f'完全重复记录数: {len(duplicate_rows)}') # 按关键字段检测重复:客户 ID 相同即视为重复 key_duplicates = df[df.duplicated(subset=['customer_id'], keep=False)] key_duplicates_sorted = key_duplicates.sort_values('customer_id') print(f'关键字段重复记录数: {len(key_duplicates_sorted)}') # 保留策略:按更新时间保留最新的一条 df_dedup = (df.sort_values('updated_at', ascending=False) .drop_duplicates(subset=['customer_id'], keep='first'))

这里的关键参数是 subset。当只传入['customer_id']时,pandas 会只关注这一列是否重复,而不管其他字段的差异。实际清洗时往往需要组合多个字段来定义重复,比如['customer_id', 'source'],因为同一个 ID 可能来自不同渠道,需要保留不同渠道的信息。排序之后再 drop_duplicates 的做法,本质上是在告诉脚本“哪条记录优先级更高”。

3.3 用脚本固化清洗规则,让清洗结果可复现

手工清洗最大的问题不是慢,而是不可复现。上一次你在 Excel 里改了 200 个格子的数据,下一次拿到更新的数据源,你根本记不清当时改了哪些。脚本化的好处就是把每次清洗动作变成一段可执行、可审查、可重跑的代码。下面这段是我处理这个数据源时最常用的清洗骨架,它把数据读取、缺失值处理、去重、类型修正四个阶段串起来。

import pandas as pd import numpy as np from pathlib import Path RAW_DIR = Path('data/raw') PROCESSED_DIR = Path('data/processed') PROCESSED_DIR.mkdir(exist_ok=True) # 第一阶段:读取并对齐列名 df = pd.read_csv(RAW_DIR / 'customers.csv', dtype=str, keep_default_na=False) df.columns = [col.strip().lower().replace(' ', '_') for col in df.columns] # 第二阶段:统一缺失值标记 na_set = {'', ' ', 'NULL', 'N/A', 'NA', 'NaN', '-', 'unknown', 'Unknown'} df = df.replace(na_set, np.nan) # 第三阶段:去重 df = df.drop_duplicates(subset=['customer_id'], keep='last') # 第四阶段:字段类型修正 df['age'] = pd.to_numeric(df['age'], errors='coerce') df['signup_date'] = pd.to_datetime(df['signup_date'], errors='coerce') df['amount'] = pd.to_numeric(df['amount'].str.replace('¥', '').str.replace(',', ''), errors='coerce') # 输出清洗结果 df.to_csv(PROCESSED_DIR / 'customers_cleaned.csv', index=False, encoding='utf-8-sig')

这段脚本值得细看的有三个地方。列名清洗用了 strip().lower().replace(' ', '_'),数据源导出的列名经常带着大小写混乱和首尾空格,这一步能避免后面所有引用列名的代码踩坑。replace(na_set, np.nan) 用字典式替换把多种缺失值形态统一成 pandas 的 NaN,这一步之后再处理缺失值就不用分情况写了。金额字段的清理用了链式操作,先去货币符号再去千位分隔符再转数值,顺序不能颠倒,否则会出现转换报错。errors='coerce' 参数特别重要,它让无法转换的值统一变成 NaN,而不是中断整个脚本。

4. 数据源里常见的脏数据形态与处理边界

4.1 编码与格式陷阱:日志文件里藏着大量半结构化数据

数据源包里往往不只包含规整的表格文件,还可能带有日志类文件,比如订单系统的访问日志、支付回调日志、爬虫抓取页面时保存的 HTML 片段。这类文件通常不是标准的 CSV 或 JSON,而是每行一个记录、字段之间用空白或竖线分隔的半结构化数据,而且编码经常不一致。有次我处理一个日志文件,打开一看中文全部变成乱码,后来才发现文件是 GBK 编码,而 pandas 默认按 UTF-8 读取。

# 处理编码问题的兜底方案:先探测再读取 import chardet with open('data/raw/logs/payment.log', 'rb') as f: raw_data = f.read() result = chardet.detect(raw_data) print(result) # 输出示例: {'encoding': 'GB2312', 'confidence': 0.99} # 按实际编码读取 df_log = pd.read_csv( 'data/raw/logs/payment.log', encoding=result['encoding'], sep='|', # 按分隔符切分 names=['time', 'order_id', 'amount', 'status'], header=None )

这段代码里的两个参数值得说明。sep='|' 用于处理非逗号分隔的日志文件,具体用什么分隔符要先用文本编辑器打开原始文件看一眼,常见的有竖线、制表符和多个空格。header=None 配合 names 参数,是因为日志文件通常没有列头,直接指定列名更省事。编码探测不是银弹,有些文件是混合编码的,探测结果只能覆盖大部分内容,遇到探测置信度低的情况,我会直接用 UTF-8 加 errors='ignore' 读取,能保留多少算多少,损坏太严重的行直接丢弃。

4.2 数值字段与时间字段的边界:类型转换失败不等于数据该删

数据清洗里最常翻车的场景之一是类型转换。年龄列里有字符串 '25岁',金额列里有 '1,234.56',日期列里有 '2023/13/45',这些在肉眼看来各有各的不对,但在脚本层面,它们的表现形式是同一个:转换失败。遇到了就删掉是很省事,但往往会误删有效信息,因为转换失败的原因可能是数据自身错误,也可能是你给定的格式不匹配。

# 时间字段的容错解析:尝试多种常见格式 date_formats = ['%Y/%m/%d', '%Y-%m-%d', '%d/%m/%Y', '%Y%m%d'] def parse_flexible_date(series): for fmt in date_formats: converted = pd.to_datetime(series, format=fmt, errors='coerce') if converted.notna().sum() > series.notna().sum() * 0.8: return converted return pd.to_datetime(series, errors='coerce') df['order_date'] = parse_flexible_date(df['order_date'])

这个函数的核心逻辑是“试探性解析”:按顺序尝试不同的日期格式,哪一种格式能成功解析超过 80% 的有效值,就采用哪一种。设置 80% 这个阈值的用意是容忍少量个别的脏数据存在,而不是因为一个格式解析率低就彻底放弃那条解析路径。实战中的日期数据通常不会统一成一种格式,这个函数比写死一种格式稳妥得多。

数值字段的边界问题也很典型。某开发者处理过一个订单数据源,金额列里混着 '免费'、'待定' 这类非数值文本,直接 to_numeric 会把它们全转成 NaN。但这时候不应该急着删除,而是应该看这些非数值文本的分布——如果某一个月突然出现大量 '待定',那可能不是数据错误,而是系统当时上线了一个新功能,产生的数据本身就是文本,需要业务侧确认口径。

5. 数据清洗数据源实战避坑:5 个高频翻车点与排查路径

5.1 翻车点一:清洗后行数突然暴增或锐减

现象:跑完清洗流程,发现最终数据比原始数据多了几万行,或者少了几千行,一看就是脚本逻辑有漏洞。原因:最常见的是去重逻辑写反了。drop_duplicates 里 subset 参数没指定,导致只按整行去重,同一个订单 ID 有多条不同状态记录时全部保留;或者去重时用 keep='last' 却忘记先排序,导致保留的是旧数据而非新数据,之后又被其他步骤误用。解决:清洗脚本跑完之后,立刻对行数做三层校验——原始行数、去重后行数、最终输出行数,任何一层出现异常都回头检查参数。特别要确认 subset 字段是否真的有唯一性约束,没有业务依据就别乱去重。

5.2 翻车点二:缺失值填充导致分布偏移

现象:某列原本缺失率是 40%,用均值填充后,模型训练效果反而变差,特征重要性也乱了。原因:缺失率高的列,缺失本身可能包含信息——比如用户没填年龄,可能说明该用户来自某个特定渠道。用均值填充相当于把这种信息抹掉了,还让分布变得更尖峰。解决:缺失率低于 5% 的列可以直接删除对应行或填充均值;5% 到 30% 的列,先用一个哑变量标记是否缺失,再把缺失值填为均值或中位数;超过 30% 的列,先评估业务含义,信息量低的直接不要,信息量高要单独做分箱处理。数据源包里的 customers 表就有这样的例子,值得刻意做一次对比实验验证差异。

5.3 翻车点三:日期时间字段的时区陷阱

现象:订单数据里时间字段看起来都正常,但和另一张表做关联时,发现同一个订单的创建时间和支付时间差了 8 小时。原因:两个表的数据源一个存的是 UTC 时间,一个存的是本地时间,导入时没有做时区统一。解决:在清洗脚本的早期阶段就对所有时间字段做统一处理,指定时区后统一转换为本地时间或 UTC,之后所有表中不会再出现混用。操作上可以先把时间字段读取为带时区的时间戳,再转换到目标时区,最后去掉时区信息。清洗数据源包里如果有跨地区的订单或日志数据,这一步尤其容易踩到。

5.4 翻车点四:字符串字段里的隐藏字符

现象:两个字段明明看起来一模一样,用 merge 却匹配不上,仔细一看一个是全角冒号,一个是半角冒号;或者某个字符串列首尾有不可见的换行符。原因:数据从不同渠道导过来时,格式化方式不同,Excel 可能会把文本里嵌的换行符保留下来,爬虫导出的数据可能带着 HTML 转义字符。解决:对文本列做统一的字符规整——去除首尾空白、全角转半角、去除零宽字符,再重新做匹配。不要小看这类问题,它们造成的匹配失败往往最难排查,我曾经在一个模拟项目X里排查了整整一天,最后发现是用了两个长得一模一样的破折号字符。

5.5 翻车点五:清洗脚本运行后的结果没有做正确性校验

现象:清洗流程跑通,数据也写出来了,但下游模型训练时发现特征分布和业务直觉完全不一致,回查清洗脚本才发现某个字段的过滤条件写反了。原因:清洗脚本本身没问题,但缺少一个独立的校验环节,导致逻辑错误在清洗阶段没有被发现。解决:花时间在清洗脚本之外,单独写一份校验脚本,对比清洗前后各字段的行数、唯一值数量、缺失率、数值分布关键分位数(比如 min、max、mean、四分位),差异超过预期就触发警告。这个习惯在清洗数据源练习阶段就必须养成,后面处理真实生产数据时能少掉很多头发。

6. 把清洗数据源变成可复用资产:验证方法与管道化进阶

清洗工作做到能跑通脚本只是及格线,离可复用还有一段距离。我一般会在清洗完第一批数据后做两个动作:一个是把校验脚本固化成自动检查流程,另一个是把清洗脚本的参数抽出来做成配置文件,让同一套代码能适配不同批次的数据源。

# 用配置文件驱动清洗流程,同一份代码适配不同数据源 import json from pathlib import Path # config.json 示例 # { # "source_file": "data/raw/customers.csv", # "output_file": "data/processed/customers_cleaned.csv", # "key_fields": ["customer_id"], # "date_fields": ["signup_date"], # "numeric_fields": ["amount"], # "text_fields": ["address", "email"] # } config = json.load(open('config.json', encoding='utf-8')) df = pd.read_csv(config['source_file'], dtype=str, keep_default_na=False) for col in config['date_fields']: df[col] = parse_flexible_date(df[col]) for col in config['numeric_fields']: df[col] = pd.to_numeric(df[col], errors='coerce') df = df.drop_duplicates(subset=config['key_fields'], keep='last') df.to_csv(config['output_file'], index=False, encoding='utf-8-sig')

这段代码的意义在于把“数据源长什么样”和“怎么清洗”分开。清洗逻辑沉淀成通用代码后,换一个新的数据源只需要改动 config.json 里的文件路径和字段配置,不需要再改一遍脚本。做数据工程的朋友一定会认可这种做法,数据源会源源不断地更新,写一次脚本覆盖全部批次,好过每次拿到新数据就重写一次。

最后分享一个我自己踩过无数次坑之后养成的习惯:数据清洗完永远不要把结果直接覆盖到 raw 目录下的原始文件里,宁可让磁盘多占一点空间,也要保留清洗前的原始副本,并且最好用日期给输出文件加个后缀——customers_cleaned_20250101.csv。这不是胆小,是后悔药。某次我自信满满地直接覆盖了原来的 CSV,后来发现清洗规则里少处理了一种特殊字符,想回退到原始数据重新处理,结果发现原始文件已经被改得面目全非,只能重新向业务方申请数据,白白耽误了两天时间。清洗数据源这个东西,看起来只是跟一堆不听话的文本格式较劲,但背后真正考验的是严谨度和流程管理能力。希望这些经验能帮你在清洗这条路上少走点弯路,把更多时间留给建模本身。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询