numpy、pandas内存优化操纵整理
媒介python作为一款不怎么关注数据范例的语言,不同范例的数据可以往同一个变量中放置
这也就直接导致,作为认识C++这种一个变量只有一个范例的程序员来说,在解读python程序时,想搞清楚变量中到底存的是什么数据范例的时候时常很头疼
所以说,精良的编程习惯真的很重要
固然上面的话跟这篇文章关系不大,但是我就是想吐槽一下 ;p
下面是我优化某个python项目的此中某个模块的内存时,对python中的numpy/pandas库调研得出的一些优化操纵,在此分享给各人
注:有的地方我理解的也不是很透彻,在此仅作为心得记录,有任何不对的地方欢迎在品评区进行指正
一、通用优化操纵
1. 指定合适的数据范例
如果数据的值域可以限定在较小的范围内,可手动指定其dtype范例进行限定
2. 制止copy
1)切片/索引层面
切片或其他索引方式中是否涉及拷贝,这部分就主要决定于各个库中具体实现细节了,可看下面对应章节部分
[*]原生python拷贝数组时留意的一个小细节:list1 = [...]
1.类似浅拷贝/引用,操作对象为同一个,无copy,操作list2就是操作list1:
list2 = list1
2.类似深拷贝,涉及copy,list2与list1不是同一个对象:
list2 = list1[:]
2)计算过程层面
如果输入数据可以“原地”处理,且可以把计算过程拆分成“原地”处理的步骤,或是库里提供了可以“原地”计算的函数,则只管进行拆分和替换“原地”处理的函数
下面是数组归一化时,将步骤进行拆分节省内存的一个例子:
[*]简单的实现,但是会产生临时数组的内存开销def Normalize(lst : list):
lst_range = lst.max - lst.min
return (lst - lst.min) / lst_range
[*]拆分步骤,每一步都是“原地”操纵def Normalize(lst : list):
lst_range = lst.max - lst.min
lst -= lst.min
lst /= lst_range
3. 文件读取
[*]chunk读取:这个也算是流程优化的一部分
读取文件时是否是一次性读取完毕?分块读取是否会影响到流程?如果可以分块读取,那么读取接口是否有提供类似chunksize的参数?
一次读取的内容过多,一方面是会直接影响到IO内存,另一方面也是思量到,大部分情况下实际要处理的数据并没有那么多,所以不如限定每次读取的数据巨细,够用即可,用完再取
[*]index索引:如果输入数据是有组织有布局地进行存储,那么通常可以通过建索引的方式记录数据的关键信息,在需要取用的时候就能通过索引快速取用本身所需的部分,一方面加快处理速度,另一方面内存也能更低
4. 数据布局的选取
主要是选用跟实际需求更匹配的数据布局,好比dict会浪费至少30%以上的内存,如果条件允许,可以通过一系列的方式对其进行优化
[*]换用自定义class
[*]class + __slots__:加上限定,能省略class内的一些隐性内存开销
[*]namedtuple:同样是限定,但与tuple类似不可更改
[*]recordclass:基于namedtuple的可变变体(使用方式可看这里)
二、numpy干系
1. 使用sparse array
固然ndarray实用于存储大量数据,但如果此中大部分数据都是空时,用稀疏矩阵能更省空间
2. 查抄库方法的内部实现
库提供的方法为了泛用性,内部一般会对输入数据的范例进行限定,如果输入数据的范例与其预期不符,可能会造成更大的内存消耗
好比:一个库方法内部会将输入数据转为int64进行操纵,如果输入的是int16,那么消耗内存会是int16 + int64,可能还不如把输入数据直接设置为int64并设置“原地”操纵
3. indexing / slicing
numpy中有两种indexing方式,不同的方式返回值不同:可能返回view(类似浅拷贝/引用),也可能返回copy
[*]判断view/copy方式:可以简单地通过.base属性是否为None来判断
[*]basic indexing:[]中为slice/integer或一个元组(仅包罗slice/integer)时触发
与python原生list切片不同,返回view
[*]Advanced indexing:[]中为非元组sequence / ndarray( of integer/bool)或一个元组(包罗至少一个sequence/ndarray)时触发
返回copy
示例:>>> import numpy as np
>>> data1 = np.arange(9).reshape(3, 3) # reshape returns a view at most time
>>> data1
array([,
,
])
>>> data1.base
array()
>>>
>>> data2 = data1[] # data2 is a copy
>>> data2
array([,
])
>>> data2.base
>>>
>>> data1[]=[, ] # change data1
>>> data1 # 可能在 = 左边时不是copy?所以data1被改变了?
array([[ 0,1,2],
,
])
>>> data2 # data2 not changed
array([,
])
[*].reshape():在大部分情况下,如果可以通过修改步长的方式来重建数组则会返回view;如果数组变得不再一连了(即修改步长重建不了了,好比ndarray.transpose)则会返回copy
特殊情况:structured array
当ndarray的dtype使用named field时,该数组变成一个有布局的数组(类似DataFrame一样有名字,而且给每一列指定不同的dtype)
>>> x = np.array([('Rex', 9, 81.0), ('Fido', 3, 27.0)],
dtype=[('name', 'U10'), ('age', 'i4'), ('weight', 'f4')])
>>> x
array([('Rex', 9, 81.), ('Fido', 3, 27.)],
dtype=[('name', '<U10'), ('age', '<i4'), ('weight', '<f4')])
[*]Individual field:返回view>>> x['age']
array(, dtype=int32)
>>> x['age'] = 5
>>> x
array([('Rex', 5, 81.), ('Fido', 5, 27.)], dtype=[('name', '<U10'), ('age', '<i4'), ('weight', '<f4')])
[*]Multiple fields:返回copy(版本<=1.15)或view(版本> 1.15)
4. 文件处理
numpy读取文件时可使用memmap对磁盘上的文件数据不读取到内存的同时当作ndarray一样去处理,但这个操纵也存在一些限定:
[*]如果读取内容很大,那么这可能会成为程序的瓶颈,由于磁盘操纵始终比内存操纵要慢
[*]如果需要不同维度的索引/切片,那么只有符合默认布局的索引/切片会快,其余的会非常慢
解决方案:可选用其他文件读取库(如Zarr/HDF5)
三、pandas干系
1. 使用sparse array
与numpy类似,DataFrame同样可以设置成稀疏矩阵方式存储数据
2.
pandas内部实际是使用numpy.ndarray来表示数据,但其对于 字符串 或 缺失值 的处理并不友好
而在pandas版本2.1以后(至少2.0以后),引入的pyarrow可以极大地优化这两方面的处理
[*]原来numpy中是用一组PyObject*存储数据,额外空间开销大;而pyarrow中使用一连的char*数组存储,对于大量短字符串的优化效果尤其显着
[*]原来pandas中对于不同范例的缺失值有不同表示方式,而且对于整形的缺失值会自动转成浮点型,比力贫困;而pyarrow中对不同范例都一套实现,数据的表示进行了统一
使用方式:实际使用时只需要在原来数据范例后面加上即可。如:
[*]string -> string
[*]int64 -> int64
[*]读取接口处可以指定dtype_backend="pyarrow",有的还需指定engine="pyarrow"
3. indexing / slicing
非常重要的一点:pandas无法保证索引操纵返回的是copy还是view
官方文档Copy on Write (CoW)的 previous behavior 末节第一句用了tricky to understand,可见它是有多难懂了
该页面主要先容了让操纵效果更加 predictable 的计谋CoW,主要思绪就是克制在一行代码中更新多个对象
如许如果有多个对象实际指向同一份数据时,如果尝试更新此中一个对象的数据会触发copy,如许能保证另一份数据不会被影响;而且由于是更新时触发,只管延缓了内存增长的时间
那么之前的代码中如果存在下面的操纵时需格外留意,后续更新pandas版本后,效果可能与预期不符:
[*]chained indexing
一般表现为df = ...,即对两个一连索引的效果进行更新
由于df得到的不确定为copy还是view,所以这里实际不确定操纵的是一个还是两个对象(具体可看Why does assignment fail when using chained indexing)
https://i-blog.csdnimg.cn/direct/327bf77a779944abbf40b351af95d481.png#pic_center
注:可能还有其他情势的 chained indexing 需要格外留意,好比CoW中还提到下面的情势:df.replace(..., inplace=True)
[*]解决办法:使用.loc取代多个索引(由于.loc操纵能保证操纵数据本身),或是将多个操纵整合到一步操纵中df.replace(col:{conditions}, inplace=True)
or
df = df.replace(conditions)
注:如果DataFrame是Multi-Index的,那么两个一连索引相当于是使用.loc了,但是仍然保举使用.loc
4. 代码优化
[*]按行迭代:用itertuples替换iterrows(速度更快)
[*]循环操纵:用.apply/.applymap替换循环
[*]大数据计算:使用pd.eval,在大数据上的较复杂计算才有速度优化效果
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页:
[1]