(**
# FSharp.Data 程序集之 CSV Type Provider
本文演示了如何使用 CSV 类型提供程序在静态类型的方式读取 CSV 文件。我们来看看如何从雅虎财经网站下载股票价格,然后再看一下类型提供程序是如何支持度量单位。这个类型提供程序使用的代码与在“金融计算”教程网站 [Try F#](http://www.tryfsharp.org)上的代码完全相同,所以你可以在那儿找到更多的例子。
CSV 类型提供程序把一个样本 CSV 作为输入,并生成一个基于样本数据列的类型。从第一行(头)获得列名,从后续行的值推断类型。
## 提供程序介绍
类型提供程序定位在 `FSharp.Data.dll` 程序集中。假设这个和谐集是在 `../bin` 目录,我们能够在 F# Interactive 中执行下面的命令进行加载:
*)
#r "../../bin/FSharp.Data.dll"
open FSharp.Data
(**
### 解析股票价格
雅虎财经网站以 CSV 格式提供每天的股票价格,结构如下(你可以在 [`docs/MSFT.csv`](../docs/MSFT.csv) 中找到一个更大的样本:
Date,Open,High,Low,Close,Volume,Adj Close
2012-01-27,29.45,29.53,29.17,29.23,44187700,29.23
2012-01-26,29.61,29.70,29.40,29.50,49102800,29.50
2012-01-25,29.07,29.65,29.07,29.56,59231700,29.56
2012-01-24,29.47,29.57,29.18,29.34,51703300,29.34
像通常的 CSV 文件一样,第一行包含头(每一列的名字),后面的行定义数据。我们可以把对该文件的引用传递给 `CsvProvider`,获得该文件的强类型视图:
*)
type Stocks = CsvProvider<"../docs/MSFT.csv">
(**
产生的类型提供两个静态方法用来加载数据。如果数据是 `string` 形式可以使用 `Parse` 方法;如果数据来自文件或网站,可以使用 `Load` 方法。下面的示例用 URL 参数调用 Load 方法,它指向了雅虎财经网站网站的一个实时 CSV 文件:
*)
// 下载股票价格
let msft = Stocks.Load("http://ichart.finance.yahoo.com/table.csv?s=MSFT")
// 看一下最前面的一行,注意一下 `Date` 是 'DateTime' 类型,而 'Open' 是 'decimal' 类型
let firstRow = msft.Data |> Seq.head
let lastDate = firstRow.Date
let lastOpen = firstRow.Open
// 以 HLOC 格式打印价格
for row in msft.Data do
printfn "HLOC: (%A, %A, %A, %A)" row.High row.Low row.Open row.Close
(**
生成的类型有一个 `Data` 属性,以行集合的形式返回 CSV 文件中的数据。我们使用 `for` 循环遍历所有行,行(生成)的类型有这样一些属性 `High`, `Low` 和 `Close`,分别对应于 CSV 文件中的列。
如你所见,类型提供程序也推断出每一行的类型。`Date` 属性推断成 `DateTime`(因为样本文件中的值都可以解析成日期),而 HLOC 价格被推断成 `decimal`。
### 绘制股票价格图表
我们可以使用 `FSharpChart` 库函数绘制简单的线性图表,展示微软公司自创立以来股票价格的变化:
*)
// 加载 F# 图表库
#load "../lib/FSharpChart.fsx"
open System
open Samples.FSharp.Charting
// 绘制股票价格
[ for row in msft.Data -> row.Date, row.Open ]
|> Chart.FastLine
(**
作为另外一个示例,使用 `Candlestick` 图表获得更详细的信息,展示了最后一个月的数据:
*)
// 以 HLOC 格式获得最后一个月的价格
let recent =
[ for row in msft.Data do
if row.Date > DateTime.Now.AddDays(-30.0) then
yield row.Date, row.High, row.Low, row.Open, row.Close ]
// 使用 Candlestick 图表展示价格
Chart.Candlestick(recent).AndYAxis(Max = 30.0, Min = 25.0)
(**
## 使用度量单位
CSV 类型提供程序的另一个有趣功能是支持 F# 的度量单位。如果头包含了标准的国际单位名或符号,那么生成的类型返回值就有相应的单位。
在这一节,我们使用一个简单的文件 [`docs/SmallTest.csv`](../docs/SmallTest.csv),看起来像下面这样:
Name, Distance (metre), Time (s)
First, 50.0, 3.7
如你所见,第二、三列分别用 `metre` 和 `s` 进行注释。要在代码中使用度量单位,需要打开有标准单位名的空间,然后,把 `SmallTest.csv` 文件作为静态参数传递类型提供程序。还要注意一下,我们在运行时还用相同的数据,因此,不需要使用 Load 方法,只要调用的构造函数就好了。
*)
let small = new CsvProvider<"../docs/SmallTest.csv">()
(**
像前一个示例一样,`small` 值表示行,有 `Data` 属性。而生成的属性 `Distance` 和 `Time` 现在已经有单位了。看一下下面简单的计算:
*)
open Microsoft.FSharp.Data.UnitSystems.SI.UnitNames
for row in small.Data do
let speed = row.Distance / row.Time
if speed > 15.0M<metre/second> then
printfn "%s (%A m/s)" row.Name speed
(**
`Distance` 和 `Time` 的数值都被推断成 `decimal`(因为,它们足够小),这样,`speed` 的类型就成为 `decimal<meter/second>` 了。这样,编译器就能静态检查,不会对不兼容的值进行丝袜,例如,每秒米与每小时公里比较。
## 自定义分隔符和制表符分隔的文件
默认情况下,CSV 类型提供程序使用逗号(`,`)作为分隔符。然而,CSV 文件有时还会用到不同的分隔符,欧洲国家通常用作数字的分隔符,因此,会用分号作为 CSV 列的分隔符。`CsvProvider`有一个可选的 `Separator` 参数,用来指定分隔符。这样,就可以自定义任意文本表格格式。下面的示例使用分号作为分隔符‘
*)
let airQuality = new CsvProvider<"../docs/AirQuality.csv", ";">()
for row in airQuality.Data do
if row.Month > 6 then
printfn "Temp: %i Ozone: %f " row.Temp row.Ozone
(**
上面用到的空气质量数据集,是统计计算语言 R 中乃至的,有大量样本。[R 语言手册]对这个数据集有简单的描述(http://stat.ethz.ch/R-manual/R-devel/library/datasets/html/airquality.html)。
如果解析制表符分隔符文件,即 `\t` 作为分隔符,也可以显式指定分隔符。然而,如果使用 URL 或者 `.tsv` 扩展名的文件,类型提供程序缺省就用 `\t`。在下面的示例中,还把 `IgnoreErrors` 设置成 `true`,这样,元素中有不正确的数值会自动忽略(在这个示例文件的最后,包含了额外的非结构化数据):
*)
let mortalityNy = new CsvProvider<"../docs/MortalityNY.tsv", IgnoreErrors=true>()
// 依据代码查找原因
// (在事故中自行车手受伤)
let cause = mortalityNy.Data |> Seq.find (fun r ->
r.``Cause of death Code`` = "V13.4")
// 输出受伤的自行车手数量
printfn "CAUSE: %s" cause.``Cause of death``
for r in mortalityNy.Data do
if r.``Cause of death Code`` = "V13.4" then
printfn "%s (%d cases)" r.County r.Count
(**
最后要注意的是,`CsvProvider` 是可以指定多种不多的分隔符的,当文件不规则时,比如一行中既有逗号又有分号,非常有用。用法:
`CsvProvider<"../docs/AirQuality.csv", Separator=";,">`.
## Missing values省略值
在有关统计的数据集中省略某些值,很常见。打开文件 [`docs/AirQuality.csv`](../docs/AirQuality.csv),你会发现,臭氧观测值(Ozone observations)被标为 `#N/A`,这样的值会解析成 float,在 F# 中表示为 `Double.NaN`。`#N/A`, `NA`, 和 `:` 默认都被认为是省略值,但是,也可以通过指定 `CsvProvider` 的 `MissingValues` 参数进行自定义。
下面的代码计算臭氧观测值,但是不包含 `Double.NaN` 值。我们首先获取每一行的 `Ozone` 属性,然后去掉省略值,最后,再用标准的 `Seq.average` 函数进行计算;
*)
let mean =
airQuality.Data
|> Seq.map (fun row -> row.Ozone)
|> Seq.filter (fun elem -> not (Double.IsNaN elem))
|> Seq.average
(**
## 控制类型推断
默认情况下,CSV 类型提供程序通过检测前面 1000 行进行类型推断,但是,也可以通过指定 `CsvProvider` 的 `InferRows` 参数进行自定义。但是,如果指定 0,将使用整个文件进行推断。
不管哪一行,如果值是省略的,CSV 类型提供程序会把 `int` and `int64` 推断为非空值,把 `bool`, `DateTime` 和 `Guid` 推断为可选值。当有可能推断为 `decimal` 时,如果有省略值,就用 `float` 代替,用 `Double.NaN` 表示省略值。`string` 类型天生就可为空,因此,默认情况下,不要生成 `string option` 了。
如果样本中没有省略值,但是,你可能想控制某一列的省略值,可以指定 `SafeMode` 参数为 true。
如果你情愿选择可空值,或相反,或者想把某一列指定为 decimal`,虽然这一列的值完全就是 `int`,那么就要覆盖其默认行为,通过在头列的尖括号中指定类型,与指定度量单位相似。可用的类型有:`int`, `int64`, `bool`, `float`, `decimal`, `date`, `guid`, `string`, `int?`, `int64?`, `bool?`, `float?`, `decimal?`, `date?`,`guid?`, `int option`, `int64 option`, `bool option`, `float option`, `decimal option`, `date option`, `guid option`, 和`string option`.
也可设置 `PreferOptionals` 参数为 true,进行全局覆盖,使所有列都有省略值可选(不是可空),对于省略的 `decimal` 或者 `float` 值,输出为 `None`,而不是 `Double.NaN`。
同时指定类型与单位也是可以的(比如 `float<metre>`)。示例:
Name, Distance (decimal?<metre>), Time (float)
First, 50, 3
另外,逊可以在 `CsvProvider` 的 `Schema` 参数中指定一些类型。格式为:
* `Type`
* `Type<Measure>`
* `Name (Type)`
* `Name (Type<Measure>)`
在 `Schema` 参数中指定的优先级要高于在头中指定的。
如果的文件的第一行不是头,而认为是数据行的话,可以指定 `HasHeaders` 参数为 false,这样,列的命名就是 Column1, Column2, ...,除非使用 `Schema` 参数这些名字。要注意的是,只能覆盖 `Schema` 参数中的名字,且可以推断出类型。例如:
*)
let csv = new CsvProvider<"1,2,3", HasHeaders = false, Schema = "Duration (float<second>),foo,float option">()
for row in csv.Data do
printfn "%f %d %f" (row.Duration/1.0<second>) row.foo (defaultArg row.Column3 1.0)
(**
也可以不覆盖所有的列,让其保持默认,就可以只指定覆盖其中的一、两个。例如,在来自 Kagge (大数据分析平台)的泰坦尼克训练数据集中([`docs/Titanic.csv`](../docs/Titanic.csv)),如果只想覆盖 `Pclass` 列,把 `int` 推断成 `string`:
*)
let titanic1 = new CsvProvider<"../docs/Titanic.csv", Schema=",,string,,,,,,,,,">()
for row in titanic1.Data do
printfn "%s" row.Pclass
let titanic2 = new CsvProvider<"../docs/Titanic.csv", Schema="Pclass=string">()
for row in titanic2.Data do
printfn "%s" row.Pclass
(**
## 转换 CSV 文件
`CsvProvider` 除了有读取功能以外,还能转换 CSV 文件,有这样操作: `Filter`, `Take`, `TakeWhile`, `Skip`, `SkipWhile`, 和 `Truncate`。所有这些操作都保留架构,因此,转换以后可以保存结果,只要重载 `Save` 方法。如果不需要以 CSV 格式保存结果,如果转换需要改变数据的结构,也可以使用 `Seq` 模块下的操作,在行数据序列上,直接通过 `Data` 属性进行导出。
*)
// 保存前 10 行没有省略值的数据到一个新的 CSV 文件
airQuality.Filter(fun row -> not (Double.IsNaN row.Ozone) &&
not (Double.IsNaN row.``Solar.R``))
.Truncate(10)
.SaveToString()
(**
为了方便,还可以把每一行当成一个元组,使用 RowType 的 `AsTuple` 属性。当有不同的 CSV 文件,但架构相似,需要用统一的方法处理时,非常有用。
*)
for row in airQuality.Data do
printfn "%A" row.AsTuple
(**
## 处理大数据集
通常,数据行会被缓存起来,这样,就不必担心在 `Data` 属性上进行多次重复操作。但是,如果只处理一次,就应该放弃缓存,方法是设置 `CsvProvider` 的 `CacheRows` 参数为 false。如果数据行非常多,也必须这样做,否则,就可能耗尽内存。但是,如果已经把数据集改得比较小以后,仍然可以在某些地方使用 `Cache` 方法缓存数据:
*)
let stocks = new CsvProvider<"http://ichart.finance.yahoo.com/table.csv?s=MSFT", CacheRows=false>()
stocks.Take(10).Cache()
(**
## 相关文章
* [F# Data: Type Providers](../fsharpdata.html) - 更多有关 `FSharp.Data` 包中其他类型提供程序的内容。
* [F# Data: CSV Parser and Reader](CsvFile.html) - 提供更多有关动态处理 CSV 的内容。
*)