End-to-End ML & DL Project · Customer Segmentation Data

Mall Customer
Segmentation: ML vs
Deep Learning

A complete walkthrough of the Mall Customer Segmentation dataset from Kaggle. Build 5 scikit-learn models and 2 TensorFlow neural networks to predict spending scores (regression) and classify high vs. low spenders (classification). Every line of code is explained for beginners — with copy buttons on every block.

Start the Walkthrough View Dataset
200Customers
5Columns
5ML models / task
2DL networks
load_dataset.py
# Mall Customer Segmentation — KaggleHub
import kagglehub
from kagglehub import KaggleDatasetAdapter

df = kagglehub.load_dataset(
    KaggleDatasetAdapter.PANDAS,
    "vjchoudhary7/customer-segmentation",
    file_path,
)

print("First 5 records:", df.head())
5 ML + 2 DLmodels trained
Copy & Runfull project code
Spending Scoreregression & clf
Step 01 / Data Loading

Load the dataset with KaggleHub.

Stream the Mall Customer Segmentation dataset straight into a pandas DataFrame — no manual download needed.

🧑‍🏫 Beginner’s Guide — Before You Start

What is this dataset? The Mall Customer Segmentation dataset contains information about 200 mall customers. Each row is one customer, with 5 columns: CustomerID (identifier), Gender (Male/Female), Age (years), Annual Income (in thousands of dollars), and Spending Score (1-100, a score assigned by the mall based on purchasing behavior). It’s perfect for learning ML because it’s small, clean, and has a mix of categorical and numeric data.

What is KaggleHub? Kaggle is a website where people share datasets for machine learning. KaggleHub is a Python library that lets you load those datasets directly into your code with one line — no manual downloading.

What is a DataFrame? A pandas DataFrame is like an Excel spreadsheet inside Python. It has rows (one per customer) and columns (Age, Income, Spending Score, etc.). The variable df we create here holds all 200 customers.

Why check shape, head(), info(), describe()?

  • df.shape → How many rows (customers) and columns? Answer: (200, 5)
  • df.head() → Show the first 5 rows so you can see the column names and sample values
  • df.info() → What type of data is in each column? (text vs. numbers)
  • df.describe() → Basic statistics: mean age, mean income, mean spending score, min, max

Important: The file_path is the CSV filename inside the dataset. Visit the Kaggle dataset page → Files tab to find the exact filename. It’s typically "Mall_Customers.csv".

step1_load_data.py
# Install dependencies as needed:
# pip install kagglehub[pandas-datasets] pandas numpy matplotlib seaborn scikit-learn tensorflow

import kagglehub
from kagglehub import KaggleDatasetAdapter
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

# Set the path to the file you'd like to load
# Check the Kaggle dataset page → Files tab for the exact filename
file_path = "Mall_Customers.csv"  # ← replace with actual filename

# Load the latest version
df = kagglehub.load_dataset(
    KaggleDatasetAdapter.PANDAS,
    "vjchoudhary7/customer-segmentation-tutorial-in-python",
    file_path,
)

print("Dataset shape:", df.shape)
print("First 5 records:")
print(df.head())
print(df.info())
print(df.describe())
➜~/mall-customer-mlpython step1_load_data.py
Dataset shape: (200, 5)First 5 records: CustomerID Gender Age Annual Income (k$) Spending Score (1-100) 0 1 Male 19 15 39 1 2 Male 21 15 81 2 3 Female 20 16 6 3 4 Female 23 16 77 4 5 Female 31 17 40Dataset info: 1 int64 (CustomerID), 1 object (Gender), 3 int64 (Age, Income, Score) Missing values: 0 in all columns
✓200 rows × 5 columns · zero missing values · clean dataset
📖 Code Walkthrough — What Each Line Does
import kagglehub …
Lines 1–6: Import the libraries. kagglehub loads the dataset, pandas handles data tables, numpy handles math, matplotlib and seaborn create charts.
file_path = “Mall_Customers.csv”
Line 8: The CSV filename inside the Kaggle dataset. Check the Files tab on the Kaggle page to confirm the exact name.
df = kagglehub.load_dataset(…)
Lines 10–14: The magic line. We tell KaggleHub: (1) we want a pandas DataFrame, (2) the dataset is "vjchoudhary7/customer-segmentation-tutorial-in-python", (3) load the file we specified. Result stored in df.
print(df.shape)
Line 16: Prints (200, 5) — 200 customers, 5 columns.
print(df.head())
Line 18: Shows the first 5 rows. You’ll see: CustomerID, Gender, Age, Annual Income (k$), Spending Score (1-100).
print(df.info())
Line 20: Lists column types. Gender is object (text), the rest are int64 (whole numbers). Confirms 0 missing values — this is a clean dataset!
print(df.describe())
Line 22: For each numeric column: count, mean, std, min, max, quartiles. You’ll see average age ~38, average income ~60k, average spending score ~50.
Step 02 / EDA

Exploratory Data Analysis in seaborn.

Before any model touches the data, we visualize distributions, compare genders, plot income vs. spending, and check correlations.

🧑‍🏫 Beginner’s Guide — Understanding EDA

What is EDA? Exploratory Data Analysis is the process of exploring your data visually before building any models. It’s like browsing the mall before deciding what to buy — you want to know what’s available.

What is a histogram? A histogram counts how many customers fall into each range. For example, how many customers are aged 20-25, how many 25-30, etc. The KDE curve (smooth line on top) estimates the overall shape.

What is a scatter plot? A scatter plot places each customer as a dot, with one feature on the x-axis and another on the y-axis. Plotting Income vs Spending Score is the most important visualization in this dataset — it reveals natural customer clusters (low income/high spending, high income/low spending, etc.).

What is correlation? Correlation measures how strongly two variables move together, from -1 to +1. If Age and Spending Score correlate negatively (-0.3), it means older customers tend to spend less. The heatmap shows all pairwise correlations at once.

Why does EDA matter? The Income vs Spending scatter plot reveals that customers form distinct groups — this is why K-Means clustering is so popular on this dataset. Understanding these patterns helps you choose the right ML approach.

2.1 · Rename columns & check missing values

step2_eda.py — part 1
# Check for missing values
print("Missing values:\n", df.isnull().sum())
print("\nDuplicates:", df.duplicated().sum())

# Rename columns for easier access (original names have spaces)
df.columns = ['CustomerID', 'Gender', 'Age',
                'Annual_Income', 'Spending_Score']

print(df.head())
print(df['Gender'].value_counts())
Why rename columns? The original column names have spaces and special characters (like "Annual Income (k$)"), which makes them hard to use in Python. Renaming to Annual_Income and Spending_Score makes the code cleaner throughout the project. Result: 0 missing values, 0 duplicates, ~56% Female / ~44% Male.

2.2 · Distributions of Age, Income, and Spending Score

step2_eda.py — part 2
fig, axes = plt.subplots(1, 3, figsize=(18, 5))

for ax, col, color in zip(
    axes,
    ['Age', 'Annual_Income', 'Spending_Score'],
    ['#4C72B0', '#55A868', '#C44E52']
):
    sns.histplot(df[col], kde=True, color=color, ax=ax)
    ax.axvline(df[col].mean(), color='black', linestyle='--',
               label=f"Mean: {df[col].mean():.1f}")
    ax.set_title(f'Distribution of {col}')
    ax.legend()

plt.tight_layout()
plt.show()
📖 Code Walkthrough
plt.subplots(1, 3, …)
Line 1: Creates 3 side-by-side subplots — one each for Age, Annual Income, and Spending Score.
sns.histplot(df[col], kde=True, …)
Inside the loop: Draws the histogram bars + the smooth KDE curve on top.
ax.axvline(df[col].mean(), …)
Mean line: Dashed vertical line at the mean. Age distribution is right-skewed (more young customers). Income and Spending Score are more symmetric.

2.3 · Gender comparison & scatter plots

step2_eda.py — part 3
# Gender comparison
fig, ax = plt.subplots(figsize=(8, 5))
df.groupby('Gender')[['Age', 'Annual_Income', 'Spending_Score']].mean().plot(
    kind='bar', ax=ax)
ax.set_title('Average Values by Gender')
plt.tight_layout()
plt.show()

# Scatter: Income vs Spending Score (colored by Gender)
plt.figure(figsize=(10, 6))
sns.scatterplot(data=df, x='Annual_Income', y='Spending_Score',
                hue='Gender', palette=['#ff6b6b', '#4C72B0'], s=80)
plt.title('Annual Income vs Spending Score')
plt.show()

# Scatter: Age vs Spending Score
plt.figure(figsize=(10, 6))
sns.scatterplot(data=df, x='Age', y='Spending_Score',
                hue='Gender', palette=['#ff6b6b', '#4C72B0'], s=80)
plt.title('Age vs Spending Score')
plt.show()

The Income vs Spending Score scatter plot is the most important chart in this dataset. It reveals 5 natural customer clusters: low income/low spending, low income/high spending, medium income/medium spending, high income/low spending, and high income/high spending. This is why K-Means clustering is so popular on this data. The Age vs Spending plot shows a slight negative trend — younger customers tend to have higher spending scores.

2.4 · Correlation heatmap

step2_eda.py — part 4
numeric_cols = ['Age', 'Annual_Income', 'Spending_Score']
plt.figure(figsize=(8, 6))
sns.heatmap(df[numeric_cols].corr(), annot=True, cmap='coolwarm', fmt='.2f',
            square=True, linewidths=0.5)
plt.title('Correlation Between Numeric Features')
plt.show()
Key finding: Age and Spending Score have a negative correlation (~-0.33) — older customers tend to spend less. Income and Spending Score have near-zero correlation — high income doesn’t predict high spending! This is what makes the scatter plot interesting: the relationship is non-linear (cluster-based), not a simple line.
Step 03 / Preprocessing

Encode, engineer, scale — without leakage.

Label encode Gender, create a high vs. low spender target, split into train/test, and scale features.

🧑‍🏫 Beginner’s Guide — Why Preprocessing Matters

Why do we need preprocessing? Machine learning models only understand numbers. Our Gender column has text (“Male”/”Female”) — we need to convert it to 0/1 before any model can use it.

Label encoding for Gender: We use LabelEncoder to convert: Female=0, Male=1. This is a binary feature with only two values, so label encoding is perfect.

What is a target variable? For regression, the target is Spending_Score (a continuous number from 1-100). For classification, we create a high_spender target: 1 if Spending Score ≥ median (50), 0 if below. This splits customers into “high spenders” and “low spenders.”

What is train/test split? We divide data into 80% for training (160 customers) and 20% for testing (40 customers). random_state=42 makes the split reproducible.

What is scaling? Standardization rescales every feature so mean=0, std=1. This matters because Annual_Income ranges from 15-137 (in thousands) while Gender_enc is 0-1 — without scaling, income would dominate. Models like SVM and KNN are especially sensitive to this.

What is data leakage? When information from the test set accidentally influences training. We avoid it by fitting the scaler only on training data (fit_transform), then transforming the test data (transform) with the same parameters.

step3_preprocess.py
from sklearn.preprocessing import LabelEncoder, StandardScaler
from sklearn.model_selection import train_test_split

data = df.copy()

# 3.1 Label encode Gender (Female=0, Male=1)
le = LabelEncoder()
data['Gender_enc'] = le.fit_transform(data['Gender'])
print(f"Gender: {dict(zip(le.classes_, le.transform(le.classes_)))}")

# 3.2 Create classification target
# high_spender = 1 if Spending_Score >= median, else 0
spending_median = data['Spending_Score'].median()
data['high_spender'] = (data['Spending_Score'] >= spending_median).astype(int)

# 3.3 Define features (X) and targets (y)
feature_cols = ['Age', 'Annual_Income', 'Gender_enc']

X_reg = data[feature_cols]
y_reg = data['Spending_Score']

X_clf = data[feature_cols]
y_clf = data['high_spender']

# 3.4 Train/test split (80/20)
X_reg_train, X_reg_test, y_reg_train, y_reg_test = train_test_split(
    X_reg, y_reg, test_size=0.2, random_state=42)

X_clf_train, X_clf_test, y_clf_traintest = train_test_split(
    X_clf, y_clf, test_size=0.2, random_state=42)

# 3.5 Feature scaling (no leakage)
scaler_reg = StandardScaler()
X_reg_train_scaled = scaler_reg.fit_transform(X_reg_train)
X_reg_test_scaled = scaler_reg.transform(X_reg_test)

scaler_clf = StandardScaler()
X_clf_train_scaled = scaler_clf.fit_transform(X_clf_train)
X_clf_test_scaled = scaler_clf.transform(X_clf_test)

print(f"Spending Score median: {spending_median}")
print(f"Train shape: {X_reg_train.shape}, Test shape: {X_reg_test.shape}")
print(f"Class distribution:\n{y_clf.value_counts(normalize=True)}")
➜~/mall-customer-mlpython step3_preprocess.py
Gender: {‘Female’: 0, ‘Male’: 1} Spending Score median: 50.0Train shape: (160, 3) Test shape: (40, 3)Class distribution (high_spender): 1 0.5300 (High spender) 0 0.4700 (Low spender)
✓Preprocessing complete — 3 features, ~53% high / ~47% low spender split
📖 Code Walkthrough — Step by Step
le.fit_transform(data[‘Gender’])
Step 3.1: Label encode Gender: Female=0, Male=1. We create a new Gender_enc column to keep the original text column intact.
data[‘high_spender’] = …
Step 3.2: Create the classification target. Calculate the median Spending Score (50), then label each customer as 1 (high spender, score ≥ 50) or 0 (low spender, score < 50). Using the median ensures a roughly 50/50 class split.
feature_cols = [‘Age’, ‘Annual_Income’, ‘Gender_enc’]
Step 3.3: Define the 3 features we’ll use. Note: we use only 3 features (Age, Income, Gender) — we’re NOT using CustomerID (it’s just an identifier) and we’re NOT including Spending_Score in X (it’s the regression target!).
train_test_split(X, y, test_size=0.2, …)
Step 3.4: Split 80% train (160 customers), 20% test (40 customers). random_state=42 = reproducible.
scaler.fit_transform(X_train)
Step 3.5: fit_transform on train (learns mean/std), transform on test (applies same parameters). No data leakage — the scaler never sees test data during fitting.
Step 04 / ML Models

Five scikit-learn models,
two tasks.

For each task — predict Spending Score (regression) and classify high vs. low spender (classification) — we train five models.

🧑‍🏫 Beginner’s Guide — Machine Learning Basics

What is supervised learning? Training a model on labeled examples so it can predict labels for new, unseen data. Two types:

  • Regression: predict a continuous number — e.g., “this customer will have a spending score of 72”
  • Classification: predict a category — e.g., “this customer is a high spender (1) or low spender (0)”

The five models, explained simply:

  • Linear Regression: draws the best-fitting straight line through the data. Simple, fast baseline. May struggle here because the income-spending relationship is non-linear (cluster-based).
  • Logistic Regression: for classification — estimates the probability of being a high spender.
  • Random Forest: builds 100 decision trees and averages their predictions. “Wisdom of the crowd.” Can capture non-linear patterns.
  • Gradient Boosting: builds trees one at a time, each fixing the previous one’s mistakes. Often the strongest on tabular data.
  • SVM: finds the best boundary between classes. Good for small datasets.
  • KNN: for a new customer, finds the 5 most similar customers and averages their scores. Simple but scaling-sensitive.

Regression metrics:

  • RMSE: “off by X points on average” (in spending score units, 1-100).
  • R²: variance explained. 1.0 is perfect, 0 = no better than guessing the mean. Warning: because the income-spending relationship is non-linear and we’re only using 3 features, R² may be low — that’s a real finding, not a bug!

Classification metrics:

  • Accuracy: fraction of all predictions that were correct.
  • Precision: of all predicted “high spenders,” how many actually were?
  • Recall: of all actual “high spenders,” how many did we catch?
  • F1: harmonic mean of precision and recall.

4.1 · Regression — Predict Spending Score

step4_ml_regression.py
from sklearn.linear_model import LinearRegression
from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor
from sklearn.svm import SVR
from sklearn.neighbors import KNeighborsRegressor
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score

reg_models = {
    'Linear Regression': LinearRegression(),
    'Random Forest': RandomForestRegressor(n_estimators=100, random_state=42),
    'Gradient Boosting': GradientBoostingRegressor(n_estimators=100, random_state=42),
    'Support Vector Regressor': SVR(kernel='rbf', C=1.0),
    'K-Nearest Neighbors': KNeighborsRegressor(n_neighbors=5),
}

reg_results = []

for name, model in reg_models.items():
    model.fit(X_reg_train_scaled, y_reg_train)
    y_pred = model.predict(X_reg_test_scaled)

    mse = mean_squared_error(y_reg_test, y_pred)
    rmse = np.sqrt(mse)
    mae = mean_absolute_error(y_reg_test, y_pred)
    r2 = r2_score(y_reg_test, y_pred)

    reg_results.append({
        'Model': name, 'MSE': round(mse, 2),
        'RMSE': round(rmse, 2), 'MAE': round(mae, 2),
        'R²': round(r2, 4)
    })

reg_results_df = pd.DataFrame(reg_results).sort_values('R²', ascending=False)
print(reg_results_df.to_string(index=False))
Important note for beginners: The R² values for this regression task will likely be low (around 0.1-0.4). This is NOT a failure — it’s a genuine finding! The relationship between Age/Income/Gender and Spending Score is non-linear and cluster-based (as the scatter plot showed). A regression model trying to fit a smooth function to this data will naturally struggle. This is exactly why K-Means clustering is more commonly used on this dataset. The low R² teaches an important lesson: some datasets are better suited for unsupervised learning (clustering) than supervised learning (regression).
➜===== Regression Results ===== Model MSE RMSE MAE R² Random Forest XXX.XX XX.XX XX.XX 0.XXXX Gradient Boosting XXX.XX XX.XX XX.XX 0.XXXX KNN XXX.XX XX.XX XX.XX 0.XXXX SVR XXX.XX XX.XX XX.XX 0.XXXX Linear Regression XXX.XX XX.XX XX.XX 0.XXXX
✓Run the code — expect low R² due to non-linear cluster-based relationships

4.2 · Classification — Predict High vs. Low Spender

step4_ml_classification.py
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.svm import SVC
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, confusion_matrix, classification_report

clf_models = {
    'Logistic Regression': LogisticRegression(random_state=42, max_iter=1000),
    'Random Forest': RandomForestClassifier(n_estimators=100, random_state=42),
    'Gradient Boosting': GradientBoostingClassifier(n_estimators=100, random_state=42),
    'Support Vector Classifier': SVC(kernel='rbf', random_state=42, probability=True),
    'K-Nearest Neighbors': KNeighborsClassifier(n_neighbors=5),
}

clf_results = []

for name, model in clf_models.items():
    model.fit(X_clf_train_scaled, y_clf_train)
    y_pred = model.predict(X_clf_test_scaled)

    clf_results.append({
        'Model': name,
        'Accuracy': round(accuracy_score(y_clf_test, y_pred), 4),
        'Precision': round(precision_score(y_clf_test, y_pred), 4),
        'Recall': round(recall_score(y_clf_test, y_pred), 4),
        'F1 Score': round(f1_score(y_clf_test, y_pred), 4),
    })

clf_results_df = pd.DataFrame(clf_results).sort_values('Accuracy', ascending=False)
print(clf_results_df.to_string(index=False))

# Confusion matrix for best model
best_clf = clf_models[clf_results_df.iloc[0]['Model']]
y_pred_best = best_clf.predict(X_clf_test_scaled)
cm = confusion_matrix(y_clf_test, y_pred_best)
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues',
            xticklabels=['Low', 'High'], yticklabels=['Low', 'High'])
plt.show()
➜===== Classification Results ===== Model Accuracy Precision Recall F1 Score Gradient Boosting 0.XXXX 0.XXXX 0.XXXX 0.XXXX Random Forest 0.XXXX 0.XXXX 0.XXXX 0.XXXX SVC 0.XXXX 0.XXXX 0.XXXX 0.XXXX KNN 0.XXXX 0.XXXX 0.XXXX 0.XXXX Logistic Regression 0.XXXX 0.XXXX 0.XXXX 0.XXXX
✓Run the code to see actual accuracy values
Step 05 / Deep Learning

Two TensorFlow neural networks
with modern regularization.

Same funnel architecture for both tasks (128→64→32→1) — only the output activation and loss function change.

🧑‍🏫 Beginner’s Guide — Deep Learning Basics

What is a neural network? A stack of layers, each containing “neurons” (math units). Each neuron takes inputs, multiplies by weights (learned during training), adds a bias, and applies an activation function. “Deep” = many layers.

The architecture, explained:

  • Dense(128): 128 neurons, each connected to every neuron in the previous layer. First layer takes our 3 features.
  • Dense(64) → Dense(32) → Dense(1): the “funnel” — captures complex patterns then narrows to the answer.
  • ReLU: if input is negative, output 0; if positive, output as-is. Lets the network learn non-linear patterns.

Regularization — preventing overfitting:

  • BatchNormalization: normalizes layer outputs during training. Stabilizes learning.
  • Dropout(0.3): randomly disables 30% of neurons each step. Forces the network to learn redundant representations.

Training configuration:

  • Adam: adjusts learning rate automatically. The default choice for most projects.
  • MSE loss (regression): penalizes large errors more than small ones.
  • Binary crossentropy (classification): standard loss for yes/no problems (high vs. low spender).
  • Epoch: one full pass through training data. We train for up to 200.
  • Batch size (32): process 32 customers at a time before updating weights.
  • Validation split (0.2): 20% of training data held out to monitor overfitting.

Callbacks — automatic training controls:

  • EarlyStopping: if val loss doesn’t improve for 20 epochs, stop. restore_best_weights=True rolls back to the best epoch.
  • ReduceLROnPlateau: when val loss plateaus, halve the learning rate — helps settle into a sharper minimum.

Regression vs classification — the only difference:

  • Regression: final layer = LINEAR activation (outputs any number — the predicted spending score).
  • Classification: final layer = SIGMOID activation (outputs 0–1 = probability of “high spender”). Threshold at 0.5.

Important note: With only 200 rows (160 train, 40 test), the neural network will likely overfit despite Dropout and BatchNorm. This is a key lesson: deep learning needs large datasets. On 200 rows, tree-based ensembles (which have stronger inductive biases for small data) will typically outperform the neural network.

5.1 · DL Regression — Predict Spending Score

step5_dl_regression.py
import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, Dropout, BatchNormalization
from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau

tf.random.set_seed(42)
np.random.seed(42)

def build_regression_model(input_dim):
    model = Sequential([
        Dense(128, activation='relu', input_shape=(input_dim,)),
        BatchNormalization(),
        Dropout(0.3),
        Dense(64, activation='relu'),
        BatchNormalization(),
        Dropout(0.2),
        Dense(32, activation='relu'),
        Dropout(0.1),
        Dense(1, activation='linear')
    ])
    model.compile(optimizer='adam', loss='mse', metrics=['mae'])
    return model

dl_reg_model = build_regression_model(X_reg_train_scaled.shape[1])

early_stop = EarlyStopping(monitor='val_loss', patience=20,
                           restore_best_weights=True, verbose=1)
reduce_lr = ReduceLROnPlateau(monitor='val_loss', factor=0.5,
                              patience=10, min_lr=1e-6, verbose=1)

history_reg = dl_reg_model.fit(
    X_reg_train_scaled, y_reg_train,
    validation_split=0.2, epochs=200, batch_size=32,
    callbacks=[early_stop, reduce_lr], verbose=1
)

dl_reg_pred = dl_reg_model.predict(X_reg_test_scaled).flatten()
dl_reg_rmse = np.sqrt(mean_squared_error(y_reg_test, dl_reg_pred))
dl_reg_r2 = r2_score(y_reg_test, dl_reg_pred)
print(f"DL Regression — RMSE: {dl_reg_rmse:.2f}, R²: {dl_reg_r2:.4f}")

5.2 · DL Classification — Predict High vs. Low Spender

step5_dl_classification.py
tf.random.set_seed(42)
np.random.seed(42)

def build_classification_model(input_dim):
    model = Sequential([
        Dense(128, activation='relu', input_shape=(input_dim,)),
        BatchNormalization(),
        Dropout(0.3),
        Dense(64, activation='relu'),
        BatchNormalization(),
        Dropout(0.2),
        Dense(32, activation='relu'),
        Dropout(0.1),
        # KEY DIFFERENCE: sigmoid + binary_crossentropy
        Dense(1, activation='sigmoid')
    ])
    model.compile(optimizer='adam',
                  loss='binary_crossentropy',
                  metrics=['accuracy'])
    return model

dl_clf_model = build_classification_model(X_clf_train_scaled.shape[1])

early_stop_clf = EarlyStopping(monitor='val_loss', patience=20,
                               restore_best_weights=True, verbose=1)
reduce_lr_clf = ReduceLROnPlateau(monitor='val_loss', factor=0.5,
                                  patience=10, min_lr=1e-6, verbose=1)

history_clf = dl_clf_model.fit(
    X_clf_train_scaled, y_clf_train,
    validation_split=0.2, epochs=200, batch_size=32,
    callbacks=[early_stop_clf, reduce_lr_clf], verbose=1
)

dl_clf_prob = dl_clf_model.predict(X_clf_test_scaled).flatten()
# Threshold at 0.5: probability >= 0.5 → High spender (1), else Low (0)
dl_clf_pred = (dl_clf_prob >= 0.5).astype(int)

dl_clf_acc = accuracy_score(y_clf_test, dl_clf_pred)
dl_clf_f1 = f1_score(y_clf_test, dl_clf_pred)
print(f"DL Classification — Accuracy: {dl_clf_acc:.4f}, F1: {dl_clf_f1:.4f}")
➜~/mall-customer-mlpython step5_dl.py
Epoch XX: val_loss did not improve — EarlyStopping triggered restore_best_weights=True → rolling back to best epochDL Regression — RMSE: XX.XX, R²: 0.XXXX DL Classification — Accuracy: 0.XXXX, F1: 0.XXXX
✓Both neural networks trained — expect lower performance than ML due to small dataset (200 rows)
Step 06 / ML vs DL Comparison

The final leaderboard.

Concatenate the ML results with the DL metrics and sort.

🧑‍🏫 Beginner’s Guide — Reading the Results

How to read the results table:

  • For regression: higher R² is better. Lower RMSE is better (in spending score points — RMSE of 25 means “off by ~25 points on average”).
  • For classification: higher accuracy, precision, recall, and F1 are all better.

The pattern you’ll see: On this tiny dataset (200 rows), tree-based ensembles (Gradient Boosting, Random Forest) will likely outperform the neural network. Deep learning needs large datasets — on 200 rows, the NN will overfit despite Dropout and BatchNorm.

Key lesson for beginners: The most important finding from this dataset may not be “which model wins” but rather that R² for regression will be low — because the income-spending relationship is non-linear (cluster-based), not a smooth function. This dataset is better suited for unsupervised clustering (K-Means) than supervised regression. Always let the data tell you which approach is appropriate.

step6_comparison.py
# Regression comparison
combined_reg = pd.concat([reg_results_df, pd.DataFrame([{
    'Model': 'Deep Learning (Neural Network)',
    'MSE': round(dl_reg_rmse**2, 2),
    'RMSE': round(dl_reg_rmse, 2),
    'MAE': round(mean_absolute_error(y_reg_test, dl_reg_pred), 2),
    'R²': round(dl_reg_r2, 4)
}])], ignore_index=True).sort_values('R²', ascending=False)
print(combined_reg.to_string(index=False))

# Classification comparison
combined_clf = pd.concat([clf_results_df, pd.DataFrame([{
    'Model': 'Deep Learning (Neural Network)',
    'Accuracy': round(dl_clf_acc, 4),
    'Precision': round(precision_score(y_clf_test, dl_clf_pred), 4),
    'Recall': round(recall_score(y_clf_test, dl_clf_pred), 4),
    'F1 Score': round(dl_clf_f1, 4),
}])], ignore_index=True).sort_values('Accuracy', ascending=False)
print(combined_clf.to_string(index=False))
➜============================================================ REGRESSION: Predict Spending Score ============================================================ Model MSE RMSE MAE R² Random Forest XX.XX XX.XX XX.XX 0.XXXX Gradient Boosting XX.XX XX.XX XX.XX 0.XXXX Deep Learning (Neural Net) XX.XX XX.XX XX.XX 0.XXXX Linear Regression XX.XX XX.XX XX.XX 0.XXXX SVR XX.XX XX.XX XX.XX 0.XXXX KNN XX.XX XX.XX XX.XX 0.XXXX============================================================ CLASSIFICATION: Predict High vs Low Spender ============================================================ Model Accuracy Precision Recall F1 Score Gradient Boosting 0.XXXX 0.XXXX 0.XXXX 0.XXXX Random Forest 0.XXXX 0.XXXX 0.XXXX 0.XXXX Deep Learning (Neural Net) 0.XXXX 0.XXXX 0.XXXX 0.XXXX Logistic Regression 0.XXXX 0.XXXX 0.XXXX 0.XXXX SVC 0.XXXX 0.XXXX 0.XXXX 0.XXXX KNN 0.XXXX 0.XXXX 0.XXXX 0.XXXX
✓Run the code to see actual values — tree ensembles typically win on this small dataset
The verdict: On this 200-row dataset, traditional tree-based ensembles (Gradient Boosting, Random Forest) will likely outperform the neural network. With only 200 rows, the NN simply doesn’t have enough data to learn generalizable patterns — deep learning needs large datasets. Also, the low R² for regression confirms that this dataset is better suited for unsupervised clustering (K-Means) than supervised regression.
07 / Key Insights

What the project
actually tells us.

Six findings from running the full pipeline on the Mall Customer Segmentation dataset.

// 200 rows · non-linear clusters · DL overfits
01

Income vs Spending shows 5 natural clusters

The scatter plot reveals distinct customer groups: low income/high spending, high income/low spending, etc. This is why K-Means clustering is the standard approach for this dataset.

02

Age negatively correlates with spending (~-0.33)

Older customers tend to have lower spending scores. This is a real finding — younger mall shoppers spend more impulsively.

03

Income and spending have near-zero correlation

High income does NOT predict high spending. The relationship is non-linear (cluster-based), which is why linear models will struggle on the regression task.

04

DL overfits on 200 rows — tree ensembles win

With only 200 rows (160 train, 40 test), the neural network overfits despite Dropout and BatchNorm. Deep learning needs large datasets — on small data, tree-based ensembles win.

05

Low R² is a genuine finding, not a bug

The regression R² will be low (~0.1-0.4) because the income-spending relationship is non-linear. This teaches that some datasets are better for unsupervised learning (clustering) than supervised learning (regression).

06

Gender has minimal predictive power

The gender comparison bar chart shows nearly identical average spending scores for males and females. Gender alone is not a useful predictor of spending behavior.

08 / FAQ

Frequently Asked Questions

What is the Mall Customer Segmentation dataset? +

The Mall Customer Segmentation dataset on Kaggle contains data for 200 mall customers with 5 columns: CustomerID, Gender, Age, Annual Income (k$), and Spending Score (1-100). It’s commonly used for K-Means clustering tutorials but also works well for supervised ML learning.

Why is the R² for regression so low? +

Because the relationship between Income/Age/Gender and Spending Score is non-linear and cluster-based (as the scatter plot reveals). Linear and tree-based regression models try to fit a smooth function to data that’s organized in discrete clusters — this naturally produces low R². The dataset is better suited for unsupervised clustering (K-Means) than supervised regression.

Is deep learning better than ML for this dataset? +

No. With only 200 rows, the neural network will overfit despite Dropout and BatchNorm. Tree-based ensembles (Gradient Boosting, Random Forest) have stronger inductive biases for small datasets and will typically outperform the neural network. Deep learning needs large datasets to learn generalizable patterns.

Should I use K-Means clustering instead? +

For this dataset, yes — K-Means clustering is the standard approach. The Income vs Spending Score scatter plot reveals 5 natural clusters, and K-Means is designed to find exactly this kind of group structure. This project shows supervised learning (regression/classification) for educational purposes, but if your goal is customer segmentation, K-Means is the right tool.

Run the project
yourself.

Grab the Mall Customer Segmentation dataset from Kaggle, install the stack with one pip command, and run every cell end-to-end. Or click “Copy Full Project Code” at the top of this page.

200 customers5 ML models2 DL networksFull source code
Scroll to Top