使用 NumPy genfromtxt 导入表格数据

NumPy 提供了多种从表格数据创建数组的函数,本文重点介绍 genfromtxt。

简而言之,genfromtxt 执行两轮主要循环:第一轮把文件中的每一行转换成字符串序列;第二轮再把每个字符串转换成合适的数据类型。两轮循环比单轮循环慢,但更加灵活。尤其是,它能处理缺失数据,而更快、更简单的 loadtxt 等函数无法以同样方式处理这些情况。

下面的示例统一使用:

>>> import numpy as np
>>> from io import StringIO

定义输入

genfromtxt 唯一必需的参数是数据来源。它可以是字符串、字符串列表、生成器,或一个已经打开、提供 read 方法的类文件对象,例如文件或 io.StringIO。

如果提供单个字符串,该字符串被视为本地或远程文件名。如果提供字符串列表或返回字符串的生成器,每个字符串被视为文件中的一行。传入远程文件 URL 时,函数会自动将文件下载到当前目录并打开。

可识别的文件类型包括文本文件与压缩文件。目前支持 gzip 和 bz2,也就是 bzip2。压缩类型由扩展名判断:文件名以 .gz 结尾时按 gzip 处理,以 bz2 结尾时按 bzip2 处理。

将数据行拆分为列

delimiter 参数

定义并打开文件后,genfromtxt 会把每个非空数据行拆分成一串字符串。空行和注释行直接跳过。delimiter 用来定义拆分方式。

通常使用单个字符分隔列。例如 CSV 文件使用逗号 , 或分号 ;:

>>> data = "1, 2, 3\n4, 5, 6"
>>> np.genfromtxt(StringIO(data), delimiter=",")
array([[1.,  2.,  3.],
       [4.,  5.,  6.]])

另一个常见分隔符是制表符 \t。分隔符并不限于单个字符,也可以是任意字符串。默认 delimiter=None,表示按空白字符,包括制表符,拆分数据;连续空白被视为一个分隔符。

固定宽度文件则按每列的字符数量划分。这时,如果所有列宽相同,将 delimiter 设为一个整数;如果各列宽不同,设为整数序列:

>>> data = "  1  2  3\n  4  5 67\n890123  4"
>>> np.genfromtxt(StringIO(data), delimiter=3)
array([[  1.,    2.,    3.],
       [  4.,    5.,   67.],
       [890.,  123.,    4.]])
>>> data = "123456789\n   4  7 9\n   4567 9"
>>> np.genfromtxt(StringIO(data), delimiter=(4, 3, 2))
array([[1234.,   567.,    89.],
       [   4.,     7.,     9.],
       [   4.,   567.,     9.]])

autostrip 参数

默认情况下,一行拆分成字符串后,不会删除各项开头或末尾的空白。将可选参数 autostrip 设为 True 可以改变这一行为:

>>> data = "1, abc , 2\n 3, xxx, 4"
>>> # Without autostrip
>>> np.genfromtxt(StringIO(data), delimiter=",", dtype="|U5")
array([['1', ' abc ', ' 2'],
       ['3', ' xxx', ' 4']], dtype='<U5')
>>> # With autostrip
>>> np.genfromtxt(StringIO(data), delimiter=",", dtype="|U5", autostrip=True)
array([['1', 'abc', '2'],
       ['3', 'xxx', '4']], dtype='<U5')

comments 参数

comments 定义标记注释开始的字符串,默认值为 '#'。注释标记可以出现在一行中的任意位置,标记之后的所有字符都会被忽略:

>>> data = """#
... # Skip me !
... # Skip me too !
... 1, 2
... 3, 4
... 5, 6 #This is the third line of the data
... 7, 8
... # And here comes the last line
... 9, 0
... """
>>> np.genfromtxt(StringIO(data), comments="#", delimiter=",")
array([[1., 2.],
       [3., 4.],
       [5., 6.],
       [7., 8.],
       [9., 0.]])

注意:有一个重要例外:如果设置 names=True,函数会检查首个用于读取列名的注释行。

跳过行与选择列

skip_header 和 skip_footer

文件头可能妨碍数据处理。使用 skip_header 可以在其他操作开始前,跳过文件开头指定数量的行,其值必须是整数。类似地,将 skip_footer 设为 n,可以跳过文件末尾的 n 行:

>>> data = "\n".join(str(i) for i in range(10))
>>> np.genfromtxt(StringIO(data),)
array([0.,  1.,  2.,  3.,  4.,  5.,  6.,  7.,  8.,  9.])
>>> np.genfromtxt(StringIO(data),
...               skip_header=3, skip_footer=5)
array([3.,  4.])

两个参数默认都为 0,即不跳过任何行。

usecols 参数

如果只需要部分列,可通过 usecols 选择要导入的列。参数可以是单个整数,也可以是对应列索引的整数序列。第一列的索引是 0,负整数的含义与普通 Python 负索引相同。

例如,只读取第一列和最后一列:

>>> data = "1 2 3\n4 5 6"
>>> np.genfromtxt(StringIO(data), usecols=(0, -1))
array([[1.,  3.],
       [4.,  6.]])

如果列有名称,也可以按列名选择:传入字符串序列,或者用逗号分隔的字符串。

>>> data = "1 2 3\n4 5 6"
>>> np.genfromtxt(StringIO(data),
...               names="a, b, c", usecols=("a", "c"))
array([(1., 3.), (4., 6.)], dtype=[('a', '<f8'), ('c', '<f8')])
>>> np.genfromtxt(StringIO(data),
...               names="a, b, c", usecols=("a, c"))
    array([(1., 3.), (4., 6.)], dtype=[('a', '<f8'), ('c', '<f8')])

选择数据类型

dtype 是控制字符串序列如何转换为其他类型的主要方式。它接受:

  • 单一类型,例如 dtype=np.float64。输出通常是指定类型的二维数组,除非使用 names 为各列命名。np.float64 也是 genfromtxt 的默认类型。
  • 类型序列,例如 dtype=(np.int_, np.float64, np.float64)。
  • 逗号分隔的类型字符串,例如 dtype="i4,f8,|U3"。
  • 包含 'names' 与 'formats' 两个键的字典。
  • (name, type) 元组序列,例如 dtype=[('A', np.int_), ('B', np.float64)]。
  • 已有的 numpy.dtype 对象。
  • 特殊值 None,表示从数据本身推断每列类型。

除第一种情况外,输出是具有结构化 dtype 的一维数组。该 dtype 的字段数与序列项数相同,字段名通过 names 指定。

使用 dtype=None 时,会逐步尝试推断每列类型:先判断字符串能否转换为布尔值,也就是是否对应小写 true 或 false;再尝试整数、浮点数、复数,最后才使用字符串。

dtype=None 很方便,但明显慢于显式指定类型。

设置列名

names 参数

处理表格时,通常会为每列分配名称。一种方法是显式使用结构化 dtype:

>>> data = StringIO("1 2 3\n 4 5 6")
>>> np.genfromtxt(data, dtype=[(_, np.int_) for _ in "abc"])
array([(1, 2, 3), (4, 5, 6)],
      dtype=[('a', '<i8'), ('b', '<i8'), ('c', '<i8')])

更简单的方法是给 names 传入字符串序列或逗号分隔的字符串:

>>> data = StringIO("1 2 3\n 4 5 6")
>>> np.genfromtxt(data, names="A, B, C")
array([(1., 2., 3.), (4., 5., 6.)],
      dtype=[('A', '<f8'), ('B', '<f8'), ('C', '<f8')])

上例依赖默认的 dtype=np.float64。提供名称序列会强制输出使用结构化 dtype。

也可以从数据本身读取列名。设置 names=True 后,会从跳过 skip_header 行之后的第一行读取名称,即使该行是注释:

>>> data = StringIO("So it goes\n#a b c\n1 2 3\n 4 5 6")
>>> np.genfromtxt(data, skip_header=1, names=True)
array([(1., 2., 3.), (4., 5., 6.)],
      dtype=[('a', '<f8'), ('b', '<f8'), ('c', '<f8')])

names 默认是 None。如果传入其他名称值,它们会覆盖 dtype 中已经定义的字段名:

>>> data = StringIO("1 2 3\n 4 5 6")
>>> ndtype=[('a', np.int_), ('b', np.float64), ('c', np.int_)]
>>> names = ["A", "B", "C"]
>>> np.genfromtxt(data, names=names, dtype=ndtype)
array([(1, 2., 3), (4, 5., 6)],
      dtype=[('A', '<i8'), ('B', '<f8'), ('C', '<i8')])

defaultfmt 参数

如果 names=None,但输出需要结构化 dtype,字段名会使用 NumPy 标准默认格式 "f%i",得到 f0、f1 等:

>>> data = StringIO("1 2 3\n 4 5 6")
>>> np.genfromtxt(data, dtype=(np.int_, np.float64, np.int_))
array([(1, 2., 3), (4, 5., 6)],
      dtype=[('f0', '<i8'), ('f1', '<f8'), ('f2', '<i8')])

同样,如果提供的名称少于 dtype 的字段数,缺失名称也会按默认模板补齐:

>>> data = StringIO("1 2 3\n 4 5 6")
>>> np.genfromtxt(data, dtype=(np.int_, np.float64, np.int_), names="a")
array([(1, 2., 3), (4, 5., 6)],
      dtype=[('a', '<i8'), ('f0', '<f8'), ('f1', '<i8')])

可以用 defaultfmt 指定其他格式字符串:

>>> data = StringIO("1 2 3\n 4 5 6")
>>> np.genfromtxt(data, dtype=(np.int_, np.float64, np.int_), defaultfmt="var_%02i")
array([(1, 2., 3), (4, 5., 6)],
      dtype=[('var_00', '<i8'), ('var_01', '<f8'), ('var_02', '<i8')])

注意:defaultfmt 只有在需要名称、但某些名称未定义时才会使用。

列名合法性

结构化 NumPy 数组也可以视为 recarray,此时可像访问属性一样访问字段。因此,应避免字段名中包含空格或无效字符,也要避免与 size、shape 等标准属性重名,以免产生混淆。

genfromtxt 提供三个参数进一步控制字段名:

  • deletechars:字符串中列出需要从字段名删除的全部字符。默认无效字符包括 ~!@#$%^&*()-=+~\|]}[{';: /?.>,< 等原文所列标点。
  • excludelist:指定排除的名称列表,例如 return、file、print。如果输入名称在列表中,会在末尾追加下划线 _。
  • case_sensitive:设为 True 保留大小写;设为 False 或 'upper' 转为大写;设为 'lower' 转为小写。

细化转换过程

converters 参数

通常指定 dtype 就足以决定转换方式,但有时需要额外控制,例如把 YYYY/MM/DD 格式日期转为 datetime 对象,或把 xx% 这样的字符串转换为 0 到 1 之间的浮点数。这时可以通过 converters 定义转换函数。

该参数通常是字典:键为列索引或列名,值为普通函数或 lambda 函数。转换函数应仅接收一个字符串,并返回一个目标类型的元素。

下面把第二列的百分数字符串转换为 0 到 1 之间的浮点数。先看没有转换器的情况:

>>> convertfunc = lambda x: float(x.strip("%"))/100.
>>> data = "1, 2.3%, 45.\n6, 78.9%, 0"
>>> names = ("i", "p", "n")
>>> # General case .....
>>> np.genfromtxt(StringIO(data), delimiter=",", names=names)
array([(1., nan, 45.), (6., nan, 0.)],
      dtype=[('i', '<f8'), ('p', '<f8'), ('n', '<f8')])

默认 dtype 是 np.float64,所以第二列也被视为浮点数。但 ' 2.3%' 和 ' 78.9%' 无法直接转换,结果变成 np.nan。添加转换器后:

>>> # Converted case ...
>>> np.genfromtxt(StringIO(data), delimiter=",", names=names,
...               converters={1: convertfunc})
array([(1., 0.023, 45.), (6., 0.789, 0.)],
      dtype=[('i', '<f8'), ('p', '<f8'), ('n', '<f8')])

也可以用第二列名称 "p" 代替索引 1,结果相同:

>>> # Using a name for the converter ...
>>> np.genfromtxt(StringIO(data), delimiter=",", names=names,
...               converters={"p": convertfunc})
array([(1., 0.023, 45.), (6., 0.789, 0.)],
      dtype=[('i', '<f8'), ('p', '<f8'), ('n', '<f8')])

转换器还可以为缺失项提供默认值。下面的 convert 先去除空白,再把字符串转为浮点数;如果为空,则使用 -999。由于默认不会清除空白,这里必须显式调用 strip():

>>> data = "1, , 3\n 4, 5, 6"
>>> convert = lambda x: float(x.strip() or -999)
>>> np.genfromtxt(StringIO(data), delimiter=",",
...               converters={1: convert})
array([[   1., -999.,    3.],
       [   4.,    5.,    6.]])

使用缺失值标记和填充值

要导入的数据可能包含缺失项。前面已经展示如何用转换器处理空字符串,但大量自定义转换器很快就会变得难以管理。

genfromtxt 还提供两种互补机制:missing_values 用来识别缺失数据,filling_values 则决定如何填充这些位置。

missing_values

默认所有空字符串都标记为缺失。也可以把 "N/A"、"???" 等字符串视为缺失或无效数据。参数接受:

  • 字符串或逗号分隔的字符串:作为所有列的缺失标记。
  • 字符串序列:各项按顺序对应各列。
  • 字典:值为字符串或字符串序列;键为列索引或列名。特殊键 None 可定义适用于全部列的默认标记。

filling_values

识别缺失项之后,还要为这些位置提供值。默认填充值取决于预期 dtype:

预期类型默认填充值
boolFalse
int-1
floatnp.nan
complexnp.nan+0j
字符串'???'

filling_values 可进一步控制填充,接受:

  • 单个值:作为全部列的默认值。
  • 值序列:每一项作为对应列的默认值。
  • 字典:键为列索引或列名,值为单个对象。特殊键 None 可定义全部列的默认值。

下面假设第一列用 "N/A" 标记缺失,第三列用 "???" 标记缺失。希望第一、第二列的缺失项填 0,最后一列填 -999:

>>> data = "N/A, 2, 3\n4, ,???"
>>> kwargs = dict(delimiter=",",
...               dtype=np.int_,
...               names="a,b,c",
...               missing_values={0:"N/A", 'b':" ", 2:"???"},
...               filling_values={0:0, 'b':0, 2:-999})
>>> np.genfromtxt(StringIO(data), **kwargs)
array([(0, 2, 3), (4, 0, -999)],
      dtype=[('a', '<i8'), ('b', '<i8'), ('c', '<i8')])

usemask

如果还希望记录哪些位置原本缺失,可以生成布尔掩码:缺失位置为 True,其他位置为 False。将可选参数 usemask 设为 True 即可,默认值为 False。此时输出数组为 MaskedArray。

原文版权声明:© Copyright 2008-2026, NumPy Developers。许可信息见 NumPy License。


原文:Importing data with genfromtxt。本文为该文档的中文译文,示例代码保留原文。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容