python pandas 加速循环_Pandas进阶之提速遍历操作

一、概念

pandas是基于numpy库的数组结构构建的,它的很多操作都是(通过numpy或者pandas自身由Cpython实现并编译成C的扩展模块)在C语言中实现的。因此,正确使用pandas,它的运行速度是非常快的。

本篇介绍几种pandas中常用的提升运行速度的方法

1)将datetime数据与时间序列一起使用的优点

2)进行批量计算的最有效途径

二、使用Datetime数据节省时间

通常创建DataFrame时,或从txt、excel中读取数据时,如果没有特殊声明,那么date_time将会成为默认的object类型,实际上,pandas和numpy都有一个dtypes的概念,可以设置数据类型;

df = pd.read_csv("speed_promotion.csv")

df.head()

type id amount dt

0 QY ac89c667-8d21-454f-b205-49e3d5ba4920 38000.0 2018/9/21 0:00

1 XYY 0cb72f4d-0059-43ac-a326-6fcf33e55632 85196.1 2019/1/29 0:00

2 QY 6937fdb5-bef7-419a-a633-7f58f664c9cc 33000.0 2018/8/29 0:00

3 QY 05b75e08-f2fe-4610-87ec-15ada55a1685 54000.0 2018/12/21 0:00

4 QY 1e9f8e40-67f9-4882-8935-4e10c5b6258f 32000.0 2018/12/21 0:00

df.dtypes

type object

id object

amount float64

dt object # date_time,默认成了object类型

dtype: object

object 类型像一个大的容器,不仅仅可以承载 str,也可以包含那些不能很好地融进一个数据类型的任何数据列,如果我们将日期作为 object 类型就会极大的影响效率;

对于时间序列的数据而言,要将date_time列格式化为datetime对象数组,pandas称之为时间戳,使用pd.to_datetime()函数即可简单实现;

df["dt"] = pd.to_datetime(df["dt"])

df.dtypes

type object

id object

amount float64

dt datetime64[ns]

dtype: object

特别地,如果数据源中的date_time不是ISO 8601 格式的,需要设置pd.to_datetime()中的format参数,进行格式化,否则pandas将使用dateutil 包把每个字符串str转化成date日期,速度并不是最快的,只有当date_time是ISO 8601 格式,pandas才可以立即使用最快速的方法来解析日期。

三、pandas数据的循环操作

基于上面的数据,如果需要根据amount列的值,构造一个新的列,要求:

0 < 金额 <= 10000,返回:金额 * 0.3

10000 < 金额 <= 100000,返回:金额 * 0.5

100000 < 金额 <= 1000000,返回:金额 * 0.8

常规的代码做法(不赞同该做法)

定义一个判断函数,写好条件的逻辑代码

def judge_amount(amount, rate):

"""计算不同投资区间的收益"""

if amount >0 and amount <= 10000:

rate = 0.3

elif amount > 10000 and amount <= 100000:

rate = 0.5

elif amount > 100000 and amount <= 1000000:

rate = 0.8

else:

raise ValueError(f"Invalid amount: {amount}")

return amount * rate

使用for循环来遍历df,根据判断函数逻辑,添加新的数据列

def add_judge_amount(df):

"""根据金额判断区间,为df增加新列"""

add_list = []

for i in range(len(df)):

amt = df.iloc[i]["amount"]

income = judge_amount(amt)

add_list.append(income)

df["income"] = add_list

# 打印运行时间

%timeit add_judge_amount(df)

5.59 s ± 190 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

对于写Pythonic风格的人来说,这个设计看起来很自然。然而,这个循环会严重影响效率,时间成本太高,不赞同这么做。原因:

它需要初始化一个将记录输出的列表;

它使用不透明对象范围(0, len(df))循环,然后在应用judge_amount()之后,必须将结果附加到用于创建新DataFrame列的列表中;

它使用df.iloc [i] ['amount']执行所谓的链式索引,这通常会导致意外的结果;

使用itertuples() 和iterrows() 循环

itertuples()函数和iterrows()函数,是pandas内置的进行遍历循环的方法,可以使遍历的效率更快一些,因为这些都是一次产生一行的生成器方法,类似scrapy中使用的yield用法;

def add_judge_amount_iter(df):

"""根据判断区间,为df增加新列"""

add_list = []

for index, row in df.iterrows():

amt = row["amount"]

income = judge_amount(amt)

add_list.append(income)

df["income"] = add_list

# 打印运行时间

%timeit add_judge_amount_iter(df)

2.68 s ± 8.87 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

对比常规做法

语法更明确,行值引用中的混乱更少,因此它更具可读性;

时间方面,快了1倍!

还有改进的空间,因为仍然在使用某种形式的Python for循环,这意味着每个函数调用都是在Python中完成的,理想情况是它可以用Pandas内部架构中内置的更快的语言完成;

使用apply()

Pandas.apply()方法接受函数(callables)并沿DataFrame的轴(所有行或所有列)应用它们;

通过apply() + lambda的方式,lambda函数将amount列传递给了定义的方法judge_amount();

# 打印运行时间

%timeit df["income"] = df["amount"].apply(lambda x: judge_amount(x))

13.5 ms ± 113 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

Pandas.apply()的语法优点很明显,行数少,代码可读性高;

对比iterrows()函数,apply()函数所花费的时间,从2.68s提升到了13.5ms,提升了200倍;

其实,这还不算是“非常快”,原因是apply()将在内部尝试循环遍历Cython迭代器,而传递的lambda并不是可以在Cython中处理的东西,它需要在Python中调用,因此并不是那么快,特别是当数据量非常大的时候。

矢量化操作

什么是矢量化操作?如果不需要基于一些条件,而是可以在一行代码中将所有金额应用于固定收益率(df["amount"]*rate),类似这种。这个特定的操作就是矢量化操作的一个例子,它是在Pandas中执行的最快方法;

进行矢量化操作的一个技巧是,根据指定的条件选择和分组DataFrame,然后对每个选定的组进行矢量化操作;

当条件是对取值范围区间的限定时,通过pd.cut()函数可以很好地实现矢量操作。也可以将取值范围的列设置为索引,通过isin()函数进行范围判断,生成一系列布尔数组,传递给DataFrame的.loc索引器,获取符合范围的切片,进行分组矢量运算;

%timeit df["income"] = df.amount * pd.cut(df.amount, bins=[0, 10000, 100000, 1000000], labels=[0.3, 0.5, 0.8]).astype("float")

2.93 ms ± 46.4 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

矢量操作,花费时间基本已经快达到极限了,速度比apply()函数提升了4倍多。而且不需要再定义任何函数,更加简便;

使用pd.cut()函数时,返回的是分类类型Categorical,需要转化成数值才能运算;

还能不能再快?因为Pandas可以与NumPy阵列和操作无缝衔接,其实,通过Numpy的digitize()函数还可以加速,它类似于Pandas的cut()。虽然仍有性能上的提升,但它本质上变得更加边缘化(没有必要),而且使用Pandas,它可以帮助维持“层次结构”。

数据循环方法排名

使用向量化操作:没有for循环的Pandas方法和函数;

将apply()方法:与可调用方法一起使用;

使用itertuples()或iterrows(),从Python的集合模块迭代DataFrame行;

使用“element-by-element”循环:使用df.loc或df.iloc一次更新一个单元格或行。

你可能感兴趣的:(python,pandas,加速循环)