提升效率的Python Pandas用法大全 – wiki基地


Pandas性能优化与效率提升:从入门到精通的实用指南

Python的Pandas库无疑是数据科学和数据分析领域的基石。它提供了强大、灵活且易于使用的数据结构(如DataFrame和Series),让数据清洗、转换、分析和可视化变得前所未有的简单。然而,随着处理的数据量从KB、MB级别跃升至GB甚至TB级别,很多初学者甚至有经验的用户都会遇到性能瓶瓶颈:一段在小数据集上运行顺畅的代码,在面对大数据时可能变得异常缓慢,甚至导致内存溢出。

效率就是生产力。在数据分析的工作流中,提升Pandas代码的执行效率,意味着更快的迭代速度、更强的模型探索能力以及更优的资源利用。本文将系统性地探讨提升Pandas效率的核心原则与实用技巧,旨在帮助您写出更高效、更“Pandas-onic”(符合Pandas设计哲学)的代码。

一、 优化的基石:正确选择数据类型 (Dtypes)

在Pandas中,性能优化的第一步,也是最容易被忽视的一步,就是为DataFrame的每一列选择最合适的数据类型(dtype)。默认情况下,Pandas在读取数据时可能会做出保守的选择,这往往导致不必要的内存消耗和计算降速。

1. 数值类型:向下转换 (Downcasting)

Pandas默认使用64位整数(int64)和64位浮点数(float64)来存储数值。然而,在很多场景下,我们并不需要如此高的精度。例如,一个表示年龄的列,其值通常不会超过200,使用int8(-128到127)或uint8(0到255)就足够了。

  • int64 vs int8: 一个int64数值占用8个字节,而int8仅占用1个字节。对于一个包含1000万行年龄数据的列,从int64转为int8,内存占用将从约76MB骤降至约9.5MB。

如何操作?

“`python
import pandas as pd
import numpy as np

创建一个示例DataFrame

df = pd.DataFrame({‘user_id’: range(1_000_000),
‘age’: np.random.randint(18, 65, size=1_000_000),
‘score’: np.random.uniform(0, 100, size=1_000_000)})

print(“原始内存占用:”)
print(df.info(memory_usage=’deep’))

向下转换数值类型

df[‘age’] = pd.to_numeric(df[‘age’], downcast=’integer’)
df[‘score’] = pd.to_numeric(df[‘score’], downcast=’float’)

print(“\n优化后内存占用:”)
print(df.info(memory_usage=’deep’))
``pd.to_numericdowncast`参数会自动选择能容纳数据范围的最小数值类型。

2. 对象类型 (Object) 的“陷阱”与 Category 类型

Pandas中最消耗内存的是object类型。它通常用于存储字符串,但其底层实现是指针数组,每个元素都是一个独立的Python对象,这带来了巨大的内存和计算开销。

如果一个object列包含大量重复的字符串(例如,城市名、产品类别、性别等),那么将其转换为category类型将带来惊人的性能提升。

category类型的工作原理是:它将原始的字符串值映射为一组唯一的整数“代码”,并保留原始字符串与整数代码之间的映射关系。这样,DataFrame中实际存储的是紧凑的整数数组,而非重复的字符串对象。

优势:
内存锐减:内存占用大幅降低。
计算加速:基于整数的运算(如groupby、排序sort_values、合并merge)远快于基于字符串的运算。

如何操作?
“`python

假设df中有一列 ‘city’

num_rows = 1_000_000
cities = [‘Beijing’, ‘Shanghai’, ‘Guangzhou’, ‘Shenzhen’, ‘Chengdu’]
df = pd.DataFrame({‘city’: np.random.choice(cities, size=num_rows)})

print(“原始内存占用 (object):”)
print(df.info(memory_usage=’deep’))

转换为Category类型

df[‘city’] = df[‘city’].astype(‘category’)

print(“\n优化后内存占用 (category):”)
print(df.info(memory_usage=’deep’))
“`
对于一个有100万行、5个独特城市名的列,内存占用可以从几十MB降低到仅几MB。

二、 告别循环,拥抱向量化 (Vectorization)

这是Pandas性能优化的黄金法则。如果你发现自己正在写一个for循环来遍历DataFrame的行,请立刻停下来,因为99%的情况下都有一个更高效的、向量化的替代方案。

向量化操作是指Pandas可以直接对整个Series或DataFrame进行运算,而无需显式的Python层循环。这些操作的底层通常是用C或Cython实现的,它们利用了CPU的并行计算能力,避免了Python解释器的巨大开销。

反面教材:使用循环
“`python

计算 A列和B列的和,存入C列

效率极低!

df = pd.DataFrame(np.random.randint(0, 100, size=(100000, 2)), columns=[‘A’, ‘B’])

错误的方式:使用循环

df[‘C’] = 0
for i in range(len(df)):
df.loc[i, ‘C’] = df.loc[i, ‘A’] + df.loc[i, ‘B’]
“`

正确姿势:向量化
“`python

向量化操作:简洁、高效

df[‘C’] = df[‘A’] + df[‘B’]
``
在一个百万行的数据集上,向量化操作的速度可能是
for`循环的数百甚至数千倍。

常见的向量化操作包括:
算术运算: +, -, *, /, **
逻辑运算: >, <, ==, & (与), | (或), ~ (非)
内置函数: df.sum(), df.mean(), np.log(df), df.abs()等。

三、 当循环不可避免:apply, itertuples, iterrows 的明智选择

尽管我们应极力避免循环,但在处理某些复杂逻辑(例如,一个无法被向量化的自定义函数)时,循环似乎在所难免。此时,选择正确的“循环”方式至关重要。

性能排序:向量化 >> apply (在某些情况下) > itertuples() > iterrows()

  1. iterrows(): 最慢的选择,尽量避免
    iterrows() 将每一行生成为一个Series对象。创建这些Series对象本身就带来了巨大的开销,并且它在数据类型转换方面也存在一些问题。

  2. itertuples(): 更好的迭代器
    itertuples() 将每一行生成为一个轻量级的命名元组(namedtuple)。它比iterrows()快得多,因为它避免了创建Series对象的开销,并且能更好地保留原始数据类型。

    “`python

    示例:使用 itertuples

    total = 0
    for row in df.itertuples(index=False, name=’Row’): # index=False避免返回索引,name=None可以返回普通元组
    total += row.A * row.B
    “`

  3. apply(): 灵活但需谨慎
    apply(axis=1) 可以将一个函数应用于DataFrame的每一行或每一列。它比itertuples()更灵活,可以直接传入复杂的函数。然而,apply在内部仍然是一个迭代过程,其性能通常介于itertuples和向量化之间。

    “`python
    def complex_logic(row):
    if row[‘A’] > 50:
    return row[‘B’] * 2
    else:
    return row[‘B’] / 2

    使用 apply

    df[‘D’] = df.apply(complex_logic, axis=1)
    ``
    **何时使用
    apply?**
    - 当你的逻辑涉及多个列,且无法通过现有向量化函数直接表达时。
    - 在使用
    apply之前,请再次思考:是否真的无法向量化?例如,上述complex_logic函数完全可以用np.where`或布尔索引实现,效率会高得多。

    “`python

    上述 apply 的向量化版本

    df[‘D_vectorized’] = np.where(df[‘A’] > 50, df[‘B’] * 2, df[‘B’] / 2)
    “`

四、 精准索引与高效查询

选择和过滤数据是日常操作的核心。使用正确的索引方法可以显著提升性能。

  1. loc, iloc, at, iat

    • loc / iloc: 用于基于标签(loc)或整数位置(iloc)选择多行/多列。它们是标准的、高效的选择方式。
    • at / iat: 是lociloc的“极速版”,专门用于访问单个标量值。如果你需要获取或设置DataFrame中的某一个单元格的值,请务必使用atiat,它们会跳过许多检查和对齐的开销,速度快得多。

    “`python

    value = df.loc[100, ‘A’]

    value = df.at[100, ‘A’]
    “`

  2. 布尔索引 (Boolean Indexing)
    这是Pandas最强大的特性之一,也是向量化查询的核心。它比使用apply或循环进行条件筛选快几个数量级。

    “`python

    筛选A列大于50且B列小于30的行

    filtered_df = df[(df[‘A’] > 50) & (df[‘B’] < 30)]
    “`

  3. query() 方法
    对于大型DataFrame,使用df.query()方法可能比传统的布尔索引更快、更节省内存,也更具可读性。query()将字符串表达式传递给底层的numexpr库进行评估,numexpr是一个优化的数值表达式求值器,可以避免创建大型的中间布尔数组。

    “`python

    使用 query 方法,可读性更强

    filtered_df_query = df.query(‘A > 50 and B < 30’)
    “`

五、 内存管理与I/O优化

处理大型数据集时,内存往往是第一个瓶颈。

  1. 高效读取CSV
    pd.read_csv() 是数据之旅的起点,优化它至关重要。

    • dtype 参数: 在读取时直接指定列的数据类型。这不仅避免了Pandas的类型推断(可能不准确且耗时),还能从一开始就节省内存。
    • usecols 参数: 如果你只需要文件中的部分列,请使用usecols只加载你需要的列。
    • chunksize 参数: 对于内存无法一次性容纳的超大文件,使用chunksize参数可以将其分块读取,返回一个迭代器。你可以在循环中逐块处理数据。

    “`python

    高效读取CSV示例

    chunk_iter = pd.read_csv(
    ‘large_dataset.csv’,
    dtype={‘user_id’: ‘int32’, ‘product_category’: ‘category’},
    usecols=[‘user_id’, ‘product_category’, ‘price’],
    chunksize=1_000_000
    )

    results = []
    for chunk in chunk_iter:
    # 对每个块进行处理
    processed_chunk = chunk[chunk[‘price’] > 100]
    results.append(processed_chunk)

    final_df = pd.concat(results)
    “`

  2. 选择更优的文件格式
    CSV是通用的文本格式,但对于性能和存储而言,它效率低下。在数据分析的中间环节,考虑使用更高效的二进制格式:

    • Parquet: 列式存储格式,压缩率高,读取速度快,尤其适合只读取部分列的场景。是大数据生态(如Spark, Hive)的标准格式。
    • Feather: 由Pandas作者Wes McKinney和RStudio的Hadley Wickham共同开发,设计目标就是实现Python和R之间数据框的极速读写。
    • HDF5: 适用于存储大量、多维度的科学数据。

六、 groupby 的进阶玩法

groupby是Pandas的性能猛兽。其内部的聚合函数(如sum, mean, count, size)都经过了高度优化(Cython化)。

  1. 使用 agg 进行多重聚合
    agg()方法可以让你对不同的列应用不同的聚合函数,或者对同一列应用多个函数。

    python
    agg_df = df.groupby('category_col').agg(
    total_sales=('sales_col', 'sum'),
    avg_price=('price_col', 'mean'),
    num_transactions=('price_col', 'count')
    )

  2. transform vs apply
    groupby对象上,apply可以执行非常灵活的组级别运算,但它可能很慢。如果你的目标是进行组级别的计算,然后将结果广播回原始DataFrame的形状,请使用transformtransform通常会利用优化的内置函数,速度远快于apply

    场景:计算每个类别的Z-score
    “`python

    慢速 apply 方法

    df.groupby(‘category’).apply(lambda g: (g[‘value’] – g[‘value’].mean()) / g[‘value’].std())

    快速 transform 方法

    group_means = df.groupby(‘category’)[‘value’].transform(‘mean’)
    group_stds = df.groupby(‘category’)[‘value’].transform(‘std’)
    df[‘z_score’] = (df[‘value’] – group_means) / group_stds
    “`

七、 超越Pandas:利用外部库加速

当单机Pandas的性能达到极限时,可以考虑以下工具:
Numba: 通过一个简单的@numba.jit装饰器,Numba可以将你的Python函数(尤其是包含数学运算和循环的函数)即时编译(JIT)成快速的机器码。它可以与apply结合,大幅提升自定义函数的执行速度。
Dask: 一个并行的计算库,它提供了与Pandas DataFrame非常相似的API。Dask可以将计算任务分解,并在多个CPU核心甚至多台机器上并行执行,非常适合处理“核外”(out-of-core)数据。
Modin: 旨在通过简单地更改一行导入代码(import modin.pandas as pd)来加速你的Pandas工作流。它在底层利用Dask或Ray来并行化Pandas操作。

结论:养成高效编码的思维习惯

提升Pandas效率不仅仅是记住几个函数或技巧,更重要的是建立一种性能优先的编码思维。在编写每一行代码时,都应思考以下问题:

  1. 数据类型对了吗? 是否有可以优化的空间?
  2. 我能向量化这个操作吗? 避免任何形式的显式循环。
  3. 如果必须循环,我用了最高效的迭代方式吗? (itertuples而非iterrows)
  4. 我的数据选择方式是最优的吗?at/iat用于单点,query用于大型查询)
  5. 我是否从I/O源头就开始考虑内存了? (read_csv的参数)
  6. 我的groupby操作是否充分利用了aggtransform等高效函数?

通过将这些原则内化于心,你将能够驾驭Pandas的全部力量,从容应对日益增长的数据挑战,让数据分析工作流如丝般顺滑。

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部