`Pandas`的`性能`调优:`数据类型`优化、`矢量化`操作与`查询`优化。

好的,下面是一篇关于Pandas性能调优的文章,包括数据类型优化、矢量化操作和查询优化,以讲座模式呈现。

Pandas 性能调优:数据类型优化、矢量化操作与查询优化

大家好,今天我们来聊聊 Pandas 性能调优的一些关键技巧。Pandas 是 Python 中用于数据分析的强大库,但在处理大型数据集时,性能往往成为一个瓶颈。本次讲座将重点介绍三种主要的优化策略:数据类型优化、矢量化操作以及查询优化。

一、数据类型优化

1.1 理解 Pandas 数据类型

Pandas 提供了多种数据类型,包括 int, float, object, category, datetime64, bool 等。选择合适的数据类型对于内存占用和性能至关重要。

Pandas 数据类型 描述
int8, int16, int32, int64 有符号整数类型,数字越大,能表示的范围越大。int8 占用 1 字节,int16 占用 2 字节,以此类推。
uint8, uint16, uint32, uint64 无符号整数类型,只能表示非负整数。
float16, float32, float64 浮点数类型,float16 精度较低,占用空间小,float64 精度较高,占用空间大。
object 通常用于存储字符串或其他混合类型的数据。在 Pandas 中,object 类型通常会消耗大量内存,因为它实际上存储的是 Python 对象指针。
category 用于表示有限数量的重复值。可以将字符串或数值转换为 category 类型,从而显著减少内存占用和提高性能。
datetime64 用于存储日期和时间。
bool 用于存储布尔值(True 或 False)。

1.2 查看数据类型

使用 DataFrame.dtypes 属性可以查看 DataFrame 中每列的数据类型。

import pandas as pd
import numpy as np

# 创建一个示例 DataFrame
data = {'col1': [1, 2, 3, 4, 5],
        'col2': [1.1, 2.2, 3.3, 4.4, 5.5],
        'col3': ['a', 'b', 'c', 'd', 'e'],
        'col4': [True, False, True, False, True]}
df = pd.DataFrame(data)

# 查看数据类型
print(df.dtypes)

输出:

col1      int64
col2    float64
col3     object
col4     object
dtype: object

注意,col4的数据类型是object,这是因为Pandas自动推断为object类型,但实际上它应该是bool类型。

1.3 数据类型转换

Pandas 提供了多种方法进行数据类型转换:

  • DataFrame.astype(): 将 DataFrame 或 Series 转换为指定的数据类型。
  • pd.to_numeric(): 将 Series 转换为数值类型。
  • pd.to_datetime(): 将 Series 转换为日期时间类型。
  • Series.cat.as_ordered(): 将 Category 类型设置为有序。
  • Series.cat.reorder_categories(): 重新排序 Category 类型。
  • Series.cat.add_categories():添加新的category
1.3.1 数值类型转换

如果你的数据包含整数,但 Pandas 默认将其存储为 int64,你可以将其转换为更小的整数类型(如 int32int16)以节省内存。

# 将 col1 转换为 int8
df['col1'] = df['col1'].astype('int8')
print(df.dtypes)

输出:

col1       int8
col2    float64
col3     object
col4     object
dtype: object

同样,对于浮点数,你可以选择 float32 而不是 float64

# 将 col2 转换为 float32
df['col2'] = df['col2'].astype('float32')
print(df.dtypes)

输出:

col1       int8
col2    float32
col3     object
col4     object
dtype: object
1.3.2 对象类型转换为 Category 类型

如果你的数据包含大量的重复字符串,将其转换为 category 类型可以显著减少内存占用。

# 将 col3 转换为 category 类型
df['col3'] = df['col3'].astype('category')
print(df.dtypes)

输出:

col1       int8
col2    float32
col3  category
col4     object
dtype: object
1.3.3 对象类型转换为 bool 类型

如果你的数据包含True或者False,将其转换为 bool 类型可以减少内存占用。

# 将 col4 转换为 bool 类型
df['col4'] = df['col4'].astype('bool')
print(df.dtypes)

输出:

col1       int8
col2    float32
col3  category
col4      bool
dtype: object
1.3.4 日期时间类型转换

将字符串转换为日期时间类型,可以使用pd.to_datetime()

dates = ['2023-01-01', '2023-01-02', '2023-01-03']
dates_series = pd.Series(dates)
dates_series = pd.to_datetime(dates_series)
print(dates_series.dtype)

输出:

datetime64[ns]

1.4 节省内存的技巧

  • 一次性读取并转换: 在读取 CSV 文件时,可以使用 dtype 参数一次性指定每列的数据类型。

    # 读取 CSV 文件并指定数据类型
    df = pd.read_csv('your_data.csv', dtype={'col1': 'int8', 'col2': 'float32', 'col3': 'category'})
  • 分块读取: 对于非常大的文件,可以分块读取,并在每个块中进行数据类型转换。

    # 分块读取 CSV 文件
    for chunk in pd.read_csv('your_data.csv', chunksize=100000):
        chunk['col1'] = chunk['col1'].astype('int8')
        # 处理 chunk
  • 缺失值处理: NaN 值会影响数据类型。如果某一列包含 NaN 值,即使其他值都是整数,该列也可能被存储为 float64。因此,在转换数据类型之前,可以考虑填充或删除 NaN 值。

二、矢量化操作

2.1 什么是矢量化操作

矢量化操作是指对整个数组或 Series 执行操作,而不是逐个元素进行处理。Pandas 和 NumPy 都是为矢量化操作而设计的,因此利用矢量化操作可以显著提高性能。

2.2 避免循环

在 Pandas 中,应尽量避免使用循环(如 for 循环)来处理数据。循环通常效率较低,特别是对于大型数据集。

# 避免使用循环
data = {'col1': range(1000000)}
df = pd.DataFrame(data)

# 使用循环计算平方
def square_loop(df):
    for i in range(len(df)):
        df['col2'] = df['col1']**2
    return df

# 使用矢量化操作计算平方
def square_vectorized(df):
    df['col2'] = df['col1']**2
    return df

import time

start_time = time.time()
square_loop(df.copy()) # 使用 copy() 防止修改原始 DataFrame
end_time = time.time()
print(f"Loop time: {end_time - start_time:.4f} seconds")

start_time = time.time()
square_vectorized(df.copy())
end_time = time.time()
print(f"Vectorized time: {end_time - start_time:.4f} seconds")

通常矢量化操作比循环快很多。

2.3 使用 Pandas 内置函数

Pandas 提供了大量的内置函数,这些函数都经过优化,可以高效地执行各种操作。尽量使用这些内置函数,而不是自己编写循环或自定义函数。

# 使用 Pandas 内置函数
data = {'col1': np.random.rand(1000000)}
df = pd.DataFrame(data)

# 使用循环计算平均值
def mean_loop(df):
    total = 0
    for i in range(len(df)):
        total += df['col1'][i]
    return total / len(df)

# 使用 Pandas 内置函数计算平均值
def mean_vectorized(df):
    return df['col1'].mean()

start_time = time.time()
mean_loop(df)
end_time = time.time()
print(f"Loop time: {end_time - start_time:.4f} seconds")

start_time = time.time()
mean_vectorized(df)
end_time = time.time()
print(f"Vectorized time: {end_time - start_time:.4f} seconds")

2.4 使用 NumPy 函数

Pandas 的 Series 和 DataFrame 可以与 NumPy 数组互相转换。NumPy 提供了大量的数学和科学计算函数,这些函数也可以用于 Pandas 数据。

# 使用 NumPy 函数
data = {'col1': np.random.rand(1000000)}
df = pd.DataFrame(data)

# 使用 NumPy 计算正弦值
df['col2'] = np.sin(df['col1'])

2.5 使用 apply() 函数

虽然应尽量避免循环,但在某些情况下,可能需要对 DataFrame 的每一行或每一列执行自定义函数。在这种情况下,可以使用 apply() 函数。但是,请注意 apply() 函数的性能通常不如矢量化操作。

# 使用 apply() 函数
data = {'col1': range(5), 'col2': range(5, 10)}
df = pd.DataFrame(data)

# 定义一个自定义函数
def custom_function(row):
    return row['col1'] + row['col2']

# 使用 apply() 函数
df['col3'] = df.apply(custom_function, axis=1) # axis=1 表示按行应用函数
print(df)

当需要对每一行进行处理时,优先考虑矢量化操作,如果无法矢量化,再考虑apply函数。

2.6 使用Cython或者Numba加速

对于无法进行矢量化操作的任务,可以考虑使用Cython或者Numba。这两个工具可以将Python代码编译成机器码,从而提高性能。

from numba import njit

@njit
def sum_array(arr):
    result = 0
    for x in arr:
        result += x
    return result

# 使用Numba加速后的函数
data = np.arange(1000000)
result = sum_array(data)
print(result)

三、查询优化

3.1 使用布尔索引

Pandas 提供了强大的布尔索引功能,可以根据条件筛选数据。使用布尔索引通常比循环或自定义函数更高效。

# 使用布尔索引
data = {'col1': range(10), 'col2': range(10, 20)}
df = pd.DataFrame(data)

# 筛选 col1 大于 5 的行
df_filtered = df[df['col1'] > 5]
print(df_filtered)

3.2 使用 isin() 函数

isin() 函数可以用于筛选 Series 或 DataFrame 中包含特定值的行。

# 使用 isin() 函数
data = {'col1': ['a', 'b', 'c', 'd', 'e'], 'col2': range(5)}
df = pd.DataFrame(data)

# 筛选 col1 包含 'a' 或 'c' 的行
df_filtered = df[df['col1'].isin(['a', 'c'])]
print(df_filtered)

3.3 使用 query() 方法

query() 方法可以使用字符串表达式来查询 DataFrame。query() 方法通常比布尔索引更简洁,并且在某些情况下可能更高效。

# 使用 query() 方法
data = {'col1': range(10), 'col2': range(10, 20)}
df = pd.DataFrame(data)

# 查询 col1 大于 5 且 col2 小于 15 的行
df_filtered = df.query('col1 > 5 and col2 < 15')
print(df_filtered)

3.4 索引优化

如果你的查询经常使用某一列作为条件,可以考虑在该列上创建索引。索引可以加速查询,但会增加内存占用和写入时间。

# 创建索引
data = {'col1': range(1000000), 'col2': range(1000000, 2000000)}
df = pd.DataFrame(data)

# 在 col1 上创建索引
df = df.set_index('col1')

# 使用索引查询
df_filtered = df.loc[df.index > 500000] # 使用 .loc[] 进行基于标签的索引
print(df_filtered.head())

注意:创建索引后,需要使用.loc[]进行基于标签的索引。

3.5 分类数据查询优化

将数据转换为 category 类型不仅可以节省内存,还可以提高查询性能。Pandas 对 category 类型进行了优化,可以更快地查找和筛选数据。

data = {'city': ['Beijing', 'Shanghai', 'Guangzhou', 'Beijing', 'Shanghai']}
df = pd.DataFrame(data)
df['city'] = df['city'].astype('category')

# 查询城市为Beijing的行
df_filtered = df[df['city'] == 'Beijing']
print(df_filtered)

3.6 使用 pd.read_csv 的参数优化

在读取 CSV 文件时,可以使用一些参数来提高性能:

  • usecols: 只读取需要的列。
  • dtype: 指定每列的数据类型。
  • parse_dates: 将指定的列解析为日期时间类型。
  • chunksize: 分块读取文件。
# 读取 CSV 文件并指定参数
df = pd.read_csv('your_data.csv', usecols=['col1', 'col2', 'col3'], dtype={'col1': 'int8', 'col2': 'float32'}, parse_dates=['col3'])

四、内存优化实例

以下是一个完整的内存优化实例,展示了如何通过数据类型转换和分块读取来减少内存占用。

import pandas as pd
import numpy as np
import os

# 生成一个大的CSV文件
def generate_large_csv(filename, num_rows=1000000):
    data = {
        'id': range(num_rows),
        'category': np.random.choice(['A', 'B', 'C', 'D'], num_rows),
        'value': np.random.rand(num_rows),
        'date': pd.date_range('2023-01-01', periods=num_rows, freq='D')
    }
    df = pd.DataFrame(data)
    df.to_csv(filename, index=False)

# 检查文件是否存在,如果不存在则生成
filename = 'large_data.csv'
if not os.path.exists(filename):
    generate_large_csv(filename)

# 优化前:读取CSV文件
before_memory = os.path.getsize(filename)
print(f"Original file size: {before_memory / (1024 * 1024):.2f} MB")

# 读取文件,不进行任何优化
df_before = pd.read_csv(filename)
print("Memory usage before optimization:")
df_before.info(memory_usage='deep')

# 优化后:使用dtype和category优化读取
dtype_dict = {'id': 'int32', 'category': 'category', 'value': 'float32'}
df_optimized = pd.read_csv(filename, dtype=dtype_dict, parse_dates=['date'])

print("nMemory usage after optimization:")
df_optimized.info(memory_usage='deep')

# 分块读取与优化
chunk_size = 100000
chunks = pd.read_csv(filename, chunksize=chunk_size, dtype=dtype_dict, parse_dates=['date'])

df_chunk_optimized = pd.concat(chunks) # 将所有chunk合并

print("nMemory usage after chunked reading and optimization:")
df_chunk_optimized.info(memory_usage='deep')

这个例子展示了如何通过指定数据类型、将字符串转换为 category 类型以及分块读取来显著减少内存占用。在实际应用中,应根据数据的特点选择合适的优化策略。

五、总结与实战建议

本次讲座我们讨论了 Pandas 性能调优的三个关键方面:数据类型优化、矢量化操作和查询优化。

  • 数据类型优化 可以减少内存占用,提高计算速度。
  • 矢量化操作 可以避免循环,充分利用 Pandas 和 NumPy 的优势。
  • 查询优化 可以加速数据筛选和查找。

在实际应用中,应该根据数据的特点和任务的需求,选择合适的优化策略。

  1. 确定瓶颈: 使用性能分析工具(如 cProfile)找出代码中的性能瓶颈。
  2. 评估优化效果: 在应用优化策略之前和之后,测量代码的运行时间和内存占用,以评估优化效果。
  3. 逐步优化: 不要一次性应用所有的优化策略,而是逐步进行,并测试每一步的效果。
  4. 考虑数据规模: 不同的优化策略在不同的数据规模下可能有不同的效果。
  5. 熟悉 Pandas 文档: Pandas 提供了丰富的文档,可以帮助你了解各种函数和方法的性能特点。

希望本次讲座对你有所帮助。谢谢大家!

发表回复

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