項目簡介
此項目為優(yōu)達學城數據分析(高級)畢業(yè)項目, 該項目需要使用Spark預測Sparkify應用程序的客戶流失率.
項目目標
Udacity在其教室中提供Sparkify應用程序部分數據集, 便于學生完成項目,數據集文件大小為128MB, 文件類型為json. 同時項目要求成果通過Git Hub提交并附帶項目過程報告.
項目思路
- 1.搭建spark運行環(huán)境
- 2.加載庫
- 3.加載與清洗數據
- 4.探索性數據分析
- 5.獲取特征
- 6.機器學習
- 7.結論匯總
項目實現(xiàn)記錄
搭建spark運行環(huán)境
在Mac os X系統(tǒng)中搭建spark運行環(huán)境時我遇到很多的坑, 耽誤很長時間, 為了保證項目進度, 選擇在Udacity自帶的運行環(huán)境中完成項目. 這里我將遇到的坑進行匯總,便于后續(xù)解決.
- 1.系統(tǒng)必須安裝java JDK 8 (支持最新版)且在環(huán)境變量中設置
- 2.在spark官網上下載Apache Spark 2.4.5, 雖然Anaconda中也可以直接安裝pyspark庫, 但是版本較低, 是2.2.x版本
- 3.python版本不能高于3.6
進行以上設置后, 使用jupyter notebook在mac os X系統(tǒng)中還是未能正常加載pyspark庫, 提示未找到Apache Hadoop??
之后和班上其他同學聊到此問題, 有部分同學表示, 如果電腦性能太低, 就算搭建好運行平臺, 模型運算速度會很慢, 甚至不如在優(yōu)達自帶運行環(huán)境中快, 但從技術角度來看, 學會在本地搭建spark運行環(huán)境還是有意義的, 后續(xù)完善本地搭建spark環(huán)境.
加載項目所需運行庫
回到項目本身, 根據項目思路, 會用到5個方面的庫.
- 1.基本的pandas\numpy, 便于對dataframe進行操作
- 2.matplotlib\seaborn, 生成可視化圖形
- 3.pyspark\在python中實例化spark對象
- 4.pyspark.sql, 將數據集轉換為spark識別的格式, 使數據在spark中能夠進行類似在SQL下的操作
- 5.pyspark.ml, spark中進行機器學習的庫
- 6.time, 記錄代碼塊運行時間的庫
加載與清洗數據
首先,實例化spark
spark = SparkSession \
.builder \
.appName("Sparkify_Spark_Sql_Session") \
.getOrCreate()
之后可以使用spark.sparkContext.getConf().getAll()方法查詢實例化spark的運行參數.
其次,加載數據集
對于陌生的數據集,我的思路是查看其列名、行數,每列的前幾行的值(便于了解每列的含義),有無空值,每列值的數據類型。
由于項目要求用Git Hub提交, 免費版Git Hub無法上傳大于100MB的文件, 需要將128MB的文件壓縮, 根據老師提示, 使用bz2壓縮文件格式, spark可以直接讀取其中數據. MacOS自帶bz2壓縮程序, 使用如下命令將文件壓縮, 壓縮后被壓縮文件將刪除.
$ bzip2 -z filename.json
加載數據集使用方法spark.read.json('filename.bz2')
數據加載后, 對數據進行觀察, 觀察數據集時用到的方法如下:
.printSchema()
.describe()
.take()
在
.describe()與.take()方法之后加上.show()方法, 可以按照一定格式顯示數據,便于閱讀.
df.describe('userAgent').show()
+-------+--------------------+
|summary| userAgent|
+-------+--------------------+
| count| 278154|
| mean| null|
| stddev| null|
| min|"Mozilla/5.0 (Mac...|
| max|Mozilla/5.0 (comp...|
+-------+--------------------+
遇到不能理解其含義的變量時, 可通過用.select方法具體觀察
df.select('auth').dropDuplicates().sort('auth').show()
+----------+
| auth|
+----------+
| Cancelled|
| Guest|
| Logged In|
|Logged Out|
+----------+
.sort()方法是將查看的數據集按照某列排序
查看所有數據集后, 對數據集有以下認識:
- 1.數據集共有286500行
- 2.共有18列
- 3.連續(xù)變量有itemInSession\length\registration\ts, 剩下變量均為分類變量
- 4.每個變量的類型與含義如下:
|-- artist: string (歌手)
|-- auth: string (登錄狀態(tài))
|-- firstName: string (名字)
|-- gender: string (性別)
|-- itemInSession: long (連續(xù)變量, 具體含義暫不明確)
|-- lastName: string (姓氏)
|-- length: double (聽歌時長)
|-- level: string (會員等級)
|-- location: string (地區(qū))
|-- method: string (分類變量, 具體含義暫不明確, 可能與用戶發(fā)送\接受app信息有關)
|-- page: string (請求頁面)
|-- registration: long (注冊時間)
|-- sessionId: long (頁面ID)
|-- song: string (歌名)
|-- status: long (分類變量, 具體含義暫不明確, 可能與連接狀態(tài)有關)
|-- ts: long (連續(xù)變量, 具體含義暫不明確)
|-- userAgent: string (用戶使用平臺信息)
|-- userId: string (用戶ID)
- 5.部分數據在查看其詳情時, 發(fā)現(xiàn)有空值需要處理
處理空值
userId為用戶ID, 在之后的數據清理過程中, 可用作數據集索引, 但發(fā)現(xiàn)其有空值, 需要處理
處理空值方法.dropna.
df.dropna(how= 'any', subset = ['userId', 'sessionId'])
使用此方法后, 發(fā)現(xiàn)還是存在ID無數值的情況, 懷疑其可能被空值填充, 使用.filter, 去除有空字符的行.
df_dropna.filter(df_dropna['userId'] != '')
之后使用.count方法計算被刪除的行數, 與數據集剩余行數.
df.count() - df_dropna.count(), df_dropna.count()
最終, 數據集有8346行被刪除, 刪除后的數據集還剩278154行.
探索性數據分析
模型標簽
項目提示使用churn作為模型的標簽, 并且建議使用Cancellation Confirmation事件來定義客戶流失. 基于對數據集的理解, Cancellation Confirmation與Downgrade事件為page變量中的兩項, Cancellation Confirmation為確認注銷, Downgrade為降級.
該問題的解決方案
- 1.新建
churn列 - 2.標記
page中的Cancellation Confirmation事件, 將轉換后的數據改為int型, 再存入新列中 - 3.之后再通過標記找到對應的用戶
結果如下:
+------+-----+
|userId|churn|
+------+-----+
|100010| 0|
|200002| 0|
| 125| 1|
| 124| 0|
| 51| 1|
+------+-----+
探索數據
定義好客戶流失后, 可以執(zhí)行一些探索性數據分析, 觀察留存用戶和流失用戶的行為.
首先把這兩類用戶的數據聚合到一起, 觀察某個特征動作的次數, 比如會員等級\性別等.
保險起見, 將需要查看的特征, 轉換為pandas下的dataframe類型, 便于之后的可視化工作.
- 1.刪除賬戶的用戶與用戶等級的關系
# 提取churn與level特征,整理排序
churn_level_df = df_new.filter('page == "Cancellation Confirmation"') \
.groupby('level') \
.count() \
.toPandas()
# 使用直方圖探索churn與level的關系
churn_level_df.plot.bar();
付費用戶注銷數量高于免費用戶注銷數量
- 2.刪除賬戶的用戶與性別的關系
# 提取churn與gender特征,整理排序
churn_gender_df = df_new.dropDuplicates(['userId', 'gender']) \
.groupby(['churn', 'gender']) \
.count() \
.sort('churn') \
.toPandas()
# 通過直方圖探索churn與gender的關系
ax = sns.barplot(x = 'churn', y = 'count', hue = 'gender', data = churn_gender_df)
plt.xlabel("Has user delete the account")
plt.ylabel("Count")
plt.title("Gender ratio of users who delete the account");
男性用戶比女性用戶刪除賬戶的人數更多
刪除賬戶的比例對一款應用來說比較高
app可能對男性吸引力更大
特征工程
熟悉數據之后,我認為以下特征可能對訓練模型產生較大影響
- 1.用戶聽過的歌手數量
- 2.性別
- 3.用戶聽歌時長
- 4.用戶所聽歌曲總和
- 5.歌曲被加入播放列表的數量
- 6.會員等級
下面詳細說說,獲取每種特征值的關鍵點
- 1.用戶聽過的歌手數量
page頁面記錄了用戶在使用app過程中的動作,獲取每個用戶在點擊頁面NextSong時的artist信息并計數,就能獲得用戶聽過的歌手數量
feature_artists = df_new.filter(df_new.page == 'NextSong') \ # 獲取頁面
.select('userId', 'artist') \ # 獲取userId、artist
.dropDuplicates() \ # 去除重復值
.groupBy('userId') \ # 按userId分組
.count() \ # 記錄同一用戶不同artist出現(xiàn)的數量
.withColumnRenamed('count', 'sum_artist') # 重命名列
- 2.性別
gender特征的問題在于要把F、M變量轉為0、1,方便模型計算
eature_gender = df_new.select('userId', 'gender') \ # 獲取userId、gender
.dropDuplicates() \ # 去除重復值
.replace(['M', 'F'], ['0', '1'], 'gender') \ # 將`F、M`變量轉為`0、1`
.select('userId', col('gender').cast('int')) # 將值轉換為int類型
- 3.用戶聽歌時長
length特征關鍵在于需要將每個用戶所有length值相加
feature_length = df_new.select('userId', 'length') \# 獲取userId、length
.groupBy('userId') \# 按userId分組
.sum() \ # 按userId相加
.withColumnRenamed('sum(length)', 'listening_time')# 重命名列
- 4.用戶所聽歌曲總和
該特征沒什么難點,之前特征的部分方法就能得到
feature_songs = df_new.select('userId', 'song') \# 獲取userId、song
.groupBy('userId') \# 按userId分組
.count() \# 計數
.withColumnRenamed('count', 'sum_song')# 重命名列
- 5.歌曲被加入播放列表的數量
該特征獲取原理是記錄Add to Playlist的次數
feature_ATP = df_new.select('userId', 'page') \
.where(df_new.page == 'Add to Playlist') \# 篩選出page等于Add to Playlist的頁面
.groupBy('userId') \
.count() \
.withColumnRenamed('count', 'add_to_play')
- 6.會員等級
該特征的獲取與性別特征獲取方法一致
feature_level = df_new.select('userId', 'level') \
.dropDuplicates() \
.replace(['free', 'paid'], ['0', '1'], 'level') \
.select('userId', col('level').cast('int'))
整理標簽數據,之后與特征數據匯總
label_churn = df_new.select('userId', col('churn') \
.alias('label')) \ # 對特征churn取別名為label
.dropDuplicates()
整合特征值與標簽
# 這里注意.join函數的用法
# 如果為空的數據,需要用0填充,不然最后模型計算會報錯
# 一定不要忘記刪除userId,userId是索引,不是特征,不能導入到模型計算
df_feature = feature_artists.join(feature_gender, 'userId', 'outer') \
.join(feature_length, 'userId', 'outer') \
.join(feature_songs, 'userId', 'outer') \
.join(feature_ATP, 'userId', 'outer') \
.join(label_churn, 'userId', 'outer') \
.fillna(0) \
.drop('userId')
特征工程的主要目的是提取特征,并生成線性代數矩陣,其中對標簽設置別名很關鍵,因為建模時使用的方法需要關鍵字label對應的變量,沒有設置別名,或者別名設置成其他值,均不能運算
建模
將完整數據集分成訓練集、測試集和驗證集。選用邏輯回歸、支持向量機與隨機森鈴機器學習方法。項目說明建議評價指標選擇準確率,選用 F1 score 作為優(yōu)化指標。
關于參數的選擇:
機器學習一般選擇4個參數作為衡量模型好壞的標準,分別為準確率(Precision)、精確度(Accuracy)、召回率(Recall)、F1分數(F1-Score),簡單闡述這幾種參數的含義
1)準確率是對給定數據集,分類正確樣本個數和總樣本數的比值;
2)精確度說明判斷為真的正例占所有判斷為真的樣例比重;
3)召回率又被稱為查全率,用來說明分類器中判定為真的正例占總正例的比率;
4)精確度和召回率之間是負相關的關系,引入F1-Score作為綜合指標,平衡準確率和召回率的影響。
根據項目需求,需要預測客戶流失率,但流失顧客數據集很小,只占數據1%不到,Accuracy很難反映模型好壞,f1分數這時候就比較關鍵。
關于機器學習算法的選擇:
1)邏輯回歸-----優(yōu)點:計算速度快,容易理解 缺點:容易產生欠擬合
2)支持向量機---優(yōu)點:數據量較小情況下解決機器學習問題,可以解決非線性問題 缺點:對缺失數據敏感
3)隨機森林-----優(yōu)點:在當前的算法中擁有特別好的精確度(Accuracy),可以有效的運行在大數據集上,有缺失數據也能夠獲得更好的結果
在模型的選擇上,我的思路是選用邏輯回歸作為模型參考,因為其容易欠擬合,其他兩種機器學習算法的分數應該比邏輯回歸更高;
支持向量機在現(xiàn)在的小數據集上應該表現(xiàn)最佳,但是數據存在小部分確實,可能對分數產生影響;
隨機森林的分數大概率與支持向量機類似,但是其更適合運用于大數據,在之后測試12GB大數據時從計算時間上會優(yōu)于SVM。
- 轉換數據
# 將特征工程中的數據集轉換為可供模型計算的結構
columns = ['sum_artist', 'gender', 'listening_time', 'sum_song', 'add_to_play']
assembler = VectorAssembler(inputCols = columns, outputCol = 'features_matrix')
data = assembler.transform(df_feature)
# 標準化數據
scaler = StandardScaler(inputCol = 'features_matrix', outputCol = 'features')
scalerModel = scaler.fit(data)
data = scalerModel.transform(data)
# 將數據集分成訓練集、測試集和驗證集
train, test, validation = data.randomSplit([0.6, 0.2, 0.2])
這里卡了一段時間,因為轉換以后的數據竟然和沒轉換之前一樣,
- 邏輯回歸
# 初始化邏輯回歸,maxIter為可迭代最大次數,邏輯回歸中必須設置maxIter
lr = LogisticRegression(maxIter=5)
# 設置評估標準
f1_score = MulticlassClassificationEvaluator(metricName = 'f1')
# 建立參數網格
paramGrid = ParamGridBuilder().build()
# 設置交叉驗證
lr_crossval = CrossValidator(estimator=lr,
estimatorParamMaps=paramGrid,
evaluator=f1_score)
# 訓練之后,通過驗證集計算準確度和f1分數
lr_result = crossval_model.transform(validation)
# 查看結果
# 時間也是衡量模型好壞的一個標準
evaluator = MulticlassClassificationEvaluator(predictionCol = 'prediction')
print("邏輯回歸分數:")
start = time()
print("準確度: {}".format(evaluator.evaluate(lr_result, {evaluator.metricName:'accuracy'})))
print("f1分數: {}".format(evaluator.evaluate(lr_result, {evaluator.metricName:'f1'})))
end = time()
print("驗證集計算準確度與f1分數用時 {}秒".format(end - start))
-
支持向量機、隨機森林
支持向量機、隨機森林兩個模型代碼與邏輯回歸結構基本一致,除了需要將代碼改為對應模型外,隨機森林可以不設置最大迭代次數
-
計算結果
- 邏輯回歸模型的準確度為 0.7755,f1分數為 0.6775,耗時 645秒
- SVM(支持向量機)模型的準確度為 0.7755,f1分數為 0.6775,耗時796秒
- 隨機森林模型的準確度為 0.7143,f1分數為 0.6463,耗時 871秒
參數優(yōu)化
由于使用模型的默認參數運算,得到的結果并不理想,調整參數,優(yōu)化結果。
下面將對以上三個模型增加交叉驗證,每種模型均交叉驗證3次:
# 初始化邏輯回歸
lr = LogisticRegression(maxIter=5)
# 設置評估標準
f1_score = MulticlassClassificationEvaluator(metricName = 'f1')
# 建立paramGrid
paramGrid = ParamGridBuilder().build()
lr_crossval = CrossValidator(estimator=lr,
estimatorParamMaps=paramGrid,
evaluator=f1_score,
numFolds=3)
代碼和之前唯一的變化就是在CrossValidator中增加了numFolds=3,其余代碼不變,SVM與隨機森林增加交叉驗證的方式與邏輯回歸相同。
-
交叉驗證后的計算結果
- 邏輯回歸模型的準確度為 0.7755,f1分數為 0.6775,耗時 667秒
- SVM(支持向量機)模型的準確度為 0.7755,f1分數為 0.6775,耗時655秒
- 隨機森林模型的準確度為 0.7143,f1分數為 0.6463,耗時 682秒
優(yōu)化邏輯回歸
由于增加交叉驗證的邏輯回歸算法與SVM無明顯差距,加上其易于理解,我將進一步優(yōu)化該算法
# 主要修改該部分代碼
paramGrid = ParamGridBuilder().addGrid(lr.regParam, [0.1, 0.01]) \
.addGrid(lr.fitIntercept, [False, True]) \
.build()
- 優(yōu)化邏輯回歸后的計算結果
- 在調整邏輯回歸的正則項系數及是否需要計算截距這兩個參數后,發(fā)現(xiàn)4個結果完全一樣,均為0.67487;
- 和之前的計算結果相比,f1分數有略微下降;
- 使用未調整參數的邏輯回歸模型分數更高。
測試集結果
# 使用f1分數最高,時間最快的模型計算測試集
lr_best = LogisticRegression(maxIter=5)
lr_best_model = lr_best.fit(train)
final_result = lr_best_model.transform(test)
# 顯示最終結果
evaluator = MulticlassClassificationEvaluator(predictionCol = 'prediction')
print("測試集結果:")
print("準確度: {}".format(evaluator.evaluate(final_result, {evaluator.metricName:'accuracy'})))
print("f1分數: {}".format(evaluator.evaluate(final_result, {evaluator.metricName:'f1'})))
在測試集上運算后,accuracy為0.7442,f1分數為0.6350,和最初在驗證集的結果上相比,accuracy與f1分數均降低了3%左右。
總結
雖然最終采用邏輯回歸作為最終模型是因為其accuracy與f1分數最高,但我認為分數還有進一步提高的可能,有這種想法主要因為以下幾點:
- 第一次運算時SVM與邏輯回歸分數一樣,一般上講邏輯回歸容易欠擬合,但和SVM模型結果一樣,說明邏輯回歸欠擬合的可能性較?。?/li>
- 三種模型的f1分數并不高,說明pricision和recall始終不平衡;
- 最佳邏輯回歸模型代入測試集運算后,accuracy與f1均下降了3%左右,并沒有與之前差很多。
綜合以上觀點,我認為在特征工程部分出現(xiàn)了偏差導致這種現(xiàn)象,我選擇的特征與用戶流失率的關系不是特別相關。在后續(xù)完善工作中,我認為以下方案可以提高accuracy與f1分數:
- 增加參考標準,計算沒有用戶流失和全部用戶流失的accuracy與f1分數,評估其他模型好壞;
- 回顧數據集,對未理解的特征進行探究,了解其含義,找到與數據集相關性更強的特征;
- 增加決策樹、梯度提升樹等其他算法,對比已使用的算法,觀察accuracy與f1分數變化。
在完成項目的過程中遇到以下難點,思考其解決方案時間較久,不過最終都解決了:
- 每次運行模型,分數總會變化。造成該問題的主要原因是每次被分割的數據集均在變化,每次關掉IDE,變量就會被清空,重新加載運行,必然重新分割數據集;
- 開始將會員是否付費作為特征,代入模型計算,但分數較現(xiàn)在更低,思考后,感覺該特征和用戶流失率并無因果關系,刪去此特征后,accuracy與f1分數均有提高。