1. 简介
pandas虽然是个非常流行的数据分析利器,但很多朋友在使用pandas处理较大规模的数据集的时候经常会反映pandas运算“慢”,且内存开销“大”。
特别是很多学生党在使用自己性能一般的笔记本尝试处理大型数据集时,往往会被捉襟见肘的算力所劝退。但其实只要掌握一定的pandas使用技巧,配置一般的机器也有能力hold住大型数据集的分析。
图1
本文就将以真实数据集和运存16G的普通笔记本电脑为例,演示如何运用一系列策略实现多快好省地用pandas分析大型数据集。
2. pandas多快好省策略
我们使用到的数据集来自kaggle上的「TalkingData AdTracking Fraud Detection Challenge」竞赛( https://www.kaggle.com/c/talkingdata-adtracking-fraud-detection ),使用到其对应的训练集,这是一个大小有7.01G的csv文件。
下面我们将循序渐进地探索在内存开销和计算时间成本之间寻求平衡,首先我们不做任何优化,直接使用pandas的read_csv()来读取train.csv文件:
- import pandas as pd
- raw = pd.read_csv('train.csv')
- # 查看数据框内存使用情况
- raw.memory_usage(deep=True)
图2
可以看到首先我们读入整个数据集所花费的时间达到了将近三分钟,且整个过程中因为中间各种临时变量的创建,一度快要撑爆我们16G的运行内存空间。
这样一来我们后续想要开展进一步的分析可是说是不可能的,因为随便一个小操作就有可能会因为中间过程大量的临时变量而撑爆内存,导致死机蓝屏,所以我们第一步要做的是降低数据框所占的内存:
(1) 指定数据类型以节省内存
因为pandas默认情况下读取数据集时各个字段确定数据类型时不会替你优化内存开销,比如我们下面利用参数nrows先读入数据集的前1000行试探着看看每个字段都是什么类型:
- raw = pd.read_csv('train.csv', nrows=1000)
- raw.info()
图3
怪不得我们的数据集读进来会那么的大,原来所有的整数列都转换为了int64来存储,事实上我们原数据集中各个整数字段的取值范围根本不需要这么高的精度来存储,因此我们利用dtype参数来降低一些字段的数值精度:
- raw = pd.read_csv('train.csv', nrows=1000,
- dtype={
- 'ip': 'int32',
- 'app': 'int16',
- 'device': 'int16',
- 'os': 'int16',
- 'channel': 'int16',
- 'is_attributed': 'int8'
- })
- raw.info()
图4
可以看到,在修改数据精度之后,前1000行数据集的内存大小被压缩了将近54.6%,这是个很大的进步,按照这个方法我们尝试着读入全量数据并查看其info()信息:
图5
可以看到随着我们对数据精度的优化,数据集所占内存有了非常可观的降低,使得我们开展进一步的数据分析更加顺畅,比如分组计数:
- (
- raw
- # 按照app和os分组计数
- .groupby(['app', 'os'])
- .agg({'ip': 'count'})
- )
图6
那如果数据集的数据类型没办法优化,那还有什么办法在不撑爆内存的情况下完成计算分析任务呢?
(2) 只读取需要的列
如果我们的分析过程并不需要用到原数据集中的所有列,那么就没必要全读进来,利用usecols参数来指定需要读入的字段名称:
- raw = pd.read_csv('train.csv', usecols=['ip', 'app', 'os'])
- raw.info()
图7
可以看到,即使我们没有对数据精度进行优化,读进来的数据框大小也只有4.1个G,如果配合上数据精度优化效果会更好:
图8
如果有的情况下我们即使优化了数据精度又筛选了要读入的列,数据量依然很大的话,我们还可以以分块读入的方式来处理数据:
(3) 分块读取分析数据
利用chunksize参数,我们可以为指定的数据集创建分块读取IO流,每次最多读取设定的chunksize行数据,这样我们就可以把针对整个数据集的任务拆分为一个一个小任务最后再汇总结果:
- from tqdm.notebook import tqdm
- # 在降低数据精度及筛选指定列的情况下,以1千万行为块大小
- raw = pd.read_csv('train.csv',
- dtype={
- 'ip': 'int32',
- 'app': 'int16',
- 'os': 'int16'
- },
- usecols=['ip', 'app', 'os'],
- chunksize=10000000)
- # 从raw中循环提取每个块并进行分组聚合,最后再汇总结果
- result = \
- (
- pd
- .concat([chunk
- .groupby(['app', 'os'], as_index=False)
- .agg({'ip': 'count'}) for chunk in tqdm(raw)])
- .groupby(['app', 'os'])
- .agg({'ip': 'sum'})
- )
- result
图9
可以看到,利用分块读取处理的策略,从始至终我们都可以保持较低的内存负载压力,并且一样完成了所需的分析任务,同样的思想,如果你觉得上面分块处理的方式有些费事,那下面我们就来上大招:
(4) 利用dask替代pandas进行数据分析
dask相信很多朋友都有听说过,它的思想与上述的分块处理其实很接近,只不过更加简洁,且对系统资源的调度更加智能,从单机到集群,都可以轻松扩展伸缩。
图10
推荐使用conda install dask来安装dask相关组件,安装完成后,我们仅仅需要需要将import pandas as pd替换为import dask.dataframe as dd,其他的pandas主流API使用方式则完全兼容,帮助我们无缝地转换代码:
图11
可以看到整个读取过程只花费了313毫秒,这当然不是真的读进了内存,而是dask的延时加载技术,这样才有能力处理「超过内存范围的数据集」。
接下来我们只需要像操纵pandas的数据对象一样正常书写代码,最后加上.compute(),dask便会基于前面搭建好的计算图进行正式的结果运算:
- (
- raw
- # 按照app和os分组计数
- .groupby(['app', 'os'])
- .agg({'ip': 'count'})
- .compute() # 激活计算图
- )
并且dask会非常智能地调度系统资源,使得我们可以轻松跑满所有CPU:
图12
关于dask的更多知识可以移步官网自行学习( https://docs.dask.org/en/latest/ )。
图13