import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder, StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.tree import DecisionTreeClassifier
from xgboost import XGBClassifier
from sklearn.metrics import (accuracy_score, precision_score, recall_score,
                             f1_score, roc_curve, auc, classification_report,
                             confusion_matrix, RocCurveDisplay)
import warnings
warnings.filterwarnings('ignore')

plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False

# 读取数据
df = pd.read_csv('hotel_bookings_updated_2024.csv')
print("=" * 60)
print("原始数据形状:", df.shape)
print("\n各列数据类型:")
print(df.dtypes)
print("\n缺失值统计:")
print(df.isnull().sum()[df.isnull().sum() > 0])
print("\n数据基本描述:")
print(df.describe())

# 第一步：缺失值处理
print("\n" + "=" * 60)
print("第一步：缺失值处理")
print("=" * 60)

# 删掉跟结果直接相关的列，避免影响模型判断
drop_cols = ['reservation_status', 'reservation_status_date']
df.drop(columns=drop_cols, inplace=True)
print(f"删除数据泄露列: {drop_cols}")

# 看看哪些列有空值
missing = df.isnull().sum()
missing_pct = (df.isnull().sum() / len(df) * 100).round(2)
missing_info = pd.DataFrame({'缺失数量': missing, '缺失比例(%)': missing_pct})
missing_info = missing_info[missing_info['缺失数量'] > 0]
print("\n缺失值详情:")
print(missing_info)

# children 空值填0，表示没有小孩
if 'children' in df.columns:
    df['children'].fillna(0, inplace=True)
    print("children 列缺失值用 0 填充")

# agent和company空值比较多，填0表示没有
for col in ['agent', 'company']:
    if col in df.columns:
        df[col].fillna(0, inplace=True)
        print(f"{col} 列缺失值用 0 填充")

# country空值用出现最多的国家填充
if 'country' in df.columns and df['country'].isnull().sum() > 0:
    df['country'].fillna(df['country'].mode()[0], inplace=True)
    print("country 列缺失值用众数填充")

# 剩下的空值：数字列用中位数填，文字列用出现最多的值填
for col in df.columns:
    if df[col].isnull().sum() > 0:
        if df[col].dtype in ['float64', 'int64']:
            df[col].fillna(df[col].median(), inplace=True)
            print(f"{col} 列缺失值用中位数填充")
        else:
            df[col].fillna(df[col].mode()[0], inplace=True)
            print(f"{col} 列缺失值用众数填充")

print(f"\n处理后缺失值总数: {df.isnull().sum().sum()}")

# 第二步：异常值处理
print("\n" + "=" * 60)
print("第二步：异常值处理")
print("=" * 60)

# 处理adr（房价）中的异常数据，去掉负值和特别离谱的高价
print(f"adr 处理前范围: [{df['adr'].min()}, {df['adr'].max()}]")
df = df[df['adr'] >= 0]
Q1 = df['adr'].quantile(0.25)
Q3 = df['adr'].quantile(0.75)
IQR = Q3 - Q1
upper_bound = Q3 + 3 * IQR
df = df[df['adr'] <= upper_bound]
print(f"adr 处理后范围: [{df['adr'].min()}, {df['adr'].max()}]，上界={upper_bound:.2f}")

# 如果大人、小孩、婴儿全是0，说明没人住，属于异常数据
mask_no_guest = (df['adults'] == 0) & (df['children'] == 0) & (df['babies'] == 0)
print(f"无住客记录数: {mask_no_guest.sum()}，已删除")
df = df[~mask_no_guest]

# 处理提前预订天数中特别离谱的值
print(f"lead_time 处理前范围: [{df['lead_time'].min()}, {df['lead_time'].max()}]")
Q1_lt = df['lead_time'].quantile(0.25)
Q3_lt = df['lead_time'].quantile(0.75)
IQR_lt = Q3_lt - Q1_lt
upper_lt = Q3_lt + 3 * IQR_lt
df = df[df['lead_time'] <= upper_lt]
print(f"lead_time 处理后范围: [{df['lead_time'].min()}, {df['lead_time'].max()}]，上界={upper_lt:.2f}")

print(f"\n异常值处理后数据形状: {df.shape}")

# 第三步：特征编码
print("\n" + "=" * 60)
print("第三步：特征编码")
print("=" * 60)

# 把列分成文字类和数字类
cat_cols = df.select_dtypes(include=['object']).columns.tolist()
num_cols = df.select_dtypes(include=['int64', 'float64']).columns.tolist()
num_cols.remove('is_canceled')  # 这是要预测的列，不算特征

print(f"分类特征 ({len(cat_cols)}): {cat_cols}")
print(f"数值特征 ({len(num_cols)}): {num_cols}")

# 把文字类的列转成数字，模型才能用
label_encoders = {}
for col in cat_cols:
    le = LabelEncoder()
    df[col] = le.fit_transform(df[col].astype(str))
    label_encoders[col] = le
    print(f"  {col}: {len(le.classes_)} 个类别 -> 编码完成")

print(f"\n编码后数据形状: {df.shape}")

# ============================================================
# 数据可视化分析（EDA）
# ============================================================
print("\n" + "=" * 60)
print("数据可视化分析")
print("=" * 60)

# ---- 图 3.1 取消与未取消预订订单占比情况 ----
fig, ax = plt.subplots(figsize=(8, 8))
cancel_counts = df['is_canceled'].value_counts().sort_index()
pie_labels = ['未取消', '取消']
pie_colors = ['#4472C4', '#ED7D31']
wedges, texts, autotexts = ax.pie(
    cancel_counts.values,
    labels=pie_labels,
    colors=pie_colors,
    autopct='%1.0f%%',
    startangle=90,
    counterclock=False,
    textprops={'fontsize': 14},
    pctdistance=1.18,
    labeldistance=1.32,
    wedgeprops={'edgecolor': 'white', 'linewidth': 1.2}
)
for t in autotexts:
    t.set_fontsize(13)
    t.set_bbox(dict(boxstyle='round,pad=0.25', facecolor='white', edgecolor='lightgray'))
ax.legend(wedges, pie_labels, loc='lower center', bbox_to_anchor=(0.5, -0.05),
          ncol=2, fontsize=12, frameon=False)
fig.text(0.5, 0.02,
         '图 3.1  取消与未取消预订订单占比情况\nFig. 3.1 The proportion of cancelled and uncancelled bookings',
         ha='center', fontsize=12)
plt.tight_layout(rect=[0, 0.06, 1, 1])
plt.savefig('fig_3_1_cancellation_pie.png', dpi=300, bbox_inches='tight')
plt.show()
print(f"未取消: {cancel_counts.get(0, 0)} ({cancel_counts.get(0, 0)/len(df)*100:.2f}%)")
print(f"取消:   {cancel_counts.get(1, 0)} ({cancel_counts.get(1, 0)/len(df)*100:.2f}%)")
print("已保存: fig_3_1_cancellation_pie.png")

# ---- 图 3.2 特征变量分布图 ----
# 绘制全部特征变量的分布
all_feats = df.columns.tolist()
n_feat = len(all_feats)
n_cols = 5
n_rows = (n_feat + n_cols - 1) // n_cols
fig, axes = plt.subplots(n_rows, n_cols, figsize=(20, 3.0 * n_rows))
axes = axes.flatten()
for i, col in enumerate(all_feats):
    axes[i].hist(df[col].dropna(), bins=20, color='#3B7DD8', edgecolor='white')
    axes[i].set_title(col, fontsize=10)
    axes[i].tick_params(labelsize=8)
for j in range(n_feat, len(axes)):
    axes[j].axis('off')
plt.tight_layout()
plt.subplots_adjust(bottom=0.05, hspace=0.6, wspace=0.35)
fig.text(0.5, 0.015,
         '图 3.2  特征变量分布图\nFig. 3.2 Distribution of characteristic variables',
         ha='center', fontsize=14)
plt.savefig('fig_3_2_feature_distribution.png', dpi=300, bbox_inches='tight')
plt.show()
print(f"已保存: fig_3_2_feature_distribution.png（共 {n_feat} 个变量）")

# ---- 图 3.6 不同入住人数的预订取消率 ----
# 入住人数 = 大人 + 小孩 + 婴儿
guest_cols = ['adults', 'children', 'babies']
if all(c in df.columns for c in guest_cols):
    df_eda = df.copy()
    df_eda['total_guests'] = (
        df_eda['adults'].astype(int)
        + df_eda['children'].astype(int)
        + df_eda['babies'].astype(int)
    )

    # 只保留样本量充足的人数类别，避免小样本造成的极端百分比
    counts = df_eda['total_guests'].value_counts().sort_index()
    valid_idx = counts[counts >= 30].index.tolist()
    df_eda = df_eda[df_eda['total_guests'].isin(valid_idx)]

    ct = pd.crosstab(df_eda['total_guests'], df_eda['is_canceled'], normalize='index') * 100
    ct = ct.rename(columns={0: '不取消', 1: '取消'})
    if '不取消' not in ct.columns:
        ct['不取消'] = 0.0
    if '取消' not in ct.columns:
        ct['取消'] = 0.0
    ct = ct.sort_index()

    fig, ax = plt.subplots(figsize=(11, 6))
    x = np.arange(len(ct.index))
    width = 0.35
    bars1 = ax.bar(x - width/2, ct['不取消'], width, label='不取消', color='#4472C4')
    bars2 = ax.bar(x + width/2, ct['取消'], width, label='取消', color='#ED7D31')
    for b, v in zip(bars1, ct['不取消']):
        ax.text(b.get_x() + b.get_width()/2, b.get_height() + 1.5,
                f'{v:.2f}%', ha='center', fontsize=10)
    for b, v in zip(bars2, ct['取消']):
        ax.text(b.get_x() + b.get_width()/2, b.get_height() + 1.5,
                f'{v:.2f}%', ha='center', fontsize=10)
    ax.set_xlabel('total_guests (入住人数 = adults + children + babies)',
                  fontsize=12, labelpad=8)
    ax.set_xticks(x)
    ax.set_xticklabels(ct.index)
    ax.set_ylim(0, 130)
    ax.yaxis.set_major_formatter(plt.matplotlib.ticker.PercentFormatter(xmax=100, decimals=2))
    ax.legend(loc='upper right', fontsize=11, frameon=False)
    plt.tight_layout()
    plt.subplots_adjust(bottom=0.28)
    fig.text(0.5, 0.02,
             '图 3.6  不同入住人数的预订取消率\nFig. 3.6 Booking cancellation rate for different number of guests',
             ha='center', fontsize=12)
    plt.savefig('fig_3_6_cancellation_by_guests.png', dpi=300, bbox_inches='tight')
    plt.show()
    print("已保存: fig_3_6_cancellation_by_guests.png")
    print("\n各入住人数下的取消率（仅列出样本量≥ 30 的类别）:")
    print(ct.round(2).to_string())
    print("\n各入住人数样本量:")
    print(counts.to_string())
else:
    print("adults / children / babies 列不完整，跳过 图 3.6")

# 第四步：数据划分
print("\n" + "=" * 60)
print("第四步：数据划分")
print("=" * 60)

X = df.drop('is_canceled', axis=1)
y = df['is_canceled']

print(f"目标变量分布:\n{y.value_counts()}")
print(f"取消率: {y.mean()*100:.2f}%")

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)
print(f"\n训练集大小: {X_train.shape}")
print(f"测试集大小: {X_test.shape}")

# 数值标准化，让不同列的数据范围统一
scaler = StandardScaler()
X_train_scaled = X_train.copy()
X_test_scaled = X_test.copy()
X_train_scaled[num_cols] = scaler.fit_transform(X_train[num_cols])
X_test_scaled[num_cols] = scaler.transform(X_test[num_cols])

# 开始训练三个模型
print("\n" + "=" * 60)
print("模型构建与训练")
print("=" * 60)

# 逻辑回归
print("\n--- 逻辑回归 ---")
lr = LogisticRegression(max_iter=1000, random_state=42)
lr.fit(X_train_scaled, y_train)
y_pred_lr = lr.predict(X_test_scaled)
y_prob_lr = lr.predict_proba(X_test_scaled)[:, 1]
print("训练完成")

# 决策树
print("\n--- 决策树 ---")
dt = DecisionTreeClassifier(max_depth=10, random_state=42)
dt.fit(X_train, y_train)
y_pred_dt = dt.predict(X_test)
y_prob_dt = dt.predict_proba(X_test)[:, 1]
print("训练完成")

# XGBoost
print("\n--- XGBoost ---")
xgb = XGBClassifier(
    n_estimators=200,
    max_depth=6,
    learning_rate=0.1,
    random_state=42,
    use_label_encoder=False,
    eval_metric='logloss'
)
xgb.fit(X_train, y_train)
y_pred_xgb = xgb.predict(X_test)
y_prob_xgb = xgb.predict_proba(X_test)[:, 1]
print("训练完成")

# 看看三个模型表现怎么样
print("\n" + "=" * 60)
print("性能评估与对比分析")
print("=" * 60)

models = {
    '逻辑回归': (y_pred_lr, y_prob_lr),
    '决策树': (y_pred_dt, y_prob_dt),
    'XGBoost': (y_pred_xgb, y_prob_xgb)
}

results = []
for name, (y_pred, y_prob) in models.items():
    acc = accuracy_score(y_test, y_pred)
    prec = precision_score(y_test, y_pred)
    rec = recall_score(y_test, y_pred)
    f1 = f1_score(y_test, y_pred)
    fpr, tpr, _ = roc_curve(y_test, y_prob)
    roc_auc = auc(fpr, tpr)
    results.append({
        '模型': name,
        '准确率': round(acc, 4),
        '精确率': round(prec, 4),
        '召回率': round(rec, 4),
        'F1分数': round(f1, 4),
        'AUC': round(roc_auc, 4)
    })
    print(f"\n{'='*40}")
    print(f"模型: {name}")
    print(f"{'='*40}")
    print(f"准确率 (Accuracy):  {acc:.4f}")
    print(f"精确率 (Precision): {prec:.4f}")
    print(f"召回率 (Recall):    {rec:.4f}")
    print(f"F1分数 (F1-Score):  {f1:.4f}")
    print(f"AUC:                {roc_auc:.4f}")
    print(f"\n分类报告:\n{classification_report(y_test, y_pred, target_names=['未取消', '已取消'])}")

# 把结果放到一张表里对比
results_df = pd.DataFrame(results)
print("\n" + "=" * 60)
print("模型性能对比汇总")
print("=" * 60)
print(results_df.to_string(index=False))

# 画图对比

# 柱状图：对比各项指标
fig, axes = plt.subplots(1, 4, figsize=(18, 5))
metrics = ['准确率', '精确率', '召回率', 'F1分数']
colors = ['#2196F3', '#4CAF50', '#FF9800']

for i, metric in enumerate(metrics):
    bars = axes[i].bar(results_df['模型'], results_df[metric], color=colors)
    axes[i].set_title(metric, fontsize=14, fontweight='bold')
    axes[i].set_ylim(0, 1.05)
    for bar, val in zip(bars, results_df[metric]):
        axes[i].text(bar.get_x() + bar.get_width()/2., bar.get_height() + 0.01,
                     f'{val:.4f}', ha='center', va='bottom', fontsize=10)
    axes[i].tick_params(axis='x', rotation=0)

plt.suptitle('三个模型性能指标对比', fontsize=16, fontweight='bold', y=1.02)
plt.tight_layout()
plt.savefig('metrics_comparison.png', dpi=300, bbox_inches='tight')
plt.show()
print("已保存: metrics_comparison.png")

# ROC曲线
fig, ax = plt.subplots(figsize=(8, 6))
colors_roc = ['#2196F3', '#4CAF50', '#FF9800']
line_styles = ['-', '--', '-.']

for idx, (name, (y_pred, y_prob)) in enumerate(models.items()):
    fpr, tpr, _ = roc_curve(y_test, y_prob)
    roc_auc = auc(fpr, tpr)
    ax.plot(fpr, tpr, color=colors_roc[idx], linestyle=line_styles[idx],
            linewidth=2, label=f'{name} (AUC = {roc_auc:.4f})')

ax.plot([0, 1], [0, 1], 'k--', linewidth=1, label='随机分类器')
ax.set_xlim([0.0, 1.0])
ax.set_ylim([0.0, 1.05])
ax.set_xlabel('假正率 (False Positive Rate)', fontsize=12)
ax.set_ylabel('真正率 (True Positive Rate)', fontsize=12)
ax.set_title('ROC 曲线对比', fontsize=14, fontweight='bold')
ax.legend(loc='lower right', fontsize=11)
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('roc_curves.png', dpi=300, bbox_inches='tight')
plt.show()
print("已保存: roc_curves.png")

# 混淆矩阵
fig, axes = plt.subplots(1, 3, figsize=(18, 5))
model_names = ['逻辑回归', '决策树', 'XGBoost']
preds = [y_pred_lr, y_pred_dt, y_pred_xgb]

for i, (name, y_pred) in enumerate(zip(model_names, preds)):
    cm = confusion_matrix(y_test, y_pred)
    sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', ax=axes[i],
                xticklabels=['未取消', '已取消'], yticklabels=['未取消', '已取消'])
    axes[i].set_title(f'{name} 混淆矩阵', fontsize=13, fontweight='bold')
    axes[i].set_xlabel('预测值', fontsize=11)
    axes[i].set_ylabel('真实值', fontsize=11)

plt.tight_layout()
plt.savefig('confusion_matrices.png', dpi=300, bbox_inches='tight')
plt.show()
print("已保存: confusion_matrices.png")

# XGBoost 特征重要性
print("\n" + "=" * 60)
print("XGBoost 特征重要性分析")
print("=" * 60)

feature_importance = xgb.feature_importances_
feature_names = X.columns
importance_df = pd.DataFrame({
    '特征': feature_names,
    '重要性': feature_importance
}).sort_values('重要性', ascending=False)

print("\nTop 15 重要特征:")
print(importance_df.head(15).to_string(index=False))

fig, ax = plt.subplots(figsize=(10, 8))
top_n = 15
top_features = importance_df.head(top_n)
bars = ax.barh(range(top_n), top_features['重要性'].values, color='#FF9800', edgecolor='#E65100')
ax.set_yticks(range(top_n))
ax.set_yticklabels(top_features['特征'].values)
ax.invert_yaxis()
ax.set_xlabel('特征重要性 (Feature Importance)', fontsize=12)
ax.set_title('XGBoost Top 15 特征重要性', fontsize=14, fontweight='bold')
for i, v in enumerate(top_features['重要性'].values):
    ax.text(v + 0.002, i, f'{v:.4f}', va='center', fontsize=10)
ax.grid(True, axis='x', alpha=0.3)
plt.tight_layout()
plt.savefig('xgboost_feature_importance.png', dpi=300, bbox_inches='tight')
plt.show()
print("已保存: xgboost_feature_importance.png")

# 模型训练参数汇总
print("\n" + "=" * 60)
print("模型训练参数汇总")
print("=" * 60)

print("\n--- 逻辑回归 (LogisticRegression) ---")
lr_params = lr.get_params()
for k, v in lr_params.items():
    print(f"  {k}: {v}")

print("\n--- 决策树 (DecisionTreeClassifier) ---")
dt_params = dt.get_params()
for k, v in dt_params.items():
    print(f"  {k}: {v}")

print("\n--- XGBoost (XGBClassifier) ---")
xgb_params = xgb.get_params()
for k, v in xgb_params.items():
    print(f"  {k}: {v}")
