处理1.5G的CSV文件,有370万行和16列,做均值、透视、聚合、匹配查找。
我需要统计每个关键词不同时期的排名,然后得出最高排名、最低排名、平均排名。根据关键词、排名、ASIN进行模糊和精确匹配筛选。使用这种方式可以发现数据之间的关联性,比如有四组元素((1,A),(1,D))
, ((2,B),(2,A),(2,C))
,((3,C),(3,D))
,((4,E),(4,F))
。有两组元素中都有A,可以发现A与1和2有关联,A,B,C在同一组中,A,B,C存在关联性,由于各自与1,2,3关联,因此1,2,3又相互关联,与4不关联。通过选择匹配方式,可以把1,2,3组单独筛选出来。
一开始的时候想都没想就用字典实现了,也比较容易,匹配速度非常快。但键值对层级多了之后容易搞混。之所以用字典,是因为原来的数据是近似随机分布的,对原数据进行遍历后生成格式化的字典,以便在匹配的时候按需索引。
同时panda也有尝试,但是pandas的处理速度耗时惊人,后来发现是我的思路不正确。最初思路是把表格数据二次展开,但是这样原本300多万行就变成了1000多万,一是消耗数据展开的时间,二是展开会出现重复数据,增加内存消耗,三是匹配数大幅增加,匹配关键词量级从X变成3X。
后来发现字典有一个很大的问题,就是内存占用,一个大型字典是完全载入内存的,原数据有1.5G,载入内存后占用飙升了好几个G。处理一套更复杂的数据时,发现内存占用100%,程序就再也运行不下了。
如何解决内存问题?
一个思路是不要把数据完全载入内存,这样就涉及到内存交换,只在需要时载入部分数据。可以用文件或者数据库实现。但是配置数据库实在太麻烦了,如果是sqlite
还好说。此外IO型的数据更新会非常慢,尤其是这几百万离散数据更新。我尝试过sqlitedict
,数据处理速度影响可能是光速换步行。
在这中间还有一个小插曲,我想试试协程读文件会不会提高读取速度,比如同时读几个文件,疯狂切换,用asyncio
和aiofiles
实现并测试,发现速度大幅下降。还是老老实实流式读文件最好。异步不要乱用,异步是提高资源利用率,而不是添乱。
回到另一个思路,压缩字典。根据业务需求选择不同的数据载入方式。比如有的数据在全程都不会用到,那么在载入的时候就应该排除掉。另外就是尽可能避免数据冗余。能够折叠的就尽量折叠,比如分层,在一些情况下还可以提高索引效率。但是对于需要完整数据的时候,还是没有办法了。
最后只能寄托于pandas了,之前说过pandas运算速度太慢,但pandas可以解决内存占用的问题,至少不会因为内存爆满而无法继续运行。用时间换空间是可以理解的。
时隔几天又重新写了一个同样功能的程序,只不过是pandas方法实现的,测试后发现耗时从原来的的50秒变成了5分钟。显然我是不甘心的。我需要即时生成数据,5分钟实在没有耐心,耗时主要花在了匹配上,二次展开后的源数据有1000多万行,与字典的一百万条左右相比,匹配时间真是无法比。分析数据后,发现是我的思路不对,源数据没必要二次展开,因为同时损失了空间和运算时间。
在read_csv
时,明确dtype=str
和usecols=range(x)
,engine='c'
进一步提升数据加载速度。
在关键词匹配的时候,先用groupby
将关键词分组,进一步提升匹配速度。
同理用groupby
分组运算最大、最小,transform
生成列,然后匹配。
把匹配结果放入字典,再做二次运算,与原来的字典相比,体积大幅降低。
测试运行时间字典方式与pandas方式不相上下。
最后我将纯python和pandas方式统一了,写了一个新的匹配引擎。
让人惊讶的是新引擎的python方式比原来的还要快,虽然方法都差不多,还加了引擎判断。
提高性能的一些tips
判断尽量在循环外。
及时中断,避免重复循环。
可以避免的运算,请放在判断中或循环外。
复杂运算在筛选后进行。
提高pandas性能的一些tips
能用itertuples
的时候就不要用iterrows()
。
合理使用groupby
整理数据,然后使用agg
或transform
运算列。
合理使用apply
运算数据。
避免数据冗余。
读取文件时选择性载入数据,明确dtype
。
这次的程序让我同时加深了:unittest
、pandas
、asyncio
,以及工厂函数的构建思路。
稍微有点理解了接口统一性。因为结构不变,但是支持两种引擎操作。