• F#奇妙游(32):缺失数据集合的处理


    有缺失值的数据

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

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

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

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

    // 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
    
    • 1
    • 2
    • 3
    "12,45,23,,23,99,33,24,,"help, Oh, help",34"
    
    • 1

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

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

    let rec func1 ... = 
        ......
    and func2 ... = 
        ......
    
    • 1
    • 2
    • 3
    • 4

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

    module String =
        let split separator (s:string) =
            let values = ResizeArray()
            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()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

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

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

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

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

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

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

    数据的使用

    缺失数据的集合
    计数
    填充
    算术运算
    累积
    逻辑判断

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

    快来快来数一数

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

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

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

    // 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)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    There are 8 valid number.
    
    • 1

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

    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)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    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|]
    
    • 1
    • 2
    • 3
    • 4

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

    // print only valid numbers
    let print numbers = 
        numbers 
        |> Array.iter (Option.iter (printf "%i "))
    
    print nums
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    12 45 23 23 99 33 24 34 
    
    • 1

    注意这里,我们的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)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    [|Some 12; Some 45; Some 23; Some 0; Some 23; Some 99; Some 33; Some 24; Some 0;
      Some 0; Some 34|]
    
    • 1
    • 2

    上面这个函数运行也是很完美的,所有的缺失数据都被填上了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)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    [|12; 45; 23; 0; 23; 99; 33; 24; 0; 0; 34|]
    
    • 1

    对数据进行算术运算

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

    这个就可以通过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)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    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|]
    
    • 1
    • 2
    • 3
    • 4

    累计运算

    如果要对包含这个集合进行累计运算,也有相应的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)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    product: 1323784128
    sum: 293
    
    • 1
    • 2

    直接对数据进行判断

    最后还有一个语义,还比较有意思,一个是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)
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    (true, true)
    
    • 1

    结论

    1. option的各个函数中,基本上都是替换Some的语义;
    2. optionmap,iter,fold,forall,exists与集合的对应函数同名,其语义值得好好体会一下。
  • 相关阅读:
    基于端智能的播放QoE优化
    通用BIOS自动化修改脚本
    测试/开发程序员,30而立,你是否觉得迷茫?又当何去何从......
    springboot大学生拼车管理系统毕业设计源码201507
    echarts 中国地图效果,并附上小旗子
    MaixII-Dock(v831)学习笔记——GPIO
    【Java并发入门】02 Java内存模型:看Java如何解决可见性和有序性问题
    springboot解决跨域问题
    漫谈:C语言 C++ 迷惑的语句、分号、大括号
    Unity LayerMask避坑笔记
  • 原文地址:https://blog.csdn.net/withstand/article/details/132813993