☰
Python数据分析实战:配套数据集读取管线与脏数据避坑指南
2026/10/30 5:06:11 网站建设 项目流程

简介:这份资源是《Python for Data Analysis, 2nd Edition》配套数据集,面向正在学习 pandas 与 Python 数据分析的读者,尤其是希望对照书中示例动手练习的数据科学初学者与分析师,可解决缺少真实数据、无法复现书中案例的问题。压缩包共 209 个文件,约 40.36MB,以 txt 文本数据、csv 表格数据为主,另含 ipynb 笔记本、py 脚本、json、pkl、xlsx 等练习素材,以及 shp、dbf 等地理数据文件和少量 xml、xsd 元数据,覆盖书中数据导入导出、清洗、合并分组、时间序列等章节所需样本。已有 350 人学习下载。借助这些数据,读者可完整复现书中示例,练习缺失值与重复值处理、merge 与 groupby 操作、时间序列分析及 Matplotlib 可视化,并在真实数据上理解 pandas 的性能优化思路,适合作为边学边练的配套素材。

1. 拿到一份“书配套数据集”,先别急着 pd.read_csv

很多人第一次接触 Python for Data Analysis, 2nd Edition 的配套数据集,是在某个压缩包里看到一堆 csv、txt、json 文件,然后下意识地pd.read_csv一把梭。结果要么编码报错,要么日期列全是 object,要么某个文件读进来只有一列。这套数据集的价值不在于“文件多”,而在于它几乎覆盖了 pandas 日常会遇到的典型脏数据形态:缺失值、混合类型、不规则分隔、时间序列、多表关联。它适合已经会写几行 pandas、但一碰到真实数据就翻车的人。下面按“先认清数据长什么样,再搭读取管线,最后处理坑”的顺序讲,每一步都能直接抄。

2. 先摸清数据集结构:文件清单、编码与分隔符的判定方法

2.1 用脚本生成文件清单,而不是靠肉眼翻目录

拿到数据集第一件事不是读内容,是建立一份可复查的清单。我一般会写一个小脚本,把每个文件的路径、大小、前两行原始字节打出来。这样做的原因是:同名文件可能在不同子目录里格式不同,靠资源管理器翻很容易漏。

import os from pathlib import Path root = Path("./pydata-book-datasets") # 换成你的数据集根目录 for p in sorted(root.rglob("*")): if p.is_file(): size_kb = p.stat().st_size / 1024 # 只读前 200 字节,避免大文件拖慢 with open(p, "rb") as f: head = f.read(200) print(f"{p.relative_to(root)}\t{size_kb:.1f}KB\t{head[:80]!r}")

这段脚本的关键在rb模式和head[:80]!r。用二进制读能保留原始字节,!r会把换行、制表符、BOM 头都显式打出来。如果你看到开头是b'\xef\xbb\xbf',说明是 UTF-8 with BOM,后面pd.read_csv要加encoding='utf-8-sig';如果看到b'\xff\xfe',那是 UTF-16,得单独处理。参数上rglob("*")会递归子目录,数据集里常见的ch02、ch05这类章节目录都能覆盖到。

2.2 编码与分隔符的快速判定表

光看字节头还不够,实际读取时还要确认分隔符。下面这张表是我处理这类数据集时常用的判定逻辑,按优先级从上往下试:

现象大概率原因处理方式
读进来只有一列,列名带\t分隔符是制表符sep='\t'
首行出现乱码UTF-8 BOMencoding='utf-8-sig'
报UnicodeDecodeError非 UTF-8 编码试encoding='latin-1'或gbk
某列全是 NaN缺失值标记不是空na_values=['NA','-','?']
日期列是 object未解析日期parse_dates=['列名']

这张表不是让你背,是让你在报错时有个排查顺序。我见过太多人一报编码错就去搜“pandas 编码大全”,其实先看字节头就能定位大半。

2.3 用 pandas 做一次“结构体检”

清单有了,接下来对每个 csv 做一次轻量体检:读前 5 行、看 dtypes、看缺失率。不要一上来就全量读,数据集里有些文件几十万行,全量读一遍调格式很浪费时间。

import pandas as pd def inspect_csv(path, **kwargs): df = pd.read_csv(path, nrows=5, **kwargs) # 只读 5 行探结构 print(f"== {path}") print("列名:", list(df.columns)) print("dtypes:\n", df.dtypes) # 全量读一次算缺失率,文件大时改成 chunksize full = pd.read_csv(path, **kwargs) miss = full.isna().mean().sort_values(ascending=False) print("缺失率 top5:\n", miss.head()) return full df = inspect_csv("ch05/example.csv", sep=",", encoding="utf-8")

nrows=5是探结构的关键参数,它让你在不付出全量读取代价的前提下看到列名和推断类型。注意dtypes在只读 5 行时可能推断不准(比如前 5 行都是整数,第 100 行出现字符串),所以体检之后还要用全量读的full.dtypes复核。缺失率排序能帮你快速判断哪些列需要重点处理,通常缺失率超过 30% 的列要考虑是否直接丢弃。

3. 搭一条可复用的读取管线:从单文件到多表关联

3.1 把读取参数收敛到一个配置字典

数据集里文件多了以后,最怕的是每个文件都手写一遍read_csv参数,改一个编码要翻十个地方。我的做法是建一个FILE_CONFIG字典,把每个文件的sep、encoding、parse_dates、na_values集中管理。

FILE_CONFIG = { "ch02/sales.csv": { "sep": ",", "encoding": "utf-8", "parse_dates": ["order_date"], "na_values": ["NA", "-"], }, "ch05/log.tsv": { "sep": "\t", "encoding": "utf-8-sig", "usecols": ["ts", "level", "msg"], # 只取需要的列 }, } def load(name): cfg = FILE_CONFIG[name] return pd.read_csv(name, **cfg) sales = load("ch02/sales.csv")

这个字典的好处是可审计:新人接手时一眼能看到每个文件的读取假设。usecols是个容易被忽略的参数,日志类文件往往有十几列,但你分析只需要三列,限定列能省内存也能避免某列类型推断失败导致整表读取报错。parse_dates直接传列名列表,pandas 会在读取时完成转换,比读完再pd.to_datetime少一次全表扫描。

3.2 多表关联前先统一键的类型

这套数据集里经常出现“订单表 + 用户表”这种需要 merge 的结构。血泪经验是:merge 之前一定先检查两边的键类型是否一致。常见翻车是左边user_id是 int64,右边因为含缺失值变成了 float64,merge 结果全空还不报错。

orders = load("ch02/sales.csv") users = load("ch02/users.csv") # 先对齐键类型,再 merge orders["user_id"] = orders["user_id"].astype("Int64") # 可空整型 users["user_id"] = users["user_id"].astype("Int64") merged = orders.merge(users, on="user_id", how="left", validate="many_to_one") print(merged.shape, merged["user_id"].isna().sum())

这里用Int64(大写 I)而不是int64,是因为 pandas 的可空整型能同时容纳整数和 NA,避免因缺失值被迫转 float。validate="many_to_one"是个后悔药参数:如果右表键有重复,它会直接抛错而不是悄悄产生笛卡尔积。很多人 merge 后行数暴涨却不知道原因,加这个参数能第一时间暴露问题。

3.3 分块读取大文件并做聚合

数据集里如果有超过内存的文件,别硬扛。chunksize分块读取配合聚合,是处理这类文件的常规做法。

def chunked_agg(path, chunk_size=100_000): results = [] for chunk in pd.read_csv(path, chunksize=chunk_size, sep=","): # 每块内先聚合,减少内存占用 agg = chunk.groupby("category")["amount"].sum() results.append(agg) # 合并各块结果再聚合一次 return pd.concat(results).groupby(level=0).sum() total = chunked_agg("ch05/large_sales.csv")

chunksize的选择没有固定值,一般让单块占内存的 1/5 到 1/10 比较稳。这段代码的关键是“块内先聚合、块间再合并”,如果你把每块都 append 到列表最后 concat 全量,那分块就失去意义了。注意groupby(level=0)是因为 concat 后索引是 category,按索引层级聚合。

4. 避坑:这套数据集最容易翻车的五个地方

4.1 日期列读成 object,后续时间序列全废

现象:df['date'].dtype是 object,做resample或时间切片时报TypeError。原因:原始日期格式不统一,比如混了2020-01-01和01/02/2020,pandas 推断失败就退回 object。解决:读取时用parse_dates指定列,格式混乱时先读成字符串再用pd.to_datetime(..., format='mixed', errors='coerce'),把解析失败的置为 NaT 后单独检查。

4.2 缺失值标记没识别,统计结果偏大

现象:某数值列mean()结果异常大。原因:缺失值用-999或?标记,pandas 当成正常数值参与计算。解决:读取时na_values=['-999','?','NA'],读完再用df.describe()复核 min/max 是否合理。这个坑在传感器类数据里特别常见。

4.3 merge 后行数暴涨

现象:两个表 merge 后行数远大于左表。原因:右表关联键有重复,产生多对多笛卡尔积。解决:merge 前对右表做duplicated(subset='key').sum()检查,merge 时加validate='many_to_one'让问题显式报错。

4.4 编码猜错导致中文列名乱码

现象:列名显示为日期这类乱码。原因:文件是 UTF-8,但读取时用了默认或错误编码。解决:按 2.1 的字节头判定,UTF-8 无 BOM 用utf-8,有 BOM 用utf-8-sig。已经读错的可以df.columns = df.columns.str.encode('latin-1').str.decode('utf-8')抢救,但不如一开始读对。

4.5 用inplace=True链式操作,结果没生效

现象:df.dropna(inplace=True)之后df还是原样。原因:对切片或链式结果用了 inplace,操作落在临时对象上。解决:统一用赋值式df = df.dropna(),避免 inplace 和链式索引混用。这是 pandas 里最玄学的一类问题,养成赋值习惯能省很多排查时间。

5. 进阶:把数据集变成可复现的分析基线

5.1 用 pytest 固化读取假设

数据集用久了,最怕某天换了个版本,列名或编码变了却没人发现。我的习惯是给关键文件写几条断言,当成回归测试。

import pytest def test_sales_schema(): df = load("ch02/sales.csv") assert {"order_date", "user_id", "amount"}.issubset(df.columns) assert df["order_date"].dtype.kind == "M" # 必须是 datetime assert df["amount"].notna().mean() > 0.9 # 缺失率不能过高

dtype.kind == "M"是判断 datetime 的稳定写法,比字符串比较 dtype 名可靠。缺失率阈值 0.9 是我根据这类数据集的经验设的,你可以按实际调整。这几条断言跑起来不到一秒,但能在数据替换时第一时间报警。

5.2 把清洗步骤写成函数链,而不是一坨脚本

分析脚本写长了容易变成“从上到下三百行”,改一步要重跑全部。我一般把清洗拆成小函数,每个函数输入输出都是 DataFrame,最后用管道串起来。

def drop_high_missing(df, threshold=0.3): keep = df.columns[df.isna().mean() < threshold] return df[keep] def fill_numeric(df): num_cols = df.select_dtypes("number").columns return df.assign(**{c: df[c].fillna(df[c].median()) for c in num_cols}) cleaned = (load("ch02/sales.csv") .pipe(drop_high_missing) .pipe(fill_numeric))

pipe的好处是每一步都能单独测试,出问题能快速定位是哪一环。assign(**{...})的写法避免了对原表逐列赋值时的 SettingWithCopyWarning。阈值 0.3 和中位数填充都是可调参数,真实项目里要根据业务含义决定,比如金额列用中位数填充未必合理,可能更适合保留缺失。

5.3 一个具体技巧:用dtype显式声明减少读取后的类型修复

最后一个技巧是我踩坑后养成的习惯:对已知类型的列,读取时直接用dtype声明,而不是读完再 astype。这样能避免 pandas 推断错误,也能在读取阶段就暴露脏值。

dtypes = {"user_id": "Int64", "amount": "float64", "category": "category"} df = pd.read_csv("ch02/sales.csv", dtype=dtypes, parse_dates=["order_date"])

category类型对低基数列(比如分类名)能显著省内存,但注意不要在需要频繁新增类别的列上用。Int64可空整型前面提过,这里再强调一次:只要键列可能有缺失,就用它。这套组合下来,读取阶段就把大部分类型问题挡在门外,后面分析脚本能干净很多。

我自己的教训是:早期总想着“先读进来再说”,结果每个分析脚本开头都堆了十几行类型修复代码,改一个文件要同步改好几处。后来强迫自己把读取配置和清洗函数分开管理,数据集再换版本,改的也只是配置字典那几行。希望帮到你。

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

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

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

立即咨询