使用 NumPy 处理数据时,经常遇到需要从其他来源(一般对应不同格式的文件)输入数据进行处理,以及把经处理后的数据输出为各种格式的文件的情况。这些工作有时候很简单,如以规整的文本文件输入或输出;有时却非常复杂,如需要对数据异常或缺失的情况进行判断,需要输入或输出为其他复杂的格式,如 Excel、SQL、JSON、Parquet、HTML、HDF5,等等。
对于相对较简单的输入和输出,NumPy 自身提供了部分输入和输出函数可胜任此项工作。对于复杂的输入和输出,则需要借助其他软件库,如著名的 pandas。
本节仅介绍 NumPy 自身的输入输出功能,如果这些已不能满足您的要求,请进一步学习 pandas 一章的内容。
以二进制文件保存和加载#
把 NumPy 数组以文件的形式保存的最快捷、最精确的方法是 save、savez 和 savez_compressed 系列函数。他们的签名如下:
numpy.save(file, arr, allow_pickle=True, fix_imports=True)
numpy.savez(file, *args, **kwds)
numpy.savez_compressed(file, *args, **kwds)
他们都保存为二进制文件(非文本文件,由字节序列而非字符序列构成,如果用文本编辑器打开,一般显示乱码)。区别是:
save将单个数组保存为.npy后缀格式的 NumPy 特有二进制文件;savez将多个数组保存为未压缩的.npz后缀格式的 NumPy 特有的单个二进制文件;savez_compressed将多个数组保存为压缩的.npz后缀格式的 NumPy 特有的单个二进制文件。
import numpy as np
a = np.arange(10).reshape(2, 5)
b = a * 2
np.save('array_a.npy', a)
np.savez('array_a_b.npz', a, b)
np.savez_compressed('array_a_b_compressed.npz', a, b)
注意:以上代码中,各个文件名都没有给出完整的路径,你可能不知道文件被保存到哪里了。其实他们都被保存到当前工作目录中了。如果你是从 VS Code 运行代码,并且只是打开一个源文件运行代码,那么当前工作目录一般是你操作系统的用户根目录;如果你用 VS Code 打开了一个目录(项目),则该目录就是工作目录。你可以使用 os.getcwd 查看当前工作目录,也可以使用 os.chdir 更改当前工作目录。
要将以上保存的二进制文件恢复回来,需要使用 load 函数。
numpy.load(file, mmap_mode=None, allow_pickle=False, fix_imports=True,
encoding='ASCII', *, max_header_size=10000)
对于 .npy 文件,load 函数直接返回一个数组对象:
import numpy as np
a = np.arange(10).reshape(2, 5)
np.save('array_a.npy', a)
b = np.load('array_a.npy')
print(b)
# [[0 1 2 3 4]
# [5 6 7 8 9]]
当用 .npz 文件保存和恢复多个数组时,情况有点复杂。默认 savez 和 savez_compressed 函数并不保存数组变量的名称,而是自动给其赋予类似 arr_0、arr_1 的名称。同时 load 返回的是 NpzFile 对象,其中包含类似 {filename: array} 键值对形式的字典,可以通过其 files 属性查看其中包含的数组。
import numpy as np
a = np.arange(10).reshape(2, 5)
b = np.sin(a)
np.savez('array_a_b.npz', a, b) # a, b 会被匹配到 *args 参数对应的元组值
npz = np.load('array_a_b.npz')
print(npz.files) # ['arr_0', 'arr_1']
print(npz['arr_0']) # 数组 a 的值
print(npz['arr_1']) # 数组 b 的值
npz.close() # 最终需要关闭此 NpzFile 文件
也可以不使用 savez 和 savez_compressed 函数的位置参数 *args,而使用其关键字参数 **kwds 显式指定各数组的保存名称:
import numpy as np
a = np.arange(10).reshape(2, 5)
b = np.sin(a)
np.savez('array_a_b.npz', a=a, b=b) # a=a, b=b 被匹配到关键字参数 **kwds
npz = np.load('array_a_b.npz')
print(npz.files) # ['a', 'b']
print(npz['a']) # 数组 a 的值
print(npz['b']) # 数组 b 的值
npz.close() # 最终需要关闭此 NpzFile 文件
以文本文件保存和加载#
以上使用二进制文件的一个明显缺点是只能使用 NumPy 加载文件,如果希望从其他源导入数据,或者将数据导出给其他程序使用,最简单的方法是将其保存为文本文件。这需要使用 savetxt 和 loadtxt 函数,他们一次只能操作一个数组。
numpy.savetxt(fname, X, fmt='%.18e', delimiter=' ', newline='\n',
header='', footer='', comments='# ', encoding=None)
numpy.loadtxt(fname, dtype=<class 'float'>, comments='#', delimiter=None,
converters=None, skiprows=0, usecols=None, unpack=False, ndmin=0,
encoding='bytes', max_rows=None, *, quotechar=None, like=None)
可以通过 fmt 指定元素值的输出格式,通过 header、footer 和 delimiter 为文本文件指定页眉、页脚和不同的分隔符,不过要记得保存和加载文件时使用相同的 delimiter。
一个示例:
import numpy as np
a = np.arange(10).reshape(2, 5)
np.savetxt('array_a.csv', a)
b= np.loadtxt('array_a.csv')
print(b)
# [[0. 1. 2. 3. 4.]
# [5. 6. 7. 8. 9.]]
可以看出,按默认方式保存为文本文件再加载回来后,数据是有损耗的,原来数组中存储的是整数,保存时却变为浮点数,加载回来后也是浮点数。
当用文本文件交换数据时,经常使用 .csv 格式的文本文件,其名称是逗号分割值(Comma-Separated Values)文件。不过数据元素之间不一定总是使用逗号分割,因此也称为字符分割值文件。对于上页中保存的文件,其内容为:
0.000000000000000000e+00 1.000000000000000000e+00 2.000000000000000000e+00 3.000000000000000000e+00 4.000000000000000000e+00
5.000000000000000000e+00 6.000000000000000000e+00 7.000000000000000000e+00 8.000000000000000000e+00 9.000000000000000000e+00
savetxt 函数在保存数组时,默认使用空格作为分隔符,不过也可以通过 delimiter 关键字参数指定其他分隔符。常用的分割符包括空格 ' '、水平制表符 '\t'、逗号 ',' 和分号 ';'。另外,使用电子表格程序,如 Excel、WPS 表格一般能很方便地导入、导出和编辑此种格式的文件。
使用 genfromtxt 函数导入#
以上 loadtxt 函数的功能较弱。例如,当文件中某个值缺失时,当需要分别指定各列的数据类型时,当仅需选择多列中的特定列时,当就无法应对。这时就需要使用 genfromtxt,这是一个更加强大和复杂的函数。该函数有很多参数:
numpy.genfromtxt(fname, dtype=<class 'float'>, comments='#',
delimiter=None, skip_header=0, skip_footer=0, converters=None,
missing_values=None, filling_values=None, usecols=None, names=None,
excludelist=None, deletechars=" !#$%&'()*+, -./:;<=>?@[\\]^{|}~",
replace_space='_', autostrip=False, case_sensitive=True, defaultfmt='f%i',
unpack=None, usemask=False, loose=True, invalid_raise=True, max_rows=None,
encoding='bytes', *, ndmin=0, like=None)
所以,请做好心理准备,掌握该函数需要花一番功夫。
genfromtxt 唯一必须提供的参数是第一个 fname,该参数可以是:
- 单个字符串。这时会将该字符串当作本地或远程文件的名称。该字符串甚至可以是一个 URL 或远程文件,这时会自动下载文件到当前工作目录并打开。
- 一个字符串的列表。这时会将整个列表看作文件内容,每个字符串被当作文件中的一行。
- 一个生成器或类似文件的对象。该对象必须实现了
read方法,如一个文件对象,或一个io.StringIO对象。
当加载文件时,文件既可以是文本文件,也可以是文本文件的压缩存档文件。目前支持的压缩类型包括 gzip 压缩(具有后缀 .gz)和 bzip2 压缩(后缀 .bz2)。
一个简单的示例:
import numpy as np
# s 是字符串列表, 各个值以逗号分割,
# 注意列表的第一个元素长度少于第二个元素
s = ['0,1,2,3,', '5,6,7,8,9']
a = np.genfromtxt(s, delimiter=",")
print(a)
# [[ 0. 1. 2. 3. nan]
# [ 5. 6. 7. 8. 9.]]
# 按照默认, 原来的整数类型仍然被转换为浮点类型了,
# 不过它能处理缺失的元素了, nan 代表非数字