Spark数据分析之第3课

来源:互联网 发布:百度云网络不给力 编辑:程序博客网 时间:2024/05/22 14:12
#连续变量的概要统计
对类别变量基数相关小的数据,非常适合用Spark的countByValue动作创建直方图。但是对连续变量,比如病人记录字段匹配分数,我们想要快速
得到其分布的基本统计信息,比如均值,标准差和极值(比如最大值和最小值)。
除了RDD[Double]的隐式动作,Spark支持RDD[Tuple2[K,V]]类型隐式类型转换,不但提供根据每个键来汇总的groupByKey和reduceByKey方法,而且提供
联结键类型相同的多个RDD的方法。
stats是RDD[Double]的一个隐式动作,它提供了我们渴望的RDD值概要统计信息。
#在parsed RDD中MatchData记录的scoes数组的第一个值上实验一下:
scala> parsed.map(md => md.scores(0)).stats
res27: org.apache.spark.util.StatCounter = (count: 5749132, mean: NaN, stdev: NaN, max: NaN, min: NaN)


槽糕的是,数组中用作占位符的缺失NaN值使Spark的概要统计信息出错了。
我们可以使用Java Double类的isNaN函数手动过滤:
import java.lang.Double.isNaN
scala> parsed.map(md => md.scores(0)).filter(!isNaN(_)).stats()
res41: org.apache.spark.util.StatCounter = (count: 5748125, mean: 0.712902, stdev: 0.388758, max: 1.000000, min: 0.000000)


只要愿意,可以使用这种方式得到scoers数组值的所有统计信息。用Scala的Range结构创建一个循环,遍历每个下标并计算该列的统计信息:
val stats = (0 until 9).map(i => {parsed.map(md => md.scores(i)).filter(!isNaN(_)).stats()})


#查看所有列的统计信息
scala> stats.foreach(println)
(count: 5748125, mean: 0.712902, stdev: 0.388758, max: 1.000000, min: 0.000000)
(count: 103698,  mean: 0.900018, stdev: 0.271316, max: 1.000000, min: 0.000000)
(count: 5749132, mean: 0.315628, stdev: 0.334234, max: 1.000000, min: 0.000000)
(count: 2464,    mean: 0.318413, stdev: 0.368492, max: 1.000000, min: 0.000000)
(count: 5749132, mean: 0.955001, stdev: 0.207301, max: 1.000000, min: 0.000000)
(count: 5748337, mean: 0.224465, stdev: 0.417230, max: 1.000000, min: 0.000000)
(count: 5748337, mean: 0.488855, stdev: 0.499876, max: 1.000000, min: 0.000000)
(count: 5748337, mean: 0.222749, stdev: 0.416091, max: 1.000000, min: 0.000000)
(count: 5736289, mean: 0.005529, stdev: 0.074149, max: 1.000000, min: 0.000000)


#或直接查看
scala> (0 until 9).map(i => {parsed.map(md => md.scores(i)).filter(!isNaN(_)).stats()}).foreach(println)
(count: 5748125, mean: 0.712902, stdev: 0.388758, max: 1.000000, min: 0.000000)
(count: 103698,  mean: 0.900018, stdev: 0.271316, max: 1.000000, min: 0.000000)
(count: 5749132, mean: 0.315628, stdev: 0.334234, max: 1.000000, min: 0.000000)
(count: 2464,    mean: 0.318413, stdev: 0.368492, max: 1.000000, min: 0.000000)
(count: 5749132, mean: 0.955001, stdev: 0.207301, max: 1.000000, min: 0.000000)
(count: 5748337, mean: 0.224465, stdev: 0.417230, max: 1.000000, min: 0.000000)
(count: 5748337, mean: 0.488855, stdev: 0.499876, max: 1.000000, min: 0.000000)
(count: 5748337, mean: 0.222749, stdev: 0.416091, max: 1.000000, min: 0.000000)
(count: 5736289, mean: 0.005529, stdev: 0.074149, max: 1.000000, min: 0.000000)




#为计算概要信息创建可重用的代码
上面的方法能完成工作,但是为了得到所有的统计信息,必须重复处理parsed RDD的所有记录9次。
虽然可以放到内存以节省时间,但是随着数据量越来越大,重复处理所有数据的开销也将越来越高。
用Spark开发分布式算法时,减少遍历数据的次数来得到所有答案是值得的。


对缺失值的分析,我们的第一个任务就是写一个类似于Spark StatCounter类的东西,以正确处理缺失值。
StatsWithMissing.scala
import org.apache.spark.util.StatCounter
class NAStatCounter extends Serializable {
    val stats: StatCounter = new StatCounter()
    var missing: Long = 0
    
    def add(x: Double): NAStatCounter = {
        if (java.lang.Double.isNaN(x)) {
            missing += 1
        } else {
            stats.merge(x)
        }
        this
    }   
    
    def merge(other: NAStatCounter): NAStatCounter = {
        stats.merge(other.stats)
        missing += other.missing
        this
    }
    
    // 覆盖方法必须加上override关键字
    override def toString = {
        "stats: " + stats.toString + " NaN: " + missing
    }
}


object NAStatCounter extends Serializable {
    def apply(x: Double) = new NAStatCounter().add(x)
}


NAStatCounter类有两个成员变量: 
StatCounter类型的不可变变量stats和Long类型的可变变量missing。我们将类标记为Serializable,因为我们要在Spark RDD内部
使用该类的实例。如果Spark不能持久化其内部的数据,我们的作业会失败。


#分析上面的类
类的第一个方法add用于将一个新Double值加到由NAStatCounter跟踪的统计信息中,如果Double值是NaN,就表明值是缺失的。如果不是NaN,就把值加到底层的
StatCounter上。方法merge向当前实例加入了统计信息,它由另一个NAStatCounter实例跟踪。这两个方法都返回了this,这样方便它们链式串联起来。


最后我们覆盖了NAStatCounter的toString方法,这样就可以轻松地在Spark shell中打印出NAStatCounter的内容。


和类定义一起,我们为NAStatCounter定义了一个伴生对象(companion object)。Scala的object 关键字用于声明一个单例对象,该对象为类提供助手方法,类似
于Java类的static方法定义。
在Scala中,apply方法有一种特殊的语法糖,如下的代码功能相同:
scala> val nastats = NAStatCounter.apply(17.29)
nastats: NAStatCounter = stats: (count: 1, mean: 17.290000, stdev: 0.000000, max: 17.290000, min: 17.290000) NaN: 0


scala> val nastats = NAStatCounter(17.29)
nastats: NAStatCounter = stats: (count: 1, mean: 17.290000, stdev: 0.000000, max: 17.290000, min: 17.290000) NaN: 0


#登录spark-shell并加载scala文件
scala> :load /var/lib/hadoop-hdfs/spark_mllib/StatsWithCounter.scala
Loading /var/lib/hadoop-hdfs/spark_mllib/StatsWithCounter.scala...
import org.apache.spark.util.StatCounter
defined class NAStatCounter
defined module NAStatCounter
warning: previously defined class NAStatCounter is not a companion to object NAStatCounter.
Companions must be defined together; you may wish to use :paste mode for this.
警告提示我们伴生对象在shell使用的增量式编译模式下是不合法的。


#测试
scala> val nas1 = NAStatCounter(10.0)
nas1: NAStatCounter = stats: (count: 1, mean: 10.000000, stdev: 0.000000, max: 10.000000, min: 10.000000) NaN: 0


scala> nas1.add(2.1)
res58: NAStatCounter = stats: (count: 2, mean: 6.050000, stdev: 3.950000, max: 10.000000, min: 2.100000) NaN: 0


scala> val nas2 = NAStatCounter(Double.NaN)
nas2: NAStatCounter = stats: (count: 0, mean: 0.000000, stdev: NaN, max: -Infinity, min: Infinity) NaN: 1


scala> nas1.merge(nas2)
res59: NAStatCounter = stats: (count: 2, mean: 6.050000, stdev: 3.950000, max: 10.000000, min: 2.100000) NaN: 1


现在我们用新的NAStatCounter类来处理parsed RDD中MatchData记录的匹配分数。
每个MatchData实例包含一个Array[Double]类型的分值数组。
scala> val arr = Array(1.0, Double.NaN, 17.29)
scala> val nas = arr.map(d => NAStatCounter(d))


scala> nas.foreach(println)
stats: (count: 1, mean: 1.000000, stdev: 0.000000, max: 1.000000, min: 1.000000) NaN: 0
stats: (count: 0, mean: 0.000000, stdev: NaN, max: -Infinity, min: Infinity) NaN: 1
stats: (count: 1, mean: 17.290000, stdev: 0.000000, max: 17.290000, min: 17.290000) NaN: 0


RDD中每条记录都有自己的Array[Double],我们可以把它转换成另一个RDD,新RDD的每条记录是一个Array[NAStatCounter]。
#使用map对md的Array数组中每个值执行NAStatCounter
val nasRDD = parsed.map(md => {md.scores.map(d => NAStatCounter(d))}) 


我们需要一种简单的方式把多个Array[NAStatCounter]实例聚合到一个Array[NAStatCounter]中。
可以使用zip方法把两个具有相同长度的数组组合在一起生成一个新Array,新Array的元素由原来两个数组中具有相同下标的两个元素组成的元素对:
scala> val nas1 = Array(1.0, Double.NaN).map(d => NAStatCounter(d))
nas1: Array[NAStatCounter] = Array(stats: (count: 1, mean: 1.000000, stdev: 0.000000, max: 1.000000, min: 1.000000) NaN: 0, stats: (count: 0, mean: 0.000000, stdev: NaN, max: -Infinity, min: Infinity) NaN: 1)


scala> val nas2 = Array(Double.NaN, 2.0).map(d => NAStatCounter(d))
nas2: Array[NAStatCounter] = Array(stats: (count: 0, mean: 0.000000, stdev: NaN, max: -Infinity, min: Infinity) NaN: 1, stats: (count: 1, mean: 2.000000, stdev: 0.000000, max: 2.000000, min: 2.000000) NaN: 0)


scala> val merged = nas1.zip(nas2).map(p => p._1.merge(p._2))
merged: Array[NAStatCounter] = Array(stats: (count: 1, mean: 1.000000, stdev: 0.000000, max: 1.000000, min: 1.000000) NaN: 1, stats: (count: 1, mean: 2.000000, stdev: 0.000000, max: 2.000000, min: 2.000000) NaN: 1)




#使用更好理解的方式
scala> val merged = nas1.zip(nas2).map {case (a,b) => a.merge(b)}
merged: Array[NAStatCounter] = Array(stats: (count: 1, mean: 1.000000, stdev: 0.000000, max: 1.000000, min: 1.000000) NaN: 1, stats: (count: 1, mean: 2.000000, stdev: 0.000000, max: 2.000000, min: 2.000000) NaN: 1)




要在Scala集合的所有记录上执行相同的merge操作,可以使用reduce函数。reduce函数的输入是一个关联函数,该函数把两个T类型的参数映射为一个T类型
的返回值。reduce函数一遍又一遍地将关联函数应用到集合的所有元素,这样就把所有值都合并在一起了。
val nas = List(nas1, nas2)
val merged = nas.reduce((n1,n2) => {
    n1.zip(n2).map { case (a,b) =>a.merge(b)}
})


RDD类同样有一个reduce动作,它和我们之前使用的Scala集合上的reduce方法类似,只是作用的对象为分布在集群上的所有数据。
val reduced = nasRDD.reduce((n1,n2) => {
    n1.zip(n2).map {case (a,b) => a.merge(b)}
})
scala> reduced.foreach(println)
stats: (count: 5748125, mean: 0.712902, stdev: 0.388758, max: 1.000000, min: 0.000000) NaN: 1007
stats: (count: 103698, mean: 0.900018, stdev: 0.271316, max: 1.000000, min: 0.000000) NaN: 5645434
stats: (count: 5749132, mean: 0.315628, stdev: 0.334234, max: 1.000000, min: 0.000000) NaN: 0
stats: (count: 2464, mean: 0.318413, stdev: 0.368492, max: 1.000000, min: 0.000000) NaN: 5746668
stats: (count: 5749132, mean: 0.955001, stdev: 0.207301, max: 1.000000, min: 0.000000) NaN: 0
stats: (count: 5748337, mean: 0.224465, stdev: 0.417230, max: 1.000000, min: 0.000000) NaN: 795
stats: (count: 5748337, mean: 0.488855, stdev: 0.499876, max: 1.000000, min: 0.000000) NaN: 795
stats: (count: 5748337, mean: 0.222749, stdev: 0.416091, max: 1.000000, min: 0.000000) NaN: 795
stats: (count: 5736289, mean: 0.005529, stdev: 0.074149, max: 1.000000, min: 0.000000) NaN: 12843




#下面我们将缺失值分析代码打包为一个函数,放在StatsWithCounter.scala文件里,统计RDD[Array[Double]]所需要的统计信息:
StatsWithMissing.scala


import org.apache.spark.rdd.RDD
import org.apache.spark.util.StatCounter
class NAStatCounter extends Serializable {
    val stats: StatCounter = new StatCounter()
    var missing: Long = 0
    
    def add(x: Double): NAStatCounter = {
        if (java.lang.Double.isNaN(x)) {
            missing += 1
        } else {
            stats.merge(x)
        }
        this
    }   
    
    def merge(other: NAStatCounter): NAStatCounter = {
        stats.merge(other.stats)
        missing += other.missing
        this
    }
    
    // 覆盖方法必须加上override关键字
    override def toString = {
        "stats: " + stats.toString + " NaN: " + missing
    }
}


object NAStatCounter extends Serializable {
    def apply(x: Double) = new NAStatCounter().add(x)
}


def statsWithMissing(rdd: RDD[Array[Double]]): Array[NAStatCounter] = {
    val nastats = rdd.mapPartitions((iter: Iterator[Array[Double]]) => {
        val nas: Array[NAStatCounter] = iter.next().map(d => NAStatCounter(d))
        iter.foreach(arr => {
            nas.zip(arr).foreach { case (n,d) => n.add(d)}
        })
        Iterator(nas)
    })
    
    nastats.reduce((n1,n2) => {
        n1.zip(n2).map { case (a,b) => a.merge(b)}
    })
}


注意:在对输入RDD的每条记录生成Array[NAStatCounter]时,并不是调用map函数,而是调用更高级的mapPartitions函数。mapPartitions函数只用一个迭代器
Iterator[Array[Double]]处理输入RDD[Array[Double]]的一个分区中的所有记录。这样只要为每个数据分区创建一个Array[NAStatCounter],并用迭代器返回
的Array[Double]类型的值来更新Array[NAStatCounter]实例的状态就好了,这种实现的效率更高。




#变量的选择和评分简介
利用statsWithMissing函数,分析parsed RDD中匹配和不匹配记录的匹配分值数组的分布差异了。
scala> parsed.filter(_.matched).map(_.scores).take(1)
res21: Array[Array[Double]] = Array(Array(0.833333333333333, NaN, 1.0, NaN, 1.0, 1.0, 1.0, 1.0, 0.0))


val statsm = statsWithMissing(parsed.filter(_.matched).map(_.scores))
val statsn = statsWithMissing(parsed.filter(!_.matched).map(_.scores))


statsm和statsn这两个数组结构相同,但对应不同的数据子集:statsm包含匹配记录匹配分值数组的概要统计信息,而statsn对应不匹配记录分值数组的概要统计
信息。
scala> statsm.zip(statsn).map { case (m,n) => (m.missing + n.missing, m.stats.mean - n.stats.mean)}.foreach(println)
(1007,0.2854529057466859)
(5645434,0.09104268062279908)
(0,0.6838772482597568)
(5746668,0.8064147192926266)
(0,0.03240818525033451)
(795,0.7754423117834042)
(795,0.5109496938298719)
(795,0.7762059675300522)
(12843,0.9563812499852178)


分析:
一个好的特征有两个属性:
第一,对匹配记录和不匹配记录它的值往往差别很大(因此均值差别也很大)
第二,在数据中出现的频率高,这样我们才能指望它在任何一对记录里都有值


特征1作用不大,它缺失的情况很多,并且对匹配记录和非匹配记录它的均值差别也相对小。
特征4也不是特别有帮助,尽管它没有缺失情况,但对匹配记录和非匹配记录它的均值差别只有区区的0.03。
特征5和特征7就特别好,他们基本上对每对记录都有值,并且对匹配记录和非匹配记录它的均值差别也非常大。
特征2,特征6和特征8看起来也是有用的,他们在数据集中通常都有值,匹配记录和非匹配记录的均值差别也不小。
特征0和特征3就有点儿处于中间地带,特征0的区分度不太好,匹配记录和非匹配记录的均值只有0.28,但它在记录对中通常都有值。
特征3匹配记录和非匹配记录的均值差别大但却几乎总是缺失。


#现在我们使用一个简单的评分模型,该模型把记录对的相似度排序。相似度的计算为特征2,5,6,7和8的值相加,这些特征明显是好特征。
创建分数和匹配值RDD并评估在不同阈值下匹配记录和不匹配记录的分数差别:
case class MatchData(id1: Int, id2: Int, scores: Array[Double], matched: Boolean)
def naz(d: Double) = if (Double.NaN.equals(d)) 0.0 else d 
case class Scored(md: MatchData , score: Double)
val ct = parsed.map(md => {
    val score = Array(2,5,6,7,8).map(i => naz(md.scores(i))).sum
    Scored(md, score)
})


过滤阈值为4.0,这个值很高,意味着5个特征的平均值是0.8。我们过滤掉了几乎所有不匹配的记录,同时保留了超过90%的匹配记录:
ct.filter(s => s.score >= 4.0).map(s => s.md.matched).countByValue()


使用一个较低的阈值为2.0,我们可以捕捉所有已知的匹配记录,但代价是误报率高。
ct.filter(s => s.score >= 2.0).map(s => s.md.matched).countByValue()

尽管误报次数有点儿多,这个更宽松的过滤条件仍然过滤掉了90%的不匹配记录,而且保留了每个真正的匹配记录。

当然你也可以尝试着寻找更好的评分函数,这个评分可使用scores数组中的其他值(包括缺失的和非缺失的),它能区分出每个匹配的记录,但误报次数少于100。



0 0
原创粉丝点击