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
s = pd.Series([10, 20, 30, 40], index=['a', 'b', 'c', 'd']) print(s)
print(s.values) print(s.index) print(s.dtype)
|
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 中。
df = pd.DataFrame({ 'name': ['Alice', 'Bob', 'Charlie'], 'age': [25, 30, 35], 'score': [90.5, 85.0, 88.5] }) print(df)
|
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}])
pd.DataFrame(np.random.randn(5, 3), columns=['A', 'B', 'C'])
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'] df.loc['r2':'r4'] df.loc[['r1', 'r3', 'r5']]
df.loc['r2', 'B'] df.loc['r2':'r4', ['A', 'C']]
df.loc[df['A'] > 2]
|
2.2 .iloc — 基于整数位置的索引
.iloc 使用整数位置(0-based)进行选择,遵循 Python 切片语法——不含末端(与 Python 列表切片一致):
df.iloc[1] df.iloc[1:4] df.iloc[1:4, 0:2] df.iloc[-1] df.iloc[[0, 2, 4]]
|
2.3 Boolean Indexing 布尔索引
df[df['age'] > 25]
df[(df['age'] > 25) & (df['score'] > 85)]
df[df['department'].isin(['Engineering', 'Data Science'])]
df[df['age'].between(25, 35)]
df.query('age > 25 and score > 85') df.query('department in ["Engineering", "Data Science"]')
df[df['name'].str.contains('li')] df[df['name'].str.startswith('A')] df[df['email'].str.match(r'^[a-z]+@')]
|
2.4 .at 和 .iat — 最快的标量访问
df.at['r2', 'A']
df.iat[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'] df_multi.loc[('bar', 'two')] df_multi.loc['bar':'foo'] df_multi.xs('one', level='second')
|
3. 缺失值处理
3.1 检测缺失值
df.isna() df.isna().sum() df.info()
|
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')
|
3.3 删除缺失值
df.dropna() df.dropna(axis=1) df.dropna(subset=['age']) df.dropna(thresh=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()
df.groupby('department')['salary'].agg(['mean', 'std', 'min', 'max', 'count'])
df.groupby('department').agg({ 'salary': 'mean', 'bonus': ['sum', 'max'], 'employee': 'count' })
|
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):对组进行筛选
df.groupby('department').filter(lambda g: len(g) >= 3)
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)
df1 = pd.DataFrame({'A': [1]}, index=[0]) df2 = pd.DataFrame({'A': [2], 'B': [3]}, index=[0]) pd.concat([df1, df2], join='inner') pd.concat([df1, df2], join='outer')
|
5.2 pd.merge — 类似 SQL JOIN 的关键列合并
pd.merge(left, right, on='key', how='inner')
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')
pd.merge(left, right, left_index=True, right_index=True, how='inner')
|
5.4 合并的去重标记
pd.merge(left, right, on='key', how='outer', indicator=True)
|
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')
df['date'] = pd.to_datetime(df['date_str'])
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 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 为索引时使用:
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()
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() ts.rolling(window=30).std() ts.rolling(window=14, min_periods=5).sum()
ts.ewm(span=7).mean() ts.ewm(alpha=0.3).mean()
|
7. 类别数据(Categorical Data)
7.1 为什么使用 Categorical?
在含有大量重复字符串列的场景中,Categorical 类型可将存储从 object 降为整数编码,大幅节省内存:
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)) 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))
|
7.2 Categorical 的排序
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() 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 数值清洗
q1, q3 = df['value'].quantile([0.01, 0.99]) df['value_clipped'] = df['value'].clip(q1, q3)
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') pd.to_datetime(df['col'], errors='coerce') pd.to_timedelta(df['col'], errors='coerce')
df = df.convert_dtypes()
|
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']
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 使用正确的遍历方式
for row in df.itertuples(): print(row.Index, row.name, row.age)
|
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() 的性能
df['result'] = pd.eval('(df.A + df.B) / (df.C - df.D)')
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
|
10. 常用数据分析工作流
10.1 完整的数据分析模板
import pandas as pd import numpy as np
df = pd.read_csv('data.csv', parse_dates=['date'], dtype={'customer_id': 'string'})
print(df.shape) print(df.info()) print(df.describe(include='all')) print(df.isna().sum()[df.isna().sum() > 0])
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')
df['month'] = df['date'].dt.month df['value_log'] = np.log1p(df['value'])
result = (df.groupby(['month', 'category']) .agg(count=('id', 'nunique'), avg_value=('value', 'mean'), total_value=('value', 'sum')) .reset_index())
pivot = df.pivot_table(values='value', index='month', columns='category', aggfunc='sum', fill_value=0)
result.to_csv('result.csv', index=False)
|
10.2 透视表(Pivot Table)
pd.pivot_table(df, values='sales', index=['region'], columns=['year', 'quarter'], aggfunc='sum', margins=True, fill_value=0)
|
11. 面试高频问答
Q1: Pandas 中 loc 和 iloc 的核心区别是什么?什么时候会出 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 的 apply 和 transform 有什么区别?什么时候应该用哪个?
transform 保证返回与原本组相同行数的结果(每个元素被映射为组内的对应变换值)。apply 不保证返回行数——它可以把一个组映射为一行(聚合)或多行(可大于或小于组内行数)。应该用 transform 当你要做组内标准化、填充缺失值(组内均值填充)等需要保持原形状的操作;用 apply 当你需要对组做任意复杂的自定义处理。
Q3: 如何处理大数据集(超过内存)的 Pandas 分析?
几个策略:
- 分块读取(
chunksize 参数)并按块聚合
- 仅在读取时选择需要的列(
usecols 参数)
- 使用更精确的 dtype 降低内存(如
int8 代替 int64,category 代替 object)
- 使用 Dask 或 Polars(前者模仿 Pandas API 但惰性执行,后者有更好的内存管理和更快的内核)
- 使用 Pandas 2.0+ 的 PyArrow 后端来降低字符串内存
Q4: Pandas 的 merge 中 validate 参数是做什么的?
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 模式,在这种模式下链式赋值会显式抛出错误而不是静默失败。