【Python】为Pandas加速(适合Pandas中级开发者)

非常好的一篇文章,解决问题的方式和思路层层递进,透彻深刻。

Pandas是个好工具,好工具要用正确高效的方式使用,才能发挥出万钧之力。

英文水平较高可直接阅读原文。Fast, Flexible, Easy and Intuitive: How to Speed Up Your pandas Projects -- Real Python

逛了一下,这个原版国外网站内容很丰富,挺不错。

这里有个 Data Science 合集,点击可进入看更多文章:Python Data Science -- Real Python

其核心观点是使用Pandas的矢量化操作(也可翻译为:向量化计算)特性,而非使用极其原始的for循环,可大幅提升Pandas操作数据的性能。

本博客翻译如下:(意译为主,省略废话,代码全部保留)


前言

本文是一个使用Pandas的指南,以充分利用其强大且易于使用的内置功能。此外,您将学习一些实用的加快处理的技巧。

Python风格代码可能不是最有效率的。和NumPy库一样,pandas被设计用来进行向量化操作,一次处理整列或者一整个数据集。不要再最开始的时候,就考虑如何处理每一个单元格或每一行,而应该再试过其他全部方法之后。

本文主要涉及以下三个内容:

  • 使用 datetime 类型处理时间序列的优势

  • 批量计算的最有效途径

  • 使用HDFStore存储数据来节省时间

使用Pandas,有很多种方法可以实现从A到B,但是并不是所有方法都能高效的扩大至更大的数据量。

阅读本文的前提是需熟练掌握Pandas库的数据选择和切片等操作。(译者注:若对某些Pandas方法不熟,可使用Kimi.ai

案例

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

从CSV文件中读取数据,A列是时间,B列是耗电量(度)。

每一行包含每个小时使用的电量,因此全年有365 x 24=8760行。每行表示当时"起始小时"的使用情况,因此1/1/13 0:00表示2013年1月1日第一个小时的使用情况。

使用Datetime类型加速

python 复制代码
>>> import pandas as pd
>>> pd.__version__
'0.23.1'

# Make sure that `demand_profile.csv` is in your
# current working directory.
>>> df = pd.read_csv('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 类型:

python 复制代码
>>> df.dtypes
date_time      object
energy_kwh    float64
dtype: object

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

这不是最佳选择。 object 不仅是 str 类型的容器,任何没有合适数据类型的列都会被存放到 object 中。将日期数据作为字符串处理是非常低效的。(同时也很消耗内存)。

处理时间序列数据,更好的方法是将 date_time 列格式化为 datetime 对象的数组。(Pandas中称之为 Timestamp。)Pandas处理的更加简洁:

python 复制代码
>>> df['date_time'] = pd.to_datetime(df['date_time'])
>>> df['date_time'].dtype
datetime64[ns]

(注意:本例中你还可以使用 Pandas 的 PeriodIndex 索引。)

df 如下:

python 复制代码
>>> 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

如何测试代码耗时?可以使用一个 timing decorator,这里我们称之为 @timeit。这个 decorator 最大程度的模仿了 Python 标准库中的 timeit.repeat() ,但是它能返回函数执行结果,并打印多次试验的平均运行时长。(Python标准库的 timeit.repeat() 只能返回时间结果,而不含函数结果)。

python 复制代码
>>> @timeit(repeat=3, number=10)
... def convert(df, column_name):
...     return pd.to_datetime(df[column_name])

>>> # Read in again so that we have `object` dtype to start 
>>> df['date_time'] = convert(df, 'date_time')
Best of 3 trials with 10 function calls per trial:
Function `convert` ran in average of 1.610 seconds.

8760行数据耗费1.6秒。还不错,但是如果要处理更大的数据量,比如数据采集频率加快到每分钟采集一次数据,那么此时的全年用电量数据,将是现在数据量的60倍多,这个代码将运行1分钟30秒。

实际上,我最近分析了来自330个网站的共10年的每小时电量数据。如果只是用来转换时间数据的类型,就要等待88分钟,那难以想象!

那该如何加速呢?只需使用 format 参数告诉 Pandas 你的时间数据的具体格式即可。

python 复制代码
>>> @timeit(repeat=3, number=100)
>>> def convert_with_format(df, column_name):
...     return pd.to_datetime(df[column_name],
...                           format='%d/%m/%y %H:%M')
Best of 3 trials with 100 function calls per trial:
Function `convert_with_format` ran in average of 0.032 seconds.

现在的耗时是0.032秒,效率是前面的50倍。如果你要从330个网页中处理数据,你就能节省86分钟,很大的进步。

还有一点需要说明,CSV中的时间格式不是 ISO 8601 格式:YYYY-MM-DD HH:MM。如果你不指定格式,Pandas 会使用 dateutil 包将字符串转为日期。

相反,如果时间数据已经是 ISO 8601 格式,Pandas 可以立即解析为日期。这就是为什么格式化时间后效率大幅提升的一个原因。另一个方式是传递 infer_datetime_format = True 参数。

注意:Pandas 的read_csv()允许在读写文件的同时解析日期。请参阅parse_dates、infer_datetime_format和date_parser参数。

简化对Pandas数据的循环

现在日期和时间已经转成正确的格式,现在可以开始计算电费了。请记住,电费因小时而异,因此您需要根据每个小时的不同电费单价计算总电费。在本例中,每小时的电费定义如下:

电费类型 美分/度 时间段
高峰 28 17:00-24:00
平时 20 7:00-17:00
低谷 12 0:00-7:00

如果电价永远都是 28 美分/度,则代码就很简单:

python 复制代码
>>> df['cost_cents'] = df['energy_kwh'] * 28

这样 df 中会产生一个新的列,代表每个小时的电费:

python 复制代码
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 特性的方案。

先看看循环方法,循环对于不熟悉 Pandas 设计原则的初学者时最喜欢的方案。

写一个普通的循环方法。

python 复制代码
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

应用到 df 中:

python 复制代码
>>> # NOTE: Don't do this!
>>> @timeit(repeat=3, number=100)
... def apply_tariff_loop(df):
...     """Calculate costs in loop.  Modifies `df` inplace."""
...     energy_cost_list = []
...     for i in range(len(df)):
...         # Get electricity used and hour of day
...         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
... 
>>> apply_tariff_loop(df)
Best of 3 trials with 100 function calls per trial:
Function `apply_tariff_loop` ran in average of 3.152 seconds.

上面这段代码看着似乎没有大问题,但是这个循环显得很呆。也可以说时反模式的,反"Pandas"的模式。它有几个问题。

首先,它需要初始化一个列表,这个列表用来存储结果。

其次,它使用不准确(opaque)的对象 range(0,len(df)) 来作为循环计算,然后在应用apply_tariff() 之后,它必须将结果附加到用于创建新DataFrame列的列表中。它还使用df.iloc[i]['date_time']进行所谓的链式索引,这通常会导致意想不到的结果。

(译者注:在循环遍历 df 的时候,又修改 df ,可能导致意外错误)

但是最大的问题还是费时,8760行数据花费了3秒钟。下面看改进后的方法。

使用itertuples() 和 iterrows()方法循环

未完待续。

相关推荐
胖哥真不错7 分钟前
Python基于TensorFlow实现GRU-Transformer回归模型(GRU-Transformer回归算法)项目实战
python·gru·tensorflow·transformer·回归模型·项目实战·gru-transformer
长潇若雪9 分钟前
结构体(C 语言)
c语言·开发语言·经验分享·1024程序员节
feilieren11 分钟前
leetcode - 684. 冗余连接
java·开发语言·算法
chusheng184014 分钟前
Python Transformer 模型的基本原理:BERT 和 GPT 以及它们在情感分析中的应用
python·bert·transformer
Peter44719 分钟前
-bash: ./my_rename.sh: /bin/bash^M: bad interpreter: No such file or directory
开发语言·bash
The Future is mine22 分钟前
Java根据word模板导出数据
java·开发语言
ChinaDragonDreamer22 分钟前
HarmonyOS:@Watch装饰器:状态变量更改通知
开发语言·harmonyos·鸿蒙
一颗甜苞谷35 分钟前
开源一款前后端分离的企业级网站内容管理系统,支持站群管理、多平台静态化,多语言、全文检索的源码
java·开发语言·开源
星夜孤帆35 分钟前
Java面试题集锦
java·开发语言