傳統(tǒng)機器學習(六)集成算法(1)—隨機森林算法及案例詳解
1、概述
集成學習(Ensemble Learning)
就是通過某種策略將多個模型集成起來,通過群體決策來提高決策準確率。
集成學習首要的問題是選擇什么樣的學習器以及如何集成多個基學習器,即集成策略。
1.1 集成學習的分類
一個有效的集成除了要讓各個基學習器的學習效果好之外,還需要各個基學習器的差異盡可能大(差異性:各個基學習器的預測結果不完全相同)。集成學習往往和方差大的模型組合時效果顯著。
-
Bagging類方法中基學習器之間不存在強依賴關系,可并行執(zhí)行。
-
Boosting類方法中基學習器之間存在強依賴關系,必須串行執(zhí)行。
-
Stacking:聚合多個分類或回歸模型(可以分階段來做)。
Boosting和Bagging通常都是使用同一種基學習器
(base learner),因此我們一般稱之為同質集成方法。
Stacking通常都是基于多個不同的基學習器
做的集成,因此我們稱之為異質集成方法。
1.1.1 Bagging
Bagging類方法是通過隨機構造訓練樣本、隨機選擇特征
等方法來提高每個基模型的獨立性。由于訓練數(shù)據的不同,獲得的學習器會存在差異性,但是若采樣的每個子集都完全不同,則每個基學習器都只能訓練一小部分數(shù)據,無法進行有效的學習。因此考慮使用相互交疊的采樣子集。代表性方法有Bagging和隨機森林
等。
-
Bagging(Bootstrap Aggregating)是
通過不同模型的訓練數(shù)據集的獨立性來提高不同模型之間的獨立性
。我們在原始訓練集上進行有放回的隨機采樣??梢圆蓸覶個含有m個樣本的數(shù)據集并行訓練得到T個模型,然后將這些基學習模型進行結合。對于基學習器的集成方法,Bagging通常對分類任務使用簡單投票法,對回歸任務使用平均法
。若預測的結果中有含有相同票數(shù)的兩個類,可以使用隨機選擇或者考察學習器投票的置信度來確定。 -
隨機森林(Random Forest)是
在Bagging的基礎上再引入了隨機特征
,進一步提高每個基模型之間的獨立性。在隨機森林中,每個基模型都是一棵決策樹,與傳統(tǒng)決策樹不同的是,在RF中,對每個基決策樹的每個節(jié)點,先從該節(jié)點的屬性集合中隨機選擇一個包含k個屬性的子集,然后從這個子集中選擇一個最優(yōu)屬性由于劃分,而傳統(tǒng)的決策樹是直接在當前節(jié)點的屬性集合中選擇一個最優(yōu)屬性來劃分集合。
1.1.2 Boosting
Boosting類方法是按照一定的順序來先后訓練不同的基模型,每個模型都針對先前模型的錯誤進行專門訓練
。根據先前模型的結果,來調整訓練樣本的權重,從而增加不同基模型之間的差異性。Boosting的過程很類似于人類學習的過程,我們學習新知識的過程往往是迭代式的。第一遍學習的時候,我們會記住一部分知識,但往往也會犯一些錯誤,對于這些錯誤,我們的印象會很深。第二遍學習的時候,就會針對犯過錯誤的知識加強學習,以減少類似的錯誤發(fā)生。不斷循環(huán)往復,直到犯錯誤的次數(shù)減少到很低的程度。
Boosting 類方法是一種非常強大的集成方法,只要基模型的準確率比隨機猜測高,就可以通過集成方法來顯著地提高集成模型的準確率。Boosting類方法的代表性方法有:AdaBoost,GBDT
等。
1.1.3 Stacking
對于一個問題來說,我們可以采用不同類型的學習器來解決學習的問題,這些學習器通常能夠學習到問題的一部分,但不能學習到問題的整個空間。Stacking的做法是首先構建多個不同類型的一級學習器,并使用他們來得到一級預測結果,然后基于這些一級預測結果,構建一個二級學習器,來得到最終的預測結果
。
Stacking的動機可以描述為:如果某個一級學習器錯誤地學習了特征空間的某個區(qū)域,那么二級學習器通過結合其他一級學習器的學習行為,可以適當糾正這種錯誤。
1.2 隨機森林原理
隨機森林(Random Fores)是將決策樹進行簡單bagging的一種集成算法。它經過多次隨機抽取樣本訓練多棵決策樹,用多棵決策樹集成決策。由于它擁有多棵樹,且每棵樹是隨機的,所以稱為隨機森林。
1.2.1 模型表達式
y = 1 k [ t 1 ? p r o b ( x ) + [ t 2 ? p r o b ( x ) + . . . + [ t k ? p r o b ( x ) ] y = \frac{1}{k}[t_{1}*prob(x) + [t_{2}*prob(x) + ...+ [t_{k}*prob(x)] y=k1?[t1??prob(x)+[t2??prob(x)+...+[tk??prob(x)]
其中:
t(i) : 決策樹
t(i).prob(x) : 第i棵樹對x的預測,輸出為各個類別的預測概率(行向量)
k : 森林規(guī)模數(shù)
即模型為多棵決策樹組成,最后的預測概率為各棵樹的概率預測均值。概率得分最大的一類,即為預測類別
1.2.2 模型訓練
模型訓練焦點在于如何訓練出多棵不同的弱樹??梢酝ㄟ^如下方式實現(xiàn):每次隨機選擇部分樣本,和部分變量訓練一棵弱樹,要使樹是弱樹,可將樹的深度設置得較淺,總之,讓樹的預測不是很精確。
-- 1、訓練流程如下
放回式抽取n個樣本,每個樣本抽到的次數(shù),作為樣本權重。
用加了權重的樣本訓練弱決策樹(弱決策樹即:最大分割特征:2),一直訓練K棵樹為止。
簡單地說,就是生成k棵樹,每棵樹用的樣本隨機抽取。最后k棵樹組合在一起就是森林。
-- 2、訓練參數(shù)
(1) 變量最大個數(shù)m,m一般遠小于總變量個數(shù)M,例如 根號M
(2) 森林規(guī)模(樹的棵數(shù)k)
1.2.3 隨機森林的泛化能力
袋外錯誤率
隨機森林可用袋外錯誤率obb(out-of-bag) error評估泛化能力.
袋外錯誤率的思路是通過未參與訓練樣本(袋外樣本)的準確率來檢驗森林的泛化能力
,由于每個樣本都只被部分樹用于訓練,可以只用森林中該樣本不參與的子森林來預測樣本,袋外錯誤率就是用該方法對所有樣本進行預測的準確率。
袋外類別預測
袋外的類別預測,是指用該樣本未參與訓練的樹對其進行概率預測,匯總所有樹對該樣本的預測結果(概率之和歸一化后的值),最后哪個類別的概率大,就認為袋外預測類別是哪個。
所有樣本的袋外預測準確率,即為袋外評分(obb_socre),袋外錯誤率 則為:obb_error = 1 - obb_socre
1.2.4 特征權重
特征權重是每個特征對隨機森林貢獻度的占比,特征權重越高,說明特征對森林的構成越重要,對決策結果影響越大。
只要將森林的每棵樹的評分求均值,并作歸一化,就是森林的特征評分。
s
=
n
o
r
m
(
1
k
∑
i
=
1
n
s
i
)
s = norm(\frac{1}{k}\sum\limits_{i=1}^ns_{i})
s=norm(k1?i=1∑n?si?)
其中,s(i)為向量,是第 i 棵樹對各個特征的評分。
1.2.5 隨機森林的優(yōu)缺點
1、隨機森林算法優(yōu)點
-
由于采用了集成算法,本身精度比大多數(shù)單個算法要好,所以準確性高
-
在測試集上表現(xiàn)良好,由于兩個隨機性的引入,使得隨機森林不容易陷入過擬合(樣本隨機,特征隨機)
-
在工業(yè)上,由于兩個隨機性的引入,使得隨機森林具有一定的抗噪聲能力,對比其他算法具有一定優(yōu)勢
-
由于樹的組合,使得隨機森林可以處理非線性數(shù)據,本身屬于非線性分類(擬合)模型
-
它能夠處理很高維度(feature很多)的數(shù)據,并且不用做特征選擇,對數(shù)據集的適應能力強:既能處理離散型數(shù)據,也能處理連續(xù)型數(shù)據,數(shù)據集無需規(guī)范化
-
訓練速度快,可以運用在大規(guī)模數(shù)據集上
-
可以處理缺省值(單獨作為一類),不用額外處理
-
由于有袋外數(shù)據(OOB),可以在模型生成過程中取得真實誤差的無偏估計,且不損失訓練數(shù)據量
-
在訓練過程中,能夠檢測到feature間的互相影響,且可以得出feature的重要性,具有一定參考意義
-
由于每棵樹可以獨立、同時生成,容易做成并行化方法
-
由于實現(xiàn)簡單、精度高、抗過擬合能力強,當面對非線性數(shù)據時,適于作為基準模型
2、隨機森林算法缺點
-
當隨機森林中的決策樹個數(shù)很多時,訓練時需要的空間和時間會比較大
-
隨機森林中還有許多不好解釋的地方,有點算是黑盒模型
-
在某些噪音比較大的樣本集上,RF的模型容易陷入過擬合
2、手動實現(xiàn)隨機森林
from sklearn.datasets import load_iris
import numpy as np
from sklearn.base import clone
from sklearn.tree import DecisionTreeClassifier
np.random.seed(888)
# ==================== 加載數(shù)據=====================================================
iris = load_iris()
X = iris.data
Y = iris.target
n_samples = X.shape[0] # 樣本個數(shù)
n_samples_bootstrap = X.shape[0] # 抽樣個數(shù)
c_n = np.unique(Y).shape[0] # 類別數(shù)
tree_num = 100 # 森林決策樹個數(shù)
trees = [] # 初始化樹列表
p_oob = np.zeros((n_samples, c_n)) # oob投票結果
random_state = np.random.mtrand._rand # 隨機狀態(tài)
max_features = 2 # 每棵樹分割最大特征數(shù)(至少有一個分割點,若一個都沒有,則無視該條件)
# 建立樹模板
base_estimator = DecisionTreeClassifier()
base_estimator.set_params(**{'criterion': 'gini',
'min_samples_split': 2,
'min_samples_leaf': 1,
'min_weight_fraction_leaf': 0.0,
'max_features': max_features,
'max_leaf_nodes': None,
'min_impurity_decrease': 0.0,
'random_state': None,
'ccp_alpha': 0.0
})
# 逐樹訓練
random_state_list = [random_state.randint(np.iinfo(np.int32).max) for i in range(tree_num)] # 初始化樹隨機狀態(tài)
for i in range(tree_num):
sample_indices = np.random.RandomState(random_state_list[i]).randint(0, n_samples, n_samples_bootstrap) # 抽樣
sample_counts = np.bincount(sample_indices, minlength=n_samples) # 抽樣分布
curr_sample_weight = np.ones((n_samples,), dtype=np.float64) * sample_counts # 樣本權重
cur_tree = clone(base_estimator) # 初始化樹
cur_tree.set_params(**{'random_state': random_state_list[i]}) # 設置當前樹隨機狀態(tài)
cur_tree.fit(X, Y, sample_weight=curr_sample_weight, check_input=False) # 訓練樹
trees.append(cur_tree) # 將本次訓練好的樹, 添加到樹列表
# 計算obb得分
un_select = ~ np.isin(range(n_samples), sample_indices) # 未選中的數(shù)據
cur_p_oob = cur_tree.predict_proba(X[un_select, :]) # 將當前未選中數(shù)據的預測結果
p_oob[un_select, :] += cur_p_oob # 投票到匯總結果
# =============== 模型指標統(tǒng)計 ===================================
oob_score = np.mean(Y == np.argmax(p_oob, axis=1), axis=0) # obb樣本正確率即為obb得分
# 計算特征得分
all_importances = [getattr(tree, 'feature_importances_') for tree in trees if tree.tree_.node_count > 1] # 獲取每棵樹中各特征評估
all_importances = np.mean(all_importances, axis=0, dtype=np.float64) # 求均值
feature_importances = all_importances / np.sum(all_importances) # 歸一化
# =============== 模型預測 ===================================
sim_p = np.zeros((X.shape[0], c_n), dtype=np.float64) # 初始化投票得分
for i in range(len(trees)): # 逐樹投票
sim_p += trees[i].predict_proba(X) / len(trees) # 投票
sim_c = np.argmax(sim_p, axis=1) # 得分最高者作為投票結果
# =================打印結果==========================
print("\n----前5條預測結果:----")
print(sim_p[0:5]) # 打印結果
print("\n----袋外準確率oob_score:----")
print(oob_score) # 打印oob得分
print("\n----特征得分:----")
print(feature_importances)
----前5條預測結果:----
[[1. 0. 0. ]
[0.99 0.01 0. ]
[1. 0. 0. ]
[1. 0. 0. ]
[1. 0. 0. ]]
----袋外準確率oob_score:----
0.9533333333333334
----特征得分:----
[0.08186032 0.02758341 0.44209899 0.44845728]
3、sklearn中的bagging和隨機森林
3.1 bagging
import numpy as np
import os
%matplotlib inline
import matplotlib
import matplotlib.pyplot as plt
plt.rcParams['axes.labelsize'] = 14
plt.rcParams['xtick.labelsize'] = 12
plt.rcParams['ytick.labelsize'] = 12
import warnings
warnings.filterwarnings('ignore')
np.random.seed(42)
'''
Bagging策略:
首先對訓練數(shù)據集進行多次采樣,保證每次得到的采樣數(shù)據都是不同的;
分別訓練多個模型,例如樹模型;
預測時需得到所有模型結果再進行集成。
'''
from sklearn.datasets import make_moons
from sklearn.model_selection import train_test_split
X, y = make_moons(n_samples=500,noise=0.25,random_state=42)
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)
plt.plot(X[:,0][y==0],X[:,1][y==0],'yo',alpha = 0.6)
plt.plot(X[:,0][y==0],X[:,1][y==1],'bs',alpha = 0.6)
'''
1、傳統(tǒng)決策樹
'''
from sklearn import tree
from sklearn.metrics import accuracy_score
tree_clf = tree.DecisionTreeClassifier(random_state=42)
tree_clf.fit(X_train,y_train)
y_pred = tree_clf.predict(X_test)
print('傳統(tǒng)決策樹精確率為:',accuracy_score(y_test,y_pred))
# 傳統(tǒng)決策樹精確率為: 0.936
'''
2、Bagging
'''
from sklearn.ensemble import BaggingClassifier
bag_clf = BaggingClassifier(
tree.DecisionTreeClassifier(), # 擬合數(shù)據集的隨機子集的基學習器
n_estimators=500, # 基學習器數(shù)目
max_samples=100, # 每個學習器抽樣的最大樣本數(shù)
bootstrap=True, # 樣本是否放回
n_jobs=-1,
oob_score=True,
random_state=42
)
bag_clf.fit(X_train,y_train)
y_pred = bag_clf.predict(X_test)
print('Bagging精確率為:',accuracy_score(y_test,y_pred))
# Bagging精確率為: 0.952
'''
3、決策邊界對比
'''
from matplotlib.colors import ListedColormap
def plot_decision_boundary(clf,X,y,axes=[-1.5,2.5,-1,1.5],alpha=0.5,contour =True):
x1s=np.linspace(axes[0],axes[1],100)
x2s=np.linspace(axes[2],axes[3],100)
x1,x2 = np.meshgrid(x1s,x2s)
X_new = np.c_[x1.ravel(),x2.ravel()]
y_pred = clf.predict(X_new).reshape(x1.shape)
custom_cmap = ListedColormap(['#fafab0','#9898ff','#a0faa0'])
plt.contourf(x1,x2,y_pred,cmap = custom_cmap,alpha=0.3)
if contour:
custom_cmap2 = ListedColormap(['#7d7d58','#4c4c7f','#507d50'])
plt.contour(x1,x2,y_pred,cmap = custom_cmap2,alpha=0.8)
plt.plot(X[:,0][y==0],X[:,1][y==0],'yo',alpha = 0.6)
plt.plot(X[:,0][y==0],X[:,1][y==1],'bs',alpha = 0.6)
plt.axis(axes)
plt.xlabel('x1')
plt.xlabel('x2')
plt.figure(figsize = (12,5))
plt.subplot(121)
plot_decision_boundary(tree_clf,X,y)
plt.title('Decision Tree')
plt.subplot(122)
plot_decision_boundary(bag_clf,X,y)
plt.title('Decision Tree With Bagging')
plt.show()
# 隨機森林可用袋外錯誤率obb(out-of-bag) error評估泛化能力.
# obb_error = 1 - obb_socre
obb_error = 1 - bag_clf.oob_score_
obb_error
# 0.06399999999999995
3.2 隨機森林
"""
sklearn的隨機森林Demo
"""
from sklearn.datasets import load_iris
from sklearn.ensemble import RandomForestClassifier
import numpy as np
np.random.seed(55)
# ==================== 加載數(shù)據 =================
iris = load_iris()
X = iris.data
y = iris.target
# ========================= 模型訓練 =============
clf = RandomForestClassifier(
n_jobs=1,
oob_score=True,
max_features=2,
n_estimators=100
)
clf.fit(X, y)
# =============================== 模型預測 ========
pred_prob = clf.predict_proba(X)
pred_c = clf.predict(X)
preds = iris.target_names[pred_c]
#=================打印結果==========================
print("\n----前5條預測結果:----")
print(pred_prob[0:5])
print("\n----袋外準確率oob_score:----")
print(clf.oob_score_)
print("\n----特征得分:----")
print(clf.feature_importances_)
----前5條預測結果:----
[[1. 0. 0.]
[1. 0. 0.]
[1. 0. 0.]
[1. 0. 0.]
[1. 0. 0.]]
----袋外準確率oob_score:----
0.9533333333333334
----特征得分:----
[0.09910252 0.0317905 0.48356935 0.38553764]
4、隨機森林案例
4.1 隨機森林預測寬帶客戶離網
數(shù)據集鏈接:https://pan.baidu.com/s/1vmjldkWZtQWlFopWlFLX9w 提取碼:ad1h
集成學習與神經網絡一樣,都屬于解釋性較差的黑盒模型,所以我們無需過分探究數(shù)據集中每個變量的具體含義,只需關注最后一個變量broadband即可,爭取通過如年齡,使用時長,支付情況以及流量和通話情況等變量對寬帶客戶是否會續(xù)費做出一個較準確的預測。
1、數(shù)據探索
import pandas as pd
import numpy as np
df = pd.read_csv('../data/broadband.csv') # 寬帶客戶數(shù)據
# 列名全部換成小寫
df.rename(str.lower, axis='columns', inplace=True)
# 只需關注參數(shù),broadband:0-離開,1-留存
df.head()
cust_id | gender | age | tenure | channel | autopay | arpb_3m | call_party_cnt | day_mou | afternoon_mou | night_mou | avg_call_length | broadband | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 63 | 1 | 34 | 27 | 2 | 0 | 203 | 0 | 0.0 | 0.0 | 0.0 | 3.04 | 1 |
1 | 64 | 0 | 62 | 58 | 1 | 0 | 360 | 0 | 0.0 | 1910.0 | 0.0 | 3.30 | 1 |
2 | 65 | 1 | 39 | 55 | 3 | 0 | 304 | 0 | 437.2 | 200.3 | 0.0 | 4.92 | 0 |
3 | 66 | 1 | 39 | 55 | 3 | 0 | 304 | 0 | 437.2 | 182.8 | 0.0 | 4.92 | 0 |
4 | 67 | 1 | 39 | 55 | 3 | 0 | 304 | 0 | 437.2 | 214.5 | 0.0 | 4.92 | 0 |
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1114 entries, 0 to 1113
Data columns (total 13 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 cust_id 1114 non-null int64
1 gender 1114 non-null int64
2 age 1114 non-null int64
3 tenure 1114 non-null int64
4 channel 1114 non-null int64
5 autopay 1114 non-null int64
6 arpb_3m 1114 non-null int64
7 call_party_cnt 1114 non-null int64
8 day_mou 1114 non-null float64
9 afternoon_mou 1114 non-null float64
10 night_mou 1114 non-null float64
11 avg_call_length 1114 non-null float64
12 broadband 1114 non-null int64
dtypes: float64(4), int64(9)
memory usage: 113.3 KB
from collections import Counter
# 查看broadband分布情況,隨機森林擅長處理數(shù)據集不平衡
print('broadband',Counter(df['broadband']))
broadband Counter({0: 908, 1: 206})
2、拆分測試集及訓練集
# 客戶id沒有用,故丟棄cust_id這一列
X = df.iloc[:,1:-1]
y = df['broadband']
from sklearn.model_selection import train_test_split
X_train,X_test,y_train,y_test = train_test_split(
X,y,test_size=0.3,random_state=888
)
3、決策樹建模
from sklearn import tree
# 直接使用交叉網格搜索來優(yōu)化決策樹模型
from sklearn.model_selection import GridSearchCV
# 網格搜索的參數(shù):正常決策樹建模中的參數(shù) - 評估指標,樹的深度,最小拆分的葉子樣本數(shù)
# 通常來說,十幾層的樹已經是比較深了
param_grid = {
'max_depth': [2, 3, 4, 5, 6, 7, 8],
'min_samples_split': [4, 8, 12, 16, 20, 24, 28]
}
clf_cv = GridSearchCV(
estimator=tree.DecisionTreeClassifier(),
param_grid=param_grid,
scoring='roc_auc',
cv=5
)
clf_cv.fit(X_train,y_train)
pred_y_test = clf_cv.predict(X_test)
import sklearn.metrics as metrics
print("決策樹 AUC:")
fpr_test, tpr_test, th_test = metrics.roc_curve(y_test, pred_y_test)
print('AUC = %.4f' % metrics.auc(fpr_test, tpr_test))
決策樹 AUC:
AUC = 0.7763
4、隨機森林建模
# 一樣是直接使用網格搜索
param_grid = {
'max_depth':[5, 6, 7, 8], # 深度:這里是森林中每棵決策樹的深度
'n_estimators':[11,13,15], # 決策樹個數(shù)-隨機森林特有參數(shù)
'max_features':[0.3,0.4,0.5], # 每棵決策樹使用的變量占比-隨機森林特有參數(shù)(結合原理)
'min_samples_split':[4,8,12,16] # 葉子的最小拆分樣本量
}
from sklearn.ensemble import RandomForestClassifier
rfc = RandomForestClassifier()
rfc_cv = GridSearchCV(estimator=rfc,
param_grid=param_grid,
scoring='roc_auc',
cv=5)
rfc_cv.fit(X_train, y_train)
# 使用隨機森林對測試集進行預測
test_est = rfc_cv.predict(X_test)
print('隨機森林 AUC...')
fpr_test, tpr_test, th_test = metrics.roc_curve(test_est, y_test) # 構造 roc 曲線
print('AUC = %.4f' %metrics.auc(fpr_test, tpr_test))
隨機森林 AUC...
AUC = 0.8181
# 查看最佳參數(shù),看是否在決策邊界上,還需重新設置網格搜索參數(shù)
rfc_cv.best_params_
{'max_depth': 7,
'max_features': 0.3,
'min_samples_split': 4,
'n_estimators': 15}
# 調整決策邊界,這里只是做示范
param_grid = {
'max_depth':[7, 8, 10, 12],
'n_estimators':[11, 13, 15, 17, 19], # 決策樹個數(shù)-隨機森林特有參數(shù)
'max_features':[0.2,0.3,0.4, 0.5, 0.6, 0.7], # 每棵決策樹使用的變量占比-隨機森林特有參數(shù)
'min_samples_split':[2, 3, 4, 8, 12, 16] # 葉子的最小拆分樣本量
}
# 重復上述步驟,可寫成函數(shù)供快捷調用
rfc_cv = GridSearchCV(estimator=rfc,
param_grid=param_grid,
scoring='roc_auc',
n_jobs=-1,
cv=5)
rfc_cv.fit(X_train, y_train)
# 使用隨機森林對測試集進行預測
test_est = rfc_cv.predict(X_test)
print('隨機森林 AUC...')
fpr_test, tpr_test, th_test = metrics.roc_curve(test_est, y_test) # 構造 roc 曲線
print('AUC = %.4f' %metrics.auc(fpr_test, tpr_test))
# 這里的 auc 只提升了很多
隨機森林 AUC...
AUC = 0.8765
4.2 隨機森林分析酒店預定取消率影響的因素
數(shù)據集下載地址:https://www.kaggle.com/datasets/jessemostipak/hotel-booking-demand
該數(shù)據集依據兩家酒店為主體,分別是一家度假酒店與一家城市酒店,這兩家均位于葡萄牙(Portugal),度假酒店(Resort hotel)在阿爾加維(Algarve),城市酒店(City hotel)則位于葡萄牙首都里斯本(Lisbon),兩個酒店在地理位置上跨度較大,數(shù)據之間相互干擾的影響較小。
1、數(shù)據探索
import pandas as pd
import matplotlib.pyplot as plt
#顯示設置
pd.set_option("display.max_columns", None) #設置顯示全部列
plt.rcParams['font.sans-serif']=['SimHei'] #用來正常顯示中文標簽
plt.rcParams['axes.unicode_minus'] = False #用來正常顯示負號
# 加載數(shù)據
data_origin = pd.read_csv("../data/hotel_bookings.csv")
#數(shù)據備份
data=data_origin.copy()
print(data.shape)
data.head()
hotel | is_canceled | lead_time | arrival_date_year | arrival_date_month | arrival_date_week_number | arrival_date_day_of_month | stays_in_weekend_nights | stays_in_week_nights | adults | children | babies | meal | country | market_segment | distribution_channel | is_repeated_guest | previous_cancellations | previous_bookings_not_canceled | reserved_room_type | assigned_room_type | booking_changes | deposit_type | agent | company | days_in_waiting_list | customer_type | adr | required_car_parking_spaces | total_of_special_requests | reservation_status | reservation_status_date | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | Resort Hotel | 0 | 342 | 2015 | July | 27 | 1 | 0 | 0 | 2 | 0.0 | 0 | BB | PRT | Direct | Direct | 0 | 0 | 0 | C | C | 3 | No Deposit | NaN | NaN | 0 | Transient | 0.0 | 0 | 0 | Check-Out | 2015-07-01 |
1 | Resort Hotel | 0 | 737 | 2015 | July | 27 | 1 | 0 | 0 | 2 | 0.0 | 0 | BB | PRT | Direct | Direct | 0 | 0 | 0 | C | C | 4 | No Deposit | NaN | NaN | 0 | Transient | 0.0 | 0 | 0 | Check-Out | 2015-07-01 |
2 | Resort Hotel | 0 | 7 | 2015 | July | 27 | 1 | 0 | 1 | 1 | 0.0 | 0 | BB | GBR | Direct | Direct | 0 | 0 | 0 | A | C | 0 | No Deposit | NaN | NaN | 0 | Transient | 75.0 | 0 | 0 | Check-Out | 2015-07-02 |
3 | Resort Hotel | 0 | 13 | 2015 | July | 27 | 1 | 0 | 1 | 1 | 0.0 | 0 | BB | GBR | Corporate | Corporate | 0 | 0 | 0 | A | A | 0 | No Deposit | 304.0 | NaN | 0 | Transient | 75.0 | 0 | 0 | Check-Out | 2015-07-02 |
4 | Resort Hotel | 0 | 14 | 2015 | July | 27 | 1 | 0 | 2 | 2 | 0.0 | 0 | BB | GBR | Online TA | TA/TO | 0 | 0 | 0 | A | A | 0 | No Deposit | 240.0 | NaN | 0 | Transient | 98.0 | 0 | 1 | Check-Out | 2015-07-03 |
# hotel 酒店類型
# is_canceled 預訂是否取消(0,1)
# lead_time 提前預訂天數(shù)
# arrival_date_year 入住年份
# arrival_date_month 入住月份
# arrival_date_week_number 入住周數(shù)
# arrival_date_day_of_month 入住日期
# stays_in_weekend_nights 周末過夜數(shù)
# stays_in_week_nights 工作日過夜數(shù)
# adults 成人人數(shù)
# children 兒童人數(shù)
# babies 嬰兒人數(shù)
# meal 餐食類型(BB早餐,HB午餐,F(xiàn)B晚餐,Undefined/SC無餐)
# country 客戶來源國家
# market_segment 市場細分
# distribution_channel 訂單渠道
# is_repeated_guest 是否是老客戶
# previous_cancellations 歷史取消預訂的次數(shù)
# previous_bookings_not_canceled 歷史未取消預訂的次數(shù)
# reserved_room_type 預定房間類型
# assigned_room_type 實際房間類型
# booking_changes 預定更改次數(shù)
# deposit_type 押金類型(No Deposit,Non Refund,Refundable)
# agent 預訂旅行社ID
# company 預訂公司/實體ID
# days_in_waiting_list 等待天數(shù)
# customer_type 客戶類型
# adr 客房日均價
# required_car_parking_spaces 車位需求數(shù)
# total_of_special_requests 特殊需求數(shù)
# reservation_status 預訂最終狀態(tài)(Canceled,Check-out,No-show)
# reservation_status_date 預訂最終狀態(tài)更新日期
data.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 119390 entries, 0 to 119389
Data columns (total 32 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 hotel 119390 non-null object
1 is_canceled 119390 non-null int64
2 lead_time 119390 non-null int64
3 arrival_date_year 119390 non-null int64
4 arrival_date_month 119390 non-null object
5 arrival_date_week_number 119390 non-null int64
6 arrival_date_day_of_month 119390 non-null int64
7 stays_in_weekend_nights 119390 non-null int64
8 stays_in_week_nights 119390 non-null int64
9 adults 119390 non-null int64
10 children 119386 non-null float64
11 babies 119390 non-null int64
12 meal 119390 non-null object
13 country 118902 non-null object
14 market_segment 119390 non-null object
15 distribution_channel 119390 non-null object
16 is_repeated_guest 119390 non-null int64
17 previous_cancellations 119390 non-null int64
18 previous_bookings_not_canceled 119390 non-null int64
19 reserved_room_type 119390 non-null object
20 assigned_room_type 119390 non-null object
21 booking_changes 119390 non-null int64
22 deposit_type 119390 non-null object
23 agent 103050 non-null float64
24 company 6797 non-null float64
25 days_in_waiting_list 119390 non-null int64
26 customer_type 119390 non-null object
27 adr 119390 non-null float64
28 required_car_parking_spaces 119390 non-null int64
29 total_of_special_requests 119390 non-null int64
30 reservation_status 119390 non-null object
31 reservation_status_date 119390 non-null object
dtypes: float64(4), int64(16), object(12)
memory usage: 29.1+ MB
data.describe()
is_canceled | lead_time | arrival_date_year | arrival_date_week_number | arrival_date_day_of_month | stays_in_weekend_nights | stays_in_week_nights | adults | children | babies | is_repeated_guest | previous_cancellations | previous_bookings_not_canceled | booking_changes | agent | company | days_in_waiting_list | adr | required_car_parking_spaces | total_of_special_requests | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
count | 119390.000000 | 119390.000000 | 119390.000000 | 119390.000000 | 119390.000000 | 119390.000000 | 119390.000000 | 119390.000000 | 119386.000000 | 119390.000000 | 119390.000000 | 119390.000000 | 119390.000000 | 119390.000000 | 103050.000000 | 6797.000000 | 119390.000000 | 119390.000000 | 119390.000000 | 119390.000000 |
mean | 0.370416 | 104.011416 | 2016.156554 | 27.165173 | 15.798241 | 0.927599 | 2.500302 | 1.856403 | 0.103890 | 0.007949 | 0.031912 | 0.087118 | 0.137097 | 0.221124 | 86.693382 | 189.266735 | 2.321149 | 101.831122 | 0.062518 | 0.571363 |
std | 0.482918 | 106.863097 | 0.707476 | 13.605138 | 8.780829 | 0.998613 | 1.908286 | 0.579261 | 0.398561 | 0.097436 | 0.175767 | 0.844336 | 1.497437 | 0.652306 | 110.774548 | 131.655015 | 17.594721 | 50.535790 | 0.245291 | 0.792798 |
min | 0.000000 | 0.000000 | 2015.000000 | 1.000000 | 1.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 1.000000 | 6.000000 | 0.000000 | -6.380000 | 0.000000 | 0.000000 |
25% | 0.000000 | 18.000000 | 2016.000000 | 16.000000 | 8.000000 | 0.000000 | 1.000000 | 2.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 9.000000 | 62.000000 | 0.000000 | 69.290000 | 0.000000 | 0.000000 |
50% | 0.000000 | 69.000000 | 2016.000000 | 28.000000 | 16.000000 | 1.000000 | 2.000000 | 2.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 14.000000 | 179.000000 | 0.000000 | 94.575000 | 0.000000 | 0.000000 |
75% | 1.000000 | 160.000000 | 2017.000000 | 38.000000 | 23.000000 | 2.000000 | 3.000000 | 2.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 229.000000 | 270.000000 | 0.000000 | 126.000000 | 0.000000 | 1.000000 |
max | 1.000000 | 737.000000 | 2017.000000 | 53.000000 | 31.000000 | 19.000000 | 50.000000 | 55.000000 | 10.000000 | 10.000000 | 1.000000 | 26.000000 | 72.000000 | 21.000000 | 535.000000 | 543.000000 | 391.000000 | 5400.000000 | 8.000000 | 5.000000 |
# 通過觀察數(shù)據,發(fā)現(xiàn)1個異常點:客房日均價(adr)不為負,應剔除adr為負的異常值,
# adr最大值為5400,顯然遠遠大于均值+3倍標準差。
# 用箱形圖看下異常值:
import seaborn as sns
plt.figure(figsize=(12, 1))
sns.boxplot(x=list(data["adr"]))
plt.show()
?
?
#查看缺失值
missing=data.isnull().sum()
missing[missing != 0]
children 4
country 488
agent 16340
company 112593
dtype: int64
2、數(shù)據清洗
1)缺失值填充
① country缺失的,無法補充,缺失值用unknown進行區(qū)分 ;
② children缺失的,可理解為沒有兒童入住,故訂單登記中未填,缺失值用0填充;
③ agent缺失的,可理解為非旅行社預訂,故訂單登記中未填,缺失值用0填充;
④ company缺失的,可理解為非公司預訂,故訂單登記中未填,缺失值用0填充;
data_fill = data.fillna(
{
"country":"unknown",
"children":0,
"agent":0,
"company":0
}
)
missing=data_fill.isnull().sum()
missing[missing != 0]
Series([], dtype: int64)
2)異常值處理
(1)adults與children入住人數(shù)之和為0;
(2)客房日均價為負,客房日均價高于1000元;
(3)meal中undefined與sc均表示無餐。
# 2)異常值處理
# 1、adults與children入住人數(shù)之和為0
drop_a = data_fill[data_fill[["adults","children"]].sum(axis=1) == 0]
# 2、客房日均價為負,客房日均價高于1000元
drop_b = data_fill[(data_fill["adr"]<0) | (data_fill["adr"]>1000)]
data_fill.drop(drop_a.index,inplace=True)
data_done=data_fill[(data_fill["adr"]>=0) & (data_fill["adr"] <1000)]
# 3、meal中undefined與sc均表示無餐
data_done["meal"].replace({"Undefined":"SC"},inplace=True)
data_done.shape # 數(shù)據清洗完以后,還剩下119208條記錄
(119208, 32)
3、數(shù)值型特征歸一化,類別特征數(shù)值化(one-hot編碼)
from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import StandardScaler,OneHotEncoder
# 1、數(shù)值型特征進行歸一化
num_features = ["lead_time",
"arrival_date_week_number",
"arrival_date_day_of_month",
"stays_in_weekend_nights",
"stays_in_week_nights",
"adults",
"children",
"babies",
"is_repeated_guest",
"previous_cancellations",
"previous_bookings_not_canceled",
"agent",
"company",
"required_car_parking_spaces",
"total_of_special_requests",
"adr"]
num_transformer = Pipeline(
steps=[
('imputer', SimpleImputer(strategy='constant',fill_value=0)), # 將空值填充為自定義的值
('scaler', StandardScaler()) # 數(shù)據歸一化
]
)
# 2、類別特征標準化(one-hot)
cat_features = ["hotel",
"arrival_date_month",
"meal",
"market_segment",
"distribution_channel",
"reserved_room_type",
"deposit_type",
"customer_type"]
cat_transformer = Pipeline(
steps=[
("imputer", SimpleImputer(strategy="constant", fill_value="Unknown")),
("onehot", OneHotEncoder(handle_unknown='ignore'))
]
)
features = num_features + cat_features
from sklearn.compose import ColumnTransformer
'''
SimpleImputer類可用于替換缺少的值,MinMaxScaler類可用于縮放數(shù)值,而OneHotEncoder可用于編碼分類變量。
ColumnTransformer()在Python的機器學習庫scikit-learn中,可以選擇地進行數(shù)據轉換。
要使用ColumnTransformer,必須指定一個轉換器列表。
每個轉換器是一個三元素元組,用于定義轉換器的名稱,要應用的轉換以及要應用于其的列索引,例如:(名稱,對象,列)
'''
preprocessor = ColumnTransformer(
transformers=[
('num', num_transformer, num_features),
('cat', cat_transformer, cat_features)
]
)
4、選用隨機森林建模,挑選最佳參數(shù)
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import KFold, cross_val_score,train_test_split,GridSearchCV
X = data_done.drop("is_canceled",axis=1)
y = data_done["is_canceled"]
# 初步調參n_estimators
# scorel = []
# for i in range(0,200,10):
# rfc_model = RandomForestClassifier(n_estimators=i+1,
# n_jobs=-1,
# random_state=0)
# rfc = Pipeline(
# steps=[
# ('preprocessor', preprocessor),
# ('model',rfc_model)
# ]
# )
# split = KFold(n_splits=10, shuffle=True, random_state=42)
# rfc_t_s = cross_val_score(rfc,
# X,
# y,
# cv=split,
# scoring="accuracy",
# n_jobs=-1
# ).mean()
# print('step',rfc_t_s)
# scorel.append(rfc_t_s)
# print(max(scorel),(scorel.index(max(scorel))*10) + 1)
# plt.figure(figsize=[20,5])
# plt.plot(range(1,201,10),scorel)
# plt.show()
# 再次調參n_estimators,注意數(shù)據集訓練比較慢
scorel = []
for i in range(150,170):
rfc_model = RandomForestClassifier(n_estimators=i+1,
n_jobs=-1,
random_state=0)
rfc = Pipeline(
steps=[
('preprocessor', preprocessor),
('model',rfc_model)
]
)
split = KFold(n_splits=10, shuffle=True, random_state=42)
rfc_t_s = cross_val_score(rfc,
X,
y,
cv=split,
scoring="accuracy",
n_jobs=-1
).mean()
print('step:',i,',score=',rfc_t_s)
scorel.append(rfc_t_s)
# 從圖像可以看出來,n_estimators應該為161
print(max(scorel),([*range(150,170)][scorel.index(max(scorel))]))
plt.figure(figsize=[20,5])
plt.plot(range(150,170),scorel)
plt.show()
param_grid = {
"model__max_depth":[*range(1,40,10)],
'model__min_samples_leaf':[*range(1,50,10)]
}
rfc_model_t = RandomForestClassifier(n_estimators=161,
criterion="gini",
max_features=0.4,
n_jobs=-1,
random_state=0)
rfc_t = Pipeline(
steps=[
('preprocessor', preprocessor),
('model',rfc_model_t)
]
)
split_t = KFold(n_splits=10, shuffle=True, random_state=42)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3)
GS = GridSearchCV(rfc_t, param_grid, cv=split_t)
GS.fit(X_train, y_train)
GS.best_params_
#
5、選用最佳參數(shù)訓練模型
#訓練模型,評估模型
rfc_model = RandomForestClassifier(n_estimators=161
,criterion="gini"
,max_depth=31
,min_samples_leaf=1
,max_features=0.4
,n_jobs=-1
,random_state=0)
rfc = Pipeline(steps=[
('preprocessor', preprocessor),
('model',rfc_model)
])
rfc = rfc.fit(X_train, y_train)
score_=rfc.score(X_test, y_test)
score_
0.8683835248720745
#交叉檢驗
split = KFold(n_splits=10, shuffle=True, random_state=42)
rfc_s = cross_val_score( rfc,
X,
y,
cv=split,
scoring="accuracy",
n_jobs=-1)
plt.plot(range(1,11),rfc_s,label = "RandomForest")
?
?
6、特征重要性
#特征重要性
import eli5
onehot_columns = list(rfc.named_steps['preprocessor'].
named_transformers_['cat'].
named_steps['onehot'].
get_feature_names_out(input_features=cat_features))
feat_imp_list = num_features + onehot_columns
#按重要排序的前10個重要特征及其系數(shù)
feat_imp_df = eli5.formatters.as_dataframe.explain_weights_df(
rfc.named_steps['model'],
feature_names=feat_imp_list)
feat_imp_df.head(10)
feature | weight | std | |
---|---|---|---|
0 | deposit_type_Non Refund | 0.144919 | 0.108617 |
1 | lead_time | 0.140804 | 0.016317 |
2 | adr | 0.093004 | 0.003363 |
3 | deposit_type_No Deposit | 0.075658 | 0.103332 |
4 | arrival_date_day_of_month | 0.066274 | 0.002718 |
5 | arrival_date_week_number | 0.053368 | 0.002609 |
6 | total_of_special_requests | 0.052200 | 0.011193 |
7 | agent | 0.043441 | 0.005135 |
8 | previous_cancellations | 0.041885 | 0.014677 |
9 | stays_in_week_nights | 0.039749 | 0.002220 |
7、對權重大于10%的影響因素(提前預訂時間以及保證金類型)進行進一步分析
# 1、預定取消與提前預定時間的關系
plt.figure(figsize=(12, 8))
lead_cancel_data = data_done.groupby("lead_time")["is_canceled"].describe()
sns.scatterplot(x=lead_cancel_data.index, y=lead_cancel_data["mean"].values * 100,color='olive')
plt.title("提前預定時間對預訂取消率的影響", fontsize=16)
plt.xlabel("提前預訂時間", fontsize=16)
plt.ylabel("預訂取消的概率", fontsize=16)
plt.show()
提前預訂時間越長,預訂取消率越高。酒店應當結合具體經營成本考慮是否限定提前預訂的最長時間,以避免資源的占用。
#預定取消與保證金類型的關系
plt.figure(figsize=(12, 8))
deposit_cancel_data = data_done.groupby("deposit_type")["is_canceled"].describe()
sns.barplot(x=deposit_cancel_data.index, y=deposit_cancel_data["mean"] * 100,palette="Blues")
plt.title("Effect of deposit_type on cancelation", fontsize=16)
plt.xlabel("Deposit type", fontsize=16)
plt.ylabel("Cancelations [%]", fontsize=16)
plt.show()
押金不退還的預訂取消率在所有押金類型中最高,這種現(xiàn)象與觀念中的消費者心理有些背離,需要再一步分析。
事實上,各個市場細分渠道中,
線下旅行社代理預訂以及團體預訂的取消率最高,
而在押金類型中,無押金的預訂量主要集中在在線預訂,
而不退押金的預訂量主要集中在線下旅行社代理預訂以及團體預訂,導致不退押金的預訂取消率較高。文章來源:http://www.zghlxwxcb.cn/news/detail-430124.html
酒店可以嘗試設計問卷,對線下旅行社以及團體進行訪問,探尋原因及解決方法。文章來源地址http://www.zghlxwxcb.cn/news/detail-430124.html
到了這里,關于傳統(tǒng)機器學習(六)集成算法(1)—隨機森林算法及案例詳解的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網!