For 循环太慢,用Pandas速度提升300倍!

本文介绍了如何通过优化Python代码提高Pandas操作的性能,从最初的for循环和apply方法,逐步过渡到使用iterrows、.apply以及矢量化操作如.isin和pd.cut。通过实例展示了如何利用这些方法减少计算时间,最终使用NumPy的digitize函数实现了最快的速度。对于处理大规模数据时,理解并运用这些技巧能显著提升效率。
部署运行你感兴趣的模型镜像


大家好,我是小z

本篇分享一个常用的加速骚操作。

for是所有编程语言的基础语法,初学者为了快速实现功能,依懒性较强。但如果从运算时间性能上考虑可能不是特别好的选择。

本次介绍几个常见的提速方法,一个比一个快,了解pandas本质,才能知道如何提速。

下面是一个例子,数据获取方式见文末。

>>> import pandas as pd
# 导入数据集
>>> 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

基于上面的数据,我们现在要增加一个新的特征,但这个新的特征是基于一些时间条件生成的,根据时长(小时)而变化,如下:

因此,如果你不知道如何提速,那正常第一想法可能就是用apply方法写一个函数,函数里面写好时间条件的逻辑代码。

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'Invalid hour: {hour}')
    return rate * kwh

然后使用for循环来遍历df,根据apply函数逻辑添加新的特征,如下:

>>> # 不赞同这种操作
>>> @timeit(repeat=3, number=100)
... def apply_tariff_loop(df):
...     """用for循环计算enery cost,并添加到列表"""
...     energy_cost_list = []
...     for i in range(len(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
... 
>>> 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.

对于那些写Pythonic风格的人来说,这个设计看起来很自然。然而,这个循环将会严重影响效率。原因有几个:

首先,它需要初始化一个将记录输出的列表。

其次,它使用不透明对象范围(0,len(df))循环,然后再应用apply_tariff()之后,它必须将结果附加到用于创建新DataFrame列的列表中。另外,还使用df.iloc [i]['date_time']执行所谓的链式索引,这通常会导致意外的结果。

这种方法的最大问题是计算的时间成本。对于8760行数据,此循环花费了3秒钟。

接下来,一起看下优化的提速方案。

一、使用 iterrows循环

第一种可以通过pandas引入iterrows方法让效率更高。这些都是一次产生一行的生成器方法,类似scrapy中使用的yield用法。

.itertuples为每一行产生一个namedtuple,并且行的索引值作为元组的第一个元素。nametuplePythoncollections模块中的一种数据结构,其行为类似于Python元组,但具有可通过属性查找访问的字段。

.iterrowsDataFrame中的每一行产生(index,series)这样的元组。

在这个例子中使用.iterrows,我们看看这使用iterrows后效果如何。

>>> @timeit(repeat=3, number=100)
... def apply_tariff_iterrows(df):
...     energy_cost_list = []
...     for index, row in df.iterrows():
...         # 获取用电量和时间(小时)
...         energy_used = row['energy_kwh']
...         hour = row['date_time'].hour
...         # 添加cost列表
...         energy_cost = apply_tariff(energy_used, hour)
...         energy_cost_list.append(energy_cost)
...     df['cost_cents'] = energy_cost_list
...
>>> apply_tariff_iterrows(df)
Best of 3 trials with 100 function calls per trial:
Function `apply_tariff_iterrows` ran in average of 0.713 seconds.

这样的语法更明确,并且行值引用中的混乱更少,因此它更具可读性。

时间成本方面:快了近5倍!

但是,还有更多的改进空间,理想情况是可以用pandas内置更快的方法完成。

二、pandas的apply方法

我们可以使用.apply方法而不是.iterrows进一步改进此操作。pandas.apply方法接受函数callables并沿DataFrame的轴(所有行或所有列)应用。下面代码中,lambda函数将两列数据传递给apply_tariff()

>>> @timeit(repeat=3, number=100)
... 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)
Best of 3 trials with 100 function calls per trial:
Function `apply_tariff_withapply` ran in average of 0.272 seconds.

apply的语法优点很明显,行数少,代码可读性高。在这种情况下,所花费的时间大约是iterrows方法的一半。

但是,这还不是“非常快”。一个原因是apply()将在内部尝试循环遍历Cython迭代器。但是在这种情况下,传递的lambda不是可以在Cython中处理的东西,因此它在Python中调用并不是那么快。

如果我们使用apply()方法获取10年的小时数据,那么将需要大约15分钟的处理时间。如果这个计算只是大规模计算的一小部分,那么真的应该提速了。这也就是矢量化操作派上用场的地方。

三、矢量化操作:使用.isin选择数据

什么是矢量化操作?

如果你不基于一些条件,而是可以在一行代码中将所有电力消耗数据应用于该价格:df ['energy_kwh'] * 28,类似这种。那么这个特定的操作就是矢量化操作的一个例子,它是在pandas中执行的最快方法。

但是如何将条件计算应用为pandas中的矢量化运算?

一个技巧是:根据你的条件,选择和分组DataFrame,然后对每个选定的组应用矢量化操作。

在下面代码中,我们将看到如何使用pandas.isin()方法选择行,然后在矢量化操作中实现新特征的添加。在执行此操作之前,如果将date_time列设置为DataFrame的索引,会更方便:

# 将date_time列设置为DataFrame的索引
df.set_index('date_time', inplace=True)

@timeit(repeat=3, number=100)
def apply_tariff_isin(df):
    # 定义小时范围Boolean数组
    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_traffic函数中的定义
    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)
Best of 3 trials with 100 function calls per trial:
Function `apply_tariff_isin` ran in average of 0.010 seconds.

提示,上面.isin()方法返回的是一个布尔值数组,如下:

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

布尔值标识了DataFrame索引datetimes是否落在了指定的小时范围内。然后把这些布尔数组传递给DataFrame.loc,将获得一个与这些小时匹配的DataFrame切片。然后再将切片乘以适当的费率,这就是一种快速的矢量化操作了。

上面的方法完全取代了我们最开始自定义的函数apply_tariff(),代码大大减少,同时速度起飞。

运行时间比Pythonic的for循环快315倍,比iterrows快71倍,比apply快27倍!

四、还能更快?

太刺激了,我们继续加速。

在上面apply_tariff_isin中,我们通过调用df.locdf.index.hour.isin三次来进行一些手动调整。如果我们有更精细的时间范围,你可能会说这个解决方案是不可扩展的。但在这种情况下,我们可以使用pandaspd.cut()函数来自动完成切割:

@timeit(repeat=3, number=100)
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']

上面代码pd.cut()会根据bin列表应用分组。

其中include_lowest参数表示第一个间隔是否应该是包含左边的。

这是一种完全矢量化的方法,它在时间方面是最快的:

>>> apply_tariff_cut(df)
Best of 3 trials with 100 function calls per trial:
Function `apply_tariff_cut` ran in average of 0.003 seconds.

到目前为止,使用pandas处理的时间上基本快达到极限了!只需要花费不到一秒的时间即可处理完整的10年的小时数据集。

但是,最后一个其它选择,就是使用 NumPy,还可以更快!

五、使用Numpy继续加速

使用pandas时不应忘记的一点是PandasSeriesDataFrames是在NumPy库之上设计的。并且,pandas可以与NumPy阵列和操作无缝衔接。

下面我们使用NumPy的 digitize()函数更进一步。它类似于上面pandascut(),因为数据将被分箱,但这次它将由一个索引数组表示,这些索引表示每小时所属的bin。然后将这些索引应用于价格数组:

@timeit(repeat=3, number=100)
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

cut函数一样,这种语法非常简洁易读。

>>> apply_tariff_digitize(df)
Best of 3 trials with 100 function calls per trial:
Function `apply_tariff_digitize` ran in average of 0.002 seconds.

0.002秒! 虽然仍有性能提升,但已经很边缘化了。

以上就是本次加速的技巧分享。

案例数据源:

链接:https://pan.baidu.com/s/1-pykKdgXRvPVaEajBFrb0g,提取码:uzyo

详解用户流失原因分析该如何入手?

12000+字超详细 SQL 语法速成!

后台回复“入群”即可加入小z干货交流群

您可能感兴趣的与本文相关的镜像

Python3.8

Python3.8

Conda
Python

Python 是一种高级、解释型、通用的编程语言,以其简洁易读的语法而闻名,适用于广泛的应用,包括Web开发、数据分析、人工智能和自动化脚本

<think>首先,用户的问题是:“我想了解在for循环中使用pandas.read_excel函数是否会造成性能问题 请问在for循环里使用pandas.read_excel函数是否会有性能问题”。这直接询问在for循环中使用pd.read_excel的性能影响。 用户提供了几个引用,但我需要基于这些引用和我的知识来回答。引用讨论了pandas的read_excel和to_excel,但没有直接提到在for循环中使用它们。引用[3]提到了xlrd错误,引用[4]展示了read_excel的使用示例。 关键点: - pd.read_excel 读取Excel文件,这可能涉及磁盘I/O和解析,因此可能是计算密集型的。 - 在for循环中反复调用它可能会导致性能问题,特别是如果Excel文件很大或循环次数很多。 系统指令: - 所有行内数学表达式使用 $...$ 格式。 - 独立公式使用 $$...$$ 并单独成段。 - 使用中文回答。 - 生成相关问题。 - 回答中引用的段落末尾自然地添加引用标识,例如 [^1]。 - 回答结构清晰,帮助用户逐步解决问题,保证真实可靠。 回答结构: 1. **直接回答问题**:明确指出在for循环中使用pd.read_excel可能会导致性能问题。 2. **解释原因**: - 每次调用read_excel都会打开文件、解析内容(可能涉及xlrd或openpyxl后端),这很耗时。 - 磁盘I/O是瓶颈;反复读取相同文件效率低。 - 内存使用:如果文件大,每个读取操作都创建一个新的DataFrame,占用更多内存。 3. **提供解决方案**: - 避免在循环中多次读取相同文件。最好一次性读取所有需要的数据。 - 如果必须处理多个文件或sheet,使用参数如sheet_name=None读取所有sheet到一个字典。 - 优化:使用pd.concat或其他方法合并数据。 4. **参考引用**:自然地整合引用。 - 引用[1]强调高效操作,提到“掌握read_excel的精髓”,我可以引用这个来支持优化建议。 - 引用[2]讨论读取多个sheet页,建议一次性读取。 - 引用[3]关于错误处理,但不太相关,除非错误影响性能。 - 引用[4]展示read_excel的使用。 5. **生成相关问题**:在结尾添加相关问题。 确保回答真实可靠: - 基于pandas文档和常识:在循环中反复读取文件是低效的。推荐一次性读取。 最终回答大纲: 1. 介绍问题:是的,在for循环中使用pd.read_excel可能导致显著性能下降。 2. 原因:解释I/O开销、解析时间、内存使用。 3. 最佳实践:建议一次性读取所有数据,或使用参数处理多个sheet。 4. 示例:提供代码片段展示高效方式。 5. 引用:在相关点添加引用标识。 6. 相关问题:生成2-3个相关问题。 相关问题想法: 1. 如何优化在pandas中处理多个Excel文件? 2. 有哪些替代read_excel的方法来提高性能? 3. 在读取大型Excel文件时应注意什么? </think> ### 在 for 循环中使用 pandas.read_excel 的性能问题分析 是的,在 for 循环中反复调用 `pandas.read_excel` 函数**很可能会造成显著的性能问题**。这主要源于 Excel 文件的读取机制,涉及磁盘 I/O、文件解析和数据加载的开销。下面我将逐步解释原因、提供优化建议,并说明如何避免常见陷阱。 #### 1. **为什么会有性能问题?** - **磁盘 I/O 开销**:每次调用 `read_excel` 都需要打开文件、读取数据并关闭文件。如果循环次数多(例如读取多个文件或同一文件的多个 sheet),反复的磁盘访问会成为瓶颈,尤其是大文件或慢速存储设备上。磁盘 I/O 通常比内存操作慢 10-100 。 - **解析成本**:Excel 文件解析(依赖于后端库如 `openpyxl` 或 `xlrd`)涉及复杂计算(如公式处理、格式解析)。多次解析会累积 CPU 时间,导致处理速度下降。 - **内存占用**:每次读取都会创建一个新的 DataFrame 对象。如果循环中处理大量文件或大文件,内存使用量会急剧增加,可能引发内存溢出错误(OOM)。 - **实际影响**:假设读取一个 10MB Excel 文件耗时 0.5 秒,循环 100 次将耗时约 50 秒;而优化后可能只需几秒。对于需要高效数据处理的任务,这种延迟是不可接受的[^1][^4]。 #### 2. **优化建议:避免在循环中多次读取** 核心原则是**减少文件读取次数**。以下是高效方法: - **一次性读取所有 sheet 或文件**: 如果循环目标是处理同一 Excel 文件中的多个 sheet,使用 `sheet_name=None` 参数一次性读取所有 sheet 到一个字典中,避免循环调用。 ```python import pandas as pd # 一次性读取所有 sheet all_sheets = pd.read_excel('data.xlsx', sheet_name=None) # 返回字典 {sheet_name: DataFrame} for sheet_name, df in all_sheets.items(): # 处理每个 sheet 的数据 process_data(df) ``` 这种方式显著减少 I/O 次数[^2][^4]。 - **合并多个文件后再处理**: 如果循环用于读取多个 Excel 文件,先用列表存储路径,然后一次性读取并合并。 ```python file_paths = ['file1.xlsx', 'file2.xlsx', 'file3.xlsx'] all_data = [] for path in file_paths: # 仅读取一次,避免在循环中多次打开文件 df = pd.read_excel(path) all_data.append(df) # 合并所有 DataFrame combined_df = pd.concat(all_data, ignore_index=True) ``` - **使用 `pd.concat` 或迭代器优化**: 对于超大文件,考虑 `pd.read_excel` 的 `chunksize` 参数分块读取(但仅支持部分格式),或先用 `openpyxl` 直接处理工作簿。 #### 3. **性能对比示例** 假设有两个方法处理同一文件的 5 个 sheet: - **低效方法(循环内读取)**: ```python for i in range(5): df = pd.read_excel('data.xlsx', sheet_name=i) # 每次循环都重新读取文件 # 处理数据 ``` **耗时**:约 5 × 单次读取时间(例如 2.5 秒)。 - **高效方法(一次性读取)**: ```python all_sheets = pd.read_excel('data.xlsx', sheet_name=None) for df in all_sheets.values(): # 处理数据 ``` **耗时**:接近单次读取时间(例如 0.5 秒),性能提升 80% 以上[^2]。 #### 4. **其他注意事项** - **错误处理**:在循环中反复读取可能增加错误风险(如文件锁定或格式问题)。确保文件路径正确,避免 `xlrd.biffh.XLRDError` 等常见错误[^3]。 - **内存管理**:读取后及时用 `del df` 或 `gc.collect()` 释放内存,尤其在大数据场景。 - **工具选择**:对于极高性能需求,考虑替代方案如: - 将 Excel 转换为 CSV 后用 `pd.read_csv`(更快)。 - 使用数据库(如 SQLite)存储数据,避免文件 I/O。 总之,`read_excel` 在循环中调用会放大性能瓶颈。通过批量读取和合并数据,您能显著提升效率,让数据处理更流畅[^4]。实践中,结合 `to_excel` 输出时也适用类似原则。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值