一、概念
- 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一次更新一个单元格或行。
作者:惑也
链接:https://www.jianshu.com/p/56a50a6c961c
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。