我如何分析客戶流失預測?Kaggle比賽思路分享

資料探勘對我來說真的是一件很有趣的事情!

Kaggle是什麼?

簡單提一下Kaggle,Kaggle是什麼呢?簡單來說就是一個資料科學家的聖地,聚集了全世界的高手以及新手,是資料人不可不知的地方,上面提供了包括資料集、教學、部落格等功能,最大的特色就是常常會舉辦全世界的數據科學比賽,很適合在那裡練功。我非常喜歡參加一些比賽來保持手感,每場比賽過後也能有自己可以學習的地方。畢竟如果沒有學習、工作,生活中比較少有機會接觸到業界資料,而Kaggle可以讓我接觸到這些資料,非常棒!除此之外,Kaggle有牌位的機制,我很喜歡這種藉由比賽提升自己個人檔案的做法(我的計畫是大三開始狂打牌位賽,目前都是拿資料來玩玩而已)可以更好認識平台上有哪些高手、自己看見學習痕跡等等…….

先來聊聊,如何用上網精進自己?

畢竟上網是輕鬆的,能夠利用上網的時間精進自己也不失為一個學習的好方式。Kaggle應該是我最常去的網站之一,相比同齡人都在上Dcard、PTT(我都睡前滑)應該是我最常去的地方,當然Dcard、PTT也有很多議題可以追蹤,每個網站的屬性不同。我覺得現在的時代如果想要吸收知識,刻意關注某些網站其實是個很棒的方式。

因為逛久了不知不覺也能累積很多自己不會發現到的軟實力,好一點的說法就是sense

有意識地利用論壇,其實類似以天地為師,了解不同知識。

以我來說,我目前的職涯規劃是當一個數據分析型的產品經理或者顧問,需要理解財務、產品、分析相關的知識,那麼我最想培養的能力就會是analysis、finance、business sense、engineer、design,因此我就會常看相關的網站,好比:

github(觀摩程式碼以及看看有趣的作品、產品)、pinterest(美美的圖片與培養美感,很放鬆),這些地方都是能夠培養自己某些層面的。我建議您可以多了解相關產業的人都會看一些什麼樣的媒體,來追蹤產業的方向。

進入文章正題,我如何分析客戶流失?

客戶流失已經是產品分析的重要一環!

客戶流失是必然的,我們能做的事情就是挽回客戶,對客戶行為做預測防止產品使用人數下降,2016年以後,電商的客戶增長速度明顯下降,16年以前的流量紅利已經不存在了,要想在科技巨頭下存活,就必須要Know your customer!不可以不關心客戶流失的問題。

比賽網址:https://www.kaggle.com/blastchar/telco-customer-churn

基本上是一個客戶流失的預測競賽 這個資料集非常小,才7000多筆資料,蠻適合作為初學者的比賽。 相比另外一個KKbox的流失預測有90萬多筆資料,剛好可以做為練習場暖暖身手。 比賽主辦方是這麼說的: “Predict behavior to retain customers. You can analyze all relevant customer data and develop focused customer retention programs.”

So yet ,目標很單純,大概可以用logistic , randomforest下去做。 那我們的資料有一個很大的part是在講說客戶有沒有使用這個服務。做資料科學有一個很重要的原則是,不要直接跳進資料裡打滾,而是先根據你對業務知識的假設來利用資料驗證,所以第一步,我們必須看看資料的樣子,提出大概三個假設讓我們的目標明確一點。 資料的樣子Kaggle上都有了,所以看一下metadata,稍微了解之後我們就可以正式出發! 值得注意的是kaggle的metadata非常方便,有簡單畫出資料的形狀,所以我們也可以在閱讀資料的時候簡單看一下資料分布、二元資料比例,這樣方便我們對資料做假設以及提前洞察極端值。

領域知識

商學院做資料科學的好處,就是我們學習過很多商業知識!

我曾經在政大商學院修過財務管理、資訊管理、管理學,對必修XD

這時候必須賣弄一下領域知識,也就是管理課程提到的客戶關係管理:

在客戶流失裡面,有一個業務知識需要先知道,就是商學院說的:「客戶關係管理」 流失基本上分成志願性流失以及非志願性流失,志願性流失通常是因為產品、公司、競爭對手,使得客戶往外走、移向另外一個產品。那非志願性流失就是遭逢公司遷廠、網址搬家使得客戶找不到等等……並非公司不好所造成的流失,因此在客戶流失裡面,我們只會把重點放在「志願性流失」。

那麼有哪些常用的客戶流失指標呢?其實我們不一定要用顧客資料來預測流失率(算是小小題外話)試想,客戶流失如果是根據產品本身,那我們透過市場規模、成長、競爭者分析也能洞察,這也透露了一件事情: 資料探勘可以有很多個面向,我們可以尋找相關的指標來挖出insight 所以實務上常常會根據你有什麼資料、資料好不好取得來作為指標構建的衡量。 而客戶流失在實務上可以與管理階層、產品部門、市場調研開會討論: • 產品 • 服務 • 員工 • 企業形象 根據這些面向去探討,利用金字塔思維拆解各個產品價值鏈的步驟是很管用的方式。

可以參考的網站:

1. 美國市場營銷協會:AMA的報告
2. Google看前五頁左右的報導
3. MBA智庫百科的相關領域知識補充

利用Python協助我們分析!

也可以用R、tableau,我自己是Python的愛好者,並不是很習慣R的函數式編程方式(可能跟我第一門學習的語言是Java有關),我自己很喜歡先利用Python處理好資料、做一些指標,再把資料放到tableau做EDA(explore data analysis),因為效率會好很多,也可以讓我專心在分析資料細節上!推薦這樣的分析方式給各位資料科學家。

首先讀入相應的library幫助我們調用函數:

# 讀入習慣的資料科學套件
import numpy as np
import pandas as pd
import os
import matplotlib.pyplot as plt
import seaborn as sns
%matplotlib inline
#讀入資料,這邊要注意檔案的儲存路徑!
df = pd.read_csv('../input/WA_Fn-UseC_-Telco-Customer-Churn.csv')
df.head()

簡單看一下數據:

# 查看數據的變數、總數、缺失數據、變數measurement(維度)
def data_overview():
print("Rows : " , df.shape[0])
print("Columns: " , df.shape[1] )
print('Missing Value number : ' , df.isnull().sum().values.sum()) #isnull.sum()會對每條series做sum up ,所以我們還要取出value做一次sum up .
print('\nUnique values' , df.nunique())
data_overview()

OK ! 到這邊我可以看到大部分的分類都是三級,有不了解的就回到比賽首頁詳讀數據解說,務必搞懂數據有哪些! 我習慣把metadata(資料敘述語言)紀錄在markdown,方便查看變數的定義為何:

Tenure : Number of months the customer has stayed with the company

這邊有趣的點是,我們發現MonthlyCharges有很多都是相同的,代表在這7000多筆資料中其實能夠找到付費習慣的pattern,可以預期這是一份有意義的資料分析專案。

Data manupulation

一個比較好的思路是,我們通常會簡單地檢查一下數據情況,然後將它們清洗乾淨送進資料倉儲(data warehouse) 而清洗原則除了透過上面的overview,另外還有metadata的閱讀與理解。 所以第一步是非常簡單EDA一下,通常要注意的點是:

  1. 需要把連續型變量分箱嗎?
  2. 缺失值的處理
  3. NaN值的處理
  4. 異常填補的處理,尤其是那種沒有missing value的數據集更要嚴謹處理
# 作圖發現有極端值的嫌疑
sns.distplot(df.tenure)
#利用這條式子查看是否存在異常值,該值如果比0.8大就很值得懷疑數據經過不正確的填補
def check_bad_smell(df):
error_event =abs((df.mode().iloc[0,] - df.median())/df.quantile(0.75) - df.quantile(0.25))
problems = error_event[error_event>0.8]
print(problems)
return problems.index.tolist()
bad_smell = check_bad_smell(df)
plt.figure(figsize = (8,3))
plt.title('Right skew')
#sns.distplot(df.TotalCharges) 這裡我們發現有空白值
# 替代掉空白值當作null
df['TotalCharges'] = df.TotalCharges.replace(' ' , np.nan)
#看看少了哪些
print('缺失數據變為:')
print(df.TotalCharges.isnull().sum())
# 查看都是哪些類型的數據缺失
df[df.TotalCharges.isnull()]
# 查看分佈
plt.figure(figsize = (12,4))
sns.distplot(df.TotalCharges.notnull().astype(float))
#還蠻驚人的,這個分佈表示存在嚴重的極端值,那麼要做數據填補很困難,所以我們把那11筆數據刪除
print('清除缺失值')
df = df[df.TotalCharges.notnull()]
df = df.reset_index()[df.columns]
#再轉換一次型態
df.TotalCharges = df.TotalCharges.astype(float)
#查看原始數據,我們知道二分數據為Yes/No,我們想要把他們編碼,這樣方便之後的modeling
df = df.replace({'Yes':1 , 'No' :0})
df.head()
#發現還有一個叫做No phone service ,相當於No,看來這筆資料也是蠻髒的,有記錄格式不統一的問題
df = df.replace({'No phone service':0})
df.head()
#連續型變量我們可以轉換成分箱變數,為什麼呢?因為上面的探索我們發現有極端值,這樣的連續型變數是很危險的
print(df.tenure.describe())
#我們發現數據還蠻平衡的,所以採取等寬分箱(range相等)
#撰寫一個將tenure轉變為離散變量的函數
def tenure_to_bins(series):
labels = [1,2,3,4,5]
bins = pd.cut(series , bins = 5 , labels = labels)
return bins
temp_tenure = df.tenure
df['tenure_group'] = tenure_to_bins(temp_tenure)
df.head()
資料表變成這樣了

接下來為了EDA方便,我們將兩類數據分開:

# 將兩類數據分開
churn = df[df.Churn == 1]
not_churn = df[df.Churn == 0]
# 將類別變數與連續變數分開
Id_col = ['customerID']
target_col = ['Churn']
cat_cols = df.nunique()[df.nunique() < 6].keys().tolist() #取出Series.index,轉成一個list
cat_cols = [col for col in cat_cols if col not in target_col]
num_cols = [x for x in df.columns if x not in Id_col + target_col + cat_cols]

EDA

在數據分析的時候,EDA是我最喜歡的步驟,因為可以深入去挖掘使用者的行為特徵以及資料,不過也要謹記需要先確立好分析架構,否則分析會變得過於發散,畢竟「分析」這項工作可以是無窮的,確保先了解商業目標、取得決策者的同意,才能形成有價值的分析。我會用plotly這個套件來協助我的Python畫圖工作,seaborn也很好用,但是跟plotly相比視覺效果便沒那麼好,適合快速探索時使用,畢竟plotly的程式碼比較複雜一些。

一樣先讀入plotly套件:

# 先導入相關套件
import plotly.offline as py
py.init_notebook_mode(connected=True) #為了能在本地端調用
import plotly.graph_objs as go
import plotly.tools as tls
import plotly.figure_factory as ff

這裡有個小技巧,就是把所有我們要看的資料都依照流失、非流失客戶畫出來,這樣做有什麼好處呢?我們可以藉由直接比較兩張圖表(流失、非流失)來觀察使用者的特徵,是個很棒的方式,但是程式碼比較複雜一些,思路就是透過找出想看的columns以及拿出values,畫到圓餅圖上:

lab = df.Churn.value_counts().keys().tolist()
values = df.Churn.value_counts().values.tolist()
trace = go.Pie(labels = lab , values = values ,
marker = dict(colors = [ 'royalblue' ,'lime'],
line = dict(color = "white",
width = 1.3)
),
rotation = 90,
hoverinfo = "label+value+text",
hole = .5
)
layout = go.Layout(dict(title = "Customer attrition in data",
plot_bgcolor = "rgb(243,243,243)",
paper_bgcolor = "rgb(243,243,243)",)
)
data = [trace]
fig = go.Figure(data=data , layout=layout)
py.iplot(fig)
得到客戶流失的比例了!

這裡我們得出一個資訊,其實通常客戶流失、信用欺詐等數據分析專案都是所謂的「不平衡資料集」,也就是說流失屬於「稀少事件」,那麼在大型數據(幾億筆資料)我們會用抽樣方法來平衡兩者 不過今天這筆資料集只有7000多,我們可以改用加權的方式處理。

接下來我用到物件導向程式設計的概念,因為我們需要好幾張圖來協助我們分析資料,那不如把操作過程都寫在函數裡,這樣在重複一件事情上面,調用函數會有效率得多,能讓我們更專心在分析上。這裡的程式碼很多,不要被嚇到XD其實我也是一邊看官方文件、一邊寫的,我的開發環境是Jupyter notebook,而Jupyter好用的地方就在於它能夠先開一個cell來測試自己的寫法對不對,對了再把他編寫成函數即可。

這麼多語法我們不可能自己都背起來,通常我在寫程式的時候會開著官方文件的頁面對照、思考怎麼改成自己的code,也就是「取用」的寫法,在數據分析領域技術層出不窮,掌握分析思路才是我們應該關心的重點,我不會特別記住複雜的可視化語法,但能夠掌握語法邏輯就足夠了!

#由於EDA會需要用到很多可視化分析,我們可以撰寫plotly的funciton來協助我們更快進行EDA,當然也可以將資料儲存為excel or csv檔,換到知名的數據可視化軟體tableau來做
#function for pie plot for customer attrition types
def plot_pie(column) :

trace1 = go.Pie(values = churn[column].value_counts().values.tolist(),
labels = churn[column].value_counts().keys().tolist(),
hoverinfo = "label+percent+name",
domain = dict(x = [0,.48]),
name = "Churn Customers",
marker = dict(line = dict(width = 2,
color = "rgb(243,243,243)")
),
hole = .6
)
trace2 = go.Pie(values = not_churn[column].value_counts().values.tolist(),
labels = not_churn[column].value_counts().keys().tolist(),
hoverinfo = "label+percent+name",
marker = dict(line = dict(width = 2,
color = "rgb(243,243,243)")
),
domain = dict(x = [.52,1]),
hole = .6,
name = "Non churn customers"
)
layout = go.Layout(dict(title = column + " distribution in customer attrition ",
plot_bgcolor = "rgb(243,243,243)",
paper_bgcolor = "rgb(243,243,243)",
annotations = [dict(text = "churn customers",
font = dict(size = 13),
showarrow = False,
x = .15, y = .5),
dict(text = "Non churn customers",
font = dict(size = 13),
showarrow = False,
x = .88,y = .5
)
]
)
)
data = [trace1,trace2]
fig = go.Figure(data = data,layout = layout)
py.iplot(fig)
#function for histogram for customer attrition types
def plot_hist(column) :
trace1 = go.Histogram(x = churn[column],
histnorm= "percent",
name = "Churn Customers",
marker = dict(line = dict(width = .5,
color = "black"
)
),
opacity = .9
)

trace2 = go.Histogram(x = not_churn[column],
histnorm = "percent",
name = "Non churn customers",
marker = dict(line = dict(width = .5,
color = "black"
)
),
opacity = .9
)

data = [trace1,trace2]
layout = go.Layout(dict(title =column + " distribution in customer attrition ",
plot_bgcolor = "rgb(243,243,243)",
paper_bgcolor = "rgb(243,243,243)",
xaxis = dict(gridcolor = 'rgb(255, 255, 255)',
title = column,
zerolinewidth=1,
ticklen=5,
gridwidth=2
),
yaxis = dict(gridcolor = 'rgb(255, 255, 255)',
title = "percent",
zerolinewidth=1,
ticklen=5,
gridwidth=2
),
)
)
fig = go.Figure(data=data,layout=layout)

py.iplot(fig)

#function for scatter plot matrix for numerical columns in data
def plot_scatter(df) :

df = df.sort_values(by = "Churn" ,ascending = True)
classes = df["Churn"].unique().tolist()
classes

class_code = {classes[k] : k for k in range(2)}
class_code
color_vals = [class_code[cl] for cl in df["Churn"]]
color_vals
pl_colorscale = "Portland"pl_colorscaletext = [df.loc[k,"Churn"] for k in range(len(df))]
text
trace = go.Splom(dimensions = [dict(label = "tenure",
values = df["tenure"]),
dict(label = 'MonthlyCharges',
values = df['MonthlyCharges']),
dict(label = 'TotalCharges',
values = df['TotalCharges'])],
text = text,
marker = dict(color = color_vals,
colorscale = pl_colorscale,
size = 3,
showscale = False,
line = dict(width = .1,
color='rgb(230,230,230)'
)
)
)
axis = dict(showline = True,
zeroline = False,
gridcolor = "#fff",
ticklen = 4
)

layout = go.Layout(dict(title =
"Scatter plot matrix for Numerical columns for customer attrition",
autosize = False,
height = 800,
width = 800,
dragmode = "select",
hovermode = "closest",
plot_bgcolor = 'rgba(240,240,240, 0.95)',
xaxis1 = dict(axis),
yaxis1 = dict(axis),
xaxis2 = dict(axis),
yaxis2 = dict(axis),
xaxis3 = dict(axis),
yaxis3 = dict(axis),
)
)
data = [trace]
fig = go.Figure(data = data,layout = layout )
py.iplot(fig)
# 將所有類別變數畫出來
for col in cat_cols:
plot_pie(col)

寫一個loop把他們畫出來,會畫出如下的圖形:

此時就可以觀察兩類別客戶的特徵囉!

接下來就是耐心看圖表,不停問為什麼、注意異常,以及深入思考可能的原因,這邊就留給大家解讀,或者我的結論如下:

  1. 男女流失比例差不多,顯示男女並非太大的影響關聯
  2. 在流失人群中,Senior citizen的比例要高一些(久了胃口就養大了(??)
  3. 沒有伴侶的人流失比例比較高,可能是沒人可以打電話XXD
  4. 沒有小孩的人流失比例比較高,理由同上XDD
  5. 有沒有使用電話服務沒什麼關聯,顯示不是聯絡的問題
  6. 多隻以上的電話服務? 總之有使用其他方案的人流失率比較高,蠻合理的
  7. 使用No網路服務的人很少流失,而使用fiber的人應該蠻多的(流失、非流失都有蠻高比例),可見網路應該是個關鍵因素
  8. 使用才一個月的流失率很高,表示許多顧客或許都是所謂的顧客紅利人,上級需要思考能夠提升客戶黏著度的方案
  9. 無紙化計費的流失比例較高,同樣可以呼應電子帳單使用者較易流失,然而作為科技的進步,這是一個難以避免的趨勢,另外我們發現在留存客戶中的支付方式都很平均,顯示支付方式並不是客戶留不留存的關鍵!

那麼連續資料怎麼處理呢?通常我們都是看「分佈」!

for col in num_cols:
plot_hist(col)
畫出這樣的分佈圖,可以讓我們觀察數據分佈情形。

其實還有很多張圖,這邊我就展示一張圖就好,那我的觀察是:

我們可以很清楚地看到,價格顯然是一個嚴重影響客戶流失的因子,基本上沒有支付多少費用的客戶留存度就不高,顯示客戶付出少少的錢買一個體驗,或許是被行銷活動吸引過來,而後續無法吸引這類客戶的結果。 那為什麼接近0會這麼高,想是因為客戶非常快就流失了,造成總付費的價格很低。 那這裡我會好奇,為什麼沒有流失的人每個月付的錢比流失的人少很多呢?我想是因為流失的人基本上都是使用服務超過一年的人,或許有年費制度之類的,我們可以寫信給業務部門詢問相關的猜測。 如此一來推論或許是:新客受到行銷活動吸引,後來被價格嚇跑,所以一個初步的策略便是降低第一次使用的價格,或者將付費機制打包成以年費計,以較低的每月價格換取較為穩定的客戶留存。 上面的圓餅圖我們發現留存兩年以上的客戶流失率非常低。

所以下一步,應該是驗證上述的假設,我們可以畫出tenure_group的分組情形驗證!

plt.figure(figsize = (20,8))
sns.countplot(x = df.tenure_group , hue = df.Churn,palette=("Blues_d"))
plt.legend(['Non Churn' , 'Churn'])
第一組跟第二組也差太多了!

這裡可以更明顯知道,如果要減少客戶流失,務必把客戶從第一群分到第二群,之後就會大致維持。更好的可視化呢?我們畫一張三維的圖,藉由總收費、月費、客戶停留時間來觀察:

利用更高維度去觀察我們的假設!

這是一個很明顯山丘狀的空間,我們可以進一步確認出:時間與金錢可能有交互作用存在,也就是說剛開始使用的時候如果遇到高一點的費用索取,客戶會流失得更快。 為了確認這個推論,接下來的建模階段我們會先做統計檢定。

Data modeling

那好,接下來我們正式進入資料科學最性感的建模階段,那因為我會使用Python著名的機器學習套件Sklearn,而Sklearn只吃連續變量,所以我們要先替類別變數編碼,此外,由於原始資料有21個變數,但是在建模的時候可能不會用到那麼多,一來是統計共線性的問題,二來是有些變數真的跟預測流失不存在關聯,就會需要用到數據降維的技術,比如說PCA或者人為篩選變量,一樣此篇教學的重點並不是教大家如何coding,而是整個數據分析的思路體系,我有將程式碼寫上相關註解,有不懂的地方歡迎留言詢問,對如何coding跟程式思路掌握好奇的話,可以看看我寫的【如何coding】系列教學:

#二元變數
bin_cols = df.nunique()[df.nunique()==2].keys().tolist()
#多元變數
multi_cols = [col for col in cat_cols if col not in bin_cols]
multi_cols
# 讀入需要的套件
# 類別編碼我們利用label 處理,那logistic必須要做標準化,又數據分佈在EDA的時候我們知道並不是常態的,所以繼續用Standard method
from sklearn.preprocessing import LabelEncoder
from sklearn.preprocessing import StandardScaler
#二元變數
bin_cols = df.nunique()[df.nunique()==2].keys().tolist()
#多元變數
multi_cols = [col for col in cat_cols if col not in bin_cols]
#將二元數值編碼
# cato = df.tenure_group.cat.codes
# df.tenure_group = cat
le = LabelEncoder()
# df[multi_cols] = df[multi_cols].replace({0:'No' , 1:'Yes'})
for col in bin_cols:
df[col] = le.fit_transform(df[col])
#將多元編碼,因為沒有序列性,我們用one-hot encoding
df = pd.get_dummies(data = df , columns=multi_cols)

# 處理連續變量
std = StandardScaler()
scaled = std.fit_transform(df[num_cols])
scaled = pd.DataFrame(scaled,columns=num_cols)#因為fit_transform會得到一群序列,所以我們還要把他們弄成data frame
# 將連續變量塞回df
df_origin = df.copy()
df = df.drop(columns=num_cols , axis = 1)
df = df.merge(scaled , left_index=True , right_index=True , how = 'left')

做PCA:

from sklearn.decomposition import  PCApca = PCA(n_components= 2)X = df[[col for col in df.columns if col not in Id_col + target_col]]
Y = df[target_col + Id_col]
#得到兩組PC,每個變數對應權重
pc = pca.fit_transform(X)
pca_data = pd.DataFrame(pc , columns=['PC1' , 'PC2'])
pca_data = pca_data.merge(Y , left_index = True , right_index = True , how = 'left')
pca_data = pca_data.replace({1:'Churn' , 0: 'Not Churn'})
def pca_scatter(target,color) :
tracer = go.Scatter(x = pca_data[pca_data["Churn"] == target]["PC1"] ,
y = pca_data[pca_data["Churn"] == target]["PC2"],
name = target,mode = "markers",
marker = dict(color = color,
line = dict(width = .5),
symbol = "diamond-open"),
text = ("Customer Id : " +
pca_data[pca_data["Churn"] == target]['customerID'])
)
return tracer
layout = go.Layout(dict(title = "Visualising data with principal components",
plot_bgcolor = "rgb(243,243,243)",
paper_bgcolor = "rgb(243,243,243)",
xaxis = dict(gridcolor = 'rgb(255, 255, 255)',
title = "principal component 1",
zerolinewidth=1,ticklen=5,gridwidth=2),
yaxis = dict(gridcolor = 'rgb(255, 255, 255)',
title = "principal component 2",
zerolinewidth=1,ticklen=5,gridwidth=2),
height = 600
)
)
trace1 = pca_scatter("Churn",'red')
trace2 = pca_scatter("Not Churn",'royalblue')
data = [trace2,trace1]
fig = go.Figure(data=data,layout=layout)
py.iplot(fig)

做完PCA之後,我們可以根據剩餘變量刻畫客戶的雷達圖,雷達圖本來用於股票分析與財務結構檢視(像是知名財經網站:財報X 的財務健檢功能就包含了雷達圖),但是用在顧客分析其實意外合適!

#二元變數我們做雷達圖出來表示
bi_cs = bin_cols
dat_rad = df[bin_cols]
#畫出雷達圖
def plot_radar(df,aggregate,title) :
data_frame = df[df["Churn"] == aggregate]
data_frame_x = data_frame[bi_cs].sum().reset_index()
data_frame_x.columns = ["feature","yes"]
data_frame_x["no"] = data_frame.shape[0] - data_frame_x["yes"]
data_frame_x = data_frame_x[data_frame_x["feature"] != "Churn"]

#count of 1's(yes)
trace1 = go.Scatterpolar(r = data_frame_x["yes"].values.tolist(),
theta = data_frame_x["feature"].tolist(),
fill = "toself",name = "count of 1's",
mode = "markers+lines",
marker = dict(size = 5)
)
#count of 0's(No)
trace2 = go.Scatterpolar(r = data_frame_x["no"].values.tolist(),
theta = data_frame_x["feature"].tolist(),
fill = "toself",name = "count of 0's",
mode = "markers+lines",
marker = dict(size = 5)
)
layout = go.Layout(dict(polar = dict(radialaxis = dict(visible = True,
side = "counterclockwise",
showline = True,
linewidth = 2,
tickwidth = 2,
gridcolor = "white",
gridwidth = 2),
angularaxis = dict(tickfont = dict(size = 10),
layer = "below traces"
),
bgcolor = "rgb(243,243,243)",
),
paper_bgcolor = "rgb(243,243,243)",
title = title,height = 700))

data = [trace2,trace1]
fig = go.Figure(data=data,layout=layout)
py.iplot(fig)
#plot
plot_radar(dat_rad,1,"Churn - Customers")
plot_radar(dat_rad,0,"Non Churn - Customers")
類似這樣的雷達圖,可以協助我們判讀客戶特色!

看了以前的資料才發現,原來真的是合約的關係。基本上流失的客戶幾乎都沒有簽一年期合約,反而都簽月份的合約,這告訴我們有必要檢討收費制度,此外,使用Fiber網路的比較容易流失,也是蠻有趣的一件事情。

Model Building

from sklearn.model_selection import  train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import confusion_matrix , accuracy_score , classification_report
from sklearn.metrics import roc_auc_score , roc_curve , scorer
from sklearn.metrics import f1_score
import statsmodels.api as sm
from sklearn.metrics import precision_score ,recall_score
from yellowbrick.classifier import DiscriminationThreshold
#splitting train and test data
train , test = train_test_split(df , test_size = 0.25 , random_state =42 )
cols = [col for col in df.columns if col not in Id_col + target_col]
train_X =train[cols]
train_Y = train[target_col]
test_X = test[cols]
test_Y = test[target_col]
#建模的時候通常會用不只一個演算法去實作
def select_model_prediction(algorithm,training_x,testing_x,
training_y,testing_y,cols,cf,threshold_plot) :

#model
algorithm.fit(training_x,training_y)
predictions = algorithm.predict(testing_x)

#分類模型的機率我們要自己掌握,否則容易有偏差,尤其我們在EDA的時候已經看過
probabilities = algorithm.predict_proba(testing_x)
#coeffs
if cf == "coefficients" :
coefficients = pd.DataFrame(algorithm.coef_.ravel())
elif cf == "features" :
coefficients = pd.DataFrame(algorithm.feature_importances_)

column_df = pd.DataFrame(cols)
coef_sumry = (pd.merge(coefficients,column_df,left_index= True,
right_index= True, how = "left"))
coef_sumry.columns = ["coefficients","features"]
coef_sumry = coef_sumry.sort_values(by = "coefficients",ascending = False)

print(algorithm)
print("\n Classification report : \n",classification_report(testing_y,predictions))
print("Accuracy Score : ",accuracy_score(testing_y,predictions))
#confusion matrix
conf_matrix = confusion_matrix(testing_y,predictions)
#roc_auc_score
model_roc_auc = roc_auc_score(testing_y,predictions)
print("Area under curve : ",model_roc_auc,"\n")
fpr,tpr,thresholds = roc_curve(testing_y,probabilities[:,1])

#plot confusion matrix
trace1 = go.Heatmap(z = conf_matrix ,
x = ["Not churn","Churn"],
y = ["Not churn","Churn"],
showscale = False,colorscale = "Picnic",
name = "matrix")

#plot roc curve
trace2 = go.Scatter(x = fpr,y = tpr,
name = "Roc : " + str(model_roc_auc),
line = dict(color = ('rgb(22, 96, 167)'),width = 2))
trace3 = go.Scatter(x = [0,1],y=[0,1],
line = dict(color = ('rgb(205, 12, 24)'),width = 2,
dash = 'dot'))

#plot coeffs
trace4 = go.Bar(x = coef_sumry["features"],y = coef_sumry["coefficients"],
name = "coefficients",
marker = dict(color = coef_sumry["coefficients"],
colorscale = "Picnic",
line = dict(width = .6,color = "black")))

#subplots
fig = tls.make_subplots(rows=2, cols=2, specs=[[{}, {}], [{'colspan': 2}, None]],
subplot_titles=('Confusion Matrix',
'Receiver operating characteristic',
'Feature Importances'))

fig.append_trace(trace1,1,1)
fig.append_trace(trace2,1,2)
fig.append_trace(trace3,1,2)
fig.append_trace(trace4,2,1)

fig['layout'].update(showlegend=False, title="Model performance" ,
autosize = False,height = 900,width = 800,
plot_bgcolor = 'rgba(240,240,240, 0.95)',
paper_bgcolor = 'rgba(240,240,240, 0.95)',
margin = dict(b = 195))
fig["layout"]["xaxis2"].update(dict(title = "false positive rate"))
fig["layout"]["yaxis2"].update(dict(title = "true positive rate"))
fig["layout"]["xaxis3"].update(dict(showgrid = True,tickfont = dict(size = 10),
tickangle = 90))
py.iplot(fig)

#用yellow_brick幫我們可視化圖片
if threshold_plot == True :
visualizer = DiscriminationThreshold(algorithm)
visualizer.fit(training_x,training_y)
visualizer.poof()
In [25]:
#寫好logistic的演算法,正則我們採用Ridge算法
logit = LogisticRegression(C=1.0, class_weight=None, dual=False, fit_intercept=True,
intercept_scaling=1, max_iter=100, multi_class='ovr', n_jobs=1,
penalty='l2', random_state=None, solver='liblinear', tol=0.0001,
verbose=0, warm_start=False)
#跑model
select_model_prediction (logit,train_X,test_X,train_Y,test_Y,
cols,"coefficients",threshold_plot = True)
看看AUC以及特徵重要性排序

通常分類模型我會看他的AUC表現如何,也就是右邊這張圖曲線下的面積。

report!

我發現model表現得沒有很好,AUC才0.69左右。根據我最近重新看的一本書:認識資料科學的第一本書 (上一次看還是一年前,那時候還是數據分析菜雞XD 搞不好現在也是XD)指出:

一個可以應用的商業模型必須達到AUC 0.7以上,才視為可用。

蠻可惡的,居然差0.1就可以用了,那還不調整一下model的參數!

還記得我們一開始有看到客戶流失比例是1:2.5嗎?因為客戶流失屬於稀有事件,因此這筆資料集其實是個不平衡資料,在金融風險管理、客戶欺詐領域裡面常常會有資料不平衡(目標的資料不是1:1)說不定可以用用看資料筆數加權的方式,也就是在model的超參數裡調整成:

class_weight = {0:1 , 1:2.5}

發現:

WOW

AUC變成0.76!代表利用樣本調整權重是個不錯的方式,模型的準確率上升了7%左右!

另外一個思路是:抽樣,不過正如我說的,這個model的樣本已經夠少了,

我自己會選擇用樣本加權,當然也不是不能抽,因為我們可以用過採樣(類似人為製造樣本)的方式,也就是用SMOTE過採樣的方式來增加流失客戶的樣本資料到兩類人比例是1:1,我自己用SMOTE之後發現AUC也是0.76左右,比加權的AUC低一點點而已,所以增採樣也是一個讓model表現比較好的方式!留給大家親自試試看!

結論,親手coding,深入分析:

那麼以上,就是一個完整客戶流失案例的分享,當然這個model也要持續更新,畢竟不同時期客戶都會有不同的反應,我們要時常關注市場趨勢並且捕捉更多客戶特徵來建模,才是好的數據分析師該有的sense,這篇文對code的講解比較少,希望大家多看註解多理解,沒有講解程式碼主要是因為,我覺得這種比較technical的東西用註解應該比較好表達(一行一行講的感覺),另外就是真的親手下去打code會比聽我講的效果好很多,所以跟著code練習,學習效果會比較好,我也是一邊查詢語法、一邊在ipad上畫程式邏輯才寫出來的,再強調一次,重點在分析過程,能不能得到好的insight就需要跟產品經理、管理層、其他團隊好好討論,由於Kaggle主要是預測為主,所以這邊我也沒做太深入的解釋報告,不過真的要寫應該也不難,多看看一些財經報告、管理顧問簡報會對報告的撰寫比較有概念,這也是要自己練習的!期許自己跟大家都能夠讓自己的分析實力越來越好!以後應該會多練習自己寫分析框架與思路,歡迎多多關注XD

這邊也想做個實驗,好讓我知道你/妳喜不喜歡這篇文章:
拍 10 下:簽個到,表示支持(謝謝鼓勵!)
拍 20 下:想要我多寫「商管相關」
拍 30 下:想要我多寫「資科相關」
拍 50 下:我有你這讀者寫這篇也心滿意足了!

--

--

戴士翔 | Dennis Dai
Finformation當資料科學遇上財務金融

外商分析顧問,Ex- Apple Data Scientist,曾在FMCG巨頭/日商管顧/MBB管顧/高成長電商從事商業分析與數位轉型,專注分享管顧、商業、數據分析的思考。分析/演講/合作歡迎來信:dennis.dai.1011@gmail.com