Pandas教学-17:Series与Dataframe的关系

下面所有输出均实跑验证(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]),
}

三个共享的东西:

  1. 共享行索引 :三个 Series 的 index 都是 [0, 1, 2]
  2. 共享列顺序 :df.columns 决定列的顺序
  3. 各自独立的 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() ───────────────┘

十二、常见坑清单

  1. df['列'] 是 Series,不是 DataFrame ------想保留二维结构要写 df[['列']]
  2. 取列的 Series 的 index 是行标签,不是列名(取行时才反过来)
  3. df.sum(axis=1) 遇文本列报 TypeError ------先选数值列,或用 numeric_only=True
  4. Series 运算按索引对齐,不是按位置------索引不一致会产生大量 NaN 而不报错
  5. Series + 标量 广播到每个元素;DataFrame + Series 需要区分 direction ,按行必须 axis=0
  6. 单列 DataFrame 和 Series 的方法返回值不同(一个返回 DataFrame,一个返回 Series)
  7. 链式赋值 df[...][...] = x 可能静默失效 ,统一用 df.loc[行, 列] = x
  8. to_dict() 默认返回嵌套字典 ,不是 Series;要 Series 用 to_dict('series')
  9. squeeze() 只在一行/一列时降维,多列时无效
  10. 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) 或单独成文。

相关推荐
发量惊人的中年网工1 小时前
分公司访问总部ERP慢怎么办?SD-WAN组网的排查与验收要点
服务器·开发语言·php
下页、再停留1 小时前
【C#桌面客户端系列学习-15】生成可视化图表之折线图、柱状图、饼状图
开发语言·c#
ShineWinsu2 小时前
对于JavaScript:操作元素的解析
开发语言·前端·javascript·面试·web·dom·元素
马良神笔2 小时前
2026 年Rag最近技术前沿总结
后端·python·django
不像程序员的程序媛2 小时前
maven基础
python·pycharm·maven
益莱储中国3 小时前
深度解析:RF 实验室从 5G迈向 6G 的过程中保持技术领先与设备升级的路径
开发语言·php
xiaogai_gai4 小时前
仅查询策略实战:从金蝶云·星空旗舰版定时拉取收料通知单
大数据·数据库·python
benchmark_cc4 小时前
Python 量化系统如何设计统一的数据访问接口:从策略解耦到数据层架构
开发语言·python·数据分析·pandas·量化交易·股票数据·quantdash
const_qiu4 小时前
第四章 00:开篇 —— 从「会聊天」到「记得住、会干活、能协作」
java·开发语言·人工智能·python