Customer Segmentation with RFM Analysis

Segment retail customers into groups using RFM analysis and K-Means clustering.

Introduction

30 Second Summary

Think about the last time you shopped at your favorite store. Some customers visit every week and spend big, while others bought once months ago and never came back. Every business wants to tell these groups apart so they can send the right message to the right person.

In this project, you will build a customer segmentation analysis using Python and Jupyter Notebook. You'll take raw retail transaction data, compute behavioral scores for each customer, and use machine learning to automatically group them into segments with specific marketing recommendations.

What You'll Build

When you run the final cell of your notebook, you'll see a color-coded scatter plot revealing 4 distinct customer segments, plus a recommendations table telling a marketing team exactly what action to take for each group.

By the end of this project, you'll have:

  • A working analysis that loads a real retail dataset and instantly reveals customer behavior patterns through summary statistics and data exploration.
  • Computed RFM scores that rank every customer by how recently they bought, how often, and how much they spent.
  • A K-Means clustering model that discovers natural customer groups from the data, visualized as clearly separated clusters in a scatter plot with a business recommendation table mapping each segment to a specific marketing action.
  • Secret Mission: Add a radar chart comparing segment profiles and calculate projected revenue impact of targeting each segment.

Are there any prerequisites?

No prior experience with data science or machine learning is needed. This guide walks you through installing all required tools and libraries from scratch.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters.

Set Up Your Environment and Load the Data

Customer segmentation starts with raw transaction data. Before you can discover patterns in how customers behave, you need the right tools installed and the dataset loaded into a format you can work with.

In this step, you will set up a Python project folder, install the data science libraries, download a real retail dataset from Kaggle, and load it into a Jupyter Notebook so you can see exactly what you are working with.

In this step, get ready to:
  • Install the required Python libraries and launch Jupyter Notebook.
  • Download the retail transactional dataset and create a new notebook.
  • Import libraries, load the CSV, and confirm the data is ready.
Create your project folder and install libraries

You need a dedicated folder for your project files and the dataset. You also need several Python libraries that handle data manipulation, machine learning, and visualization.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal and press Enter to open it.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir customer-segmentation
cd customer-segmentation
  • Install the required Python libraries by running:
pip install pandas scikit-learn matplotlib seaborn jupyter

You should see download progress bars and a final line listing the successfully installed packages.

What does this command do?

This installs five libraries your project depends on. pandas handles tabular data. scikit-learn provides machine learning algorithms. matplotlib and seaborn create visualizations. jupyter gives you the interactive notebook environment.

NumPy is also installed automatically as a dependency of pandas and scikit-learn.

🙋‍♀️ Seeing a permissions error?

If pip fails with a permissions error, try running pip install --user pandas scikit-learn matplotlib seaborn jupyter instead. On some systems, installing to the user directory avoids needing administrator access.

help me fix a pip permissions error when installing pandas and scikit-learn

  • Launch Jupyter Notebook by running:
jupyter notebook

What happens when you run this?

This command starts a local Jupyter server and opens a new browser tab showing the Jupyter file browser. Your terminal will display server logs and stay occupied while Jupyter runs. Do not close the terminal window.

Your browser opens to the Jupyter file browser showing the contents of your customer-segmentation folder (currently empty).

Download the dataset and create your notebook

You need a real retail dataset to analyze. The Retail Transactional Dataset on Kaggle contains 29 columns of customer demographics, purchase history, and product information.

  • Open this link in a new browser tab: Retail Transactional Dataset on Kaggle.
  • Sign in to your Kaggle account. If you do not have one, create a free account using the Register button on the page.
  • Click the Download button on the dataset page to download a ZIP file.
  • Extract the ZIP file. You will get a CSV file.
  • Move or copy the CSV file into your customer-segmentation folder on the Desktop. Rename the file to retail_transactional_dataset.csv if it has a different name.

Finding your project folder

Open Finder (macOS) or File Explorer (Windows) and navigate to Desktop > customer-segmentation. Drop the CSV file there.

  • Switch to the Jupyter browser tab.
  • Click New in the top right corner, then select Python 3 to create a new notebook.
  • Click Untitled at the top of the notebook page to rename it.
  • Type customer_segmentation as the new name and click Rename.

You now have customer_segmentation.ipynb open with an empty cell ready for code.

Import libraries and load the data

With the notebook open, your first cell will import all the libraries you need and load the CSV into a pandas DataFrame. This confirms your installation worked and gives you a first look at the raw data.

  • In the first cell of your notebook, paste the following code:
# Cell 1: Import Libraries and Load Data
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score

# Load the dataset
df = pd.read_csv('retail_transactional_dataset.csv')
print(f"Dataset shape: {df.shape}")
df.head()

What does this code do?

  • The import lines bring in pandas for data tables, numpy for math, matplotlib and seaborn for plotting, and three scikit-learn modules for scaling, clustering, and evaluation.
  • pd.read_csv() reads the CSV file into a DataFrame called df.
  • df.shape prints the number of rows and columns so you know how large the dataset is.
  • df.head() displays the first 5 rows so you can inspect the column names and sample values.

Before you run this cell, what do you expect to see printed? Think about what df.shape returns for a dataset described as having 29 columns.

  • Press Shift+Enter to run the cell.

You should see a line reading Dataset shape: followed by a tuple showing the row count and 29 columns. Below that, a formatted table displays the first 5 rows with columns like Transaction ID, Customer ID, Date, Amount, and more.

Seeing a FileNotFoundError?

This means the CSV file is not in the same folder as your notebook. Check that retail_transactional_dataset.csv is inside the customer-segmentation folder on your Desktop. The filename must match exactly, including underscores.

help me fix a FileNotFoundError when loading my CSV in Jupyter

✔️ Awesome, I've got everything!

Great. Make sure you have saved your notebook before moving on.

ⓧ I'd like to double check the full code

Your first cell should contain exactly this code:

# Cell 1: Import Libraries and Load Data
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score

# Load the dataset
df = pd.read_csv('retail_transactional_dataset.csv')
print(f"Dataset shape: {df.shape}")
df.head()

Your data science environment is ready and you have confirmed the dataset loads correctly. Next up, you will explore and clean this data to prepare it for analysis.

Explore and Clean the Data

Your data is loaded and you can see its shape. Before you can analyze customer behavior, you need to understand what the data actually contains and whether it has quality issues.

Raw retail datasets almost always have problems. Missing customer IDs, duplicate transactions, and inconsistent date formats can all skew your segmentation results. In this step, you'll explore the data to find these issues, then clean them up so every row is trustworthy.

In this step, get ready to:
  • Explore data types, missing values, and duplicates in the raw dataset.
  • Clean the DataFrame by removing invalid rows and fixing column types.
  • Engineer a TotalSpend feature and confirm the dataset is analysis-ready.
Explore data quality with info, nulls, and duplicates

The first thing any data scientist does with a new dataset is audit its quality. You need to know which columns have missing values, what data types each column holds, and whether there are duplicate rows that could inflate your counts.

  • In your notebook, create a new cell below Cell 1 and add the following data exploration code:
# Cell 2: Data Exploration
print("=== Data Info ===")
print(df.info())
print("\n=== Missing Values ===")
print(df.isnull().sum())
print(f"\n=== Duplicates: {df.duplicated().sum()} ===")
print("\n=== Basic Statistics ===")
df.describe()

What does this code do?

  • df.info() prints a summary of every column: its name, how many non-null values it has, and its data type. This reveals missing data at a glance.
  • df.isnull().sum() counts the exact number of missing values per column, so you know where the gaps are.
  • df.duplicated().sum() counts how many rows are exact duplicates of another row.
  • df.describe() shows statistical summaries (mean, min, max, quartiles) for all numeric columns.
  • Run this cell by pressing Shift+Enter.

You should see a detailed breakdown of all 29 columns with their non-null counts, data types, a per-column missing value count, the duplicate count, and a statistical summary table.

What should I look for?

Pay attention to the Non-Null Count column in the df.info() output. Any column with fewer non-null values than the total row count has missing data. Also note the data types. The Date column might show as object instead of datetime64, which means you'll need to convert it.

🙋‍♀️ Seeing a NameError?

  • Make sure you ran Cell 1 first. The variable df only exists after Cell 1 executes successfully.
  • Check that your CSV file is named retail_transactional_dataset.csv and sits in the same folder as your notebook.

Help me debug this NameError in my Jupyter Notebook.

Clean invalid rows and fix data types

Now that you know where the problems are, it's time to fix them. Rows without a Customer ID are useless for segmentation because you can't assign them to any customer. Duplicate transactions would inflate frequency counts.

  • Create a new cell below Cell 2 and add the following cleaning code:
# Cell 3: Data Cleaning
print(f"Original shape: {df.shape}")

# Drop rows with missing Customer ID
df = df.dropna(subset=['Customer ID'])
print(f"After dropping missing Customer ID: {df.shape}")

# Remove duplicate transactions
df = df.drop_duplicates(subset=['Transaction ID'])
print(f"After removing duplicates: {df.shape}")

# Convert Date to datetime
df['Date'] = pd.to_datetime(df['Date'])

What does this code do?

  • dropna(subset=['Customer ID']) removes any row where the Customer ID is missing. You cannot segment a customer without an ID.
  • drop_duplicates(subset=['Transaction ID']) removes rows that share the same Transaction ID, keeping only the first occurrence.
  • pd.to_datetime(df['Date']) converts the Date column from a text string into a proper datetime object. This allows you to calculate time differences for the Recency metric later.
Create the TotalSpend column and display summary statistics

For the Monetary dimension of RFM Analysis, you need a single column that represents how much each transaction was worth. The dataset may already have this as Total Amount. If not, you'll compute it from the existing columns.

  • Below the datetime conversion line in the same Cell 3, add this code to complete the cell:
# Ensure we have a TotalSpend column
if 'Total Amount' in df.columns:
    df['TotalSpend'] = df['Total Amount']
else:
    df['TotalSpend'] = df['Amount'] * df['Total Purchases']

# Summary statistics
print(f"\nUnique Customers: {df['Customer ID'].nunique()}")
print(f"Date Range: {df['Date'].min()} to {df['Date'].max()}")
print(f"Average Spend per Customer: ${df.groupby('Customer ID')['TotalSpend'].sum().mean():.2f}")

What does this code do?

  • The if/else block checks whether the dataset already has a Total Amount column. If it does, that becomes TotalSpend. Otherwise, it multiplies Amount by Total Purchases to calculate the total.
  • nunique() counts how many distinct customers exist in the cleaned dataset.
  • The final groupby aggregation shows the average total spend per customer across all their transactions.

Before you run this cell, what do you think will happen to the row count after cleaning? Will it drop significantly or stay mostly the same?

  • Run Cell 3 by pressing Shift+Enter.

You should see the original shape, the shape after dropping missing Customer IDs, the shape after removing duplicates, the number of unique customers, the date range of transactions, and the average spend per customer.

Seeing a KeyError on Customer ID?

  • Make sure you typed Customer ID with a space between the two words and with both words capitalized. Column names are case-sensitive.
  • If you edited Cell 1 but forgot to re-run it, your df variable might be stale. Re-run Cell 1 first, then Cell 3.

Help me fix a KeyError in my data cleaning cell.

✔️ Awesome, I've got everything!

Great. Confirm you've saved your notebook with Ctrl+S (Windows) or Cmd+S (macOS).

ⓧ I'd like to double check the full code

Your notebook should have these three cells in order. Scroll up and confirm each one is present:

  • Cell 1: Imports (pandas, numpy, matplotlib, seaborn, sklearn modules) and data loading with pd.read_csv().
  • Cell 2: Data exploration with df.info(), df.isnull().sum(), df.duplicated().sum(), and df.describe().
  • Cell 3: Data cleaning with dropna, drop_duplicates, pd.to_datetime(), TotalSpend creation, and summary statistics.

Your data is clean and you know your customers. Next up, you'll condense each customer's entire purchase history into three powerful numbers: Recency, Frequency, and Monetary value.

Compute RFM Scores

In Step 2, you cleaned the dataset and confirmed it has no missing Customer IDs, no duplicates, and proper date formatting. Now you have a solid foundation to work from.

Your challenge now is comparing thousands of customers against each other. RFM Analysis condenses each customer's behavior into three numbers: how recently they purchased, how often they buy, and how much they spend. These three metrics let you rank every customer on a common scale.

In this step, get ready to:
  • Compute Recency, Frequency, and Monetary values for each customer.
  • Score each dimension into quintiles (1-5) and combine into a single RFM score.
  • Identify your top 10 Champions and bottom 10 Lost customers.
Compute raw RFM metrics for each customer

To compare customers, you need a single row per customer with three aggregated values. Recency measures how many days since their last purchase. Frequency counts their total transactions. Monetary sums their total spending.

  • Create a new cell in your notebook and add the following code to compute RFM metrics:
# Cell 4: Compute RFM Metrics
# Set reference date as max date + 1 day
reference_date = df['Date'].max() + pd.Timedelta(days=1)

# Group by Customer ID and compute RFM
rfm = df.groupby('Customer ID').agg(
    Recency=('Date', lambda x: (reference_date - x.max()).days),
    Frequency=('Transaction ID', 'count'),
    Monetary=('TotalSpend', 'sum')
).reset_index()

print(f"RFM table shape: {rfm.shape}")
rfm.head(10)

What does this code do?

  • reference_date is set one day after the latest transaction in the dataset. This gives you a fixed point to measure "days since last purchase" from.
  • The .groupby('Customer ID').agg() call collapses all rows for each customer into a single row with three computed columns.
  • Recency counts days between the reference date and the customer's most recent purchase. Lower is better (they bought recently).
  • Frequency counts total transactions. Monetary sums total spending.
  • Run the cell by pressing Shift+Enter.

You should see the RFM table shape printed (one row per unique customer, 4 columns) followed by the first 10 rows showing Recency, Frequency, and Monetary values.

Seeing a KeyError or empty DataFrame?

  • Make sure you ran Cell 3 (Data Cleaning) first. The TotalSpend column must already exist.
  • Check that your column names match exactly: Customer ID, Date, Transaction ID, and TotalSpend. Column names are case-sensitive.
  • Help me debug my RFM groupby cell
Score each RFM dimension into quintiles

Raw RFM numbers are not directly comparable. Recency is measured in days, Frequency is a count, and Monetary is in dollars. Quintile scoring converts each dimension into a 1-5 rank so you can compare and combine them.

Why is Recency scored inversely?

A low Recency value (few days since last purchase) means the customer bought recently. That is good behavior. So a customer with Recency of 2 days gets a high score (5), while a customer with Recency of 300 days gets a low score (1).

Frequency and Monetary work the other way around. Higher values are better, so they get higher scores directly.

  • Create a new cell and add the following code to score each dimension and combine them:
# Cell 5: Score RFM with Quintiles
# Score Recency (lower is better, so reverse the labels)
rfm['R_Score'] = pd.qcut(rfm['Recency'], q=5, labels=[5, 4, 3, 2, 1]).astype(int)

# Score Frequency (higher is better)
rfm['F_Score'] = pd.qcut(rfm['Frequency'].rank(method='first'), q=5, labels=[1, 2, 3, 4, 5]).astype(int)

# Score Monetary (higher is better)
rfm['M_Score'] = pd.qcut(rfm['Monetary'].rank(method='first'), q=5, labels=[1, 2, 3, 4, 5]).astype(int)

# Combined RFM Score
rfm['RFM_Score'] = rfm['R_Score'] + rfm['F_Score'] + rfm['M_Score']

print("=== Top 10 Customers (Champions) ===")
print(rfm.nlargest(10, 'RFM_Score')[['Customer ID', 'Recency', 'Frequency', 'Monetary', 'RFM_Score']])
print("\n=== Bottom 10 Customers (Lost) ===")
print(rfm.nsmallest(10, 'RFM_Score')[['Customer ID', 'Recency', 'Frequency', 'Monetary', 'RFM_Score']])

What does this code do?

  • pd.qcut() splits values into 5 equal-sized bins (quintiles). Each customer gets a score from 1 to 5 based on which bin they fall into.
  • For Recency, labels=[5, 4, 3, 2, 1] inverts the scoring so low Recency (recent buyers) gets a 5.
  • For Frequency and Monetary, .rank(method='first') handles tied values that would otherwise cause bin-edge errors in qcut.
  • RFM_Score combines all three scores (range 3-15). Higher means a better customer overall.
  • The final prints show your best customers (Champions with scores near 15) and worst (Lost with scores near 3).

Before you run this cell, think about what you expect. Should the top 10 customers have low or high Recency values? What about their Monetary values?

  • Run the cell by pressing Shift+Enter.

You should see two tables. The top 10 Champions have low Recency (bought recently), high Frequency, high Monetary, and RFM_Scores near 15. The bottom 10 Lost customers show the opposite pattern: high Recency, low Frequency, low Monetary, and scores near 3.

Seeing a ValueError about bin edges?

  • This happens when too many customers share the same value, making it impossible to create 5 distinct bins. The .rank(method='first') call on Frequency and Monetary should prevent this. Make sure you included it.
  • If the error is on the Recency line, check that your Cell 4 ran correctly and that rfm['Recency'] contains a range of different integer values.
  • Help me fix my quintile scoring ValueError

✔️ Awesome, I've got everything!

Great. Double check you have both Cell 4 and Cell 5 saved and executed in your notebook.

ⓧ I'd like to double check the full code

Scroll up in your notebook and confirm the following cells are present and executed in order:

  • Cell 1: Import Libraries and Load Data (imports pandas, numpy, matplotlib, seaborn, sklearn modules and loads the CSV).
  • Cell 2: Data Exploration (prints info, missing values, duplicates, and describe).
  • Cell 3: Data Cleaning (drops missing Customer IDs, removes duplicates, converts Date, creates TotalSpend).
  • Cell 4: Compute RFM Metrics (sets reference_date, groups by Customer ID, creates rfm DataFrame with Recency, Frequency, Monetary).
  • Cell 5: Score RFM with Quintiles (adds R_Score, F_Score, M_Score, RFM_Score columns, prints top 10 and bottom 10).

You now have every customer ranked on a 3-15 scale. You can already see who your Champions and Lost customers are. Next up, you will use K-Means clustering to discover natural groupings in the data that go beyond simple score thresholds.

Apply K-Means Clustering

You have RFM scores that rank every customer on a 3-15 scale. The natural next question is: where do you draw the line between segments?

You could manually set thresholds (scores 12-15 are Champions, 8-11 are Loyal) but those boundaries are arbitrary. Different datasets need different cutoffs. K-Means clustering solves this by finding natural groupings in the data itself, so the segments emerge from the numbers rather than from guesswork.

In this step, get ready to:
  • Standardize RFM features so K-Means can compare them fairly.
  • Run the Elbow Method to find the optimal number of clusters.
  • Fit the final model and examine each cluster's profile.
Standardize the RFM features

K-Means calculates distance between data points to form clusters. If one feature (like Monetary) ranges into the thousands while another (like Frequency) stays in single digits, the large-scale feature dominates the clustering.

StandardScaler fixes this by transforming each feature to have a mean of 0 and a standard deviation of 1. All three RFM dimensions then contribute equally to the clustering.

  • In your notebook, create a new cell (Cell 6) and standardize the RFM features by pasting this code:
# Cell 6: Standardize Features for Clustering
X = rfm[['Recency', 'Frequency', 'Monetary']]

scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

print(f"Scaled features shape: {X_scaled.shape}")
print(f"Mean after scaling: {X_scaled.mean(axis=0).round(4)}")
print(f"Std after scaling: {X_scaled.std(axis=0).round(4)}")

What does this code do?

  • X = rfm[['Recency', 'Frequency', 'Monetary']] selects only the raw RFM values (not the scores) as the input features for clustering.
  • StandardScaler() creates a scaler that will center each column at 0 and scale it to unit variance.
  • fit_transform(X) learns the mean and standard deviation of each column, then applies the transformation in one step.
  • Run the cell by pressing Shift+Enter.
  • You should see the shape of your scaled array printed, plus a mean of approximately 0.0 and a standard deviation of approximately 1.0 for each feature.

Seeing a NameError for rfm?

Make sure you ran all previous cells (1-5) in order. Jupyter cells share state, so if you restarted the kernel you need to re-run from the top.

Help me fix a NameError in my Jupyter notebook.

Run the Elbow Method to find the optimal K

K-Means needs you to tell it how many clusters to create. The Elbow Method helps you choose by running K-Means for several values of K and plotting how well each one fits.

  • Create a new cell (Cell 7) and run the Elbow Method by pasting this code:
# Cell 7: Elbow Method to Find Optimal K
inertias = []
silhouette_scores = []
K_range = range(2, 11)

for k in K_range:
    kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)
    kmeans.fit(X_scaled)
    inertias.append(kmeans.inertia_)
    silhouette_scores.append(silhouette_score(X_scaled, kmeans.labels_))

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5))

ax1.plot(K_range, inertias, 'bo-')
ax1.set_xlabel('Number of Clusters (K)')
ax1.set_ylabel('Inertia')
ax1.set_title('Elbow Method')
ax1.axvline(x=4, color='r', linestyle='--', label='Chosen K=4')
ax1.legend()

ax2.plot(K_range, silhouette_scores, 'go-')
ax2.set_xlabel('Number of Clusters (K)')
ax2.set_ylabel('Silhouette Score')
ax2.set_title('Silhouette Score vs K')
ax2.axvline(x=4, color='r', linestyle='--', label='Chosen K=4')
ax2.legend()

plt.tight_layout()
plt.show()

What does this code do?

  • The loop fits K-Means for K=2 through K=10, recording two metrics each time: inertia (how tightly points cluster together) and silhouette score (how well-separated the clusters are).
  • The left plot shows inertia dropping as K increases. The "elbow" is where adding more clusters stops helping much.
  • The right plot shows silhouette score. A higher score means better-defined clusters.
  • The red dashed line at K=4 marks the chosen cluster count where both metrics agree.

Before you run this cell, what shape do you expect the inertia line to take as K increases? Do you think it will drop steadily or flatten out at some point?

  • Run the cell by pressing Shift+Enter.
  • You should see two side-by-side plots. The left plot (Elbow Method) shows inertia dropping sharply and then flattening around K=4. The right plot (Silhouette Score) confirms 4 is a strong choice.

Why K=4 specifically?

The "elbow" is where the curve bends from steep to flat. Before K=4, each additional cluster significantly reduces inertia. After K=4, the improvement is marginal. This tells you 4 clusters capture most of the structure in the data without overfitting.

🙋‍♀️ Plot not displaying?

If you see no plot output, check that your Cell 1 includes import matplotlib.pyplot as plt and that you ran it. Also confirm the cell finishes running (no asterisk [*] stuck next to it).

Help me display my matplotlib plots in Jupyter.

Fit the final K-Means model

Now that the Elbow Method confirmed K=4, you can fit the final model and assign every customer to one of 4 clusters.

  • Create a new cell (Cell 8) and fit the final K-Means model by pasting this code:
# Cell 8: Fit Final K-Means Model
kmeans_final = KMeans(n_clusters=4, random_state=42, n_init=10)
rfm['Cluster'] = kmeans_final.fit_predict(X_scaled)

print("=== Cluster Profiles ===")
cluster_summary = rfm.groupby('Cluster')[['Recency', 'Frequency', 'Monetary', 'RFM_Score']].mean().round(2)
cluster_summary['Count'] = rfm.groupby('Cluster')['Customer ID'].count().values
print(cluster_summary)

What does this code do?

  • KMeans(n_clusters=4, random_state=42, n_init=10) creates a K-Means model that will group data into 4 clusters. random_state=42 makes results reproducible. n_init=10 runs the algorithm 10 times with different starting points and keeps the best result.
  • fit_predict(X_scaled) trains the model on your standardized data and returns a cluster label (0, 1, 2, or 3) for each customer.
  • The cluster_summary table shows the average Recency, Frequency, Monetary, and RFM Score per cluster so you can see how the groups differ.

Before you run this, do you think all 4 clusters will have roughly equal customer counts, or will some be much larger than others?

  • Run the cell by pressing Shift+Enter.
  • You should see a table with 4 rows (clusters 0-3), each showing different average values for Recency, Frequency, Monetary, RFM_Score, and Count. The clusters have noticeably different profiles.

Look at the Recency and Monetary columns across the clusters. One cluster has low Recency and high Monetary (your best customers). Another has high Recency and low Monetary (your worst). K-Means found these groupings automatically from the data.

Seeing different cluster sizes each time?

If your cluster counts change between runs, make sure you included random_state=42 in the KMeans constructor. Without it, the algorithm starts from random positions each time and can produce different results.

Help me get consistent K-Means results.

✔️ Awesome, I've got everything!

Great. Double check you've saved your notebook with all three new cells (6, 7, and 8) present and executed.

ⓧ I'd like to double check the full code

Scroll through your notebook and confirm these cells are present in order, each showing output:

  • Cell 1: Import Libraries and Load Data (prints dataset shape, shows first 5 rows).
  • Cell 2: Data Exploration (prints info, missing values, duplicates, describe).
  • Cell 3: Data Cleaning (prints shape changes, unique customers, date range, average spend).
  • Cell 4: Compute RFM Metrics (prints RFM table shape, shows first 10 rows).
  • Cell 5: Score RFM with Quintiles (prints top 10 and bottom 10 customers).
  • Cell 6: Standardize Features for Clustering (prints scaled shape, mean ~0, std ~1).
  • Cell 7: Elbow Method to Find Optimal K (shows two side-by-side plots with red line at K=4).
  • Cell 8: Fit Final K-Means Model (prints cluster profiles table with 4 rows).

Your customers are now grouped into 4 distinct behavioral clusters. Next up, you will turn these numbered clusters into named segments with clear visualizations and business recommendations that a marketing team can act on.

Visualize Segments and Deliver Business Recommendations

Your K-Means model discovered 4 distinct customer clusters. You can see different average Recency, Frequency, and Monetary values for each group.

Numbers in a table are useful for you as the analyst, but they don't convince stakeholders. A marketing team needs clear visualizations and plain-English recommendations they can act on immediately. In this step, you'll translate cluster numbers into named segments, create portfolio-ready charts, and deliver a business action plan.

In this step, get ready to:
  • Label each cluster with a meaningful business name based on its RFM profile.
  • Create scatter plots and bar charts that visually separate your customer segments.
  • Build a recommendations table mapping each segment to a specific marketing action.
Label clusters with business names

Right now your clusters are labeled 0, 1, 2, and 3. Those numbers mean nothing to a marketing team. You need to assign descriptive names based on each cluster's RFM profile.

  • Create a new cell in your notebook and label it Cell 9. Add the following code to map cluster numbers to business names:
# Cell 9: Label Clusters with Business Names
cluster_order = cluster_summary.sort_values('RFM_Score', ascending=False).index.tolist()

segment_names = {
    cluster_order[0]: 'Champions',
    cluster_order[1]: 'Loyal Customers',
    cluster_order[2]: 'At Risk',
    cluster_order[3]: 'Lost'
}

rfm['Segment'] = rfm['Cluster'].map(segment_names)
print("=== Segment Distribution ===")
print(rfm['Segment'].value_counts())

What does this code do?

Instead of manually guessing which cluster number is which, you sort clusters by their average RFM_Score from highest to lowest. The cluster with the highest score becomes "Champions" and the lowest becomes "Lost".

This approach is dynamic. If you re-run clustering with different data, the labels still map correctly because they're based on relative performance, not hard-coded cluster numbers.

  • Run the cell.

You should see a segment distribution showing how many customers fall into each named group: Champions, Loyal Customers, At Risk, and Lost.

Seeing a KeyError or NaN values?

Make sure you ran Cell 8 first so that cluster_summary exists. If you restarted your kernel, re-run all cells from Cell 1 through Cell 8 before running Cell 9.

Help me fix a KeyError when mapping cluster names to my RFM DataFrame.

Create scatter plots of your segments

Scatter plots reveal how well-separated your clusters are. If the segments overlap heavily, the model may not be capturing meaningful differences. Clear separation means your segments represent truly distinct customer behaviors.

  • Create a new cell (Cell 10) and add the following code to plot Frequency vs Monetary and Recency vs Monetary side by side:
# Cell 10: Visualize - Scatter Plots
fig, axes = plt.subplots(1, 2, figsize=(16, 6))

sns.scatterplot(data=rfm, x='Frequency', y='Monetary', hue='Segment',
                palette='Set2', alpha=0.7, ax=axes[0])
axes[0].set_title('Customer Segments: Frequency vs Monetary')
axes[0].set_xlabel('Frequency (Number of Purchases)')
axes[0].set_ylabel('Monetary (Total Spend)')

sns.scatterplot(data=rfm, x='Recency', y='Monetary', hue='Segment',
                palette='Set2', alpha=0.7, ax=axes[1])
axes[1].set_title('Customer Segments: Recency vs Monetary')
axes[1].set_xlabel('Recency (Days Since Last Purchase)')
axes[1].set_ylabel('Monetary (Total Spend)')

plt.tight_layout()
plt.show()

What does this code do?

The hue='Segment' parameter tells seaborn to color each point by its segment name. The left plot shows spending vs purchase frequency. The right plot shows spending vs how recently the customer bought.

Champions should cluster in the high-frequency, high-monetary corner. Lost customers should sit in the high-recency (long time since purchase), low-monetary area.

  • Run the cell.

You should see two scatter plots with color-coded dots. Each color represents a different customer segment. Look for visual separation between the groups.

All dots are the same color?

Check that the Segment column exists in your rfm DataFrame by running rfm.columns. If it's missing, re-run Cell 9 first.

Help me debug why my scatter plot segments aren't showing different colors.

Build bar charts and a recommendations table

Scatter plots show separation, but stakeholders also want to know the size of each segment and how much each group spends on average. Bar charts answer these questions at a glance.

  • Create a new cell (Cell 11) and add the following code to generate bar charts for segment size and average spend:
# Cell 11: Visualize - Segment Size and Average Spend
fig, axes = plt.subplots(1, 2, figsize=(14, 5))

segment_counts = rfm['Segment'].value_counts()
sns.barplot(x=segment_counts.index, y=segment_counts.values, palette='Set2', ax=axes[0])
axes[0].set_title('Number of Customers per Segment')
axes[0].set_xlabel('Segment')
axes[0].set_ylabel('Customer Count')

segment_spend = rfm.groupby('Segment')['Monetary'].mean().sort_values(ascending=False)
sns.barplot(x=segment_spend.index, y=segment_spend.values, palette='Set2', ax=axes[1])
axes[1].set_title('Average Spend per Segment')
axes[1].set_xlabel('Segment')
axes[1].set_ylabel('Average Monetary Value')

plt.tight_layout()
plt.show()

What does this code do?

The left bar chart shows how many customers are in each segment. The right bar chart shows the average total spend per segment, sorted from highest to lowest.

Together these answer two key business questions: "How big is each group?" and "How much is each group worth?" Champions may be a small group with outsized spending power.

  • Run the cell.

You should see two bar charts side by side. The left chart shows customer counts per segment. The right chart shows average monetary value per segment with Champions at the top.

Bar labels are overlapping or cut off?

This can happen if segment names are long. Make sure your figsize is set to (14, 5) which gives enough horizontal space. If labels still overlap, check that plt.tight_layout() is included before plt.show().

Help me fix overlapping labels on my seaborn bar charts.

Now for the final piece. A marketing team needs a concrete action plan, not just charts. You'll build a recommendations table that maps each segment to a specific strategy.

  • Create a new cell (Cell 12) and add the following code to build the business recommendations table:
# Cell 12: Business Recommendations Table
recommendations = pd.DataFrame({
    'Segment': ['Champions', 'Loyal Customers', 'At Risk', 'Lost'],
    'Customer Count': [
        rfm[rfm['Segment'] == 'Champions'].shape[0],
        rfm[rfm['Segment'] == 'Loyal Customers'].shape[0],
        rfm[rfm['Segment'] == 'At Risk'].shape[0],
        rfm[rfm['Segment'] == 'Lost'].shape[0]
    ],
    'Avg RFM Score': [
        rfm[rfm['Segment'] == 'Champions']['RFM_Score'].mean().round(1),
        rfm[rfm['Segment'] == 'Loyal Customers']['RFM_Score'].mean().round(1),
        rfm[rfm['Segment'] == 'At Risk']['RFM_Score'].mean().round(1),
        rfm[rfm['Segment'] == 'Lost']['RFM_Score'].mean().round(1)
    ],
    'Avg Spend': [
        rfm[rfm['Segment'] == 'Champions']['Monetary'].mean().round(2),
        rfm[rfm['Segment'] == 'Loyal Customers']['Monetary'].mean().round(2),
        rfm[rfm['Segment'] == 'At Risk']['Monetary'].mean().round(2),
        rfm[rfm['Segment'] == 'Lost']['Monetary'].mean().round(2)
    ],
    'Marketing Action': [
        'Reward program, exclusive perks, early access to products',
        'Upsell, loyalty program, referral incentives',
        'Win-back campaign, personalized discounts, re-engagement emails',
        'Aggressive reactivation offers, last-chance deals, survey for feedback'
    ]
})

print("=== Customer Segment Recommendations ===")
print(recommendations.to_string(index=False))

What does this code do?

This builds a single DataFrame that a stakeholder can read like a decision matrix. Each row tells them: what this segment is called, how many customers are in it, how valuable they are on average, and exactly what marketing action to take.

The .to_string(index=False) prints the table without the row index numbers, making it cleaner for presentation.

Before you run this cell, what do you expect to see? Think about what columns and rows the output table will have.

  • Run the cell.

You should see a clean table with 4 rows (one per segment) and 5 columns: Segment, Customer Count, Avg RFM Score, Avg Spend, and Marketing Action. Each segment has a specific, actionable recommendation.

Seeing an empty DataFrame or all zeros?

Make sure the segment names in Cell 12 match exactly what Cell 9 assigned. Check by running rfm['Segment'].unique() in a separate cell. The names must be 'Champions', 'Loyal Customers', 'At Risk', and 'Lost' with exact capitalization and spacing.

Help me debug why my recommendations table shows zeros or empty values.

✔️ Awesome, I've got everything!

Great. Make sure all cells from Cell 9 through Cell 12 are saved in your notebook.

ⓧ I'd like to double check the full code

Scroll through your notebook and confirm each of these cells is present and matches what you built above, in order:

  • Cells 1-8 from previous steps (imports, exploration, cleaning, RFM metrics, scoring, standardization, elbow method, K-Means).
  • Cell 9: Label Clusters with Business Names (creates segment_names dict and adds Segment column).
  • Cell 10: Visualize - Scatter Plots (Frequency vs Monetary and Recency vs Monetary).
  • Cell 11: Visualize - Segment Size and Average Spend (two bar charts).
  • Cell 12: Business Recommendations Table (the recommendations DataFrame with 4 rows and 5 columns).

Your Python visualizations and recommendations table are complete. You know who your best and worst customers are. Next up, you'll dig into what they're buying and when, uncovering product demand patterns and seasonal sales trends.

Analyze Product Demand and Sales Trends

You've segmented customers by their buying behavior. Now the marketing team wants to know what those customers are buying and when they buy most.

In this step, you'll analyze product categories to find which items drive the most revenue, identify the least popular products, and uncover monthly sales patterns that reveal seasonality.

In this step, get ready to:
  • Identify the most and least demanded product categories.
  • Analyze monthly sales volume to find peak and slow months.
  • Visualize product demand and seasonal trends with bar charts and line plots.
Analyze product category demand

The dataset includes a Product Category column for every transaction. By aggregating total spend and transaction count per category, you can see which products drive revenue and which are underperforming.

  • Create a new cell in your notebook (Cell 14) and analyze product demand by running this code:
# Cell 14: Product Category Demand Analysis
# Count transactions and total revenue per product category
product_demand = df.groupby('Product Category').agg(
    Transaction_Count=('Transaction ID', 'count'),
    Total_Revenue=('Total Amount', 'sum'),
    Avg_Order_Value=('Total Amount', 'mean')
).round(2)

# Sort by total revenue descending
product_demand = product_demand.sort_values('Total_Revenue', ascending=False)

print("=== Product Category Demand ===")
print(product_demand)
print(f"\nTop category: {product_demand.index[0]}")
print(f"Bottom category: {product_demand.index[-1]}")

What does this code do?

The groupby('Product Category') groups all transactions by their product type. For each group, you calculate three metrics: how many transactions occurred, the total revenue generated, and the average order value.

Sorting by revenue reveals which categories bring in the most money. The top category is your cash cow. The bottom category may need a promotional push or may not be worth stocking.

  • Run the cell.

You should see a table listing each product category with its transaction count, total revenue, and average order value. The top and bottom categories are printed below the table.

Seeing a KeyError for Product Category or Total Amount?

The original dataset uses exact column names with spaces. Run df.columns.tolist() to see your actual column names. If you renamed columns during cleaning, use your renamed versions instead.

Help me fix a KeyError when grouping by Product Category in my retail dataset.

Now visualize the demand to make the differences obvious at a glance.

  • Create a new cell (Cell 15) and visualize product demand by running this code:
# Cell 15: Visualize Product Demand
fig, axes = plt.subplots(1, 2, figsize=(16, 6))

# Bar chart: total revenue by category
sns.barplot(x=product_demand['Total_Revenue'],
            y=product_demand.index,
            palette='viridis', ax=axes[0])
axes[0].set_title('Total Revenue by Product Category')
axes[0].set_xlabel('Total Revenue')
axes[0].set_ylabel('Product Category')

# Bar chart: transaction count by category
sns.barplot(x=product_demand['Transaction_Count'],
            y=product_demand.index,
            palette='viridis', ax=axes[1])
axes[1].set_title('Number of Transactions by Product Category')
axes[1].set_xlabel('Transaction Count')
axes[1].set_ylabel('Product Category')

plt.tight_layout()
plt.show()
  • Run the cell.

You should see two horizontal bar charts side by side. The left shows revenue contribution per category. The right shows transaction volume. A category with high revenue but low transactions means high-ticket items.

Bars not appearing or chart is blank?

Make sure Cell 14 ran successfully first so product_demand exists. Also check that plt and sns were imported in Cell 1.

Help me debug why my seaborn barplot is showing a blank chart for product demand.

Identify monthly sales trends

Seasonal patterns tell you when to ramp up marketing spend and when to run clearance sales. The dataset has Month and Year columns you can use to spot peaks and dips.

  • Create a new cell (Cell 16) and analyze monthly sales trends by running this code:
# Cell 16: Monthly Sales Trend Analysis
# Aggregate sales by month
monthly_sales = df.groupby('Month').agg(
    Total_Sales=('Total Amount', 'sum'),
    Order_Count=('Transaction ID', 'count')
).round(2)

# Sort by month number for chronological order
monthly_sales = monthly_sales.sort_index()

print("=== Monthly Sales Summary ===")
print(monthly_sales)
print(f"\nPeak month: Month {monthly_sales['Total_Sales'].idxmax()}")
print(f"Slowest month: Month {monthly_sales['Total_Sales'].idxmin()}")

What does this code do?

This groups every transaction by its month number (1-12) and calculates total sales and order count for each month. Sorting by index puts January through December in order so the trend is easy to read.

The peak month tells you when customers spend the most. The slowest month is where targeted promotions could lift revenue.

  • Run the cell.

You should see a 12-row table with total sales and order count per month. The peak and slowest months are printed at the bottom.

  • Create a new cell (Cell 17) and visualize the monthly trend by running this code:
# Cell 17: Visualize Monthly Sales Trend
fig, ax = plt.subplots(figsize=(12, 5))

# Line plot showing sales across months
ax.plot(monthly_sales.index, monthly_sales['Total_Sales'],
        marker='o', linewidth=2, color='#2196F3')
ax.fill_between(monthly_sales.index, monthly_sales['Total_Sales'],
                alpha=0.1, color='#2196F3')
ax.set_title('Monthly Sales Trend')
ax.set_xlabel('Month')
ax.set_ylabel('Total Sales')
ax.set_xticks(range(1, 13))
ax.set_xticklabels(['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
                    'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'])
ax.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()
  • Run the cell.

You should see a line chart with dots at each month. Peaks show high-demand periods. Valleys show slow months where promotions could help.

Line chart showing a flat line or missing months?

If the chart is flat, check that your Month column contains numeric values (1-12), not strings. Run df['Month'].dtype to check. If it's an object type, convert it with df['Month'] = df['Month'].astype(int).

Help me fix my monthly sales line chart that's showing unexpected results.

You now know which products drive your revenue and when sales peak or dip. Next up, you'll explore where your most valuable buyers are located geographically.

Explore Geographic Buying Patterns

You've identified what sells and when. Now the question is where your highest-value customers live. Geographic insights tell the marketing team which regions to prioritize for campaigns and where to expand.

In this step, you'll analyze buyer distribution across countries and cities, compare regional spending power, and identify the top markets driving the most revenue.

In this step, get ready to:
  • Identify which countries and cities have the most buyers.
  • Compare average spend across regions.
  • Visualize geographic distribution with bar charts.
Analyze buyer distribution by country

The dataset includes Country, State, and City columns for every customer. Aggregating by region reveals where your customer base is concentrated.

  • Create a new cell (Cell 18) and analyze geographic distribution by running this code:
# Cell 18: Geographic Buyer Distribution
# Count unique customers and total revenue per country
country_stats = df.groupby('Country').agg(
    Customer_Count=('Customer ID', 'nunique'),
    Total_Revenue=('Total Amount', 'sum'),
    Avg_Spend=('Total Amount', 'mean')
).round(2)

# Sort by customer count descending
country_stats = country_stats.sort_values('Customer_Count', ascending=False)

print("=== Buyers by Country ===")
print(country_stats.head(10))
print(f"\nTop market: {country_stats.index[0]} ({country_stats.iloc[0]['Customer_Count']} customers)")

What does this code do?

Using nunique() on Customer ID counts distinct customers per country, not repeat transactions. This tells you how many individual people buy from each region, not just how many orders were placed.

The average spend per transaction reveals which countries have higher purchasing power, even if they have fewer customers.

  • Run the cell.

You should see a table listing the top 10 countries by unique customer count, along with their total revenue and average spend per transaction.

Seeing only one country in the results?

Some retail datasets have limited geographic diversity. Run df['Country'].nunique() to check how many unique countries exist. If there's only one, switch your analysis to State or City instead.

Help me adjust my geographic analysis when the dataset has limited country diversity.

Compare regional spending and visualize top markets

A country with many buyers isn't always the most profitable one. You need to compare both volume (customer count) and value (average spend) to prioritize markets.

  • Create a new cell (Cell 19) and analyze city-level patterns by running this code:
# Cell 19: Top Cities by Revenue and Customer Count
# Analyze at city level for granular geographic insight
city_stats = df.groupby('City').agg(
    Customer_Count=('Customer ID', 'nunique'),
    Total_Revenue=('Total Amount', 'sum'),
    Avg_Spend=('Total Amount', 'mean')
).round(2)

# Get top 10 cities by revenue
top_cities_revenue = city_stats.sort_values('Total_Revenue', ascending=False).head(10)

# Get top 10 cities by customer count
top_cities_customers = city_stats.sort_values('Customer_Count', ascending=False).head(10)

print("=== Top 10 Cities by Revenue ===")
print(top_cities_revenue[['Total_Revenue', 'Customer_Count']])
print("\n=== Top 10 Cities by Customer Count ===")
print(top_cities_customers[['Customer_Count', 'Total_Revenue']])
  • Run the cell.

You should see two tables: one showing cities ranked by revenue, and another ranked by customer count. Cities that appear in both lists are your strongest markets.

  • Create a new cell (Cell 20) and visualize the geographic data by running this code:
# Cell 20: Visualize Geographic Buying Patterns
fig, axes = plt.subplots(1, 2, figsize=(16, 7))

# Top 10 cities by revenue
sns.barplot(x=top_cities_revenue['Total_Revenue'],
            y=top_cities_revenue.index,
            palette='coolwarm', ax=axes[0])
axes[0].set_title('Top 10 Cities by Revenue')
axes[0].set_xlabel('Total Revenue')
axes[0].set_ylabel('City')

# Top 10 cities by customer count
sns.barplot(x=top_cities_customers['Customer_Count'],
            y=top_cities_customers.index,
            palette='coolwarm', ax=axes[1])
axes[1].set_title('Top 10 Cities by Customer Count')
axes[1].set_xlabel('Number of Unique Customers')
axes[1].set_ylabel('City')

plt.tight_layout()
plt.show()
  • Run the cell.

You should see two horizontal bar charts. The left shows the top revenue-generating cities. The right shows where the most buyers are concentrated. A city ranking high on revenue but low on customer count means fewer customers spending more per order.

City names overlapping or cut off?

If city names are long, increase the figure width. Change figsize=(16, 7) to figsize=(18, 8). Also make sure plt.tight_layout() is included before plt.show().

Help me fix overlapping city names on my geographic bar charts.

You now know where your buyers are and which regions drive the most revenue. Next up, you'll discover which products customers buy together using Market Basket Analysis and the Apriori algorithm.

Discover Product Associations with Market Basket Analysis

You know who your customers are, what they buy individually, and where they're located. But which products do customers buy together? Market Basket Analysis answers this by finding hidden product associations in transaction data.

In this step, you'll use the Apriori algorithm to discover which product categories and brands customers frequently purchase in the same transaction. These rules power the "customers who bought X also bought Y" recommendations you see on e-commerce sites.

In this step, get ready to:
  • Install mlxtend and prepare transaction data for association rule mining.
  • Run the Apriori algorithm to find frequently purchased product combinations.
  • Extract association rules showing which products are bought together.
Install mlxtend and prepare transaction data

The mlxtend library provides the Apriori implementation. You need to transform your transaction data into a one-hot encoded format where each row is a transaction and each column is a product category.

  • Install mlxtend by running this command in a new cell:
pip install mlxtend
  • Create a new cell (Cell 21) and prepare your transaction data by running this code:
# Cell 21: Prepare Transaction Data for Market Basket Analysis
from mlxtend.frequent_patterns import apriori, association_rules
from mlxtend.preprocessing import TransactionEncoder

# Group products by Customer ID to create "baskets"
baskets = df.groupby('Customer ID')['Product Category'].apply(list).tolist()

# One-hot encode the baskets
te = TransactionEncoder()
te_array = te.fit(baskets).transform(baskets)
basket_df = pd.DataFrame(te_array, columns=te.columns_)

print(f"Basket matrix shape: {basket_df.shape}")
print(f"Product categories: {list(basket_df.columns)}")
print(basket_df.head())

What does this code do?

Each customer's purchases are grouped into a "basket" (a list of product categories they bought). The TransactionEncoder converts these baskets into a binary matrix: True if the customer bought that category, False if not. This format is what the Apriori algorithm requires.

  • Run the cell.

You should see the matrix shape (rows = customers, columns = product categories) and a preview of True/False values showing which categories each customer purchased.

ModuleNotFoundError for mlxtend?

If you see ModuleNotFoundError: No module named 'mlxtend', make sure you ran the pip install mlxtend command first. You may need to restart your Jupyter kernel after installing.

Help me fix the mlxtend import error after installing.

Run the Apriori algorithm and extract association rules

Now you'll find which product combinations appear frequently and extract rules showing purchase associations.

  • Create a new cell (Cell 22) and run the Apriori algorithm by running this code:
# Cell 22: Run Apriori and Extract Association Rules
# Find frequent itemsets (products bought together)
frequent_items = apriori(basket_df, min_support=0.2,
                         use_colnames=True)

# Generate association rules with confidence threshold
rules = association_rules(frequent_items,
                          metric='lift',
                          min_threshold=1.0,
                          num_itemsets=len(basket_df))

# Sort by lift (strongest associations first)
rules = rules.sort_values('lift', ascending=False)

print(f"Found {len(rules)} association rules")
print("\n=== Top 5 Product Associations ===")
for i, row in rules.head(5).iterrows():
    ant = ', '.join(list(row['antecedents']))
    con = ', '.join(list(row['consequents']))
    print(f"  {ant} -> {con}")
    print(f"    Confidence: {row['confidence']:.2f}, Lift: {row['lift']:.2f}")

What are support, confidence, and lift?

Support measures how often a product combination appears across all transactions. A min_support=0.2 means the combination must appear in at least 20% of all baskets.

Confidence measures how often the rule is correct. If confidence is 0.8 for "Electronics -> Clothing", it means 80% of customers who bought Electronics also bought Clothing.

Lift measures how much more likely products are bought together than independently. A lift above 1.0 means a real positive association exists.

  • Run the cell.

You should see the number of rules found and the top 5 product associations ranked by lift. Each rule shows which product leads to which other product being purchased, with confidence and lift scores.

Found 0 association rules?

If you see zero rules, lower min_support to 0.1 or 0.05. Some datasets have diverse product categories where no single combination is very common. Also try min_threshold=0.5 for the association rules.

Help me fix zero association rules from my Apriori analysis.

You've uncovered hidden product relationships in your data. These associations can power cross-selling strategies and product bundling. Next up, you'll predict how much each customer is worth over their lifetime.

Predict Customer Lifetime Value

You've segmented customers, found product associations, and mapped geographic patterns. The final analytical question is: how much is each customer worth over time? Customer Lifetime Value (CLV) predicts the total revenue a customer will generate during their relationship with your business.

In this step, you'll calculate CLV using your existing RFM data and a predictive formula. You'll identify which customers are worth the most investment and combine CLV with your segments to create prioritized retention strategies.

In this step, get ready to:
  • Calculate Customer Lifetime Value using purchase frequency and monetary data.
  • Rank customers by predicted lifetime value.
  • Combine CLV with RFM segments to prioritize marketing spend.
Calculate Customer Lifetime Value

CLV combines three factors: how much a customer spends per transaction (average order value), how often they buy (purchase frequency), and how long they remain active (customer lifespan). Multiplying these gives you the predicted revenue from each customer.

  • Create a new cell (Cell 23) and calculate CLV by running this code:
# Cell 23: Calculate Customer Lifetime Value (CLV)
# Compute CLV components from the original dataset
clv_data = df.groupby('Customer ID').agg(
    total_transactions=('Transaction ID', 'count'),
    total_spend=('Total Amount', 'sum'),
    first_purchase=('Date', 'min'),
    last_purchase=('Date', 'max')
).reset_index()

# Average Order Value
clv_data['avg_order_value'] = clv_data['total_spend'] / clv_data['total_transactions']

# Purchase frequency (transactions per month)
clv_data['lifespan_days'] = (clv_data['last_purchase'] - clv_data['first_purchase']).dt.days
clv_data['lifespan_months'] = clv_data['lifespan_days'] / 30.44
clv_data.loc[clv_data['lifespan_months'] == 0, 'lifespan_months'] = 1
clv_data['purchase_frequency'] = clv_data['total_transactions'] / clv_data['lifespan_months']

# CLV = Average Order Value x Purchase Frequency x 12 months
clv_data['CLV_12_months'] = (clv_data['avg_order_value'] *
                              clv_data['purchase_frequency'] * 12).round(2)

print("=== CLV Summary Statistics ===")
print(clv_data['CLV_12_months'].describe())
print(f"\nTop 5 highest-value customers:")
print(clv_data.nlargest(5, 'CLV_12_months')[['Customer ID', 'CLV_12_months', 'total_transactions', 'avg_order_value']])

What does this code do?

The formula CLV = Average Order Value x Purchase Frequency x Time Period projects revenue over 12 months. A customer who spends $50 per order and buys twice a month has a 12-month CLV of $1,200.

Customers with zero lifespan (only one purchase date on record) get a default of 1 month to avoid division by zero.

  • Run the cell.

You should see CLV summary statistics (mean, median, max) and the top 5 highest-value customers with their predicted 12-month revenue.

CLV showing NaN or all zeros?

If you see all NaN or zero values, check that your Date column is in datetime format. Run df['Date'].dtype to check. If it shows object, convert it with df['Date'] = pd.to_datetime(df['Date']).

Help me fix NaN values in my CLV calculation.

Combine CLV with RFM segments

The real power of CLV comes when you combine it with your segments. A "Champions" customer with high CLV deserves different treatment than an "At Risk" customer with high CLV. Both are valuable, but one needs retention and the other needs rewards.

  • Create a new cell (Cell 24) and combine CLV with segments by running this code:
# Cell 24: Combine CLV with RFM Segments
# Merge CLV data with segment information
clv_segments = rfm[['Customer ID', 'Segment']].merge(
    clv_data[['Customer ID', 'CLV_12_months', 'avg_order_value', 'purchase_frequency']],
    on='Customer ID'
)

# Average CLV per segment
segment_clv = clv_segments.groupby('Segment')['CLV_12_months'].agg(
    ['mean', 'median', 'sum', 'count']
).round(2).sort_values('mean', ascending=False)

print("=== CLV by Customer Segment ===")
print(segment_clv)
print(f"\nTotal projected 12-month revenue: ${clv_segments['CLV_12_months'].sum():,.2f}")
print(f"Champions alone account for: ${clv_segments[clv_segments['Segment']=='Champions']['CLV_12_months'].sum():,.2f}")

Why combine CLV with segments?

Merging CLV with segments reveals that Champions likely have the highest average CLV, confirming they deserve reward programs. At Risk customers with above-average CLV are the most urgent retention targets because their potential revenue loss is the highest.

  • Run the cell.

You should see a table showing mean, median, total, and count of CLV per segment. Champions should have the highest mean CLV, and the total projected revenue tells you the overall 12-month forecast.

  • Create a new cell (Cell 25) and visualize CLV by segment by running this code:
# Cell 25: Visualize CLV by Segment
fig, ax = plt.subplots(figsize=(10, 5))

sns.boxplot(data=clv_segments, x='Segment', y='CLV_12_months',
            palette='Set2', ax=ax,
            order=['Champions', 'Loyal Customers', 'At Risk', 'Lost'])
ax.set_title('12-Month CLV Distribution by Customer Segment')
ax.set_xlabel('Segment')
ax.set_ylabel('Predicted 12-Month CLV ($)')

plt.tight_layout()
plt.show()
  • Run the cell.

You should see a box plot showing the CLV distribution for each segment. Champions should show a higher and wider box (more valuable and more varied), while Lost customers cluster near the bottom.

You now have a complete analytical picture: customer segments, product associations, sales trends, geographic patterns, and lifetime value predictions. Next up, you'll bring all these insights together in an interactive Power BI dashboard.

Build an Interactive Dashboard in Power BI

You've mapped out who your customers are, what they buy, when they buy, and where they're located. Now it's time to put all those insights into a format stakeholders can explore on their own. Power BI Desktop lets you build interactive, filterable dashboards from the same data you already prepared.

In this step, you'll export your segmented RFM data to a CSV file, import it into Power BI Desktop, and create a polished dashboard with scatter plots, bar charts, and slicers that let viewers filter by segment.

Power BI Desktop is Windows-only

Power BI Desktop is only available on Windows. If you're on macOS or Linux, you can skip this step. Your matplotlib and seaborn visualizations from Step 5 are already portfolio-ready.

In this step, get ready to:
  • Export your RFM segmentation data to a CSV file.
  • Import the CSV into Power BI Desktop and build interactive visuals.
  • Save your finished dashboard as a .pbix file.
Export your RFM data to CSV

Power BI imports data from CSV files. You need to save your segmented DataFrame so Power BI can read it.

  • Create a new cell in your notebook (Cell 13) and export your RFM data by running this code:
# Cell 13: Export RFM data for Power BI
# Select the columns Power BI needs for the dashboard
export_df = rfm[['Customer ID', 'Recency', 'Frequency', 'Monetary',
                 'R_Score', 'F_Score', 'M_Score', 'RFM_Score',
                 'Cluster', 'Segment']].copy()

# Save to CSV without the index column
export_df.to_csv('rfm_segments.csv', index=False)
print(f"Exported {len(export_df)} rows to rfm_segments.csv")

What does this code do?

You select only the columns that matter for the dashboard and save them as a clean CSV. The index=False parameter prevents pandas from adding a row number column that Power BI would misread as data.

  • Run the cell.

You should see a message confirming the export with the row count. The CSV file now lives in the same folder as your notebook.

  • Confirm the file exists by running:
ls rfm_segments.csv

You should see rfm_segments.csv listed in the output.

File not found?

Make sure you ran Cell 13 first. If you started Jupyter from a different folder, the CSV may be saved there instead. Run import os; print(os.getcwd()) in a new cell to see the current working directory.

Help me find where my exported CSV file was saved.

Import the CSV into Power BI Desktop

Now you'll bring your segmented customer data into Power BI where you can build an interactive dashboard.

  • Press the Windows key to open your search bar.
  • Type Power BI Desktop and press Enter to open it.
  • Click Get Data in the Home tab of the ribbon.
  • Select Text/CSV from the dropdown menu.
  • Navigate to the folder containing rfm_segments.csv, select it, and click Open.
  • In the preview window, verify you see columns like Customer ID, Recency, Segment, etc.
  • Click Load to import the data into Power BI.

After loading, you should see your columns listed in the Fields pane on the right side of the screen.

Columns showing wrong data types?

If numeric columns like Monetary appear as text, click Transform Data instead of Load. In Power Query Editor, select the column header and change the type to Decimal Number. Then click Close & Apply.

Help me fix data type issues when importing my CSV into Power BI.

Create interactive visuals and save your dashboard

With your data loaded, you'll create three visuals: a scatter plot of segments, a bar chart of segment sizes, and a slicer that lets viewers filter by segment name.

  • In the Visualizations pane on the right, click the Scatter chart icon (dots in a grid pattern).
  • Drag Frequency to the X Axis well.
  • Drag Monetary to the Y Axis well.
  • Drag Segment to the Legend well. Each segment now appears as a different color.
  • Drag Customer ID to the Details well so each dot represents one customer.

You should see a color-coded scatter plot with your four segments separated across the canvas.

  • Click an empty area on the canvas to deselect the scatter chart.
  • In the Visualizations pane, click the Clustered bar chart icon.
  • Drag Segment to the Y Axis well.
  • Drag Customer ID to the X Axis well. Power BI automatically counts the IDs per segment.

You should see a horizontal bar chart showing the number of customers in each segment.

  • Click an empty area on the canvas to deselect the bar chart.
  • In the Visualizations pane, click the Slicer icon (funnel with a list).
  • Drag Segment into the slicer's Field well.

Click any segment name in the slicer. Both the scatter plot and bar chart instantly filter to show only that segment. This is the interactive power that static matplotlib charts cannot offer.

Why slicers matter for stakeholders

A marketing manager can click "At Risk" in the slicer and immediately see how many customers are slipping away and their spend patterns. No code required, no Python knowledge needed.

  • Press Ctrl+S to save your dashboard.
  • Name the file rfm_dashboard.pbix and save it to your Desktop.

You should see your file saved as rfm_dashboard.pbix on your Desktop. Your interactive dashboard is complete.

Scatter chart showing one big dot instead of individual customers?

This happens when Power BI aggregates your data. Make sure you dragged Customer ID to the Details well. If it's in the wrong well, Power BI groups all customers into one point per segment.

Help me fix my Power BI scatter chart that's showing aggregated dots.

Your RFM analysis is now presentation-ready. You've taken raw transaction data all the way through to an interactive dashboard that any stakeholder can explore without writing a line of code. Next up, the Secret Mission challenges you to add a radar chart and revenue impact analysis.

Secret mission

Radar Chart and Revenue Impact Analysis

Your segmentation analysis is solid, but interviewers want to see you go further. Can you visualize all four segments in a single radar chart and calculate the projected revenue impact of targeting each one? This extension proves you can think in business terms, not just data terms.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally with free, open-source libraries, so there are no ongoing costs.

Resources you used:

  • Project folder on your Desktop containing customer_segmentation.ipynb.
  • The retail_transactional_dataset.csv file (~26 MB) downloaded from Kaggle.
  • Python libraries installed via pip (pandas, scikit-learn, matplotlib, seaborn, jupyter).

Keep everything running

No action needed. Choose this if you plan to revisit the notebook, tweak parameters, or expand the analysis further.

  • Your notebook and dataset remain in your project folder on the Desktop.
  • The installed Python libraries stay available for any future data science projects.
  • There are no ongoing costs or background processes to worry about.

Pause - I'll come back to this later

Shut down the Jupyter Notebook server to free up memory, but keep your files for later.

  • Return to the terminal where Jupyter is running and press Ctrl+C to stop the server.
  • Confirm the shutdown when prompted.
  • Your notebook and dataset stay on disk. Relaunch anytime with jupyter notebook from the project folder.

Delete - I don't want to use this again

Remove all project files and uninstall the libraries to start fresh.

  • Stop the Jupyter server by pressing Ctrl+C in the terminal where it is running.
  • Delete the project folder by running this command from your Desktop:

macOS

cd ~/Desktop
rm -rf customer-segmentation

Windows

cd ~\Desktop
rmdir /s /q customer-segmentation
  • Uninstall the Python libraries by running:
pip uninstall pandas scikit-learn matplotlib seaborn jupyter -y
  • Confirm the files are gone by running ls ~/Desktop (macOS) or dir ~\Desktop (Windows). You should no longer see the project folder listed.

Nice Work!

Nice Work!

You just built a complete customer segmentation analysis that transforms raw retail transaction data into actionable business intelligence.

You've learned how to:

  • Clean and explore a real retail dataset, then engineer RFM features that condense each customer's behavior into comparable metrics.
  • Apply K-Means clustering with the Elbow Method to discover natural customer segments from standardized data.
  • Create portfolio-ready visualizations (scatter plots, bar charts) and deliver a recommendations table mapping each segment to a specific marketing action.
  • Secret Mission: Built a radar chart comparing segment profiles and calculated projected revenue impact with ROI estimates for each customer segment.

Ready to quiz yourself?