File size: 7,887 Bytes
6c2997c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8956b1e
6c2997c
 
 
 
 
 
 
 
 
8956b1e
 
 
 
 
 
 
 
 
 
 
 
 
 
6c2997c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8956b1e
 
6c2997c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8956b1e
 
6c2997c
8956b1e
 
 
 
 
6c2997c
 
 
 
 
 
 
 
8956b1e
6c2997c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
import os
import pickle
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder, StandardScaler
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import classification_report, accuracy_score, roc_auc_score, confusion_matrix
import xgboost as xgb

def train_delay_model():
    cleaned_csv_path = os.path.join("Data", "cleaned_data.csv")
    models_dir = "Models"
    os.makedirs(models_dir, exist_ok=True)
    
    if not os.path.exists(cleaned_csv_path):
        print(f"Error: Cleaned data not found at {cleaned_csv_path}. Run data_cleaning.py first.")
        return

    print("Loading cleaned dataset...")
    df = pd.read_csv(cleaned_csv_path)
    
    # Define Target Variable
    target_col = "late_delivery_risk"
    if target_col not in df.columns:
        print(f"Error: Target column '{target_col}' not found in dataset.")
        return
        
    print(f"Target distribution:\n{df[target_col].value_counts(normalize=True)}")

    # 1. PREVENT DATA LEAKAGE: Identify and drop post-shipment features
    leakage_cols = [
        "days_for_shipping_real", 
        "delivery_status", 
        "shipping_date", 
        "order_status"
    ]
    
    # Drop columns that are unique identifiers or text fields that can't be easily modeled
    id_text_cols = [
        "customer_fname",
        "customer_lname",
        "customer_street",
        "customer_zipcode",
        "order_zipcode",
        "product_name",
        "category_name",
        "department_name"
    ]
    
    drop_cols = leakage_cols + id_text_cols
    drop_cols = [col for col in drop_cols if col in df.columns]
    
    print(f"Dropping post-shipment (leakage) and text/id columns: {drop_cols}")
    df_features = df.drop(columns=drop_cols)
    
    # 2. FEATURE ENGINEERING
    print("Performing advanced feature engineering...")
    # Convert order_date to datetime to extract temporal features
    df_features["order_date"] = pd.to_datetime(df_features["order_date"])
    df_features["order_year"] = df_features["order_date"].dt.year
    df_features["order_month"] = df_features["order_date"].dt.month
    df_features["order_day"] = df_features["order_date"].dt.day
    df_features["order_hour"] = df_features["order_date"].dt.hour
    df_features["order_dayofweek"] = df_features["order_date"].dt.dayofweek
    df_features["is_weekend"] = df_features["order_dayofweek"].isin([5, 6]).astype(int)
    
    # Cross-feature interactions
    # 1. Check if shipping is domestic or international (cross-border)
    df_features["is_domestic"] = (df_features["customer_country"] == df_features["order_country"]).astype(int)
    # 2. Check if shipping is within same state
    df_features["is_same_state"] = (df_features["customer_state"] == df_features["order_state"]).astype(int)
    # 3. Check if shipping is within same city
    df_features["is_same_city"] = (df_features["customer_city"] == df_features["order_city"]).astype(int)
    
    # 4. Calculate discount amount in currency
    df_features["discount_amount"] = df_features["sales"] * df_features["order_item_discount_rate"]
    
    # 5. Calculate unit price per item
    df_features["price_per_item"] = df_features["sales"] / (df_features["order_item_quantity"] + 1e-5)
    
    # Drop original order_date
    df_features = df_features.drop(columns=["order_date"])
    
    # 3. ENCODE CATEGORICAL VARIABLES
    print("Encoding categorical features...")
    categorical_cols = [
        "type", 
        "customer_segment", 
        "customer_city", 
        "customer_state", 
        "customer_country",
        "market", 
        "order_city", 
        "order_state", 
        "order_country", 
        "order_region",
        "shipping_mode"
    ]
    
    label_encoders = {}
    for col in categorical_cols:
        if col in df_features.columns:
            df_features[col] = df_features[col].fillna("Unknown").astype(str)
            le = LabelEncoder()
            df_features[col] = le.fit_transform(df_features[col])
            label_encoders[col] = le
            
    # Save encoders for future inference
    encoders_path = os.path.join(models_dir, "label_encoders.pkl")
    with open(encoders_path, "wb") as f:
        pickle.dump(label_encoders, f)
    print("Saved label encoders.")

    # 4. TRAIN-TEST SPLIT
    X = df_features.drop(columns=[target_col])
    y = df_features[target_col]
    
    print(f"Number of features: {X.shape[1]}")
    
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42, stratify=y)
    print(f"Train shape: {X_train.shape}, Test shape: {X_test.shape}")
    
    # 5. SCALE NUMERICAL FEATURES
    # Identify numerical columns to scale
    numerical_cols = [
        "benefit_per_order", "sales_per_customer", "latitude", "longitude",
        "order_item_discount", "order_item_discount_rate", "order_item_product_price",
        "order_item_profit_ratio", "order_item_quantity", "sales", "order_item_total",
        "order_profit_per_order", "product_price", "days_for_shipping_scheduled",
        "discount_amount", "price_per_item"
    ]
    numerical_cols = [col for col in numerical_cols if col in X.columns]
    
    scaler = StandardScaler()
    X_train[numerical_cols] = scaler.fit_transform(X_train[numerical_cols])
    X_test[numerical_cols] = scaler.transform(X_test[numerical_cols])
    
    # Save scaler for future inference
    scaler_path = os.path.join(models_dir, "scaler.pkl")
    with open(scaler_path, "wb") as f:
        pickle.dump(scaler, f)
    print("Saved feature scaler.")

    # 6. MODEL TRAINING
    # Model A: Random Forest (Baseline)
    print("\nTraining Random Forest Classifier (Baseline)...")
    rf_model = RandomForestClassifier(n_estimators=100, max_depth=12, random_state=42, n_jobs=-1)
    rf_model.fit(X_train, y_train)
    rf_preds = rf_model.predict(X_test)
    
    print("\nRandom Forest Test Evaluation:")
    print(f"Accuracy: {accuracy_score(y_test, rf_preds):.4f}")
    print(f"ROC-AUC: {roc_auc_score(y_test, rf_model.predict_proba(X_test)[:, 1]):.4f}")
    print("\nClassification Report:")
    print(classification_report(y_test, rf_preds))
    
    # Model B: XGBoost Classifier (Advanced - Tuned)
    print("\nTraining Tuned XGBoost Classifier...")
    xgb_model = xgb.XGBClassifier(
        n_estimators=300,
        max_depth=8,
        learning_rate=0.05,
        subsample=0.8,
        colsample_bytree=0.8,
        random_state=42,
        eval_metric="logloss",
        n_jobs=-1
    )
    xgb_model.fit(X_train, y_train)
    xgb_preds = xgb_model.predict(X_test)
    xgb_probs = xgb_model.predict_proba(X_test)[:, 1]
    
    print("\nTuned XGBoost Test Evaluation:")
    print(f"Accuracy: {accuracy_score(y_test, xgb_preds):.4f}")
    print(f"ROC-AUC: {roc_auc_score(y_test, xgb_probs):.4f}")
    print("\nClassification Report:")
    print(classification_report(y_test, xgb_preds))
    print("\nConfusion Matrix:")
    print(confusion_matrix(y_test, xgb_preds))

    # Feature Importance (XGBoost)
    importances = xgb_model.feature_importances_
    feat_imp = pd.Series(importances, index=X.columns).sort_values(ascending=False)
    print("\nTop 10 Feature Importances (XGBoost):")
    print(feat_imp.head(10))

    # 7. SAVE THE BEST MODEL
    xgb_model_path = os.path.join(models_dir, "best_xgb_model.json")
    xgb_model.save_model(xgb_model_path)
    print(f"\nSaved best XGBoost model to {xgb_model_path}")
    
    # Save a pickle version of the model for easy loading
    xgb_pkl_path = os.path.join(models_dir, "best_xgb_model.pkl")
    with open(xgb_pkl_path, "wb") as f:
        pickle.dump(xgb_model, f)
    print(f"Saved best XGBoost model as pickle to {xgb_pkl_path}")
    print("Model training completed successfully!")

if __name__ == "__main__":
    train_delay_model()