有缺失值的数据

var code = "940ed96f-02b8-49fd-afbd-67d8f9eca85a"

假设我们有一个类似于CSV的文件,每行的数据用’,'隔开。文件中有些数据是缺失的,也有些数据没有保存,采用字符串说明。

12,45,23,,23,99,33,24,,"help, Oh, help",34
7,8,3,53,,9,13,22,"help, Oh, help",,24

我们省略文件读取的过程,仅仅把第一行数据拿来作为例子。

// fake data seperated with ',', contains string and empty slot that are invalid
let data = "12,45,23,,23,99,33,24,,\"help, Oh, help\",34"
printfn "%A" data
"12,45,23,,23,99,33,24,,"help, Oh, help",34"

这个数据的解析也挺麻烦的,因为碰到有引号的字符串,我们要把他当做一个单独的单元,考虑到引号内可能会有’,'。

我们单独实现一个String的方法,来完成这个功能。这个函数里有两个相互调用的递归函数,定义的语法如下。

let rec func1 ... = 
    ......
and func2 ... = 
    ......

这样定义才能保证在func1中递归调用func2

module String =
    let split separator (s:string) =
        let values = ResizeArray<string>()
        let rec gather start i =
            let add () = s.Substring(start,i-start) |> values.Add
            if i = s.Length then add()
            elif s[i] = '"' then inQuotes start (i+1) 
            elif s[i] = separator then add(); gather (i+1) (i+1) 
            else gather start (i+1)
        and inQuotes start i =
            if s[i] = '"' then gather start (i+1)
            else inQuotes start (i+1)
        gather 0 0
        values.ToArray()

识别字符串中的数字可以使用System.Int32.TryParse,这个函数返回的是一个元组,元组的第一个元素是个布尔变量,标识是否成功识别,如果成功识别,则元组的第二个元素是数字本身。

当然,这个函数识别之后得到是一个int option

let tryParse (s:string) = 
    match System.Int32.TryParse(s) with
    | true, i -> Some i
    | _ -> None

现在所有的工具都齐全了,我们就能对CSV文件的数据行进行处理。

首先,我们把这个字符串数组转变成一个int option数组。

let nums = 
    data 
    |> String.split ',' 
    |> Array.map tryParse 
printfn "%A" nums
[|Some 12; Some 45; Some 23; None; Some 23; Some 99; Some 33; Some 24; None;
  None; Some 34|]

数据的使用

接下来我们就简单展示一下如何使用这个array<int option>数组。在实际的数据中,这是一个比较常见的状态。

快来快来数一数

首先,我们需要对数据的状态进行报告,例如有多少个数?

printfn "There are %A items in the array." (Array.length nums)
There are 11 items in the array.

那么接下来就是,有多少个数是有效的呢?

// count valid numbers and test valids and invalids
let count numbers = 
    numbers 
    |> Array.map Option.count 
    |> Array.sum

printfn "There are %A valid number." (count nums)
There are 8 valid number.

如果我们想知道哪些数字是有效的,哪些数字是无效的呢?

let testInvalid numbers = 
    numbers 
    |> Array.map Option.isNone
let testValid numbers = 
    numbers 
    |> Array.map Option.isSome
    
printfn "Are items invalid? \n%A" (testInvalid nums)
printfn "Are items valid? \n%A" (testValid nums)
Are items invalid? 
[|false; false; false; true; false; false; false; false; true; true; false|]
Are items valid? 
[|true; true; true; false; true; true; true; true; false; false; true|]

如果我们只关心有效的数字,并且想把他们打印出来呢?

// print only valid numbers
let print numbers = 
    numbers 
    |> Array.iter (Option.iter (printf "%i "))

print nums
12 45 23 23 99 33 24 34 

注意这里,我们的Option.iter配合对应集合的iter方法使用。

缺失数字填充

还有一种情况,我们经常会遇到,就是要把缺失的数据补全。

// fill invalid number with default value
let fill num numbers = numbers |> Array.map (Option.orElse (Some num))
let fill0 = fill 0

printfn "%A" (fill0 nums)
[|Some 12; Some 45; Some 23; Some 0; Some 23; Some 99; Some 33; Some 24; Some 0;
  Some 0; Some 34|]

上面这个函数运行也是很完美的,所有的缺失数据都被填上了Some 0。当然,当我们把缺失数据都填上了的时候,我们有很大的可能性就不需要那个什么option

下面这个函数,填上数据之后,就把Some给脱掉。

// fill invalid number with default value and return numbers instead of option array
let fillNum defaultValue numbers = numbers |> Array.map (Option.defaultValue defaultValue)
let fillNum0 = fillNum 0

printfn "%A" (fillNum0 nums)
[|12; 45; 23; 0; 23; 99; 33; 24; 0; 0; 34|]

对数据进行算术运算

如果我们需要对数据进行算术运算,当然,只包含哪些有意义的数字。

这个就可以通过Option.map函数配合集合的map函数来完成,或者用Option.bind配合集合的map来完成。

两个的效果是一样的,但是我们会更加倾向于用Option.map,可以少写一个Some

// valid number arithmatic: same result
let doubleIt numbers = numbers |> Array.map (Option.map (fun x-> 2*x))
let doubleItBind numbers = numbers |> Array.map (Option.bind (fun x->Some (2*x)))

printfn "Option.bind: %A" (doubleItBind nums)
printfn "Option.map: %A" (doubleIt nums)
Option.bind: [|Some 24; Some 90; Some 46; None; Some 46; Some 198; Some 66; Some 48; None;
  None; Some 68|]
Option.map: [|Some 24; Some 90; Some 46; None; Some 46; Some 198; Some 66; Some 48; None;
  None; Some 68|]

累计运算

如果要对包含这个集合进行累计运算,也有相应的Option.foldOption.foldBack来完成。

下面的例子很简单的把中缀的(*)(+)应用起来,当然任何int -> int -> int的函数都可以用来替换这两个函数。

配合Array.fold也是比较清晰的。

let product numbers = 
    numbers 
    |> Array.fold (Option.fold (*)) 1

let sum numbers = 
    numbers
    |> Array.fold (Option.fold (+)) 0

printfn "product: %A" (product nums)
printfn "sum: %A" (sum nums)
product: 1323784128
sum: 293

直接对数据进行判断

最后还有一个语义,还比较有意思,一个是forall,对于集合而言,就代表所有元素均满足判断条件;而对于option,则是直接跳过缺失的值(取为true)。

另外一个是exists,对于集合而言,就需要一个满足条件的元素即可;对于option而言,同样是跳过缺失的值(取为false)。

// Test to find all items satisfy some prediction, so None returns true
let allBiggerThan num numbers = numbers |> Array.forall (Option.forall (fun x -> x > num))

// Test to find any items satisfy some prediction, so None returns false
let anyBiggerThan num numbers = numbers |> Array.exists (Option.exists (fun x -> x > num))

printfn "%A" (anyBiggerThan 90 nums, allBiggerThan 10 nums)

(true, true)

结论

  1. option的各个函数中,基本上都是替换Some的语义;
  2. optionmap,iter,fold,forall,exists与集合的对应函数同名,其语义值得好好体会一下。
09-12 20:09