NumPy 与 Pandas 交互

Pandas 是构建在 NumPy 之上的数据分析库,两者紧密集成。NumPy 提供了高效的数组运算,Pandas 则在此基础上增加了标签、索引、数据对齐和缺失值处理等功能。掌握它们之间的交互,可以让你在数据分析中同时利用两者的优势。

1. Series 与 ndarray 的转换

Pandas 的 Series 是一维带标签的数组,底层数据存储在 NumPy 数组中。可以轻松地在 Series 和 ndarray 之间转换。

1.1 Series 转 ndarray

通过 .values.to_numpy() 获取底层的 NumPy 数组。推荐使用 .to_numpy(),因为它更明确且支持 future 行为。

import numpy as np
import pandas as pd

s = pd.Series([10, 20, 30], index=['a', 'b', 'c'])
print("Series:\n", s)

# 转换为 ndarray
arr = s.to_numpy()   # 或 s.values
print("ndarray:", arr)
print("类型:", type(arr))

输出:

Series:
 a    10
b    20
c    30
dtype: int64
ndarray: [10 20 30]
类型: 

1.2 ndarray 转 Series

使用 pd.Series() 构造函数,可以指定 index。

arr = np.array([100, 200, 300])
s_new = pd.Series(arr, index=['x', 'y', 'z'])
print("从 ndarray 创建的 Series:\n", s_new)

输出:

从 ndarray 创建的 Series:
 x    100
y    200
z    300
dtype: int64

2. DataFrame 与 ndarray 的转换

DataFrame 是二维表格,每一列可以是不同的数据类型,但整体数据可以转换为二维 NumPy 数组(所有列类型相同)或结构化数组。

2.1 DataFrame 转 ndarray

使用 .to_numpy().values 得到二维数组。如果各列数据类型不同,会向上转型为兼容类型(通常为 object)。

df = pd.DataFrame({
    'A': [1, 2, 3],
    'B': [4.0, 5.0, 6.0],
    'C': ['x', 'y', 'z']
})
print("DataFrame:\n", df)

arr = df.to_numpy()
print("转换为 ndarray:\n", arr)
print("数据类型:", arr.dtype)   # 可能是 object,因为包含字符串

输出:

DataFrame:
    A    B  C
0  1  4.0  x
1  2  5.0  y
2  3  6.0  z
转换为 ndarray:
 [[1 4.0 'x']
 [2 5.0 'y']
 [3 6.0 'z']]
数据类型: object

2.2 ndarray 转 DataFrame

通过 pd.DataFrame() 传入二维数组,并可指定列名和索引。

arr = np.random.rand(3, 2)
df_new = pd.DataFrame(arr, columns=['col1', 'col2'], index=['r1', 'r2', 'r3'])
print("从 ndarray 创建的 DataFrame:\n", df_new)

2.3 结构化数组转 DataFrame

NumPy 的结构化数组(复合 dtype)可以完美转换为 DataFrame,字段名成为列名。

dtype = [('name', 'U10'), ('age', 'i4'), ('weight', 'f8')]
structured_arr = np.array([('Alice', 25, 55.5), ('Bob', 30, 75.2)], dtype=dtype)
df_from_struct = pd.DataFrame(structured_arr)
print("结构化数组转 DataFrame:\n", df_from_struct)

3. 在 Pandas 中使用 NumPy 函数

Pandas 的 Series 和 DataFrame 兼容 NumPy 的通用函数(ufunc),可以直接应用,结果保留索引和列名。

3.1 一元 ufunc 应用

df = pd.DataFrame({
    'A': [1, 2, 3],
    'B': [4, 5, 6]
})

# 应用 np.log
log_df = np.log(df)
print("np.log(df):\n", log_df)

# 应用 np.sqrt
sqrt_df = np.sqrt(df)
print("np.sqrt(df):\n", sqrt_df)

3.2 二元 ufunc 与广播

两个 DataFrame 之间或 DataFrame 与 Series 之间的运算也会利用广播机制。

# DataFrame 与 Series 相加(沿行广播)
s = pd.Series([10, 20], index=['A', 'B'])
result = df + s
print("df + s:\n", result)

3.3 聚合函数

NumPy 的聚合函数可以直接应用于 DataFrame,返回聚合后的 Series(保留列名)或标量(对整个 DataFrame)。

print("np.sum(df):\n", np.sum(df))          # 各列之和
print("np.mean(df, axis=1):\n", np.mean(df, axis=1))  # 各行均值

4. 使用 NumPy 加速 Pandas 操作

虽然 Pandas 本身已很高效,但某些操作(如逐行 apply)可能较慢。通过提取底层 NumPy 数组并向量化,可以显著提升性能。

4.1 避免逐行 apply,使用向量化

df = pd.DataFrame({
    'x': np.random.rand(1000000),
    'y': np.random.rand(1000000)
})

# 慢速:使用 apply 逐行计算
def my_func(row):
    return np.sqrt(row['x']**2 + row['y']**2)

%timeit -n1 -r1 df.apply(my_func, axis=1)

# 快速:使用 NumPy 向量化
%timeit -n1 -r1 np.sqrt(df['x'].to_numpy()**2 + df['y'].to_numpy()**2)

向量化版本通常快几十倍。

4.2 利用 NumPy 的布尔逻辑过滤

Pandas 的布尔索引基于 NumPy,但直接使用 NumPy 条件数组有时更快。

mask = (df['x'] > 0.5) & (df['y'] < 0.5)   # Pandas 布尔 Series
filtered = df[mask]

# 使用 NumPy 数组作为掩码(也兼容)
mask_np = (df['x'].to_numpy() > 0.5) & (df['y'].to_numpy() < 0.5)
filtered_np = df.iloc[mask_np]   # 需使用 iloc

5. 线性代数与随机数

NumPy 的线性代数模块(np.linalg)和随机数生成可以直接在 Pandas 数据上使用,但通常需要先提取数组。

# 协方差矩阵
cov_matrix = np.cov(df.T)   # df.T 转置使每行是一个变量

# 矩阵乘法
result = df.values @ df.values.T   # 二维数组乘法

# 随机数填充新列
df['rand'] = np.random.default_rng().normal(size=len(df))

6. 注意事项与性能对比

  • 索引对齐:Pandas 在算术运算时会自动对齐索引,这可能导致开销。如果不需要对齐,可以先转换为 NumPy 数组,计算完再重新构建 DataFrame。
  • 数据类型:Pandas 可能会自动改变数据类型(如 int 提升为 float),使用 NumPy 数组可以更精确控制。
  • 内存占用:Pandas 的 overhead 比 NumPy 大。对于大型数值数据,仅用 NumPy 存储可能更省内存。
  • 缺失值:Pandas 使用 NaN 表示缺失值,NumPy 函数如 np.mean 会返回 NaN,但 np.nanmean 可以处理。Pandas 的 .mean() 默认跳过 NaN。

6.1 性能对比示例:对齐开销

df1 = pd.DataFrame(np.random.rand(1000, 100))
df2 = pd.DataFrame(np.random.rand(1000, 100))

# 有对齐
%timeit -n10 df1 + df2

# 无对齐(直接操作数组)
%timeit -n10 pd.DataFrame(df1.values + df2.values, index=df1.index, columns=df1.columns)

7. 综合示例:数据清洗与转换

# 创建带有缺失值的 DataFrame
df = pd.DataFrame({
    'A': [1, 2, np.nan, 4],
    'B': [5, np.nan, 7, 8],
    'C': [9, 10, 11, 12]
})

# 使用 NumPy 的 nan 函数填充均值
col_means = np.nanmean(df.values, axis=0)   # 各列均值
df_filled = df.fillna(col_means)

# 使用 NumPy 计算标准化
df_normalized = (df_filled - np.mean(df_filled, axis=0)) / np.std(df_filled, axis=0)
print("标准化后的 DataFrame:\n", df_normalized)
提示: 在交互式环境中,使用 %timeit 测试性能,找到最适合你的数据大小和操作的模式。通常,对于大规模数值计算,提取 NumPy 数组进行处理,然后重新构建 Pandas 对象是最佳实践。

总结

NumPy 和 Pandas 是 Python 数据科学生态系统中的黄金搭档。通过灵活地在两者之间转换和利用各自的优势,可以编写出既高效又易读的数据处理代码。掌握交互技巧,能让你的数据分析工作如虎添翼。下一章我们将讨论 NumPy 的最佳实践和常见陷阱。