Pandas 学习笔记:核心对象与常用操作
Pandas 是 Python 强大的数据分析库,就像是 Python 世界里的 Excel 表格帮手。对于有一定 Python 基础的工程师,掌握 Pandas 的两个核心数据结构——Series 和 DataFrame,以及常见的增删改查操作,是迈入数据分析世界的重要一步。本文以实例代码为主线,用亲切自然的语言一步步介绍 Pandas 的基础用法,并辅以实战中常见问题的提示,希望帮助你快速上手。
认识 Series
Series 是 Pandas 提供的一种一维数组数据结构,它带有标签索引(index),有点类似于带索引的数据列表。一方面,Series 可以看作带索引的 NumPy 一维数组;另一方面,它也有点像 Python 的字典(dict),因为可以通过键值对形式查找数据。不过 Series 更强大:它支持向量化运算,且保留了数据顺序。
简单来说,可以把 Series 想象成 Excel 表格中的一列数据,它有两部分组成:一是索引(这一列每个数据的“行标签”),二是值。下面通过几个例子来创建 Series 并查看其内容。
import pandas as pd
import numpy as np
# 创建一个最简单的 Series:不指定索引时,会自动使用0,1,2,...作为索引
s1 = pd.Series([10, 20, 30, 40])
print(s1)
# 输出:
# 0 10
# 1 20
# 2 30
# 3 40
# dtype: int64
# 创建 Series 时指定索引
s2 = pd.Series([100, 98, 67, 23], index=['zs', 'ls', 'ww', 'sl'])
print(s2)
# 输出:
# zs 100
# ls 98
# ww 67
# sl 23
# dtype: int64
# 这是一个 Series:左边是索引(index),右边是对应的值,dtype 表示值的数据类型
# 通过字典创建 Series,键会成为索引,值成为数据
d = {'zs': 100, 'ls': 98, 'ww': 67, 'sl': 23}
s3 = pd.Series(d)
print(s3)
# 输出与上面 s2 相同(字典键的插入顺序即索引顺序)
上面代码展示了三种创建 Series 的方式:直接由列表创建(可以指定索引,也可以不指定用默认索引0,1,2,…),以及由字典创建。一般来说,通过列表创建可以灵活指定索引顺序,而通过字典创建会使用字典的键作为索引(顺序为插入顺序)。两者效果类似,都得到一个带索引的一维数据序列。
还有一种快捷方式,如果想要创建内容全部相同的 Series,可以直接传入一个常数值和索引。例如下面代码将生成10个元素都为0.2的Series:
# 创建一个包含10个0.2的 Series
s4 = pd.Series(0.2, index=range(10))
print(s4.head(3)) # 先看头3行
# 输出:
# 0 0.2
# 1 0.2
# 2 0.2
# dtype: float64
可以看到,s4的每个元素都是0.2,指数0到9。head(3)方法返回前3行,便于我们预览数据。
Series 的数据访问
Series 有点类似于 Python 的列表和字典的结合,在访问数据时既可以用位置下标也可以用标签索引。需要注意的是:Series同时支持基于位置和基于标签的索引方式,这两套索引彼此独立。
- 位置索引(下标):按照元素的位置顺序,从0开始计数。例如 s[0] 取第一个元素,s[1:3] 取切片。
- 标签索引(index):按照我们定义的索引值来取。例如索引是字符串 ‘zs’,用 s[‘zs’] 来取对应的值。
下面用具体例子说明 Series 的两种索引方法:
s = pd.Series([100, 78, 98, 79], index=['zs', 'ls', 'ww', 'sl'])
# 这个 Series 的索引为 'zs','ls','ww','sl',对应的值如列表所给
# 按位置索引访问
print(s[3]) # 第4个元素,位置从0开始计数
print(s[:2]) # 前2个元素的切片(不包含位置2)
print(s[[0, 2]]) # 位置0和2的元素,返回一个新的 Series
# 按标签索引访问
print(s['sl']) # 索引标签为 'sl' 的元素
print(s['zs':'ww']) # 按标签切片,包含 'zs' 到 'ww'(注意包含末端)
print(s[['zs', 'ww', 'sl']])# 指定标签列表,返回多个元素
上述代码中,s[3] 通过位置获取到了第4个元素 79,而 s[‘sl’] 则通过标签获取到了对应的值 79。区别:切片时,s[:2]按位置取不包括索引2,而 s[‘zs’:‘ww’]按标签取则是包含 ‘ww’ 这个索引的(也就是包含切片区间的末端)。这点与 Python 基本切片规则不同,需要特别注意。
另外,Series 不支持像 Python 列表那样的负索引。也就是说,s[-1] 并不能取最后一个元素(除非你的索引恰好有 -1 这个标签)。如果需要取最后一个元素,可以使用 s.iloc[-1](iloc 是后面会介绍的按位置选取的通用方式)。总之,Series 的下标索引只有正向的,没有反向的。
我们还可以查看 Series 的一些属性来了解数据结构:
print(s.index) # Index(['zs', 'ls', 'ww', 'sl'], dtype='object')
print(s.values) # [100 78 98 79],值本身是一个 numpy.ndarray
print(s.dtype) # int64,值的数据类型
print(s.shape) # (4,),Series 的形状,相当于长度为4的一维数组
print(s.size) # 4,元素个数
print(s.ndim) # 1,维度,对于Series永远是1
index 属性返回一个 Index 对象,包含 Series 的索引列表;values 则返回一个包含所有值的 NumPy 数组。在这个例子中,值全是整数,所以 dtype 是 int64。如果数据包含浮点或出现 NaN(缺失值),dtype 可能变为 float64。shape、size、ndim 等属性和 NumPy 的数组类似,分别表示数据的形状、元素数量和维度。
DataFrame 是什么
DataFrame 是 Pandas 中更为常用的二维表结构,可以理解为“带行标签和列标签的表格数据”。如果说 Series 有点像 Excel 中的一列,那么 DataFrame 就像整个表格(由多列组成),同时拥有行索引(index)和列名(columns)。DataFrame 也可以看作由多个共享同一个索引的 Series 组成的字典,每一列是一个 Series,列名是字典的键。
举个例子,一个 DataFrame 可以用来表示多个人的若干属性,例如姓名、年龄、性别,每一列是一种属性,每一行代表一个人。我们可以使用多种方式来构造 DataFrame,常见的方法包括:由字典、由列表组成的列表、由多个 Series 等。下面通过代码来演示 DataFrame 的创建和基本属性。
import pandas as pd
# 通过字典创建 DataFrame,每个键代表一列
data = {
'Name': ['Tom', 'Jerry', 'Jack', 'Rose'],
'Age': [18, 18, 20, 20]
}
df = pd.DataFrame(data)
print(df)
# 输出:
# Name Age
# 0 Tom 18
# 1 Jerry 18
# 2 Jack 20
# 3 Rose 20
上面用字典创建了一个简单的 DataFrame df,包含两列:Name 和 Age。因为我们没有指定索引,Pandas 自动用了默认的整数索引 0,1,2,3。可以看到 DataFrame 打印输出时,会显示行索引和列名,以及每个单元格的数据。
我们也可以指定自己的索引。例如,如果我们想用 a, b, c, d 作为每行的索引标签,可以传入参数 index= 来指定;另外用 columns= 可以指定列名的顺序。下面我们构造一个稍微复杂一点的 DataFrame:多加一列 Gender,并且让不同列来自不同长度的 Series,以演示 Pandas 会如何对齐数据。
# 用字典包含多个 Series 来创建 DataFrame,各 Series 按索引自动对齐
data2 = {
'Name': pd.Series(['Tom', 'Jerry', 'Jack', 'Rose'], index=['a', 'b', 'c', 'd']),
'Age': pd.Series([18, 18, 20], index=['a', 'b', 'c']), # 少了索引 'd'
'Gender': pd.Series(['M', 'M', 'F'], index=['a', 'c', 'd']) # 少了索引 'b'
}
df2 = pd.DataFrame(data2)
print(df2)
# 输出:
# Name Age Gender
# a Tom 18.0 M
# b Jerry 18.0 NaN
# c Jack 20.0 F
# d Rose NaN M
在这个 df2 DataFrame 中,我们指定了索引为 ‘a’, ‘b’, ‘c’, ‘d’。Name 列有这四个索引的值,但 Age 列缺少 ‘d’的值,Gender 列缺少 ‘b’的值。Pandas 会根据索引自动对齐不同列的数据:对于缺失的位置以 NaN 填充。上面输出中,索引 b 的 Gender 显示为 NaN(因为没有提供 b 对应的 Gender),索引 d 的 Age 为 NaN(没有提供 d 对应的 Age)。NaN 表示缺失值,在 Pandas 中是一种特殊的浮点型值。
注意: 由上述例子也可以看出,DataFrame 中每列数据可以是不同类型(Name 是字符串,Age 是数字,Gender 是字符串)。当某列出现 NaN 时,该列会被Upcast为浮点型,因为 NaN 被视作浮点类型特殊值。例如 Age 列本来是整数,一旦出现 NaN,就变成了 float。
如果我们不想让某些值缺失,可以在创建 DataFrame 时提供完整的数据,对齐索引。或者创建后再手动填充缺失值。但这些属于后续处理范畴。
DataFrame 的属性和基本操作
类似于 Series,DataFrame 提供了许多属性和方法方便我们了解数据集的总体情况:
- df.index:获取行索引(Index对象)。
- df.columns:获取列名(Index对象)。
- df.values:以二维 numpy 数组的形式获取表格中的值。
- df.shape:返回 (行数, 列数) 的元组。
- df.dtypes:获取每一列的数据类型。
- df.size:返回元素总数 (行数 × 列数)。
- df.empty:布尔值,表示 DataFrame 是否为空(没有任何数据)。
- df.head(n):查看前 n 行数据(默认 n=5)。
- df.tail(n):查看最后 n 行数据。
- df.describe():对数值列进行统计汇总(计数、均值、标准差、最小值、四分位数等)。
- df.T:获取转置的 DataFrame(行列互换)。
举例来说,我们可以查看 df2 的一些基本信息:
print(df2.index) # Index(['a', 'b', 'c', 'd'], dtype='object')
print(df2.columns) # Index(['Name', 'Age', 'Gender'], dtype='object')
print(df2.shape) # (4, 3) -> 4 行 3 列
print(df2.dtypes)
# Name object
# Age float64
# Gender object
# dtype: object
print(df2.head(2))
# Name Age Gender
# a Tom 18.0 M
# b Jerry 18.0 NaN
print(df2.describe())
# Age
# count 3.000000
# mean 18.666667
# std 1.154701
# min 18.000000
# 25% 18.000000
# 50% 18.000000
# 75% 19.000000
# max 20.000000
这里我们看到:index 和 columns 列出了 DataFrame 的索引和列标签,shape说明有4行3列。dtypes表明 Name 和 Gender 是object类型(字符串),Age是float64。head(2)输出了前两行数据,describe()则对 Age 列给出了统计摘要(因为 Name 和 Gender 非数字列被自动忽略)。
了解了 Series 和 DataFrame 的基本结构,下面我们进入更具体的增删改查操作,以 DataFrame 为中心进行讲解。在以下示例中,我们将继续使用刚才创建的 df2 来演示。
列操作 CRUD
通常我们需要对 DataFrame 的列进行各种操作,例如读取某几列数据、添加新列、修改已有列或者删除列。DataFrame 在列操作上非常类似于 Python 的字典:把 DataFrame 当做一个字典的话,键就是列名,值是一列数据(一个 Series)。因此许多列操作可以类比为字典操作来记忆。
下面我们按照查(读取)、增(添加)、改(修改)、删(删除)的顺序,演示列的常见操作。
查(读取列): DataFrame 可以通过列名来获取列数据。最简单的方式是使用方括号 df[‘列名’],这会返回一个 Series,包含该列的所有数据和对应的行索引。另外,也可以传入一个列名列表 df[[‘列名1’,‘列名2’]] 来一次获取多列数据(此时返回一个新的 DataFrame)。需要注意的是,不能直接使用切片 df[‘col1’:‘col3’] 来选取多列,方括号中如果给的是切片会被解释为行切片(这是为了保持语义简单一致,方括号直接索引默认为列,切片符号默认为行切片,这点细节稍微有点反直觉)。
我们来看具体例子:
df = df2 # 继续使用前面的 df2: 有 Name, Age, Gender 三列,索引 a,b,c,d
# 访问单列数据
name_col = df['Name']
print(name_col) # 获取 Name 列(类型为 Series)
# 访问多列数据
subset = df[['Name', 'Age']]
print(subset) # 获取 Name 和 Age 两列,返回 DataFrame
# 利用 df.columns 得到列名列表,比如去掉最后一列:
print(df[df.columns[:-1]])# 获取除最后一列外的所有列
输出(省略部分)将展示 name_col 是一个 Series,内容就是每个索引对应的 Name;而 subset 是一个 DataFrame,包含两列 Name 和 Age。最后一行通过 df.columns[:-1] 切片拿到了除最后一列(Gender)以外的列名列表,实现了获取多列的另一种方法。
值得一提的是,Pandas 允许通过属性访问的方式获取列,例如 df.Name 可以直接得到 Name 列,这跟 df[‘Name’] 是等价的(前提是列名是合法的 Python 标识符且不与现有属性方法同名)。但是如果列名包含空格或特殊字符,或者碰巧和 DataFrame 的方法名冲突,就不能用点属性访问。建议初学者还是使用 df[…] 这种明确的方式。
注意: 如果列名中包含空格,比如 “Total Sales” 这种,不能用 df.Total Sales(会语法错误或解析错误),此时必须用 df[‘Total Sales’] 来访问。同样地,列名包含点号、减号等特殊字符,或列名叫max、count等与DataFrame方法重名,也只能使用 df[‘列名’] 的方式。如果想方便地用点访问,最好的办法是统一修改列名,例如用 df.columns = df.columns.str.replace(’ ’, ’_’) 将空格替换为下划线,或者使用 df.rename 重命名列。
增(添加列): 向 DataFrame 增加新列也很直观:直接像字典赋值一样 df[‘新列名’] = … 即可。赋值的“…”可以是以下几种:
- 一个固定值:则整列都会被这个值填充。
- 一个列表或 ndarray:长度需要跟 DataFrame 的行数相等,数据会按顺序填充这一列。
- 一个 Pandas 的 Series:如果 Series 的索引能对齐 DataFrame 的索引,那么会根据索引匹配填充,索引没有对应上的位置将出现 NaN。
也可以一次性添加多列。Pandas 提供了 pd.concat 方法,可以用来把两个DataFrame按列方向拼接(axis=1)从而增加新列。此外,df.assign() 方法也可以同时添加多列,不过这里不展开。下面通过代码演示各种添加列的方式:
# 原始 DataFrame 回顾:
print(df)
# Name Age Gender
# a Tom 18.0 M
# b Jerry 18.0 NaN
# c Jack 20.0 F
# d Rose NaN M
# 1. 直接赋值列表,添加一列
df['Math'] = [100, 100, 100, 100] # 所有行的 Math 值都是100
print(df)
# 新增了 Math 列:
# Name Age Gender Math
# a Tom 18.0 M 100
# b Jerry 18.0 NaN 100
# c Jack 20.0 F 100
# d Rose NaN M 100
# 2. 赋值一个 Series,按索引对齐添加新列
df['English'] = pd.Series([95, 96, 97], index=['a', 'b', 'd'])
print(df)
# 新增 English 列(对齐索引,有缺失):
# Name Age Gender Math English
# a Tom 18.0 M 100 95.0
# b Jerry 18.0 NaN 100 96.0
# c Jack 20.0 F 100 NaN # c 没有 English 值,NaN
# d Rose NaN M 100 97.0
# 3. 用 concat 横向拼接添加多列
new_cols = pd.DataFrame({
'Chinese': [50, 50, 50, 50],
'P.E.': [60, 60, 60, 60]
}, index=['a', 'b', 'c', 'd'])
df = pd.concat([df, new_cols], axis=1)
print(df)
# 新增了 Chinese 和 P.E. 两列:
# Name Age Gender Math English Chinese P.E.
# a Tom 18.0 M 100 95.0 50 60
# b Jerry 18.0 NaN 100 96.0 50 60
# c Jack 20.0 F 100 NaN 50 60
# d Rose NaN M 100 97.0 50 60
上面的操作依次给 DataFrame 添加了 Math 列(所有值相同),English 列(只有索引 a,b,d 有值,c 自动补NaN),以及通过 pd.concat 一次拼接两个新列 Chinese 和 P.E.。需要留意的是,我们使用了 ‘P.E.’ 作为列名,这里包含了一个点号,今后访问 P.E. 列就不能用 df.P.E.(会被解释成连续的属性访问而出错),只能用 df[‘P.E.’]。这再次说明了列名命名的规范性重要性。
改(修改列): 修改列的值实际上和添加列是一样的操作——用新的值赋值给已有的列名。如果该列存在,就会被新值覆盖;如果不存在,就会新建列。因此在语法上没有专门的“改列”方法,直接赋值即可。
例如,我们可以把刚刚添加的 Chinese 列的分数全部改成 100:
df['Chinese'] = [100, 100, 100, 100]
print(df)
# Chinese 列的值已更新为100:
# Name Age Gender Math English Chinese P.E.
# a Tom 18.0 M 100 95.0 100 60
# b Jerry 18.0 NaN 100 96.0 100 60
# c Jack 20.0 F 100 NaN 100 60
# d Rose NaN M 100 97.0 100 60
这里将 df[‘Chinese’] 直接赋值为新的列表,[100,100,100,100],达到了修改整列的目的。同样地,你也可以只修改某几个值,比如使用按行索引的 .loc 来定位再赋值,这属于“行操作”范畴,我们稍后介绍。
删(删除列): 删除 DataFrame 的列有多种方式:
- 使用 del df[‘列名’],就像删除字典键一样。
- 使用 df.pop(‘列名’),会返回被删除的列的 Series。
- 使用 df.drop(columns=[…]) 或 df.drop([…], axis=1),可以一次删除一列或多列。drop 默认返回一个新的 DataFrame,除非指定 inplace=True 原地删除。
下面依次演示用上述方法删除我们不需要的列:
# 1. del 关键字删除列
del df['P.E.']
print(df.columns) # Index(['Name', 'Age', 'Gender', 'Math', 'English', 'Chinese'], dtype='object')
# 2. pop 方法删除列
df.pop('Chinese')
print(df.columns) # Index(['Name', 'Age', 'Gender', 'Math', 'English'], dtype='object')
# 3. drop 删除多列
df.drop(['Math', 'English'], axis=1, inplace=True)
print(df.columns) # Index(['Name', 'Age', 'Gender'], dtype='object')
print(df)
# Name Age Gender
# a Tom 18.0 M
# b Jerry 18.0 NaN
# c Jack 20.0 F
# d Rose NaN M
可以看到,先用 del 删掉了 P.E. 列,然后用 pop 删掉 Chinese 列,最后用 drop 一次性删掉 Math 和 English 列。删除后我们的 DataFrame 又只剩下最初的三列(Name, Age, Gender)以及原来的行索引。使用 drop 时记得加 axis=1 表示按列删除,并且可以根据需要选择是否 inplace。如果不使用 inplace,那么应当用 df = df.drop([…], axis=1) 赋值回去,因为 drop 默认返回删除指定列后的新 DataFrame。
行操作 CRUD
有时候我们需要对表格的行进行操作,例如按行查看数据、添加或删除某些行等。行的操作在 Pandas 中主要通过索引(index)来定位,分为按标签索引(index label)和按整数位置索引两大方式。
对行的“查、增、改、删”,我们分别介绍。继续使用前面的 DataFrame(目前有 Name, Age, Gender 列,索引 a, b, c, d),来演示行操作。
查(读取行): 和列不同,直接使用 df[…] 的方括号不能直接按行标签取行。比如 df[‘a’] 并不会返回索引为 a 的那一行,而是试图取名为 ‘a’ 的列(通常不存在,会报错)。因此,Pandas 提供了专门的索引器属性:
- df.loc[行标签] 用行标签选取数据。
- df.iloc[行号] 用行位置(整数下标)选取数据。
这两个索引器非常重要:loc 基于标签,iloc 基于整数位置。使用它们我们可以非常灵活地按行或按行列组合选取数据。
loc 用法示例:
# 使用 loc 按标签索引行
print(df.loc['a']) # 索引为 'a' 的整行数据,返回一个 Series
print(df.loc['b':'c']) # 从索引 'b' 到 'c' 的行(包括 'c' 行)
print(df.loc[['a', 'c'], ['Name', 'Gender']])
# 上面一行选取索引 a 和 c 的两行,且只取其中的 Name 和 Gender 两列
iloc 用法示例:
# 使用 iloc 按位置索引行
print(df.iloc[0]) # 第 0 号位置的行(即索引 'a' 这一行)
print(df.iloc[1:3]) # 位置 1 到 2 的行(不包括位置3),对应索引 b, c 两行
print(df.iloc[[0, 2], [0, 2]])
# 上面一行选取位置 0 和 2 的两行,以及位置 0 和 2 的两列,实现行列交叉选取
loc 和 iloc 都可以接受单个索引、切片、列表等形式来选取多行/多列,非常灵活。需要注意几点:
- 用 loc 做标签切片时,区间两端都是闭区间,也就是包含结束标签。例如上例中 df.loc[‘b’:‘c’] 包含 ‘c’ 行。
- 用 iloc 做位置切片则和 Python 列表切片规则一致,左闭右开,不包含结束的索引位置。
- iloc 允许索引列表中有重复值或顺序打乱,这种情况下会相应返回重复或乱序的行。这在需要根据位置重复数据时可能有用。
- 如果 DataFrame 的索引是数字且你用 df[some_number] 直接索引,Pandas会优先尝试按标签解析(因为索引可能正好是那个数字)。因此当索引是数字时,要特别注意区分 loc(按标签)和默认的[]行为。最安全的做法是始终使用 loc 或 iloc 来取行,避免歧义。
增(添加行): DataFrame 添加行相对没有添加列那么直接。以往 Pandas 有 df.append() 方法可以直接添加一行新的数据,但自 Pandas 1.4 起该方法已被废弃,官方建议使用 pd.concat 来完成这一功能。
添加新行的思路是:先构造一个与原 DataFrame 列结构相同的 DataFrame 或 Series,然后通过 pd.concat([…], axis=0) 将其与原 DataFrame 按行拼接。注意保持新行的数据在列上的对齐。下面演示添加一行数据:
# 构造一行新数据,注意列名需与原 DataFrame 一致
new_row = pd.DataFrame([['ZhangSan', 21, 'M']],
columns=['Name', 'Age', 'Gender'],
index=['e'])
# 用 concat 连接新行
df = pd.concat([df, new_row], axis=0)
print(df)
# 输出:
# Name Age Gender
# a Tom 18.0 M
# b Jerry 18.0 NaN
# c Jack 20.0 F
# d Rose NaN M
# e ZhangSan 21.0 M
上面我们添加了一行索引为 ‘e’ 的数据,Name 为 ZhangSan,Age 为21,Gender 为 M。拼接后可以看到 df 多了一行 ‘e’。如果不指定 index=[‘e’],new_row 会默认用0作为索引,与原 DataFrame 的索引类型不一致甚至冲突。在这种情况下 concat 仍会成功(它不会因为索引重复而报错),但新行索引会是0,有可能与原有索引冲突导致两个索引相同。这通常不是我们想要的,所以指定一个不会重复的新索引比较安全。
改(修改行): 修改行通常可以通过 loc 或 iloc 来定位整行然后赋值。例如,我们把刚才添加的 ‘e’ 行数据的 Name 和 Age 修改一下:
df.loc['e'] = ['Peter', 22.0, 'M'] # 将索引 'e' 的整行重置为新的值
print(df.loc['e'])
# 输出:
# Name Peter
# Age 22.0
# Gender M
# Name: e, dtype: object
我们成功地把索引 e 那行的数据更新为 Name = Peter, Age = 22.0, Gender = ‘M’。这里直接对 df.loc[‘e’] 赋予一个列表,新值会按照列的顺序填入对应的三个列。如果只想修改某个单元格,也可以使用 df.at[row_index, col_name] = value 或 df.iat[row_position, col_position] = value 来定位赋值。
删(删除行): 删除行使用 drop 方法,指定 axis=0(或者不指定,默认就是0也表示按行删)。可以传入要删除的索引标签列表。与删除列类似,drop 默认返回新对象,除非设置 inplace=True。示例:
# 删除索引为 'b' 和 'd' 的两行
df = df.drop(['b', 'd'], axis=0)
print(df)
# 输出:
# Name Age Gender
# a Tom 18.0 M
# c Jack 20.0 F
# e Peter 22.0 M
索引 b 和 d 的行被删除了。可以看到现在 DataFrame 只剩下 a, c, e 三行数据。如果索引是默认的整数且希望按位置删除第几行,也可以直接传索引值(因为默认索引标签就是数字)。例如 df.drop([0], axis=0) 会删除索引为0的行。但是一旦索引不是默认整序,还是应该用标签。另外,如果想按条件删除(比如删除 Age 大于20的行),可以构造一个布尔型索引掩码然后用 df.drop(df[condition].index, axis=0) 实现,这里不展开。
CSV 文件读写实践
数据通常保存在文件中,Pandas 支持读取和保存多种格式的数据文件,其中CSV(Comma-Separated Values)是最常见的文本格式。下面我们介绍 Pandas 如何高效地读取CSV,以及一些常用参数和技巧。同样,写回CSV也非常简单。
读取 CSV: 使用 pd.read_csv 函数即可读取CSV文件,基本用法是传入文件路径,Pandas 会自动将其解析为 DataFrame。示例:
import pandas as pd
# 基本读取(假设 CSV 第一行为表头)
df = pd.read_csv('data.csv')
print(df.head(5)) # 查看前5行
如果CSV文件包含表头(第一行是列名),pd.read_csv 会自动将其作为列名。如果CSV没有表头行,我们需要告诉函数不要将第一行当作列名,可以使用 header=None,同时通过 names= 指定列名列表。例如:
# 如果 CSV 没有表头行,指定 header=None 并提供列名
df = pd.read_csv('data_no_header.csv', header=None, names=['col1', 'col2', 'col3'])
read_csv 还有许多有用的参数,常用的包括:
- sep:指定分隔符,默认是逗号,。如果是制表符分隔的文件,可以用 sep=‘\t’。
- header:用来指定哪一行作为列名。如果第一行是数据而非列名,用 header=None。
- names:当没有表头或者想自行指定列名时,用这个参数传入列表作为列名。
- index_col:指定将某一列读入时设置为行索引。例如 index_col=‘Date’ 会将”Date”那列作为索引而不是普通数据列。
- usecols:可以传入一个列名列表,表示只读取这些列,忽略其他列。对于很大的文件,读取必要的列可以明显加快速度、节省内存。
- dtype:可以指定列的数据类型,比如将某列强制读成字符串 dtype={‘col’: str}。正确指定类型也有助于加快读取和减少内存占用。
- nrows:只读取前 n 行,用于预览大型文件或测试。
- encoding:指定文件编码,比如中文常用 encoding=‘utf-8’(默认)、‘gbk’ 等。
- engine:解析引擎,一般不用特别指定。只有在分隔符比较特殊(例如多字符)时,会用到 engine=‘python’。
我们将上述一些参数综合在一个例子里:假设有一个文件包含股票数据,没有列名,我们想读取其中的日期(date)作为索引,以及开盘价(open)、最高价(high)、最低价(low)、收盘价(close)这几列。文件用逗号分隔。读取代码可以这么写:
df = pd.read_csv('aapl.csv',
sep=',', # 分隔符为逗号
header=None, # 文件中无表头行
names=['name', 'date', '_', 'open', 'high', 'low', 'close', 'volume'], # 列名列表
index_col='date',# 将"date"列作为行索引
usecols=['date', 'open', 'high', 'low', 'close'] # 只读取需要的五列
)
print(df.head(3))
# 输出示例:
# open high low close
# date
# 2011-01-28 344.17 344.40 333.53 336.10
# 2011-01-31 335.80 340.04 334.30 339.32
# 2011-02-01 341.30 345.65 340.98 345.03
在这个例子中,我们通过 names 提供了8个列名(文件每行有8个字段,其中第三个字段我们命名为_表示我们不关心),然后用 usecols 选择了我们想要的5列数据(date 和 OHLC 四列),并将 date 列设为索引。head(3) 打印输出验证了读取效果。使用 usecols 有效避免了读取不需要的数据,提高了速度。
常见问题: “如果 CSV 有五万行,那么用 Pandas 读会不会很慢、很卡?”
答案是:不会。 五万行对 Pandas 而言不算很多,一般几百毫秒就能读完。不过如果数据达到上百万行乃至更大,内存和耗时就需要考虑。这种情况下可以使用分块读取:pd.read_csv(…, chunksize=10000) 会返回一个可迭代的 TextFileReader,每次读取10000行,我们可以遍历每个chunk逐步处理。这样可以避免一次性占用太多内存。另一个方法是使用 dtype 精确指定类型,避免Pandas猜测类型的开销和潜在的不必要的高精度类型。总的来说,对于超大文件,分块读取并逐步处理是常用策略。
写出 CSV: 将 DataFrame 保存为 CSV 使用 DataFrame.to_csv 方法即可。例如:
df.to_csv('output.csv', index=False)
上面代码将 DataFrame 写入当前目录下的 output.csv 文件,并设置 index=False 表示不保存行索引(因为有些情况下行索引没意义或者不要导出)。默认情况下 to_csv 会保存行索引和列标签,如果不想要可以分别用 index=False 和 header=False 关闭。to_csv 也支持 sep, encoding 等参数,自行根据需要设置即可。
除了 CSV 之外,Pandas 也支持读取/写入多种常见格式:
- Excel:使用 pd.read_excel(‘file.xlsx’) 读取,DataFrame.to_excel(‘out.xlsx’) 写入。注意读取 Excel 需要安装相应的引擎库,如 openpyxl。一般 .xlsx 文件都会用 openpyxl 引擎。
- JSON:使用 pd.read_json(‘file.json’) 读取,DataFrame.to_json(‘out.json’) 写出。read_json 可以通过参数 orient 来指定 JSON 的组织格式(比如记录为行还是列)。
- SQL:使用 pd.read_sql(query, connection) 可以从数据库读取查询结果为 DataFrame,to_sql 可以写入数据库表。(这需要SQLAlchemy等支持,这里略过)
- 以及其它格式例如 Parquet (read_parquet)、HTML表(read_html)、Stata (read_stata) 等等。
简单来说,Pandas 提供了一套统一的高层接口来读取和存储数据,让数据在不同媒介之间转移变得非常方便。你可以将其视为数据转换的瑞士军刀:CSV、Excel、JSON 转DataFrame,DataFrame再转回各种格式,都不是问题。
总结
Pandas 的两个核心数据结构:Series(一维带索引数组)和 DataFrame(二维表格)。在实际工程中,Pandas 可以大幅提高我们处理表格数据的效率。遇到“表格里的列名有空格”这样的细节问题,我们知道可以通过更换访问方式或重命名来解决;面对“怎样读大文件不卡”这样的性能问题,我们也了解了分块读取等方案。熟悉这些技巧能让我们更加游刃有余地使用 Pandas 处理数据。
脱敏说明:本文所有出现的表名、字段名、接口地址、变量名、IP地址及示例数据等均非真实,仅用于阐述技术思路与实现步骤,示例代码亦非公司真实代码。示例方案亦非公司真实完整方案,仅为本人记忆总结,用于技术学习探讨。
• 文中所示任何标识符并不对应实际生产环境中的名称或编号。
• 示例 SQL、脚本、代码及数据等均为演示用途,不含真实业务数据,也不具备直接运行或复现的完整上下文。
• 读者若需在实际项目中参考本文方案,请结合自身业务场景及数据安全规范,使用符合内部命名和权限控制的配置。Data Desensitization Notice: All table names, field names, API endpoints, variable names, IP addresses, and sample data appearing in this article are fictitious and intended solely to illustrate technical concepts and implementation steps. The sample code is not actual company code. The proposed solutions are not complete or actual company solutions but are summarized from the author's memory for technical learning and discussion.
• Any identifiers shown in the text do not correspond to names or numbers in any actual production environment.
• Sample SQL, scripts, code, and data are for demonstration purposes only, do not contain real business data, and lack the full context required for direct execution or reproduction.
• Readers who wish to reference the solutions in this article for actual projects should adapt them to their own business scenarios and data security standards, using configurations that comply with internal naming and access control policies.版权声明:本文版权归原作者所有,未经作者事先书面许可,任何单位或个人不得以任何方式复制、转载、摘编或用于商业用途。
• 若需非商业性引用或转载本文内容,请务必注明出处并保持内容完整。
• 对因商业使用、篡改或不当引用本文内容所产生的法律纠纷,作者保留追究法律责任的权利。Copyright Notice: The copyright of this article belongs to the original author. Without prior written permission from the author, no entity or individual may copy, reproduce, excerpt, or use it for commercial purposes in any way.
• For non-commercial citation or reproduction of this content, attribution must be given, and the integrity of the content must be maintained.
• The author reserves the right to pursue legal action against any legal disputes arising from the commercial use, alteration, or improper citation of this article's content.Copyright © 1989–Present Ge Yuxu. All Rights Reserved.