Mathew K Analytics

Lesson 38 · Python for Retail E-commerce Analytics

Channel and Regional Sales Analysis Using Python for Retail E-commerce Insights

In this lesson, we will analyze retail sales data by sales channel and region. Understanding sales performance by channel (such as online versus offline)…

⬇ Download notebookOpen in Colab ↗

What you'll learn

Data

No separate download needed — the notebook creates or downloads everything it uses.

📓 Full notebook

Download .ipynb

Channel and Regional Sales Analysis#

  • In this lesson, we will analyze retail sales data by sales channel and region.
  • Understanding sales performance by channel (such as online versus offline) and by geographic region helps drive targeted marketing and inventory decisions.
  • You will learn to identify which sales channels and regions generate the most revenue and spot opportunities for business growth.
  • The analysis produces actionable insights for optimizing marketing spend, inventory allocation, and sales strategies.
import pandas as pd
import numpy as np
import warnings
warnings.filterwarnings('ignore')

Core Concepts: Retail Channel and Regional Analysis#

  • Retail datasets capture transactions, orders, products and customer information.
  • Revenue is calculated as Quantity times Price per transaction.
  • Channels typically refer to online versus offline sales, while regions are geographic segments such as countries or cities.
  • Beginners often confuse revenue with quantity or overlook grouping by both channel and region.
  • Accurate groupings and aggregations are vital for correct business insights.
# Load the Online Retail Transactions dataset (UCI)
url = 'https://archive.ics.uci.edu/ml/machine-learning-databases/00502/online_retail_II.xlsx'
df_retail = pd.read_excel(url, sheet_name='Year 2010-2011')
df_retail['InvoiceDate'] = pd.to_datetime(df_retail['InvoiceDate'])
print(df_retail.shape)
print(df_retail[['Invoice', 'StockCode', 'Description', 'Quantity', 'InvoiceDate', 'Price', 'Customer ID', 'Country']].head(3))
(541910, 8)
  Invoice StockCode                         Description  Quantity  \
0  536365    85123A  WHITE HANGING HEART T-LIGHT HOLDER         6   
1  536365     71053                 WHITE METAL LANTERN         6   
2  536365    84406B      CREAM CUPID HEARTS COAT HANGER         8   

          InvoiceDate  Price  Customer ID         Country  
0 2010-12-01 08:26:00   2.55      17850.0  United Kingdom  
1 2010-12-01 08:26:00   3.39      17850.0  United Kingdom  
2 2010-12-01 08:26:00   2.75      17850.0  United Kingdom  
# Preview unique countries to analyze regional breakdowns
countries = df_retail['Country'].unique()
print('Number of unique countries:', len(countries))
print('Sample countries:', countries[:7])
Number of unique countries: 38
Sample countries: ['United Kingdom' 'France' 'Australia' 'Netherlands' 'Germany' 'Norway'
 'EIRE']
# Add a synthetic 'Channel' column to simulate channel data
np.random.seed(42)
possible_channels = ['Online', 'Offline']
df_retail['Channel'] = np.random.choice(possible_channels, len(df_retail))
print(df_retail[['Invoice', 'Country', 'Channel']].head(5))
  Invoice         Country  Channel
0  536365  United Kingdom   Online
1  536365  United Kingdom  Offline
2  536365  United Kingdom   Online
3  536365  United Kingdom   Online
4  536365  United Kingdom   Online
# Calculate revenue per transaction row
df_retail['Revenue'] = df_retail['Quantity'] * df_retail['Price']
print(df_retail[['Quantity', 'Price', 'Revenue']].head(5))
   Quantity  Price  Revenue
0         6   2.55    15.30
1         6   3.39    20.34
2         8   2.75    22.00
3         6   3.39    20.34
4         6   3.39    20.34
# Beginner Example 1: Total sales revenue by channel
channel_sales = df_retail.groupby('Channel')['Revenue'].sum().sort_values(ascending=False)
print('Total revenue by sales channel:')
print(channel_sales)
Total revenue by sales channel:
Channel
Online     4963989.341
Offline    4783776.593
Name: Revenue, dtype: float64
# Beginner Example 2: Total sales revenue by region (country)
country_sales = df_retail.groupby('Country')['Revenue'].sum().sort_values(ascending=False)
print('Top 5 revenue-generating countries:')
print(country_sales.head(5))
Top 5 revenue-generating countries:
Country
United Kingdom    8187806.364
Netherlands        284661.540
EIRE               263276.820
Germany            221698.210
France             197421.900
Name: Revenue, dtype: float64
# Beginner Example 3: Number of sales transactions per channel
transactions_per_channel = df_retail['Channel'].value_counts()
print('Number of sales transactions per channel:')
print(transactions_per_channel)
Number of sales transactions per channel:
Channel
Offline    270993
Online     270917
Name: count, dtype: int64
# Intermediate Example 1: Average revenue per transaction, by channel
avg_revenue_channel = df_retail.groupby('Channel')['Revenue'].mean()
print('Average revenue per transaction by channel:')
print(avg_revenue_channel)
Average revenue per transaction by channel:
Channel
Offline    17.652768
Online     18.322916
Name: Revenue, dtype: float64
# Intermediate Example 2: Channel and region: total revenue pivot table
channel_region_pivot = pd.pivot_table(df_retail, values='Revenue', index='Country', columns='Channel', aggfunc='sum', fill_value=0)
print('Revenue by country and channel:')
print(channel_region_pivot.head(8))
Revenue by country and channel:
Channel           Offline    Online
Country                            
Australia        68133.02  68944.25
Austria           5342.80   4811.52
Bahrain            179.90    368.50
Belgium          20337.53  20573.43
Brazil             531.60    612.00
Canada            2282.50   1383.88
Channel Islands  10493.97   9592.32
Cyprus            6916.58   6029.71
# Intermediate Example 3: Identify regions with over 10,000 total revenue in offline sales
offline_high = channel_region_pivot[channel_region_pivot['Offline'] > 10000]
print('Regions with over 10,000 in offline sales:')
print(offline_high.index.tolist())
Regions with over 10,000 in offline sales:
['Australia', 'Belgium', 'Channel Islands', 'EIRE', 'Finland', 'France', 'Germany', 'Hong Kong', 'Japan', 'Netherlands', 'Norway', 'Portugal', 'Spain', 'Sweden', 'Switzerland', 'United Kingdom']
# Intermediate Example 4: Monthly trend of online versus offline sales
df_retail['Month'] = df_retail['InvoiceDate'].dt.to_period('M')
trend = df_retail.groupby(['Month', 'Channel'])['Revenue'].sum().unstack()
print('Monthly revenue trend by channel:')
print(trend.tail(6))
Monthly revenue trend by channel:
Channel     Offline      Online
Month                          
2011-07  346060.821  335239.290
2011-08  370382.200  312298.310
2011-09  509980.071  509707.551
2011-10  529343.740  541360.930
2011-11  738169.130  723587.120
2011-12  201002.430  232683.580
# Advanced Example 1: Find the country-channel pairs with the highest and lowest average transaction value
mean_trans = df_retail.groupby(['Country','Channel'])['Revenue'].mean().reset_index()
max_pair = mean_trans.loc[mean_trans['Revenue'].idxmax()]
min_pair = mean_trans.loc[mean_trans['Revenue'].idxmin()]
print('Highest average transaction value:', max_pair.to_dict())
print('Lowest average transaction value:', min_pair.to_dict())
Highest average transaction value: {'Country': 'Netherlands', 'Channel': 'Online', 'Revenue': 126.00951838879159}
Lowest average transaction value: {'Country': 'Hong Kong', 'Channel': 'Online', 'Revenue': -0.46413793103448286}
# Advanced Example 2: Proportion of online vs offline sales in the UK
uk = df_retail[df_retail['Country'] == 'United Kingdom']
uk_channel_counts = uk['Channel'].value_counts(normalize=True)
print('Share of online vs offline transactions in the UK:')
print(uk_channel_counts)
Share of online vs offline transactions in the UK:
Channel
Online     0.500151
Offline    0.499849
Name: proportion, dtype: float64
# Advanced Example 3: Calculate the coefficient of variation for revenue by channel and region
def coef_of_variation(x): return np.std(x) / np.mean(x) if np.mean(x) != 0 else np.nan
cv_table = df_retail.groupby(['Country','Channel'])['Revenue'].agg(coef_of_variation).unstack()
print('Coefficient of variation (CV) of revenue by region and channel:')
print(cv_table.head(8))
Coefficient of variation (CV) of revenue by region and channel:
Channel           Offline    Online
Country                            
Australia        1.431992  1.486696
Austria          1.496914  1.206993
Bahrain          0.371799  2.802538
Belgium          0.805119  0.769247
Brazil           1.121966  0.620563
Canada           2.715862  0.670965
Channel Islands  1.348794  1.657065
Cyprus           1.894020  1.286525
# Error Example 1: What happens if we group by the wrong field?
try:
    error_group = df_retail.groupby('Price')['Revenue'].sum()
    print(error_group.head())
except Exception as e:
    print('Error:', str(e))
Price
-11062.060   -22124.120
 0.000            0.000
 0.001            0.004
 0.010           -7.200
 0.030         -291.600
Name: Revenue, dtype: float64
# Error Example 2: Handling missing data in the 'Revenue' column
df_retail.loc[df_retail.sample(frac=0.01, random_state=42).index, 'Revenue'] = np.nan
missing_rev = df_retail['Revenue'].isna().sum()
print(f'Number of missing revenue values: {missing_rev}')
mean_rev = df_retail['Revenue'].mean(skipna=True)
print('Mean revenue after introducing missing values:', mean_rev)
Number of missing revenue values: 5419
Mean revenue after introducing missing values: 17.99531994758533
# Error Example 3: Incorrect aggregationsumming Quantity and Price instead of Revenue
agg_wrong = df_retail.groupby('Country')[['Quantity', 'Price']].sum().head()
print('WRONG: Summing Quantity and Price directly (incorrect for revenue):')
print(agg_wrong)
agg_correct = df_retail.groupby('Country')['Revenue'].sum().head()
print('CORRECT: Summing Revenue for total sales:')
print(agg_correct)
WRONG: Summing Quantity and Price directly (incorrect for revenue):
           Quantity    Price
Country                     
Australia     83653  4054.75
Austria        4827  1701.52
Bahrain         260    86.57
Belgium       23152  7540.13
Brazil          356   142.60
CORRECT: Summing Revenue for total sales:
Country
Australia    136402.17
Austria       10039.12
Bahrain         530.70
Belgium       40342.60
Brazil         1143.60
Name: Revenue, dtype: float64
# Best Practice 1: Use fillna() to handle missing revenue
df_retail['Revenue'] = df_retail['Revenue'].fillna(df_retail['Revenue'].median())
print('Number of missing values after filling:', df_retail['Revenue'].isna().sum())
Number of missing values after filling: 0
# Best Practice 2: Segment your customers by purchase frequency
customer_counts = df_retail.groupby('Customer ID')['Invoice'].nunique().sort_values(ascending=False)
top_customers = customer_counts.head(5)
print('Top 5 customers by purchase frequency:')
print(top_customers)
Top 5 customers by purchase frequency:
Customer ID
14911.0    248
12748.0    224
17841.0    169
14606.0    128
15311.0    118
Name: Invoice, dtype: int64
# Best Practice 3: Product category performance by channel (requires more data)
df_product = pd.DataFrame({'ProductID': df_retail['StockCode'], 'Category': np.random.choice(['Electronics','Clothing','Home','Sports','Beauty'], len(df_retail)), 'Price': df_retail['Price']})
cat_perf = df_retail.copy()
cat_perf['Category'] = df_product['Category']
performance = cat_perf.groupby(['Category','Channel'])['Revenue'].sum().unstack()
print('Product category revenue by channel:')
print(performance)
Product category revenue by channel:
Channel          Offline       Online
Category                             
Beauty        963109.290   952563.360
Clothing      973952.742  1096267.110
Electronics   788579.280   961395.191
Home         1091295.700   988057.710
Sports        948508.741   943433.320
# Analytical Pattern: Find periods of peak demand by country and channel
peak = df_retail.groupby(['Country','Channel'])['Revenue'].sum().reset_index()
peak_country = peak.loc[peak.groupby('Country')['Revenue'].idxmax()]
print('Channel with peak revenue for each country:')
print(peak_country[['Country','Channel','Revenue']].head(8))
Channel with peak revenue for each country:
            Country  Channel   Revenue
1         Australia   Online  68740.60
2           Austria  Offline   5311.60
5           Bahrain   Online    360.55
7           Belgium   Online  20418.18
9            Brazil   Online    612.00
10           Canada  Offline   2282.50
12  Channel Islands  Offline  10481.22
14           Cyprus  Offline   6892.43
# Analytical Pattern: Simple demand forecasting via recent monthly growth
trend_recent = trend.tail(3)
growth = (trend_recent.iloc[-1] - trend_recent.iloc[0]) / trend_recent.iloc[0]
print('Recent 3-month growth by channel:')
print(growth)
Recent 3-month growth by channel:
Channel
Offline   -0.620280
Online    -0.570188
dtype: float64
# End-to-End Problem: Which country and channel combination should we invest in next month?
recent_month = trend.index[-1]
recent_results = df_retail[df_retail['Month'] == recent_month]
pivot = pd.pivot_table(recent_results, values='Revenue', index='Country', columns='Channel', aggfunc='sum', fill_value=0)
pivot['Total'] = pivot.sum(axis=1)
top_region_channel = pivot[['Online','Offline']].stack().idxmax()
print('Recommend investing in:', top_region_channel)
print('Reason: This country-channel pair had the highest revenue last month.')
Recommend investing in: ('United Kingdom', 'Online')
Reason: This country-channel pair had the highest revenue last month.
 

Found this useful?

All lessons, notebooks and datasets here are free. If they helped you, a coffee keeps new lessons coming.