Python Pandas详解:数据处理与分析核心库
在当今数据驱动的时代,能够高效地处理和分析数据是各行各业专业人士必备的核心技能。Python 语言凭借其简洁的语法、丰富的库支持以及活跃的社区,已成为数据科学领域的首选语言。而在 Python 的数据科学生态系统中,Pandas 库无疑是最耀眼的明星之一,它为处理结构化数据提供了强大、灵活且易用的工具,是进行数据清洗、转换、分析和可视化的基石。
一、Pandas 简介:为何如此重要?
Pandas(Panel Data Analysis)最初由 Wes McKinney 于 2008 年开发,旨在为金融数据分析提供一个高性能、易用的工具。如今,Pandas 已经发展成为一个通用的数据分析库,广泛应用于学术研究、商业智能、机器学习等多个领域。
Pandas 的核心优势:
- 强大的数据结构: 提供了 Series(一维)和 DataFrame(二维)两种核心数据结构,能够高效处理不同类型的数据(数值、字符串、时间序列、布尔值等)。
- 便捷的数据读写: 支持从多种格式的文件中读取数据(如 CSV, Excel, SQL 数据库, JSON, HTML 等),并能将处理后的数据轻松写回。
- 灵活的数据操作: 提供了丰富的数据清洗、转换、重塑、切片、索引、合并、分组和聚合等功能。
- 高效的性能: 底层部分代码由 Cython 或 C 实现,保证了在处理大规模数据集时的运算效率。
- 与生态系统无缝集成: 与 NumPy(数值计算)、Matplotlib/Seaborn(数据可视化)、Scikit-learn(机器学习)等 Python 库紧密集成,构成了强大的数据分析流水线。
- 时间序列处理能力: 内置了强大的时间序列数据处理功能,非常适合金融、经济等领域的数据分析。
二、Pandas 的核心数据结构
Pandas 的功能主要围绕其两个核心数据结构构建:Series 和 DataFrame。
1. Series:一维带标签数组
Series 是一种类似于一维 NumPy 数组的对象,但它带有一个与之关联的标签数组,称为索引(index)。索引可以是数字、字符串或其他 Python 对象。
创建 Series:
“`python
import pandas as pd
import numpy as np
从列表创建 Series,默认数字索引
s1 = pd.Series([1, 3, 5, np.nan, 6, 8])
print(“s1:\n”, s1)
从列表创建 Series,并指定索引
data = [10, 20, 30, 40]
labels = [‘a’, ‘b’, ‘c’, ‘d’]
s2 = pd.Series(data, index=labels)
print(“\ns2:\n”, s2)
从字典创建 Series,字典的键作为索引
data_dict = {‘apple’: 10, ‘banana’: 20, ‘orange’: 15}
s3 = pd.Series(data_dict)
print(“\ns3:\n”, s3)
“`
Series 的基本操作:
- 访问元素: 可以通过索引标签或位置进行访问。
python
print("\ns2['b']:", s2['b']) # 通过标签访问
print("s2[1]:", s2[1]) # 通过位置访问 (如果索引是默认数字索引,标签和位置可能重合)
print("s2[['a', 'c']]:\n", s2[['a', 'c']]) # 选择多个元素 - 属性:
index
(获取索引),values
(获取值,NumPy 数组形式),dtype
(数据类型),shape
(形状),name
(名称)。 - 向量化运算: Series 支持 NumPy 风格的向量化运算。
python
print("\ns2 * 2:\n", s2 * 2)
print("np.exp(s2/10):\n", np.exp(s2/10)) - 布尔索引:
python
print("\ns2[s2 > 15]:\n", s2[s2 > 15])
2. DataFrame:二维带标签表格型数据结构
DataFrame 是一个表格型的数据结构,它包含一组有序的列,每列可以是不同的值类型(数值、字符串、布尔值等)。DataFrame 既有行索引(index),也有列索引(columns)。可以将其看作是由多个 Series 共享相同索引的字典。
创建 DataFrame:
“`python
1. 从字典创建,字典的键作为列名,值为列表或 Series
data_df1 = {
‘name’: [‘Alice’, ‘Bob’, ‘Charlie’, ‘David’],
‘age’: [25, 30, 35, 28],
‘city’: [‘New York’, ‘Paris’, ‘London’, ‘Tokyo’]
}
df1 = pd.DataFrame(data_df1)
print(“df1:\n”, df1)
指定行索引
df1_indexed = pd.DataFrame(data_df1, index=[‘p1’, ‘p2’, ‘p3’, ‘p4’])
print(“\ndf1_indexed:\n”, df1_indexed)
2. 从 NumPy 数组创建,指定列名和行索引
dates = pd.date_range(‘20230101’, periods=6) # 创建日期索引
df2 = pd.DataFrame(np.random.randn(6, 4), index=dates, columns=list(‘ABCD’))
print(“\ndf2:\n”, df2)
3. 从 Series 字典创建
s_a = pd.Series([1, 2, 3], index=[‘x’, ‘y’, ‘z’])
s_b = pd.Series([4, 5, 6, 7], index=[‘x’, ‘y’, ‘z’, ‘w’])
df3 = pd.DataFrame({‘col1’: s_a, ‘col2’: s_b}) # 会根据索引自动对齐,缺失处填充 NaN
print(“\ndf3:\n”, df3)
4. 从列表的列表或元组的列表创建
data_list_of_lists = [[‘Alice’, 25], [‘Bob’, 30]]
df4 = pd.DataFrame(data_list_of_lists, columns=[‘Name’, ‘Age’])
print(“\ndf4:\n”, df4)
“`
DataFrame 的基本操作:
-
查看数据:
head(n)
: 查看前 n 行数据,默认为 5。tail(n)
: 查看后 n 行数据,默认为 5。index
: 查看行索引。columns
: 查看列名。values
: 获取 DataFrame 的 NumPy 表示。describe()
: 生成描述性统计数据(计数、均值、标准差、最小值、四分位数、最大值等)。info()
: 提供 DataFrame 的简要摘要(索引类型、列数据类型、非空值数量、内存使用情况)。shape
: 返回一个表示 DataFrame 维度的元组 (行数, 列数)。dtypes
: 返回每列的数据类型。
python
print("\ndf1.head(2):\n", df1.head(2))
print("\ndf1.index:\n", df1.index)
print("\ndf1.columns:\n", df1.columns)
print("\ndf1.describe():\n", df1.describe(include='all')) # include='all' 会包含非数值列的统计
print("\ndf1.info():")
df1.info() -
选择数据(索引和切片):
- 选择列:
python
print("\ndf1['name']:\n", df1['name']) # 选择单列,返回 Series
print("\ndf1[['name', 'city']]:\n", df1[['name', 'city']]) # 选择多列,返回 DataFrame - 选择行(基于标签):
loc
loc
主要基于标签进行选择,但也可以接受布尔数组。
python
print("\ndf1.loc[0]:\n", df1.loc[0]) # 选择第一行 (如果索引是数字)
print("\ndf1_indexed.loc['p1']:\n", df1_indexed.loc['p1']) # 选择标签为 'p1' 的行
print("\ndf1.loc[0:2]:\n", df1.loc[0:2]) # 选择第0行到第2行 (包含结束标签)
print("\ndf1_indexed.loc['p1':'p3']:\n", df1_indexed.loc['p1':'p3']) - 选择行(基于整数位置):
iloc
iloc
完全基于整数位置进行选择(从0开始)。
python
print("\ndf1.iloc[0]:\n", df1.iloc[0]) # 选择第一行
print("\ndf1.iloc[0:2]:\n", df1.iloc[0:2]) # 选择前两行 (不包含结束位置)
print("\ndf1.iloc[[0, 2, 1], [0, 2]]:\n", df1.iloc[[0, 2, 1], [0, 2]]) # 选择特定行和列 - 选择行和列:
python
print("\ndf1.loc[0, 'name']:", df1.loc[0, 'name']) # 第0行 'name' 列的元素
print("\ndf1.loc[0:2, ['name', 'age']]:\n", df1.loc[0:2, ['name', 'age']])
print("\ndf1.iloc[0:2, 0:2]:\n", df1.iloc[0:2, 0:2]) - 布尔索引:
python
print("\ndf1[df1['age'] > 25]:\n", df1[df1['age'] > 25])
print("\ndf1[df1['city'].isin(['New York', 'Tokyo'])]:\n", df1[df1['city'].isin(['New York', 'Tokyo'])])
- 选择列:
三、常用数据操作
Pandas 提供了大量函数和方法来执行各种数据操作。
1. 数据加载与保存
Pandas 可以轻松读写多种数据格式。
- CSV 文件:
python
# 假设有一个名为 'data.csv' 的文件
# df_csv = pd.read_csv('data.csv')
# df1.to_csv('output.csv', index=False) # index=False 表示不将行索引写入文件 - Excel 文件:
python
# 需要安装 openpyxl 或 xlrd/xlwt 库
# df_excel = pd.read_excel('data.xlsx', sheet_name='Sheet1')
# df1.to_excel('output.xlsx', sheet_name='MyData', index=False) - SQL 数据库:
python
# 需要安装 SQLAlchemy 和相应的数据库驱动 (如 psycopg2 for PostgreSQL, mysqlclient for MySQL)
# from sqlalchemy import create_engine
# engine = create_engine('sqlite:///mydatabase.db') # 示例:SQLite
# df_sql = pd.read_sql_query('SELECT * FROM my_table', engine)
# df1.to_sql('new_table', engine, if_exists='replace', index=False) # if_exists: 'fail', 'replace', 'append'
2. 数据清洗
真实世界的数据往往是不完美的,包含缺失值、重复值或错误数据。
-
处理缺失值 (NaN):
isnull()
/notnull()
: 检测缺失值,返回布尔型的 DataFrame 或 Series。dropna(axis=0, how='any', thresh=None, subset=None, inplace=False)
: 删除包含缺失值的行或列。axis
: 0 表示行,1 表示列。how
: ‘any’ (只要有一个 NaN 就删除),’all’ (所有值都是 NaN 才删除)。thresh
: 保留至少有thresh
个非 NaN 值的行/列。subset
: 在指定的列中查找 NaN。
fillna(value=None, method=None, axis=None, inplace=False, limit=None)
: 填充缺失值。value
: 用于填充的标量值或字典/Series/DataFrame。method
: ‘ffill’ (向前填充), ‘bfill’ (向后填充)。
python
data_missing = {'col1': [1, 2, np.nan, 4], 'col2': [np.nan, 5, 6, 7], 'col3': [8, 9, 10, np.nan]}
df_miss = pd.DataFrame(data_missing)
print("\nDataFrame with missing values:\n", df_miss)
print("\nDetect missing values:\n", df_miss.isnull())
print("\nDrop rows with any NaN:\n", df_miss.dropna())
print("\nFill NaN with a scalar (0):\n", df_miss.fillna(value=0))
print("\nFill NaN with mean of column:\n", df_miss.fillna(df_miss.mean()))
print("\nForward fill:\n", df_miss.fillna(method='ffill')) -
处理重复数据:
duplicated(subset=None, keep='first')
: 判断行是否重复,返回布尔 Series。subset
: 指定要考虑的列。keep
: ‘first’ (标记除第一个外的重复项), ‘last’ (标记除最后一个外的重复项), False (标记所有重复项)。
drop_duplicates(subset=None, keep='first', inplace=False)
: 删除重复行。
python
data_duplicates = {'colA': ['X', 'Y', 'X', 'Z', 'Y'], 'colB': [1, 2, 1, 3, 2]}
df_dup = pd.DataFrame(data_duplicates)
print("\nDataFrame with duplicates:\n", df_dup)
print("\nBoolean series indicating duplicates (keeping first):\n", df_dup.duplicated())
print("\nDrop duplicates (keeping first):\n", df_dup.drop_duplicates())
print("\nDrop duplicates based on 'colA', keeping last:\n", df_dup.drop_duplicates(subset=['colA'], keep='last'))
3. 数据转换与操作
-
应用函数 (Apply):
apply(func, axis=0, ...)
: 将函数应用于 DataFrame 的行或列。map(arg, na_action=None)
: (Series 方法) 用于对 Series 的每个元素应用一个函数或映射。applymap(func, na_action=None)
: (DataFrame 方法) 用于对 DataFrame 的每个元素应用一个函数。
“`python
print(“\ndf2:\n”, df2)
print(“\nApplying lambda to each column (e.g., max-min):\n”, df2.apply(lambda x: x.max() – x.min())) # axis=0 (default)
print(“\nApplying custom function to each row:\n”, df2.apply(np.sum, axis=1))map for Series
s_map = pd.Series([1,2,3,4])
print(“\nSeries map example:\n”, s_map.map(lambda x: x*100))applymap for DataFrame
print(“\nDataFrame applymap example (format as string):\n”, df2.applymap(lambda x: f”{x:.2f}”))
“` -
数据类型转换:
astype(dtype)
: 转换列的数据类型。
python
df_types = pd.DataFrame({'A': ['1', '2', '3'], 'B': [4.0, 5.1, 6.2]})
df_types.info()
df_types['A'] = df_types['A'].astype(int)
df_types['B'] = df_types['B'].astype(str)
print("\nAfter astype:")
df_types.info()
-
字符串操作:
Pandas Series 具有.str
访问器,可以方便地对字符串列执行向量化的字符串操作。
python
s_str = pd.Series(['apple pie', 'banana bread', 'Orange Juice', 'GRAPES'])
print("\nString Series:\n", s_str)
print("\nLowercase:\n", s_str.str.lower())
print("\nSplit by space:\n", s_str.str.split(' '))
print("\nContains 'an':\n", s_str.str.contains('an'))
print("\nReplace ' ' with '-':\n", s_str.str.replace(' ', '-'))
4. 分组与聚合 (Group By)
groupby()
操作是 Pandas 中最强大的功能之一,它遵循 “split-apply-combine” 的模式:
1. Split: 根据某些标准将数据分成组。
2. Apply: 对每个组独立应用一个函数(如求和、计数、平均值等)。
3. Combine: 将结果组合成一个新的数据结构。
“`python
data_group = {
‘Team’: [‘A’, ‘B’, ‘A’, ‘B’, ‘A’, ‘C’, ‘C’, ‘B’],
‘Player’: [‘P1’, ‘P2’, ‘P3’, ‘P4’, ‘P5’, ‘P6’, ‘P7’, ‘P8’],
‘Points’: [10, 12, 8, 15, 11, 9, 10, 13],
‘Assists’: [5, 7, 4, 8, 6, 3, 5, 6]
}
df_group = pd.DataFrame(data_group)
print(“\nDataFrame for grouping:\n”, df_group)
按 ‘Team’ 分组并计算每队的平均 ‘Points’ 和 ‘Assists’
grouped_team_mean = df_group.groupby(‘Team’).mean()
print(“\nMean points and assists per team:\n”, grouped_team_mean)
按 ‘Team’ 分组并计算每队的 ‘Points’ 总和
grouped_team_sum_points = df_group.groupby(‘Team’)[‘Points’].sum()
print(“\nSum of points per team:\n”, grouped_team_sum_points)
按 ‘Team’ 分组并应用多个聚合函数
grouped_team_agg = df_group.groupby(‘Team’).agg({
‘Points’: [‘sum’, ‘mean’, ‘count’],
‘Assists’: [‘mean’, ‘min’, ‘max’]
})
print(“\nMultiple aggregations per team:\n”, grouped_team_agg)
按多列分组
grouped_multi = df_group.groupby([‘Team’, df_group[‘Points’] > 10]).size() # 第二个分组条件是Points是否大于10
print(“\nGroup by Team and (Points > 10):\n”, grouped_multi)
“`
5. 合并、连接与拼接
Pandas 提供了多种方式来组合多个 DataFrame 或 Series。
-
concat()
: 拼接
用于沿特定轴(行或列)将多个 Pandas 对象堆叠在一起。
“`python
df_c1 = pd.DataFrame({‘A’: [‘A0’, ‘A1’], ‘B’: [‘B0’, ‘B1’]})
df_c2 = pd.DataFrame({‘A’: [‘A2’, ‘A3’], ‘B’: [‘B2’, ‘B3’]})
df_c3 = pd.DataFrame({‘C’: [‘C0’, ‘C1’], ‘D’: [‘D0’, ‘D1’]})按行拼接 (默认 axis=0)
concatenated_rows = pd.concat([df_c1, df_c2])
print(“\nConcatenated by rows:\n”, concatenated_rows)按列拼接 (axis=1)
concatenated_cols = pd.concat([df_c1, df_c3], axis=1)
print(“\nConcatenated by columns:\n”, concatenated_cols)
“` -
merge()
: 数据库风格的连接
基于一个或多个键将 DataFrame 的行连接起来,类似于 SQL 中的 JOIN。
“`python
left = pd.DataFrame({‘key’: [‘K0’, ‘K1’, ‘K2’, ‘K3’],
‘A’: [‘A0’, ‘A1’, ‘A2’, ‘A3’],
‘B’: [‘B0’, ‘B1’, ‘B2’, ‘B3’]})
right = pd.DataFrame({‘key’: [‘K0’, ‘K1’, ‘K2’, ‘K4’],
‘C’: [‘C0’, ‘C1’, ‘C2’, ‘C4’],
‘D’: [‘D0’, ‘D1’, ‘D2’, ‘D4’]})内连接 (how=’inner’, 默认)
merged_inner = pd.merge(left, right, on=’key’, how=’inner’)
print(“\nInner merge on ‘key’:\n”, merged_inner)左连接 (how=’left’)
merged_left = pd.merge(left, right, on=’key’, how=’left’)
print(“\nLeft merge on ‘key’:\n”, merged_left)右连接 (how=’right’)
merged_right = pd.merge(left, right, on=’key’, how=’right’)
print(“\nRight merge on ‘key’:\n”, merged_right)外连接 (how=’outer’)
merged_outer = pd.merge(left, right, on=’key’, how=’outer’)
print(“\nOuter merge on ‘key’:\n”, merged_outer)如果连接键的列名不同
left_diff_key = pd.DataFrame({‘lkey’: [‘K0’, ‘K1’], ‘val_l’: [1,2]})
right_diff_key = pd.DataFrame({‘rkey’: [‘K0’, ‘K1’], ‘val_r’: [3,4]})
merged_diff_key = pd.merge(left_diff_key, right_diff_key, left_on=’lkey’, right_on=’rkey’)
print(“\nMerge with different key names:\n”, merged_diff_key)
“` -
join()
: 基于索引的连接
join()
主要用于基于索引合并 DataFrame,也可以通过on
参数指定列。
“`python
left_idx = pd.DataFrame({‘A’: [‘A0’, ‘A1’, ‘A2’]}, index=[‘K0’, ‘K1’, ‘K2’])
right_idx = pd.DataFrame({‘B’: [‘B0’, ‘B1’, ‘B2’]}, index=[‘K0’, ‘K1’, ‘K3’])joined_df = left_idx.join(right_idx, how=’outer’) # 默认左连接
print(“\nJoined DataFrame (outer):\n”, joined_df)
“`
6. 时间序列处理
Pandas 最初就是为金融时间序列分析而设计的,因此其时间序列功能非常强大。
* 创建时间序列索引: pd.date_range()
* 时间戳对象: pd.Timestamp
* 时间间隔对象: pd.Timedelta
* 重采样: resample()
方法,用于将时间序列数据从一个频率转换到另一个频率(例如,从日数据到月数据)。
* 窗口函数: rolling()
和 expanding()
,用于计算移动平均、移动总和等。
“`python
创建一个时间序列 DataFrame
dates = pd.date_range(‘20230101′, periods=100, freq=’D’) # 100天的日数据
ts_df = pd.DataFrame(np.random.randn(100, 2), index=dates, columns=[‘Value1’, ‘Value2’])
print(“\nTime series DataFrame head:\n”, ts_df.head())
重采样到月度频率,计算每月平均值
monthly_mean = ts_df.resample(‘M’).mean() # ‘M’ for month end frequency
print(“\nMonthly mean:\n”, monthly_mean.head())
计算7天移动平均
rolling_mean_7d = ts_df[‘Value1’].rolling(window=7).mean()
print(“\n7-day rolling mean for Value1 (tail):\n”, rolling_mean_7d.tail())
“`
7. 数据可视化
Pandas DataFrame 和 Series 对象内置了 .plot()
方法,可以方便地调用 Matplotlib 库进行快速绘图。
“`python
import matplotlib.pyplot as plt # 通常需要导入
简单的线图
ts_df[‘Value1′].plot(figsize=(10, 4), title=’Value1 Over Time’)
plt.show() # 在脚本中需要 plt.show() 来显示图像,Jupyter Notebook中通常会自动显示
柱状图
df_group.groupby(‘Team’)[‘Points’].sum().plot(kind=’bar’, title=’Total Points by Team’)
plt.ylabel(‘Total Points’)
plt.show()
直方图
df_group[‘Points’].plot(kind=’hist’, bins=5, title=’Distribution of Points’)
plt.xlabel(‘Points’)
plt.show()
散点图
df_group.plot(kind=’scatter’, x=’Points’, y=’Assists’, title=’Points vs Assists’)
plt.show()
“`
为了更高级和美观的可视化,通常会结合 Seaborn 库使用。
四、Pandas 性能优化与最佳实践
- 使用向量化操作: 尽量避免使用 Python 循环遍历 DataFrame 的行,Pandas 和 NumPy 的内置函数(如
sum()
,mean()
, 算术运算符,apply()
等)都是向量化的,速度远快于显式循环。 - 选择正确的数据类型: 使用
astype()
将列转换为最合适的数据类型(如category
类型对于低基数类别列,int8/16/32/64
或float32/64
对于数值列),可以显著减少内存占用并提高运算速度。 - 明智地使用
apply()
: 虽然apply()
很灵活,但它通常比专门的向量化函数慢。如果可能,优先选择内置的 Pandas/NumPy 函数。 - 避免不必要的副本: 许多 Pandas 操作会返回数据的副本。如果不需要副本,可以使用
inplace=True
(但需谨慎,因为它会修改原始 DataFrame),或者将结果赋回原变量。 - 处理大型数据集:
- 分块读取: 使用
pd.read_csv()
的chunksize
参数可以分块读取大文件,逐块处理,避免一次性加载到内存。 - 指定
dtype
: 在读取数据时(如read_csv
),通过dtype
参数预先指定列的数据类型,可以节省内存和解析时间。 - 考虑 Dask 或 Vaex: 对于远超内存容量的数据集,可以考虑使用 Dask 或 Vaex 等库,它们提供了与 Pandas 类似的 API,但支持并行计算和核外计算。
- 分块读取: 使用
五、总结
Pandas 是 Python 数据科学生态系统中不可或缺的组成部分。它提供的 Series 和 DataFrame 数据结构,以及围绕它们构建的丰富功能集,使得数据的导入、清洗、转换、分析和可视化变得异常高效和便捷。无论是初学者还是经验丰富的数据分析师,掌握 Pandas 都是一项至关重要的技能。通过不断实践和探索,你会发现 Pandas 的强大之处远不止于此,它将是你探索数据世界、洞察数据价值的得力助手。随着数据量的持续增长和数据分析需求的日益复杂,Pandas 及其相关工具的重要性只会越来越突出。
希望这篇文章能够帮助你更好地理解和使用 Pandas!