Introduction

For this project, I am analyzing a dataset of online retail (E-Commerce) transactions from 2021 to 2025. Each transaction is recorded, including information about the order (ex. identifier, # of items purchased and revenue/profit), as well as the customer (ex. demographic data, marketing method utilized, and loyalty status). This anaylsis is primarily focused on identifying trends in financial performance (gross sales and profit), as well as order volume drivers.

Dataset

The dataset is comprised of 138,116 transactions, with 45 data points for each. These transactions represent 775,555 items sold, generating 177,134,263.74 in net sales and 76,146,395.76 in profit from 2021-2025. 24,911 customers engaged with the platform (48.1% female, 47.9% male, 4% non-binary) through 10 separate marketing channels (including social media platforms, ad campaigns, and referrals). 75% of orders are made through web-based sales channels (website and mobile app), while the remainder occur through social media channels (i.e., TikTok Shop).

Seasonality of Orders and Profit

This visualization shows how orders within the dataset are distributed throughout the year. There is a clear seasonality effect, with relative increases in order quantity and profit in March (winter->spring), June (spring->summer), and August (summer->fall). The most notable order concentration is in November and December, coinciding with Black Friday and holiday shopping - Fridays in December have the single greatest quantity of orders, as well as the highest profit.

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import warnings
from matplotlib.ticker import StrMethodFormatter
path = 'C:/Users/dobso/Documents/R Coding/R_datafiles/ECommerce/'
filename= path+'ecommerce_sales_customer_analytics_150k.csv'

df=pd.read_csv(filename)
df['order_date_time']=df['order_date']+' '+df['order_time']
df['order_date_time'] = pd.to_datetime(df['order_date_time'], format= 'ISO8601')
df['Hour']=df.order_date_time.dt.hour
df['Day']=df.order_date_time.dt.day
df['Month']=df.order_date_time.dt.month
df['Year']=df.order_date_time.dt.year
df['Weekday']=df.order_date_time.dt.strftime('%A')
df2=(df.groupby(['Month', 'Weekday'], as_index=False).agg(sum_of_profit=('profit', 'sum'),count_of_order_id=('order_id', 'count')))
df2['order_id_hundreds']=df2['count_of_order_id']/10
weekday_order = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
df2['Weekday'] = pd.Categorical(df2['Weekday'], categories=weekday_order, ordered=True)
df2 = df2.sort_values('Weekday')
plt.figure(figsize=(20,12))
plt.scatter(df2['Weekday'], df2['Month'], marker='o', cmap='viridis',
            c=df2['sum_of_profit'],s=df2['count_of_order_id'],edgecolors='black')
plt.title('Profit and Order Quantity by Month and Weekday', fontsize=20)
plt.xlabel('Days of the Week', fontsize=15)
plt.ylabel('Months of the Year', fontsize=15)
cbar=plt.colorbar()
cbar.set_label('Cumulative Profit', rotation=270, fontsize=15, color='black', labelpad=30)
cbar_ticks=[*range(650000,1200000,50000)]
cbar.set_ticks(cbar_ticks)
cbar.set_ticklabels([f'${tick:,.0f}' for tick in cbar_ticks])

y_ticks=[*range(df2['Month'].min(),df2['Month'].max()+1,1)]
plt.yticks(y_ticks,fontsize=15,color='black');

plt.figtext(0.5, 0.02, 'Note: Bubble size represents total orders across time period.\n Color represents total profit.',
            ha='center', fontsize=12, style='italic', color='gray')

plt.show()

Ranking Marketing Channels

This visualization shows how customer engagement changed over the timeframe of the dataset through the various marketing channels that the platform utilizes. Overall, there is not much variation in the ranking of marketing channels over the timeframe. Organic search consistently drives the most sales, pulling in $5,302,921.71 in 2023. Direct marketing and Google ads drive the next most sales, occasionally swapping positions. Affiliate and social media channels (TikTok and YouTube) drive the lowest sales totals, but potentially represent avenues for overall brand building and awareness, even if they don’t tangibly drive sales.

df4=(df.groupby(['marketing_channel','Year'], as_index=False).agg(sum_of_sales=('net_sales', 'sum')))
from matplotlib.ticker import FuncFormatter

fig=plt.figure(figsize=(24,20))
ax=fig.add_subplot(1,1,1)

my_colors={'Affiliate':'blue',
           'Direct':'red',
           'Email Marketing':'green',
           'Facebook Ads':'gray',
           'Google Ads':'purple',
           'Instagram':'gold',
           'Organic Search':'brown',
           'Referral':'orange',
           'TikTok':'darkblue',
           'YouTube':'black'}
for key, grp in df4.groupby('marketing_channel'):
    grp.plot(ax=ax, kind='line', x='Year', y='sum_of_sales', color=my_colors[key], label=key, marker='8', linewidth=5, markersize=10)

plt.title('Sales by Marketing Channel per Year', fontsize=18)
ax.set_xlabel('year', fontsize=18)
ax.set_ylabel('Total Sales ($M)', fontsize=18, labelpad=20)
ax.tick_params(axis='x', labelsize=14, rotation=0)
ax.tick_params(axis='y', labelsize=14, rotation=0)
ax.set_xticks(np.arange(2021,2026,1))

handles, labels=ax.get_legend_handles_labels()
handles= [handles[6],handles[1],handles[4],handles[3],handles[5],handles[7],handles[2],handles[0],handles[8],handles[9]]
labels= [  labels[6], labels[1], labels[4], labels[3], labels[5], labels[7], labels[2], labels[0], labels[8], labels[9]]
plt.legend(handles,labels,loc='best', fontsize=14, ncol=1, bbox_to_anchor=(1,1.05))

ax.yaxis.set_major_formatter(FuncFormatter(lambda x, pos:('$%1.1fM')%(x*1e-6)))

plt.plot(layout="constrained");

plt.show()

Top 10 Customers

This visualization shows the top 10 highest-paying customers across the timeframe. There’s no prominent outlier within the group, but compared to the homogeneous gender split in the overall data set, here 6 out of 10 customers are female, and male and non-binary have 2 each. This suggests that while the customer base is evenly divided, there is likely a gender variance in spending habits that could be explained by product mix or marketing efforts.

top10cust_df = (df.groupby('customer_id')['net_sales'].sum().reset_index(name='Total_Sales')
    .sort_values('Total_Sales', ascending=False).reset_index(drop=True))
customer_info = (df[['customer_id', 'customer_name', 'gender']].drop_duplicates(subset='customer_id'))
top10cust_df = top10cust_df.merge(customer_info, on='customer_id', how='left')
top10cust_df = top10cust_df[['customer_id', 'customer_name', 'gender', 'Total_Sales']]
gender_colors = {'Male':'lightblue','Female':'pink','Non-Binary':'lightgreen'}

plot_df = top10cust_df.loc[0:9].copy()
colors = plot_df['gender'].map(gender_colors)

plt.figure(figsize=(20, 10))
bars = plt.bar(plot_df['customer_name'],plot_df['Total_Sales'],color=colors,label='Lifetime Sales')

plt.title('Top 10 Customers by Lifetime Sales', fontsize=20)
plt.xlabel('Customer', fontsize=15)
plt.ylabel('Lifetime Sales', fontsize=15)
plt.gca().yaxis.set_major_formatter(StrMethodFormatter('${x:,.0f}'))

for bar in bars:
    height = bar.get_height()
    plt.text(bar.get_x() + bar.get_width() / 2,height * 1.01,f'${height:,.0f}',fontsize=11,
             ha='center',va='bottom')
from matplotlib.patches import Patch
legend_handles = [Patch(facecolor=gender_colors.get(g, '#7f7f7f'), label=g)
    for g in plot_df['gender'].unique()]

plt.legend(handles=legend_handles, title='Gender', fontsize=12, title_fontsize=14)
plt.xticks(rotation=45, ha='right')
plt.tight_layout()
plt.show()

Customer Demographics

The chart below shows how gross sales are divided by customer demographics - gender and age range. The highest sales comes from the 30-39 bracket, followed by 20-29, suggesting that the primary customer base is younger-leaning. The recorded age range for customers is 18-74, and there is a slight downward trend as age increases.

demo_df=df.groupby(['gender','customer_age'])['gross_sales'].sum().reset_index(name='Gross Sales')
lower = (demo_df['customer_age'] // 10) * 10
demo_df['age_bracket'] = lower.astype(int).astype(str) + '-' + (lower + 9).astype(int).astype(str)
demo_df=demo_df.groupby(['age_bracket','gender'])['Gross Sales'].sum().reset_index(name='Gross Sales')
demo_df=demo_df.pivot(index='age_bracket', columns='gender', values='Gross Sales')
fig=plt.figure(figsize=(18,10))
ax=fig.add_subplot(1,1,1)

demo_bars=demo_df.plot(kind='bar', stacked=True, ax=ax)
plt.title('Gross Sales by Age Group and Gender\n Stacked Bar Plot', fontsize=18)
ax.set_xlabel('Age Group', fontsize=14)
ax.set_ylabel('Gross Sales', fontsize=14, labelpad=20)

ax.yaxis.set_major_formatter(FuncFormatter(lambda x, pos:('$%1.2fM')%(x*1e-6)))

handles, labels=ax.get_legend_handles_labels()
handles= [handles[2],handles[1],handles[0]]
labels=  [ labels[2], labels[1], labels[0]]
plt.legend(handles,labels,loc='upper right', fontsize=14, ncol=1)

totals = demo_df.sum(axis=1)
max_idx = totals.idxmax()
min_idx = totals.idxmin()
max_total = totals.max()
min_total = totals.min()

idx_to_x = {idx: x for x, idx in enumerate(totals.index)}

ax.text(idx_to_x[max_idx], max_total * 1.01, f'${max_total * 1e-6:.2f}M',
        ha='center', va='bottom', fontsize=12, fontweight='bold', color='darkgreen')

ax.text(idx_to_x[min_idx], min_total * 1.01, f'${min_total * 1e-6:.2f}M',
        ha='center', va='bottom', fontsize=12, fontweight='bold', color='darkred')

plt.show()

Customer Segment Impact

This plot shows how orders are broken out by customer type, with three levels of individual consumers (Consumer, Premium, and VIP) as well as B2B sales. Base level consumers make up approximately 55% of orders, with Premium members accounting for an additional 25%. This remains relatively static looking across the timeframe, however there is a slight upward trend for Premium customers, suggesting that there amy be improving adoption rates of the enhanced loyatly program.

seg_df=df.groupby(['customer_segment','Year'])['order_id'].count().reset_index(name='Total_Orders')
number_outside_colors = len(seg_df.customer_segment.unique())
outside_color_ref_number = np.arange(number_outside_colors)*4
fig = plt.figure(figsize=(9, 9))
ax = fig.add_subplot(1, 1, 1)

colormap = plt.get_cmap("tab20c")
colors_outer = colormap(outside_color_ref_number)
colors_inner = colormap([1, 5, 9, 13, 17])
all_orders = seg_df['Total_Orders'].sum()

seg_df.groupby(['customer_segment'])['Total_Orders'].sum().plot(
       kind='pie', radius=1,colors=colors_outer, pctdistance=.85, labeldistance=1.05, 
       wedgeprops=dict(edgecolor='white'), textprops={'fontsize':14},
       autopct=lambda p: '{:.2f}%\n({:,.0f})'.format(p,(p/100)*all_orders),
       startangle=90)


inner_sizes = seg_df.groupby(['customer_segment', 'Year'])['Total_Orders'].sum()
wedges_inner, texts_inner, autotexts_inner = ax.pie(inner_sizes,radius=0.7,colors=colors_inner,
    pctdistance=0.75,labels=None,wedgeprops=dict(edgecolor='white'),textprops={'fontsize': 7},
    autopct=lambda p: '{:.2f}% ({:,.0f})'.format(p, (p/100)*all_orders),startangle=90)

for wedge, autotext in zip(wedges_inner, autotexts_inner):
    ang = (wedge.theta2 - wedge.theta1) / 2 + wedge.theta1
    if 90 < ang < 270:
        ang += 180
    autotext.set_rotation(ang)

hole = plt.Circle((0, 0), 0.3, fc='white')
ax.add_artist(hole)

years = sorted(seg_df['Year'].unique())
legend_handles = [plt.Line2D([0], [0], marker='o', color='w',
               markerfacecolor=colors_inner[i % len(colors_inner)],
               markersize=12, label=str(year))
    for i, year in enumerate(years)]
ax.legend(handles=legend_handles,title='Year',loc='upper right',fontsize=12,title_fontsize=13)
ax.set_title('Orders by Customer Segment (2021-2025)', fontsize=18)
plt.show()

Conclusion

Overall, while many aspects of this dataset remain fairly homogeneous across dimensions, there are multiple indicators pointing to growing success with a younger, female customer base, building on word-of-mouth and loyalty campaigns to further the platform’s success.