Pandas数据处理加速技巧汇总

Pandas 处理数据的效率还是很优秀的,相对于大规模的数据集只要掌握好正确的方法,就能让在数据处理时间上节省很多很多的时间。

Pandas 是建立在 NumPy 数组结构之上的,许多操作都是在 C 中执行的,要么通过 NumPy,要么通过 Pandas 自己的 Python 扩展模块库,这些模块用 Cython 编写并编译为 C。理论上来说处理速度应该是很快的。

那么为什么同样一份数据由2个人处理,在设备相同的情况下处理时间会出现天差地别呢?

需要明确的是,这不是关于如何过度优化 Pandas 代码的指南。如果使用得当 Pandas 已经构建为可以快速运行。此外优化和编写干净的代码之间存在很大差异。

这是以 Python 方式使用 Pandas 以充分利用其强大且易于使用的内置功能的指南。

数据准备

此示例的目标是应用分时能源关税来计算一年的能源消耗总成本。 也就是说,在一天中的不同时间,电价会有所不同,因此任务是将每小时消耗的电量乘以消耗该小时的正确价格。

从一个包含两列的 CSV 文件中读取数据,一列用于日期加时间,另一列用于以千瓦时 (kWh) 为单位消耗的电能。

Pandas数据处理加速技巧汇总_第1张图片

日期时间数据优化

Pandas数据处理加速技巧汇总_第2张图片

import pandas as pd
df = pd.read_csv('数据科学必备Pandas实操数据处理加速技巧汇总/demand_profile.csv')
df.head()
     date_time  energy_kwh
0  1/1/13 0:00       0.586
1  1/1/13 1:00       0.580
2  1/1/13 2:00       0.572
3  1/1/13 3:00       0.596
4  1/1/13 4:00       0.592

乍一看这看起来不错,但有一个小问题。 Pandas 和 NumPy 有一个 dtypes(数据类型)的概念。 如果未指定任何参数,则 date_time 将采用 object dtype。

df.dtypes
date_time      object
energy_kwh    float64
dtype: object

type(df.iat[0, 0])
str

object 不仅是 str 的容器,而且是任何不能完全适合一种数据类型的列的容器。将日期作为字符串处理会既费力又低效(这也会导致内存效率低下)。为了处理时间序列数据,需要将 date_time 列格式化为日期时间对象数组( Timestamp)。

df['date_time'] = pd.to_datetime(df['date_time'])
df['date_time'].dtype
datetime64[ns]

现在有一个名为 df 的 DataFrame,有两列和一个用于引用行的数字索引。

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

使用 Jupyter 自带的 %%time 计时装饰器进行测试。

def convert(df, column_name):
	return pd.to_datetime(df[column_name])

%%time
df['date_time'] = convert(df, 'date_time')

Wall time: 663 ms

def convert_with_format(df, column_name):
	return pd.to_datetime(df[column_name],format='%d/%m/%y %H:%M')

%%time
df['date_time'] = convert(df, 'date_time')

Wall time: 1.99 ms

处理效率提高将近350倍。如果在处理大规模数据的情况下,处理数据的时间会无限的放大。

数据的简单循环

既然日期和时间格式处理完毕,就可以着手计算电费了。成本因小时而异,因此需要有条件地将成本因素应用于一天中的每个小时。

在此示例中,使用时间成本将定义成三个部分。

data_type = {
    # 高峰
    "Peak":{"Cents per kWh":28,"Time Range":"17:00 to 24:00"},
    # 正常时段
    "Shoulder":{"Cents per kWh":20,"Time Range":"7:00 to 17:00"},
    # 非高峰
    "Off-Peak":{"Cents per kWh":12,"Time Range":"0:00 to 7:00"}, 
}

如果价格是一天中每小时每千瓦时 28 美分。

df['cost_cents'] = df['energy_kwh'] * 28

               date_time    energy_kwh       cost_cents
0    2013-01-01 00:00:00         0.586           16.408
1    2013-01-01 01:00:00         0.580           16.240
2    2013-01-01 02:00:00         0.572           16.016
3    2013-01-01 03:00:00         0.596           16.688
4    2013-01-01 04:00:00         0.592           16.576
...

但是成本计算取决于一天中的不同时间。这就是你会看到很多人以意想不到的方式使用 Pandas 的地方,通过编写一个循环来进行条件计算。

def apply_tariff(kwh, hour):
    """计算给定小时的电费"""    
    if 0 <= hour < 7:
        rate = 12
    elif 7 <= hour < 17:
        rate = 20
    elif 17 <= hour < 24:
        rate = 28
    else:
        raise ValueError(f'无效时间: {hour}')
    return rate * kwh

def apply_tariff(kwh, hour):
    """计算给定小时的电费"""    
    if 0 <= hour < 7:
        rate = 12
    elif 7 <= hour < 17:
        rate = 20
    elif 17 <= hour < 24:
        rate = 28
    else:
        raise ValueError(f'无效时间: {hour}')
    return rate * kwh

def apply_tariff_loop(df):
    energy_cost_list = []
    for i in range(len(df)):
    	# 循环数据直接修改df
        energy_used = df.iloc[i]['energy_kwh']
        hour = df.iloc[i]['date_time'].hour
        energy_cost = apply_tariff(energy_used, hour)
        energy_cost_list.append(energy_cost)

    df['cost_cents'] = energy_cost_list

Wall time: 2.59 s

循环 .itertuples() 和 .iterrows() 方法

Pandas 实际上 for i in range(len(df)) 通过引入 DataFrame.itertuples() 和 DataFrame.iterrows() 方法使语法就可能显得多余,这些都是yield一次一行的生成器方法。

.itertuples() 为每一行生成一个命名元组,行的索引值作为元组的第一个元素。 名称元组是来自 Python 集合模块的数据结构,其行为类似于 Python 元组,但具有可通过属性查找访问的字段。

.iterrows() 为 DataFrame 中的每一行生成 (index, Series) 对(元组)。

def apply_tariff_iterrows(df):
    energy_cost_list = []
    for index, row in df.iterrows():
        energy_used = row['energy_kwh']
        hour = row['date_time'].hour
        energy_cost = apply_tariff(energy_used, hour)
        energy_cost_list.append(energy_cost)
    df['cost_cents'] = energy_cost_list

%%time
apply_tariff_iterrows(df)

Wall time: 808 ms

速度提高又3倍之多。

.apply() 方法

可以使用 .apply() 方法进一步改进此操作。 Pandas 的 .apply() 方法采用函数(可调用对象)并将它们沿 DataFrame 的轴(所有行或所有列)应用。

lambda 函数将两列数据传递给 apply_tariff()。

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)

%%time
apply_tariff_withapply(df)

Wall time: 181 ms

.apply() 的语法优势很明显,代码简洁、易读、明确。在这种情况下所用时间大约是该 .iterrows() 方法的4分之一。

.isin() 数据选择

但是如何在 Pandas 中将条件计算应用为矢量化操作呢?一个技巧是根据的条件选择和分组 DataFrame 的部分,然后对每个选定的组应用矢量化操作。

使用 Pandas 的.isin()方法选择行,然后在矢量化操作中应用。在执行此操作之前,如果将 date_time 列设置为 DataFrame 的索引会更方便。

df.set_index('date_time', inplace=True)

def apply_tariff_isin(df):
    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))

    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

%%time
apply_tariff_isin(df)

Wall time: 53.5 ms

其中整个过程方法返回一个布尔列表。

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

.cut() 数据分箱

设置时间切分的列表和对那个计算的函数公式,让操作起来更简单,但是这个对于新手来说代码阅读起来有些困难。

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']
    
%%time
apply_tariff_cut(df)

Wall time: 2.99 ms

Numpy 方法处理

Pandas Series 和 DataFrames 是在 NumPy 库之上设计的。这为提供了更大的计算灵活性,因为 Pandas 可以与 NumPy 数组和操作无缝协作。

使用 NumPy 的digitize()函数。它与 Pandas 的相似之处cut()在于数据将被分箱,但这次它将由一个索引数组表示,该数组表示每个小时属于哪个箱。然后将这些索引应用于价格数组。

import numpy as np

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

%%time
apply_tariff_digitize(df)

Wall time: 1.99 ms

处理效率比较

对比一下上面几种不同的处理方式的效率吧。

Pandas数据处理加速技巧汇总_第3张图片

功能 运行时间(秒)
apply_tariff_loop() 2.59 s
apply_tariff_iterrows() 808 ms
apply_tariff_withapply() 181 ms
apply_tariff_isin() 53.5 ms
apply_tariff_cut() 2.99 ms
apply_tariff_digitize() 1.99 ms

HDFStore 防止重新处理

通常构建复杂的数据模型时,对数据进行一些预处理会很方便。如果有 10 年的分钟频率用电量数据,即指定了格式参数简单地将日期和时间转换为日期时间也可能需要 20 分钟。只需要这样做一次而不是每次运行模型时都需要进行测试或分析。

可以在这里做的一件非常有用的事情是预处理,然后以处理后的形式存储数据,以便在需要时使用。但是如何才能以正确的格式存储数据而无需再次重新处理呢?如果要保存为 CSV 只会丢失您的日期时间对象,并且在再次访问时必须重新处理它。

Pandas 有一个内置的解决方案使用 HDF5,一种专为存储表格数据数组而设计的高性能存储格式。Pandas 的 HDFStore 类允许将 DataFrame 存储在 HDF5 文件中,以便可以有效地访问它,同时仍保留列类型和其他元数据。dict 是一个类似字典的类,因此可以像对 Python对象一样进行读写。

将预处理的耗电量 DataFrame 存储df在 HDF5 文件中。

data_store = pd.HDFStore('processed_data.h5')

# 将 DataFrame 放入对象中,将键设置为 preprocessed_df 
data_store['preprocessed_df'] = df
data_store.close()

从 HDF5 文件访问数据的方法,并保留数据类型。

data_store = pd.HDFStore('processed_data.h5')

preprocessed_df = data_store['preprocessed_df']
data_store.close()

到此这篇关于Pandas数据处理加速技巧汇总的文章就介绍到这了,更多相关Pandas数据处理内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

你可能感兴趣的:(Pandas数据处理加速技巧汇总)