【Python系列课程】Pandas(一):Series与DataFrame详解
18 Pandas(一):Series与DataFrame详解——数据分析的真正入口
NumPy让我们玩转了数组,但现实世界的数据不是整齐的
(3, 4)数组——它有列名、有缺失值、有字符串、有日期,还有各种乱七八糟的脏数据。这就是Pandas存在的理由:让Python做数据分析像Excel一样直观,像SQL一样强大。
本篇开始Pandas四部曲,先搞定最核心的两个数据结构:Series(一维) 和 DataFrame(二维)。
一、Pandas是什么?为什么要用它?
Pandas是建立在NumPy基础上的数据分析库,名字来源于"Panel Data"(面板数据)。它的两大核心数据结构:
| 数据结构 | 维度 | 类比 | 典型场景 |
|---|---|---|---|
| Series | 一维 | 带标签的列 | 某门课的所有学生成绩 |
| DataFrame | 二维 | Excel表格 | 全班学生的完整成绩单 |
Pandas的核心优势:
- 自带列名和行索引,不用再记"第3列是什么"
- 自动对齐数据(按索引对齐,不用手动匹配)
- 原生支持缺失值(NaN)处理
- 可以直接读CSV、Excel,一行代码搞定
import pandas as pd
import numpy as np
# 验证一下pandas是基于NumPy的
arr = np.array([1, 2, 3])
s = pd.Series(arr)
print(type(s.values)) # <class 'numpy.ndarray'> ← 底层还是NumPy数组

二、Series:带标签的一维数组
2.1 创建Series的五种方式
Series = 数据值 + 索引标签,默认索引是从0开始的整数,但你也可以自定义。
方式1:用标量创建
import pandas as pd
# 标量值会按照index的数量进行重复
s = pd.Series(data=99)
print(s)
# 0 99
# dtype: int64
# 指定index后,标量会按index数量重复
s = pd.Series(data=99, index=[1, 2, 3])
print(s)
# 1 99
# 2 99
# 3 99
# dtype: int64
方式2:用字符串创建(字符串被当作标量处理!)
s = pd.Series(data='abc', index=[1, 2, 3])
print(s)
# 1 abc
# 2 abc
# 3 abc
# dtype: object
⚠️ 注意:字符串
'abc'被当作一个整体标量,不是三个字符!如果想每个元素一个字符,要用列表['a','b','c']。
方式3:用列表(list)创建
s = pd.Series([10, 20, 30, 40])
print(s)
# 0 10
# 1 20
# 2 30
# 3 40
# dtype: int64
# 自定义索引
s = pd.Series([10, 20, 30], index=['a', 'b', 'c'], name='分数')
print(s)
# a 10
# b 20
# c 30
# Name: 分数, dtype: int64
方式4:用NumPy数组创建
import numpy as np
s = pd.Series(np.array([1, 2, 3]),
index=('one', 'two', 'three'),
name='test-series',
dtype=np.float64)
print(s)
# one 1.0
# two 2.0
# three 3.0
# Name: test-series, dtype: float64
方式5:用字典(dict)创建(最常用!)
# 字典的键自动变成索引,值变成数据
s = pd.Series({'语文': 85, '数学': 92, '英语': 78})
print(s)
# 语文 85
# 数学 92
# 英语 78
# dtype: int64
# 如果指定了index,index中不在字典键里的会变成NaN
s = pd.Series({'语文': 85, '数学': 92, '英语': 78},
index=['语文', '物理', '英语'])
print(s)
# 语文 85.0
# 物理 NaN ← 字典里没有'物理',所以是缺失值
# 英语 78.0
# dtype: float64

2.2 访问Series数据:两种方式
Series支持位置索引(像NumPy数组)和标签索引(像字典)两种访问方式。
位置索引访问(类似NumPy):
s = pd.Series([10, 20, 30, 50, 60], index=['a', 'b', 'c', 'd', 'e'])
print(s[1]) # 20 ← 位置1的元素(第二个)
print(s[1:3]) # b:20, c:30 ← 位置切片,右开区间(和Python列表一样)
print(s[:-2:2]) # a:10, c:30 ← 从开头到倒数第2个,步长2
print(s[[2, 1, 3]]) # c:30, b:20, d:50 ← 用位置列表随机访问
标签索引访问(Series的特色):
s = pd.Series([10, 20, 30, 50, 60], index=['a', 'b', 'c', 'd', 'e'])
print(s['c']) # 30 ← 标签'c'对应的元素
print(s['e']) # 60 ← 标签'e'对应的元素
# ⚠️ 重点:标签切片是「闭区间」!和位置切片不一样!
print(s['a':'d']) # a:10, b:20, c:30, d:50 ← 包含'd'!
print(s[:'c':2]) # a:10, c:30 ← 从头到'c',步长2
print(s[['c', 'e', 'a']]) # 用标签列表随机访问,顺序按列表来

一句话总结:
- 位置切片:
[1:3]= 左闭右开,不含3 - 标签切片:
['a':'d']= 左右都闭,包含’d’
2.3 修改Series的索引和数据
s = pd.Series([4, 7, -5, 3], index=['a', 'b', 'c', 'd'])
print(s)
# 修改索引:直接给index属性赋值
s.index = ['aa', 'bb', 'cc', 'dd']
print(s)
# 修改数据:通过索引赋值
s['aa'] = 100
print(s)
# 通过标签切片修改多个值
s['b':'d'] = [7, 8, 9]
print(s)
2.4 Series常用属性
s = pd.Series([1, 2, 3, 4], index=['a', 'b', 'c', 'd'], name="测试数据")
print(s.dtype) # int64 ← 数据类型
print(s.name) # 测试数据 ← Series名称
print(s.size) # 4 ← 元素个数
print(s.shape) # (4,) ← 形状(元组)
print(s.values) # [1 2 3 4] ← 以NumPy数组形式返回数据
print(s.index) # Index(['a', 'b', 'c', 'd']) ← 索引对象
2.5 Series的运算
Series保留了NumPy的所有数组运算,而且运算时索引对齐:
s1 = pd.Series([15, 20], index=['a', 'b'])
s2 = pd.Series([1, 2], index=['c', 'a'])
print(s1 + 1) # 每个元素加1,索引不变
# a 16
# b 21
print(s1 + s2) # 按索引对齐后相加,对不上的变成NaN
# a 35.0 ← s1['a']+s2['a'] = 15+2=17?等等,20+1=21?
# 实际上:s1有a:15,b:20;s2有c:1,a:2
# 对齐后:a:15+2=17, b:20+NaN=NaN, c:NaN+1=NaN
# a 17.0
# b NaN
# c NaN

三、DataFrame:二维表格型数据结构
如果说Series是一列数据,那DataFrame就是多列Series拼在一起,每列可以有不同的数据类型,既有行索引也有列索引。
3.1 创建DataFrame的六种方式
方式1:用NumPy数组创建
import pandas as pd
import numpy as np
d = np.array([[1, 2, 3],
[4, 5, 6]])
df = pd.DataFrame(data=d, dtype=np.float64)
print(df)
# 0 1 2
# 0 1.0 2.0 3.0
# 1 4.0 5.0 6.0
方式2:用单一列表创建(会变成单列DataFrame)
d = ['Tom', 'Bob', 'Linda']
df = pd.DataFrame(data=d)
print(df)
# 0
# 0 Tom
# 1 Bob
# 2 Linda
方式3:用嵌套列表创建(最直观)
d = [['Tom', 17],
['Bob', 18],
['Linda', 26]]
df = pd.DataFrame(data=d,
index=['p1', 'p2', 'p3'],
columns=['name', 'age'])
print(df)
# name age
# p1 Tom 17
# p2 Bob 18
# p3 Linda 26
方式4:用字典嵌套列表创建(最常用!)
# 字典的键变成列名,值(列表)变成每列的数据
d = {
'name': ['Tom', 'Bob', 'Linda'],
'age': [17, 18, 26]
}
df = pd.DataFrame(data=d, index=['p1', 'p2', 'p3'])
print(df)
# name age
# p1 Tom 17
# p2 Bob 18
# p3 Linda 26
⚠️ 注意:字典中每个键对应的值(列表)长度必须相同,否则会报错!
方式5:用Series创建DataFrame
# 方式5a:用字典包裹Series
d = {
'name': pd.Series(['Tom', 'Bob', 'Linda'], index=['p1', 'p2', 'p3']),
'age': pd.Series([17, 18, 26], index=['p1', 'p2', 'p8']) # 注意p8!
}
df = pd.DataFrame(data=d)
print(df)
# name age
# p1 Tom 17.0
# p2 Bob 18.0
# p3 Linda NaN ← p3在age的Series里没有,所以是NaN
# p8 NaN 26.0 ← p8在name的Series里没有,所以是NaN
方式6:用列表包裹Series创建
d = [
pd.Series(['Tom', 'Bob', 'Linda'], index=['p1', 'p2', 'p3'], name="name"),
pd.Series([17, 18, 26], index=['p1', 'p2', 'p3'], name="age")
]
df = pd.DataFrame(data=d)
print(df)
# name age
# p1 Tom 17
# p2 Bob 18
# p3 Linda 26

3.2 访问DataFrame数据:两大套路
DataFrame访问数据有两种核心方式,一定要分清楚:
| 访问方式 | 语法 | 本质 | 适用场景 |
|---|---|---|---|
| 索引取列 | df['列名'] |
取一列或多列,返回Series/DataFrame | 知道列名,取整列 |
| 切片取行 | df[行切片] |
切片取行,返回DataFrame | 按位置取连续行 |
| loc | df.loc[行标签, 列标签] |
用标签索引访问 | 知道行/列的标签名 |
| iloc | df.iloc[行位置, 列位置] |
用整数位置访问 | 知道行/列的位置编号 |
套路一:索引取列 + 切片取行
df = pd.DataFrame({
'name': ['Tom', 'Bob', 'Linda'],
'age': [17, 18, 26],
'height': [172, 176, 188]
}, index=['p1', 'p2', 'p3'])
# 取单列 → 返回Series
print(df['age'])
# p1 17
# p2 18
# p3 26
# Name: age, dtype: int64
# 取多列 → 返回DataFrame
print(df[['age', 'name']])
# age name
# p1 17 Tom
# p2 18 Bob
# p3 26 Linda
# 切片取行(位置切片,右开区间)
print(df[0:1]) # 只取第0行
# name age height
# p1 Tom 17 172
# 切片取行(标签切片,左右都闭!)
print(df['p1':'p2'])
# name age height
# p1 Tom 17 172
# p2 Bob 18 176
套路二:loc(标签索引)和 iloc(位置索引)
loc——用标签访问(记住:loc只能接收标签!)
df = pd.DataFrame({
'name': ['Tom', 'Bob', 'Linda'],
'age': [17, 18, 26],
'height': [172, 176, 188]
}, index=['p1', 'p2', 'p3'])
# 取单行(返回Series)
print(df.loc['p1'])
# name Tom
# age 17
# height 172
# Name: p1, dtype: object
# 取单个值(行标签 + 列标签)
print(df.loc['p2', 'age']) # 18
# 取某行的某些列
print(df.loc['p2', ['age', 'name']])
# age 18
# name Bob
# Name: p2, dtype: object
# 取多行的某些列
print(df.loc[['p3', 'p2'], ['age', 'name']])
# age name
# p3 26 Linda
# p2 18 Bob
iloc——用整数位置访问(记住:iloc只能接收整数!)
# 取单行(第0行)
print(df.iloc[0])
# 取单个值(第1行,第1列)
print(df.iloc[1, 1]) # 18
# 取某行的某些列(用位置列表)
print(df.iloc[1, [1, 0]])
# age 18
# name Bob
# Name: p2, dtype: object
# 取多行的某些列
print(df.iloc[[2, 1], [1, 0]])
# age name
# p3 26 Linda
# p2 18 Bob

记忆口诀:
loc= label oriented(标签导向)→ 用标签iloc= integer oriented(整数导向)→ 用位置编号
3.3 修改DataFrame的索引和列名
df = pd.DataFrame({
'name': ['Tom', 'Bob', 'Linda'],
'age': [17, 18, 26],
'height': [172, 176, 188]
}, index=['p1', 'p2', 'p3'])
# 修改行索引
df.index = ['n1', 'n2', 'n3']
# 修改列名
df.columns = ['names', 'ages', 'heights']
print(df)
# names ages heights
# n1 Tom 17 172
# n2 Bob 18 176
# n3 Linda 26 188
3.4 修改DataFrame的数据
这是Pandas里最灵活也最容易搞混的部分。核心原则:用访问数据的方式拿到目标,然后赋值。
df = pd.DataFrame({
'name': ['Tom', 'Bob', 'Linda'],
'age': [17, 18, 26],
'height': [172, 176, 188]
}, index=['p1', 'p2', 'p3'])
# ===== 修改单列数据(5种等价写法)=====
df['height'] = pd.Series([1.72, 1.88, 1.76], index=df.index)
df['height'] = [1.72, 1.88, 1.76] # 直接列表赋值(推荐)
df.loc[:, 'height'] = [1.72, 1.88, 1.76] # 用loc
df.iloc[:, 2] = [1.72, 1.88, 1.76] # 用iloc
print(df)
# ===== 修改多列数据 =====
df[['name', 'age']] = [['Bob', 19], ['Tom', 22], ['Jack', 27]]
df.loc[:, ['name', 'age']] = [['Bob', 19], ['Tom', 22], ['Jack', 27]]
df.iloc[:, :2] = [['Bob', 19], ['Tom', 22], ['Jack', 27]]
print(df)
# ===== 追加新列(如果列名不存在,自动追加!)=====
df['weight'] = [65, 75, 60] # 直接赋值
df.loc[:, 'weight'] = [65, 75, 60] # 用loc
print(df)
# name age height weight
# p1 Bob 19 1.72 65
# p2 Tom 22 1.88 75
# p3 Jack 27 1.76 60
# ===== 修改单行数据 =====
df.iloc[1] = ['Tony', 23, 178]
df.loc['p2'] = ['Tony', 23, 178]
print(df)
# ===== 追加新行(用loc,指定新的行索引)=====
df.loc['p4'] = ['Toby', 23, 178]
print(df)
# name age height
# p1 Bob 19 1.72
# p2 Tony 23 1.88
# p3 Jack 27 1.76
# p4 Toby 23 178.00 ← 新行!

3.5 DataFrame常用属性
df = pd.DataFrame({
'name': ['Tom', 'Bob', 'Linda'],
'age': [17, 18, 26]
}, index=['p1', 'p2', 'p3'])
print(df.T) # 转置(行变列,列变行)
print(df.dtypes) # 每列的数据类型
# name object
# age int64
# dtype: object
print(df.shape) # (3, 2) → 3行2列
print(df.size) # 6 → 总共6个元素
print(df.index) # 行索引: Index(['p1', 'p2', 'p3'])
print(df.columns) # 列索引: Index(['name', 'age'])
print(df.axes) # 同时返回行索引和列索引(列表形式)
print(df.values) # 以NumPy数组形式返回数据(不含索引)
# [['Tom' 17]
# ['Bob' 18]
# ['Linda' 26]]
四、Series与DataFrame的运算
Pandas的运算同样遵循索引对齐规则:
import pandas as pd
import numpy as np
# DataFrame之间的运算
df1 = pd.DataFrame(np.arange(9).reshape((3, 3)),
columns=list('abc'),
index=['n1', 'n2', 'n3'])
# a b c
# n1 0 1 2
# n2 3 4 5
# n3 6 7 8
df2 = pd.DataFrame(np.arange(16).reshape((4, 4)),
columns=list('dacf'),
index=['n1', 'n2', 'n3', 'n4'])
# d a c f
# n1 0 1 2 3
# n2 4 5 6 7
# n3 8 9 10 11
# n4 12 13 14 15
print(df1 + df2)
# a b c d f
# n1 1.0 NaN 4.0 NaN NaN
# n2 8.0 NaN 11.0 NaN NaN
# n3 15.0 NaN 18.0 NaN NaN
# n4 NaN NaN NaN NaN NaN
# 解读:只有行索引和列索引都对齐的位置才有值,其余都是NaN
运算规则速查:
| 运算符 | 说明 | 示例 |
|---|---|---|
+ |
加法(索引对齐) | df1 + df2 |
- |
减法 | df1 - 2 |
* |
乘法 | df1 * 3 |
/ |
除法 | df1 / 2 |
> |
比较运算 | df1 > 3 返回布尔DataFrame |
五、本文总结
| 知识点 | 核心要点 |
|---|---|
| Series创建 | 5种方式,字典方式最常用;index控制行标签 |
| Series访问 | 位置切片右开,标签切片左右都闭 |
| DataFrame创建 | 6种方式,字典嵌套列表最常用 |
| loc | 用标签访问,df.loc['行','列'] |
| iloc | 用整数位置访问,df.iloc[行,列] |
| 索引对齐 | 运算时按索引对齐,对不上的变NaN |
| 修改数据 | 用访问方式拿到目标,直接赋值即可 |
下一篇我们将深入Pandas的数据清洗功能——如何处理缺失值、去重、类型转换,这些是真实数据分析工作中80%的时间在做的事。
动手练习
-
用字典创建一个Series,键为
['语文','数学','英语','物理'],值为你自己的四门课成绩,然后:(a)用位置切片取前3门课的成绩;(b)用标签切片取’数学’到’物理’的成绩;(c)找出最高分的科目。 -
创建一个DataFrame,包含5个学生的
姓名、年龄、成绩三列,然后:(a)用iloc取出第2、3个学生的数据;(b)用loc将所有成绩大于80分的学生的年龄改为20;(c)新增一列是否及格,成绩>=60为是,否则为否。 -
创建两个DataFrame:
df1 = pd.DataFrame({'A':[1,2],'B':[3,4]})和df2 = pd.DataFrame({'B':[10,20],'C':[30,40]}),计算df1 + df2,解释结果中NaN的来源。 -
有一个Series
s = pd.Series([10,20,30,40,50], index=['a','b','c','d','e']),不使用循环,把所有大于25的值改为100,小于等于25的值改为0。
下一篇预告:《19 Pandas(二):数据清洗与缺失值处理——真实数据的"洗澡"指南》
更多推荐


所有评论(0)