Lesson 6 · Real-World Data Analytics
Python Data Analytics #06: Price & Discount Impact Analysis in Python
Video six of the hundred-video real-world data analytics series. Measuring real price elasticity of demand directly from real historical price changes…
- CourseReal-World Data Analytics
- Lesson6 of 100
- Video30 min
- FormatJupyter notebook · 30 code cells
- Data1 dataset
What you'll learn
- Where Real Price Variation Comes From
- Real Description Lookup per Product
- How Many Real Products Actually Have Price Variation
- Picking One Real Product for a Deep Dive
- Real Quantity Sold at Each Real Price Point
- Visualizing Real Price vs Real Quantity
- Real Log-Log Elasticity Regression
- Interpreting the Real Elasticity Value
Datasets used in this lesson
Save these next to the notebook. In Google Colab, upload them with the 📁 icon on the left first.
- online_retail_clean.csv36.2 MB
📓 Full notebook
Download .ipynbData Analytics 100, Video 6: Price and Discount Impact Analysis#
- Video six of the hundred-video real-world data analytics series.
- Measuring real price elasticity of demand directly from real historical price changes already sitting in the same retail data.
- Let's get into it.
Part 1: Where Real Price Variation Comes From#
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
clean = pd.read_csv('online_retail_clean.csv', parse_dates=['InvoiceDate'])
clean.shape
Part 2: Real Description Lookup per Product#
desc_lookup = clean.groupby('StockCode')['Description'].agg(lambda s: s.mode().iloc[0])
desc_lookup.head(3)
Part 3: How Many Real Products Actually Have Price Variation#
price_variation = clean.groupby('StockCode')['UnitPrice'].nunique()
(price_variation > 1).sum(), len(price_variation)
Part 4: Picking One Real Product for a Deep Dive#
deep_dive_sc = '85099B'
desc_lookup[deep_dive_sc]
sub = clean[clean['StockCode'] == deep_dive_sc]
sub.shape[0]
Part 5: Real Quantity Sold at Each Real Price Point#
by_price = sub.groupby('UnitPrice').agg(TotalQty=('Quantity', 'sum'), Orders=('InvoiceNo', 'nunique')).reset_index()
by_price.sort_values('UnitPrice')
Part 6: Visualizing Real Price vs Real Quantity#
plt.figure(figsize=(8, 6))
plt.scatter(by_price['UnitPrice'], by_price['TotalQty'], s=80, color='darkorange')
plt.xlabel('Real Unit Price (GBP)')
plt.ylabel('Real Total Quantity Sold')
plt.title(f'Real Price vs Quantity for {desc_lookup[deep_dive_sc]}')
plt.tight_layout()
plt.savefig('price_vs_quantity_scatter.png', dpi=120)
plt.close()
Part 7: Real Log-Log Elasticity Regression#
valid = by_price[by_price['TotalQty'] > 0]
log_price = np.log(valid['UnitPrice'])
log_qty = np.log(valid['TotalQty'])
elasticity, intercept = np.polyfit(log_price, log_qty, 1)
round(elasticity, 2)
Part 8: Interpreting the Real Elasticity Value#
is_elastic = abs(elasticity) > 1
print(f'Elasticity of {round(elasticity, 2)} means this product is {"elastic" if is_elastic else "inelastic"}: a 1% price cut is associated with roughly a {round(abs(elasticity), 1)}% change in units sold.')
Part 9: Defining a Real Regular Price vs Real Discount Price#
regular_price = sub['UnitPrice'].mode().iloc[0]
regular_price
sub = sub.copy()
sub['IsDiscounted'] = sub['UnitPrice'] < regular_price
sub['IsDiscounted'].mean().round(3)
Part 10: Real Average Order Size, Discounted vs Full Price#
avg_qty_discounted = sub[sub['IsDiscounted']]['Quantity'].mean()
avg_qty_full = sub[~sub['IsDiscounted']]['Quantity'].mean()
round(avg_qty_discounted, 1), round(avg_qty_full, 1)
Part 11: Did Real Discounting Actually Grow Total Revenue#
revenue_discounted = sub[sub['IsDiscounted']]['Revenue'].sum()
revenue_full = sub[~sub['IsDiscounted']]['Revenue'].sum()
round(revenue_discounted, 2), round(revenue_full, 2)
Part 12: Real Revenue per Percent Discount#
discount_rows = sub[sub['IsDiscounted']].copy()
discount_rows['DiscountPct'] = (regular_price - discount_rows['UnitPrice']) / regular_price * 100
discount_rows.groupby(pd.cut(discount_rows['DiscountPct'], bins=[0, 10, 20, 30, 100]), observed=True)['Quantity'].mean().round(1)
Part 13: Scaling Up to Real Many Products#
qty_by_sc = clean.groupby('StockCode')['Quantity'].sum()
price_var_by_sc = clean.groupby('StockCode')['UnitPrice'].nunique()
candidates = price_var_by_sc[(price_var_by_sc >= 4) & (qty_by_sc > 500)].index
len(candidates)
Part 14: Real Elasticity Loop Across Products#
results = []
for sc in candidates:
s = clean[clean['StockCode'] == sc]
bp = s.groupby('UnitPrice')['Quantity'].sum()
bp = bp[bp > 0]
if len(bp) < 4 or bp.index.to_series().std() == 0:
continue
lp, lq = np.log(bp.index.values.astype(float)), np.log(bp.values.astype(float))
e, _ = np.polyfit(lp, lq, 1)
results.append((sc, desc_lookup[sc], e, len(bp)))
len(results)
Part 15: Real Elasticity Results Table#
elasticity_df = pd.DataFrame(results, columns=['StockCode', 'Description', 'Elasticity', 'NumPricePoints'])
elasticity_df.shape[0]
elasticity_df.sort_values('Elasticity').head(5)
Part 16: What Fraction Show Real Normal Demand Behavior#
pct_negative = (elasticity_df['Elasticity'] < 0).mean()
round(pct_negative * 100, 1)
Part 17: Real Average Elasticity Across the Catalog#
elasticity_df['Elasticity'].mean().round(2)
elasticity_df['Elasticity'].median().round(2)
Part 18: Visualizing the Real Elasticity Distribution#
plt.figure(figsize=(9, 5))
plt.hist(elasticity_df['Elasticity'], bins=30, color='seagreen', edgecolor='white')
plt.axvline(-1, color='red', linestyle='--', label='Real elastic/inelastic boundary')
plt.xlabel('Real Estimated Elasticity')
plt.ylabel('Real Number of Products')
plt.title('Real Distribution of Price Elasticity Across the Catalog')
plt.legend()
plt.tight_layout()
plt.savefig('elasticity_distribution.png', dpi=120)
plt.close()
Part 19: Real Most Price-Sensitive Products#
most_elastic = elasticity_df.sort_values('Elasticity').head(10)
most_elastic[['Description', 'Elasticity']]
Part 20: Real Least Price-Sensitive Products#
least_elastic = elasticity_df[elasticity_df['Elasticity'] > -1].sort_values('Elasticity', ascending=False).head(10)
least_elastic[['Description', 'Elasticity']]
Part 21: Real Revenue-Maximizing Price for the Deep-Dive Product#
candidate_prices = np.linspace(valid['UnitPrice'].min(), valid['UnitPrice'].max(), 50)
predicted_qty = np.exp(intercept) * candidate_prices ** elasticity
predicted_revenue = candidate_prices * predicted_qty
optimal_price = candidate_prices[np.argmax(predicted_revenue)]
round(optimal_price, 2)
Part 22: Visualizing the Real Predicted Revenue Curve#
plt.figure(figsize=(8, 6))
plt.plot(candidate_prices, predicted_revenue, color='purple')
plt.axvline(optimal_price, color='red', linestyle='--', label=f'Real optimal price: {round(optimal_price, 2)}')
plt.xlabel('Real Hypothetical Unit Price (GBP)')
plt.ylabel('Real Predicted Total Revenue')
plt.title(f'Real Predicted Revenue Curve for {desc_lookup[deep_dive_sc]}')
plt.legend()
plt.tight_layout()
plt.savefig('revenue_curve_optimal_price.png', dpi=120)
plt.close()
Part 23: Real Sanity Check on the Elasticity Math#
manual_pct_price_change = (valid['UnitPrice'].iloc[-1] - valid['UnitPrice'].iloc[0]) / valid['UnitPrice'].iloc[0]
manual_pct_qty_change = (valid['TotalQty'].iloc[-1] - valid['TotalQty'].iloc[0]) / valid['TotalQty'].iloc[0]
np.sign(manual_pct_qty_change) != np.sign(manual_pct_price_change)
Part 24: Saving the Real Elasticity Results#
elasticity_df.sort_values('Elasticity').to_csv('product_price_elasticity.csv', index=False)
reloaded = pd.read_csv('product_price_elasticity.csv')
reloaded.shape[0] == elasticity_df.shape[0]
Part 25: Real Business Takeaway#
n_elastic = (elasticity_df['Elasticity'] < -1).sum()
n_inelastic = (elasticity_df['Elasticity'] >= -1).sum()
print(f'{n_elastic} real products are genuinely elastic and good discounting candidates; {n_inelastic} are inelastic, where discounting mostly just gives away real margin.')
Part 26: Real Weekday Effect on Discount Timing#
sub['Weekday'] = sub['InvoiceDate'].dt.day_name()
sub.groupby('Weekday')['IsDiscounted'].mean().round(3).sort_values(ascending=False)
Part 27: Real Country Mix Among Discount Buyers#
sub[sub['IsDiscounted']]['Country'].value_counts().head(5)
Part 28: Real Correlation Between Elasticity and Sales Volume#
volume_lookup = clean.groupby('StockCode')['Quantity'].sum()
elasticity_df['TotalVolume'] = elasticity_df['StockCode'].map(volume_lookup)
elasticity_df[['Elasticity', 'TotalVolume']].corr().iloc[0, 1].round(3)
Part 29: Real Price Point Count vs Elasticity Reliability#
reliable = elasticity_df[elasticity_df['NumPricePoints'] >= 6]
reliable.shape[0]
reliable['Elasticity'].mean().round(2)
Part 30: Real Recap Print#
print(f'Estimated price elasticity for {len(elasticity_df)} real products; {n_elastic} are genuinely discount-worthy.')
Wrap-Up: What You Learned#
- Real price elasticity of demand can genuinely be estimated straight from a real product's own historical price changes, no experiment required.
- A real log-log regression of quantity against price gives a real slope that is, by definition, the elasticity.
- An elasticity below negative one means demand is genuinely elastic, discounting pays off in real extra volume; above negative one, discounting mostly gives away real margin.
- The real most-frequent price makes a reasonable real stand-in regular price when no explicit discount flag exists in the raw data.
- A real fitted elasticity curve can even suggest a real revenue-maximizing price directly, not just whether to discount at all.
- Next video: real customer lifetime value modeling, projecting how much each real customer is actually worth going forward.
Found this useful?
All lessons, notebooks and datasets here are free. If they helped you, a coffee keeps new lessons coming.



