加载数值数据

基本知识介绍

什么是数值数据?

数值数据框架下有许多不同的数据格式(并且本质上这些数据格式不一定是数值). 数值数据与普通数据的主要区别是数据需要的 Wolfram 语言格式将由实数、整数、有理数或者复数,而不是字符串或者符号表示.
数值数据的典型例子是由 Comma Separated Values (CSV) 格式存储的浮点数组成的两列数组.
数值数据的另一种常见形式是时间序列数据,它有与某种测量方法(温度、速度、压力等)相关联的印时戳(time stamp). 下面是包含在 Wolfram 语言中的金融时间序列数据组成的样本.

加载数据的函数与策略

Wolfram 语言函数中有大量不同函数可用于加载数据. 下面图表总结了这些函数,以及它们各自的优缺点.
Import
对用户友好,并支持多种格式
ReadList
快速灵活地控制类型,支持数据流
BinaryReadList
比前两种函数更好的性能
Get(DumpSave)
最有效的加载操作,防止数组封装
Import
高内存花销,有限的性能
ReadList
对用户不友好,最适用于普通文本格式
BinaryReadList
通常是最不用户友好的,可能涉及大量数据处理
Get(DumpSave)
文件不可转移,需要先由其他机制加载
对于小型文件,并且当第一次作用于新数据时,通常使用 Import 来加载数据是最简单的. Import 提供了最友好的用户界面来加载数据,并且,通常在这些情况下,额外的花销不是主要问题. 但是,当部署一个应用程序或者使用特别大的数据集时,使用函数例如 ReadList 或者 BinaryReadList 能够更好地提高数据加载的性能.

ReadList 的局限性

作为一个普遍适用的规则,ReadList 在速度和内存开销上比 Import 操作更优. 部分原因是受 ReadList 中使用的第二个参数的类型限制,这使得 Wolfram 语言可以跳过 Import 中使用的各种解析操作(这确保解释为 stringsrealsintegers 的字符串项目正确用于数值). 这通常产生对用户更加友好的 Import,代价是需要额外的时间和内存来做与 ReadList 相同的操作.
如果您想要看看在内存使用量上的区别,请尝试应用 MemoryConstrained[,4*1024^2](把操作限制于 4 MB 的 RAM):
ReadList 在大型数据文件具有单个类型(比如实数或者整数组成的行和列)的情况下易于使用,但是当使用多个类型使,需要更多操作(比如当字符串和实数混合时,或者在每个列的第一行中具有实数值的数据文件中).
Import 自动转化时间序列数据的日期字符串:
Head 验证了每个列表的第二个元素解释为 Real 数值:
相比之下,ReadListRecord 返回每个行:
FullForm 表明整个行作为 String 处理:
ReadList 的其他指定可能把每个行作为 {String,Number} 列表加载:
FullForm 表明由 ReadList 产生的错误源.
RecordSeparators 只应用于 Record 类型,而非数据:
若要获得 Wolfram 语言中的期望类型,需要额外处理步骤.
对于小型数据集,通常对时间序列数据使用 Import 更有效,但是在读取大量信息时,与 Import 相关联的处理开销使额外的处理更加有价值,因此您的数据可以使用较少的内存较快地处理好.

使用转换器导入

不是所有 Import 操作都由 Wolfram 语言直接处理,但是依赖于额外的转换器来处理导入操作. 在许多情况下,这些二进制文件在文件大小上有自身的局限性,或者可能具有某些与它们相关联的开销.
XLS 文件(Microsoft Excel 文档),例如,使用 XLS 转换器来处理 XLS 文档,并且将它们读取到 Wolfram 语言中. 该过程需要额外的内存,由于不单是 Wolfram 语言而且还有转换器也需要内存中的一份文件来处理,因此必须把数据通过 Wolfram Symbolic Transfer Protocol (WSTP) 传给 Wolfram 语言. 与导入这些文件格式相关联的内存开销很大是不常见的.
对 BigData 友好的格式
下面是由 BigData 友好的文件格式组成的表格(这些较少涉及格式化的格式可以直接由 Wolfram 语言加载,并且不需要更多内存来加载).
Comma-Separated Values.csv
使用逗号和换行分隔记录的普通文本格式
Tab-Separated Values.tsv
使用制表符和换行分隔记录的普通文本格式
Table.dat
使用制表符和换行分隔记录的普通文本文件
Plain Text.txt
普通文本文件可以使用任意记录分隔符存储数据
Binary*
二进制数据格式;可以具有各种不同的文件扩展名,或者什么都不用
速度和内存性能

测量

当优化数据加载操作时,对加载时间和内存开销进行正确测量是重要的. Wolfram 语言函数有大量函数可用于这两种任务,它们能够帮助您确定加载和处理数据的最佳方式.

AbsoluteTiming vs. Timing

Wolfram 语言中有两个函数测量执行内核操作所需的时间(注意这些值不包括在前端绘制的时间):TimingAbsoluteTiming.
TimingAbsoluteTiming 之间的最大不同是 Timing 测量内核中的计算时间,而 AbsoluteTiming 测量内核中的经过时间. 在下面例子中很容易看到区别.
Timing 测量在内核进行计算所需的时间,并且没有算上等待时间.
Timing 也可用于多线程,因此如果操作只花两秒钟经过时间,但是运行了四个线程,那么 Timing 将返回 8 秒计算时间.
当读进数据时,查看 Timing 的结果会是有用的,但是在大多数情况下,经过时间(因此使用 AbsoluteTiming)是速度的有效测量方式,由于所花的实际时间很可能影响应用. 下列示例显示了在范例数据集上 TimingAbsoluteTiming 的不同.
ClearSystemCache 用于每次计算之前,以确保时间值不受缓存结果影响:
另一种测量时间的方法是在数据加载操作之前和之后手动创建计时戳,您可以看到这给出与 AbsoluteTiming 相同的数值(在这种情况下,它实际上给出比 DateList 更高的分解率).

测量时间的注意事项

当测量时间时,通常在特定操作上设置时间限制是个好办法. TimeConstrained 可以加在目标函数上,并且指定一个时间限制(以秒为单位).
注意,TimeConstrained 使用与 AbsoluteTiming 测量的经过时间相同的类型,因此时间限制不基于计算时间.

MemoryInUse 与 MaxMemoryUsed

Wolfram 语言中有两种函数测量 Wolfram 语言内核中的内存开销(注意,这些值通常不包含前端开销):MemoryInUseMaxMemoryUsed.
正如函数名所示,MemoryInUseMaxMemoryUsed 测量目前内核使用多少内存,并且当前的内核进程使用的最高内存是多少.
MemoryInUseMaxMemoryUsed 的值可以有显著不同:

测量内存的注意事项

很重要的一点是,注意,尤其当使用大型数据文件时,Wolfram 语言保持一份过去计算的历史数据,这由环境变量$HistoryLength 管理.
这意味着,即使清除了一个变量,仍然有一份该变量中的数据保存在内存中. 当操作大型数据集时,通常把 $HistoryLength 设置为一个较小的值或者零以防止不必要的内存开销是个好办法.
当开始使用大型数据集时,运行需要消耗大量系统资源的计算很容易. 当第一次对您的应用创建模型时,在计算封装在 MemoryConstrained 中是很有帮助的,它限制了特定计算可以使用的资源量.

ByteCount 的注意事项

ByteCount 结果可以与由 MemoryInUse 返回的结果不同,因为 ByteCount 分别计算每个表达式和子表达式,但是事实上,子表达式可以是相同的,因此可以共享. 下面的例子说明这一点.
这些结果是不同的,因为一个 1,000,000 元素的表达式(比如 x)使用 8,000,000 个字节,与什么元素无关. (坐在使用 64 位机器,其中一个指针是 8 字节,因此一百万指针组成的数组是 8 百万字节.)所有这些指针指向相同的表达式,String 的内容是 "Sample long string...".
ByteCount 使用简单的方法来计算表达式的大小:它把每个分量的大小和容器的大小加起来. 下面是例子.
返回 8,000,000 个字节,加上 String 表达式的一百万个备份,产生一个 152 百万的估计字节数.

局限性

在 Wolfram 语言第 9 版以及更高的版本中,您能够导入的数据量的限制是系统上可用的内存量,或者更具体一些是您的操作系统允许对单个进程使用多少内存. 换句话说,当加载数据时,存在大量因素您需要考虑,比如它们可能限制您加载大型数据集的能力.

数据维度与文件大小

值得注意的是,内存不仅受表达式中的内容影响,也受数据维度影响. 例如,下面的两个加载数据集在相同的机器上使用 ReadListImport,每个都使用新的内核,并且具有相同的元素数目,但是需要很不同的内存来加载和存储表达式.
数据由位于 0 和 10 之间的 Real 数,以及单个列中的 4.68 百万个元素组成:
该数据由 0 和 10 之间的 Real 数,以及 4.68 百万个元素组成,但是排放在 23.4k×200 矩阵中:
注意,封装的表达式都具有相同的大小(正如每个数据文件的 ImportReadList 的未封装大小),但是在基于文件维度需要加载和存储表达式的内存量上存在显著区别(在某一维度上是长的表达式通常需要更多内存).

改善局限

前面的章节给出了把数据加载入 Wolfram 语言的一些局限. 在本章节中我们将讨论如何改善这些局限所采用的技术,并且特别使用大型数据集来说明这些技术.

ReadList 与 Import 对比

ReadListImport 之间的最大性能差异是加载过程需要的内存开销,虽然速度可能有很大差别,尤其对于小型数据集.
在前面的例子中,Import 需要比 ReadList 多两倍的时间来加载数据,并且也需要明显更多的内存.
在前面的例子中,ReadList 将使用 Import 所需的一半内存:
当您想要克服这些局限时,使用 ReadListBinaryReadList 或者 Get 来替换 Import 是个好办法.

二进制文件

BinaryReadList 是加载数据最节省内存最节省时间的函数,基本上1个文件大小,只需1个单位内存的消耗.
使用 BinaryReadList 读取 ASCII 文件
正如前面的章节所示,当操作二进制格式的数据时,通常 BinaryReadListReadList 操作运行得更快. 但是,可能有些情况下您的数据已经是以普通文本/ASCII 格式存储,这时可能仍然可以使用 BinaryReadList.
数据的首次加载是以 "Byte" (8位字符)读入文件,并且在行末使用 Split 分割(行末的 ASCII 字符是 10).
下一步是把字节列表翻译为 ASCII 字符,使用 FromCharacterCode(它的默认格式是 $CharacterEncoding,这种情况下是 UTF-8)然后删除多余的空格.
这些步骤可以合并为一个帮助函数来解释数据,比如前面使用的时间序列数据.
技巧与方法

编程帮助函数

当创建一个加载数据文件的应用或者程序包时,通过创建自定义帮助函数来处理或者帮助数据加载可以提高性能.
这些函数可以用于减少加载数据文件所需的内存和时间;通常过程与将 Import 替换为 ReadList 并且包含合适的选项相同.
对于这些例子,使用一些示例时间序列数据:
在开发过程中,您可能在文件上使用 Import 以获取数据,并且让 Wolfram 语言来处理格式化和数据解释.
注意,使用不带任何选项的 ReadList 产生由 strings 组成的列表,而不是由 real 数值和日期字符串组成的嵌套列表.
RecordSeparators 中添加 ",",以及默认的分隔符,给出嵌套列表:
RecordSeparators 给出数据的恰当维度,项目仍然被作为字符串加载. 每个列表的第二个元素上加上 ToExpression 将返回预期的结果.
可以把这些步骤合并到一个函数上以导出这种格式的时间序列数据.
该函数比 Import 快 5 倍,并且使用要求的部分内存,因为它根据数据格式自动调整,而不是例如 Import 的普通函数(导入普通文本文件通常是一个很鲁棒的,普通 ReadList 函数来处理大量数据格式,另外进行一些后续处理).
创建自己的数据加载函数的另一个好处是额外的处理步骤可以插到加载操作中,而不是必须在结果上进行后续处理.
从前面的例子中修改 readTimeSeries
这比在数据集上映射 DateList 更提高性能.

读取文件的一部分

大型数据文件,尤其是那些通过脚本或者记录系统合成的文件,有时候可以消耗大量空间,致使它们无法一次加载到系统内存中. 但是,这并不意味着数据是 Wolfram 语言无法访问的,并且存在大量方法,可用于从相当大的文件中读取部分数据.
ReadList 可直接在大型文件上使用,以读取数据子集,如果数据位于文件顶端.
但是,如果数据在文件很靠里的位子,不可能在需要的信息前加载所有数据,OpenReadFind 可以用于在数据内,比如日期内定位特定元素.
注意,FindStreamPosition 设置为指定信息后面的第一个项目,因此数据流上运行的 ReadList 将从下一个记录开始,即 January 13.

重新加载数据

有些应用可能要求您每次使用时重新加载数据,或者可能要求访问数据文件的特定列表. 在这些情况下,数据加载可以通过使用 Wolfram 语言的 DumpSave 函数得到大幅度优化.
Wolfram 语言含有两个内置方法,可以在数据文件中存储变量,除了正常的 Write/Export 函数. SaveDumpSave 可以用于在文件系统中存储变量值,允许它们将来使用 Get 被重新加载.
SaveDumpSave 之间有一些重要区别. 关于数据存储的最大不同是 Save 使用普通文本格式来存储信息,而 DumpSave 使用二进制数据格式(它使用较少的空间,并且可以更快地读取). DumpSave 也保存封装数组,这可以同时提高速度和内存开销.
DumpSave 的主要局限性是它的编码系统要求数据在与编码使用的相同机器架构上加载,因此不同不能在平台之间转移.
下面的四个面板给出 Get(在 DumpSavedSaved 数据)、ReadListImport 之间的性能差异.
所有文件都是使用 585000×8 矩阵中的 RandomReal 数组成的封装数组产生的: