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%的时间在做的事。


动手练习

  1. 用字典创建一个Series,键为['语文','数学','英语','物理'],值为你自己的四门课成绩,然后:(a)用位置切片取前3门课的成绩;(b)用标签切片取’数学’到’物理’的成绩;(c)找出最高分的科目。

  2. 创建一个DataFrame,包含5个学生的姓名年龄成绩三列,然后:(a)用iloc取出第2、3个学生的数据;(b)用loc将所有成绩大于80分的学生的年龄改为20;(c)新增一列是否及格,成绩>=60为,否则为

  3. 创建两个DataFrame:df1 = pd.DataFrame({'A':[1,2],'B':[3,4]})df2 = pd.DataFrame({'B':[10,20],'C':[30,40]}),计算 df1 + df2,解释结果中NaN的来源。

  4. 有一个Series s = pd.Series([10,20,30,40,50], index=['a','b','c','d','e']),不使用循环,把所有大于25的值改为100,小于等于25的值改为0。


下一篇预告:《19 Pandas(二):数据清洗与缺失值处理——真实数据的"洗澡"指南》

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐