美文网首页呆鸟的Python数据分析Python
Pandas进阶之提速遍历操作

Pandas进阶之提速遍历操作

作者: 惑也 | 来源:发表于2019-07-06 01:35 被阅读0次

    一、概念

    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一次更新一个单元格或行。

    相关文章

      网友评论

        本文标题:Pandas进阶之提速遍历操作

        本文链接:https://www.haomeiwen.com/subject/jiiehctx.html