目录
  1. 1. 1. 核心数据结构:Series 与 DataFrame 的内部机制
    1. 1.1. 1.1 Series
    2. 1.2. 1.2 DataFrame
    3. 1.3. 1.3 创建 DataFrame 的多种方式
  2. 2. 2. 索引进阶:.loc, .iloc, Boolean Indexing
    1. 2.1. 2.1 .loc — 基于标签的索引
    2. 2.2. 2.2 .iloc — 基于整数位置的索引
    3. 2.3. 2.3 Boolean Indexing 布尔索引
    4. 2.4. 2.4 .at 和 .iat — 最快的标量访问
    5. 2.5. 2.5 多重索引(MultiIndex / Hierarchical Index)
  3. 3. 3. 缺失值处理
    1. 3.1. 3.1 检测缺失值
    2. 3.2. 3.2 填充缺失值
    3. 3.3. 3.3 删除缺失值
  4. 4. 4. GroupBy:聚合、变换与过滤
    1. 4.1. 4.1 聚合(Aggregation)
    2. 4.2. 4.2 变换(Transform):保持原形状
    3. 4.3. 4.3 过滤(Filter):对组进行筛选
    4. 4.4. 4.4 自定义 Apply
    5. 4.5. 4.5 性能注意事项
  5. 5. 5. 合并与连接:merge, concat, join
    1. 5.1. 5.1 pd.concat — 轴向拼接
    2. 5.2. 5.2 pd.merge — 类似 SQL JOIN 的关键列合并
    3. 5.3. 5.3 DataFrame.join — 索引对齐合并
    4. 5.4. 5.4 合并的去重标记
    5. 5.5. 5.5 合并中的重复列问题
  6. 6. 6. 日期时间与重采样
    1. 6.1. 6.1 DatetimeIndex 的创建与操作
    2. 6.2. 6.2 时间偏移(DateOffset)
    3. 6.3. 6.3 重采样(Resampling)
    4. 6.4. 6.4 滑动窗口(Rolling Window)
  7. 7. 7. 类别数据(Categorical Data)
    1. 7.1. 7.1 为什么使用 Categorical?
    2. 7.2. 7.2 Categorical 的排序
    3. 7.3. 7.3 类别操作的陷阱
  8. 8. 8. 数据清洗配方
    1. 8.1. 8.1 删除重复行
    2. 8.2. 8.2 字符串清洗
    3. 8.3. 8.3 数值清洗
    4. 8.4. 8.4 类型转换
  9. 9. 9. 性能优化技巧
    1. 9.1. 9.1 向量化代替循环
    2. 9.2. 9.2 使用正确的遍历方式
    3. 9.3. 9.3 分块读取大文件
    4. 9.4. 9.4 eval() 和 query() 的性能
    5. 9.5. 9.5 dtypes 优化
  10. 10. 10. 常用数据分析工作流
    1. 10.1. 10.1 完整的数据分析模板
    2. 10.2. 10.2 透视表(Pivot Table)
  11. 11. 11. 面试高频问答
【Python系列】Pandas基础篇

DataFrame 是 Python 中 Pandas 库中的一种数据结构,它类似 Excel,是一种二维表。或许说它可能有点像 MATLAB 的矩阵,但是 MATLAB 的矩阵只能放数值型值(当然 MATLAB 也可以用 cell 存放多类型数据),DataFrame 的单元格可以存放数值、字符串等,这和 Excel 表很像。同时 DataFrame 可以设置列名 columns 与行名 index,可以通过位置获取数据也可以通��列名和行名定位,具体方法在后面细说。


1. 核心数据结构:Series 与 DataFrame 的内部机制

1.1 Series

Series 是一维带标签数组,可以视为”带索引的 NumPy array”。其底层数据存储在 numpy.ndarray 中,索引存储在 pd.Index 对象中。

import pandas as pd
import numpy as np

# 创建 Series
s = pd.Series([10, 20, 30, 40], index=['a', 'b', 'c', 'd'])
print(s)
# a 10
# b 20
# c 30
# d 40
# dtype: int64

# Series 的三大属性
print(s.values) # array([10, 20, 30, 40]) — NumPy 数组
print(s.index) # Index(['a', 'b', 'c', 'd'], dtype='object')
print(s.dtype) # int64

Series 底层:Pandas 内部使用 NumPy 的连续内存块存储数据,索引是一个独立的 Index 对象。对于数值类型,直接使用 NumPy 的 int64/float64 数组;对于字符串/对象类型,使用 object dtype 的 NumPy 数组(每个元素是一个 Python 对象指针)。

Series 的自定义索引 vs 默认整数索引:即使设置了自定义索引,仍然可以通过隐式的整数位置进行访问(.iloc 语义)。

1.2 DataFrame

DataFrame 是二维带标签数据结构,可以视为”由多个 Series 组成的字典”(每列是一个 Series)。底层数据以列为主序(column-major)存储在 BlockManager 中,相同 dtype 的列会被合并到一个 Block 中。

# 创建 DataFrame
df = pd.DataFrame({
'name': ['Alice', 'Bob', 'Charlie'],
'age': [25, 30, 35],
'score': [90.5, 85.0, 88.5]
})
print(df)
# name age score
# 0 Alice 25 90.5
# 1 Bob 30 85.0
# 2 Charlie 35 88.5

DataFrame 的内部存储 Block Manager

DataFrame
├── BlockManager
│ ├── Block 0: float64 (score 列)
│ ├── Block 1: int64 (age 列)
│ └── Block 2: object (name 列)
├── Row Index: RangeIndex(0, 3)
└── Column Index: Index(['name', 'age', 'score'])

这种按 dtype 分块存储的策略称为 consolidated blocks,好处是:

  • 相同 dtype 的列共享底层连续内存,NumPy 操作可以向量化
  • 减少内存碎片
  • 列级操作(如对整个 float 列加减)非常高效

Pandas 2.0+ 的变化:引入了基于 PyArrow 后端的 ArrowDtype,改用了列式内存布局而非 BlockManager,与 Apache Arrow 生态对齐,显著降低字符串和高基数列的内存开销。

1.3 创建 DataFrame 的多种方式

# 从字典创建
pd.DataFrame({'A': [1, 2], 'B': [3, 4]})

# 从列表的字典创建
pd.DataFrame([{'a': 1, 'b': 2}, {'a': 3, 'b': 4}])

# 从 NumPy 数组创建
pd.DataFrame(np.random.randn(5, 3), columns=['A', 'B', 'C'])

# 从 CSV 读取(最常用)
df = pd.read_csv('data.csv',
dtype={'id': str}, # 指定列类型
parse_dates=['date'], # 自动解析日期
na_values=['NA', '?']) # 自定义缺失值标记

# 从数据库读取
from sqlalchemy import create_engine
engine = create_engine('sqlite:///mydb.db')
df = pd.read_sql('SELECT * FROM table_name', engine)

2. 索引进阶:.loc, .iloc, Boolean Indexing

2.1 .loc — 基于标签的索引

.loc 使用行列标签(label)进行选择,遵循 Python 切片语法——包含两端(start 和 stop 都在结果中):

df = pd.DataFrame({
'A': [1, 2, 3, 4, 5],
'B': [10, 20, 30, 40, 50],
'C': [100, 200, 300, 400, 500]
}, index=['r1', 'r2', 'r3', 'r4', 'r5'])

# 选择行
df.loc['r2'] # 单行 → Series
df.loc['r2':'r4'] # 行切片 → DataFrame(包含 r4)
df.loc[['r1', 'r3', 'r5']] # 多行(花式索引)

# 选择行 + 列
df.loc['r2', 'B'] # 标量值
df.loc['r2':'r4', ['A', 'C']] # 行切片 + 列列表

# 条件选择
df.loc[df['A'] > 2] # 布尔索引(等价于 df[df['A'] > 2])

2.2 .iloc — 基于整数位置的索引

.iloc 使用整数位置(0-based)进行选择,遵循 Python 切片语法——不含末端(与 Python 列表切片一致):

df.iloc[1]          # 第 2 行 → Series
df.iloc[1:4] # 第 2~4 行(不含第 5 行),与 Python 切片一致
df.iloc[1:4, 0:2] # 第 2~4 行,第 1~2 列
df.iloc[-1] # 最后一行
df.iloc[[0, 2, 4]] # 花式索引:第 1, 3, 5 行

2.3 Boolean Indexing 布尔索引

# 单条件
df[df['age'] > 25]

# 多条件 — 使用 & (and)、| (or)、~ (not),必须用括号包裹每个条件
df[(df['age'] > 25) & (df['score'] > 85)]

# 使用 .isin()
df[df['department'].isin(['Engineering', 'Data Science'])]

# 使用 .between()
df[df['age'].between(25, 35)]

# 使用 .query() — 更可读的字符串表达式(适合复杂条件)
df.query('age > 25 and score > 85')
df.query('department in ["Engineering", "Data Science"]')

# 使用 .str 访问器进行字符串条件
df[df['name'].str.contains('li')] # 包含 'li'
df[df['name'].str.startswith('A')] # 以 A 开头
df[df['email'].str.match(r'^[a-z]+@')] # 正则匹配

2.4 .at.iat — 最快的标量访问

# .at — 基于标签的标量访问(比 .loc 快,但不支持布尔索引)
df.at['r2', 'A'] # 等价于 df.loc['r2', 'A'],但更快

# .iat — 基于位置的标量访问
df.iat[1, 0] # 等价于 df.iloc[1, 0],但更快

loc vs iloc vs at vs iat 性能对比

方法 索引方式 返回值 速度 使用场景
.loc[] 标签 标量/Series/DataFrame 通用的标签选择
.iloc[] 整数位置 标量/Series/DataFrame 通用的位置选择
.at[] 标签 标量 最快 单值读写
.iat[] 整数位置 标量 最快 单值读写

2.5 多重索引(MultiIndex / Hierarchical Index)

arrays = [['bar', 'bar', 'baz', 'baz', 'foo', 'foo'],
['one', 'two', 'one', 'two', 'one', 'two']]
tuples = list(zip(*arrays))
index = pd.MultiIndex.from_tuples(tuples, names=['first', 'second'])
df_multi = pd.DataFrame(np.random.randn(6, 2), index=index, columns=['A', 'B'])

# 多级索引的选择
df_multi.loc['bar'] # 选择 first='bar' 的所有行
df_multi.loc[('bar', 'two')] # 选择 first='bar' AND second='two'
df_multi.loc['bar':'foo'] # 跨级别的切片
df_multi.xs('one', level='second') # xs = cross-section,选择特定级别的值

3. 缺失值处理

3.1 检测缺失值

df.isna()        # 返回布尔 DataFrame,标记每个位置是否缺失
df.isna().sum() # 每列的缺失值计数
df.info() # 查看每列的非空计数和 dtype

# Pandas 的缺失值体系
# NaN (float) → np.nan, pd.NA
# NaT (datetime) → 时间类型的缺失值
# None → Python None,通常被自动转换为 NaN
# pd.NA → Pandas 1.0+ 统一缺失值(支持 nullable dtypes)

3.2 填充缺失值

# 固定值填充
df.fillna(0)
df.fillna({'age': 0, 'name': 'unknown', 'score': df['score'].median()})

# 前向/后向填充(沿时间序列传播值)
df.fillna(method='ffill') # 用前一行的值填充
df.fillna(method='bfill') # 用后一行的值填充

# 插值填充(线性、二次、三次等)
df.interpolate(method='linear') # 线性插值
df.interpolate(method='quadratic') # 二次插值
df.interpolate(method='time') # 时间感知插值(索引为 DatetimeIndex)

3.3 删除缺失值

df.dropna()                 # 删除任何包含 NaN 的行
df.dropna(axis=1) # 删除任何包含 NaN 的列
df.dropna(subset=['age']) # 仅当 'age' 列缺失时才删除该行
df.dropna(thresh=3) # 保留至少有 3 个非空值的行

4. GroupBy:聚合、变换与过滤

GroupBy 是 Pandas 中最强大的操作之一,其基本流程为:Split(分组)→ Apply(应用函数)→ Combine(合并结果)

4.1 聚合(Aggregation)

df = pd.DataFrame({
'department': ['Eng', 'Eng', 'DS', 'DS', 'Eng'],
'employee': ['Alice', 'Bob', 'Charlie', 'David', 'Eve'],
'salary': [120, 100, 130, 140, 110],
'bonus': [10, 8, 12, 15, 9]
})

# 单函数聚合
df.groupby('department')['salary'].mean()
# department
# DS 135.0
# Eng 110.0

# 多函数聚合
df.groupby('department')['salary'].agg(['mean', 'std', 'min', 'max', 'count'])

# 不同列使用不同聚合函数
df.groupby('department').agg({
'salary': 'mean',
'bonus': ['sum', 'max'],
'employee': 'count'
})

4.2 变换(Transform):保持原形状

transform 返回与原始 DataFrame 同样长度(groupby 形状对齐)的结果:

# 组内标准化
df['salary_zscore'] = df.groupby('department')['salary'].transform(
lambda x: (x - x.mean()) / x.std()
)

# 组内排名
df['salary_rank_in_dept'] = df.groupby('department')['salary'].transform('rank')

transform vs agg 的区别:

操作 输入 输出 输出行数
agg group → values 聚合值 = 组数
transform group → values 广播到组内每行 = 原表行数
apply sub-DataFrame 任意 可变(取决于函数)

4.3 过滤(Filter):对组进行筛选

# 仅保留包含至少 3 名员工的部门
df.groupby('department').filter(lambda g: len(g) >= 3)

# 仅保留平均薪资 > 120 的部门
df.groupby('department').filter(lambda g: g['salary'].mean() > 120)

4.4 自定义 Apply

# 对每个分组执行复杂的自定义操作
def top_n_salaries(group, n=2):
return group.nlargest(n, 'salary')

df.groupby('department').apply(top_n_salaries, n=2)

4.5 性能注意事项

  • 内置方法优先groupby.sum() 远快于 groupby.apply(sum)(内置方法使用 Cython 加速)
  • 避免 apply 中的循环apply 内部应使用向量化操作
  • pd.Grouper 用于时间分组df.groupby(pd.Grouper(key='date', freq='M'))

5. 合并与连接:merge, concat, join

5.1 pd.concat — 轴向拼接

# 行拼接(纵向堆叠)
df1 = pd.DataFrame({'A': [1, 2], 'B': [3, 4]})
df2 = pd.DataFrame({'A': [5, 6], 'B': [7, 8]})
pd.concat([df1, df2], ignore_index=True) # 重置索引

# 列拼接(横向拼接)
df3 = pd.DataFrame({'C': [9, 10], 'D': [11, 12]})
pd.concat([df1, df3], axis=1)

# 对齐:intersection(交集,默认)vs union(并集)
df1 = pd.DataFrame({'A': [1]}, index=[0])
df2 = pd.DataFrame({'A': [2], 'B': [3]}, index=[0])
pd.concat([df1, df2], join='inner') # 只保留共有的列 A
pd.concat([df1, df2], join='outer') # 保留所有列,缺失值填 NaN

5.2 pd.merge — 类似 SQL JOIN 的关键列合并

# 内连接
pd.merge(left, right, on='key', how='inner')

# 左连接:保留 left 的所有行
pd.merge(left, right, on='key', how='left')

# 不同列名的连接键
pd.merge(left, right, left_on='lkey', right_on='rkey')

# 多键连接
pd.merge(left, right, on=['key1', 'key2'])

# 按索引连接
pd.merge(left, right, left_index=True, right_index=True)

四种连接方式说明

how 参数 行为 SQL 等价
'inner' 仅保留两表都匹配的行 INNER JOIN
'left' 保留左表的所有行 LEFT JOIN
'right' 保留右表的所有行 RIGHT JOIN
'outer' 保留两表的所有行 FULL OUTER JOIN

5.3 DataFrame.join — 索引对齐合并

# 按索引连接
left.join(right, how='inner')

# left.join 本质上是 pd.merge 的包装,等价于:
pd.merge(left, right, left_index=True, right_index=True, how='inner')

5.4 合并的去重标记

# merge 时添加指示器列,标记每行的来源
pd.merge(left, right, on='key', how='outer', indicator=True)
# indicator 列:'left_only', 'right_only', 'both'

5.5 合并中的重复列问题

当两个 DataFrame 有同名非键列时,merge 会自动添加后缀 _x_y

pd.merge(left, right, on='key', suffixes=('_L', '_R'))

6. 日期时间与重采样

6.1 DatetimeIndex 的创建与操作

# 生成日期范围
pd.date_range('2023-01-01', periods=12, freq='ME') # 月末
pd.date_range('2023-01-01', '2023-12-31', freq='W-WED') # 每周三

# 将字符串列转换为 Datetime
df['date'] = pd.to_datetime(df['date_str'])
# 自动推断格式(可指定 format 以提升速度和可靠��)
df['date'] = pd.to_datetime(df['date_str'], format='%Y-%m-%d')

# 提取日期组件
df['year'] = df['date'].dt.year
df['month'] = df['date'].dt.month
df['dayofweek'] = df['date'].dt.dayofweek # Monday=0, Sunday=6
df['quarter'] = df['date'].dt.quarter
df['week_of_year'] = df['date'].dt.isocalendar().week

6.2 时间偏移(DateOffset)

from pandas.tseries.offsets import MonthEnd, BusinessDay

df['next_month_end'] = df['date'] + MonthEnd(1)
df['next_business_day'] = df['date'] + BusinessDay(1)

6.3 重采样(Resampling)

重采样将时间序列从一个频率转换到另一个频率。当以 DatetimeIndex 为索引时使用:

# 创建带 DatetimeIndex 的 Series
ts = pd.Series(np.random.randn(365),
index=pd.date_range('2023-01-01', periods=365, freq='D'))

# 下采样(高频 → 低频):需要指定聚合方式
ts.resample('ME').mean() # 月均值
ts.resample('W').sum() # 周求和
ts.resample('MS').first() # 月首值(MS = Month Start)

# 上采样(低频 → 高频):需要指定插值方式
ts_monthly = ts.resample('ME').mean()
ts_monthly.resample('D').ffill() # 前向填充
ts_monthly.resample('D').interpolate() # 插值

常用频率别名

频率字符串 含义 频率字符串 含义
'D' 日历日 'W' 周(周日结束)
'B' 工作日 'ME' / 'M' 月末
'H' 小时 'MS' 月初
'T' / 'min' 分钟 'Q' / 'QE' 季度末
'S' 'A' / 'YE' 年末

6.4 滑动窗口(Rolling Window)

# 滚动统计
ts.rolling(window=7).mean() # 7 日滚动均值
ts.rolling(window=30).std() # 30 日滚动标准差
ts.rolling(window=14, min_periods=5).sum() # 最少 5 个非空值

# 指数加权(EMA)
ts.ewm(span=7).mean() # span=7 的指数加权移动平均
ts.ewm(alpha=0.3).mean() # 指定平滑因子 alpha

7. 类别数据(Categorical Data)

7.1 为什么使用 Categorical?

在含有大量重复字符串列的场景中,Categorical 类型可将存储从 object 降为整数编码,大幅节省内存:

# 创建 categorical
df['color'] = pd.Categorical(['red', 'blue', 'green', 'red', 'blue'])
# 或者
df['color'] = df['color'].astype('category')

# 查看内存使用对比
df_color_obj = pd.DataFrame({'color': np.random.choice(['red', 'blue', 'green', 'yellow', 'purple'], 1000000)})
print(df_color_obj.memory_usage(deep=True)) # ~60+ MB (object 类型)
df_color_cat = df_color_obj.copy()
df_color_cat['color'] = df_color_cat['color'].astype('category')
print(df_color_cat.memory_usage(deep=True)) # ~1 MB (category 类型)

7.2 Categorical 的排序

# 有序 category
size_order = pd.CategoricalDtype(categories=['S', 'M', 'L', 'XL'], ordered=True)
df['size'] = df['size'].astype(size_order)
df[df['size'] > 'M'] # 有序比较有效

7.3 类别操作的陷阱

  • groupby + observed=True:category 默认展示所有可能的类别(即使没有数据的类别),使用 observed=True 仅展示实际出现的数据组
  • 内存:对于高基数(类别数多)的列,categorical 可能比 object 占用更多内存(类别映射表的开销)
  • 合并:不同 DataFrame 的同一 categorical 列可能有不同的 categories,合并后需要 cat.set_categories() 统一

8. 数据清洗配方

8.1 删除重复行

df.duplicated()           # 返回布尔 Series,标记重复行
df.duplicated(subset=['name', 'date']) # 基于特定列的重复判断
df.drop_duplicates() # 删除重复行(保留第一个出现)
df.drop_duplicates(keep='last') # 保留最后一个出现
df.drop_duplicates(subset=['id'], keep=False) # 删除全部(包括首次出现)

8.2 字符串清洗

# 去除空白
df['name'] = df['name'].str.strip()
df['name'] = df['name'].str.replace(r'\s+', ' ', regex=True) # 合并多余空格

# 大小写标准化
df['city'] = df['city'].str.title() # 首字母大写
df['email'] = df['email'].str.lower()

# 提取子串
df['area_code'] = df['phone'].str.extract(r'\((\d{3})\)')
df['domain'] = df['email'].str.extract(r'@(.+)')

# 拆分列
df[['first_name', 'last_name']] = df['full_name'].str.split(' ', expand=True)

8.3 数值清洗

# 剪裁 outliers
q1, q3 = df['value'].quantile([0.01, 0.99])
df['value_clipped'] = df['value'].clip(q1, q3)

# 基于 IQR 去除异常值
Q1 = df['value'].quantile(0.25)
Q3 = df['value'].quantile(0.75)
IQR = Q3 - Q1
df_clean = df[(df['value'] >= Q1 - 1.5 * IQR) & (df['value'] <= Q3 + 1.5 * IQR)]

# 标准化/归一化
df['z_score'] = (df['value'] - df['value'].mean()) / df['value'].std()
df['min_max'] = (df['value'] - df['value'].min()) / (df['value'].max() - df['value'].min())

8.4 类型转换

# 安全类型转换
pd.to_numeric(df['col'], errors='coerce') # 无法转换的变为 NaN
pd.to_datetime(df['col'], errors='coerce')
pd.to_timedelta(df['col'], errors='coerce')

# 使用 nullable dtypes(Pandas 1.0+)
df = df.convert_dtypes() # 自动转换为最合适的 nullable dtype
# Int64(可含 NA), Float64, boolean, string(不是 object)

9. 性能优化技巧

9.1 向量化代替循环

# 错误(慢):逐行遍历
for i in range(len(df)):
df.iloc[i, 2] = df.iloc[i, 0] + df.iloc[i, 1]

# 正确(快):向量化操作
df['C'] = df['A'] + df['B']

# 条件赋值:np.where/np.select 或 .loc
df['category'] = np.where(df['value'] > 0, 'positive', 'non-positive')

conditions = [df['score'] >= 90, df['score'] >= 60, df['score'] < 60]
choices = ['A', 'B', 'C']
df['grade'] = np.select(conditions, choices, default='F')

9.2 使用正确的遍历方式

# 如果需要逐行操作,按以下优先顺序:
# 1. 向量化(最快)
# 2. itertuples()(快,返回 namedtuple)
# 3. iterrows()(慢,每行创建 Series)
# 4. for + iloc(最慢,不推荐)

# itertuples 示例
for row in df.itertuples():
print(row.Index, row.name, row.age) # Index 是行索引

9.3 分块读取大文件

# 分块迭代读取
chunk_size = 100000
chunks = []
for chunk in pd.read_csv('large_file.csv', chunksize=chunk_size):
# 每块独立处理
chunks.append(chunk.groupby('category')['value'].sum())

result = pd.concat(chunks).groupby('category').sum()

9.4 eval() 和 query() 的性能

# 使用 numexpr 引擎的 pd.eval() 加速复杂表达式
df['result'] = pd.eval('(df.A + df.B) / (df.C - df.D)')

# DataFrame.eval() 支持列名作为变量
df.eval('ratio = (A + B) / (C - D)', inplace=True)

9.5 dtypes 优化

# 降级数值精度以节省内存
df['int_col'] = pd.to_numeric(df['int_col'], downcast='integer')
df['float_col'] = pd.to_numeric(df['float_col'], downcast='float')

# 查看内存使用
df.memory_usage(deep=True).sum() / 1024**2 # MB

10. 常用数据分析工作流

10.1 完整的数据分析模板

import pandas as pd
import numpy as np

# 1. 加载数据
df = pd.read_csv('data.csv',
parse_dates=['date'],
dtype={'customer_id': 'string'})

# 2. 初始探索
print(df.shape)
print(df.info())
print(df.describe(include='all')) # 含非数值列的统计
print(df.isna().sum()[df.isna().sum() > 0])

# 3. 清洗
df = df.drop_duplicates(subset=['id'])
df['date'] = pd.to_datetime(df['date'], errors='coerce')
df['category'] = df['category'].str.strip().str.lower()
df['value'] = pd.to_numeric(df['value'], errors='coerce')

# 4. 特征工程
df['month'] = df['date'].dt.month
df['value_log'] = np.log1p(df['value'])

# 5. 分组分析
result = (df.groupby(['month', 'category'])
.agg(count=('id', 'nunique'),
avg_value=('value', 'mean'),
total_value=('value', 'sum'))
.reset_index())

# 6. 透视表
pivot = df.pivot_table(values='value', index='month',
columns='category', aggfunc='sum',
fill_value=0)

# 7. 输出
result.to_csv('result.csv', index=False)
# result.to_excel('result.xlsx', sheet_name='Summary', index=False)

10.2 透视表(Pivot Table)

# 类似于 Excel 数据透视表
pd.pivot_table(df,
values='sales',
index=['region'],
columns=['year', 'quarter'],
aggfunc='sum',
margins=True, # 添加汇总行列
fill_value=0)

11. 面试高频问答

Q1: Pandas 中 lociloc 的核心区别是什么?什么时候会出 bug?

loc 基于标签索引,iloc 基于整数位置索引。最常见的 bug 出现在整数索引上:当 DataFrame 的索引就是整数(如默认的 RangeIndex)时,df.loc[0]df.iloc[0] 碰巧返回相同结果;但当你修改索引为非连续的整数(比如随机打乱)后,df.loc[0] 查找的是索引标签为 0 的行,而 df.iloc[0] 始终返回第一行。另一个区别是切片语义:loc[start:stop] 包含 stop,iloc[start:stop] 不包含 stop。

Q2: GroupBy 的 applytransform 有什么区别?什么时候应该用哪个?

transform 保证返回与原本组相同行数的结果(每个元素被映射为组内的对应变换值)。apply 不保证返回行数——它可以把一个组映射为一行(聚合)或多行(可大于或小于组内行数)。应该用 transform 当你要做组内标准化、填充缺失值(组内均值填充)等需要保持原形状的操作;用 apply 当你需要对组做任意复杂的自定义处理。

Q3: 如何处理大数据集(超过内存)的 Pandas 分析?

几个策略:

  1. 分块读取(chunksize 参数)并按块聚合
  2. 仅在读取时选择需要的列(usecols 参数)
  3. 使用更精确的 dtype 降低内存(如 int8 代替 int64category 代替 object
  4. 使用 Dask 或 Polars(前者模仿 Pandas API 但惰性执行,后者有更好的内存管理和更快的内核)
  5. 使用 Pandas 2.0+ 的 PyArrow 后端来降低字符串内存

Q4: Pandas 的 mergevalidate 参数是做什么的?

validate 参数用于检查合并键的唯一性,防止意外的多对多连接导致结果行数爆炸:

  • "one_to_one":合并键在左右表中都是唯一的
  • "one_to_many":合并键在左表中唯一,右表可以有重复
  • "many_to_one":合并键在右表中唯一,左表可以有重复
  • "many_to_many":允许合并键在两张表中都有重复

在数据清洗工作中,validate 可以在早期发现”我以为某列是主键,但实际不是”的问题。

Q5: 解释 Pandas 的链式赋值(chained assignment)问题以及如何避免。

链式赋值(如 df[df['A'] > 0]['B'] = 0)会产生不可预测的结果,因为 Pandas 可能返回一个视图(view)或副本(copy),赋值操作可能不会反映在原 DataFrame 上。正确做法是使用单个 .loc 操作:df.loc[df['A'] > 0, 'B'] = 0。还可以通过 pd.options.mode.copy_on_write = True(Pandas 2.1+)启用 Copy-on-Write 模式,在这种模式下链式赋值会显式抛出错误而不是静默失败。

文章作者: Leo·Cheung
文章链接: http://tufusi.com/2021/09/08/%E3%80%90Python%E7%B3%BB%E5%88%97%E3%80%91Pandas%E5%9F%BA%E7%A1%80%E7%AF%87/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 ONE·PIECE
打赏
  • 微信
  • 支付宝

评论