Analyze Online Retail Data with Python

Clean and analyze 541,909 real e-commerce transactions with pandas and seaborn.

Introduction

30 Second Summary

Every online retailer sits on thousands of transactions. The ones who grow fastest are the ones who can read the story their data tells about which products sell, when customers buy, and where revenue spikes.

In this project, you will analyze 541,909 real e-commerce transactions from a UK gift retailer. You will clean messy data, uncover revenue trends, and build publication-quality visualizations that tell a business story.

What You'll Build

A Jupyter Notebook you can run end-to-end and walk someone through in a data analyst interview, showing exactly how you think about messy data and turn it into actionable business insights.

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

  • A monthly revenue trend line chart that reveals a clear seasonal spike heading into the holiday shopping season.
  • A top products and countries breakdown identifying which items and markets drive the most revenue.
  • A data-backed business recommendation connecting your findings to inventory planning and marketing decisions.
  • Secret Mission: RFM customer segmentation that identifies your most valuable customers by purchasing behavior.

Are there any prerequisites?

This project assumes you have basic Python experience and have worked with pandas before (loading data, filtering rows, selecting columns). You should also be comfortable creating simple matplotlib plots.

Before We Start

You already have Python, pandas, and matplotlib ready to go. To complete this project, you need two more libraries and a real-world dataset to analyze.

In this step, you'll install seaborn for statistical visualization and openpyxl for reading Excel files. You'll also download the UCI Online Retail dataset and set up your Jupyter Notebook.

In this step, get ready to:
  • Install seaborn and openpyxl.
  • Download and extract the UCI Online Retail dataset.
  • Create a Jupyter Notebook and verify your setup.
Install seaborn and openpyxl

Your existing pandas installation can read CSV files natively. This dataset is an Excel file (.xlsx), so pandas needs the openpyxl engine to parse it. Seaborn adds expressive statistical plots on top of matplotlib.

  • Install both libraries by running this command in your terminal:
pip install seaborn==0.13.2 openpyxl==3.1.5

What does this command do?

This installs exact versions of each library. Pinning versions (==0.13.2) ensures your results match this project's instructions. openpyxl is the engine pandas uses behind the scenes when you call pd.read_excel().

  • Verify seaborn installed correctly by running:
python -c "import seaborn; print(seaborn.__version__)"
  • You should see 0.13.2 printed in your terminal.

Seeing a ModuleNotFoundError?

Make sure you ran the pip install command in the same Python environment where you normally run Jupyter. If you use a virtual environment or conda, activate it first, then re-run the install command.

Help me figure out why seaborn didn't install in the right Python environment.

Download the UCI Online Retail dataset

The UCI Online Retail dataset contains 541,909 real e-commerce transactions from a UK-based gift retailer. It is a free, publicly available dataset commonly used for data analysis practice.

  • Create a working folder for this project. For example, create a folder called data-analysis-project on your Desktop or Documents.
  • Open https://archive.ics.uci.edu/dataset/352/online+retail in your browser.
  • Click the Download button on the page (22.6 MB zip file).
  • Extract the zip file. You'll get a file called Online Retail.xlsx.
  • Move Online Retail.xlsx into your data-analysis-project folder.
  • Confirm the file is in place by opening your data-analysis-project folder in Finder (macOS) or File Explorer (Windows). You should see Online Retail.xlsx listed there.

Why an Excel file?

Many real-world business datasets live in Excel spreadsheets. Knowing how to load .xlsx files directly into pandas is a common interview-relevant skill. That's exactly why you installed openpyxl in the previous substep.

Create your notebook and verify imports

Your analysis will live in a Jupyter Notebook. Each cell runs independently, so you can iterate on code without re-running the entire script.

  • Open a terminal and navigate to your data-analysis-project folder by running:
cd ~/Desktop/data-analysis-project
  • Launch Jupyter Notebook by running:
jupyter notebook
  • Your browser opens to the Jupyter file browser. Click New, then select Python 3 to create a new notebook.
  • Click the notebook title at the top (it says Untitled) and rename it to retail_analysis.

Before you run any code, what do you think will happen when you import these libraries? Will you see any output, or just silence?

  • In the first cell of your notebook, paste the following code and press Shift+Enter to run it:
# Cell 1: Setup and imports
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

print(f"pandas: {pd.__version__}")
print(f"seaborn: {sns.__version__}")
  • You should see version numbers printed below the cell, confirming both libraries loaded successfully:
pandas: 2.x.x
seaborn: 0.13.2

What does this code do?

The import lines load each library with a short alias (pd, plt, sns). These aliases are the standard convention in data analysis. The print statements confirm the imports worked by displaying each library's version.

Not seeing version output?

If you see an ImportError, the notebook may be using a different Python kernel than where you ran pip install. In the notebook menu, click Kernel, then Change kernel, and select the Python environment where you installed seaborn.

Help me fix an ImportError when running my Jupyter Notebook cell.

✔️ Awesome, I've got everything!

Great. Double check you've saved your notebook by pressing Ctrl+S (Windows) or Cmd+S (macOS).

ⓧ I'd like to double check the full code

Here is the complete Cell 1 code. Make sure your cell matches this exactly:

# Cell 1: Setup and imports
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

print(f"pandas: {pd.__version__}")
print(f"seaborn: {sns.__version__}")

Your environment is ready. Next up, you'll load the dataset and get your first look at over half a million real e-commerce transactions.

Load and Inspect the Raw Data

Your environment is set up and ready to go. Now it's time to meet your data.

Before you can clean or analyze anything, you need to understand what you're working with. Running inspection commands reveals the shape, data types, and quality issues hiding in the raw dataset.

In this step, get ready to:
  • Load the raw Excel file and assess its shape.
  • Investigate null values, negative quantities, and cancellations.
  • Add a Revenue column to spot outliers in the data.
Load the Excel file and inspect its structure

The dataset lives in an Excel file with over half a million rows. Loading it and running .info() tells you the column names, data types, and how many values are missing in each column.

  • In your notebook, add a new cell below your imports and run this code to load and inspect the dataset:
# Cell 2: Load and inspect
df = pd.read_excel("Online Retail.xlsx")
print(f"Shape: {df.shape}")
df.info()
df.head()

What does this code do?

  • pd.read_excel() reads the .xlsx file into a DataFrame (this uses openpyxl under the hood to parse the Excel format).
  • df.info() prints the column names, data types, and non-null counts for each column.
  • df.head() displays the first five rows so you can see what the actual data looks like.

Before you look at the output, predict: do you think every column will have the same number of non-null entries?

You should see Shape: (541909, 8) confirming 541,909 rows and 8 columns. In the df.info() output, notice that CustomerID shows approximately 406,829 non-null entries while other columns show 541,909. That gap tells you roughly 135,000 rows are missing a customer ID.

Seeing a FileNotFoundError?

  • Make sure Online Retail.xlsx is in the same folder as your notebook. The filename includes a space.
  • Confirm you extracted the zip file fully. The .xlsx file should be about 22.6 MB.

Why can't my notebook find the Excel file?

Investigate data quality problems

You already spotted that CustomerID has missing values. Now you need to quantify the full picture: how many nulls exist per column, how many rows have negative quantities, and how many invoices are actually cancellations.

  • Quantify the data quality issues by adding a new cell and running this code:
# Cell 3: Investigate data quality
print(f"\nNull values:\n{df.isnull().sum()}")
print(f"\nNegative quantities: {(df['Quantity'] < 0).sum()}")
print(f"\nInvoices starting with 'C' (cancellations): {df['InvoiceNo'].astype(str).str.startswith('C').sum()}")

What do these checks reveal?

  • df.isnull().sum() counts missing values per column. CustomerID and Description have significant nulls.
  • Negative quantities represent returns or adjustments that weren't formally cancelled.
  • Invoices starting with C are cancellations. These would inflate your transaction counts and distort revenue if left in.

Run the cell. You should see null counts printed for each column, a count of negative quantities (in the thousands), and a count of cancellation invoices.

Seeing a KeyError?

  • Check that you ran Cell 2 first. The variable df must exist before you can query its columns.
  • Column names are case-sensitive. The dataset uses Quantity and InvoiceNo (no space, capital letters).

Help me debug this KeyError in my data quality check.

Add a Revenue column to spot outliers

Neither Quantity nor UnitPrice alone tells you how much money a transaction generated. Multiplying them creates a Revenue column that makes financial outliers visible immediately.

  • Create the Revenue column and view its summary statistics by adding a new cell and running this code:
# Cell 4: Add Revenue column
df['Revenue'] = df['Quantity'] * df['UnitPrice']
print(f"\nRevenue stats:\n{df['Revenue'].describe()}")

What does this reveal?

The .describe() output shows the min, max, mean, and percentiles of Revenue. You should see negative values in the minimum. Those come from the cancellations and negative-quantity rows you just identified.

This confirms why cleaning matters: any analysis you run right now would be distorted by these bad rows.

Before you look at the output, predict: will the minimum Revenue value be positive or negative?

Run the cell. The minimum Revenue is negative, confirming that cancellations and returns are producing impossible revenue figures. This is exactly what you will fix in the next step.

All Revenue values showing as zero?

  • Make sure you're using the multiplication operator * between df['Quantity'] and df['UnitPrice']. A typo in the column name would produce a KeyError, not zeros.
  • Confirm Cell 2 ran successfully. If the load failed silently, df might be empty.

Help me debug my Revenue column calculation.

✔️ Awesome, I've got everything!

Great. Double check you've saved your notebook.

ⓧ I'd like to double check the full code

Here's the complete notebook code up to this point. Compare each cell against yours:

# Cell 1: Setup and imports
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

print(f"pandas: {pd.__version__}")
print(f"seaborn: {sns.__version__}")

# Cell 2: Load and inspect
df = pd.read_excel("Online Retail.xlsx")
print(f"Shape: {df.shape}")
df.info()
df.head()

# Cell 3: Investigate data quality
print(f"\nNull values:\n{df.isnull().sum()}")
print(f"\nNegative quantities: {(df['Quantity'] < 0).sum()}")
print(f"\nInvoices starting with 'C' (cancellations): {df['InvoiceNo'].astype(str).str.startswith('C').sum()}")

# Cell 4: Add Revenue column
df['Revenue'] = df['Quantity'] * df['UnitPrice']
print(f"\nRevenue stats:\n{df['Revenue'].describe()}")

You now have a clear picture of what's wrong with the raw data: missing customer IDs, hidden cancellations, and negative quantities producing impossible revenue. Next up, you'll clean all of this systematically so your analysis rests on solid ground.

Clean the Data

In the last step, you loaded the raw dataset and uncovered its quality problems: missing CustomerIDs, cancellations hiding as negative quantities, and zero-price adjustments.

Now you need to fix these problems before any analysis. Cleaning is the step interviewers care about most because it shows you make deliberate, documented decisions rather than blindly dropping rows.

In this step, get ready to:
  • Filter out cancellations, null CustomerIDs, and price anomalies.
  • Parse dates and add time-based columns for trend analysis.
  • Check for duplicates and verify your cleaned dataset.
Filter out cancellations and anomalies

Each filter you apply is a deliberate decision. Cancellations inflate transaction counts, null CustomerIDs prevent customer-level analysis, and zero-price rows represent internal adjustments.

  • Create a new cell in your notebook and add the cleaning logic by pasting this code:
# Cell 5: Data cleaning
df_clean = df.copy()
# Remove cancellations (InvoiceNo starting with 'C')
df_clean = df_clean[~df_clean['InvoiceNo'].astype(str).str.startswith('C')]
# Remove rows with missing CustomerID (guest checkouts, can't attribute to a customer)
df_clean = df_clean.dropna(subset=['CustomerID'])
# Remove zero or negative prices (adjustments, not real sales)
df_clean = df_clean[df_clean['UnitPrice'] > 0]
# Remove negative quantities (returns that weren't caught by cancellation filter)
df_clean = df_clean[df_clean['Quantity'] > 0]
# Recalculate Revenue on clean data
df_clean['Revenue'] = df_clean['Quantity'] * df_clean['UnitPrice']
print(f"Rows before: 541909, Rows after: {len(df_clean)}")
print(f"Null CustomerIDs remaining: {df_clean['CustomerID'].isnull().sum()}")
print(f"Negative Revenue remaining: {(df_clean['Revenue'] <= 0).sum()}")

What does this code do?

Each line applies one filter, and the inline comments document why you made that choice.

  • df.copy() creates a separate DataFrame so you preserve the original raw data for reference.
  • The cancellation filter removes invoices starting with C because the UCI dataset uses that prefix to indicate cancelled orders.
  • Dropping null CustomerID rows removes guest checkouts that cannot be attributed to a specific customer.
  • Revenue is recalculated after filtering so it only reflects valid, positive transactions.

Before you run this cell, how many rows do you think will survive all four filters?

  • Run the cell by pressing Shift+Enter.

You should see output showing approximately 390,000 rows remaining, 0 null CustomerIDs, and 0 negative Revenue values.

Seeing a NameError for df?

Make sure you ran all previous cells (Cells 1 through 4) before running Cell 5. Jupyter cells must be executed in order for variables to be available.

help me debug a NameError in my Jupyter Notebook cleaning cell

Parse dates and add time columns

To analyze revenue trends over time, you need proper datetime objects and columns that group by month or day of week.

  • Create a new cell and add the date parsing logic by pasting this code:
# Cell 6: Parse dates and add time columns
df_clean['InvoiceDate'] = pd.to_datetime(df_clean['InvoiceDate'])
df_clean['YearMonth'] = df_clean['InvoiceDate'].dt.to_period('M')
df_clean['Month'] = df_clean['InvoiceDate'].dt.month
df_clean['DayOfWeek'] = df_clean['InvoiceDate'].dt.day_name()
print(f"Date range: {df_clean['InvoiceDate'].min()} to {df_clean['InvoiceDate'].max()}")

What does this code do?

  • pd.to_datetime() converts the string dates into proper datetime objects that pandas can sort and group.
  • YearMonth groups transactions into monthly periods for trend analysis.
  • Month and DayOfWeek let you spot patterns like seasonal spikes or quiet weekdays.
  • Run the cell by pressing Shift+Enter.

You should see the date range printed as December 2010 to December 2011.

Seeing a ParserError or date format issue?

Make sure you ran Cell 5 first so that df_clean exists. The InvoiceDate column should already be in a recognizable date format from the original Excel file.

help me fix a date parsing error in my pandas DataFrame

Check for duplicates and verify the cleaned dataset

Duplicate rows can inflate counts and revenue totals. This final check ensures your analytical layer is trustworthy.

  • Create a new cell and add the duplicate check by pasting this code:
# Cell 7: Remove duplicates
dupes = df_clean.duplicated().sum()
print(f"Duplicate rows: {dupes}")
if dupes > 0:
    df_clean = df_clean.drop_duplicates()
    print(f"After removing duplicates: {len(df_clean)}")
df_clean.describe()

What does this code do?

  • .duplicated().sum() counts rows that are exact copies of another row across all columns.
  • The conditional block only drops duplicates if any exist, so the code handles both cases cleanly.
  • .describe() at the end gives you a statistical summary of the cleaned data, confirming all values look reasonable.
  • Run the cell by pressing Shift+Enter.

You should see the duplicate count printed, followed by a describe() table showing all-positive Quantity, UnitPrice, and Revenue statistics.

Revenue min still shows a negative number?

Go back to Cell 5 and confirm the line df_clean = df_clean[df_clean['Quantity'] > 0] is present. Then re-run Cells 5, 6, and 7 in order.

help me figure out why my cleaned DataFrame still has negative values

✔️ Awesome, I've got everything!

Great. Double check you have saved your notebook.

ⓧ I'd like to double check the full code

Your notebook should now have 7 cells in this order. Scroll through and confirm each one is present:

  • Cell 1: Setup and imports (import pandas, matplotlib, seaborn).
  • Cell 2: Load and inspect (read_excel, shape, info, head).
  • Cell 3: Investigate data quality (null counts, negative quantities, cancellation count).
  • Cell 4: Add Revenue column (Quantity * UnitPrice, describe).
  • Cell 5: Data cleaning (copy, 4 filters, recalculate Revenue, print verification).
  • Cell 6: Parse dates and add time columns (to_datetime, YearMonth, Month, DayOfWeek, print date range).
  • Cell 7: Remove duplicates (duplicated check, conditional drop, describe).

Your data is clean, documented, and ready for analysis. Next up, you will use this cleaned DataFrame to uncover revenue trends and identify top-performing products.

Analyze Revenue Trends and Product Performance

Your data is clean and analysis-ready. You made deliberate decisions about cancellations, null customers, and pricing anomalies.

Now comes the analytical thinking that separates a data analyst from a business analyst who would stop at "the data is clean." With groupby aggregations and time-series analysis, you'll answer: which months drive the most revenue, and which products are top performers?

In this step, get ready to:
  • Calculate monthly revenue trends across 13 months of transactions.
  • Identify the top 10 products and top 10 countries by revenue.
  • Compute month-over-month growth rates to spot seasonal patterns.
Calculate monthly revenue trends

The first question any stakeholder asks about revenue is: "how is it trending over time?" A monthly aggregation reveals seasonality patterns that raw transaction rows never could.

  • Create a new cell in your notebook (Cell 8) and calculate monthly revenue by running this code:
# Cell 8: Monthly revenue
monthly_revenue = df_clean.groupby('YearMonth')['Revenue'].sum().reset_index()
monthly_revenue['YearMonth'] = monthly_revenue['YearMonth'].astype(str)
print("Monthly Revenue:")
print(monthly_revenue.to_string())

What does this code do?

This groups every transaction by its YearMonth period, sums the Revenue within each month, and resets the index to get a flat DataFrame.

The .astype(str) conversion is needed because pandas Period objects don't play nicely with seaborn's plotting functions. Converting to strings now saves trouble later.

Before you run this cell, what do you expect to see? Think about the date range of the dataset (December 2010 to December 2011). How many months should appear?

  • Run the cell.

You see a DataFrame with 13 months of revenue data. Look at November 2011. That spike is the holiday shopping season in action.

Seeing a TypeError or empty output?

  • Make sure you ran all previous cells in order. The YearMonth column was created in Cell 6 during cleaning.
  • If you restarted your kernel, re-run all cells from Cell 1 onwards before running this one.

Why is my monthly revenue cell returning an error?

Identify top products and countries by revenue

Monthly trends show the "when." Now you need the "what" and "where." Knowing which products and countries generate the most revenue tells a stakeholder exactly where to focus inventory and marketing spend.

  • Create a new cell (Cell 9) and find the top performers by running this code:
# Cell 9: Top products and countries
top_products = (df_clean.groupby('Description')['Revenue']
                .sum()
                .sort_values(ascending=False)
                .head(10)
                .reset_index())
top_products.columns = ['Product', 'TotalRevenue']
print("\nTop 10 Products by Revenue:")
print(top_products.to_string())

top_countries = (df_clean.groupby('Country')['Revenue']
                 .sum()
                 .sort_values(ascending=False)
                 .head(10)
                 .reset_index())
top_countries.columns = ['Country', 'TotalRevenue']
print("\nTop 10 Countries by Revenue:")
print(top_countries.to_string())

What does this code do?

  • The first block groups by Description (product name), sums revenue, sorts highest-first, and takes the top 10. Renaming columns to Product and TotalRevenue makes the output readable.
  • The second block does the same aggregation grouped by Country. This reveals geographic concentration.
  • Run the cell.

You see two ranked tables. Notice how the United Kingdom dominates the country list. This is a UK-based retailer, so that's expected. The interesting insight is which countries come next.

Seeing unexpected product names?

  • Some product descriptions in this dataset include adjustment entries or manual corrections. Your cleaning removed most of these, but a few may remain at the top.
  • If you see blank or generic descriptions at the top of your product list, verify that your Cell 5 cleaning code ran successfully.

Help me understand my top products output.

Calculate month-over-month growth rate

Absolute revenue numbers show scale, but growth rates show momentum. A stakeholder wants to know: "are we growing or shrinking month to month?" The pct_change() method computes this in one line.

  • Create a new cell (Cell 10) and calculate growth rates by running this code:
# Cell 10: Month-over-month growth
monthly_revenue_series = df_clean.groupby('YearMonth')['Revenue'].sum()
mom_growth = monthly_revenue_series.pct_change() * 100
print("\nMonth-over-Month Growth (%):")
print(mom_growth)

What does this code do?

  • Unlike Cell 8 which used .reset_index() to get a DataFrame, here we keep the result as a Series (no reset). This makes pct_change() work directly on the values.
  • Multiplying by 100 converts the decimal ratio (e.g., 0.45) into a percentage (45%). The first month shows NaN because there is no prior month to compare against.
  • Run the cell.

You see percentage growth for each month. Look for September, October, and November 2011. The large positive jumps confirm the holiday seasonality pattern you spotted in the monthly revenue table.

Why does December show a massive drop?

The dataset ends on December 9, 2011. That means December only has 9 days of data compared to a full 30 days in November. The apparent revenue "drop" is a data truncation artifact, not a real business decline. This is exactly the kind of nuance you'd call out in a DA interview.

Seeing all NaN values?

  • Only the first row should be NaN. If all rows are NaN, check that your groupby produced numeric Revenue values. Re-run Cell 5 to ensure the Revenue column is properly calculated.

Help me debug my month-over-month growth calculation.

✔️ Awesome, I've got everything!

Great. Confirm you have saved your notebook with all three new cells (Cells 8, 9, and 10).

ⓧ I'd like to double check the full code

Scroll through your notebook and confirm these cells are present in order:

  • Cells 1-7 from the previous steps (Setup, Load, Investigate, Revenue, Cleaning, Dates, Duplicates).
  • Cell 8: Monthly revenue. Groups by YearMonth, sums Revenue, converts YearMonth to string, and prints the table.
  • Cell 9: Top products and countries. Two groupby aggregations producing top_products and top_countries DataFrames, each with 10 rows.
  • Cell 10: Month-over-month growth. Creates monthly_revenue_series and mom_growth using pct_change() multiplied by 100.

You now have the numbers that tell the story. But numbers in tables are hard for stakeholders to absorb at a glance. Next up, you'll turn these findings into publication-quality visualizations with seaborn.

Build a Visual Story with Seaborn

You have monthly revenue totals, top product rankings, and growth rates. But raw numbers in tables are hard for stakeholders to interpret at a glance.

Seaborn produces publication-quality statistical plots with minimal code. In this step, you will turn your findings into a visual narrative that a stakeholder can understand in seconds. This is the "so what" that makes an analysis actionable.

In this step, get ready to:
  • Create a monthly revenue line plot showing the seasonal trend.
  • Build side-by-side bar charts comparing top products and countries.
  • Create a revenue heatmap and write a data-backed business recommendation.
Set up the seaborn style and create the monthly revenue line plot

Before plotting, you will set a global seaborn theme so every chart in your notebook has a consistent, professional look. The whitegrid style adds subtle gridlines that make values easy to read without cluttering the chart.

  • Create a new code cell in your notebook and paste the following code to set the theme and plot monthly revenue:
sns.set_theme(style="whitegrid", palette="muted")

fig, ax = plt.subplots(figsize=(12, 5))
sns.lineplot(data=monthly_revenue, x='YearMonth', y='Revenue', marker='o', ax=ax)
ax.set_title('Monthly Revenue Trend (Dec 2010 - Dec 2011)', fontsize=14)
ax.set_xlabel('Month')
ax.set_ylabel('Revenue (GBP)')
ax.tick_params(axis='x', rotation=45)
plt.tight_layout()
plt.show()

What does this code do?

  • sns.set_theme(style="whitegrid", palette="muted") sets the global style for all seaborn plots. Every chart after this line inherits the same clean look.
  • sns.lineplot() draws a line chart directly from your monthly_revenue DataFrame. The marker='o' adds dots at each data point so individual months are easy to spot.
  • ax.tick_params(axis='x', rotation=45) rotates the month labels so they do not overlap each other.
  • Run the cell.

You should see a line chart with circular markers showing revenue for each month from December 2010 to December 2011. Notice the clear upward spike in September through November.

Not seeing a chart?

  • Make sure you ran all previous cells first (Cells 1 through 10). The chart depends on the monthly_revenue DataFrame created in Cell 8.
  • If you see a NameError for monthly_revenue, go back and re-run Cell 8.
  • Help me debug my seaborn line plot
Create the product and country comparison charts

A side-by-side layout lets your reader compare two related rankings without scrolling. You will place the top products and top countries charts next to each other in a single figure.

  • Create a new code cell and paste the following code to build the side-by-side bar charts:
fig, axes = plt.subplots(1, 2, figsize=(14, 6))

sns.barplot(data=top_products, x='TotalRevenue', y='Product', ax=axes[0], orient='h')
axes[0].set_title('Top 10 Products by Revenue')
axes[0].set_xlabel('Revenue (GBP)')

sns.barplot(data=top_countries, x='TotalRevenue', y='Country', ax=axes[1], orient='h')
axes[1].set_title('Top 10 Countries by Revenue')
axes[1].set_xlabel('Revenue (GBP)')

plt.tight_layout()
plt.show()

What does this code do?

  • plt.subplots(1, 2) creates a figure with two side-by-side panels. The axes array lets you target each panel independently.
  • sns.barplot() with orient='h' draws horizontal bars. Horizontal bars work better for long product names because the labels remain readable.
  • plt.tight_layout() automatically adjusts spacing so titles and labels never overlap between the two panels.
  • Run the cell.

You should see two horizontal bar charts side by side. The left chart shows the top 10 products ranked by revenue. The right chart shows the top 10 countries ranked by revenue, with the UK dominating.

Charts looking squished or overlapping?

  • Check that your figsize is set to (14, 6). A smaller figure will cause labels to overlap.
  • Make sure plt.tight_layout() appears before plt.show(). Without it, matplotlib does not auto-adjust spacing.
  • Help me fix my side-by-side bar charts
Create a revenue heatmap and write your business recommendation

A heatmap reveals patterns across two dimensions simultaneously. You will build one that shows revenue by day of week and month, making it easy to spot which days and seasons drive the most sales.

  • Create a new code cell and paste the following code to build the revenue heatmap:
# Heatmap: Revenue by DayOfWeek x Month
heatmap_data = df_clean.pivot_table(
    values='Revenue', index='DayOfWeek', columns='Month', aggfunc='sum'
)
day_order = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Sunday']
heatmap_data = heatmap_data.reindex(day_order)

fig, ax = plt.subplots(figsize=(12, 5))
sns.heatmap(heatmap_data, annot=True, fmt='.0f', cmap='YlOrRd', ax=ax)
ax.set_title('Revenue by Day of Week and Month')
ax.set_xlabel('Month')
ax.set_ylabel('Day of Week')
plt.tight_layout()
plt.show()

What does this code do?

  • pivot_table() reshapes your flat DataFrame into a grid where rows are days and columns are months. Each cell contains the total revenue for that day-month combination.
  • day_order excludes Saturday because this UK retailer has no Saturday transactions in the dataset. Reindexing ensures days display in a logical order rather than alphabetical.
  • annot=True, fmt='.0f' prints the rounded revenue number inside each cell. The cmap='YlOrRd' color scale moves from yellow (low) to red (high), drawing your eye to peak revenue cells.

Before you run this cell, what pattern do you expect to see? Think about which months had the highest revenue in your line plot.

  • Run the cell.

You should see a color-coded grid with annotated numbers. The darkest red cells cluster in the November and October columns, confirming the seasonal spike you saw in the line plot.

Why is Saturday missing?

This UK-based gift retailer does not operate on Saturdays. There are zero Saturday transactions in the entire dataset. Excluding it from your heatmap is a data-aware decision that avoids a misleading empty row.

The final piece of a professional analysis is a written recommendation that connects your visual findings to a business action. You will add this as a Markdown cell below your heatmap.

  • In your Jupyter Notebook, click the + button to add a new cell below your heatmap cell.
  • Change the cell type from Code to Markdown using the dropdown in the toolbar.
  • Paste the following content into the Markdown cell:
## Key Findings & Recommendation

1. **Seasonality**: Revenue peaks sharply in September-November, driven by holiday gift purchasing. December drops because the dataset ends on Dec 9, 2011.
2. **Product concentration**: The top 10 products contribute a disproportionate share of revenue. Stock availability for these items during peak months is critical.
3. **Geographic focus**: The UK dominates revenue, but Germany and France represent growth opportunities.

**Recommendation**: Increase inventory of top-10 products by August to capture the September-November surge. Consider targeted marketing in Germany and France during Q3.

Why write a recommendation in your notebook?

A notebook that ends with charts and no interpretation is incomplete. Stakeholders want to know "so what?" Your recommendation shows you can translate data patterns into business action. This is what separates a data analyst from someone who just runs code.

  • Run the Markdown cell by pressing Shift+Enter.

You should see your recommendation rendered as formatted text with bold headings and a numbered list.

Seeing raw markdown text instead of formatted output?

  • Make sure the cell type is set to Markdown in the toolbar dropdown, not Code. If it says Code, the cell will try to execute the text as Python.
  • Double-click the cell to re-enter edit mode, confirm the cell type, then press Shift+Enter again to render it.
  • Help me render my Markdown cell in Jupyter

✔️ Awesome, I've got everything!

Great. Confirm you have saved your notebook with all three visualization cells (line plot, bar charts, heatmap) plus the Markdown recommendation cell.

ⓧ I'd like to double check the full code

Scroll through your notebook and confirm these cells are present in order:

  • Cell 11: Monthly revenue line plot with sns.set_theme() and sns.lineplot().
  • Cell 12: Side-by-side bar charts with plt.subplots(1, 2) and two sns.barplot() calls.
  • Cell 13: Revenue heatmap with pivot_table(), day_order reindex, and sns.heatmap().
  • Markdown cell: Key Findings & Recommendation with three numbered findings and a bold recommendation.

Secret mission

Customer Segmentation with RFM Analysis

Your analysis tells you what products sell and when revenue peaks. But who are your best customers? RFM analysis segments every customer by how recently they bought, how often they buy, and how much they spend. Add it to your notebook to identify your Champions and your At-Risk customers.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your files and packages for future exploration, or delete them to free up disk space.

Resources you used:

  • Project folder containing retail_analysis.ipynb and Online Retail.xlsx.
  • Installed Python packages: seaborn and openpyxl.

Keep everything running

No action needed. Choose this if you plan to continue exploring the dataset, adding more visualizations, or extending the RFM analysis.

  • Your notebook and dataset stay in your project folder, ready to reopen anytime.
  • The seaborn and openpyxl packages remain installed in your Python environment.

Pause - I'll come back to this later

Shut down Jupyter Notebook to free up memory. Your files stay on disk so you can pick up where you left off.

  • In the terminal where Jupyter is running, press Ctrl+C to shut down the notebook server.
  • Your retail_analysis.ipynb file saves all cell outputs, so your charts and results are preserved.
  • To resume later, navigate back to your project folder in the terminal and run jupyter notebook.

Delete - I don't want to use this again

Remove all project files and uninstall the packages you added during this project.

  • Shut down Jupyter Notebook by pressing Ctrl+C in the terminal where it is running.
  • Delete your project folder by running the appropriate command for your system.

macOS

rm -rf ~/Desktop/data-analysis-project

Windows

rmdir /s /q %USERPROFILE%\Desktop\data-analysis-project
  • Uninstall the packages you installed for this project by running:
pip uninstall seaborn openpyxl -y

This removes only the two packages added during this project. Your existing pandas and matplotlib installations are unaffected.

Nice Work!

Nice Work!

You just completed an end-to-end exploratory data analysis of over half a million real e-commerce transactions. You went from a messy Excel file to a polished notebook with documented cleaning decisions, business insights, and publication-quality visualizations.

You've learned how to:

  • Made deliberate, documented data cleaning decisions on a real-world dataset: removing cancellations, null customers, and price anomalies while explaining why each filter matters.
  • Computed monthly revenue trends, identified top-performing products and countries, and calculated month-over-month growth rates using groupby aggregations.
  • Built a visual story with seaborn including line plots, horizontal bar charts, and a revenue heatmap, then wrote a data-backed business recommendation connecting findings to inventory and marketing decisions.
  • Secret Mission: Segmented customers using RFM analysis to identify Champions, At-Risk, and Lost customer groups with quantile-based scoring.

Ready to quiz yourself?