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

了解使用者流失的潛在模式

YL-Tsai
31 min readOct 24, 2018
圖片來源 : Kaggle twintter https://twitter.com/kaggle

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

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

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

目錄

Prepare

Members table

Transactions table

快速連結

User_logs reviews

結論

參考資料

Prepare

資料讀取及環境建置

讀者可以在該比賽的Data Source頁面下載資料,由於檔案數多,資料數多,其中最大的使用者聽歌日誌v1和v2合併之後csv檔案有42.05GB,約4億1千萬筆資料,我們可以採取雲端環境、分散式儲存/運算、或改變資料型態來處理,這裡筆者採用雲端環境以及改變資料型態來處理資料,如果讀者對雲端環境的建置不熟悉,可以看我上一篇文章,詳細了介紹Google Cloud雲端環境的建置以及上傳資料,熟悉的讀者,就上傳到自己的環境吧!

大致上來說是這樣的:

註 1 : 當時這項比賽在比賽期間,資料非常的原始,不少Kaggler依照主辦方的定義自己全新標註訓練資料集,得到乾淨度較高的資料,如今比賽已經結束,我們跳過重新標註資料的部分。註 2 : 筆者自己在2個月前分析時,還沒有新版的UI,中間過程有使用過新版UI上傳資料集,但是當時Schema的部分有時候讀取不正確,如果各位讀者使用新版UI沒有問題,則可以忽略,不過筆者建議轉為舊版UI。註 3 : 選擇從Storage上傳時,舊版UI會需要填入路徑,Storage的路徑可以在Storage --> 點選資料夾 --> 點選總覽 --> gsutil 連結。
例如我的就是gs://kk_data。

第一次探索性分析(First EDA)

同樣的本篇文章的重點放在特徵工程以及模型表現上,而相較上一個鐵達尼號的專案,這個比賽僅有575個隊伍參賽,相對的可以學習的資源也就少非常多,但是強大的Kaggler, Head or Tails也有加入比賽,提供了非常高品質的EDA:

(1) Should I stay or should I go by Head or Tails

Base Model

在開始測試各式各樣的特徵之前,我們需要選擇一個模型,這裡選擇(隨機森林)RandomForest以及極限梯度提升樹(xgboost),基於樹的模型再加上ensemble,提供了很好的抗噪性,而選用xgboost除了精準度確實較高之外,也在程式包裡面提供了缺失值處理的方便,可惜的是一google發現並不是使用林軒田老師說的代理孕母的技術,而是將缺失值列為一個類別,在分裂葉節點時分邊,挑結構增益較大的一邊。

接著我們從打開google cloud platform (GCP)的colud shell開始:

datalab connect kk-churn

雖然上一篇文章稱為使用Datalab及BigQuery進行大數據分析[Part 2],但實際上也就是本專案的Base Model!

使用了兩個從使用者日誌萃取出來的特徵 : 聽歌天數(day_listen)以及潛在滿意度(user_latent_satisfaction)達到log_loss = 0.15693 ,這約是Top63%。

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

Members table

首先開一個新的notebook,轉成python3,並載入先前兩個特徵,以及member資料表,接著全部合併在一起:

Glance at members table

沒什麼特別的,就是import需要的東西XD,接著讀取member資料集:

%gcs 是google storage在datalab中的magic command,在啟動datalab時就已經串接好,我們可以直接套用來讀取google storage中的資料,需要注意的是使用magic command時要單個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日註冊,有了對資料的初始認識之後,讀者亦可以搭配Head and Tails的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:

我們的Validation set error從0.19215來到0.16左右,可以看到xgb比rf有更低的誤差,但實際上是因為我們在randomforest中設定了最小分割點為0.05。

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

Age

年齡這個特徵在此資料集被稱為bd,筆者認為年齡在此資料集中應該含有一些資訊,例如較年輕的使用者(例如學生)普遍來說還沒有經濟能力,這可能導致他們有較高的流失率,反過來說,有經濟能力的使用者,普遍來說應該會有較低的流失率,先看看這個特徵分布的情況:

我們可以從min及max看到負的年齡以及2000歲的使用者,這應該是不可能的,不合理的資料無法被列入考量,畢竟garbage in, garbage out。接著我們計算一下有效的年齡共有多少百分比,在筆者的分析中取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只是找到好方法清洗資料之前的權宜之計,接著我們切分訓練集以及測試集:

確認訓練集以及測試集都完成了特徵工程的Double check,接著使用XGBoost訓練模型,這裡並沒做參數最佳化,僅僅測試特徵是否有效:

我們看到驗證集的logloss比起之前的0.16376下降到了0.16152,這證明了我們的特徵訓練出來的機率分布和測試集的目標分布更接近了,接著丟到Kaggle看看LB的分數:

LB並沒有下降的很多,僅下降了0.001左右,我們也可以在EDA當中就看出比起Registered_via,age這個特徵確實沒有那麼強的區分性,而目前的logloss使我們晉升到Kaggle Public LB的Top21%

Transactions table

接著我們看看交易紀錄這個資料表,起初筆者認為這個資料集也是蠻大的,使用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函數以及向量化操作

這個部分筆者花了不少時間研究如何優化,畢竟在專案過程中,總是會重複讀取資料,其中還有一個可以和筆者分享的地方是停止使用apply function:例如:

for col in df_num.columns:   
df_down_num.loc[:,col] = pd.to_numeric(df_num.loc[:,col],downcast='signed')

由於我們一些日常習慣的累積我們可能會寫:

for col in df_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之外,沒有缺失的資料,這有兩個好消息,一是我們不用在這個資料集困擾缺失值問題,二是我們已經探索了兩個資料集,他們的缺失值如果和交易日誌資料表的特徵有關係,就可以用一個很不錯的方式填補缺失值!

Glance at transactions table

msno                 --> 匿名id
is_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) < -30 ,按照原始合約天數(payment_plan_days)加上交易日期為到期日。
  • 如果異常值不是取消的交易(is_cancel = 0),合約天數(membership_int) < -30 ,而原始合約天數(payment_plan_days)又為0,按照實際所付價格(actual_amount_paid)對應的天數加上交易日期為到期日。
  • 最後異常值,但是合約天數(membership)在-30~450之間,由於無法推斷到底有多少情況參與作用(例如可能有優惠活動而導致多送的天數,或是不知名原因的提早幾天到期),在此當作噪聲(noise)處理。

上表我們可以看到異常值按照各情況的原始合約天數(payment_plan_days)以及實際所付價格(actual_amount_paid)的統計次數,接著進行資料清洗:

接著就是照著上面的規則一一清洗資料,先定義一個將時間間隔轉換為TimeDelta的函數,然後一一寫下情況,使用loc來操作,讀者如果直接copy程式碼可能也看得懂,裡面都有寫註解,接著對測試集做一模一樣的事情,就不重複一次,讀者如果要對照程式碼可以在最下方的完整程式碼或是github中對照,接著還需要一些處理,由於當時比賽共釋放兩次交易日誌,然而我們訓練集中是要預測3月是否流失,因此我們不能使用3月的交易數據,否則就是數據洩漏了(Data leakage),這在實務上也沒有意義,對於測試集而言我們則要丟掉4月的數據,先看看是否有要丟的數據:

我們觀察到3月的交易數據共有853,747筆,這些需要丟棄,而測試集則沒有,預處理的工作站時告一個段落,雖然並非完整的乾淨,但可以進行特徵工程,只要結果還可以接受即可,接著我們來思考各種可能影響使用者3月是否流失的特徵:

Last_last_churn

透過了解使用者在2月是否是連續使用KKBOX,或是其實1月有斷掉過,2月才回來使用,可以了解使用者是否非常需要這份訂閱,而我們也可以推測,非連續使用的使用者(last_last_churn = 1),為一個群體的話,顯然在3月的時候流失率會更高:

  • 這裡再抽取特徵時使用了shift函數,這在時間序列的資料處理中很常用到,是一個相當實用的手法,然而網路上的資料其實蠻少的,後來翻一翻發現這種稱為lag-feature的處理手法算是偏高階的手法,甚至要自己讀pandas文件才能發現! 透過shift將日期相減以及shift辨別使否為同一個使用者,我們就可以算出使用者是否是連續訂閱,藉以定義特徵。
  • 在pandas.DataFrame.groupby的函數中,cumsum沒辦法調整排序方向,所以在函數定義中再重新排序一次。

我們可以看到非連續訂閱的使用者事實上並不多,而測試集的非連續訂閱使用者又更少,其中-1為缺失值,這裡的缺失值是我們創建特徵的過程產生的,並非原本資料庫缺失,然而在創建這個特徵時哪裡會產生缺失值呢?

                          沒有上一筆資料的使用者

沒有上一筆資料的使用者當然就沒有連續訂閱的紀錄,也就是說,這個特徵還碰巧定義出了新使用者(僅有一次訂閱紀錄),按照這個邏輯推測,我們也大致了解視覺化的大概結果,連續訂閱的使用者流失率最低,再來是新使用者,接下來是非連續訂閱的使用者,按照這個預期結果,我們來看看三個類別對於流失率的統計結果:

橫軸為last_last_churn(表示尚上次使否流失,0表示為連續訂閱的使用者,-1表示為新使用者,1表示為非連續訂閱的使用者,縱軸為流失率,黑線為95%信賴區間。)
  • 在了解資料之後,出來的統計結果符合預期,而且信賴區間也不會太寬,表示有一定的可信度,並且我們可以量化的看到結果,新使用者的流失率約是20%,而非連續使用者的流失率更是高達50%

接下來我們丟到XGBoost來進行測試,首先確定特徵工程是完整的,並選定特徵:

接著進行訓練:

接著上傳到Kaggle觀察testing set的logloss:

我們從0.13161下降到了0.12698,這是Top16%,筆者認為抓出新的使用者以及辨別出非連續使用者是非常有用的。

Trans_times and client_level_code

在交易記錄的資料集中我們有每一個使用者的交易紀錄,我們可以經由交易次數大略推估使用者對這個訂閱服務的喜好程度,這有助於我們辨別持續使用者的流失情況以及分群:

  • 從交易紀錄的DataFrame中以每個使用者為一組,計算共有幾列,這項特徵的工程難度並不高,接著要按照交易紀錄次數分組,這裡取1(新使用者,1~6次,6~12次,12~24次,24次以上),我們可以預期越是老顧客直覺上越不會流失,這裡筆者隱含假設了每個交易紀錄是1個月,因此超過1個月的用這種方式分組就會顯得不適合,在此當作噪聲(noise),瞭解這一點在往後改進模型以及協助決策簡報及討論時能夠發揮很好的效用:
左圖為交易次數對流失的統計,y軸為log-scale,藍色為未流失,綠色為流失,右圖為分組的情況,分別按照0~1次,1~6次,6~12次,12~24次,24~最大值分組,黑線為95%信賴區間,右圖的圖內圖為每個分類的占比,虛線為平均流失率。
  • 我們可以看到左圖在交易次數40次以上的藍色有突出的一塊,這印證了我們的直覺,老客戶們的流失率是更低的,同時也反映在右圖的第4個類別,流失率很低,低於2.5%
  • 右圖的類別0和上一個特徵Last_last_churn有重複解釋的部分,都表示為新使用者,也可以當作驗算,流失率為20%,接下來隨著訂閱次數的累積,使用者的流失率逐漸穩定下來,在類別3之後的流失率開始低於平均,我們亦可以依據此資料粗估,使用者在訂閱1年後,逐漸被過濾出喜歡kkbox服務的顧客。

接著,我們丟入XGBoost進行訓練,參數這裡沒有多做調整,只需加入client_level_code這個分組特徵,這裡不再重複一次,但你可以在最下面的完整程式碼找到! 這裡附上上傳至Kaggle的logloss:

我們可以看到加入了client_level_code之後logloss並沒有降很多,約為0.0025。

Last_auto_renew

是否自動更新這個特徵筆者認為某種程度顯示了使用者的經濟情況以及對商品的愛好度,畢竟一個不算充裕或是沒有很喜歡產品的使用者在發現了商品會自動延續訂閱這件事之後多半會覺得是不是想要坑消費者,進而取消自動更新(像筆者自己就是一個會取消自動更新的消費者......),針對這個特徵我們以使用者為單位,來看看每一位使用者所有交易紀錄中,自動更新的比率:

圖為使用者自動更新的比率的次數分布,y軸為統計次數,使用square-scale。
  • 這裡從圖我們可以發現,多數的使用者都是在兩端,意即目前都自動更新或是目前都手動更新,目前自動更新的使用者佔所有訓練集使用者的80.8%,目前都是手動更新的使用者為10.9%,有調整過的則為8.4%。
  • 而針對目前皆自動更新的使用者以及目前皆手動更新的使用者計算流失比率,分別為1.3%以及28.7%。這是一個相當有效的分類,並且在這個類別不平均的任務當中,算是樣本數較平均的一項!,比例達到高流失:低流失 ~ 1 : 8。
  • 接下來粗略的建造一個特徵,我們取出上一次交易是否為自動更新來做為特徵,對於上述的觀察現象,則是忽略中間的情況,算是一種抽樣調查的結果:
橫軸為上一次是否為自動更新,1表示為自動更新,0表示為手動更新,縱軸為流失率,表格為類別所佔比例,虛線為平均流失率,黑線為95%信賴區間。
  • 我們可以看到我們的抽樣結果和剛剛的觀察有一些差異 12.1% : 87.9%,這來自於忽略中間自動更新比率在0~1之間,以及隨機抽樣的結果,流失率的部分,我們發現和剛剛的觀察是差不多的,手動更新的使用者平均流失率為28%,自動更新則低於2.5%。

接著我們丟入XGBoost進行測試,並將結果上傳至Kaggle,同樣的,只需加入特徵,並沒有調整參數,所以這裡不列出程式碼,但你可以完整程式碼的部分找到:

加入last_auto_renew之後,我們的logloss來到了0.11617,這是LeaderBroad的Top7%,使用者在是否自動更新這個欄位所帶有的資訊由此可知是相當大的。

User_logs review

One_month_day_listen

實際上筆者在加入以上特徵之後還是在交易紀錄這DataFrame裡東翻西找,但是找了5,6個特徵效果都普普,結果回來再找了User_log中的特徵,之前在使用Datalab及BigQuery進行大數據分析[Part 2]中我們討論了聽歌天數這個特徵,當時討論了有效性以及缺失值的比例,最後選了過去6個月的聽歌天數,似乎可以達到最好的效果,然而過長的時間也把較為即時的使用者行為平均掉或是累加掉了,比起只選一個特徵,我們意可以加入過去一個月的聽歌天數,使用兩個特徵來描繪使用者在不同時間尺度下的行為,而驗證集的測試也表明了效果還不錯!

%bq dryrun -q query
%bq extract -f csv -H -p 'gs://kk_data/ft_one_month_day_listen.csv' -q query --verbose

這裡使用標準SQL語法從BigQuery中撈取資料,先創建一個暫時表(CTE),接著使用LEFT JOIN將兩張表結合起來,讀取至Google storage,接著對測試集做一樣的事,但是測試集是要預測4越是否流失,所以我們要撈取3月的資料:

%bq dryrun -q query
%bq extract -f csv -H -p 'gs://kk_data/sub_ft_one_month_day_listen.csv' -q query --verbose

僅僅更改日期範圍,JOIN的表,以及存取的檔案名稱,接著從Google Storage讀取進來,並和原本的訓練集及測試集接在一起:

%gcs read --object gs://kk_data/ft_one_month_day_listen.csv -v df_one_month_day_listen%gcs read --object gs://kk_data/sub_ft_one_month_day_listen.csv -v df_sub_ft_one_month_day_listen

同樣的,兩行讀取命令必須分開執行。

圖為過去一個月的聽歌天數分布圖,縱軸為統計比率,其中藍色為流失的,綠色為未流失的。

類似的結果,我們可以看到聽歌天數較多的未流失的比例逐漸升高,而只取過去一個月可以比起過去六個月了解更近期的使用者行為,接著丟進模型測試並提交

結論

我們的logloss來到了0.11335,透過萃取使用者在聽歌行為,以及訂閱情況,以及註冊時的基本資料,我們建立了一個可以預測使用者下個月是否回流失的模型,最後我們使用了8個特徵,分別是:

User_logs
six_month_day_listen 過去六個月內的使用者聽歌次數
six_month_user_latent_satisfaction 過去六個月內的使用者聽歌滿意度
one_month_day_listen 過去一個月內的聽歌次數
members
registered_via 經由?裝置註冊(推測)
bd 註冊時年齡
transactions
last_last_churn 連續/非連續使用者/新使用者
client_level_code 註冊次數
last_auto_renew 上一次交易是否自動更新
其中歌曲滿意度定義為
(100%的歌曲數) / (所有聽歌的歌曲數(25%,50%,75%,95%,98.5%,100%))

在Kaggle競賽中達到了Top5%的成績!

GitHub

完整程式碼

原本給自己的時間是100天,在這個past competition中達到Top5%,結果大約50天就完成了,也對顧客流失的議題有比較務實的了解,同時也學到了一些處理大數據的方式,對於初步了解機器學習並想更進一步的自己,算是相當有幫助的,SQL是資料科學家繼Python之後第2重要的程式語言,在這裡也有一個初步的了解,想學甚麼,果然直接透過一個專案來實做事最快的!

同時我的Medium上也會繼續記錄各種我嘗試過Machine Learning的side project,如果讀者們有興趣,就按下Follow吧!

--

--

YL-Tsai

Machine Learning Engineer with 4y+ experience | Exploring the data world | Recommendation, Search, Ad System.