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.
# 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())
Six steps from raw CSV
to model verdict.
The complete pipeline for the Mall Customer Segmentation machine learning project. Click any step to jump straight to its code and beginner-friendly explanation.
Data Loading with KaggleHub
Stream Mall_Customers.csv into a pandas DataFrame — 200 rows × 5 columns: CustomerID, Gender, Age, Annual Income (k$), and Spending Score (1-100). No manual download.
Exploratory Data Analysis
Histograms + KDE for Age, Income, and Spending Score. Gender comparison bar chart. Scatter plots of Income vs Spending and Age vs Spending. Correlation heatmap of numeric features.
Preprocessing & Encoding
Rename columns for easier access, label encode Gender (Female=0, Male=1), create high vs. low spender classification target from Spending Score median, scale features without data leakage.
Machine Learning Models
Train Linear/Logistic Regression, Random Forest, Gradient Boosting, SVM and KNN — to predict Spending Score (regression) and classify high vs. low spender (classification).
Deep Learning Models
Two 4-layer TensorFlow networks (128→64→32→1) with ReLU, BatchNorm, Dropout, EarlyStopping and ReduceLROnPlateau — one regression, one classification.
ML vs DL Comparison
Side-by-side leaderboard: R², RMSE, MAE for regression; accuracy, precision, recall, F1 for classification. The verdict on tree ensembles vs neural networks on small customer data.
Load the dataset with KaggleHub.
Stream the Mall Customer Segmentation dataset straight into a pandas DataFrame — no manual download needed.
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 valuesdf.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".
# 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())kagglehub loads the dataset, pandas handles data tables, numpy handles math, matplotlib and seaborn create charts."vjchoudhary7/customer-segmentation-tutorial-in-python", (3) load the file we specified. Result stored in df.(200, 5) — 200 customers, 5 columns.Gender is object (text), the rest are int64 (whole numbers). Confirms 0 missing values — this is a clean dataset!Exploratory Data Analysis in seaborn.
Before any model touches the data, we visualize distributions, compare genders, plot income vs. spending, and check correlations.
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
# 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())"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
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()2.3 · Gender comparison & scatter plots
# 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
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()Encode, engineer, scale — without leakage.
Label encode Gender, create a high vs. low spender target, split into train/test, and scale features.
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.
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)}")Female=0, Male=1. We create a new Gender_enc column to keep the original text column intact.random_state=42 = reproducible.fit_transform on train (learns mean/std), transform on test (applies same parameters). No data leakage — the scaler never sees test data during fitting.Five scikit-learn models,
two tasks.
For each task — predict Spending Score (regression) and classify high vs. low spender (classification) — we train five models.
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
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))4.2 · Classification — Predict High vs. Low Spender
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()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.
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=Truerolls 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
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
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}")The final leaderboard.
Concatenate the ML results with the DL metrics and sort.
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.
# 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))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 overfitsIncome 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.
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.
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.
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.
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).
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.
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.
