Pandas进阶之提速遍历操作

一、概念

  1. pandas是基于numpy库的数组结构构建的,它的很多操作都是(通过numpy或者pandas自身由Cpython实现并编译成C的扩展模块)在C语言中实现的。因此,正确使用pandas,它的运行速度是非常快的。
  2. 本篇介绍几种pandas中常用的提升运行速度的方法
    1)将datetime数据与时间序列一起使用的优点
    2)进行批量计算的最有效途径

二、使用Datetime数据节省时间

  1. 通常创建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
  1. object 类型像一个大的容器,不仅仅可以承载 str,也可以包含那些不能很好地融进一个数据类型的任何数据列,如果我们将日期作为 object 类型就会极大的影响效率;
  2. 对于时间序列的数据而言,要将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
  1. 特别地,如果数据源中的date_time不是ISO 8601 格式的,需要设置pd.to_datetime()中的format参数,进行格式化,否则pandas将使用dateutil 包把每个字符串str转化成date日期,速度并不是最快的,只有当date_time是ISO 8601 格式,pandas才可以立即使用最快速的方法来解析日期。

三、pandas数据的循环操作

  1. 基于上面的数据,如果需要根据amount列的值,构造一个新的列,要求:
    0 < 金额 <= 10000,返回:金额 * 0.3
    10000 < 金额 <= 100000,返回:金额 * 0.5
    100000 < 金额 <= 1000000,返回:金额 * 0.8
  2. 常规的代码做法(不赞同该做法)
  • 定义一个判断函数,写好条件的逻辑代码

 

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']执行所谓的链式索引,这通常会导致意外的结果;
  1. 使用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内部架构中内置的更快的语言完成;
  1. 使用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中调用,因此并不是那么快,特别是当数据量非常大的时候。
  1. 矢量化操作
  • 什么是矢量化操作?如果不需要基于一些条件,而是可以在一行代码中将所有金额应用于固定收益率(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,它可以帮助维持“层次结构”。
  1. 数据循环方法排名
  • 使用向量化操作:没有for循环的Pandas方法和函数;
  • 将apply()方法:与可调用方法一起使用;
  • 使用itertuples()或iterrows(),从Python的集合模块迭代DataFrame行;
  • 使用“element-by-element”循环:使用df.loc或df.iloc一次更新一个单元格或行。



作者:惑也
链接:https://www.jianshu.com/p/56a50a6c961c
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

上一篇:作业比赛编号 : 100000592 - 《算法笔记》5.5小节——数学问题->质因子分解 问题 E: 完数与盈数


下一篇:CCF201912-1报数