1. 首页
  2. 科技部落

Python运行速度慢?试试看这些

在机器学习领域,Python因为其兼具对用户友好和功能强大两个特点,是最热门的语言之一。但在Python初步入门时,可能会遇到运行很慢的问题。尤其当数据量很大时,某些操作会降低整个项目的效率。今天我来聊聊我在Python入门时的一些体会。Pandas读取数据时,谁吃掉了我的内存?

1)Numpy Array vs Python List

Pandas是基于Numpy的数组array构建的,大家最常使用的库之一。在导入数据时,大家有时会遇到内存占用过大的问题,但是如果注意一些小技巧,可以将内存占用大大降低。在具体聊怎么操作之前,我们先看一下为什么要使用Pandas处理数据。

Python运行速度慢?试试看这些

Figure 1: Numpy Array vs Python List 存储形式图。

图片来源:https://jakevdp.github.io/blog/2014/05/09/why-python-is-slow/

Fig. 1图右是一个Numpy Array的存储形式图, 图左是一个Python List的存储形式图。相比于Python将数据存放在不连续的区域,Numpy在创建array的时候,是寻找内存上的一连串区域来存放。因此在批量处理数据时,Numpy只需要在这一片连续的区域内走动,就可以实现快速索引,从而比用Python List处理数据高效得多。

2)优化Pandas的object类型

回到Pandas读取数据,这里我随便选取了一个样本数据,数据内容不必在意,操作都是在jupyter notebook中进行的。首先导入数据,并用info查看内存占用情况。

Python运行速度慢?试试看这些

这里我们看到data中有58列float64类型,30列int64类型以及8列object类型。在Pandas中,每种数据类型都是分开存储的,如果分别看这几类数据占了多少内存,就会发现仅8列object类型,就占用了接近50%(894.MB/1.9G)的内存消耗。

Python运行速度慢?试试看这些

object类型是个什么意思呢,它就是表示使用Python内置类型存储字符串数据。于是我们又看到了这张熟悉的结构图。Fig. 2左边是以NumPy数据类型存储数值数据结构图,右边是使用Python内置类型存储字符串数据结构图。

Python运行速度慢?试试看这些

Figure 2: Pandas中数值型和字符串型数据存储形式图

图片来源:https://jakevdp.github.io/blog/2014/05/09/why-python-is-slow/

当使用object类型时(Fig. 2左),尽管每个指针只占用1字节的内容,但如果每个字符串在 Python 中都是单独存储的,那就会占用实际字符串那么大的空间。其实我们接触的字符型数据,往往只有有限的unique values,比如性别变量只有男,女,缺失三种情况。针对这种数据,如果将object类型转化成category类型,Pandas就使用最节省空间的int子类型来表示该列中的所有不同值,从而可以大大减少内存占用。

Python运行速度慢?试试看这些

我们看到仅仅是将样本数据中的4列object类型转化为category类型后,内存就从1.9GB降到了1.5GB。如果你查看这几列数据,除了这几列的数据类型变了之外,数据看起来是完全一样的。

观察一下我们的样本数据,剩下的object列中存储的都是日期。对于日期数据,可以转化成datetime类型。Pandas中datetime类型是64位的,所以如果你的日期变量是以数值型存储,那转换成datetime也许并没有内存优势。但还是建议转换成datetime,因为这样日期方面的操作会变得很简单。一个小技巧,在进行datetime转换时,不指定日期格式也可以,但是指定日期格式会极大缩短转换操作时间,其中具体原因这里就不解释了。

Python运行速度慢?试试看这些

再次查看一下内存使用,我们看到内存占用又降低了0.4GB。所以在对object类型优化后,内存占用从1.9GB降低到1.1GB,一共降低了0.8GB。

除了对object类型优化,还可以对数值型数据优化。比如当正整数变量的类型是有符号整数时,可以将其设为无符号整数类,数值范围很小的变量从int64转换为int32等等,这里就不讨论了。

矢量化操作

当我们对Pandas进行一些操作时,如果你抱怨某个操作花费了很多时间,有可能是你使用了不必要的循环。比如对我们刚刚的样本数据,想根据列login1和createtime的以下规则,增加新的一列new_col:

Python运行速度慢?试试看这些

一个刚刚接触python的人也许会写一个循环,先增加一列new_col,然后再根据每行中createtime和login1的值来给new_col赋值。这种操作效率很低。

另外一种大家常用的方法是apply。Pandas的apply方法接受函数并沿DataFrame的轴(所有行或所有列)应用它们,但是因为apply在内部尝试循环遍历Cython迭代器,所以效率也并不高。若想高效使用apply,请记得运用multiprocessing使python并行运行apply,这将大大提高你的代码运行速度。这里我不举例子了,因为我更想讲的是矢量化操作。一个刚刚接触python的人也许会写一个循环,先增加一列new_col,然后再根据每行中createtime和login1的值来给new_col赋值。这种操作效率很低。

什么是矢量化操作?如果在一行代码中,直接对某一列数据df [col] 进行某种操作, 比如df[col]*1.2,这便是矢量化操作的一个例子。矢量化操作是Pandas中执行的最快方法,究其原因是SIMD(single instruction, multiple data)的应用。不像在上文中提到的运用multiprocessing使python进行apply并行运算,矢量化操作不需要你的设置,会直接在内部将运算拆分到多核并行运算。这也是我们大家为什么都爱Numpy和Pandas的原因。

但是如何将条件计算应用为Pandas中的矢量化运算?一个技巧是根据你的条件选择和分组DataFrame,然后对每个选定的组应用矢量化操作。在下面的代码中,你将看到如何使用Pandas的isin方法选择行。另外关于Dataframe的datetime类型,假如要选择data的createtime(类型为datetime)那一列的hour,需要用data.createtime.dt.hour。别的性质,比如weekday,同理。

Python运行速度慢?试试看这些

我们看到对于160万行的数据,以上操作花费1秒,这个时间完全是可接受的。

除了手动选择DataFrame分组外,还可以应用pd.cut()。而且这里仅仅将一天分成了3段时间,如果有更多的时间段时,pd.cut()将会更加方便简洁,代码如下。

Python运行速度慢?试试看这些

我们看到运用了pd.cut()后,运行时间更短了,只花费了不到0.4秒。我使用Numpy和Pandas的一点经验是,尽量使用它们内置的函数。这些函数往往通过C语言,实行了很好的并行效果。

View 和 Copy

在初期用Numpy时,很多人会困惑的一点就是Copy和View的区分。简单来说,Copy和View就是字面上的意思,Copy将数据重新拷贝了一份,View是直接取原数据的索引部分。修改Copy的话,不会改变原数据,而修改View的话,会改变原数据。那很自然的,用View是比Copy快的,因为不再需要额外生成一份数据。那哪些是Copy,哪些是View呢?当a是ndarray时,下面举例的都是View的方式:

Python运行速度慢?试试看这些

这些是Copy的方式:

Python运行速度慢?试试看这些

留意区分View和Copy, 可以避免一些不必要的错误, 比如不小心改动了原数据又或者以为改变了原数据,但其实并没有。

因为View比Copy快,所以如果可以使用View,尽量选择用View。比如一开始大家可能以为ravel和flatten是一样的,但是flatten返回的是Copy,所以如果大家去检查运行速度的话,会发现ravel耗时更短。又比如a*=2和a=a*2,前者是View,后者是Copy,前者的运行更快。

 

还有一点是,在初期使用Pandas时,很多人都遇到过以下烦人的报错:

 Python运行速度慢?试试看这些

当遇到这个报错时,往往运行结果好像一切正常,但是报错就是很烦呀。其实这个报错也和View和Copy有关,就是当你使用链式赋值,Pandas不能确定是View还是Copy,就会报错提醒。那什么是链式操作呢?

Python运行速度慢?试试看这些

这种就是链式操作,解决办法很简单,使用loc:

Python运行速度慢?试试看这些

但有的时候你发现使用loc后还是报错,那请往上检查你的代码,是否出现了类似以下操作:

Python运行速度慢?试试看这些

如果你先进行了类似操作,在后面又对df1赋值的话,这是一个隐性的链式操作,还是会引起报错。所以请尽量避免这种隐性链式操作,或者直接明确拷贝。

Python运行速度慢?试试看这些

没有SettingWithCopyWarning报错后,整个人都神清气爽了!

最后总结一下,Python的Numpy和Pandas的很多操作都是通过C实现的,所以如果正确使用,运行速度应该非常快的。而且要实现某一步操作,往往有很多方法。如果你发现你的某一步操作特别费时的话,网上搜一搜,一般都可以发现更加高效的方法。