下面所有输出均实跑验证(pandas 3.0.5,Python 3.12)。
一、一句话说清关系
DataFrame就是"若干个共用同一套行索引的Series,按列拼起来",Series 是它的"一列"。
两种皆正确的比喻:
| 比喻 | 对应关系 |
|---|---|
| Excel 表格 | DataFrame = 整张表;Series = 表里的一列 或一行 |
| Python 字典 | DataFrame = {'列名1': Series1, '列名2': Series2, ...} |
| NumPy 数组 | Series = 一维数组 + 索引标签;DataFrame = 二维数组 + 行标签 + 列标签 |
二、结构对比:维度、轴、dtype
python
df = pd.DataFrame({
'姓名': ['张三', '李四', '王五'],
'语文': [88, 92, 76],
'数学': [95, 81, np.nan],
})
姓名 语文 数学
0 张三 88 95.0
1 李四 92 81.0
2 王五 76 NaN
实测结构差异:
| 对象 | 类型 | ndim |
shape |
轴 |
|---|---|---|---|---|
df |
DataFrame | 2 | (3, 3) |
index(行) + columns(列) |
df['语文'] |
Series | 1 | (3,) |
只有 index |
df[['语文']] |
DataFrame | 2 | (3, 1) |
index + columns |
df.loc[0] |
Series | 1 | (3,) |
只有 index |
关键差别 :ndim 一个 2 一个 1,这是"容器"与"元素"的区别。
dtype 也不一样:
df.dtypes
姓名 str ← 每列 dtype 可以各不相同
语文 int64
数学 float64
df.loc[0].dtype → object ← 一行的 dtype 必须统一,混合类型只能变 object
教学要点 :DataFrame 是"列式存储"------按列管理 dtype;Series 内部 dtype 必须统一。 这也是"取一行"会把数字变成 object、导致无法直接求和的原因(下文第八节的 TypeError)。
三、DataFrame 的本质:Series 的字典
python
print(df.columns.tolist()) # ['姓名', '语文', '数学']
for c in df.columns:
print(f"df[{c!r}] 是 Series, dtype={df[c].dtype}")
df['姓名'] 是 Series, dtype=str
df['语文'] 是 Series, dtype=int64
df['数学'] 是 Series, dtype=float64
也就是说,这个 DataFrame 等价于:
python
{
'姓名': Series(['张三','李四','王五']),
'语文': Series([88, 92, 76]),
'数学': Series([95.0, 81.0, nan]),
}
三个共享的东西:
- 共享行索引 :三个 Series 的 index 都是
[0, 1, 2] - 共享列顺序 :
df.columns决定列的顺序 - 各自独立的 dtype:每列可以不同类型
注意一个细节 :每次 df['语文'] 都会新建一个 Series 包装对象 (实测 id(df['数学']) == id(df['数学']) → False),不是同一个对象缓存。所以别指望用 is 判断,修改也建议走 df.loc[](见第十节)。
四、从 DataFrame 取 Series(三种方式 + 一个坑)
columns → 姓名 语文 数学
index
↓ 0 张三 88 95.0
1 李四 92 81.0
2 王五 76 NaN
1. 取一列:df['语文'] → Series(竖着切)
python
col = df['语文']
0 88
1 92
2 76
Name: 语文, dtype: int64
index = [0, 1, 2] ← 索引是"行标签"
name = '语文' ← 名字是"列名"
2. 取一行:df.loc[0] → Series(横着切)
python
row = df.loc[0]
姓名 张三
语文 88
数学 95.0
Name: 0, dtype: object
index = ['姓名', '语文', '数学'] ← 索引变成"列标签"!
name = 0 ← 名字是"行标签"
这是最重要的认知:
| 取法 | 得到的 Series 的 index |
Series 的 name |
|---|---|---|
取列 df['列名'] |
行标签 | 列名 |
取行 df.loc[标签] |
列标签 | 行标签 |
一句话 :Series 的 index 永远是"另一个方向的标签"------竖着取,index 是行;横着取,index 是列。
3. ⚠️ 最容易踩的坑:单层方括号 vs 双层方括号
python
df['语文'] # Series shape (3,)
df[['语文']] # DataFrame shape (3, 1) ← 注意这里多了一层 []
| 写法 | 结果类型 | 用途 |
|---|---|---|
df['语文'] |
Series | 当作一列向量用(算均值、加减) |
df[['语文']] |
DataFrame | 保留表格结构(后续要拼表、导出) |
典型后果:
python
df['语文'].mean() # ✅ 84.0 Series 才能直接算
df[['语文']].mean() # Series(index 是列名)
df['语文'].fillna(0) # 返回 Series
df[['语文']].fillna(0) # 返回 DataFrame
两者结果"看起来"差不多,但类型不同,后续能调用的方法也不同,这是学生最常问的问题。
五、从 Series 组装 DataFrame
python
s1 = pd.Series([88, 92, 76], index=['张三', '李四', '王五'], name='语文')
s2 = pd.Series([95, 81, 60], index=['张三', '李四', '王五'], name='数学')
built = pd.DataFrame({'语文': s1, '数学': s2})
语文 数学
张三 88 95
李四 92 81
王五 76 60
三类信息的来源一目了然:
| DataFrame 的部件 | 来自 Series 的什么 |
|---|---|
| 列名 | Series 的 name(或字典的键,字典键优先) |
| 行索引 | Series 的 index |
| 数据 | Series 的 values |
如果 Series 没有
name,用字典形式建表时字典的键会作为列名 ,所以name可以省略。
六、索引对齐:Series 之间运算的核心机制(重点)
pandas 的 Series 做运算时不是按位置对齐,而是按索引(标签)对齐:
python
a = pd.Series([88, 92, 76], index=['张三', '李四', '王五'])
b = pd.Series([95, 81], index=['张三', '赵六'])
print(a + b)
张三 183.0 ← 两人都有,相加
李四 NaN ← a 有 b 没有
王五 NaN ← a 有 b 没有
赵六 NaN ← b 有 a 没有
dtype: float64
规则 :取索引的并集 ,两边都有的才计算,缺的一边补 NaN。
这个机制同样适用于组装 DataFrame:
python
d = pd.DataFrame({'a': a, 'b': b})
a b
张三 88.0 95.0
李四 92.0 NaN
王五 76.0 NaN
赵六 NaN 81.0 ← 行索引取并集
教学价值:
- ✅ 好处:合并两个不同顺序的数据源时,不用担心顺序错乱(这才是 pandas 相比列表的杀手级优势)
- ⚠️ 风险 :索引不一致时会悄悄产生大量 NaN ,且不报错。所以数据清洗时第一步常写
df = df.set_index('学号')或reset_index(drop=True),先统一索引
七、聚合结果为什么是 Series
对 DataFrame 做"沿某一方向压缩"的运算,结果一定是 Series:
python
df.mean(numeric_only=True) # → Series,index = 列名
df[['语文', '数学']].sum(axis=1) # → Series,index = 行索引
df.mean() → Series, index = ['语文', '数学'] ← 每列一个值,挂在列上
df.sum(axis=1) → Series, index = [0, 1, 2] ← 每行一个值,挂在行上
| 操作 | 结果 | 结果 Series 的 index |
|---|---|---|
df.mean(axis=0) |
Series | 列名 |
df.mean(axis=1) |
Series | 行索引 |
df['列'].mean() |
标量(float) | 无 |
df['列'].value_counts() |
Series | 取值 |
这条规律和之前讲的 apply 完全一致 :axis=0 的结果挂在列上,axis=1 的结果挂在行上。
推论 :想"一次算出多列多指标的汇总表",需要用 pd.DataFrame({...}) 把多个 Series 再拼回二维------这就是"Series → DataFrame"的现实用途。
八、广播:DataFrame 与 Series 之间的运算
Series 比 DataFrame 低一维,两者相减时会发生广播(broadcasting):
1. DataFrame 与"列对齐的 Series"(默认按列)
python
scores = df[['语文', '数学']]
print(scores - scores.mean())
语文 数学
0 2.666667 7.0
1 6.666667 -7.0
2 -9.333333 NaN
scores.mean() 的 index 是列名,pandas 自动按列名对齐,把每列的均值"广播"到该列每一行------这就是"每个学生成绩减去科目均分"的标准写法。
2. 标量广播到每个元素
python
print(df['语文'] + 10)
0 98
1 102
2 86
Name: 语文, dtype: int64
3. 与"行"运算必须显式指定 axis=0
python
print(scores.sub(scores.mean(axis=1), axis=0))
语文 数学
0 -3.5 3.5
1 5.5 -5.5
2 0.0 NaN
要点 :Series 的 index 是行标签时,默认会去找同名列 ,找不到就报错或产生全 NaN,所以必须写 axis=0 告诉 pandas"按行对齐"。
4. 一个真实的报错(教学用)
因为 DataFrame 取行后 dtype 变成 object,直接按行求和会失败:
python
df.sum(axis=1)
# TypeError: can only concatenate str (not "int") to str
原因 :这一行里有 '张三'(str)和 88(int),pandas 试图做 '张三' + 88。正确写法是先选数值列:
python
df[['语文', '数学']].sum(axis=1) # 0 183.0 / 1 173.0 / 2 76.0
从底层理解 :这也印证了"DataFrame 是按列组织 dtype 的"------整列类型一致就能向量化计算;一旦横跨不同列(取行),类型混杂就只能退化成 object。
九、相互转换(实用清单,全部实测)
Series → DataFrame
python
df['语文'].to_frame() # 一列 DataFrame
df['语文'].to_frame('语文成绩') # 顺便改列名
row.to_frame().T # 一行 → 一行 DataFrame(需要转置)
语文 语文成绩 姓名 语文 数学
0 88 0 88 0 张三 88 95.0
1 92 1 92
2 76 2 76
要点 :to_frame() 把 Series 的 name 变成列名;如果是"行 Series"(name 是行标签),转成 DataFrame 后会变成一列竖排,要加 .T 才是横的一行。
DataFrame → Series
python
df[['语文']].squeeze() # 单列表 → Series
要点 :squeeze() 只在"只剩一行或一列"时才降维;有多列时原样返回 DataFrame。
DataFrame → 字典(三种模式)
python
df.to_dict() # {'语文': {'张三': 88, '李四': 92}, ...} 嵌套字典
df.to_dict('series') # {'语文': Series, '数学': Series} ← 真正的"Series 的字典"
df.to_dict('list') # {'语文': [88, 92], '数学': [95, 81]}
to_dict('series') 就是第三节那个本质关系的直接证据------DataFrame 拆出来就是一组 Series。
十、修改数据时的"视图 / 副本"关系(最容易出错)
python
sub = df[['语文', '数学']] # 新对象,与 df 不同(is 判断为 False)
sub2 = sub.copy() # 显式副本
sub2.loc[0, '语文'] = 0
print(df.loc[0, '语文']) # 88 ← 原表没变
| 写法 | 是否影响原表 | 建议 |
|---|---|---|
df['语文'] = ... |
✅ 改原表 | 推荐:明确赋值 |
df.loc[0, '语文'] = 0 |
✅ 改原表 | 推荐:精确定位 |
df[['语文']].loc[0] = 0 |
⚠️ 可能不生效,并触发 SettingWithCopyWarning |
别这么写 |
sub = df[['语文']].copy() 后修改 |
❌ 不影响 | 安全 |
教学建议 :修改数据一律用 df.loc[行, 列] = 值 或 df['列'] = 新Series ,不要链式赋值(df[...][...] = ...),避免"改了却没生效"的隐蔽 bug。
十一、关系全景图
┌────────────────────── DataFrame(二维)──────────────────────┐
│ columns: 姓名 语文 数学 │
│ index │
│ 0 张三 88 95.0 ← 每一列是一个 Series │
│ 1 李四 92 81.0 │
│ 2 王五 76 NaN │
└──────────────────────────────────────────────────────────┘
│ df['语文'] ↓ 竖切一列 │ df.loc[1] ↓ 横切一行
▼ ▼
Series(index=行标签, name=列名) Series(index=列标签, name=行标签)
0 88 姓名 李四
1 92 语文 92
2 76 数学 81.0
Name: 语文 Name: 1
│ │
└──── .to_frame() ──────────────→ DataFrame(1 列 / 1 行)
◄──── .squeeze() ───────────────┘
十二、常见坑清单
df['列']是 Series,不是 DataFrame ------想保留二维结构要写df[['列']]- 取列的 Series 的
index是行标签,不是列名(取行时才反过来) df.sum(axis=1)遇文本列报TypeError------先选数值列,或用numeric_only=True- Series 运算按索引对齐,不是按位置------索引不一致会产生大量 NaN 而不报错
Series + 标量广播到每个元素;DataFrame + Series需要区分 direction ,按行必须axis=0- 单列 DataFrame 和 Series 的方法返回值不同(一个返回 DataFrame,一个返回 Series)
- 链式赋值
df[...][...] = x可能静默失效 ,统一用df.loc[行, 列] = x to_dict()默认返回嵌套字典 ,不是 Series;要 Series 用to_dict('series')squeeze()只在一行/一列时降维,多列时无效- Series 的
name会影响to_frame()的列名,组装 DataFrame 时也靠它决定列名
十三、关系小结表
| Series | DataFrame | |
|---|---|---|
| 维度 | 1 维 | 2 维 |
| 轴 | index |
index + columns |
| dtype | 全部统一 | 各列可不同 |
| 本质 | 带标签的一维数组 | 共用行索引的 Series 的字典 |
| 类比 | 表格的一列(或一行) | 整张表 |
| 从对方获取 | --- | df['列'] / df.loc[行] |
| 转成对方 | .to_frame() |
.squeeze() / .to_dict('series') |
| 常见来源 | 取列、取行、聚合结果 | pd.DataFrame(...)、read_csv、read_excel |
| 典型用途 | 单列计算、向量运算 | 多列分析、展示、导出 |
一句话收尾:
Series 是"一条数据线",DataFrame 是"一扎按列排好的数据线"。
拧成一扎(
to_frame/ 拼表)、拆出一条(df['列']/squeeze)、两条线按标签对齐(索引对齐)------这三件事就是 pandas 的全部基本功。
需要的话,我可以把这份「Series 与 DataFrame 关系」整理成讲义章节(配 6 道辨析练习:df['列'] vs df[['列']]、取行取列的 index 差异、索引对齐陷阱、广播方向等),追加到 Python函数精选讲义.md(file:///e:/MyTrae/Python教学/Python函数精选讲义.md) 或单独成文。