pyspark入門---機器學習實戰預測嬰兒出生率(一)使用MLlib庫


機器學習是通過算法對訓練數據構建出模型並對模型進行評估,評估的性能如果達到要求就拿這個模型來測試其他的數據,如果達不到要求就要調整算法來重新建立模型,再次進行評估,如此循環往復,最終獲得滿意的經驗來處理其他的數據的過程。

簡單點講,機器學習就是通過一定的模型,讓計算機可以從大量的數據中學習到相關的知識,然後利用學習來的知識來預測以後的未知事物。
image-20200512223510996

image-20200512223408251

首先看一下MLlib的構成:

MLlib包涉及數據科學任務的衆多方面,其核心功能涉及以下三個方面:

1、數據準備:包括用於特徵提取和變換、分類特徵的散列和導入預言模型標記語言構建的模型

2、常見算法:包括流行的迴歸、頻繁模式挖掘、分類和聚類算法

3、實用功能:實現了常用的統計方法和模型評估方法

image-20200512205707752

MLlib主要是爲RDD和Dstream設計的,我們這裏爲了便於數據的轉換,將數據格式轉換成DataFrame格式。在Spark SQL中,我們瞭解了創建DataFrame的兩種方式,這裏採用指定數據集schema的方式。

案列:

1.加載數據

數據地址:
鏈接:https://pan.baidu.com/s/1xgSiJZyWeb9km-GJP7ZVmQ
提取碼:pu0f

對於DataFrame的格式,我們先指定Schema格式,
labels變量是指定的數據屬性和數據類型組成元組的列表
schema爲數據架構,即結構
births爲讀取的csv文件數據
指定的格式是在對csv文件中的數據進行研讀的基礎上設計的
header 參數指定爲True 表示源文件中有頭信息,也就是有屬性行,我們用schema 指定數據的正確類型。

from pyspark.sql import SparkSession
from pyspark.sql.types import *
#選擇標籤
labels = [
    ('INFANT_ALIVE_AT_REPORT', StringType()),
    ('BIRTH_YEAR', IntegerType()),
    ('BIRTH_MONTH', IntegerType()),
    ('BIRTH_PLACE', StringType()),
    ('MOTHER_AGE_YEARS', IntegerType()),
    ('MOTHER_RACE_6CODE', StringType()),
    ('MOTHER_EDUCATION', StringType()),
    ('FATHER_COMBINED_AGE', IntegerType()),
    ('FATHER_EDUCATION', StringType()),
    ('MONTH_PRECARE_RECODE', StringType()),
    ('CIG_BEFORE', IntegerType()),
    ('CIG_1_TRI', IntegerType()),
    ('CIG_2_TRI', IntegerType()),
    ('CIG_3_TRI', IntegerType()),
    ('MOTHER_HEIGHT_IN', IntegerType()),
    ('MOTHER_BMI_RECODE', IntegerType()),
    ('MOTHER_PRE_WEIGHT', IntegerType()),
    ('MOTHER_DELIVERY_WEIGHT', IntegerType()),
    ('MOTHER_WEIGHT_GAIN', IntegerType()),
    ('DIABETES_PRE', StringType()),
    ('DIABETES_GEST', StringType()),
    ('HYP_TENS_PRE', StringType()),
    ('HYP_TENS_GEST', StringType()),
    ('PREV_BIRTH_PRETERM', StringType()),
    ('NO_RISK', StringType()),
    ('NO_INFECTIONS_REPORTED', StringType()),
    ('LABOR_IND', StringType()),
    ('LABOR_AUGM', StringType()),
    ('STEROIDS', StringType()),
    ('ANTIBIOTICS', StringType()),
    ('ANESTHESIA', StringType()),
    ('DELIV_METHOD_RECODE_COMB', StringType()),
    ('ATTENDANT_BIRTH', StringType()),
    ('APGAR_5', IntegerType()),
    ('APGAR_5_RECODE', StringType()),
    ('APGAR_10', IntegerType()),
    ('APGAR_10_RECODE', StringType()),
    ('INFANT_SEX', StringType()),
    ('OBSTETRIC_GESTATION_WEEKS', IntegerType()),
    ('INFANT_WEIGHT_GRAMS', IntegerType()),
    ('INFANT_ASSIST_VENTI', StringType()),
    ('INFANT_ASSIST_VENTI_6HRS', StringType()),
    ('INFANT_NICU_ADMISSION', StringType()),
    ('INFANT_SURFACANT', StringType()),
    ('INFANT_ANTIBIOTICS', StringType()),
    ('INFANT_SEIZURES', StringType()),
    ('INFANT_NO_ABNORMALITIES', StringType()),
    ('INFANT_ANCEPHALY', StringType()),
    ('INFANT_MENINGOMYELOCELE', StringType()),
    ('INFANT_LIMB_REDUCTION', StringType()),
    ('INFANT_DOWN_SYNDROME', StringType()),
    ('INFANT_SUSPECTED_CHROMOSOMAL_DISORDER', StringType()),
    ('INFANT_NO_CONGENITAL_ANOMALIES_CHECKED', StringType()),
    ('INFANT_BREASTFED', StringType())
]
schema = StructType([StructField(e[0],e[1],False) for e in labels])
#讀取csv要根據標籤對應
births = spark.read.csv("data/births_train.csv",header=True,schema=schema)

由於特徵太多,去除不需要的,我們的目標是預測 ‘INFANT_ALIVE_AT_REPORT’ 是 1 or 0,嬰兒是否存活。
因此,我們要去除其他與嬰兒無關的特徵。利用.select()方法提取與預測指標相關的列。

selected_feas = ["INFANT_ALIVE_AT_REPORT",
        "BIRTH_PLACE",
        "MOTHER_AGE_YEARS",
        "FATHER_COMBINED_AGE",
        "CIG_BEFORE",    
        "CIG_1_TRI",
        "CIG_2_TRI",
        "CIG_3_TRI",
        "MOTHER_HEIGHT_IN",    
        "MOTHER_PRE_WEIGHT",
        "MOTHER_DELIVERY_WEIGHT",
        "MOTHER_WEIGHT_GAIN",
        "DIABETES_PRE",
        "DIABETES_GEST",
        "HYP_TENS_PRE",
        "HYP_TENS_GEST",
        "PREV_BIRTH_PRETERM",
        ]

births_trim = births.select(selected_feas)

此處我們需要做一個特徵字典映射:

0意味着母親在懷孕前或懷孕期間不抽菸;

1-97表示抽菸的實際人數,

98表示98或更多;

而99表示未知,我們將假設未知是0並相應地重新編碼。

recode()方法從recode_dictionary中返回key對應的值,

correct_cig方法檢查特徵feat的值何時不等於99,若不等於99,則返回特徵的值;如果這個值等於99,則返回0。

我們不能直接在DataFrame上使用recode函數;它需要轉換爲Spark理解的UDF。User Define Function, 用戶自定義函數,簡稱UDF,用戶可以在Spark SQL 裏自定義實際需要的UDF來處理數據。

rec_integer函數:通過傳入我們指定的recode函數並指定返回值數據類型,我們可以使用rec_integer做字典映射,傳入參數爲recode函數並指定返回值數據類型。

image-20200513072447409

import pyspark.sql.functions as func

def recode(col,key):
    return recode_dictionary[key][col] 
  
# correct_cig方法檢查特徵feat的值何時不等於99不等於99,則返回特徵的值,等於99,則返回0
def correct_cig(feat):
    return func.when(func.col(feat) != 99, func.col(feat)).otherwise(0)

 
#更正與吸菸相關的特徵.withColumn(…) 方法第一個參數是新列名,第二個參數是指定原數據的某列。

births_transformed = births_trim.withColumn("CIG_BEFORE",correct_cig("CIG_BEFORE"))  \
    .withColumn("CIG_1_TRI",correct_cig("CIG_1_TRI")) \
    .withColumn("CIG_2_TRI",correct_cig("CIG_2_TRI")) \
    .withColumn("CIG_3_TRI",correct_cig("CIG_3_TRI"))
    
#閱讀數據時會發現數據集中有很多特徵是字符串,進行機器學習前需要將其轉換爲數值形式,經過閱讀數據,可以發現數據中的字符串以U/N/Y三種數值存在依次表示unknown,no,yes,因此這裏可以採用字典映射的方式。
recode_dictionary = {"YNU":{"Y":1,"N":0,"U":0}}


#transform the function recode into UDF which could be used in spark
rec_integer = func.udf(recode,IntegerType()) 

#然後找出哪些特徵是Yes/No/Unknown : 
# 我們先創建一個元素爲包含列名和數據類型元祖(cols)的列表
# 遍歷這個列表,計算所有字符串列的不同值,如果Y在返回的列表中,將列名追加到YNU_cols列表。
#將字符型UYN改爲數值型
cols = [(col.name, col.dataType) for col in births_trim.schema]
YNU_cols = []
for i,s in enumerate(cols):
    if s[1] == StringType():
        dis = births.select(s[0]).distinct().rdd.map(lambda x:x[0]).collect()
        if "Y" in dis:
            YNU_cols.append(s[0])
#批量特徵轉換
exprs_YNU = [rec_integer(x,func.lit("YNU")).alias(x) if x in YNU_cols else x for x in births_transformed.columns]    

births_transformed = births_transformed.select(exprs_YNU)
#births_transformed.select(YNU_cols[-5:]).show(5)
#散列技巧將字符串轉化爲數值類型特徵
#create dataset for model predict
#translate dataframe to LabeledPoint RDD

加載數據完成後,再進行數據的探索,首先導入pyspark.mllib包中的stat統計分析相應的模塊,

然後指定要選擇的特徵放在特徵列表numeric_cols中

使用select方法選擇相應的特徵,轉化爲RDD並進行map處理

最後對選定的特徵進行統計分析,包括求均值、方差等等

import pyspark.mllib.feature as ft
import pyspark.mllib.regression as reg
hashing = ft.HashingTF(7)
births_hashed = births_transformed.rdd.map(lambda row:[list(hashing.transform(row[1]).toArray()) if col == "BIRTH_PLACE" else row[i] for i,col in enumerate(features_to_keep)]) \
    .map(lambda row:[[e] if type(e) == int else e for e in row]) \
    .map(lambda row:[item for sublist in row for item in sublist]) \
    .map(lambda row:reg.LabeledPoint(row[0],ln.Vectors.dense(row[1:])))

#summary feas  colStats
import pyspark.mllib.stat as st
import numpy as np
# 指定特徵放到列表中
numerical_cols = ["MOTHER_AGE_YEARS","FATHER_COMBINED_AGE","CIG_BEFORE","CIG_1_TRI","CIG_2_TRI","CIG_3_TRI","MOTHER_HEIGHT_IN","MOTHER_PRE_WEIGHT","MOTHER_DELIVERY_WEIGHT","MOTHER_WEIGHT_GAIN"]
#選擇特徵並轉化
numeric_rdd = births_transformed.select(numerical_cols).rdd.map(lambda row:[e for e in row])
mllib_stats = st.Statistics.colStats(numeric_rdd)
#求均值,方差等等
for col,m,v in zip(numerical_cols,mllib_stats.mean(),mllib_stats.variance()):
    print ("{0}:\t{1:.2f}\t{2:.2f}".format(col,m,np.sqrt(v)))

image-20200512223824210

這一步運行時間挺長,大概30分鐘

如圖得到數據的初步的描述性統計結果,得到每個特徵的均值和方差這些基本數據。

可以看出,與父親的年齡相比,母親的年齡更小:母親的平均年齡是28歲,而父親的平均年齡是超過44歲;且許多的母親懷孕後開始戒菸(這是一個好的現象)

接下來我們來探索各個特徵間的相關性:

2.數據的探索:特徵相關性

相關性可以幫助識別具有共線性數值的特徵,也可以針對這些特徵進行處理,

我們可以使用corr協方差函數進行相關性分析,通過相關性分析,結果在此不再展示,得出 CIG…特徵是高度相關的,所以我們可以選取部分,這裏僅保留CIG_1_TRI,刪除其他cig特徵。重量也是高度相關的,我們這裏只保留MOTHER_PRE_WEIGHT,刪除其他weight特徵

#數據探索,找特徵相關性,那肯定是要找不怎麼相關的,發現他們的相關性
#calc categorical variables
categorical_cols = [e for e in births_transformed.columns if e not in numerical_cols]
categorical_rdd = births_transformed.select(categorical_cols).rdd.map(lambda row:[e for e in row])

#for i,col in enumerate(categorical_cols):
#    agg = categorical_rdd.groupBy(lambda row:row[i]).map(lambda row:(row[0],len(row[1])))
#    print (col,sorted(agg.collect(),key=lambda el:el[1],reverse=True))

#numerical feas correlation
#特徵間的相關性corr協方差函數
corrs = st.Statistics.corr(numeric_rdd)
print (corrs)
for i,e in enumerate(corrs > 0.5):
    correlated = [(numerical_cols[j],corrs[i][j]) for j,e in enumerate(e) if e == 1.0 and j != i]
    if len(correlated) > 0:
        for e in correlated:
            print ("{0}-to-{1}:{2:.2f}".format(numerical_cols[i],e[0],e[1]))

#刪除相關性較高的特徵
features_to_keep = [
    'INFANT_ALIVE_AT_REPORT',
    'BIRTH_PLACE',
    'MOTHER_AGE_YEARS',
    'FATHER_COMBINED_AGE',
    'CIG_1_TRI',
    'MOTHER_HEIGHT_IN',
    'MOTHER_PRE_WEIGHT',
    'DIABETES_PRE',
    'DIABETES_GEST',
    'HYP_TENS_PRE',
    'HYP_TENS_GEST',
    'PREV_BIRTH_PRETERM'
]

births_transformed = births_transformed.select([e for e in features_to_keep])

image-20200513073025024

經過查看實際數據,發現BIRTH_PlACE特徵類型是字符串,這裏使用散列技巧將字符串轉換成數值類型特徵,經過轉換,特徵全部轉換爲數值型。至此,數據準備階段結束,接下來開始經行數據挖掘

3.統計校驗

在通過特徵變量的相關係數選擇特徵時,對於一般的分類變量而言,我們無法計算它們之間的相關係數,但是我們可以通過對它們進行卡方校驗來檢測它們的分佈之間是否存在較大的差異。

卡方檢驗:是用途非常廣的一種假設檢驗方法,它在分類資料統計推斷中的應用,包括:兩個樣本率或兩個構成比比較的卡方檢驗;多個樣本率或多個構成比比較的卡方檢驗以及分類資料的相關分析等。

卡方檢驗就是統計樣本的實際觀測值與理論推斷值之間的偏離程度,實際觀測值與理論推斷值之間的偏離程度就決定卡方值的大小,卡方值越大,越不符合;卡方值越小,偏差越小,越趨於符合,若兩個值完全相等時,卡方值就爲0,表明理論值完全符合。

而在PySpark中你可以用 .chiSqTest() 方法來輕鬆實現卡方檢驗。

import pyspark.mllib.linalg as ln
for cat in categorical_cols[1:]:
    agg = births_transformed.groupby("INFANT_ALIVE_AT_REPORT") \
        .pivot(cat) \
        .count()
    agg_rdd = agg.rdd.map(lambda row:(row[1:])).flatMap(lambda row:[0 if e == None else e for e in row]).collect()
    row_length = len(agg.collect()[0]) - 1
    agg = ln.Matrices.dense(row_length,2,agg_rdd)
    test = st.Statistics.chiSqTest(agg)
    print (cat,round(test.pValue,4))

image-20200513074104055

從結果我們可以看出,所有分類變量對理論值的預測都是有意義的,因此,我們在構建最後的預測模型的時候都要考慮上這些分類型特徵變量。

4.創建最後的待訓練數據集

經過一輪的數據分析和特徵變量篩選之後,最終到了我們最終的建模階段了。首先我們將篩選出來以DataFrame數據結構模型表達的數據轉換成以LabeledPoints形式表示的RDD。

LabeledPoint 是 MLlib 中的一種數據結構,它包含了兩個屬性值:label(標識),features(特徵)一般用作機器學習模型的訓練。

其中,label就是我們目標的分類的標識而features就是我們用於分類的特徵,
通常是一個Numpy 數組,列表,psyspark.mllib.linalg.SparseVector,pyspark.mllib,linalg.DenseVector或者是scipy.sparse的形式。

import pyspark.mllib.feature as ft
import pyspark.mllib.regression as reg
hashing = ft.HashingTF(7)
births_hashed = births_transformed \
  .rdd \
  .map(lambda row: [
      list(hashing.transform(row[1]).toArray())
          if col == 'BIRTH_PLACE'
          else row[i]
      for i, col
      in enumerate(features_to_keep)]) \
  .map(lambda row: [[e] if type(e) == int else e
          for e in row]) \
  .map(lambda row: [item for sublist in row
          for item in sublist]) \
  .map(lambda row: reg.LabeledPoint(
      row[0],
      ln.Vectors.dense(row[1:]))
      )

5.劃分訓練集和測試集

形如sklearn.model_selection.train_test_split隨機劃分訓練集和測試集的模塊一般,在PySpark中RDDs也有一個便利的**.randomSplit(…)**方法用於隨機劃分訓練集和測試集。

在本例中可以這樣使用

births_train,births_test = births_hashed.randomSplit([0.6,0.4])

沒錯,僅僅需要上面這樣一行的代碼,我們就可以將我們的待訓練數據按照隨機60%,40%來劃分好我們的訓練集和測試集了。

6.開始建模

在一切準備就緒之後,我們就可以開始通過我們上面的訓練數據集來建模了。在這裏我們來嘗試建立兩個模型:一個線性的Logistic迴歸模型,一個非線性的隨機森林模型。然後,在初次建模的時候,我們先採用篩選出來的全部特徵來建模,然後我們再通過**ChiSqSelector(…)**方法來歸納出最能代表全部整體的四個主成分。

7.Logistic 迴歸模型

#邏輯迴歸模型
from pyspark.mllib.classification import LogisticRegressionWithLBFGS as LR
lr_model = LR.train(births_train,iterations=10)#迭代10次

lr_results = (births_test.map(lambda row:row.label) \
        .zip(lr_model.predict(births_test.map(lambda row:row.features)))).map(lambda row:(row[0],row[1] * 1.0))

#模型評估
import pyspark.mllib.evaluation as ev
lr_ev = ev.BinaryClassificationMetrics(lr_results)
print ("Area under PR:{}".format(lr_ev.areaUnderPR))
print ("Area under ROC: {}".format(lr_ev.areaUnderROC))

從上面的建模過程可以看出,使用PySpark訓練一個模型也是非常簡單的。我們只需要調用**.train(…)**方法,並傳入之前處理好的LabeledPoints數據即可。不過需要注意的一點是我們要提前指定一個較小訓練的迭代次數以免訓練時間過長。

同時,在上面的代碼中,我們在訓練完一個模型之後使用MLlib中爲我們提供的評估分類和迴歸準確度的**.BinaryClassificationMetrics(…)**方法來分析我們最後預測的結果。

最後,結果圖示如下:

image-20200513074232120

8.選取出最具代表性的分類特徵

通常來說,一個採取更少的特徵的簡單模型,往往會比一個複雜的模型,在分類問題上更具有代表性和可解釋性。而在MLlib中,則可以通過**.Chi-Square selector**來提取出模型中最具代表性的一些分類特徵變量來簡化我們的模型

#feature selection with chi-square
selector = ft.ChiSqSelector(4).fit(births_train)
topFeatures_train = (
    births_train.map(lambda row:row.label) \
    .zip(selector.transform(births_train.map(lambda row:row.features)))

).map(lambda row:reg.LabeledPoint(row[0],row[1]))

topFeatures_test = (
    births_test.map(lambda row:row.label) \
    .zip(selector.transform(births_test.map(lambda row:row.features)))

).map(lambda row:reg.LabeledPoint(row[0],row[1]))



9.隨機森林模型

隨機森林模型(Random forest 後面簡稱RF)在訓練上總體與Logistic類似,不同的參數是RF在訓練前需要指定類別總數:numClasses,樹的棵數:numTrees

#random forest model
from pyspark.mllib.tree import RandomForest
rf_model = RandomForest.trainClassifier(data=topFeatures_train,
                numClasses=2,
                categoricalFeaturesInfo={},
                numTrees=6,
                featureSubsetStrategy="all",seed=666)

rf_results = (topFeatures_test.map(lambda row:row.label) \
        .zip(rf_model.predict(topFeatures_test.map(lambda row:row.features))))


rf_ev = ev.BinaryClassificationMetrics(rf_results)
print ("Area under PR:{}".format(rf_ev.areaUnderPR))
print ("Area under ROC: {}".format(rf_ev.areaUnderROC))

注:在隨機森林模型的創建中,我們採用的是上面提取出來的最具代表性的有效特徵,這就意味着模型用到的特徵是比之前的Logistic要少的。

最後,結果圖示如下:

image-20200513074755589

通過結果我們可以看出,隨機森林模型,在採用比之前更少的特徵下的建模的最終預測效果是優於之前的Logistic迴歸模型的。

下面我們同樣使用代表性特徵來重建一次Logistic迴歸模型

LR_Model_2 = LR.train(topFeatures_train, iterations=10)
LR_results_2 = (
topFeatures_test.map(lambda row: row.label).zip(LR_Model_2.predict(topFeatures_test.map(lambda row: row.features)))).map(lambda row: (row[0], row[1] * 1.0))
LR_evaluation_2 = ev.BinaryClassificationMetrics(LR_results_2)
print('Area under PR: {0:.2f}'.format(LR_evaluation_2.areaUnderPR))
print('Area under ROC: {0:.2f}'.format(LR_evaluation_2.areaUnderROC))
LR_evaluation_2.unpersist()

image-20200513075021554

通過結果,我們可以看出,雖然沒有達到RF模型的準確度,但是與採用了全特徵的Logistic迴歸模型處於同一水平。所以,我們在可選的情況下,通常採用更少的特徵來構建更爲簡化和有效的模型。

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