资讯专栏INFORMATION COLUMN

Pandas之旅(七) 谁说pandas慢

genedna / 1681人阅读

摘要:下面让我们开始提速假设我们现在的电价是定值,不根据用电时间段来改变,那么中最快的方法那就是采用,这就是一个简单的矢量化操作示范。它基本是在中运行最快的方式。

Pandas 加速

大家好,今天我们来看有关pandas加速的小技巧,不知道大家在刚刚接触pandas的时候有没有听过如下的说法

pandas太慢了,运行要等半天

其实我想说的是,慢不是pandas的错,大家要知道pandas本身是在Numpy上建立起来的包,在很多情况下是支持向量化运算的,而且还有C的底层设计,所以我今天
主要想从几个方面和大家分享一下pandas加速的小技巧,与往常一样,文章分成四部分,本文结构如下:

使用datetime类型来处理和时间序列有关的数据

批量计算的技巧

通过HDFStore存储数据节省时间

源码,相关数据及GitHub地址

现在就让我们开始吧

1. 使用datetime类型来处理和时间序列有关的数据

首先这里我们使用的数据源是一个电力消耗情况的数据(energy_cost.csv),非常贴近生活而且也是和时间息息相关的,用来做测试在合适不过了,这个csv文件大家可以在第四部分找到下载的地方哈

import os
# 这两行仅仅是切换路径,方便我上传Github,大家不用理会,只要确认csv文件和py文件再一起就行啦
os.chdir("F:Python教程segmentfaultpandas_sharePandas之旅_07 谁说pandas慢")

现在让我们看看数据大概长什么样子

import numpy as np
import pandas as pd
f"Using {pd.__name__},{pd.__version__}"
"Using pandas,0.23.0"



df = pd.read_csv("energy_cost.csv",sep=",")
df.head()
date_time energy_kwh
0 2001/1/13 0:00 0.586
1 2001/1/13 1:00 0.580
2 2001/1/13 2:00 0.572
3 2001/1/13 3:00 0.596
4 2001/1/13 4:00 0.592

现在我们看到初始数据的样子了,主要有date_time和energy_kwh这两列,来表示时间和消耗的电力,比较好理解,下面让我们来看一下数据类型

df.dtypes
>>> date_time      object
    energy_kwh    float64
    dtype: object
type(df.iat[0,0])
>>> str

这里有个小问题,Pandas和NumPy有dtypes(数据类型)的概念。如果未指定参数,则date_time这一列的数据类型默认object,所以为了之后运算方便,我们可以把str类型的这一列转化为timestamp类型:

df["date_time"] = pd.to_datetime(df["date_time"])
df.dtypes

>>> date_time     datetime64[ns]
    energy_kwh           float64
    dtype: object

先在大家可以发现我们通过用pd.to_datetime这个方法已经成功的把date_time这一列转化为了datetime64类型

df.head()
date_time energy_kwh
0 2001-01-13 00:00:00 0.586
1 2001-01-13 01:00:00 0.580
2 2001-01-13 02:00:00 0.572
3 2001-01-13 03:00:00 0.596
4 2001-01-13 04:00:00 0.592

现在再来看数据, 发现已经和刚才不同了,我们还可以通过指定format参数实现一样的效果,速度上也会快一些

%%timeit -n 10
def convert_with_format(df, column_name):
    return pd.to_datetime(df[column_name],format="%Y/%m/%d %H:%M")

df["date_time"]=convert_with_format(df, "date_time")

>>>722 µs ± 334 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

有关具体的日期自定义相关方法,大家点击这里查看

2. 批量计算的技巧

首先,我们假设根据用电的时间段不同,电费价目表如下:

Type cents/kwh periode
Peak 28 17:00 to 24:00
Shoulder 20 7:00 to 17:00
Off-Peak 12 0:00 to 7:00

假设我们想要计算出电费,我们可以先写出一个根据时间动态计算电费的方法“apply_tariff“

def apply_tariff(kwh, hour):
    """Calculates cost of electricity for given hour."""    
    if 0 <= hour < 7:
        rate = 12
    elif 7 <= hour < 17:
        rate = 20
    elif 17 <= hour < 24:
        rate = 28
    else:
        raise ValueError(f"Invalid hour: {hour}")
    return rate * kwh

好啦,现在我们想要在数据中新增一列 "cost_cents" 来表示总价钱,我们有很多选择,首先能想到的方法便是iterrows(),它可以让我们循环遍历Dataframe的每一行,根据条件计算并赋值给新增的‘cost_cents’列

iterrows()

首先我们能做的是循环遍历流程,让我们先用.iterrows()替代上面的方法来试试:

%%timeit -n 10
def apply_tariff_iterrows(df):
    energy_cost_list = []
    for index, row in df.iterrows():
        # Get electricity used and hour of day
        energy_used = row["energy_kwh"]
        hour = row["date_time"].hour
        # Append cost list
        energy_cost = apply_tariff(energy_used, hour)
        energy_cost_list.append(energy_cost)
    df["cost_cents"] = energy_cost_list

apply_tariff_iterrows(df)
983 ms ± 65.5 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

我们为了测试方便,所有的方法都会循环10次来比较耗时,这里很明显我们有很大的改进空间,下面我们用apply方法来优化

apply()
%%timeit -n 10
def apply_tariff_withapply(df):
    df["cost_cents"] = df.apply(
        lambda row: apply_tariff(
            kwh=row["energy_kwh"],
            hour=row["date_time"].hour),
        axis=1)

apply_tariff_withapply(df)
247 ms ± 24.3 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

这回速度得到了很大的提升,但是显然我们还没有get到pandas加速的精髓:矢量化操作。下面让我们开始提速

isin()

假设我们现在的电价是定值,不根据用电时间段来改变,那么pandas中最快的方法那就是采用(df["cost_cents"] = df["energy_kwh"] * price),这就是一个简单的矢量化操作示范。它基本是在Pandas中运行最快的方式。

目前的问题是我们的价格是动态的,那么如何将条件判断添加到Pandas中的矢量化运算中呢?答案就是我们根据条件选择和分组DataFrame,然后对每个选定的组应用矢量化操作:

#先让我们把时间序列作为索引
df.set_index("date_time", inplace=True)
%%timeit -n 10
def apply_tariff_isin(df):
    # Define hour range Boolean arrays
    peak_hours = df.index.hour.isin(range(17, 24))
    shoulder_hours = df.index.hour.isin(range(7, 17))
    off_peak_hours = df.index.hour.isin(range(0, 7))

    # Apply tariffs to hour ranges
    df.loc[peak_hours, "cost_cents"] = df.loc[peak_hours, "energy_kwh"] * 28
    df.loc[shoulder_hours,"cost_cents"] = df.loc[shoulder_hours, "energy_kwh"] * 20
    df.loc[off_peak_hours,"cost_cents"] = df.loc[off_peak_hours, "energy_kwh"] * 12

apply_tariff_isin(df)
5.7 ms ± 871 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

这回我们发现速度是真正起飞了,首先我们根据用电的三个时段把df进行分三组,再依次进行三次矢量化操作,大家可以发现最后减少了很多时间,原理很简单:

在运行的时候,.isin()方法返回一个布尔值数组,如下所示:

[False, False, False, ..., True, True, True]

接下来布尔数组传递给DataFrame的.loc索引器时,我们获得一个仅包含与3个用电时段匹配DataFrame切片。然后简单的进行乘法操作就行了,这样做的好处是我们已经不需要刚才提过的apply方法了,因为不在存在遍历所有行的问题

我们可以做的更好吗?

通过观察可以发现,在apply_tariff_isin()中,我们仍然在通过调用df.loc和df.index.hour.isin()来进行一些“手动工作”。如果想要进一步提速,我们可以使用cut方法

%%timeit -n 10
def apply_tariff_cut(df):
    cents_per_kwh = pd.cut(x=df.index.hour,
                           bins=[0, 7, 17, 24],
                           include_lowest=True,
                           labels=[12, 20, 28]).astype(int)
    df["cost_cents"] = cents_per_kwh * df["energy_kwh"]
140 ns ± 29.9 ns per loop (mean ± std. dev. of 7 runs, 10 loops each)

效果依然锋利,速度上有了成倍的提升

不要忘了用Numpy

众所周知,Pandas是在Numpy上建立起来的,所以在Numpy中当然有类似cut的方法可以实现分组,从速度上来讲差不太多

%%timeit -n 10
def apply_tariff_digitize(df):
    prices = np.array([12, 20, 28])
    bins = np.digitize(df.index.hour.values, bins=[7, 17, 24])
    df["cost_cents"] = prices[bins] * df["energy_kwh"].values
54.9 ns ± 19.3 ns per loop (mean ± std. dev. of 7 runs, 10 loops each)


正常情况下,以上的加速方法是能满足日常需要的,如果有特殊的需求,大家可以上网看看有没有相关的第三方加速包

3. 通过HDFStore存储数据节省时间

这里主要想强调的是节省预处理的时间,假设我们辛辛苦苦搭建了一些模型,但是每次运行之前都要进行一些预处理,比如类型转换,用时间序列做索引等,如果不用HDFStore的话每次都会花去不少时间,这里Python提供了一种解决方案,可以把经过预处理的数据存储为HDF5格式,方便我们下次运行时直接调用。

下面就让我们把本篇文章的df通过HDF5来存储一下:

# Create storage object with filename `processed_data`
data_store = pd.HDFStore("processed_data.h5")

# Put DataFrame into the object setting the key as "preprocessed_df"
data_store["preprocessed_df"] = df
data_store.close()

现在我们可以关机下班了,当明天接着上班后,通过key("preprocessed_df")就可以直接使用经过预处理的数据了

# Access data store
data_store = pd.HDFStore("processed_data.h5")

# Retrieve data using key
preprocessed_df = data_store["preprocessed_df"]
data_store.close()
preprocessed_df.head()
energy_kwh cost_cents
date_time
2001-01-13 00:00:00 0.586 7.032
2001-01-13 01:00:00 0.580 6.960
2001-01-13 02:00:00 0.572 6.864
2001-01-13 03:00:00 0.596 7.152
2001-01-13 04:00:00 0.592 7.104

如上图所示,现在我们可以发现date_time已经是处理为index了

4. 源码,相关数据及GitHub地址

这一期为大家分享了一些pandas加速的实用技巧,希望可以帮到各位小伙伴,当然,类似的技巧还有很多,但是核心思想应该一直围绕矢量化操作上,毕竟是基于Numpy上建立的包,如果大家有更好的办法,希望可以在我的文章底下留言哈

我把这一期的ipynb文件,py文件以及我们用到的energy_cost.csv放到了Github上,大家可以点击下面的链接来下载:

Github仓库地址: https://github.com/yaozeliang/pandas_share

希望大家能够继续支持我,这一篇文章已经是Pandas系列的最后一篇了,虽然一共只写了7篇文章,但是我认为从实用性上来讲并没有太逊色于收费课程(除了少了很多漂亮的ppt),接下来我会再接再厉,分享一下我对R (ggplot2)或者matplotlib的学习经验!!

Pandas之旅到此结束。撒花

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/43518.html

相关文章

  • Pandas之旅(三)最实用的Merge, Join,Concat方法详解

    摘要:基于上的我们还可以实现几个基于的,还是老样子,先让我们创建两个好了,现在我们想要实现两个的,但是条件是通过的和的这样我们也可以得到结果。 Merge, Join, Concat 大家好,我有回来啦,这周更新的有点慢,主要是因为我更新了个人简历哈哈,如果感兴趣的朋友可以去看看哈: 我的主页 个人认为还是很漂亮的~,不得不说,很多时候老外的设计能力还是很强。 好了,有点扯远了,这一期我想和...

    CloudwiseAPM 评论0 收藏0
  • Pandas之旅(四) : 可能是社区内最实用的Pandas技巧

    摘要:不为人知的七大实用技巧大家好,我今天勤快地回来了,这一期主要是和大家分享一些的实用技巧,会在日常生活中大大提升效率,希望可以帮助到大家还是老样子,先给大家奉上这一期的章节目录自定义选项,设置实用中模块构建测试数据巧用访问器合并其他列拼接使用 Pandas不为人知的七大实用技巧 大家好,我今天勤快地回来了,这一期主要是和大家分享一些pandas的实用技巧,会在日常生活中大大提升效率,希望...

    iflove 评论0 收藏0
  • Pandas之旅(一): 让我们把基础知识一次撸完,申精干货

    为什么你需要pandas 大家好,今天想和大家分享一下有关pandas的学习新的,我因工作需要,从去年12月开始接触这个非常好用的包,到现在为止也是算是熟悉了一些,因此发现了它的强大之处,特意想要和朋友们分享,特别是如果你每天和excel打交道,总是需要编写一些vba函数或者对行列进行groupby啊,merge,join啊之类的,相信我,pandas会让你解脱的。 好啦,闲话少说,这篇文章的基础...

    tuomao 评论0 收藏0
  • Pandas之旅(二): 有关数据清理的点点滴滴

    摘要:数据清洗大家好,这一期我将为大家带来我的学习心得第二期数据清理。这一期我会和大家分享一些比较好用常见的清洗方法。首先还是让我们来简单看一下本文将会用到的数据源这是一个超小型的房地产行业的数据集,大家会在文章最后找到下载地址。 数据清洗 大家好,这一期我将为大家带来我的pandas学习心得第二期:数据清理。这一步非常重要,一般在获取数据源之后,我们紧接着就要开始这一步,以便为了之后的各种...

    wenyiweb 评论0 收藏0
  • Pandas之旅(六): 字符串实用方法汇总

    摘要:有关字符串基本方法大家好,我又回来了之前的几期我们已经简单了解了的基础操作,但是只要涉及到数据,最常见的就是字符串类型,所以很多时候我们其实都在和字符串打交道,所以今天,我会把我自己总结的,有关字符串的常用方法分享给大家,希望能够帮到各位小 有关字符串基本方法 大家好,我又回来了! 之前的几期我们已经简单了解了pandas的基础操作,但是只要涉及到数据,最常见的就是String(字符串...

    高胜山 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<