[機器學習專案]Kaggle競賽-kkbox顧客流失預測(Top5%) - YL-Tsai

文章推薦指數: 80 %
投票人數:10人

這篇文章主要紀錄了KKBOX於2017年9月在Kaggle上舉辦的比賽WSDM — KKBox's Churn Prediction Challenge,預測哪些使用者可能會流失,當初決定做這個專案練習一方便是 ... GetunlimitedaccessOpeninappHomeNotificationsListsStoriesWrite[機器學習專案]Kaggle競賽-kkbox顧客流失預測(Top5%)了解使用者流失的潛在模式圖片來源:Kaggletwintterhttps://twitter.com/kaggle。

這篇文章主要紀錄了KKBOX於2017年9月在Kaggle上舉辦的比賽WSDM—KKBox’sChurnPredictionChallenge,預測哪些使用者可能會流失,當初決定做這個專案練習一方便是因為筆者自己本身蠻喜歡音樂的,能夠將所學應用到自己喜歡的領域實在是一件令人很興奮的事情,另一方面,顧客流失為機器學習解決業界問題的一個重要應用,特別在金融(信用卡續約),電商(是否回購產品,是否續訂),中占有一席之地,通常會被稱為顧客關係管理,顧客價值分析,或是商業智慧顧客價值分析......等等的。

本文將透過多次的探索性分析(exploratorydataanalysis),搭配特徵工程,並使用隨機森林(randomforest)以及極限梯度提升樹(xgboost),來進行模型訓練,最終使用8個萃取特徵,並達到比賽中的Top5%,同時之間,了解可能導致使用者流失的潛在模式,在程式語言的使用方面,使用了python分析資料以及標準SQL撈取資料,建議讀者至少對其中一項語言有實作經驗會比較容易跟上本篇分析,如果不太熟悉SQL的讀者,也可以先上KaggleLearn的SQL課程玩一玩,我想以本篇專案的任務來說,大致上也是夠用的,而針對大數據的處理,除了使用GoogleBigQuery作為解決方法之一,亦針對了pythonpandas中的資料型態儲存格式優化,使用向量化以及探索重複值的程式設計技巧,等三項資料工程技術進行實作探討。

若有轉載,請標明文章出處。

目錄Prepare資料讀取集環境建置第一次探索性分析(FirstEDA)BaseModelMemberstableGlanceat“members”tableRegistered_viaAgeTransactionstablePandas中的資料型態快速的日期格式轉換Apply函數集向量化操作Glanceat“transactionstable”資料清洗Last_last_churnTrans_timesandclient_level_codeLast_auto_renew快速連結User_logsreviewsOne_month_day_listen結論參考資料Prepare資料讀取及環境建置讀者可以在該比賽的DataSource頁面下載資料,由於檔案數多,資料數多,其中最大的使用者聽歌日誌v1和v2合併之後csv檔案有42.05GB,約4億1千萬筆資料,我們可以採取雲端環境、分散式儲存/運算、或改變資料型態來處理,這裡筆者採用雲端環境以及改變資料型態來處理資料,如果讀者對雲端環境的建置不熟悉,可以看我上一篇文章,詳細了介紹GoogleCloud雲端環境的建置以及上傳資料,熟悉的讀者,就上傳到自己的環境吧!大致上來說是這樣的:從Kaggle上的KKBox’sChurnPredictionChallenge使用KaggleAPI下載資料集到你的筆電上(本地端)。

從網友InfiniteWing重新標註的資料集下載訓練集(註1)。

將檔案上傳到Storage。

進入BigQuery,並進入舊版UI(註2),在BigQuery中建立一個資料集,建立資料表,依序將資料從Storage上傳至BigQuery(註3)。

註1:當時這項比賽在比賽期間,資料非常的原始,不少Kaggler依照主辦方的定義自己全新標註訓練資料集,得到乾淨度較高的資料,如今比賽已經結束,我們跳過重新標註資料的部分。

註2:筆者自己在2個月前分析時,還沒有新版的UI,中間過程有使用過新版UI上傳資料集,但是當時Schema的部分有時候讀取不正確,如果各位讀者使用新版UI沒有問題,則可以忽略,不過筆者建議轉為舊版UI。

註3:選擇從Storage上傳時,舊版UI會需要填入路徑,Storage的路徑可以在Storage-->點選資料夾-->點選總覽-->gsutil連結。

例如我的就是gs://kk_data。

第一次探索性分析(FirstEDA)同樣的本篇文章的重點放在特徵工程以及模型表現上,而相較上一個鐵達尼號的專案,這個比賽僅有575個隊伍參賽,相對的可以學習的資源也就少非常多,但是強大的Kaggler,HeadorTails也有加入比賽,提供了非常高品質的EDA:(1)ShouldIstayorshouldIgobyHeadorTailsBaseModel在開始測試各式各樣的特徵之前,我們需要選擇一個模型,這裡選擇(隨機森林)RandomForest以及極限梯度提升樹(xgboost),基於樹的模型再加上ensemble,提供了很好的抗噪性,而選用xgboost除了精準度確實較高之外,也在程式包裡面提供了缺失值處理的方便,可惜的是一google發現並不是使用林軒田老師說的代理孕母的技術,而是將缺失值列為一個類別,在分裂葉節點時分邊,挑結構增益較大的一邊。

接著我們從打開googlecloudplatform(GCP)的coludshell開始:datalabconnectkk-churn雖然上一篇文章稱為使用Datalab及BigQuery進行大數據分析[Part2],但實際上也就是本專案的BaseModel!使用了兩個從使用者日誌萃取出來的特徵:聽歌天數(day_listen)以及潛在滿意度(user_latent_satisfaction)達到log_loss=0.15693,這約是Top63%。

實際上筆者在使用者日誌這個表當中萃取不少特徵,但是效果都不理想,接著我們看看別的特徵。

Memberstable首先開一個新的notebook,轉成python3,並載入先前兩個特徵,以及member資料表,接著全部合併在一起:Glanceatmemberstable沒什麼特別的,就是import需要的東西XD,接著讀取member資料集:%gcs是googlestorage在datalab中的magiccommand,在啟動datalab時就已經串接好,我們可以直接套用來讀取googlestorage中的資料,需要注意的是使用magiccommand時要單個cell執行,不然後面-v的變數值會被洗掉,基本上就是一些討人厭的坑,筆者先幫讀者們踩了XD。

並且可以看到5個特徵中有4個有完全一樣的缺失比例(city,bd,gender,register_via,registration_init_time),經過檢查之後,他們是一起缺失的,意味著如果要填補缺失,需要靠其他資料表的相關性來尋找,接著探索和流失有關係的特徵,接下來看一眼資料,大致了解資料表達了甚麼事情:df_data.head()six_month_day_listen:從user_log萃取出,過去六個月內的聽歌天數six_month_satis:從user_log萃取出,過去六個月內歌曲滿意度歌曲滿意度定義為(100%的歌曲數)/(所有聽歌的歌曲數(25%,50%,75%,95%,98.5%,100%))city:註冊時的所填城市bd:註冊時的所填年齡genger:註冊時的所填性別regisetered_via:註冊時的經由管道(編碼過後)registration_init_time:註冊的初始日期例如我們可以看到編號2號的使用者QKXN8F……,在2017年3月並未流失,過去六個月內並未聽任何歌曲/有聽歌曲,但資料缺失,亦無滿意度資料,註冊時在1號城市,0歲,性別未知,經由7號管道註冊,2013年8月25日註冊,有了對資料的初始認識之後,讀者亦可以搭配HeadandTails的EDA交互觀察,藉以更了解資料,接著我們基於這樣的資料開始思考哪些因素會影響顧客是否流失,抽取出特徵,並放入模型測試:Registered_via由於官方提供的資料是編碼過的,所以只能猜測可能是什麼,而這個特徵可以有效地切割出流失的比例,讀者個人推測極有可能是經由甚麼裝置註冊,例如registered_via=13有可能對應到經由Safari或是IOS裝置註冊,registered_via=7有可能是對應到經由Chrome或是Android裝置註冊等。

橫軸為經由什麼裝置註冊(筆者推測),y軸為該群體的平均流失率,虛線為全體的平均流失率,黑線為95%的信賴區間。

雖然這一切是基於腦補的基礎,但是如果原本的資料確實是類似的分類,我們也可以從這些資訊看出一些端倪,例如使用apple的使用者(Safari,IOS)某種程度上也表現出了使用者的品味,這也常常影響了該使用者對某個產品的傾向。

如果可以知道這特徵原始代表的意思,就可以更有把握地做推測。

另一方面,5種樣本的數據量都還算足夠,能夠讓95%信賴區間不至於太寬,原本丟進模型的處理方式可以使用OneHot—encoding,但是在還沒有找到缺失值填補的好方法之前,不做處理,並將缺失值填為-1。

這也是多數Kaggler使用的方法之一。

接著建立模型:參數的部分沒有特別調整,僅調整到不至於overfitting的程度,作為特徵測試之用,而xgboost當中,可以在XGBClassifier指派缺失值為-1,模型在遇到-1時就會當作缺失值處理,接著準備好特徵,丟進兩個模型裡測試:訓練時間大約2~3分鐘,如果等不及的讀者們可以把虛擬機(VM)調整成8CPU,提交結果至Kaggle:我們的Validationseterror從0.19215來到0.16左右,可以看到xgb比rf有更低的誤差,但實際上是因為我們在randomforest中設定了最小分割點為0.05。

所以只要每當分割時的樣本數少於原本的5%,就不分割了,這讓我們的randomforest模型處於underfitting的狀態,筆者實測,事實上xgb和rf的訓練完之後的validationerror差別並不大,約在0.001的數量級,但是如果在Kaggle比賽中,這可能是狠甩好幾個名次,不過基於xgboost對缺失值的處理以及之後可用GPU來加速訓練,往後在商業運用上的運算速度很有優勢,因此學習XGBoost模型是很有幫助的,同時我們也經由模型來驗證registered_via這個特徵是有效的。

Age年齡這個特徵在此資料集被稱為bd,筆者認為年齡在此資料集中應該含有一些資訊,例如較年輕的使用者(例如學生)普遍來說還沒有經濟能力,這可能導致他們有較高的流失率,反過來說,有經濟能力的使用者,普遍來說應該會有較低的流失率,先看看這個特徵分布的情況:我們可以從min及max看到負的年齡以及2000歲的使用者,這應該是不可能的,不合理的資料無法被列入考量,畢竟garbagein,garbageout。

接著我們計算一下有效的年齡共有多少百分比,在筆者的分析中取0~90歲(共佔了資料的35%),其餘的全部令其=-1(如同前面特徵處理缺失值一般),接著我們看看以是否流失來分組的分布圖:左圖為年齡的分布圖,藍色為未流失,綠色為流失,虛線為26歲年齡分界線,右圖為年齡是否小於26歲,0表示大於26歲且小於90歲,1表示小於等於26歲,-1表示沒有意義的值,虛線為平均流失率。

我們可以從左圖中看到流失的分布確實必較左偏一些,經過取值發現,其和未流失的分布高峰界線恰巧在26歲,台灣的碩士畢業生平均年齡約在24歲(如果有當兵則是24.5~25歲),加個一年之後開始比較有經濟能力,這個界限還蠻合理的,可以納入特徵之中,其餘的值對於是否會流失可能不是那麼重要,很有可能造成overfitting,因此筆者這裡將其建構為二元特徵,而沒有意義的則指派為-1,如上右圖,我們可以看到26~90歲的群組流失率為26歲以下的1/2,其中有意思的是缺失的部分反而有更低的流失率,這在Kaggle競賽中蠻常見,有可能是想要參賽者去重新清洗資料,藉此發現資料中的更多價值,這一點筆者也列為待處理的任務,列為-1只是找到好方法清洗資料之前的權宜之計,接著我們切分訓練集以及測試集:確認訓練集以及測試集都完成了特徵工程的Doublecheck,接著使用XGBoost訓練模型,這裡並沒做參數最佳化,僅僅測試特徵是否有效:我們看到驗證集的logloss比起之前的0.16376下降到了0.16152,這證明了我們的特徵訓練出來的機率分布和測試集的目標分布更接近了,接著丟到Kaggle看看LB的分數:LB並沒有下降的很多,僅下降了0.001左右,我們也可以在EDA當中就看出比起Registered_via,age這個特徵確實沒有那麼強的區分性,而目前的logloss使我們晉升到KagglePublicLB的Top21%Transactionstable接著我們看看交易紀錄這個資料表,起初筆者認為這個資料集也是蠻大的,使用bigquery來做一些簡單的聚合並觀察,後來發現其實直接讀進來datalab在沒有改變資料結構的情況下大約2.2G(包含訓練資料和測試資料),算是還可以接受,再加上bigquery似乎某些情況下沒有支援排序,這對我們觀察時間序列來說不太方便,因此筆者這裡採用的方式是從Storage讀進datalab,調整資料型態以及改變聚合的運算方式來處理資料,第一次讀取資料我們先把transactions.csv,transactions_v2.csv串接起來,再分別和我們的df_train,df_sub,連接在一起,因為交易日誌的資料中有些使用者是沒有出現在訓練以及測試資料中的,接著再存回Storage中:這裡全部筆者都把他標住起來了,因為我們只要做一次就好,之後我們就可以直接讀取進來,其中要注意的是使用%gcs時,要分開執行,因為後面的-v是儲存在暫存變數,如果一起執行會被洗掉,接著我們就從Storage讀取近來:同樣要注意%gcs記得要分開,由於檔案蠻大的,這裡讀取需要一些時間,筆者這裡的測試約是1分30秒,而讀取完畢之後兩個DataFrame的行列數分別為:Pandas的資料型態兩個Dataframe所佔的記憶體大約都是1.1G,算是可以接受,但是為了學習怎麼處理往後碰到的大數據,這裡將DataFrame進行資料型態的轉換,經過一些研究:pandas會將非數字類型的資料存成object,而越有彈性的設計相對來說就要越吃記憶體,如果確認該行列之中的重複值超過50%,我們就將其轉換成category,這會大大降低記憶體的佔用,其原理就是同樣的物體pandas將其存成一個物件,再加上不同位置的標註,這確實比起object,每一筆資料都存成一個物件,省記憶體的多至於數字的部分,pandas提供了downcast的函數,如果該行的數字不需要用到很多數字,就用比較小的數字型態,例如int8支援-255到255,int16,int32,int64則是支援更多數字,也有unit及float等型態。

快速的日期格式轉換而交易日誌中有日期的數據,在等等的分析會非常重要,原本DataFrame中使用int64來表示,我們將其轉換成datetime64的格式,而考慮到效能的問題,如果使用內建函式來轉換,pandas會將數據逐一轉換,這造成了在電腦前等到天荒地老的囧境,經筆者研究之後,日期這種欄位通常重複性非常高,先在行之中取出所有單獨的日期,轉換格式之後,在map回去,就快多了。

綜合以上三點,自定義一個壓縮資料型態的函數,以及日期轉換函數,並把一些欄位的名稱改短一點比較好閱讀:Apply函數以及向量化操作這個部分筆者花了不少時間研究如何優化,畢竟在專案過程中,總是會重複讀取資料,其中還有一個可以和筆者分享的地方是停止使用applyfunction:例如:forcolindf_num.columns:df_down_num.loc[:,col]=pd.to_numeric(df_num.loc[:,col],downcast='signed')由於我們一些日常習慣的累積我們可能會寫:forcolindf_num.columns:df_down_num.loc[:,col]=df_num.loc[:,col].apply(pd.to_numeric,downcast='signed)apply的方法直覺,而且可以套用任何自訂函數,但是apply仍然是一筆資料一筆資料轉換,我們原本的寫法為一行一行資料轉換,因此apply在資料量大時的劣勢就出現了,對於交易日誌的處理資料都是使用一行一行轉換的(稱作向量化(vectorlization)),若讀者們使用apply函數一定會在操作大型資料時等到天荒地老,因此在處理transactions中的資料時,將所有可運算盡可能的向量化是非常非常有幫助的!接著我們繼續定義日期的轉換函數:先前提到的apply與向量化操作,筆者的實測是100秒以及15秒,速度差約莫6倍多!(事實上,你手上的資料越大,速度差就會越大)接著將測試集的目標變數標為缺失,並看一下缺失值分布的情況:除了目標函數is_churn之外,沒有缺失的資料,這有兩個好消息,一是我們不用在這個資料集困擾缺失值問題,二是我們已經探索了兩個資料集,他們的缺失值如果和交易日誌資料表的特徵有關係,就可以用一個很不錯的方式填補缺失值!Glanceattransactionstablemsno-->匿名idis_churn-->是否流失payment_method_id-->使用什麼管道付費payment_plan_days-->訂閱幾天plan_list_price-->該交易的價格actual_amount_paid-->該交易使用者實付多少錢(有時候可能有優惠,會看見0)is_auto_renew-->該交易是否是自動更新(KKBOX訂閱時的預設是"是")trans_date-->該交易的發生日期mem_expire_date-->該交易的會員到期日is_cancel-->是否為使用者取消(取消不一定代表流失,可能為替換方案)我們將交易紀錄分別按照使用者以及日期排序,就可以看到同一個使用者的所有交易情況被放在一起,這點似乎在bigquery中辦不到,所以就使用pandas處理,在這5筆交易裡,我們就可以看到一位使用者從2016,11,16一路訂閱kkbox,都是30天的約,然後一路續約到2017,04,15,使用的是41號管道付費,皆為99元(讀者們可以自行觀看更多資料,就會發現,99元是訂閱一段時間過後才會有的優惠價格),而續約方式都是系統自動更新,都是在即將到期時由系統自動續約,接著我們思考可能影響是否流失的特徵:是否取消(取出真正流失的部分,有一部分為更換方案)是否是回鍋使用者(表示曾經流失過)是否關閉自動更新......然而在觀察是否曾經流失過時,發現一些異常值,我們建立一個合約天數的特徵來觀察得更清楚:合約天數=會員到期日-交易日期membership=mem_expire_date-trans_date左為訓練集的分布,右為測試集。

資料清洗筆者發現有些日期不太對,可能的原因是資料庫的錯誤(例如日期跳回1970/1/1),或是直接跳到2020等等,事實上現實生活中很多這種資料,根本處理不完(如果你沒有一個專門資料清理的團隊的話),權衡之計就是評估這種資料的比例,以及他對於我們想預測的目標影響力有多大,我們在BaseModel中的使用者日誌中討論過這個問題,在交易日誌中筆者認為這些髒資料對我們想預測的目標影響力非常大,因為:如果是曾經流失過的使用者,又回鍋使用kkbox,則之後流失的機率也會比較高,這可建構出多個特徵(例如上一次是否流失,總共流失幾次......等等)而我們可以透過原本該筆交易的合約天數,來重新修正會員到期日,原本想像的大概就是這麼簡單,但是這個資料集實在很頑強,有很多難搞的地方,在說明異常值清洗的規則之前,再說明一項筆者的發現:若該交易紀錄為取消的紀錄(is_cancel=1),到期日多半和交易日相同,或是到期日在交易日後5天之內,佔了不少比例。

這很合理,畢竟如果是取消,則在取消之後幾天內到期,給個緩衝。

如果異常值是取消的交易(is_cancel=1),則直接將到期日修正為交易日。

如果異常值不是取消的交易(is_cancel=0),合約天數(membership_int)



請為這篇文章評分?