Spark简单数据分析---Spark学习笔记(2)

来源:互联网 发布:单片机芯片型号 编辑:程序博客网 时间:2024/06/16 20:10

上次学习Spark还是两个月前的事情,期中好多事情耽搁了,现在开始正式地学习Spark的使用。前面所学习的Scala基本知识也能用上了,终于可以从简单了解过渡到应用和实现的阶段。
这次学习的内容跟进的是《Spark高技术据分析》一章,里面的内容感觉很灵活,不仅是从最简单的Spark对数据的处理开始,而且其中稍带的Scala知识提及,可以加深对Scala的印象,从而运用起来就更加能了解其中的意义。
1.用Spark建立弹性分布式数据集(RDD)
弹性分布式数据集(Resilient Distributed Dataset):Spark所提供的基本抽象,代表分布在集群中多台机器上的对象集合。
SparkContext:负责协调集群上Spark作业的执行
有两种方式创建RDD:

  1. 基于外部数据源创建RDD
  2. 在一个过或多个RDD上执行转换操作来创建RDD,例如:过滤、汇总、关联等
    例如可以调用SparkContext如下方法创建一个自定义的RDD:
scala> val rdds=sc.parallelize(Array(1,2,3,4),4)

第一个参数代表并行化的对象集合,第二个参数代表分区的个数。分区就是数据集中的子集,Spark的并行单位就是分区。
也可以调用textFile方法来获取本地文件或者集群上的文件,来得到数据源:

scala> val rdd=sc.textFile("/home/coder-z/linkage")

如果路径是一个目录的话,会将目录中所有的文件数据作为RDD的输入。
假设我们是从文件读入数据的,那么得到的RDD中的数据基本单位是String,现在的问题就是如何将数据转换为我们想要的数据类型。
2.RDD数据处理
RDD动作:RDD的操作并不会导致集群的分布式计算,只有调用了action时分布式计算才会执行。
当我们得到一个RDD时,为了检验获得的数据集是否是指定文件中的,可以调用RDD的first方法:

scala> rdd.first

这里的first是一个方法,而不是字段。因为scala中,如果方法没有设置参数,那么可以直接省略括号。
现在执行如下动作:

val head=rdd.take(10)               //从rdd中抽取前10个数据记录head.fliter(!isHead(_)).foreach(println)    //过滤掉头信息,并输出

head是一个Array[String]类型的数组,调用filter方法用来过滤数组中的信息。这行代码体现了Scala作为函数式编程的简易性,尽可能使用函数作为一种“变量”来使用。其中的isHead方法定义如下:

def isHead(line:String)=line.contains("id_1")

是一个返回Boolean类型的函数,Scala申明函数时可以不申明返回类型,Scala会自行推断,但是如果函数情况比较复杂,Scala不一定能够推断出函数的返回值类型。
foreach方法接收一个函数println作为参数对数据集上的每个元素进行println定义的处理。而每个元素就作为参数,传入了println函数中。
同样地,数据集中的每个元素作为参数也传入isHead中,可以用占位符”_”来表示传入的元素。当然,如下代码也做相同处理:

head.filter(x=>!isHead(x)).foreach(println)

x就代表正在处理的元素,交给isHead进行处理。

3.使用元组和case class进行数据结构化*
现在我们要将获得的字符串解析成所需要的格式,先来看一下我们需要的数据格式。

607,53170,1,?,1,?,1,1,1,1,1,TRUE

这是数据集中其中的一条记录,根据数据集的标题信息,我们得到格式:

(Int,Int,Double,Double,Double,Double,Double,Double,Double,Double,Double,Boolean)

其中记录里会有”?”数值,这是不确定的信息,应该在格式转化的时候过滤为”NaN”,这样的数据过滤在之后对数据集的分析中也是需要的。
先简单定义一个能够把第3-11个数据转换成Double的函数:

scala> def toDoubles(s:String)={     if("?".equals(s)) Double.NaN else s.toDouble    }

再利用Scala中的case class 语法,我们可以自定义一条数据记录的格式:

case class MatchData(id_1:Int,id_2:Int,scores:Array[Double],matched:Boolean)

运用MatchData作为一条记录,可以直接使用传入的参数名作为字段名调用。
例如:matchData.id_1,而不是matchData(0)这样用序号使用了。
接着,我们就指定定义一个结构化一整条记录的函数:

def parse(line:String)={    val pieces=line.split(",")    val scores=pieces.slice(2,11).map(toDoubles)    val id_1=pieces(0).toInt    val id_2=pieces(1).toInt    val matched=pieces(11).toBoolean    MatchData(id_1,id_2,scores,matched)}

如下代码运用parse进行对数据集的结构化:

 val mas=head.filter(!isHead(_)).map(line=>parse(line))

map函数是对RDD数据集合上的所有数据进行处理。

RDD缓存:
对于parse的调用,parse只会在RDD执行某种输出调用时才会真正调用,所以每一次的这类调用parse都会执行一遍。因此需要有cache方法持久化数据。
Spark提供了缓存RDD的机制,RDD的cache方法调用后,会指示在下一次的RDD计算后对RDD进行存储。例如下面的示例:

cached.cache()cached.count()cached.take(10)

RDD的第一次计算出现在count()方法中,所以当count()方法调用完后,RDD会被储存起来,之后的take方法获得的就是已经经过储存的RDD,而不是从本地取出重新计算一次的RDD。

4.概要信息
关于前面获取的数据,我们已经做到可以将它们结构化为我们想要的格式了,但是里面还是稍微有一点点的瑕疵就是数据记录并不是完善的,其中的”?”符号代表数据记录项的缺失,那么我们如何统计我们的数据集的概要信息?
这里我们就要用到Spark提供的StatCounter对象生成统计概要信息,我们自定义一个NAStatCounter如下:

import org.apache.spark.rdd.RDDimport org.apache.spark.util.StatCounterclass NAStatCounter extends Serializable {  val stats:StatCounter=new StatCounter()    //StatCount对象,用来生成概要信息  var missing:Long=0                         //统计NaN的个数  def add(x:Double): NAStatCounter = {           if(java.lang.Double.isNaN(x)) {                missing=missing+1                        }else{      stats.merge(x)                         //向当前实例stats加入统计信息    }    this  }  def merge(others:NAStatCounter):NAStatCounter= { //加入一组的概要信息      stats.merge(others.stats)      missing+=others.missing      this  }  override def toString: String = {    "stats:"+stats.toString()+"NaN:"+missing  }object NAStatCounter extends Serializable {  def apply(x:Double)=new NAStatCounter().add(x)}
val arr=Array(1.0,Double.NaN,17.29)     // 自定义的新数组val nas=arr.map(d=>NAStatCounter(d))        //对数组每个元素进行概要信息统计

这个时候我们也可以做到将多个Array[NAStatCount]h合并到一起,使用zip方法就可以将两个相同长度的数组进行合并。如下所示:

val nas1=Array(1.0,Double.NaN,20.0).map(d=>NAStatCounter(d))val nas2=Array(Double.NaN,2.0,20.7).map(d=>NAStatCounter(d))val merged=nas1.zip(nas2).map{case(a,b)=>a.merge(b)}

如果要在Scala集合上的所有记录执行merge操作,使用reduce方法就可以代替。
reduce 使用关联函数,把集合中两两为T类型的元素映射为一个T类型的元素。
由此,我们对上面的代码段进行修改,使用reduce方法代替:

val nas=List(nas1,nas2)val merge=nas.reduce((n1,n2)=>{    n1.zip(n2).map{(a,b)=>a.merge(b)}})

现在用一段精炼的函数,把对RDD数据集上所有数据记录进行概要信息统表示出来:

def statWithMissing(rdd:RDD[Array[Double]]):Array[NAStatCounter]={    val natats=rdd.mapPartitions((iter:Iterator[Array[Double]])=>{    val nas=iter.next().map(d=>NAStatCounter(d))    iter.foreach(arr=>{                             //对每一个Array[Double]操作        nas.zip(arr).foreach{(n,d)=>n.add(d)}   //对Array[NAStatCounter]操作    })    Iterator(nas)                              //最终返回一个Array[NAStatCounter]}) natats.reduce((n1,n2)=>{        n1.zip(n2).map{case(a,b)=>a.merge(b)}  //将所有NAStatCounter做聚合处理        })}

我觉得以上这段代码应该多多分析分析透,因为它的风格是很多Spark程序都具有的,并且能够表达出函数式编程的风格所在。


终于等到寒假的第一天开始继续研究研究Spark了,前面事情太多,没有空闲把重心放在Spark上。这也是今年的第一篇吧,曾经看到别人将一礼拜更新一篇博客为目标,那个时候还觉得好像很轻松吧?现在看来根本不是啊,上个月劈头盖脸瞎忙,也没能做到一个礼拜写一次(两个礼拜一次都没有!),接下来要边学着用Spark一边把KMeans写出来,然后还要配合上Hadoop集群,感觉任务也不小呢,加油吧!

0 0