跳转至

文件处理

在将数据读入 Python 进行分析之前,了解存储数据的文件至关重要。通常我们需要先搞清楚两个基本问题:

  1. 数据量有多大?
  2. 源文件的格式是什么?

如果文件过大或格式不符合预期,直接加载到 DataFrame 中可能会失败或出错。

本书主要关注 数据表 (Data Tables) 形式的数据,例如 Pandas DataFrames 和 SQL 关系表。这是因为表格数据有成熟的工具支持,数学基础深厚(类似于矩阵),且在实际应用中非常普遍。

本章概览

本章我们将学习:

  1. 典型文件格式与编码:重点介绍纯文本文件。
  2. 文件大小的度量:如何使用 Python 工具或 Shell 命令来查看文件大小。
  3. 检查源文件:使用 Python 和 Shell 探索文件内容。
  4. Shell 解释器:介绍如何使用 Shell 命令在 Python 环境之外快速获取文件信息,这在处理大数据时非常高效。
  5. 数据的形状与粒度:检查行数、列数以及每一行数据的含义。

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-8Latin-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 处理大数据的策略

当数据量超过内存限制(即所谓的“大数据”场景)时,我们可以采取以下策略:

  1. 数据子集化 (Subset)

    • 只读取其中的一部分列或行。
    • 进行随机抽样。
    • 缺点:可能会丢失稀有事件的信息。
  2. 使用数据库系统 (Use a Database)

    • 如 SQLite (单机)、MySQL/PostgreSQL (服务器)。
    • 数据库设计用于存储和查询超出内存限制的数据。
    • 方法:使用 SQL 进行筛选、聚合,将结果(通常较小)读入 Python 进行分析。
  3. 使用分布式计算系统 (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
这表明文件有约 22.9 万行和 2.8 亿个字符。

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 行。每一行代表什么?是通过查看前几行和主键来确定的。

  1. 观察数据:前两行显示了餐厅的具体信息。
  2. 验证主键business_id 看起来是唯一标识符。

print("Number of records:", len(bus))
print("Number of unique business ids:", len(bus['business_id'].unique()))
如果两者相等(都是 6406),则说明 business_id 确实是主键,每一行代表一家独特的餐厅。

对于 insp (Inspections) 表,行数 (14222) 远多于餐厅数。一个餐厅可能有多次检查。

  • 粒度:大致是“一次检查”。
  • 主键:通常是 business_iddate 的组合。
    • 注意:如果同一天有两次检查记录,可能需要进一步清洗数据。

对于 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. 本章总结

数据清理是避免分析偏差的关键。本章我们主要关注了数据清理的第一步:从源文件读取数据

我们回顾了以下关键点:

  1. 文件格式与编码:学习了如何通过 pd.read_csvpd.read_fwf 读取不同格式的文本文件,并使用 encoding 参数处理乱码。
  2. 文件大小:学会利用 os.path.getsize 评估文件大小,判断是否能直接读入内存。
  3. Shell 工具:展示了命令行工具在快速检查大文件时的优势。
  4. 形状与粒度:强调了理解“数据的一行代表什么”的重要性。

关键检查清单 (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)

弄清楚了文件的物理属性(格式、编码、大小)和逻辑属性(形状、粒度),我们就为数据分析打下了坚实的基础。

下一章,我们将讨论如何 评估和提升数据质量