文件处理
在将数据读入 Python 进行分析之前,了解存储数据的文件至关重要。通常我们需要先搞清楚两个基本问题:
- 数据量有多大?
- 源文件的格式是什么?
如果文件过大或格式不符合预期,直接加载到 DataFrame 中可能会失败或出错。
本书主要关注 数据表 (Data Tables) 形式的数据,例如 Pandas DataFrames 和 SQL 关系表。这是因为表格数据有成熟的工具支持,数学基础深厚(类似于矩阵),且在实际应用中非常普遍。
本章概览
本章我们将学习:
- 典型文件格式与编码:重点介绍纯文本文件。
- 文件大小的度量:如何使用 Python 工具或 Shell 命令来查看文件大小。
- 检查源文件:使用 Python 和 Shell 探索文件内容。
- Shell 解释器:介绍如何使用 Shell 命令在 Python 环境之外快速获取文件信息,这在处理大数据时非常高效。
- 数据的形状与粒度:检查行数、列数以及每一行数据的含义。
1. 数据来源示例
为了演示文件处理的概念,我们选择了两个具有代表性的数据集:
1.1 药物滥用预警网络 (DAWN) 调查
DAWN (Drug Abuse Warning Network) 是一项监测全美药物滥用趋势的医疗调查。它通过统计医院急诊室的就诊记录,来评估药物滥用对医疗系统的影响。
- 数据内容:包含药物误用、滥用、意外摄入、自杀企图等各种类型的急诊记录。
- 数据特点:
- 固定宽度格式 (Fixed-width formatting):这是一种特殊的文本格式,通常需要配合代码簿 (Codebook) 才能解读。
- 数据量大:适合用来演示如何处理大文件。
- 粒度特殊:每一行数据代表一次“急诊就诊 (ER visit)”,而不是一个人。单次就诊可能涉及多种药物。
1.2 旧金山餐厅食品安全数据
这是来自 旧金山公共卫生部 的行政管理数据。检查员会突击检查餐厅的食品安全,并根据违规情况打分。
- 数据来源:通过旧金山开放数据平台 (DataSF) 获取。
- 数据特点:
- 多文件结构:包含检查摘要、违规详情、餐厅基本信息等多个文件,各自结构不同。
- 粒度多样:有的文件是按检查记录,有的是按违规项记录。
- 现实意义:这是典型的政府公开数据,包含从严重违规到轻微标牌张贴不当等各种细节。
这两个数据集虽然都是纯文本文件,但格式差异巨大。在接下来的章节中,我们将演示如何通过分析文件格式,正确地将它们读取到 DataFrame 中。
2. 文件格式 (File Formats)
文件格式描述了数据在存储设备上的存储方式。了解文件格式有助于我们将数据正确读入 Python 并转换为数据表。本节主要介绍几种用于存储表格数据的常见纯文本格式。
数据结构 vs 文件格式
- 数据结构是我们对数据的心理表征(如“表格”由行和列组成)。
- 文件格式是数据在文件中的实际存储方式(如 CSV 或 JSON)。同一个表格数据可以存储为多种不同的文件格式。
2.1 分隔符格式 (Delimited Format)
分隔符格式使用特定字符来分隔数据值。
- 常见分隔符:逗号 (CSV)、制表符 (TSV)、空格或冒号。
- 结构:每一行代表一条记录(通常用换行符分隔),行内的值由分隔符隔开。第一行通常包含列名。
旧金山餐厅评分数据就是 CSV 格式的典型例子。
我们可以使用 Python 的 pathlib 库来跨平台处理文件路径并读取内容:
from pathlib import Path
# 创建指向数据文件的跨平台路径
insp_path = Path() / 'data' / 'inspections.csv'
# 读取文件内容的前几行文本
text = insp_path.read_text()
print('\n'.join(text.split('\n')[:5]))
输出示例:
"business_id","score","date","type"
19,"94","20160513","routine"
19,"94","20171211","routine"
可以看到,字段名在第一行,值之间用逗号分隔。
CSV/TSV 不是 Excel 文件
虽然 Excel 可以打开 CSV 文件,但 CSV/TSV 是纯文本格式,而 Excel (.xlsx) 是复杂的二进制或 XML 格式。在 Python 中读取它们需要使用不同的 Pandas 函数。
2.2 固定宽度格式 (Fixed-Width Format)
固定宽度格式 (FWF) 不使用分隔符。相反,特定字段的值出现在每一行的完全相同的位置。
DAWN 调查数据就使用了这种格式。由于没有分隔符,数据看起来像是挤在一起的,我们需要依赖外部文档(代码簿)来知道每一列数据的确切起始位置和长度。
1 2251082 .9426354082 3 4 1 2201141 2 865 105 1102005 1
2 2291292 5.9920106887 911 1 3201134 12077 81 82 283-8
注意看数据主要是在垂直方向上对齐的。
通常,我们可以通过文件扩展名(如 .csv, .tsv, .txt)来推测格式,但这只是建议,并不保证内容格式一定正确。
2.3 其他文本格式
除了上述两种用于表格数据的格式外,还有:
- 层级格式 (Hierarchical Formats):如 JSON, XML, HTML。它们以嵌套的形式存储数据,常用于网络通信和文档存储。
- 松散格式文本 (Loosely Formatted Text):如 Web 日志、仪器读数等。虽然不是标准的表格格式,但通常包含某种模式(如日期在方括号内),可以通过字符串处理提取信息。
[26/Jan/2004:10:47:58 -0800] "GET /stat141/Winter04 HTTP/1.1" 301 328
在本章中,我们将重点关注纯文本格式。需要注意的是,即使是纯文本文件也存在字符编码 (Encoding) 的问题,如果编码指定错误,读出的数据可能会变成乱码。
3. 文件编码 (File Encoding)
计算机将数据存储为位序列(0 和 1)。字符编码 (Character Encoding)(比如 ASCII)告诉计算机如何将这些位转换为文本。例如,在 ASCII 中,位 100 0001 代表字母 A。
- ASCII:最基本的纯文本编码,仅支持标准 ASCII 字符(大小写英文字母、数字、标点符号和空格)。
- UTF-8 / Latin-1:ASCII 无法表示许多特殊字符或其他语言。现代编码如 UTF-8 和 Latin-1 (ISO-8859-1) 支持更多字符。UTF-8 包含超过一百万个字符,并且向后兼容 ASCII。
3.1 确定编码
拿到文本文件时,我们需要弄清楚它的编码。如果使用错误的编码读取,Python 可能会读取到错误的值或直接抛出错误。 最好的方法是查看数据的文档说明,文档通常会明确指出编码格式。
3.2 推测编码
如果不知道编码,我们只能进行猜测。chardet 包有一个 detect() 函数,可以推断文件的编码并给出置信度(0 到 1 之间)。
让我们用它来检查示例文件:
import chardet
from pathlib import Path
# 定义打印格式
line = '{:<25} {:<10} {}'.format
print(line('File Name', 'Encoding', 'Confidence'))
# 遍历 data 目录下的所有文件
for filepath in Path('data').glob('*'):
# 读取原始字节进行检测
result = chardet.detect(filepath.read_bytes())
print(line(str(filepath), result['encoding'], result['confidence']))
输出结果可能如下:
File Name Encoding Confidence
data/inspections.csv ascii 1.0
data/co2_mm_mlo.txt ascii 1.0
...
data/businesses.csv ISO-8859-1 0.73
检测结果显示大多数文件是 ASCII 编码,但 businesses.csv 很可能是 ISO-8859-1 编码。
3.3 处理编码错误
如果我们忽略这一点,尝试直接将 businesses.csv 读入 pandas(默认通常使用 UTF-8),就会遇到问题:
# 错误示范:未指定编码
pd.read_csv('data/businesses.csv')
这将导致 UnicodeDecodeError,提示无法解码某些字节。为了成功读取数据,我们需要在 read_csv 中指定编码:
bus = pd.read_csv('data/businesses.csv', encoding='ISO-8859-1')
bus.head()
这样就能正确读取包含特殊字符的文本了。
文件编码有时比较隐晦,如果没有明确的元数据,就需要像这样进行推测。确定了编码后,我们还需要关注文件的另一个重要方面:文件大小。如果文件太大,我们可能无法将其一次性读入 DataFrame。
4. 文件大小 (File Size)
计算机资源是有限的。如果文件超过了计算机的处理能力,我们可能需要采用不同的策略。
- 小文件:可以直接用文本编辑器、Excel 或 Python 完全读入内存。
- 大文件:可能需要分块读取、使用数据库或分布式计算工具。
4.1 内存 (RAM) vs 磁盘 (Disk)
通常,许多数据文件存储在磁盘上。为了使用 Python 分析数据,我们需要将其读入内存 (RAM)。 关键问题:计算机的磁盘容量通常远大于内存容量(例如 1TB 磁盘 vs 16GB 内存)。这意味着许多存储在磁盘上的文件可能大大超过内存的承载能力。
4.2 文件大小单位
文件大小通常用字节 (Byte) 衡量。对于大文件,我们使用以下前缀:
| 单位 | 符号 | 字节数 (Bytes) | 备注 |
|---|---|---|---|
| Kibibyte | KiB | \(1,024 = 2^{10}\) | 接近千字节 (KB) |
| Mebibyte | MiB | \(1,024^2 = 2^{20}\) | 接近兆字节 (MB) |
| Gibibyte | GiB | \(1,024^3 = 2^{30}\) | 接近吉字节 (GB) |
| Tebibyte | TiB | \(1,024^4 = 2^{40}\) | 接近太字节 (TB) |
| Pebibyte | PiB | \(1,024^5 = 2^{50}\) | 接近拍字节 (PB) |
KiB vs KB
我们使用 KiB/MiB/GiB 这些基于 1024 (\(2^{10}\)) 的单位,因为它们在计算机科学中定义更明确。而 KB/MB/GB 有时指 1000 倍,有时指 1024 倍,容易混淆。
4.3 检查文件大小
在加载数据前,使用 Python 的 os 库检查文件大小是个好习惯:
from pathlib import Path
import os
import numpy as np
kib = 1024
line = '{:<25} {}'.format
print(line('File', 'Size (KiB)'))
for filepath in Path('data').glob('*'):
size = os.path.getsize(filepath)
print(line(str(filepath), np.round(size / kib)))
输出示例:
File Size (KiB)
data/inspections.csv 455.0
data/violations.csv 3639.0
data/DAWN-Data.txt 273531.0
data/businesses.csv 645.0
可以看到 DAWN-Data.txt 约为 270 MB (273,531 KiB),这在现代计算机上通常可以处理,但如果是在较旧的机器上可能会变慢。
内存经验法则
使用 Pandas 读取文件时,所需的内存通常是文件大小的 5 倍。 例如,读取一个 1 GiB 的文件,可能需要 5 GiB 的可用内存。如果你的电脑只有 8 GiB 内存,且运行着浏览器和其他软件,这可能会导致内存溢出。
4.4 处理大数据的策略
当数据量超过内存限制(即所谓的“大数据”场景)时,我们可以采取以下策略:
-
数据子集化 (Subset):
- 只读取其中的一部分列或行。
- 进行随机抽样。
- 缺点:可能会丢失稀有事件的信息。
-
使用数据库系统 (Use a Database):
- 如 SQLite (单机)、MySQL/PostgreSQL (服务器)。
- 数据库设计用于存储和查询超出内存限制的数据。
- 方法:使用 SQL 进行筛选、聚合,将结果(通常较小)读入 Python 进行分析。
-
使用分布式计算系统 (Use Distributed Computing):
- 如 Spark, Ray, MapReduce。
- 将数据和计算任务分割到多台计算机上并行处理。
- 适用于海量数据,但配置和维护成本较高。
除了 Python,我们还可以使用 Shell 命令来快速获取文件信息,这在处理大文件时往往更加高效。我们将在下一节介绍 Shell 工具。
5. Shell 和命令行工具 (The Shell and Command-Line Tools)
几乎所有的操作系统都提供了 shell 解释器(如 Bash, Zsh)。我们可以使用 命令行界面 (CLI) 工具对文件进行操作。
- 优点:语法简洁,执行速度快,适合批量处理和查看大文件。
- 注意:Windows 系统的命令可能不同。本文主要介绍类 Unix 系统(macOS/Linux)的 Bash 命令。Windows 用户可以使用 Git Bash 或 WSL (Windows Subsystem for Linux)。
Jupyter Notebook 支持在命令前加 ! 来直接运行 Shell 命令。
5.1 查看文件列表 (ls)
使用 ls 列出目录内容。常用参数:
* -l: 显示详细信息(权限、所有者、大小、日期)。
* -h: 以人类可读的格式显示文件大小(如 KB, MB)。
# 列出 data 目录下的文件及其大小
$ ls -lh data/
total 556664
-rw-r--r-- 1 nolan staff 267M Dec 10 14:03 DAWN-Data.txt
-rw-r--r-- 1 nolan staff 645K Dec 10 14:01 businesses.csv
-rw-r--r-- 1 nolan staff 3.6M Dec 10 14:01 violations.csv
...
5.2 统计字数和行数 (wc)
wc (word count) 可以快速统计文件的行数、单词数和字符数。这对于了解数据规模非常有帮助。
$ wc data/DAWN-Data.txt
229211 22695570 280095842 data/DAWN-Data.txt
5.3 计算目录总大小 (du)
ls 不能直接显示文件夹的总大小,这时我们需要 du (disk usage)。
-s: 显示总和 (summary)。-h: 人类可读格式。
$ du -sh data/
272M data/
5.4 查看文件内容 (head, tail, cat)
在读取文件前瞥一眼内容非常重要,有助于我们确认格式(如 CSV 表头)。
head -n 5 file.csv: 查看前 5 行。tail -n 5 file.csv: 查看后 5 行。cat file.csv: 打印整个文件(警告:不要对大文件使用!)。
$ head -n 4 data/inspections.csv
"business_id","score","date","type"
19,"94","20160513","routine"
19,"94","20171211","routine"
24,"98","20171101","routine"
5.5 确定文件类型 (file)
file 命令可以帮助检测文件编码和类型:
$ file -I data/*
data/DAWN-Data.txt: text/plain; charset=us-ascii
data/businesses.csv: application/csv; charset=iso-8859-1
businesses.csv 是 ISO-8859-1 编码。
总结
Shell 命令提供了一种编程方式来处理文件,相比于鼠标点击,它更易于记录、复现和自动化,特别是在处理海量数据时。
在将数据读入 DataFrame 后,我们的下一个任务就是弄清楚表格的形状 (Shape) 和粒度 (Granularity)。我们将从找出表格的行数和列数开始,然后深入理解每一行代表的意义,这是检查数据质量的第一步。
6. 表格形状和粒度 (Table Shape and Granularity)
我们将表格的心理表征称为结构(即行和列),用 形状 (Shape) 来量化行数和列数,用 粒度 (Granularity) 来描述表格中每一行代表什么。
6.1 检查形状
确定文件格式后,我们可以加载数据并检查其形状:
# 指定编码读取 businesses.csv
bus = pd.read_csv('data/businesses.csv', encoding='ISO-8859-1')
insp = pd.read_csv("data/inspections.csv")
viol = pd.read_csv("data/violations.csv")
print(" Businesses:", bus.shape, "\t Inspections:", insp.shape,
"\t Violations:", viol.shape)
Businesses: (6406, 9) Inspections: (14222, 4) Violations: (39042, 3)
6.2 确定粒度:餐厅数据
对于 bus (Businesses) 表,我们有 6406 行。每一行代表什么?是通过查看前几行和主键来确定的。
- 观察数据:前两行显示了餐厅的具体信息。
- 验证主键:
business_id看起来是唯一标识符。
print("Number of records:", len(bus))
print("Number of unique business ids:", len(bus['business_id'].unique()))
business_id 确实是主键,每一行代表一家独特的餐厅。
对于 insp (Inspections) 表,行数 (14222) 远多于餐厅数。一个餐厅可能有多次检查。
- 粒度:大致是“一次检查”。
- 主键:通常是
business_id和date的组合。- 注意:如果同一天有两次检查记录,可能需要进一步清洗数据。
对于 viol (Violations) 表,行数更多 (39042)。
- 粒度:一次检查中的一个违规项。一次检查可能发现多个违规行为,因此会对应多行。
6.3 确定粒度:DAWN 调查数据
DAWN 数据是固定宽度格式,我们需要根据代码簿 (Codebook) 指定列位置 (colspecs) 来读取。
# 示例代码:读取固定宽度文件
colspecs = [(0,6), (14,29), (33,35), (35, 37), (37, 39), (1213, 1214)]
varNames = ["id", "wt", "age", "sex", "race","type"]
dawn = pd.read_fwf('data/DAWN-Data.txt', colspecs=colspecs,
header=None, index_col=0, names=varNames)
代码详解:read_fwf
这段代码展示了如何读取固定宽度格式的文件。由于这种文件没有分隔符,我们需要手动指定每列数据的位置范围。
colspecs: 元组列表[(开始, 结束), ...]。例如(0, 6)表示第 0 到 5 位字符是第一列。这些位置通常来自“代码簿”。- 跳过无关数据:注意
colspecs从(37, 39)直接跳到了(1213, 1214)。这是 FWF 的一大优势:我们可以只读取感兴趣的列,完全忽略中间不关心的内容(即第 39 到 1213 字符之间的数据会被直接丢弃),这能极大节省内存。 header=None: 原文件没有列名行,告诉 Pandas 不要把第一行数据当成表头。names=varNames: 手动指定列名。index_col=0: 使用第一列(即id)作为行索引。
- 形状:
(229211, 5),行数与文件行数一致。 - 粒度:每一行代表一次 急诊室就诊 (Emergency Room Visit)。
- 权重 (Weights):由于这是抽样调查,为了反映全国人口情况,每条记录都有一个权重 (
wt)。- 在计算统计量(如平均值、比例)时,必须使用这些权重,否则结果会有偏差。
权重的重要性
例如,计算女性比例。如果不加权,结果可能是 48.0%;加权后,结果可能是 52.3%。加权后的结果更能代表真实的全国人口情况。
总结
在处理文件时,我们不仅要关注如何读取(格式、编码、大小),还要关注读取后的数据结构(形状、粒度)。无论是简单的 CSV 还是复杂的调查数据,理解每一行代表的意义都是正确分析的前提。
7. 本章总结
数据清理是避免分析偏差的关键。本章我们主要关注了数据清理的第一步:从源文件读取数据。
我们回顾了以下关键点:
- 文件格式与编码:学习了如何通过
pd.read_csv和pd.read_fwf读取不同格式的文本文件,并使用encoding参数处理乱码。 - 文件大小:学会利用
os.path.getsize评估文件大小,判断是否能直接读入内存。 - Shell 工具:展示了命令行工具在快速检查大文件时的优势。
- 形状与粒度:强调了理解“数据的一行代表什么”的重要性。
关键检查清单 (Checklist)
在开始分析前,请确保你能回答以下关于数据粒度的问题:
- 这一行代表什么? (Record definition)
- 粒度是否统一? 是否混入了汇总行(如“Total”行)? (Mixed granularity)
- 数据是否已被聚合? 是原始记录还是平均值?聚合后的数据往往波动更小,关系看起来更强。 (Aggregation)
- 不仅是读取:例如 DAWN 数据的粒度是“单次就诊”,这意味着我们可以分析就诊特征,但不能直接推断“患者”特征(因为一人可能多次就诊)。
常用读取函数回顾
| 任务 | 核心函数/方法 | 常用场景 |
|---|---|---|
| 读取 CSV | pd.read_csv(path, encoding='...') |
最通用的表格数据格式 |
| 读取固定宽度 | pd.read_fwf(path, colspecs=...) |
旧式数据或大型调查数据 (如 DAWN) |
| 读取文本 | Path(path).read_text() |
快速查看文件内容或非结构化文本 |
| 读取字节 | Path(path).read_bytes() |
用于检测编码 (chardet.detect) |
弄清楚了文件的物理属性(格式、编码、大小)和逻辑属性(形状、粒度),我们就为数据分析打下了坚实的基础。
下一章,我们将讨论如何 评估和提升数据质量。