如果说 NumPy 是科学计算的基石,那么 Pandas 就是数据分析的皇冠。它建立在 NumPy 之上,提供了高性能、易用的数据结构(Series 和 DataFrame)以及数据分析工具。无论是处理 Excel 表格、SQL 查询结果,还是清洗杂乱的日志文件,Pandas 都是数据科学家和分析师的首选工具。
一、Pandas 的核心数据结构
1.1 Series:带标签的一维数组
Series 类似于 NumPy 的一维数组,但多了**索引(Index)**功能,允许通过标签访问数据。
【代码实践】:
import pandas as pd
import numpy as np
# 1. 从列表创建
data = [10, 20, 30, 40]
s1 = pd.Series(data)
print("默认索引:\n", s1)
# 2. 指定自定义索引
labels = ['a', 'b', 'c', 'd']
s2 = pd.Series(data, index=labels)
print("\n自定义索引:\n", s2)
# 3. 访问数据
print(f"通过标签访问 'b': {s2['b']}")
print(f"通过位置访问第2个: {s2.iloc[1]}")
# 4. 向量运算
print("\n乘以2:\n", s2 * 2)
print("\n大于25的过滤:\n", s2[s2 > 25])
1.2 DataFrame:带标签的二维表格
DataFrame 是 Pandas 最核心的对象,可以看作是由多个共享同一索引的 Series 组成的字典。
【代码实践】:
# 1. 从字典创建 (最常用)
data_dict = {
'姓名': ['张三', '李四', '王五', '赵六'],
'年龄': [25, 30, 28, 35],
'城市': ['北京', '上海', '广州', '深圳'],
'薪资': [15000, 22000, 18000, 25000]
}
df = pd.DataFrame(data_dict)
print("基础 DataFrame:\n", df)
# 2. 设置索引
df_indexed = pd.DataFrame(data_dict, index=['E001', 'E002', 'E003', 'E004'])
print("\n设置索引后:\n", df_indexed)
# 3. 从 CSV/Excel 读取 (模拟)
# df_real = pd.read_csv('data.csv')
# df_excel = pd.read_excel('data.xlsx')
二、数据的查看与基本信息
在处理新数据时,第一步永远是“了解数据”。
【代码实践】:
# 使用上面的 df_indexed 进行演示
# 1. 查看前/后 N 行
print("前3行:\n", df_indexed.head(3))
print("后2行:\n", df_indexed.tail(2))
# 2. 查看基本信息 (类型、非空值数量)
print("\n基本信息:")
print(df_indexed.info())
# 3. 描述性统计 (仅针对数值列)
print("\n统计摘要:")
print(df_indexed.describe())
# 输出: count(计数), mean(均值), std(标准差), min, 25%, 50%, 75%, max
# 4. 查看列名和索引
print(f"列名: {df_indexed.columns.tolist()}")
print(f"索引: {df_indexed.index.tolist()}")
# 5. 转置 (行变列,列变行)
print("\n转置后:\n", df_indexed.T)
三、数据的选择与切片(核心难点)
Pandas 提供了多种选择数据的方式,初学者容易混淆。请记住两个核心属性:
.loc[].iloc[]:基于**整数位置(Integer Position)**的选择。
3.1 列的选择
# 选择单列 (返回 Series)
ages = df_indexed['年龄']
# 选择多列 (返回 DataFrame)
subset = df_indexed[['姓名', '薪资']]
3.2 行的选择:loc vs iloc
# --- .loc (标签索引) ---
# 选取索引为 'E002' 的行
row_loc = df_indexed.loc['E002']
# 选取索引 'E001' 到 'E003' 的行 (包含结束标签!)
rows_loc_range = df_indexed.loc['E001':'E003']
# 选取特定行和特定列
val_loc = df_indexed.loc['E002', '城市']
# --- .iloc (位置索引) ---
# 选取第 1 行 (索引从0开始)
row_iloc = df_indexed.iloc[1]
# 选取第 0 到 2 行 (不包含结束位置,类似 Python 切片)
rows_iloc_range = df_indexed.iloc[0:2]
# 选取第 1 行,第 3 列 (城市)
val_iloc = df_indexed.iloc[1, 2]
print("Loc 选取城市:", val_loc)
print("Iloc 选取城市:", val_iloc)
3.3 布尔索引(条件筛选)
这是数据清洗中最强大的功能。
# 1. 单条件筛选:薪资大于 20000
high_salary = df_indexed[df_indexed['薪资'] > 20000]
print("高薪员工:\n", high_salary)
# 2. 多条件筛选:(薪资 > 16000) 且 (城市 == '北京')
# 注意:多条件必须用 & (与), | (或), ~ (非),且每个条件要加括号
beijing_high = df_indexed[(df_indexed['薪资'] > 16000) & (df_indexed['城市'] == '北京')]
print("北京高薪:\n", beijing_high)
# 3. 使用 isin 筛选
cities_of_interest = ['上海', '深圳']
target_cities = df_indexed[df_indexed['城市'].isin(cities_of_interest)]
print("目标城市员工:\n", target_cities)
四、数据清洗与预处理
真实世界的数据往往是“脏”的:缺失值、重复值、格式错误。
4.1 处理缺失值 (NaN)
# 构造含缺失值的数据
df_dirty = pd.DataFrame({
'A': [1, 2, np.nan, 4],
'B': [5, np.nan, np.nan, 8],
'C': [10, 11, 12, 13]
})
# 1. 检测缺失值
print("缺失值矩阵:\n", df_dirty.isnull())
print("每列缺失数量:\n", df_dirty.isnull().sum())
# 2. 删除缺失值
# dropna() 默认删除任何含有 NaN 的行
df_dropped = df_dirty.dropna()
# 也可以指定阈值:至少要有2个非空值才保留
df_thresh = df_dirty.dropna(thresh=2)
# 3. 填充缺失值
# 用固定值填充
df_filled_const = df_dirty.fillna(0)
# 用均值填充 (常用于数值列)
df_filled_mean = df_dirty.fillna(df_dirty.mean())
# 用前一个值填充 (常用于时间序列)
df_filled_ffill = df_dirty.fillna(method='ffill')
print("\n均值填充后:\n", df_filled_mean)
4.2 处理重复值
df_dup = pd.DataFrame({
'ID': [1, 2, 2, 3],
'Value': [10, 20, 20, 30]
})
# 检查重复
print("重复行标记:\n", df_dup.duplicated())
# 删除重复行 (保留第一次出现的)
df_unique = df_dup.drop_duplicates()
print("\n去重后:\n", df_unique)
4.3 数据类型转换与字符串操作
df_misc = pd.DataFrame({
'price_str': ['100', '200', '300'],
'date_str': ['2023-01-01', '2023-01-02', '2023-01-03'],
'name': [' Alice ', 'Bob', ' Charlie ']
})
# 1. 类型转换
df_misc['price_num'] = df_misc['price_str'].astype(int)
df_misc['date'] = pd.to_datetime(df_misc['date_str'])
# 2. 字符串向量化操作 (.str 访问器)
# 去除空格
df_misc['name_clean'] = df_misc['name'].str.strip()
# 转大写
df_misc['name_upper'] = df_misc['name'].str.upper()
# 包含判断
mask = df_misc['name'].str.contains('li', case=False) # 不区分大小写
print("\n清洗后数据:\n", df_misc)
print(f"日期列类型: {df_misc['date'].dtype}")
五、数据变换与衍生
5.1 应用函数 (apply & map)
当内置函数无法满足需求时,使用 apply 对行或列应用自定义函数。
df_trans = pd.DataFrame({
'薪资': [10000, 15000, 20000],
'部门': ['技术', '产品', '技术']
})
# 1. map: 用于 Series,常用于替换值 (字典映射)
dept_map = {'技术': 'Tech', '产品': 'Product'}
df_trans['部门_EN'] = df_trans['部门'].map(dept_map)
# 2. apply: 用于 DataFrame 或 Series
# 对 '薪资' 列应用 lambda 函数:计算税后 (假设税率 10%)
df_trans['税后薪资'] = df_trans['薪资'].apply(lambda x: x * 0.9)
# 对整行应用函数:生成描述信息
defgenerate_desc(row):
returnf"{row['部门_EN']}部员工,税后收入{row['税后薪资']}"
df_trans['描述'] = df_trans.apply(generate_desc, axis=1)
print("\n变换后数据:\n", df_trans)
5.2 排序
df_sort = df_trans.sort_values(by='税后薪资', ascending=False)
print("\n按税后薪资降序:\n", df_sort)
# 多列排序
# df.sort_values(by=['部门', '薪资'], ascending=[True, False])
六、数据聚合与分组 (GroupBy)
Split-Apply-Combine 模式:拆分 -> 应用函数 -> 合并。这是数据分析中最核心的操作之一。
【代码实践】:
data = {
'部门': ['销售', '销售', '技术', '技术', '技术', '人力'],
'员工': ['A', 'B', 'C', 'D', 'E', 'F'],
'薪资': [5000, 6000, 12000, 13000, 11000, 7000],
'绩效': [80, 90, 85, 95, 88, 70]
}
df_group = pd.DataFrame(data)
# 1. 单列分组求均值
print("各部门平均薪资:\n", df_group.groupby('部门')['薪资'].mean())
# 2. 多列分组,多种聚合函数
# 对 '薪资' 求和、均值;对 '绩效' 求最大值
agg_result = df_group.groupby('部门').agg({
'薪资': ['sum', 'mean'],
'绩效': 'max'
})
print("\n复杂聚合:\n", agg_result)
# 3. 重置索引 (将分组后的索引变回普通列)
clean_result = df_group.groupby('部门')['薪资'].mean().reset_index()
print("\n重置索引后:\n", clean_result)
# 4. 分组转换 (Transform):保留原行数,填入组内统计值
# 例如:计算每个人薪资与该部门平均薪资的差额
df_group['部门平均薪资'] = df_group.groupby('部门')['薪资'].transform('mean')
df_group['薪资差异'] = df_group['薪资'] - df_group['部门平均薪资']
print("\n添加组内统计列:\n", df_group)
七、数据合并与连接
7.1 合并 (Merge):类似 SQL Join
基于共同的列(键)进行连接。
df_left = pd.DataFrame({
'key': ['K0', 'K1', 'K2', 'K3'],
'A': ['A0', 'A1', 'A2', 'A3']
})
df_right = pd.DataFrame({
'key': ['K0', 'K1', 'K2', 'K4'],
'B': ['B0', 'B1', 'B2', 'B4']
})
# 1. 内连接 (Inner Join):只保留两边都有的 key
merge_inner = pd.merge(df_left, df_right, on='key', how='inner')
print("内连接:\n", merge_inner)
# 2. 外连接 (Outer Join):保留所有 key,缺失填 NaN
merge_outer = pd.merge(df_left, df_right, on='key', how='outer')
print("\n外连接:\n", merge_outer)
# 3. 左连接 (Left Join):以左边为主
merge_left = pd.merge(df_left, df_right, on='key', how='left')
7.2 拼接 (Concat):物理堆叠
将多个 DataFrame 上下或左右拼在一起。
df1 = pd.DataFrame({'A': ['A0', 'A1'], 'B': ['B0', 'B1']})
df2 = pd.DataFrame({'A': ['A2', 'A3'], 'B': ['B2', 'B3']})
# 上下拼接 (默认 axis=0)
concat_vert = pd.concat([df1, df2], ignore_index=True) # ignore_index=True 重置索引
print("上下拼接:\n", concat_vert)
# 左右拼接 (axis=1)
concat_horiz = pd.concat([df1, df2], axis=1)
八、透视表与交叉表
快速进行多维数据分析,类似 Excel 的 Pivot Table。
df_pivot = pd.DataFrame({
'日期': ['2023-01', '2023-01', '2023-02', '2023-02', '2023-01'],
'地区': ['北', '南', '北', '南', '北'],
'销售额': [100, 200, 150, 250, 120],
'利润': [10, 20, 15, 25, 12]
})
# 1. 透视表
# 行:日期,列:地区,值:销售额,聚合函数:求和
pivot_table = pd.pivot_table(
df_pivot,
values='销售额',
index='日期',
columns='地区',
aggfunc='sum',
fill_value=0# 缺失值填0
)
print("销售透视表:\n", pivot_table)
# 2. 交叉表 (频数统计)
# 统计每个日期、地区的记录数
cross_tab = pd.crosstab(df_pivot['日期'], df_pivot['地区'])
print("\n频次交叉表:\n", cross_tab)
九、综合实战案例:电商销售数据分析
场景:你拿到了一份电商订单数据,需要回答以下业务问题:
import pandas as pd
import numpy as np
# 1. 构造模拟数据集
np.random.seed(42)
n_rows = 1000
data = {
'订单ID': range(1, n_rows + 1),
'用户ID': np.random.choice(['U001', 'U002', 'U003', 'U004', 'U005'], n_rows),
'日期': pd.date_range(start='2023-01-01', periods=n_rows, freq='H'), # 小时级数据
'地区': np.random.choice(['华东', '华北', '华南', '西部'], n_rows),
'商品类别': np.random.choice(['电子产品', '家居', '服装', '食品'], n_rows),
'销售额': np.random.uniform(50, 500, n_rows).round(2),
'数量': np.random.randint(1, 5, n_rows)
}
df_shop = pd.DataFrame(data)
# 2. 数据预处理
# 提取星期几 (0=周一, 6=周日)
df_shop['星期'] = df_shop['日期'].dt.dayofweek
df_shop['是否周末'] = df_shop['星期'].apply(lambda x: 1if x >= 5else0)
# --- 问题 1: 哪个地区的销售额最高? ---
region_sales = df_shop.groupby('地区')['销售额'].sum().sort_values(ascending=False)
print("=== 各地区总销售额 ===")
print(region_sales)
# --- 问题 2: 哪类商品在周末的销量(数量)最好? ---
# 先筛选周末数据
weekend_data = df_shop[df_shop['是否周末'] == 1]
# 再分组聚合
weekend_cat_sales = weekend_data.groupby('商品类别')['数量'].sum().sort_values(ascending=False)
print("\n=== 周末各类商品销量 ===")
print(weekend_cat_sales)
# --- 问题 3: 计算每个用户的复购率 (购买次数 > 1 的用户比例) ---
# 方法:统计每个用户的订单数
user_order_counts = df_shop.groupby('用户ID')['订单ID'].count()
# 找出订单数 > 1 的用户
repeat_users = user_order_counts[user_order_counts > 1].count()
total_users = user_order_counts.count()
repurchase_rate = repeat_users / total_users
print(f"\n=== 用户复购分析 ===")
print(f"总用户数: {total_users}")
print(f"复购用户数: {repeat_users}")
print(f"复购率: {repurchase_rate:.2%}")
# --- 进阶:生成月度销售报表 (透视表) ---
df_shop['月份'] = df_shop['日期'].dt.to_period('M')
monthly_report = pd.pivot_table(
df_shop,
values='销售额',
index='月份',
columns='地区',
aggfunc='sum',
fill_value=0
)
print("\n=== 月度分地区销售报表 ===")
print(monthly_report)
核心回顾
- 数据结构:
Series (一维) 和 DataFrame (二维) 是核心。 - 索引选择:牢记
.loc (标签) 和 .iloc (位置) 的区别。 - 数据清洗:
dropna, fillna, drop_duplicates, astype 是日常高频操作。 - 分组聚合:
groupby + agg 是解决“分类统计”问题的万能钥匙。 - 合并连接:
merge (SQL风格) 和 concat (堆叠风格) 用于整合多源数据。 - 时间序列:Pandas 对时间处理极其强大 (
dt 访问器,resample 重采样)。
性能优化小贴士
- 避免循环:尽量使用向量化操作或
groupby,不要用 for 循环遍历 DataFrame 的行。 - 数据类型:加载大文件时,主动指定
dtype(如将对象类型的 ID 列设为 category),可节省大量内存。 - 链式调用:Pandas 支持链式操作,但要注意
SettingWithCopyWarning 警告,尽量使用 .loc 进行赋值。