基於Spark的學生成績分析系統

本文首發於我的個人博客QIMING.INFO,轉載請帶上鍊接及署名。

本文是本人碩士期間雲計算課程的一次大作業,所以可能部分內容有充字數的嫌疑,還望各位看官無視。。。但是也正因爲此,本文對一些基礎概念描述的也挺詳細,包括但不限於Spark簡介、Spark與Hadoop對比、Spark架構介紹、Pearson相關係數簡介、Spark中的combineByKey函數簡介、Spark中提交併運行作業的方法等。

問題說明

提出問題及目標

學生成績是評價學生學習效果和老師教學效果的重要指標,如何充分利用已有的學生成績數據,發現其中的規律,提高教學質量是廣大教育工作者普遍關注的問題。

一般而言,學生各科成績之間或多或少都存在聯繫,例如一個學習好的學生各科成績普遍都比較高。研究者們對此進行了大量的數據採集、統計工作,從他們的研究結果來看,學生各科成績之間的確存在一定的相關性,只是不同課程之間相關性的強弱不同而已。

通過對學生成績進行統計分析,可以發現學生成績中隱藏的課程關聯規則和模式,這些知識可以幫助老師更加合理地安排教學內容,從而對教學起到促進作用。

現有西工大某學院2006級至2015級學生的全部成績,我們想利用這些數據統計出:①各學科的整體情況;②各課程之間的相關性

目標細化及難點

經過討論,我們決定用每個年級的各科平均成績來反映該學科的整體情況,並分年級計算各個課程之間的Pearson相關係數以反映各課程間的相關性。並最後將分析的結果保存在HDFS上。

要做到這些工作,我們需要解決以下難點:
① 2006至2015學生人數衆多,且課程種類多,爲分析帶來了極大困難
② 用單機運行處理大量數據需要大量的時間
所以我們便考慮使用分佈式計算引擎Spark來克服這些難點。

背景

Spark簡介

Spark是專爲大規模數據處理而設計的快速通用的計算引擎,最初在2009年由加州大學伯克利分校的AMP實驗室用Scala語言開發,並於2010年成爲Apache的開源項目。

Spark是基於MapReduce算法實現的分佈式計算,擁有Hadoop MapReduce所具有的優點;但不同於MapReduce的是中間過程的輸出和結果可以保存在內存中,從而不再需要讀寫HDFS,大大提高了速度。

Spark和Hadoop的關係圖如下:

官方資料介紹,和Hadoop相比,Spark可以讓你的程序在內存中運行時速度提升100倍,就算在硬盤上,運行速度也能提升10倍。

Spark架構

Spark的架構示意圖如下:

如圖可見,Spark主要有以下模塊:
① Spark Core:包含Spark的基本功能;尤其是定義RDD的API、操作以及這兩者上的動作。其他Spark的庫都是構建在RDD和Spark Core之上的。
② Spark SQL:提供通過Apache Hive的SQL變體Hive查詢語言(HiveQL)與Spark進行交互的API。每個數據庫表被當做一個RDD,Spark SQL查詢被轉換爲Spark操作。
③ Spark Streaming:對實時數據流進行處理和控制。Spark Streaming允許程序能夠像普通RDD一樣處理實時數據
④ MLlib:一個常用機器學習算法庫,算法被實現爲對RDD的Spark操作。這個庫包含可擴展的學習算法,比如分類、迴歸等需要對大量數據集進行迭代的操作。
⑤ GraphX:控制圖、並行圖操作和計算的一組算法和工具的集合。GraphX擴展了RDD API,包含控制圖、創建子圖、訪問路徑上所有頂點的操作。

Spark RDD簡介

RDD(Resilient Distributed Dataset)即彈性分佈式數據集。

RDD是Spark的核心,在Spark中,對數據的所有操作不外乎創建RDD、轉化已有RDD以及調用RDD操作進行求值。每個RDD都被分爲多個分區,這些分區運行在集羣中的不同節點上。RDD可以包含Python、Java、Scala中任意類型的對象,甚至可以包含用戶自定義的對象。

RDD是Spark中的抽象數據結構類型,任何數據在Spark中都被表示爲RDD。從編程的角度來看,RDD可以簡單看成是一個數組。和普通數組的區別是,RDD中的數據是分區存儲的,這樣不同分區的數據就可以分佈在不同的機器上,同時可以被並行處理。因此,Spark應用程序所做的無非是把需要處理的數據轉換爲RDD,然後對RDD進行一系列的變換和操作從而得到結果。

更多內容可參考本站的這篇文章Spark RDD的簡單使用

Pearson相關係數簡介

Pearson相關係數 (Pearson Correlation Coefficient)是用來衡量兩個數據集合是否在一條線上面,它用來衡量定距變量間的線性關係。Pearson是一個介於-1和1之間的值,用來描述兩組線性的數據一同變化移動的趨勢。

Pearson係數的計算公式如下:

注:Cov(X,Y)表示X和Y的協方差,E(X ) 表示X的平均值。

Pearson 相關係數大小的含義如下表所示:

相關性 負相關 正相關
-0.09 至 0.0 0.0 至 0.09
-0.3 至 -0.1 0.1 至 0.3
-0.5 至 -0.3 0.3 至 0.5
-1.0 至 -0.5 0.5 至 1.0

實現步驟

源數據是在MySQL數據庫中存儲的,所以在進行統計分析工作前要先將數據從MySQL數據庫中讀取出來。

平均成績分析

取數據

根據課程id取出每個年級的各個學生此門課程的成績,SQL語句如下:

def sqlStr(courseId: Int): String = {
  s"""SELECT c.gradeId,sc.score FROM tb_student st
      LEFT JOIN tb_score sc ON st.studentId = sc.studentId
      LEFT JOIN tb_class c  ON st.classId = c.classId
      WHERE sc.courseId = $courseId
      ORDER BY sc.studentId"""
}

轉化數據

從MySQL中取得的數據在Spark中是DataFrame類型的,爲了方便計算,我們需要將其轉化爲一個鍵值對RDD,鍵爲“科目+年級”,值爲每個學生成績,如( C語言2006級,89.0),代碼如下:

val scoreRdd = scoreDataFrame.map(x => (courseStr(courseId) + gradeStr(x.getLong(0).toInt), x.getDecimal(1).doubleValue)).rdd

計算平均成績

調用RDD的combineByKey()方法來計算平均數,此方法先將相同鍵的值加起來,之後除以這個鍵的個數,得到這個鍵對應的平均成績,代碼如下:

val averageScoreRdd = scoreRdd.combineByKey(
  createCombiner = (v: Double) => (v: Double, 1),
  mergeValue = (c: (Double, Int), v: Double) => (c._1 + v, c._2 + 1),
  mergeCombiners = (c1: (Double, Int), c2: (Double, Int)) => (c1._1 + c2._1, c1._2 + c2._2),
  numPartitions = 3
).map { case (k, v) => (k, v._1 / v._2) }

代碼說明:

  • createCombiner即當遇到一個新鍵時,創建一個此鍵對應的累加器,如第一次遇到(C語言2006級,89.0)時,創建(C語言2006級,(89.0,1));
  • mergeValue即當遇到之前已遇到過的鍵時,將該鍵的累加器與當前值合併,如再遇到(C語言2006級,90.5)時,合併爲(C語言2006級,(179.5,2));
  • mergeCombiners即將各個分區的結果進行合併,得到每門課程對應不同年級的總成績和總人數,如(C語言2006級,(19173.7,241));
  • numPartitions即表示Spark分了3個分區來並行處理數據;

代碼最後一行的map即爲將每個鍵的累加器的總成績除以總人數,得到一個值爲平均成績的RDD,如(C語言2006級,79.559)。

保存結果

調用saveAsTextFile()方法將計算出的平均成績存儲在HDFS中,每個科目的平均成績保存在用該科目名命名的文件夾中,代碼如下:

averageScoreRdd.sortByKey().saveAsTextFile("hdfs://localhost:9000/user/hadoop/output/score-avg/" + courseStr(courseId))

相關性分析

取數據

根據年級id取出當前年級每個學生所有科目的成績,SQL語句如下:

def allScoreSqlStr(gradeId: Int): String = {
  s"""SELECT
        MAX(case cou.courseId when '1' THEN IFNULL(sc.score, 0) END) '高等數學',
        MAX(case cou.courseId when '2' THEN IFNULL(sc.score, 0) END) '外語',
        MAX(case cou.courseId when '3' THEN IFNULL(sc.score, 0) END) '離散數學',
        MAX(case cou.courseId when '4' THEN IFNULL(sc.score, 0) END) 'C語言' ,
        MAX(case cou.courseId when '5' THEN IFNULL(sc.score, 0) END) '數據結構',
        MAX(case cou.courseId when '6' THEN IFNULL(sc.score, 0) END) '組成原理',
        MAX(case cou.courseId when '7' THEN IFNULL(sc.score, 0) END) '操作系統'
      FROM  tb_student st
      LEFT JOIN tb_score sc on st.studentId = sc.studentId
      LEFT JOIN tb_course cou on cou.courseId = sc.courseId
      LEFT JOIN tb_class cla ON  st.classId = cla.classId
      WHERE cla.gradeId = $gradeId
      GROUP BY st.studentId
      ORDER BY st.studentId"""
}

轉化數據

先將此DataFrame類型的數據轉化成數值類型的RDD:

val data = scoreDataFrame.map(x => x.toString).rdd.
  map(x => x.substring(1, x.length - 1).split(",")).
  map(x => x.map(x => x.toDouble))

計算Pearson相關係數可以調用Spark MLlib庫中的方法,但是該方法要求RDD爲向量類型的RDD,所以繼續轉化:

val data1 = data.map(x => Vectors.dense(x))

計算Pearson相關係數

直接調用Spark MLlib庫中的Statistics.corr方法進行計算:

val corr = Statistics.corr(data1, "pearson")

保存結果

經過上述計算,得到的corr是一個矩陣,我們想將結果保存成CSV以便於查看,所以應先將corr轉化成一個數值類型的RDD,再將RDD轉化成DataFrame以便於保存爲CSV文件,最後將結果保存到HDFS上,每個年級的各科相關係數保存在以該年級命名的文件夾中,代碼如下:

val tmpRdd = sc.parallelize(corr.rowIter.toArray.map(x => x.toArray)).map(x => (x(0), x(1), x(2), x(3), x(4), x(5), x(6)))
val tmpDF = tmpRdd.toDF("高等數學", "外語", "離散數學", "C語言", "數據結構", "組成原理", "操作系統")
tmpDF.write.format("csv").save("hdfs://localhost:9000/user/hadoop/output/score-pearson/" + gradeStr(gradeId))

提交運行

代碼編寫完成後,我們需要將代碼打包成jar文件(打包過程略),然後提交到spark集羣上運行,有以下三步:

啓動master

進入spark的安裝目錄,輸入:

./sbin/start-master.sh

啓動worker

./bin/spark-class org.apache.spark.deploy.worker.Worker spark://localhost:7077  

這裏的地址爲:啓動master後,在瀏覽器輸入localhost:8080,查看到的master地址:

啓動成功後,用jps查看進程:

因本實驗中還用到了HDFS,所以也必須啓動它(使用start-dfs.sh),啓動後再用jps查看一下進程:

提交作業

./bin/spark-submit --master spark://localhost:7077 --class ScoreAnalysis /home/hadoop/scoreanalysis.jar

注:這裏將打包好的jar包放在了/home/hadoop/目錄下

可以在4040端口查看作業進度:

結果展示

上文說過,我們將結果保存在了HDFS中,所以當程序運行完成後,要查看結果,必須用Hadoop HDFS提供的命令或者進入namenode管理頁面進行查看。

在控制檯上輸入以下命令:

$ hadoop fs –ls /user/hadoop/output/

結果如下圖:

爲了便於查看,我們也可以進入namenode管理頁面。

查看平均成績

進入score-avg文件夾,可以看到每個科目創建了一個文件夾。

進入外語文件夾,可以看到有四個文件,_SUCCESS表示文件存儲成功,其他三個文件即Spark保存時有三個分區,分別進行了保存。

爲了方便查看,我們使用cat命令,將所有文件內容整合到一個本地文件中,如下圖所示:

查看Pearson相關係數

進入score-pearson文件夾,可以看到,我們將不同課程的相關性按年級分成了不同的文件夾。

以2006級爲例說明,進入2006級文件夾,看到5個文件,_SUCCSESS說明保存成功,其餘四個文件即Spark保存時有四個分區,分別進行了保存。

同樣,我們用cat命令將其合併到一個文件中,即在控制檯中輸入:

hadoop fs -cat /user/hadoop/output/score-pearson/2006級/part-* >  //home/hadoop/score-pearson/2006級.csv

之後打開2006級.csv,便能清晰的看到2006級學生成績各科的pearson相關係數了:

從圖中可以看出,組成原理和操作系統的Pearson相關係數最高,達到了0.62,說明組成原理和操作系統的相關性較強。

總結

在本次實驗中,我們小組共同合作,完成了用Spark進行對西工大某學院學生2006級至2015級各課程的成績統計與分析。

在這個過程中,我們學習到了Hadoop、Spark環境的搭建,Spark RDD的使用,Scala語言的用法,以及分佈式開發的思想,併成功得出了各課程的平均成績與相關性。

這次實踐我們收穫到了很多,但由於能力與時間有限,本實驗還有很多可以改進的地方。以後還有很多值得學習與研究的地方,我們會再接再厲,努力做得更好。

附完整代碼

import java.util.Properties

import org.apache.spark.mllib.linalg.Vectors
import org.apache.spark.mllib.stat.Statistics
import org.apache.spark.sql.SparkSession

/**
  * xu qi ming
  * 2018.06.15
  */
object ScoreAnalysis {

  /**
    * 根據課程id查找每個學生的成績和所在年級的SQL語句
    *
    */
  def sqlStr(courseId: Int): String = {
    s"""SELECT c.gradeId,sc.score FROM tb_student st
          LEFT JOIN tb_score sc ON st.studentId = sc.studentId
          LEFT JOIN tb_class c ON  st.classId = c.classId
          WHERE sc.courseId = $courseId
          ORDER BY sc.studentId"""
  }

  /**
    * 查找當前年級每個學生所有科目的成績的SQL語句
    *
    */
  def allScoreSqlStr(gradeId: Int): String = {
    s"""SELECT
          MAX(case cou.courseId when '1' THEN IFNULL(sc.score, 0) END) '高等數學',
          MAX(case cou.courseId when '2' THEN IFNULL(sc.score, 0) END) '外語',
          MAX(case cou.courseId when '3' THEN IFNULL(sc.score, 0) END) '離散數學',
          MAX(case cou.courseId when '4' THEN IFNULL(sc.score, 0) END) 'C語言' ,
          MAX(case cou.courseId when '5' THEN IFNULL(sc.score, 0) END) '數據結構',
          MAX(case cou.courseId when '6' THEN IFNULL(sc.score, 0) END) '組成原理',
          MAX(case cou.courseId when '7' THEN IFNULL(sc.score, 0) END) '操作系統'
        FROM  tb_student st
        LEFT JOIN tb_score sc on st.studentId = sc.studentId
        LEFT JOIN tb_course cou on cou.courseId = sc.courseId
        LEFT JOIN tb_class cla ON  st.classId = cla.classId
        WHERE cla.gradeId = $gradeId
        GROUP BY st.studentId
        ORDER BY st.studentId"""
  }


  /**
    * 將年級id轉換成字符串
    *
    */
  def gradeStr(gradeId: Int): String = {
    gradeId match {
      case 1 => "2006級"
      case 2 => "2007級"
      case 3 => "2008級"
      case 4 => "2009級"
      case 5 => "2010級"
      case 6 => "2011級"
      case 7 => "2012級"
      case 8 => "2013級"
      case 9 => "2014級"
      case 10 => "2015級"
    }
  }

  /**
    * 將課程id轉換成相應課程名字符串
    *
    */
  def courseStr(corseId: Int): String = {
    corseId match {
      case 1 => "高等數學"
      case 2 => "外語"
      case 3 => "離散數學"
      case 4 => "C語言"
      case 5 => "數據結構"
      case 6 => "組成原理"
      case 7 => "操作系統"
    }
  }

  def main(args: Array[String]): Unit = {

    // 生成SparkSession對象
    val spark = new SparkSession.Builder().appName("ScoreAnalysis").getOrCreate()
    //生成SparkContext對象
    val sc = spark.sparkContext

    //建立與mysql數據庫的連接
    val connProperties = new Properties()
    connProperties.put("driver", "com.mysql.jdbc.Driver")
    connProperties.put("user", "root")
    connProperties.put("password", "Root_1234")
    connProperties.put("fetchsize", "100")

    import spark.implicits._

    /**
      * 計算指定課程的每個年級的平均成績
      *
      */
    def averageScore(courseId: Int): Unit = {

      //連接數據庫,將取得的成績保存成DataFrame
      val scoreDataFrame = spark.read.jdbc(
        "jdbc:mysql://localhost:3306/db_score",
        s"(${sqlStr(courseId)}) as table01",
        connProperties)

      //將DataFrame轉化成一個鍵值對RDD,鍵是科目+年級,值是每個學生的成績
      val scoreRdd = scoreDataFrame.map(x => (courseStr(courseId) + gradeStr(x.getLong(0).toInt), x.getDecimal(1).doubleValue)).rdd

      //計算每個年級的平均成績
      val averageScoreRdd = scoreRdd.combineByKey(
        createCombiner = (v: Double) => (v: Double, 1),
        mergeValue = (c: (Double, Int), v: Double) => (c._1 + v, c._2 + 1),
        mergeCombiners = (c1: (Double, Int), c2: (Double, Int)) => (c1._1 + c2._1, c1._2 + c2._2),
        numPartitions = 3
      ).map { case (k, v) => (k, v._1 / v._2) }

      //保存到HDFS中
      averageScoreRdd.sortByKey().saveAsTextFile("hdfs://localhost:9000/user/hadoop/output/score-avg/" + courseStr(courseId))
    }

    //循環這7門課,求每個課程每個年級的平均成績
    for (i <- 1 to 7) {
      averageScore(i)
    }

    /**
      * 計算指定年級的所有科目的pearson相關係數
      *
      */
    def pearsonCorr(gradeId: Int): Unit = {

      //連接數據庫,將取得的成績保存成DataFrame
      val scoreDataFrame = spark.read.jdbc(
        "jdbc:mysql://localhost:3306/db_score",
        s"(${allScoreSqlStr(gradeId)}) as table01",
        connProperties)

      //將DataFrame轉成數值類型的RDD
      val data = scoreDataFrame.map(x => x.toString).rdd.
        map(x => x.substring(1, x.length - 1).split(",")).
        map(x => x.map(x => x.toDouble))

      //將數值RDD轉爲Vector類型的RDD
      val data1 = data.map(x => Vectors.dense(x))

      //調用機器學習庫的統計學習中的算法計算pearson相關係數
      //將結果返回成一個矩陣
      val corr = Statistics.corr(data1, "pearson")

      //保存計算的結果到HDFS
      val tmpRdd = sc.parallelize(corr.rowIter.toArray.map(x => x.toArray)).map(x => (x(0), x(1), x(2), x(3), x(4), x(5), x(6)))
      val tmpDF = tmpRdd.toDF("高等數學", "外語", "離散數學", "C語言", "數據結構", "組成原理", "操作系統")
      tmpDF.write.format("csv").save("hdfs://localhost:9000/user/hadoop/output/score-pearson/" + gradeStr(gradeId))
    }

    //循環這10個年級,求每個年級的各科相關係數情況
    for (i <- 1 to 10) {
      pearsonCorr(i)
    }
  }

}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章