1. 先说说我为什么从 Pandas 切到了 Dask
事情发生在处理一批某零售企业历史订单数据的时候。单表 180GB,要按门店和品类做月度汇总,还要跟会员画像表做关联特征。我一开始用 Pandas 的read_csv分块读取,配合pd.concat拼结果,跑了两个小时之后内存还是被挤爆,最后只剩一条路:换工具。
当时摆在我面前的可选项有 Spark、Modin、Dask 这些。我选了 Dask,理由很直接:我不想去搭一套整体的分布式集群环境,不想学 RDD 那套抽象,我只想把写 Pandas 的逻辑稍加改动,让它能跑在比单机内存更大的数据上。Dask 正好长在这个位置上——它把 Pandas 的 DataFrame 切成了很多小块,分散到多个核心甚至多台机器上处理,对外仍然表现出一个完整的 DataFrame 接口。你把import pandas as pd换成import dask.dataframe as dd,大部分分析代码能原样跑通。
这篇内容适合什么人参考?如果你正在做数据清洗、特征工程、离线报表这类常规任务,数据量已经超过 10GB,发现 Pandas 越来越吃力,但又不希望为了处理数据而专门去搭一套完整集群系统,Dask 是一个非常平滑的升级路径。我会把 Dask 的原理、常用操作、性能调优和我在实际项目中踩过的坑都过一遍,争取让你看完之后能直接上手。
需要先说明一点:Dask 不是万能药。它解决的主要是"数据太大,单机内存放不下"和"计算太慢,但任务可以切分"这两类问题。如果你的数据已经少到df.describe()能秒出,或者你需要的是毫秒级交互式查询,那不应该上 Dask。工具要放在对的场景里用。
2. Dask 到底解决了什么问题:先理解它的懒加载设计
2.1 Pandas 认真做事,Dask 先画个计划再做事
Pandas 之所以在读大文件的时候容易内存崩溃,是因为它把全部数据塞进内存,然后才开始处理。Dask 不一样,它把计算分成两个阶段:先做计划,再真正执行。
这个设计叫惰性计算。我第一次接触这个概念时,总觉得它故弄玄虚——你让我跑groupby,它返回的居然不是结果而是一个"任务描述"。实际上这个设计极为重要。你在 Dask 里写的每一条 DataFrame 操作,都不会立刻触发计算,而是被记录成一张任务图。这张图描述的是:每一步操作依赖哪些数据块,每个块上要执行什么函数。当所有操作都写完了,你调用compute(),Dask 才按照任务图把计算真正发出去,并且尽可能并行执行。
我个人的体会是:把 Dask 理解成点外卖。Pandas 是那种你一打电话,商家立刻炒好一盘菜送你桌上的方式——菜量大了,你的餐桌(内存)根本放不下。Dask 是那种你先下订单、由调度中心统筹哪个菜让哪个厨房同时做的方式——真正开火之前,一切安排都在系统里。
2.2 任务图是怎么工作的:一步一节点
拿一个最经典的操作来说:
import dask.dataframe as dd df = dd.read_csv("data/*.csv", assume_missing=True) monthly = df.groupby("month")["amount"].sum().compute()第二行groupby只是构建了一个"求月度总金额"的计算节点,第三行.compute()才触发执行。Dask 内部会把文件切成很多分区,每个分区分别做 groupby,然后合并所有分区的部分结果,最后汇总输出。这里的"每个分区做一次 partial groupby"和"合并 partial 结果"就是一个典型的任务图节点。
实际工作中,我往往是先在 Jupyter 里一步步搭计算链,中途观察 task graph 的规模和形状,确认没有出现不可控的暴涨,再调用 compute。这样调试起来非常舒服,你随时能看当前的计算计划是否合理,而不是像某些框架那样,写错了就让你白等二十分钟。
2.3 Dask 的三个核心数据结构:DataFrame、Array、Bag
Dask 不止有 DataFrame,它覆盖了大数据生态里三类常见的抽象。它们的适用场景要分清:
dask.dataframe(dd):按行划分的分布式 DataFrame,最适合做结构化表格数据的清洗、聚合、关联。只要你的数据有明确的列,而且你的操作是 Pandas 风格,即便涉及索引对齐、join、groupby,都可以用它。
dask.array(da):按块划分的 NumPy 风格数组,适合做数值计算。我认识的数据科学团队用它对基因表达矩阵做标准化,一个样本矩阵可能有几万行乘几万列,单机存不下,就用 dask.array 做分块运算、分块存储。
dask.bag(db):按记录划分的弹性集合,适合处理半结构化数据,比如 JSON 日志、嵌套字典、文本文件。它支持
map、filter、flatten这类函数式操作,有点像 Python 里的多线程 MapReduce。如果数据是纯文本非表格结构,DataFrame 反而不方便,bag 更顺手。
你不需要一开始就把三个都学会。大多数人只用到 DataFrame,那是最常用的一块;等你哪天发现要处理不对齐的 JSON 日志,再回头学 bag 会很轻松。
2.4 Dask 的调度器:线程、进程、分布式三种模式
Dask 的调度器模式决定了计算发生在哪里。理解这个模型,调优就有方向:
| 模式 | 特点 | 适用场景 |
|---|---|---|
| 多线程调度器 | 默认模式,多个线程共享同一进程内存 | 主要用于解决 GIL 限制、任务图并发,适合小规模本地数据,官方默认是线程数=CPU 核心数,适合处理 DataFrame 这类可以充分利用 C 扩展的函数 |
| 多进程调度器 | 进程隔离,拥有独立内存空间 | 适合包装了纯 Python 函数、不释放 GIL 的计算任务,因为进程间不共享内存,但数据传递有序列化成本 |
| 分布式调度器 | 可跑到多台机器上,支持弹性扩展 | 当你本地的 CPU 和内存都不够时,启动一个分布式集群,把任务发给多台机器 |
我在本地处理十几 GB 数据时,直接用它默认的线程调度器,不需要额外配置。如果数据到了上百 GB 且机器有多核,我会加一个LocalCluster来显式指定进程数、线程数和内存上限,这样可控性更高。等到需要多机部署的时候,再去搭配集群调度器,它的使用方式完全一致——任务代码不用改,只是Client的启动参数不同。
3. 实操篇:从安装到第一个并行 DataFrame 任务
3.1 环境的安装与最低配置:5 分钟上手
Dask 的安装非常简单。
pip install "dask[complete]"这个命令会帮你装好 dask 核心库,以及配套的 dataframe、array、bag、diagnostics 等模块。如果你只想用 DataFrame,可以只安装dask本体,但[complete]会包含可视化诊断所需的图形库,对调优有帮助,建议装全。
我的个人推荐是以 conda 环境隔离:
conda create -n dask_env python=3.11 conda activate dask_env pip install "dask[complete]" jupyterlab然后启动 Jupyter,在 notebook 里验证:
import dask.dataframe as dd import dask print(dask.__version__)能打印版本号,环境就通了。整个过程真的不会超过 5 分钟,这比搭一套 Spark 或 Hadoop 环境清爽太多。
3.2 读取数据:read_csv、read_parquet 与通配符
Dask 读取数据的语法和 Pandas 基本一致,但它多了一个很关键的参数:blocksize,用来控制每个分区的数据量。
import dask.dataframe as dd # 一次性读取所有 2023 年订单数据 df = dd.read_csv( "/data/orders/2023/*.csv", dtype={ "user_id": "float64", "discount": "float64", }, assume_missing=True, )为什么我在这里用dtype显式指定类型?Dask 在读取多个 CSV 的时候,需要推断每一列的类型。如果文件成千上万,它默认会读一小部分做采样推断。但如果列在不同文件里的格式不一致,比如有的文件user_id是数值,有的文件是空字符串,推断就容易翻车。显式指定 dtype 能强制所有分区按统一类型处理,避免后来遇到"数值列里混入字符串"的异常。
如果你处理的是 Parquet 格式的数据,情况会好很多。Parquet 自带 schema 信息,不需要采样推断类型,读取速度也快得多:
df = dd.read_parquet("/data/orders/year=2023/")Dask 对 Parquet 是按行组划分区读取的,比 CSV 要稳很多。强烈建议你在自己的数据管道里逐步将 CSV 转为 Parquet 格式落盘,会省下很多麻烦。
3.3 常用操作与 Lazy 机制的配合
到了实操层面,你可能最关心的是:我平时怎么对 Dask DataFrame 做筛选、分组、聚合、排序、关联?
筛选
filtered = df[df["amount"] > 100]Dask 会把这个筛选操作记录成任务图节点,不会立即执行。如果你之后继续用filtered做别的计算,Dask 会优化整个图,把可下沉的筛选操作提到尽量靠前的位置,减少数据量。
分组聚合
result = df.groupby(["store_id", "month"])["amount"].agg(["sum", "mean", "count"]).compute()这一行是很多数据报表场景的核心。Dask 的 groupby 执行策略分两种:如果 groupby 的 key 本身就能把数据按分区切分干净,那么每个分区直接算;但大多数情况下 groupby 的 key 是任意分布的,Dask 会先对每个分区做局部聚合,再按 key 进行重分区,最后做全局合并。这个"局部聚合+全局合并"的过程,内部其实就是 shuffle。
关联
merged = dd.merge( orders, customers, on="user_id", how="left", npartitions=20, # 控制辅助 join 的分区数 )merge 是 Dask 里比较重的操作,因为它几乎一定会触发跨分区的数据 shuffle。我第一次跑一个 50GB 订单表和 200MB 用户表的 left join,没设定npartitions,结果任务图变得非常巨大,跑了很久。后来我把右侧小表 broadcast 给所有分区,任务图立刻变小了。如果你的 join 是一张大表和一张小表,用how为 left 时可在 merge 参数里把小表加上broadcast=True,不过不同版本 Dask 对 broadcast join 的写法略有差异,建议直接查看当前版本文档。
排序
sorted_df = df.sort_values("transaction_time")排序和 groupby 类似,也要 shuffle 数据。如果你后续操作所依赖的 key 和排序 key 相同,Dask 可以复用分区信息,减少一次 shuffle。所以我会提前规划好任务的关键列,尽量不要在管道中途频繁换 key 做 sort 和 groupby。
3.4 真正触发计算的时机:compute 与 persist
提醒一下新手最容易忽略的点:Dask 几乎所有的操作都是惰性的。如果你只写了df.groupby(...).sum()但没调用compute(),控制台只是打印一个 DataFrame 的示意描述,你等多久都不会有结果。所有真正计算都发生在compute()或persist()的时候。
具体来说:
compute():计算结果,并把结果从分布式结构转换为本地 Pandas 对象。适合数据量不大、最终需要拿到主进程内存里进一步处理的场景。persist():把中间计算结果留在集群内存中,方便后续多次使用,但不返回具体结果。适合你在循环里反复调用同一个df_filtered的情况。
这两者的区别我用一个业务场景说明。假设你要对一个 30GB 的 DataFrame 先做清洗,然后同时做三个不同的汇总报表。如果每个报表都从原始文件开始算,Dask 会重复执行三次清洗,费时费力。这时候你把清洗结果.persist()在内存里,后面三个计算共用这个中间结果,速度快得多。如果清洗后的内存占用巨大、超出可用内存,那就不能 persist 太多步,需要在内存和 I/O 成本之间做取舍。
我在实际工作中,习惯性地把管道分成两段:第一段是数据预处理,能 persist 就 persist;第二段是汇聚分析,尽量把中间变量保持在图里,只在最后一步 compute。这样既保证了复用,又不会让任务图膨胀得不可控。
3.5 把结果写回磁盘
处理完了,最终往往要落盘。Dask 写入 parquet 的代码:
result.to_parquet("/output/monthly_summary.parquet", engine="pyarrow", compression="snappy")如果你希望按某列分区存储,以便后续按分区查询,可以:
result.to_parquet( "/output/monthly_summary.parquet", partition_cols=["month"], )分区字段会在磁盘上生成子目录,比如month=2023-01/。这种方式对后续read_parquet过滤非常友好,因为 Dask 能直接跳过不匹配的分区。另外,如果数据量大,写入时就担心磁盘 IO 压力。官方推荐用s3fs或gcsfs甚至 HDFS 这样兼容对象存储的接口,代码里to_parquet可以传入对应存储的路径。我的经验是:如果还不是特别熟悉存储后端,优先把数据写本地,验证逻辑没问题后再切换远程存储,免得排查 IO 问题的时候又混入了网络故障的因素。
4. 性能调优:为什么我的 Dask 任务跑不快
4.1 分区数:太大了也不行,太小了也不行
大部分 Dask 任务跑得不够快,首要原因就是分区数不合适。
分区太小:任务图里有太多小任务,调度器频繁切换,序列化浪费时间,单个任务开销可能超过实际计算时间。分区太大:单个核心处理一个分区耗时太长,负载不均衡,甚至导致单块内存溢满。
分区大小的经验参考值:每个分区的数据量在100MB 到 250MB之间比较合适。换句话说,一个 20GB 的 DataFrame,分区数在 80~200 之间是合理的。实际操作时,我一般用df.npartitions和df.memory_usage(deep=True)看当前分区数和预估内存占用,然后手动做repartition:
# 将 DataFrame 重分区为 100 个分区 df = df.repartition(npartitions=100)或者更直接地按分区大小控制:
df = df.repartition(partition_size="200MB")这个命令我不建议在长计算链路中间反复调用,每次 repartition 都可能触发一次全量 shuffle。应该是在读取阶段确定合理分区,如果确实需要重分区,尽量放在整个链路最开始或最末尾。
4.2 用可视化工具看任务图,找到瓶颈
Dask 自带诊断面板和任务图可视化,这是它对比其他框架很好用的地方。在 Jupyter 里你可以这样:
from dask.distributed import Client, LocalCluster from dask.diagnostics import ProgressBar cluster = LocalCluster() client = Client(cluster) # Jupyter 里可以直接显示仪表盘链接 client仪表盘能让你实时看到每个 worker 的内存使用、任务执行情况、渲染的任务图。我第一次使用 Dask 时,光靠日志打印根本判断不了瓶颈在哪。后来在仪表盘上发现某个数据块被重复读了 5 次,原因是我的多个过滤操作各自引用了不同的分区切片,Dask 没有把它们合并。我把几条 filter 组合到一个表达式之后,重复读取消失了,整体时间直接下降 40%。
如果你只是跑一个很长的管道,不想打开完整的仪表盘,可以用ProgressBar:
with ProgressBar(): result = df.groupby("store_id")["amount"].sum().compute()每个分区的计算进度会以进度条显示出来。只要看到进度条卡在某个耗时点不动,那个区块就是要优化的地方。
4.3 线程还是进程?任务类型说了算
回到调度器的选择上来。有一个误区是:我机器有 8 个核心,Dask 默认线程数也是 8,那我的 Python 代码就一定被 8 核并行跑满了。
实际上,如果你的计算是以 NumPy、Pandas 的 C 扩展为主,线程调度器运行得很快,因为 C 扩展大多能释放 GIL,所以多线程可以真并行。Dask 的 DataFrameapply或自定义纯 Python 函数的时候,线程并行会遇到 GIL 限制,导致效率上不去。这类任务,就要把它显式转成多进程调度:
df.map_partitions(apply_python_function, meta=meta).compute(scheduler="processes")注意:多进程模式下,每个分区都要被序列化后发送到独立进程,再序列化传回来。如果函数本身很简单,数据传递的开销可能比省下的计算时间还大。所以我建议只对纯 Python 计算的、 CPU 密集型的函数使用多进程,对 I/O 和向量化计算还是用默认线程调度即可。
4.4 内存溢出:大任务下如何控制内存
还有一种常见问题:Dask 某个 worker 的内存占用持续飙升,最终进程被系统杀掉。这种现象经常发生在groupby的 shuffle 阶段,因为 shuffle 会把相同 key 的数据集中到一个 worker,如果单个 key 的数据量特别大,比如某个热门商品在汇总时占据了 80% 的订单量,就会造成严重的数据倾斜。
遇到这种场景,可以试试:
- 给
groupby指定split_out,允许 Dask 把同一个 key 的中间结果分裂到多个分区再合并。这个参数可以缓解倾斜。 - 如果计算目标是对一个超大 key 做简单聚合,比如 count、sum、mean,可以先在原始分区上做一次局部聚合,再对局部结果做全局聚合。Dask 的 groupby 内部其实已经这么做了,但如果键分布极度不均,局部聚合可能仍然产生很大的中间结果,需要你手动引入"预聚合"。
- 尽可能减少中间结果的列数。我在处理多个字段的 groupby 时,会把结果先统一成最小需要的列,再做后续关联,减少内存压力。
另外,对LocalCluster可以限制单个 worker 的内存:
cluster = LocalCluster( n_workers=4, threads_per_worker=4, memory_limit='4GB', )这个配置会让 Dask 在内存接近上限时尝试把数据溢写到磁盘,从而防止 OOM。虽然溢写带来额外 I/O,但总比整个任务挂掉要好。
4.5 读入数据时就想好类型和压缩
读取阶段最容易被人忽略的性能提升点,其实是文件格式与压缩方式。CSV 虽然通用,但 Dask 读取时每列都要做类型推断,明显拖慢速度。如果你能一手把数据落地为 Parquet,那么读取速度可能提高一个数量级。另外 Parquet 自带页压缩,默认用 snappy 时,磁盘占用也比较小。
我自己的处理流程一般是:
# 第一次从外面拿到的 CSV df_raw = dd.read_csv("/data/raw/*.csv", assume_missing=True) df_raw.to_parquet("/data/clean/raw_parquet/", engine="pyarrow", compression="snappy")之后所有分析都从/data/clean/raw_parquet/读。这个转换只做一次,整个项目后续所有任务都能吃到红利。
4.6 从单机到多机:调度器的升级路线
某些项目场景,比如全量用户行为日志的周报,单机内存真的不够,那么就得把 Dask 部署到多台机器上。单机到多机的升级路径,代码层面改动很小,变的只是 client 启动方式:
from dask.distributed import Client, SSHCluster # 例如 3 台机器 cluster = SSHCluster( ["192.168.1.10", "192.168.1.11", "192.168.1.12"], worker_options={"nthreads": 4, "memory_limit": "8GB"}, ) client = Client(cluster)以上只是示例写法,实际项目里我更推荐用 k8s 或 YARN 的 Dask 部署,因为 SSH 集群还需要处理机器间的免密登录和依赖同步,比较繁琐。
关键点在于:你的数据分片和操作逻辑,在单机和多机模式下几乎是一样的。我在单机上调试好的 500 行分析代码,切到多机集群,往往除了启动 client 部分,别的都不用动。这是 Dask 对普通程序员的一个极大的友好之处。
5. 避坑指南:Dask 实战中的高频问题排查
5.1 分区和索引不匹配导致的重复计算
有一次我写了两个 DataFrame,一个按user_id索引分区,一个按event_time索引分区,然后做 merge。Dask 检测到两者分区方式不一致,只能先对其中一个做全量 shuffle 对齐,任务图体积瞬间巨大,性能很差。
排查思路:查看每个 DataFrame 的divisions属性,确认它们是否完全覆盖了 key 的范围,有没有缺失边界。如果 merge 的 key 不匹配 divisions,就用set_index先把 key 调整一致。
df_l = df_l.set_index("user_id") df_r = df_r.set_index("user_id") merged = dd.merge(df_l, df_r, left_index=True, right_index=True)这样 Dask 就能按索引直接分区对齐,省去不必要的全量 shuffle。
5.2 groupby 后为什么结果显示的是 null
Dask 对缺失值的处理策略跟 Pandas 略有差异。在read_csv时如果某列全是空值,或者一部分分区全空、另一部分分区全数值,Dask 的类型推断可能将整列判为object或 float64。用groupby聚合时,如果本来全是空值,聚合结果就会出现空值。最稳妥的做法是用dtype显式指定,或者先.astype("float64")转一下再聚合,能省掉很多莫名其妙的问题。
5.3 apply 函数不返回 DataFrame,meta 报错
自定义函数处理是 Dask 新手最容易掉进去的坑之一。当你使用map_partitions或apply时,Dask 不知道你函数的输出结构,需要你提供meta:
def enrich(df_partition): df_partition["score"] = df_partition["amount"] * 2 return df_partition[["score", "month"]] df_enriched = df.map_partitions( enrich, meta=dd.DataFrame({ "score": "f8", "month": "i8", }) )meta是告诉 Dask"这个函数返回的列叫什么、类型是什么"。如果你不写,Dask 会默认对第一个分区做一次推断,然后把这个推断结果作为 meta。问题是如果第一个分区数据比较特殊,就可能报结构不匹配。所以在写比较复杂的分区自定义函数时,我都是直接手动给一个meta,这样不会产生额外的推断过程,也更可控。
5.4 执行过程中大量abort或 worker 重启
出现这种状况,多数情况下是某个 worker 内存被打爆,或者节点上某个任务反复异常。排查步骤如下:
- 先看仪表盘的 worker 内存曲线,如果某个 worker 峰值远超均值,就是数据倾斜。
- 看日志里的错误堆栈。如果是
MemoryError,就按照前面数据倾斜、中间结果列数多的方法处理。 - 如果 worker 一直重启,可能是集群配置的内存上限太低。把
memory_limit调大,或者减少 worker 数量。
我见过一个常见现象:处理 JSON 日志时,单个条目的解析突然返回异常类型,导致整个分区计算失败。后来我用map_partitions包了一个 try-except 返回一个空 DataFrame,问题就解决了。在生产管道里,对脏数据做容错,是很有必要的。
5.5 为什么我的compute()特别慢,但明明每个分区都很快
这个问题往往出现在任务图优化上。你可能在前序操作里用了某个分区字段,然后后续突然转成另一种分区方式,Dask 只好在全图上插入一次 shuffle。每次 shuffle 都意味着数据的全局重排,IO 和网络延迟会陡然上升。
我在写长任务链时,会拆分出几个关键节点并用persist()把它们固定下来,后续围绕这些中间结果做多路查询。这样可以避免一旦某步骤变更,整条图被重算。如果你的任务是多个阶段、每次都从源头开始算,那 Dask 的任务图优化器只会在固定规则内做优化,不会帮你记忆某个中间结果,所以persist()很重要。
5.6 对很多小文件做 read_csv 时特别慢
Dask 虽然能通过通配符读取多个文件,但如果文件数量达到成千上万而且每个文件只有几 KB,那任务调度的开销会远超过数据本身处理的时间。此时最有效的办法是先把小文件合并成大文件或统一转换为 Parquet。
# 先读入,小文件合并落盘 import dask.dataframe as dd df = dd.read_csv("/data/small_logs/*.csv", assume_missing=True) df.to_parquet( "/data/consolidated/logs.parquet", engine="pyarrow", compression="snappy", )读取 Parquet 的话,Dask 能快速按行组跳过,几百个小文件聚合成一个 Parquet 后,性能提升非常明显。
6. 数据可视化、实时更新与 Dask 生态的进一步扩展
处理数据分析任务时,难免还要往前走一步:把结果画出来,或者定期跑批更新。Dask 本身不提供绘图接口,但它计算出来的 Pandas DataFrame 可以直接交给 Matplotlib、Plotly、Seaborn。
一个常见做法是:
import matplotlib.pyplot as plt result_df = df.groupby("month")["amount"].sum().compute() result_df.plot(kind="bar") plt.show()compute()返回的 Pandas DataFrame 是常规对象,你可以直接用所有熟悉的可视化库。唯一要注意的是,如果compute()返回的数据量依然巨大,比如几百万行,你画图前还是得采样或聚合。我一般在 Dask 里就已经把结果聚合到比较小的粒度,再交给可视化。
另外,如果你有"定时更新报表"的需求,可以用schedule或云平台的定时器执行一个 Python 脚本,脚本里加载 Dask,读取最新数据,计算后输出图表或落库。这样你就有了一个轻量级的数据管道。
如果你需要实时更新的场景呢?Dask 并不是传统意义上的流处理引擎。尽管 Dask 对每次批处理都很快,但如果你的业务要求毫秒级迟延的实时计算,那最佳选择可能是专门的流处理框架。不过,Dask 常被用来配合消息队列做"微批次"处理:定时间隔 5 分钟,批量消费新数据并计算。这个模式在很多中小型项目里已经够用。
最后再说一个值得关注的方向:Dask 与大型语言模型、向量检索这类新的数据场景也有交集。比如在做向量数据的预处理时,dask.array 能按块并行做归一化和降维。如果你在处理大规模特征工程时还涉及 GPU,Dask 也支持通过dask-cuda把计算调度到 GPU 上,这对深度学习特征管线是很大的加分项。
在我的实际体验里,从熟悉 Pandas 过渡到 Dask 的曲线非常平滑。最初的两三次任务,可能因为分区设置不当或 shuffle 理解不深而稍显别扭,但一旦把握住"惰性计算 + 任务图 + 分区"这三个关键词,后面基本一路通畅。如果你正在被超大 CSV、死慢的 groupby、反复的内存崩溃折磨,真心建议试一下 Dask。花一个下午把环境搭起来,找一个中等规模的数据集跑通一遍流程,你就能完全理解它和 Pandas 的边界在哪里,以及什么时候用它、什么时候用老伙计 Pandas。