【精选优质专栏推荐】


每个专栏均配有案例与图文讲解,循序渐进,适合新手与进阶学习者,欢迎订阅。

在这里插入图片描述

引言

如今,在处理大规模数据分析项目、管理高速生成的流数据,或构建大型机器学习模型时,遇到无法完全加载到内存(RAM)中的数据集已经十分常见。例如,尝试将一个 100 GB 的 CSV 文件加载到 Pandas DataFrame 中时,就会面临显著的内存压力。在这些情况下,内存限制可能中断整个数据处理流程,有时甚至带来高昂的代价。这个问题被称为“内存不足”(Out-of-Memory,简称 OOM),它直接影响系统的可扩展性、效率以及成本。

本文将概述几种在 Python 项目中应对 OOM 问题的实用技术和策略,并提供各种工具的“尝鲜体验”,帮助数据科学家和开发者在数据无法完全加载到内存时,仍能流畅地处理数据——包括分块处理数据、用磁盘替代 RAM,或者在多台机器上进行分布式计算。


应对 OOM 数据的策略尝鲜

本文将以一个 10 万条客户数据(100K customers dataset)为示例来演示处理 OOM 问题的策略。尽管该数据集并非真正意义上的海量数据,但其规模(10 万条记录)足以清晰展示所涉及的技术和方法。


数据分块(Data Chunking)

第一种策略可以在数据读取和加载的过程中“即时”应用,即将数据划分为多个块进行处理。在 Pandas 中,可以通过 read_csv() 函数的 chunksize 参数实现,指定每个数据块包含的记录数。

import pandas as pd

url = "https://raw.githubusercontent.com/gakudo-ai/open-datasets/refs/heads/main/customers-100000.csv"
reader = pd.read_csv(url, chunksize=30000)

for i, chunk in enumerate(reader):
    print(f"Chunk {i}: {chunk.shape}")

分块处理是防止 CSV 文件形式且结构简单的数据集出现 OOM 问题的有效方法。然而,当数据格式较为复杂,例如实例之间存在依赖关系或嵌套的 JSON 结构时,这种方法就不太适用。


使用 Dask 进行并行 DataFrame 与惰性计算

如果希望几乎无缝地扩展类似 Pandas 的数据处理流程,Dask 是一个非常不错的选择:该库利用并行与惰性计算处理大规模数据集,同时其逻辑在很大程度上与独立的 Pandas 保持一致。

下面示例展示了如何使用 requests 先将 CSV 文件下载到本地,再用 Dask 读取,避免可能的服务器传输问题(例如编码错误):

import dask.dataframe as dd
import requests

url = "https://raw.githubusercontent.com/gakudo-ai/open-datasets/refs/heads/main/customers-100000.csv"
local_filename = "customers-100000.csv"

# 将 CSV 文件下载到本地后再读取到 Dask DataFrame
response = requests.get(url)
response.raise_for_status()  # 状态码异常时抛出异常
with open(local_filename, 'wb') as f:
    f.write(response.content)

df = dd.read_csv(local_filename)
df[df["Country"] == "Spain"].head()

在使用 Dask 时,务必通过 Dask 直接读取文件,而不要使用 pd.read_csv()。否则,所有数据将一次性加载到内存中,这正是我们想要避免的。


使用 Polars 高效管理大数据

Polars 是另一个可用于大数据处理的库,其核心用 Rust 编写,可在内存有限的情况下高效管理数据。相较于分块处理,Polars 更加自动化和灵活,非常适合单机场景,但不具备 Dask 的分布式计算能力。

下面示例展示如何加载大数据集并进行惰性查询(Lazy Query),注意使用 collect() 方法触发执行并获得最终结果:

import polars as pl

url = "https://raw.githubusercontent.com/gakudo-ai/open-datasets/refs/heads/main/customers-100000.csv"
df = pl.read_csv(url)
lazy_result = df.lazy().filter(pl.col("Country") == "France").select("First Name", "Email").collect()
print(lazy_result)

使用 Pandas 与 SQLite 执行 SQL 查询

如果需要反复从超大数据集文件中查询子集而不想重复加载数据,且熟悉 SQL 语法,这是一种优化内存使用的有效策略。该方法适合探索性过滤和选择性数据加载,但若需执行复杂的数据处理计算,Dask 会更高效。

下面示例展示如何结合 sqlite3 与 Pandas 分块处理,将数据增量加载到 SQL 数据库中,并查询西班牙客户,而无需一次性加载整个数据集:

import pandas as pd
import sqlite3

url = "https://raw.githubusercontent.com/gakudo-ai/open-datasets/refs/heads/main/customers-100000.csv"

# 创建内存中的 SQLite 数据库连接
conn = sqlite3.connect(":memory:")

# 将 CSV 分块读取并依次追加到 SQL 表
reader = pd.read_csv(url, chunksize=10000)
for i, chunk in enumerate(reader):
    # 第一个块创建表,后续块追加
    if_exists_strategy = 'replace' if i == 0 else 'append'
    chunk.to_sql("customers", conn, if_exists=if_exists_strategy, index=False)

# 查询数据库而无需一次性加载整个文件
df = pd.read_sql_query("SELECT * FROM customers WHERE Country = 'Spain'", conn)
print(df.head())

conn.close()

需要注意的是,对于非常大的数据集,基于 SQLite 的方法在执行深度分析时可能比 Dask 或 Polars 稍慢。


总结

本文介绍了四种不同策略和技术,用于在内存受限情况下处理超大数据集时,避免出现著名的内存不足(OOM)问题。选择具体策略取决于对各方法的熟悉程度以及具体需求。

下表简要概览了各方法的特点:

方法 描述
Pandas 分块 适合将大型 CSV 文件分块读取,内存控制灵活,设置简单,但需要手动处理聚合和合并逻辑。
Dask DataFrame 通过惰性计算和并行处理,将基于 DataFrame 的工作流程扩展到超内存数据。适合在管道中执行全数据集级别操作。
Polars(惰性模式) 内存高效、速度快,自动优化查询,适合单机大表数据处理,但不具备 Dask 的分布式能力。
SQLite(结合 Pandas) 适合在磁盘上查询大数据集文件而不加载到内存,适用于反复筛选或结构化访问,但执行速度可能较慢。

更多推荐