@TOC
前言
之前工作很少遇到内存方面的困扰。前段时间朋友找我帮忙做一家某小型上市银行一年期的交易数据JE,百亿量级。吭哧吭哧写完代码,结果放在她的本地运行直接内存爆掉。于是稍微尝试了一些内存优化的方法,记录一下心德。
选择正确的方法
选择正确的操作方式尤为重要。其实百亿还好,不太用到hive。之前她们领导也是说了推荐SQL server去做。anyway,最后和她同学一起帮她写了python,而且没有服务器,还要在本地运行,她公司电脑装不上任何IDE,还要用CMD指令。好的,由于我们远程没法帮她调试,但是强烈建议,无论何任务都要选择最恰当的解决办法(好像是废话)。
ChunkSize
chunk——块。假设我们现在有一个200Mb数据矩阵,我们对这个矩阵进行计算,而内存上限为210Mb, 一不小心就会超出内存,导致后面的计算通通停掉。这个方法通俗讲就是,一块大蛋糕我一口气吃不下,那就切成n块小蛋糕一点一点吃。因此,我们可以看出,这样的方法节省时间,但是会很耗时。时间复杂度和空间复杂度,鱼和熊掌不可兼得。(第一次朋友貌似跑了一晚上呵呵0.0~~)
with open(name, 'r', encoding="utf-8") as fp:
line = fp.readline()
if "Field_1" in line:
skip_head = True
if skip_head:
table = pd.read_csv(name, sep='|', chunksize=20000, header=0,
names=['Field_1', 'Field_2', 'Field_3', 'Field_4', 'Field_5', 'Field_6', 'Field_7',
'Field_8', 'Field_9', 'Field_10', 'Field_11', 'Field_12', 'Field_13', 'Field_14',
'Field_15', 'Field_16', 'Field_17', 'Field_18', 'Field_19'])
else:
table = pd.read_csv(name, sep='|', chunksize=20000, header=None)
table.columns = ['Field_1', 'Field_2', 'Field_3', 'Field_4', 'Field_5', 'Field_6', 'Field_7', 'Field_8',
'Field_9', 'Field_10', 'Field_11', 'Field_12', 'Field_13', 'Field_14', 'Field_15',
'Field_16', 'Field_17', 'Field_18', 'Field_19']
由于是个超大的del文件,因此sep='|', chunksieze设定就是20000。
之后的计算,每一个chunk去执行打包好的算法(如float64_lower, Criteria_A..),最后再append,或者concat在一起,大框架就是如此。
for chunk in table:
print("cal {}".format(ddc * 20000))
chunk.columns = [........]
df1 = self.Float64_lower(chunk)
df_A = self.Criteria_A(df1)
df_B = self.Criteria_B(df1)
.........
内存释放
Python对于占用较大的内存不会立刻释放,即便你del X,或者赋一个空值,所占用的空间都无法释放。
mydata=pd.DataFrame(DF)
mydata.info(memory_usage='deep')
此处该mydata占用32Mb.释放其缓存方法,配合python垃圾回收机制gc包:
import gc
del mydata
gc.collect()
此后,这个变量引用被删掉,并释放内存。一般在进行大量计算时候,要酌情一边计算一边清空释放内存,避免溢出。
制作json(或字典)
当需要去从另外一个dataframe,数据源查找匹配的时候,一般操作都是merge、join、caoncat,这很像SQL的思路。但是当两个数据库都极其庞大的时候,我们为了节省空间,其实可以将需要查找、匹配的第二第三数据源制作成一个json、字典dict、甚至是一个array组。这样可以避免dataframe其他的数据占用过多空间。
==1.手工输入,或者传入参数==
mydict={'A':0.3, 'B':0.5,'C':0.2}
df_2['Result']=df_1['Catogory'].apply(lambda x: mydict[x]*2)
==2.去除多余数据将dataframe制作为map==
myarray = np.array([['A', 'B','C'],[0.3, 0.5, 0.2],['boy','cow','cat'],['AB','CD','EF']])
df = pd.DataFrame(myarray).T
df.columns=['0','1','2','3']
df_map = df.groupby('0')['1'].sum()
得到的字典如下:
接下来可以进行上面一样的操作了。这种“伪字典”占用空间有点大,但是很方便,现成的dataframe可以直接用。
==3.直接用dict去定义==
dic={}
with open('Level.csv', newline='', encoding='UTF-8') as woww:
readerw=csv.DictReader(woww)
for row in readerw:
dic[row['\ufeffCIK']]=row['URLF']
for k,v in dic.items():
with urlopen(v) as text:
soup = BeautifulSoup(text, 'html.parser')
其实可以直接写成建立空dic{},并指定key, value。用于后续运算。
矩阵压缩
说道矩阵压缩,首先介绍下稀疏矩阵(sparse matrix)和稠密矩阵(Dense Matrix)。在矩阵中,若数值为0的元素数目远远多于非0元素的数目,并且非0元素分布没有规律时,则称该矩阵为稀疏矩阵;与之相反,若非0元素数目占大多数时,则称该矩阵为稠密矩阵。定义非零元素的总数比上矩阵所有元素的总数为矩阵的稠密度。
矩阵压缩。则是把这些元素分别用行列index和value来表示。
(1,1,1):数据元素为 1,在矩阵中的位置为 (1,1);
(3,3,1):数据元素为 3,在矩阵中的位置为 (3,1);
(5,2,3):数据元素为 5,在矩阵中的位置为 (2,3);
除此之外,还要存储矩阵的行数 3 和列数 3;
我们利用scipy包中的sparse可以轻松压缩和还原稀松矩阵。
但是更建议用sparse来存储数据,以很低的内存耗用量。使用的时候释放还原。避免在计算峰值的时候溢出。
七种sparse:
- csc_matrix: Compressed Sparse Column format
- csr_matrix: Compressed Sparse Row format
- bsr_matrix: Block Sparse Row format
- lil_matrix: List of Lists format
- dok_matrix: Dictionary of Keys format
- coo_matrix: COOrdinate format (aka IJV, triplet format)
- dia_matrix: DIAgonal format
from scipy import sparse
myrow = np.array([0, 0, 1, 2, 2, 2])
mycol = np.array([0, 2, 2, 0, 1, 2])
mydata = np.array([1, 2, 3, 4, 5, 6])
matrix = sparse.csr_matrix((data, (row, col)), shape=(3, 3))
matrix就是这样的一种存储, 当然可以从dataframe里直接拎出array来,毕竟也是numpy底层。
##当需要还原的时候
matrix.toarray()
datatype转换
我们知道数据类型有很多种,参与计算最常见的就是int(), float()。
int一般比float的计算要慢一点(待认证),所以我每次都习惯pd.to_numeric把整数转成float。然后,int/float 都会有64、32、16、8位的格式。如:float64,int32.....
下图我们可以看到不同数据类型,占用占用内存不一样。因此一般如果不需要那么高精度的时候,可以通过转换数据类型来减缓内存占用量。
1. 首先,很多数据类型,即便你看到的数字,py默认都是object,尽量把它都转成datetime或者numeric形式,内存会小很多。
如图对比,我们只转换了三列object位float64.内存从19.7M降低到14.4M。
2. Float Int 类型转换降级。
DF = df_Final.select_dtypes(include=['float64'])
##转换前数据占用量
DF.info(memory_usage='deep')
##转换成最低
DF_2 = DF.apply(pd.to_numeric,downcast='unsigned')
DF_2.info(memory_usage='deep')
我们可以看到内存从421kb--->81kb。巨幅下降。
3. Object转换为category。
这个转换简直是令人惊奇!!
##转换前
DF = df_Final.select_dtypes(include=['object'])
DF.info(memory_usage='deep')
##转换后
DF2 = DF.astype('category')
DF2.info(memory_usage='deep')
记住!!Object转Category神技!直接从13.7M降低到299.3Kb!
去除冗余
其实这个很好理解,很多时候我们的数据存在很多冗余,重复。可以用drop把它去掉。所以预先的数据清洗很重要。
另外,千万不要以为groupy会让数据内存表小,看上去表小但是实际上内存占用更大!!
欢迎一起讨论,共同进步,如有错误帮忙指正~!