当前位置: 首页 > news >正文

【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类型加速

>>> 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 类型:

>>> 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处理的更加简洁:

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

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

df 如下:

>>> 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() 只能返回时间结果,而不含函数结果)。

>>> @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 你的时间数据的具体格式即可。

>>> @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数据的循环

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

电费类型美分/度 时间段
高峰2817:00-24:00
平时207:00-17:00
低谷120:00-7:00

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

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

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

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 设计原则的初学者时最喜欢的方案。

写一个普通的循环方法。

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

应用到 df 中:

>>> # 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()方法循环

未完待续。


http://www.mrgr.cn/news/59968.html

相关文章:

  • Unity性能优化2【脚本篇】
  • python笔记一
  • 快速搭建SpringBoot3+Prometheus+Grafana
  • 搭建 mongodb 副本集,很详细
  • 【深度学习】【OpenVINO】【C++】模型转化、环境搭建以及模型部署的详细教程
  • vue3.0 + vite打包完成后,将dist下的资源包打包成zip的两种方法
  • PG数据库之数据类型入门
  • 【mysql】什么是当前读
  • JMeter 接口和性能测试常用函数最全解析!
  • ICP之点云特征计算
  • 只需要写几行 SQL,这个网站就搭好了?
  • shell脚本每日一练4
  • GitHub 上传项目保姆级教程
  • 【C++单调栈 贡献法】907. 子数组的最小值之和|1975
  • python基于django线上视频学习系统设计与实现_j0189d4x
  • 【Linux系统编程】——Linux入门指南:从零开始掌握操作系统的核心(指令篇)
  • 基于SpringBoot的中药材进存销管理系统设计与实现
  • 在浏览器中运行 Puppeteer:解锁新能力
  • React 中组件通信的几种主要方式
  • Python实现摇号系统
  • 还没想好说什么
  • Linux:指令再认识
  • 【在WindoWs 10 cmd查询管理目录下所有文件及其相对位置】
  • C语言基础题(大合集2)
  • 重学SpringBoot3-Spring WebFlux之SSE服务器发送事件
  • Rust中常用的命令工具