kaggle实战:酒店预订建模
本文是针对kaggle上面一份酒店预订数据的分析与建模,主要内容包含:
结果
官网上面有一个大佬的模型跑出来acc为99.5%,参考了他的一些代码(向大佬学习),加入了一些我自己的处理思路,最终结果为99.6%,主要还是体现在特征工程上面下了功夫!
原kaggle官网数据集地址:
https://www.kaggle.com/code/niteshyadav3103/hotel-booking-prediction-99-5-acc/notebook
导入库
主要是用于数据处理、可视化、建模、评分等
1 | import pandas as pd |
导入数据
查看数据的基本信息:
查看数据的基本信息:
In [3]:
1 |
|
Out[3]:
1 | Index(['hotel', 'is_canceled', 'lead_time', 'arrival_date_year', |
字段的具体中文含义:
- 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 每日房间均价 (Average Daily Rate)
- required_car_parking_spaces 停车位数量
- total_of_special_requests 特殊需求数(例如高层或双床)
- reservation_status 订单状态
- reservation_status_date 订单状态确定日期
In [4]:
1 | # 2、总字段个数 |
Out[4]:
1 | 32 |
In [5]:
1 |
|
Out[5]:
1 | hotel object |
In [6]:
1 | # 4、不同类型字段统计 |
Out[6]:
1 | int64 16 |
In [7]:
1 |
|
Out[7]:
1 | (119390, 32) |
In [8]:
1 | # 6、描述统计信息 |
info函数能够查看数据的完整信息,包含字段类型,行列索引、缺失值情况等
1 | # 7、数据完整信息 |
缺失值信息
统计每个字段缺失值信息
统计每个字段的缺失值数量及比例
In [10]:
1 | null_df = pd.DataFrame({"Null Values": df.isnull().sum(), |
缺失值可视化
将缺失值信息进行可视化展示:
In [11]:
1 | msno.bar(df, color="blue") |
缺失值处理
1、字段 children和字段country 缺失值比例都不到1%,比例很小;我们直接把缺失值的部分删除
1 | # 把非缺失值的数据筛选出来 |
2、字段company缺失值比例高达94.3%,我们考虑直接删除该字段:
In [14]:
1 | df.drop("company", axis=1, inplace=True) |
3、字段agent(代理商费用)的缺失值为13.68%,处理为:
In [15]:
1 | # 1、先查看字段具体信息 |
Out[15]:
1 | 9.0 31959 |
我们可以考虑使用的值来进行填充,比如:
- 0:无法确定缺失值的具体数据
- 9:众数
- 均值:字段现有值的均值
在这里我们考虑使用0来进行填充:
In [16]:
1 | df["agent"].fillna(0,inplace=True) |
特殊处理
处理1:入住人数不能为0
考虑到一个房间中adults、children和babies的数量不能同时为0:
In [18]:
1 | special = (df["children"] == 0) & (df.adults == 0) & (df.babies == 0) |
Out[18]:
1 | 0 False |
1 | # 排除特殊情况 |
处理2:adr(日均价)
- 取值不能为负数
- 最大值为5400,可以判断属于异常值
In [21]:
1 | df["adr"].value_counts().sort_index() |
Out[21]:
1 | -6.38 1 |
In [22]:
通过小提琴图来查看数据的分布情况:处理前明显有离群点
1 | px.violin(y=df["adr"]) # 处理前 |
箱型图也可以观察:
1 | px.box(df,y="adr") |
可以看到这个特殊点在City Hotel中:
实施删除的过程:
In [25]:
1 | # 删除大于1000的信息 df = df.drop(df[df.adr >1000].index) |
In [26]:
1 | px.violin(y=df["adr"]) # 删除后 |
1 | px.box(df,y="adr",color="hotel") # 删除后 |
数据EDA-Exploratory Data Analysis
取消和未取消的顾客数对比
In [28]:
1 | df["is_canceled"].value_counts() |
Out[28]:
1 | 0 74589 |
In [29]:
1 | # 取消和未取消人数对比 0-未取消 1-取消 |
未取消的顾客来自哪里?
In [30]:
1 | data = df[df.is_canceled == 0] # 未取消的数据 |
In [31]:
1 | number_no_canceled = data["country"].value_counts().reset_index() |
1 | # 地图可视化 |
结论1:预订的顾客主要是来自Portugal,大部分是欧洲的国家
房间的每日均价是多少?
In [33]:
1 | px.box(data, # 数据 |
结论2:每个房间的均价还是取决于它的类型和标准差
全年每晚的价格是多少?
两种不同类型酒店的全年均价变化
In [34]:
1 | data_resort = [ ["hotel"] == "Resort Hotel"] |
In [35]:
1 | resort_hotel = data_resort.groupby(['arrival_date_month'])['adr'].mean().reset_index() |
1 | # 合并两个数据 |
为了让月份按照正常时间排序,安装两个包:
jupyter notebook直接安装:前面要加!
In [37]:
1 | !pip install sort-dataframeby-monthorweek |
In [39]:
1 | import sort_dataframeby_monthorweek as sd |
In [40]:
1 | new_total_hotel = sort_month(total_hotel, "month") |
正确的顺序排列
1 | fig = px.line(new_total_hotel, |
结论:
- Resort Hotel在夏季的价格明显比 City Hotel的价格高
- City Hotel的价格变化相对更小。但是City Hotel的价格从4月开始就已经很高,一直持续到9月份
KDE图
KDE(Kernel Density Estimation,核密度图),可以认为是对直方图的加窗平滑。通过KDE分布图场内看数据在不同情形下的分布
In [42]:
1 | plt.figure(figsize=(6,3), dpi=150) |
最为繁忙的季节-the most busy months
In [43]:
1 | resort_guests = data_resort['arrival_date_month'].value_counts().reset_index() |
同样的将月份进行排序处理:
In [45]:
1 | new_final_guests = sort_month(final_guests, "Month") |
1 | fig = px.line(new_final_guests, |
结论:
- 很明显:City Hotel的人数是高于Resort Hotel,更受欢迎
- City Hotel在7-8月份的时候,尽管价格高(上图),但人数也达到了峰值
- 两个Hotel在冬季的顾客都是很少的
顾客停留多久?
In [47]:
1 | data["total_nights"] = data['stays_in_weekend_nights'] + data['stays_in_week_nights'] |
两个不同酒店在不同停留时间下的统计:
In [48]:
1 | stay_groupby = (data.groupby(['total_nights', 'hotel'])["is_canceled"] |
Out[48]:
total_nights | hotel | Number of stays | |
---|---|---|---|
0 | 0 | City Hotel | 251 |
1 | 0 | Resort Hotel | 366 |
2 | 1 | City Hotel | 9155 |
3 | 1 | Resort Hotel | 6368 |
4 | 2 | City Hotel | 10983 |
In [49]:
1 | fig = px.bar(stay_groupby, |
数据预处理-Data Pre Processing
相关性判断
In [50]:
1 | plt.figure (figsize=(24,12)) |
查看每个特征和目标变量is_canceled的相关系数的绝对值,并降序排列:
In [51]:
1 | corr_with_iscanceled = df.corr()["is_canceled"].abs().sort_values(ascending=False) |
Out[51]:
1 | is_canceled 1.000000 |
删除无效字段
In [52]:
1 | no_use_col = ['arrival_date_year', 'assigned_room_type', |
In [53]:
1 | df.drop(no_use_col, axis=1, inplace=True) |
特征工程
离散型变量处理
In [54]:
1 | df["hotel"].dtype # Series型数据的字段类型 |
Out[54]:
1 | dtype('O') |
In [55]:
1 | cat_cols = [col for col in df.columns if df[col].dtype == "O"] |
Out[55]:
1 | ['hotel', |
In [56]:
1 | cat_df = df[cat_cols] |
In [57]:
1 | cat_df.dtypes |
Out[57]:
1 | hotel object |
In [58]:
1 | # 1、转成时间类型数据 |
In [59]:
1 | # 2、提取年月日 |
In [60]:
1 |
|
In [61]:
1 | # 4、每个字段的唯一值 |
特征编码
In [62]:
1 | # 酒店 |
连续型变量处理
In [63]:
1 | num_df = df.drop(columns=cat_cols,axis=1) |
1 | # 方差偏大的字段进行对数化处理 |
建模
合并两份df
In [68]:
1 | X = pd.concat([cat_df, num_df], axis=1) |
In [69]:
1 | print(X.shape) |
切割数据
In [70]:
1 | X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.2,random_state=412) |
建模1:逻辑回归
In [71]:
1 | # 模型实例化 |
In [72]:
1 | # 混淆矩阵可视化 |
模型2:KNN
In [73]:
1 | knn = KNeighborsClassifier() |
模型3:决策树
In [74]:
1 | dtc = DecisionTreeClassifier() |
模型4:随机森林
In [75]:
1 | rd_clf = RandomForestClassifier() |
模型5:AdaBoost
In [76]:
1 | ada = AdaBoostClassifier(base_estimator = dtc) |
模型6:梯度提升树-Gradient Boosting Classifier
In [77]:
1 | gb = GradientBoostingClassifier() |
模型7:XgBoost Classifier
In [78]:
1 | xgb = XGBClassifier(booster='gbtree', |
模型8:Cat Boost分类器
In [79]:
1 | cat = CatBoostClassifier(iterations=100) |
模型9:极端树-Extra Trees Classifier
In [80]:
1 | etc = ExtraTreesClassifier() |
模型10:LGBM
In [81]:
1 | lgbm = LGBMClassifier(learning_rate = 1) |
模型11:投票分类器-Voting Classifier
这个是重点建模:多分类器的投票表决
In [82]:
1 | classifiers = [('Gradient Boosting Classifier', gb), |
1 | y_pred_vc = vc.predict(X_test) |
基于深度学习keras建模
数据预处理和切割
In [84]:
1 | from tensorflow.keras.utils import to_categorical |
In [85]:
1 | # 切割数据 |
In [86]:
1 | import tensorflow as tf |
搭建网络
In [87]:
1 | X.shape[1] |
Out[87]:
1 | 25 |
In [88]:
1 | model = Sequential() |
指标可视化-loss
In [89]:
1 | train_loss = model_history.history["loss"] |
Out[89]:
train_loss | val_loss | |
---|---|---|
0 | 0.320637 | 0.189505 |
1 | 0.152539 | 0.394518 |
2 | 0.112900 | 0.094703 |
3 | 0.091933 | 0.105522 |
4 | 0.078177 | 0.096059 |
In [90]:
1 | fig = px.line(loss, |
指标可视化-acc
In [91]:
1 | train_acc = model_history.history["accuracy"] |
1 | #最终预测值 |
模型对比
不同模型的结果对比
In [93]:
1 | models = pd.DataFrame({ |
不同模型的得分可视化对比:
1 | fig = px.bar(models, |
可以看到Cat Boost分类达到了惊人的99.61%
又是收获满满的一篇文章✌🏻!