← Back to all projects Python · pandas · EDA

Superstore Sales: Python Analysis

Which discounts and categories help profit, and which hurt it? I cleaned 9,994 orders with reusable Python classes, explored them, and turned the patterns into recommendations.

ToolsPython · pandas · seaborn · Plotly
Data9,994 orders · 2016–2019
Approach3 reusable classes
OutputEDA · KPI report · recommendations
$2.30MTotal sales
$286KTotal profit
15.6%Average discount
12.0%Average profit margin
5,009Orders
−45.9%Memory after optimising
Walkthrough

How I approached it

Inspect the data

9,994 rows and 21 columns. No duplicates. 11 postal codes were missing (all from Burlington), so I filled them with 0 to keep the column numeric.

Why: a quick audit tells you what needs fixing before any analysis.

Build a DataCleaner class

Converts repeated text columns to the category type, trims text, and flags outliers with the IQR rule: 1,167 in Sales, 1,881 in Profit, 856 in Discount and 170 in Quantity.

Why: a class makes the cleaning reusable on any similar dataset.

Optimise memory

Changing 7 text columns to categories cut memory from 8.27 MB to 4.48 MB (a 45.9% reduction) with no loss of data.

Engineer new features

Added profit margin, shipping duration in days, and a sales performance label (Low, Medium, High) based on quartiles.

Why: these turn raw columns into things a manager can act on.

Explore

Correlations, distributions, discount analysis, monthly trend, region and sub-category comparisons.

Summarise with a KPI_Report class

One call returns total sales and profit, average discount and margin, best category and region, shipping time and order count.

Why: stakeholders get the headline numbers without reading code.

Code sample

A reusable cleaning class

python · DataCleaner
class DataCleaner:

    def __init__(self, df):
        self.df = df.copy()

    def convert_to_category(self, cat_columns):
        for col in cat_columns:
            try:
                self.df[col] = self.df[col].astype("category")
            except KeyError:
                print(f"Column '{col}' not found, skipped.")
        return self.df

    def clean_text_columns(self, text_columns):
        for col in text_columns:
            try:
                self.df[col] = self.df[col].astype(str).str.strip()
            except KeyError:
                print(f"Column '{col}' not found, skipped.")
        return self.df

    def detect_outliers_iqr(self, numeric_columns):
        report = {}
        for col in numeric_columns:
            try:
                Q1, Q3 = self.df[col].quantile([0.25, 0.75])
                IQR = Q3 - Q1
                lower, upper = Q1 - 1.5 * IQR, Q3 + 1.5 * IQR
                mask = (self.df[col] < lower) | (self.df[col] > upper)
                report[col] = mask.sum()
            except Exception as e:
                print(f"Error checking '{col}': {e}")
        return report

Checking the worst offender: Binders

python · discount analysis
total_binders = superstore[superstore["Sub-Category"] == "Binders"].shape[0]
high_discount_binders = superstore[(superstore["Sub-Category"] == "Binders") & (superstore["Discount"] >= 0.7)].shape[0]

binders_normal_profit = superstore[(superstore["Sub-Category"] == "Binders") & (superstore["Discount"] < 0.7)]["Profit"].sum()
binders_high_discount_profit = superstore[(superstore["Sub-Category"] == "Binders") & (superstore["Discount"] >= 0.7)]["Profit"].sum()
Charts

What the data looks like

Correlation heatmap
Correlation heatmapDiscount and profit margin have a strong inverse relationship (−0.86), the strongest in the dataset.
Discount vs profit
Discount vs profitBeyond roughly 50% discount, maximum profit drops and losses grow, down to −$6,600 at 70–80%.
Monthly sales trend
Monthly sales trendSales peak every November and December, with the highest point in November 2019 (about $118K).
Sales by region
Sales by regionSouth lags well behind West: about $390K against $725K.
Top 10 sub-categories by sales
Top 10 sub-categories by salesPhones and Chairs lead at about $330K each.
Findings

What I found

🏷️

Discounting erodes profit

Discount vs profit margin has a −0.86 correlation. Furniture has the highest average discount (17.4%) and the lowest margin (3.9%).

📎

Binders: the biggest discount problem

40.2% of Binders orders (613 of 1,523) carry a 70%+ discount. They lose $38,510, against $68,732 earned from normally discounted ones.

🧭

South region underperforms

About $390K in sales, nearly half of West, so it is worth asking whether this is demand or market coverage.

🎄

Year-end seasonality

Every year (2016–2019) peaks in Nov/Dec, a predictable pattern.

💻

Technology is the benchmark

The largest share of profit (51%) from the smallest share of volume (18%) and a low discount share.

🚚

Shipping is not a risk

Delivery takes 0–7 days with low variance, and it doesn’t correlate with anything else.

Recommendations

What I would do

1

Review discounts on Furniture and Binders

Revisit pricing or cap discounts. This is where the biggest losses sit.

2

Cap Binders discounts

The single largest source of discount-driven loss in the dataset.

3

Investigate the South region

Check demand, customer coverage and market access.

4

Copy the Technology model

High margin with low discounting is the benchmark for other categories.

5

Plan for Nov/Dec

Prepare inventory and staffing ahead of the recurring year-end peak.

Project files

Download the notebook