目录
一、项目背景
数据来源:kaggle上的酒店预订需求预测问题
该数据集包含了两家酒店的预订信息近12w条记录。根据背景资料,两家酒店均位于葡萄牙,一家是城市酒店,另一家是度假酒店。数据的时间跨度从2015年7月1日至2017年8月31日,数据中包含了诸如预订的时间,停留时间,成人,儿童或婴儿的数量以及可用停车位的数量等32个字段信息,如下表所示:
二、研究问题
1、酒店运营管理分析。分析譬如哪些房型的预订率以及入住率最高,什么时间用户取消预订单的概率最大等问题。可以从酒店基本情况、销售渠道、订单流量、用户基本情况、入住情况、预定情况等六个维度进行分析,能够帮助酒店实现精细化、精准化运营,为提高营收提供思路。
2、用户行为分析。分析譬如用户为什么会取消预订,具有什么样的行为特征说明用户有取消预订的倾向等问题。用户对于公司提供的产品服务所表现出的行为背后往往代表用户对于公司,对于产品的态度,从而可以在一定程度上预示用户的去留。
3、对用户是否会取消预订单进行预测,可以帮助酒店规划人员安排和食物需求。
三、查看数据
导入工具包
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import plotly.express as px
from sklearn.model_selection import train_test_split, KFold, cross_validate, cross_val_score
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import LabelEncoder, OneHotEncoder
from sklearn.impute import SimpleImputer
from sklearn.ensemble import RandomForestClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.tree import DecisionTreeClassifier
from xgboost import XGBClassifier
from sklearn.metrics import accuracy_score
import eli5
%matplotlib inline
plt.rcParams['font.sans-serif']=['SimHei']
plt.rcParams['axes.unicode_minus']=False
import warnings
warnings.filterwarnings('ignore')
C:\Users\ASUS\Anaconda3\lib\site-packages\sklearn\utils\deprecation.py:143: FutureWarning: The sklearn.metrics.scorer module is deprecated in version 0.22 and will be removed in version 0.24. The corresponding classes / functions should instead be imported from sklearn.metrics. Anything that cannot be imported from sklearn.metrics is now part of the private API.
warnings.warn(message, FutureWarning)
C:\Users\ASUS\Anaconda3\lib\site-packages\sklearn\utils\deprecation.py:143: FutureWarning: The sklearn.feature_selection.base module is deprecated in version 0.22 and will be removed in version 0.24. The corresponding classes / functions should instead be imported from sklearn.feature_selection. Anything that cannot be imported from sklearn.feature_selection is now part of the private API.
warnings.warn(message, FutureWarning)
导入数据
data=pd.read_csv(r"F:\kaggle\hotel booking demand\hotel_bookings.csv")
pd.set_option('display.max_columns',None)
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 |
data.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 119390 entries, 0 to 119389
Data columns (total 32 columns):
hotel 119390 non-null object
is_canceled 119390 non-null int64
lead_time 119390 non-null int64
arrival_date_year 119390 non-null int64
arrival_date_month 119390 non-null object
arrival_date_week_number 119390 non-null int64
arrival_date_day_of_month 119390 non-null int64
stays_in_weekend_nights 119390 non-null int64
stays_in_week_nights 119390 non-null int64
adults 119390 non-null int64
children 119386 non-null float64
babies 119390 non-null int64
meal 119390 non-null object
country 118902 non-null object
market_segment 119390 non-null object
distribution_channel 119390 non-null object
is_repeated_guest 119390 non-null int64
previous_cancellations 119390 non-null int64
previous_bookings_not_canceled 119390 non-null int64
reserved_room_type 119390 non-null object
assigned_room_type 119390 non-null object
booking_changes 119390 non-null int64
deposit_type 119390 non-null object
agent 103050 non-null float64
company 6797 non-null float64
days_in_waiting_list 119390 non-null int64
customer_type 119390 non-null object
adr 119390 non-null float64
required_car_parking_spaces 119390 non-null int64
total_of_special_requests 119390 non-null int64
reservation_status 119390 non-null object
reservation_status_date 119390 non-null object
dtypes: float64(4), int64(16), object(12)
memory usage: 29.1+ MB
四、数据清洗
4.1缺失值处理
data.isnull().sum()[data.isnull().sum()!=0]
children 4
country 488
agent 16340
company 112593
dtype: int64
总共有119389条数据,32个观测指标,children、country、agent、company四列存在缺失数据,company缺失较多且单个值分布太散,可以考虑删除,children和country、agent较少,考虑填充。
处理方法:
children孩子数为空值,可能是没有孩子,填充为0;
country字段为空值,可能是忘记登记导致,填充为unknown;
agent为空,则很有可能是在没有代理人的条件下预定的,因此填充为0;
company公司名称字段删除。
#数据备份
data_new = data.copy(deep = True)
# 删除company字段
data_new.drop("company", axis=1, inplace=True)
# 替换缺失值:
nan_replacements = {"children": 0.0, "country": "Unknown", "agent": 0}
data_new = data_new.fillna(nan_replacements)
# 检查缺失值处理情况
data_new.isnull().sum()[data_new.isnull().sum()!=0]
Series([], dtype: int64)
4.2异常值处理
# 删除客户数量是0的异常记录,即adult+children+babies=0的情况
zero_guest=data_new[data_new['adults']+data_new['children']+data_new['babies']==0].index
data_new.drop(zero_guest,inplace=True)
# 在数据解释中,meal字段下“SC”和“Undefined”实际为同一类别
data_new["meal"].replace("Undefined", "SC", inplace=True)
# 快速查看异常值
data_new.plot(legend=False)
<matplotlib.axes._subplots.AxesSubplot at 0x24e27778fc8>
data_new = data_new[data_new.adr<2000] #查找到1条异常值,adr高达5400,删除这条异常值
4.3其他处理
#将预定到店日期的三列合并成一列作为抵达日期,并将其和reservation_status_date都转为日期格式
data_new['arrival_date']=data_new['arrival_date_year'].map(str)+'/'+data_new['arrival_date_month'].map(str)+'/'+data_new['arrival_date_day_of_month'].map(str)
data_new['arrival_date']=pd.to_datetime(data_new.arrival_date)
data_new['reservation_status_date']=pd.to_datetime(data_new.reservation_status_date)
五、EDA及可视化
5.1酒店基本情况分析
酒店预订及入住率情况分析
nocancel_data=data_new.loc[data_new['is_canceled']==0]
cancel_data=data_new.loc[data_new['is_canceled']==1]
nocancel_percent=list(nocancel_data['hotel'].value_counts()/data_new['hotel'].value_counts())
cancel_percent=list(cancel_data['hotel'].value_counts()/data_new['hotel'].value_counts())
fig,axes=plt.subplots(1,2,figsize=(12,8))
ax1=sns.countplot(x='hotel',data=data_new,order=['City Hotel','Resort Hotel'],ax=axes[0])
ax1.set_title('总预定需求',fontsize=16)
ax1.set_ylabel('总预定数量',fontsize=14)
ax2=plt.bar([1,2],cancel_percent,tick_label=['City Hotel','Resort Hotel'],label='取消率')
ax2=plt.bar([1,2],nocancel_percent,bottom=cancel_percent,label="入住率")
plt.title('酒店入住率及取消率',fontsize=16)
plt.ylabel('占比',fontsize=14)
plt.legend()
<matplotlib.legend.Legend at 0x24e2767f4c8>
城市酒店的总体预定量远高于度假酒店,差不多是度假酒店的两倍;
从取消订单数来看,城市酒店的预定取消率也高于度假酒店;
这可能与两种酒店的地理位置及所承担的入住功能不同有关。
新老客户比例(复购率)及订单取消情况分析
repeated_gust = data_new['is_repeated_guest'].value_counts()
plt.figure(figsize=(12, 8))
plt.pie(x = repeated_gust.values,labels=['新客户','老客户'],autopct='%.2f%%'
,explode=[0,0.1],pctdistance=0.7,labeldistance = 1.1,radius = 0.8
,textprops = {'fontsize':14, 'color':'black'})
plt.title('新老客户比例',fontsize=16)
Text(0.5, 1.0, '新老客户比例')
plt.figure(figsize=(12, 8))
sns.barplot(x='is_repeated_guest',y='is_canceled',data=data_new)
plt.title('新老客户订单取消情况',fontsize=16)
plt.xticks([0,1],['新客户','老客户'],fontsize=14)
plt.ylabel('取消率',fontsize=14)
plt.xlabel('')
Text(0.5, 0, '')
新客户占比96.85%且取消预订的概率更高;回头客数量仅有很少一部分;
虽然预定酒店不像电商购物那样可以很容易做到重复购买,但酒店方在拉新的同时,也应该关注老用户的留存问题,如定期给老客户发送优惠券等,赠送各类兑换券等。
不同月份订单取消情况分析
ordered_months = ["January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"]
data_new["arrival_date_month"] = pd.Categorical(data_new["arrival_date_month"], categories=ordered_months, ordered=True)
plt.figure(figsize=(12, 8))
sns.barplot(x='arrival_date_month',y='is_canceled',hue='hotel',hue_order = ["City Hotel", "Resort Hotel"],data=data_new)
plt.title('不同月份预约取消率',fontsize=16)
plt.xlabel('月份',fontsize=14)
plt.ylabel('取消率',fontsize=14)
plt.xticks(rotation=30)
(array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]),
<a list of 12 Text xticklabel objects>)
城市酒店每个月的订单取消率相差不大,在40%上下小幅度波动;
度假酒店在夏季取消率最高,冬季取消率最低,猜测因为度假酒店夏季销量高,导致其取消率也高。
不同房型的订单数量分析
#只研究确实到访的客人,所以要排除掉已取消预约的记录
rh = data_new.loc[(data_new["hotel"] == "Resort Hotel") & (data_new["is_canceled"] == 0)]
ch = data_new.loc[(data_new["hotel"] == "City Hotel") & (data_new["is_canceled"] == 0)]
resort_room = rh.groupby("reserved_room_type")["hotel"].count()
city_room = ch.groupby("reserved_room_type")["hotel"].count()
resort_room_data = pd.DataFrame({"type": list(resort_room.index),
"hotel": "Resort Hotel",
"reserved_room": list(resort_room.values)})
city_room_data = pd.DataFrame({"type": list(city_room.index),
"hotel": "City Hotel",
"reserved_room": list(city_room.values)})
full_room_data = pd.concat([resort_room_data,city_room_data], ignore_index=True)
plt.figure(figsize=(12, 8))
sns.barplot(x = "type", y = "reserved_room", hue="hotel", data=full_room_data,
hue_order = ["City Hotel", "Resort Hotel"])
plt.title("不同房型的订单数量", fontsize=16)
plt.xlabel("房型", fontsize=14)
plt.ylabel("订单数量", fontsize=14)
plt.legend(loc="upper right")
plt.show()
城市酒店和度假酒店都是A、D两款房型入住率较高,可以判断A和D是酒店的主力房型,而B、C、F、G、H、L房型则预订者寥寥;
除了A、D房型以外,度假酒店E房型的入住数量也不少,猜测这和度假酒店的度假特性有关,可能是可以观景的房间;
不同季节各类房型的订单数量分析
season = {"January":"冬季", "February":"春季", "March":"春季", "April":"春季", "May":"夏季", "June":"夏季",
"July":"夏季", "August":"秋季", "September":"秋季", "October":"秋季", "November":"冬季", "December":"冬季"}
rh["arrival_date_season"] = [season[x] for x in rh.arrival_date_month]
ch["arrival_date_season"] = [season[x] for x in ch.arrival_date_month]
resort_room_season = rh.groupby(["reserved_room_type","arrival_date_season"])["hotel"].count()
city_room_season = ch.groupby(["reserved_room_type","arrival_date_season"])["hotel"].count()
resort_room_data = pd.DataFrame({"房型": list(resort_room_season.index.get_level_values("reserved_room_type").values),
"季节":list(resort_room_season.index.get_level_values("arrival_date_season").values),
"酒店类型": "度假酒店",
"入住数量": list(resort_room_season.values)})
city_room_data = pd.DataFrame({"房型": list(city_room_season.index.get_level_values("reserved_room_type").values),
"季节":list(city_room_season.index.get_level_values("arrival_date_season").values),
"酒店类型": "城市酒店",
"入住数量": list(city_room_season.values)})
full_room_data = pd.concat([resort_room_data,city_room_data], ignore_index=True)
plt.figure(figsize=(12, 8))
sns.catplot(x = "房型", y = "入住数量", hue="酒店类型",col="季节", data=full_room_data,kind="bar",
col_order=['春季','夏季','秋季','冬季'], hue_order = ["城市酒店", "度假酒店"],height=4, aspect=.7)
<seaborn.axisgrid.FacetGrid at 0x24e27906388>
<Figure size 864x576 with 0 Axes>
总体来讲,四季各类房型订单量的分布情况与整体房型的订单数量分布呈现相同的趋势;
一年四季A、D两款房型都是入住率较高的房型,度假酒店E房型的入住数量也较多;
度假酒店和城市酒店冬季客流量都会减少。
不同房型每晚人均费用分析
#选择未取消预约的数据进行分析
# normalize price per night (adr):
data_new["adr_pp"] = data_new["adr"] / (data_new["adults"] + data_new["children"])#因为babies年龄过小,所以人均价格中未将babies带入计算
data_new_guests = data_new.loc[data_new["is_canceled"] == 0] # 只分析未取消预订的记录
room_prices = data_new_guests[["hotel", "reserved_room_type", "adr_pp"]].sort_values("reserved_room_type")
plt.figure(figsize=(12, 8))
sns.boxplot(x="reserved_room_type", y="adr_pp", hue="hotel",data=room_prices, hue_order=["City Hotel", "Resort Hotel"],fliersize=0)
plt.title("不同房型人均每晚费用", fontsize=16)
plt.xlabel("房型", fontsize=14)
plt.ylabel("价格 [EUR]", fontsize=14)
plt.legend(loc="upper right")
plt.ylim(0, 160)
(0, 160)
A、D两种房型价格偏低,且预订人数较多,合理猜测它们可能是性价比较高的房型;
L房型价格是度假酒店价格最高的房型,猜测其可能是高档观景房,也因此预订人数寥寥。
5.2酒店销售渠道分析
不同细分市场订单数量分析
plt.figure(figsize=(12, 8))
sns.countplot(x='market_segment',hue='hotel',hue_order=["City Hotel", "Resort Hotel"],data=nocancel_data)
plt.title('不同细分市场的订单数量',fontsize=16)
plt.xlabel('细分市场',fontsize=14)
plt.ylabel('订单数量',fontsize=14)
Text(0, 0.5, '订单数量')
城市酒店和度假酒店都是通过网上预订(Online TA)的订单量最大,其次是通过线下的旅行社(offline TA/TO)和直接现场预订(Direct)两种方式,通过航空公司渠道预订的订单量最低。
不同细分市场每晚人均费用分析
plt.figure(figsize=(12, 8))
sns.boxplot(x="market_segment",y="adr_pp", hue="hotel",data=data_new_guests, hue_order=["City Hotel", "Resort Hotel"],fliersize=0)
plt.title("不同细分市场人均每晚费用", fontsize=16)
plt.xlabel("细分市场", fontsize=14)
plt.ylabel("价格 [EUR]", fontsize=14)
plt.legend(loc="upper right")
plt.ylim(0, 160)
(0, 160)
结合上面两图我们可以知道,城市酒店和度假酒店网上预订(Online TA)、通过线下的旅行社预订(offline TA/TO)和直接现场预订(Direct)三种方式订单量较高的原因在于其价格较低,客户更愿意购买;而航空公司的价格高,因此通过航空公司渠道预订的客户量最低。
不同细分市场订单取消情况分析
plt.figure(figsize=(12, 8))
sns.barplot(x='market_segment',y='is_canceled',data=data_new)
plt.title('不同细分市场订单取消情况',fontsize=16)
plt.xlabel('细分市场',fontsize=14)
plt.ylabel('取消率',fontsize=14)
plt.xticks(rotation=30)
(array([0, 1, 2, 3, 4, 5, 6, 7]), <a list of 8 Text xticklabel objects>)
除了Undefined这一未知市场外,组团市场(Groups)取消订单的概率最大,其次是线上旅行社(Online TA)和线下旅行社/承包商(offline TA/TO)市场。
不同分销渠道订单数量分析
plt.figure(figsize=(12, 8))
sns.countplot(x='distribution_channel',hue='hotel',hue_order=["City Hotel", "Resort Hotel"],data=nocancel_data)
plt.title('不同分销渠道订单数量',fontsize=16)
plt.xlabel('分销渠道',fontsize=14)
plt.ylabel('订单数量',fontsize=14)
Text(0, 0.5, '订单数量')
城市酒店和度假酒店都是通过旅行社/承包商(TA/TO)渠道预定的订单量最大,甚至是其他所有渠道订单量之和的三倍还多,通过GDS渠道预定的订单量最少,几乎没有。
5.3酒店流量分析
不同月份每晚人均费用分析
room_prices_mothly = data_new_guests[["hotel", "arrival_date_month", "adr_pp"]].sort_values("arrival_date_month")
ordered_months = ["January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"]
room_prices_mothly["arrival_date_month"] = pd.Categorical(room_prices_mothly["arrival_date_month"], categories=ordered_months, ordered=True)
plt.figure(figsize=(12, 8))
sns.lineplot(x = "arrival_date_month", y="adr_pp", hue="hotel", data=room_prices_mothly,
hue_order = ["City Hotel", "Resort Hotel"], ci="sd", size="hotel", sizes=(2.5, 2.5))
plt.title("不同月份每晚人均费用", fontsize=16)
plt.xlabel("月份", fontsize=14)
plt.xticks(rotation=45)
plt.ylabel("价格 [EUR]", fontsize=14)
Text(0, 0.5, '价格 [EUR]')
度假酒店价格随月份上下浮动较大,并且毫无疑问暑期8月价格最贵,而此时城市酒店的价格却较为便宜,分析可能这个时间大家习惯去度假,对城市酒店的需求较少;
城市酒店一年内整体价格波动不算大,在春秋两季价格偏高;
在冬季,城市酒店和度假酒店的价格均为一年中的最低价格。
不同月份订单数量分析
rh = data_new.loc[(data_new["hotel"] == "Resort Hotel") & (data_new["is_canceled"] == 0)]
ch = data_new.loc[(data_new["hotel"] == "City Hotel") & (data_new["is_canceled"] == 0)]
resort_guests_monthly = rh.groupby("arrival_date_month")["hotel"].count()
city_guests_monthly = ch.groupby("arrival_date_month")["hotel"].count()
resort_guest_data = pd.DataFrame({"month": list(resort_guests_monthly.index),
"hotel": "Resort hotel",
"guests": list(resort_guests_monthly.values)})
city_guest_data = pd.DataFrame({"month": list(city_guests_monthly.index),
"hotel": "City hotel",
"guests": list(city_guests_monthly.values)})
full_guest_data = pd.concat([resort_guest_data,city_guest_data], ignore_index=True)
ordered_months = ["January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"]
full_guest_data["month"] = pd.Categorical(full_guest_data["month"], categories=ordered_months, ordered=True)
# 七月和八月一共出现了三次,其他月份出现了两次,因此分开计算均值
full_guest_data.loc[(full_guest_data["month"] == "July") | (full_guest_data["month"] == "August"),
"guests"] /= 3
full_guest_data.loc[~((full_guest_data["month"] == "July") | (full_guest_data["month"] == "August")),
"guests"] /= 2
plt.figure(figsize=(12, 8))
sns.lineplot(x = "month", y="guests", hue="hotel", data=full_guest_data,
hue_order = ["City hotel", "Resort hotel"], size="hotel", sizes=(2.5, 2.5))
plt.title("不同月订单数量", fontsize=16)
plt.xlabel("月份", fontsize=14)
plt.xticks(rotation=45)
plt.ylabel("订单数量", fontsize=14)
Text(0, 0.5, '订单数量')
结合上面两图可以知道,城市酒店在春秋两季的预定量最大,并且其价格也最高;在7、8月份预定量较少且价格也较低;
度假酒店在7-9月预定量较少,但这期间价格却最高;
在冬季,城市酒店和度假酒店的预定量最少,且价格也均为一年中的最低价格;
城市酒店各月份的酒店预订量都高于度假酒店。
5.4用户基本情况分析
用户国籍分析
country_data=pd.DataFrame({'客户总数':data_new['country'].value_counts()})
px.choropleth(country_data, locations=country_data.index, color='客户总数', hover_name='客户总数'
,color_continuous_scale=px.colors.sequential.Plasma, title="客户分布").show()
由于酒店地点在葡萄牙(欧洲南部),因此我们可以看到游客主要集中在欧洲地区。
用户类型分析
customer_type = data_new['customer_type'].value_counts()
plt.figure(figsize=(12, 8))
plt.pie(x = customer_type.values, labels=customer_type.index, autopct='%.2f%%',pctdistance=0.7
, labeldistance = 1.1,radius = 0.8,textprops = {'fontsize':14, 'color':'black'})
plt.title('用户类型分布',fontsize=14)
Text(0.5, 1.0, '用户类型分布')
75.06的用户都是*出行用户,团体用户和合同用户很少,仅占所有用户的3.9%。
5.5用户入住情况分析
用户居住总天数分析
#总居住天数=工作日居住天数+周末居住天数
nocancel_data['total_nights']=nocancel_data['stays_in_weekend_nights']+nocancel_data['stays_in_week_nights']
nights_data=nocancel_data.groupby(['total_nights','hotel'],as_index=False).agg({'is_canceled':'count'})
#将数据拆成City Hotel和Resort Hotel
city_nights_data=nights_data.loc[nights_data['hotel']=='City Hotel']
Resort_nights_data=nights_data.loc[nights_data['hotel']=='Resort Hotel']
#因为两类酒店的总预订数不同,计算居住天数的占比数更方便进行比较
city_nights_data['number %']=city_nights_data['is_canceled']/city_nights_data['is_canceled'].sum()
Resort_nights_data['number %']=Resort_nights_data['is_canceled']/Resort_nights_data['is_canceled'].sum()
nights_data = pd.concat([city_nights_data, Resort_nights_data], ignore_index=True)
plt.figure(figsize=(12, 8))
sns.barplot(x = 'total_nights', y = 'number %', hue='hotel', data=nights_data)
plt.xlim(0,22)
plt.title('用户居住总天数分布', fontsize=16)
plt.xlabel('居住天数', fontsize=14)
plt.ylabel('占比', fontsize=14)
plt.legend(loc='upper right')
<matplotlib.legend.Legend at 0x24e39876208>
城市酒店绝大部分订单的居住天数在1-4天,居住超过7天的订单数量很少;
度假酒店中有超过20%的客户居住天数在7天及以上,居住1-4天的客户也不少,且居住一天的客户占比最高,超过20%;
猜测造成二者的差异主要是因为度假酒店多数为度假客户,所以会有较多的居住超过7天的情况。
用户入住时间分析
#构造用户周几到达特征
data_new_guests['arrival_weekday']=data_new_guests['arrival_date'].dt.weekday + 1 #选择未取消预订的用户进行分析
weekday_data = data_new_guests.groupby(['arrival_weekday','hotel'],as_index=False).agg({'is_canceled':'count'})
#将数据拆成City Hotel和Resort Hotel
city_weekday_data=weekday_data.loc[weekday_data['hotel']=='City Hotel']
Resort_weekday_data=weekday_data.loc[weekday_data['hotel']=='Resort Hotel']
#因为两类酒店的总预订数不同,计算居住天数的占比数更方便进行比较
city_weekday_data['number %']=city_weekday_data['is_canceled']/city_weekday_data['is_canceled'].sum()
Resort_weekday_data['number %']=Resort_weekday_data['is_canceled']/Resort_weekday_data['is_canceled'].sum()
weekday_data = pd.concat([city_weekday_data, Resort_weekday_data], ignore_index=True)
plt.figure(figsize=(12, 8))
sns.lineplot(x = 'arrival_weekday', y = 'number %', hue='hotel', data=weekday_data)
plt.title('用户入住时间分布', fontsize=16)
plt.ylim(0.1,0.2)
plt.xlabel('星期', fontsize=14)
plt.ylabel('占比', fontsize=14)
plt.legend(loc='upper right')
<matplotlib.legend.Legend at 0x24e39ac6308>
城市酒店在星期五时客户入住量最高,星期一次之,在周末两天入住量均较低,猜测可能大部分用户是利用周末假期出行游玩,所以周五入住人数较多;
度假酒店在星期一、四、六预定入住的订单都较高,猜测可能因为选择度假酒店的用户大多会选择居住时间久一些,而不局限于仅周末度假。
5.6用户预订情况分析
用户修改订单次数分析
booking_changes = data_new['booking_changes'].value_counts()
plt.figure(figsize=(12, 8))
plt.pie(x = booking_changes.values, labels=booking_changes.index, autopct='%.2f%%',pctdistance=0.7
, labeldistance = 1.1,radius = 0.8,textprops = {'fontsize':14, 'color':'black'})
plt.title('用户修改订单次数分布',fontsize=14)
plt.legend(loc='upper right')
<matplotlib.legend.Legend at 0x24e2e8eb488>
84.92%的用户没有修改过预定要求,10.62%的用户修改过一次预定要求,修改两次及以上订单的用户很少。
用户修改订单次数对订单取消的影响分析
changes=data_new.groupby('booking_changes')['is_canceled'].describe()
plt.figure(figsize=(12, 8))
sns.regplot(x=list(changes.index),y=changes['mean'].values)
plt.title('修改预约次数对预约取消率的影响',fontsize=16)
plt.xlabel('修改预约次数',fontsize=14)
plt.ylabel('取消率(%)',fontsize=14)
Text(0, 0.5, '取消率(%)')
对于城市酒店和度假酒店来说,随着用户修改订单次数的增加,用户取消预订的可能性减小;
猜测原因可能是修改订单次数多的的用户对订所定酒店较为满意,不想取消订单。
用户提前预定时长分析
nocancel_data["lead_time_bin"] = 0
nocancel_data.loc[(nocancel_data["lead_time"]>0)&(nocancel_data["lead_time"]<=7), "lead_time_bin"] = "1-7"
nocancel_data.loc[(nocancel_data["lead_time"]>7)&(nocancel_data["lead_time"]<=14), "lead_time_bin"] = "8-14"
nocancel_data.loc[(nocancel_data["lead_time"]>14)&(nocancel_data["lead_time"]<=30), "lead_time_bin"] = "15-30"
nocancel_data.loc[(nocancel_data["lead_time"]>30)&(nocancel_data["lead_time"]<=60), "lead_time_bin"] = "31-60"
nocancel_data.loc[(nocancel_data["lead_time"]>60)&(nocancel_data["lead_time"]<=90), "lead_time_bin"] = "61-90"
nocancel_data.loc[(nocancel_data["lead_time"]>90), "lead_time_bin"] = "90+"
lead_time_data=nocancel_data.groupby(['lead_time_bin','hotel'],as_index=False).agg({'is_canceled':'count'})
#将数据拆成City Hotel和Resort Hotel
city_lead_time_data=lead_time_data.loc[lead_time_data['hotel']=='City Hotel']
Resort_lead_time_data=lead_time_data.loc[lead_time_data['hotel']=='Resort Hotel']
#因为两类酒店的总预订数不同,计算提前预定时长的占比数更方便进行比较
city_lead_time_data['number %']=city_lead_time_data['is_canceled']/city_lead_time_data['is_canceled'].sum()
Resort_lead_time_data['number %']=Resort_lead_time_data['is_canceled']/Resort_nights_data['is_canceled'].sum()
lead_time_data = pd.concat([city_lead_time_data, Resort_lead_time_data], ignore_index=True)
plt.figure(figsize=(12, 8))
sns.barplot(x = 'lead_time_bin', y = 'number %', hue='hotel', data=lead_time_data)
#plt.xlim(0,22)
plt.title('用户提前预定时长分布', fontsize=16)
plt.xlabel('提前预定时长', fontsize=14)
plt.ylabel('占比', fontsize=14)
plt.legend(loc='upper left')
<matplotlib.legend.Legend at 0x24e3bbb8808>
城市酒店和度假酒店均是提前三个月预定的用户数量最多,且均占各自订单总量的30%以上;
度假酒店当天预定的占比为10%+,城市酒店当天预定的占比仅为5%+;
根据上图分析,用户似乎更倾向于提前较长时间预定城市酒店而不是度假酒店,这一点可能与我们的普遍认知有点出入。猜测可能是因为人们去度假时通常花费时间较长,投入资金也较多,若提前太早预订,从预定到入住这段时间的不确定性因素太多,可能会导致最终的行成取消,因此,为了规避风险,用户会选择临近时间再定酒店。
用户餐食预定情况分析
#餐型选择
ch_nocancel_meal = nocancel_data.loc[nocancel_data['hotel']=='City Hotel','meal'].value_counts()
rh_nocancel_meal = nocancel_data.loc[nocancel_data['hotel']=='Resort Hotel','meal'].value_counts()
plt.figure(figsize=(12,8))
plt.subplot(1,2,1)
plt.pie(x=ch_nocancel_meal.values,labels=ch_nocancel_meal.index, autopct='%.2f%%',pctdistance=0.7
,labeldistance = 1.1,radius = 0.8,textprops = {'fontsize':14, 'color':'black'})
plt.title("城市酒店客户不同餐食选择占比(%)", fontsize=16)
plt.legend()
plt.subplot(1,2,2)
plt.pie(x=rh_nocancel_meal.values,labels=rh_nocancel_meal.index, autopct='%.2f%%',pctdistance=0.7
,labeldistance = 1.1,radius = 0.8,textprops = {'fontsize':14, 'color':'black'})
plt.title("度假酒店客户不同餐食选择占比(%)", fontsize=16)
plt.legend()
<matplotlib.legend.Legend at 0x24e2ecb3a48>
城市酒店与度假酒店餐食类型选择大体一致,大多数用户选择仅早餐(BB)
用户餐食选择对订单取消的影响分析
canceled_meal = cancel_data['meal'].value_counts()
nocancel_meal = nocancel_data['meal'].value_counts()
plt.figure(figsize=(12, 8))
plt.subplot(121)
plt.pie(canceled_meal.values, labels=canceled_meal.index,autopct='%.2f%%',pctdistance=0.7
,labeldistance = 1.1,radius = 0.8,textprops = {'fontsize':14, 'color':'black'})
plt.title("未取消订单用户餐食选择分布", fontsize=14)
plt.legend(loc="upper right")
plt.subplot(122)
plt.pie(nocancel_meal.values, labels=nocancel_meal.index,autopct='%.2f%%',pctdistance=0.7
,labeldistance = 1.1,radius = 0.8,textprops = {'fontsize':14, 'color':'black'})
plt.title("取消订单用户餐食选择分布", fontsize=14)
plt.legend(loc="upper right")
<matplotlib.legend.Legend at 0x24e2ed81748>
无论是取消预定的还是未取消预定的客户餐食选择比例基本相同,可见餐食这一字段对订单是否取消影响不大,可以排除因餐食而导致订单取消这一原因。
5.7影响订单取消的因素分析
#将分类数据进行标签化处理,方便进行后续的相关性计算
le = LabelEncoder()
data_new['hotel'] = le.fit_transform(data_new['hotel'])
data_new['arrival_date_month'] = le.fit_transform(data_new['arrival_date_month'])
data_new['meal'] = le.fit_transform(data_new['meal'])
data_new['country'] = le.fit_transform(data_new['country'])
data_new['market_segment']= le.fit_transform(data_new['market_segment'])
data_new['distribution_channel']=le.fit_transform(data_new['distribution_channel'])
data_new['reserved_room_type'] = le.fit_transform(data_new['reserved_room_type'])
data_new['deposit_type'] = le.fit_transform(data_new['deposit_type'])
data_new['agent'] = le.fit_transform(data_new['agent'])
data_new['customer_type'] = le.fit_transform(data_new['customer_type'])
data_new['reservation_status'] = le.fit_transform(data_new['reservation_status'])
data_corr=data_new.corr(method='spearman')
np.abs(data_corr['is_canceled']).sort_values(ascending=False)
is_canceled 1.000000
reservation_status 0.942698
deposit_type 0.477094
lead_time 0.316456
previous_cancellations 0.270321
total_of_special_requests 0.258737
country 0.257373
required_car_parking_spaces 0.197603
booking_changes 0.184329
distribution_channel 0.173743
hotel 0.137076
previous_bookings_not_canceled 0.115394
customer_type 0.099372
days_in_waiting_list 0.098420
is_repeated_guest 0.083744
reserved_room_type 0.068025
adults 0.065665
adr 0.049909
stays_in_week_nights 0.041443
babies 0.034389
market_segment 0.026344
agent 0.024747
arrival_date_year 0.018038
meal 0.013490
arrival_date_week_number 0.007760
adr_pp 0.006758
arrival_date_day_of_month 0.005973
stays_in_weekend_nights 0.004075
children 0.003002
arrival_date_month 0.001180
Name: is_canceled, dtype: float64
通过分析特征间的相关性发现,与订单取消有一定相关性的前三个因素为预付款方式,提前预定时间以及客户之前订单的取消次数(reservation_status该字段本身就表示了该订单是否被取消,故排除该项)。下面详细分析一下这三个特征对订单取消率的影响。
用户预付款方式对订单取消的影响分析
plt.figure(figsize=(8, 6))
sns.barplot(x='deposit_type', y='is_canceled',data=data)
plt.title('预付款方式对预约取消的影响', fontsize=14)
plt.xlabel('预付款方式', fontsize=14)
plt.ylabel('取消率', fontsize=14)
Text(0, 0.5, '取消率')
可以看到预付款类型“不退定金”的预约取消率接近百分之百,而“没有定金”和“定金可退”两种预定方式取消率却偏低,这种现象与正常逻辑存在较大偏差,猜测可能是数据本身存在问题或标签类别有误。
接着我们查看按存款类型分组的所有数据平均值进一步分析上述情况发生的原因。
deposit_mean_data = data.groupby("deposit_type").mean()
deposit_mean_data
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 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
deposit_type | ||||||||||||||||||||
No Deposit | 0.283770 | 88.756615 | 2016.174014 | 27.135310 | 15.814652 | 0.970165 | 2.556799 | 1.862597 | 0.118400 | 0.009069 | 0.035760 | 0.042039 | 0.154911 | 0.249634 | 90.996175 | 189.496772 | 1.519347 | 103.525067 | 0.071129 | 0.651427 |
Non Refund | 0.993624 | 212.908891 | 2016.031466 | 27.448619 | 15.595462 | 0.621718 | 2.091109 | 1.811407 | 0.000617 | 0.000000 | 0.004387 | 0.411462 | 0.010626 | 0.012477 | 55.248165 | 179.189723 | 7.992253 | 89.964017 | 0.000069 | 0.001782 |
Refundable | 0.222222 | 152.098765 | 2016.141975 | 20.932099 | 23.456790 | 0.975309 | 2.851852 | 1.907407 | 0.030864 | 0.000000 | 0.024691 | 0.000000 | 0.018519 | 0.592593 | 189.625000 | 227.936842 | 9.586420 | 76.209753 | 0.123457 | 0.141975 |
将不退定金和定金可退的各项平均值进行比较,发现:
不退还定金的特点是预约提前期相较定金可退延长近2倍;重复预订的客人较少;以前的取消次数是定金可退的10倍。
猜测可能是这些因素加起来导致不退还定金这种预付款类型的客户预约取消率很高。
用户提前预定时长对订单取消率分析
#提前预定期对旅客是否选择取消预订也有很大影响,因为lead_time字段中的值分布多且散乱,所以使用散点图比较合适,同时还可以绘制一条回归线。
lead_cancel_data = data_new.groupby("lead_time")["is_canceled"].describe()
lead_cancel_data_10 = lead_cancel_data.loc[lead_cancel_data["count"] >= 10]
plt.figure(figsize=(12, 8))
sns.regplot(x=list(lead_cancel_data_10.index), y=lead_cancel_data_10["mean"].values)
plt.title("用户提前预定时长对取消率的影响", fontsize=16)
plt.xlabel("提前预约时长", fontsize=14)
plt.ylabel("取消率", fontsize=14)
#plt.xlim(0,365)
Text(0, 0.5, '取消率')
可以明显看到不同的提前预定时长对用户是否取消预定有一定影响;
并且通常预定日期离入住日期越近,越不容易取消酒店房间预定。
用户之前取消订单次数对本次订单取消的影响
previous_cancellations_data=data_new.groupby('previous_cancellations')['is_canceled'].describe()
plt.figure(figsize=(8, 6))
sns.regplot(x=list(previous_cancellations_data.index),y=previous_cancellations_data['mean']*100)
plt.title('之前取消预约次数对预约取消率的影响', fontsize=16)
plt.xlabel('之前预约订单的取消次数', fontsize=14)
plt.ylabel('取消率(%)', fontsize=14)
Text(0, 0.5, '取消率(%)')
数据主要集中在两个区域内;之前取消订单次数较少的客户他们本次的订单取消率要明显低于之前取消次数较多的客户。
六、构建模型
预测什么样的酒店预定订单可能会取消预定
#选择恰当的特征进行模型训练
#删除arrival_date_year,assigned_room_type,reservation_status, reservation_status_date,为订单完成或取消后的事务
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","booking_changes","agent",
"days_in_waiting_list","required_car_parking_spaces", "total_of_special_requests", "adr"]
cat_features = ["hotel","arrival_date_month","meal","country","market_segment",
"distribution_channel","reserved_room_type","deposit_type","customer_type"]
#分离特征和预测值
features = num_features + cat_features
X = data[features]
y = data["is_canceled"]
# 数值特征处理:
# 对于大多数num_cols,缺失值填充0
num_transformer = SimpleImputer(strategy="constant")
# 分类特征的预处理:
cat_transformer = Pipeline(steps=[
("imputer", SimpleImputer(strategy="constant", fill_value="Unknown")),
("onehot", OneHotEncoder(handle_unknown='ignore'))])
# 数值和分类特征的预处理:
preprocessor = ColumnTransformer(transformers=[("num", num_transformer, num_features),
("cat", cat_transformer, cat_features)])
# 定义要测试的模型:
base_models = [("DT_model", DecisionTreeClassifier(random_state=42)),
("RF_model", RandomForestClassifier(random_state=42,n_jobs=-1)),
("LR_model", LogisticRegression(random_state=42,n_jobs=-1)),
("XGB_model", XGBClassifier(random_state=42, n_jobs=-1))]
#将数据分成“kfold”部分进行交叉验证,
#使用shuffle确保数据的随机分布:
kfolds = 4
split = KFold(n_splits=kfolds, shuffle=True, random_state=42)
#对每个模型进行预处理、拟合、预测和评分:
for name, model in base_models:
#将数据和模型的预处理打包:
model_steps = Pipeline(steps=[('preprocessor', preprocessor),
('model', model)])
#获取每个模型的交叉验证分数:
cv_results = cross_val_score(model_steps,
X, y,
cv=split,
scoring="accuracy",
n_jobs=-1)
# 输出结果
min_score = round(min(cv_results), 4)
max_score = round(max(cv_results), 4)
mean_score = round(np.mean(cv_results), 4)
std_dev = round(np.std(cv_results), 4)
print(f"{name} 交叉验证准确性得分为: {mean_score} +/- {std_dev} (std) min: {min_score}, max: {max_score}")
DT_model 交叉验证准确性得分为: 0.8526 +/- 0.0017 (std) min: 0.8514, max: 0.8556
RF_model 交叉验证准确性得分为: 0.8887 +/- 0.002 (std) min: 0.886, max: 0.8911
LR_model 交叉验证准确性得分为: 0.8043 +/- 0.0015 (std) min: 0.8017, max: 0.8055
XGB_model 交叉验证准确性得分为: 0.8729 +/- 0.0028 (std) min: 0.8694, max: 0.8771
可以看到采用随机森林RF_model进行预测的效果最好
接下来选择随机森林模型进行预测并对其进行一些超参数的优化
# 我找到的最优参数结果入下:
rf_model_enh = RandomForestClassifier(n_estimators=160,
max_features=0.4,
min_samples_split=2,
n_jobs=-1,
random_state=0)
split = KFold(n_splits=kfolds, shuffle=True, random_state=42)
model_pipe = Pipeline(steps=[('preprocessor', preprocessor),
('model', rf_model_enh)])
cv_results = cross_val_score(model_pipe,
X, y,
cv=split,
scoring="accuracy",
n_jobs=-1)
# 输出结果:
min_score = round(min(cv_results), 4)
max_score = round(max(cv_results), 4)
mean_score = round(np.mean(cv_results), 4)
std_dev = round(np.std(cv_results), 4)
print(f"Enhanced RF model cross validation accuarcy score: {mean_score} +/- {std_dev} (std) min: {min_score}, max: {max_score}")
Enhanced RF model cross validation accuarcy score: 0.8911 +/- 0.0023 (std) min: 0.8883, max: 0.8942
可以看到经过参数调整的模型预测精度有适当提高,最终预测准确率平均得分为0.8911。
#评估特征重要性:
model_pipe.fit(X,y)
# 从独热编码中获取列的名称:
onehot_columns = list(model_pipe.named_steps['preprocessor'].
named_transformers_['cat'].
named_steps['onehot'].
get_feature_names(input_features=cat_features))
feat_imp_list = num_features + onehot_columns
# 展示重要性排名前10的特征:
feat_imp_df = eli5.formatters.as_dataframe.explain_weights_df(
model_pipe.named_steps['model'],
feature_names=feat_imp_list)
feat_imp_df.head(10)
feature | weight | std | |
---|---|---|---|
0 | deposit_type_Non Refund | 0.120919 | 0.107538 |
1 | lead_time | 0.117889 | 0.012516 |
2 | deposit_type_No Deposit | 0.086481 | 0.103455 |
3 | adr | 0.068449 | 0.003370 |
4 | country_PRT | 0.063977 | 0.029664 |
5 | agent | 0.049753 | 0.009710 |
6 | arrival_date_day_of_month | 0.048266 | 0.001724 |
7 | total_of_special_requests | 0.047548 | 0.009820 |
8 | arrival_date_week_number | 0.043775 | 0.001772 |
9 | market_segment_Online TA | 0.034299 | 0.017000 |
经过特征重要性评估可以知道deposit_type_Non Refund、lead_time和deposit_type_No Deposit是预测用户是否会取消订单的三个最重要的特征。
七、结论和建议
根据以上分析可以得到一些有用的结论和建议:
1、酒店的客户大多数为新客户,老客户只占3.15%,酒店需要考虑在老客户方面的提升。如可以通过推出会员制度、发放优惠券等活动,提高客户的回购率。
2、城市酒店和度假酒店的预定量在冬季最少,且价格也均为一年中的最低价格。但一年四季城市酒店和度假酒店都是A、D两款房型最受欢迎,且入住率较高。酒店可以针对这两款房型在不同季节制定更多个性化的营销活动,提高用户预订率及入住率。
3、在出行方式上,75%的顾客都是自己规划自己的出行活动,参加旅行社或由公司统一组织出行的顾客较少。此外,顾客首选的订票方式是通过线上承包商这一渠道。酒店可以在线上订票方面多做一些营销活动的尝试,提高顾客的购买转化率。
4、在订单取消率方面,城市酒店每个月波动不大,而度假酒店受季节影响较大,夏季订单量多取消率也高,冬季订单量少,取消率也相对较少。与订单取消密切相关的特征包括用户预付定金类型、提前预定时长、平均每日收费等,酒店需要重点关注这些特征,并采取相应对策,如提前与客户打电话沟通等方式进行主动干预,尽可能减少客户临时取消预订事件的发生。同时,酒店要充分利用已有数据构建顾客是否会取消订单的预测模型,对顾客是否会取消订单进行预测,在预测到的取消订单的数量范围制定酒店房源分配计划,及时调整运营策略,最小化顾客取消订单对酒店营收等各方面造成的影响。