Lesson 19 · Real-World Data Analytics
Python Data Analytics #19: Value-at-Risk & Drawdown Analysis in Python
Video nineteen of the hundred-video real-world data analytics series. Three real ways to estimate Value-at-Risk, plus a full real drawdown and…
- CourseReal-World Data Analytics
- Lesson19 of 26
- Video31 min
- FormatJupyter notebook · 30 code cells
- Data1 dataset
What you'll learn
Datasets used in this lesson
Save these next to the notebook. In Google Colab, upload them with the 📁 icon on the left first.
- aapl_clean.csv58.9 KB
📓 Full notebook
Download .ipynbData Analytics 100, Video 19: Value-at-Risk and Drawdown Analysis#
- Video nineteen of the hundred-video real-world data analytics series.
- Three real ways to estimate Value-at-Risk, plus a full real drawdown and underwater-period breakdown for Apple.
- Let's get into it.
Part 1: Quantifying Real Tail Risk#
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
aapl = pd.read_csv('aapl_clean.csv', parse_dates=['Date'])
returns = aapl['AAPL.Close'].pct_change().dropna()
Part 2: Real Historical VaR#
var95_historical = np.percentile(returns, 5)
round(var95_historical * 100, 2)
Part 3: Real Parametric VaR#
from scipy.stats import norm
mean_return = returns.mean()
std_return = returns.std()
z_score_95 = norm.ppf(0.05)
var95_parametric = mean_return + z_score_95 * std_return
round(var95_parametric * 100, 2)
Part 4: Real Monte Carlo VaR#
np.random.seed(42)
simulated_returns = np.random.normal(mean_return, std_return, 100000)
var95_montecarlo = np.percentile(simulated_returns, 5)
round(var95_montecarlo * 100, 2)
Part 5: Comparing All Three Real VaR Methods#
var_comparison = pd.Series({'Historical': var95_historical, 'Parametric': var95_parametric, 'Monte Carlo': var95_montecarlo})
(var_comparison * 100).round(2)
Part 6: Real Conditional VaR (Expected Shortfall)#
tail_losses = returns[returns <= var95_historical]
cvar95 = tail_losses.mean()
round(cvar95 * 100, 2)
Part 7: Visualizing Real VaR and Real CVaR on the Return Distribution#
plt.figure(figsize=(11, 5))
plt.hist(returns, bins=50, color='steelblue', edgecolor='white', alpha=0.8)
plt.axvline(var95_historical, color='orange', linestyle='--', label=f'Real VaR 95% ({round(var95_historical*100,2)}%)')
plt.axvline(cvar95, color='red', linestyle='--', label=f'Real CVaR 95% ({round(cvar95*100,2)}%)')
plt.xlabel('Real Daily Return')
plt.ylabel('Real Frequency')
plt.title('Real Return Distribution with VaR and CVaR')
plt.legend()
plt.tight_layout()
plt.savefig('var_cvar_distribution.png', dpi=120)
plt.close()
Part 8: Real Multi-Day VaR Scaling#
var95_10day = var95_historical * np.sqrt(10)
var95_30day = var95_historical * np.sqrt(30)
round(var95_10day * 100, 2), round(var95_30day * 100, 2)
Part 9: Real VaR Backtesting, Counting Exceedances#
exceedances = (returns < var95_historical).sum()
expected_exceedances = len(returns) * 0.05
exceedances, round(expected_exceedances, 1)
Part 10: Real Exceedance Rate#
exceedance_rate = exceedances / len(returns)
round(exceedance_rate * 100, 2)
Part 11: Visualizing Real VaR Breaches Over Time#
breach_dates = aapl['Date'].iloc[1:][returns.values < var95_historical]
breach_returns = returns[returns < var95_historical]
plt.figure(figsize=(11, 5))
plt.plot(aapl['Date'].iloc[1:], returns, color='gray', alpha=0.5, label='Real Daily Return')
plt.scatter(breach_dates, breach_returns, color='red', s=25, label='Real VaR Breach', zorder=5)
plt.axhline(var95_historical, color='orange', linestyle='--', label='Real VaR 95% Threshold')
plt.xlabel('Real Date')
plt.ylabel('Real Daily Return')
plt.title('Real VaR Breaches Over Time')
plt.legend()
plt.tight_layout()
plt.savefig('var_breaches.png', dpi=120)
plt.close()
Part 12: Real Drawdown Series#
cumulative = (1 + returns).cumprod()
running_peak = cumulative.cummax()
drawdown = (cumulative - running_peak) / running_peak
drawdown.describe().round(3)
Part 13: Real Maximum Drawdown#
max_drawdown = drawdown.min()
max_drawdown_date = aapl['Date'].iloc[1:][drawdown.values == max_drawdown].iloc[0]
round(max_drawdown * 100, 2), max_drawdown_date
Part 14: Visualizing the Real Drawdown Curve#
plt.figure(figsize=(11, 5))
plt.fill_between(aapl['Date'].iloc[1:], drawdown * 100, 0, color='crimson', alpha=0.5)
plt.xlabel('Real Date')
plt.ylabel('Real Drawdown (%)')
plt.title('Real Apple Drawdown from Prior Peak')
plt.tight_layout()
plt.savefig('full_drawdown_curve.png', dpi=120)
plt.close()
Part 15: Real Underwater Periods#
is_underwater = drawdown < 0
pct_time_underwater = is_underwater.mean()
round(pct_time_underwater * 100, 1)
Part 16: Real Longest Underwater Streak#
longest_streak = 0
current_streak = 0
for underwater_flag in is_underwater:
if underwater_flag:
current_streak += 1
longest_streak = max(longest_streak, current_streak)
else:
current_streak = 0
longest_streak
Part 17: Real Recovery Time from Maximum Drawdown#
bottom_idx = drawdown.values.argmin()
post_bottom_drawdown = drawdown.iloc[bottom_idx:]
recovery_points = post_bottom_drawdown[post_bottom_drawdown >= -0.001]
recovered = len(recovery_points) > 0
recovered
Part 18: Real Number of Distinct Drawdown Episodes#
underwater_starts = (is_underwater & ~is_underwater.shift(1, fill_value=False)).sum()
underwater_starts
Part 19: Real Average Drawdown Depth#
average_drawdown_when_underwater = drawdown[is_underwater].mean()
round(average_drawdown_when_underwater * 100, 2)
Part 20: Real VaR at a Different Confidence Level#
var99_historical = np.percentile(returns, 1)
round(var99_historical * 100, 2)
Part 21: Real 99% Exceedance Backtest#
exceedances_99 = (returns < var99_historical).sum()
expected_99 = len(returns) * 0.01
exceedances_99, round(expected_99, 1)
Part 22: Real Dollar VaR for a Hypothetical Position#
hypothetical_position_usd = 100000
dollar_var95 = hypothetical_position_usd * abs(var95_historical)
round(dollar_var95, 2)
Part 23: Real Rolling VaR Over Time#
rolling_var95 = returns.rolling(60).apply(lambda x: np.percentile(x, 5), raw=True)
rolling_var95.dropna().describe().round(4)
Part 24: Visualizing Real Rolling VaR#
plt.figure(figsize=(11, 5))
plt.plot(aapl['Date'].iloc[1:], rolling_var95, color='darkred')
plt.xlabel('Real Date')
plt.ylabel('Real Rolling 60-Day VaR (95%)')
plt.title('Real Rolling VaR Over Time, Apple')
plt.tight_layout()
plt.savefig('rolling_var.png', dpi=120)
plt.close()
Part 25: Saving the Real VaR and Drawdown Summary#
risk_summary = pd.DataFrame({'Metric': ['Historical VaR 95%', 'Parametric VaR 95%', 'Monte Carlo VaR 95%', 'CVaR 95%', 'Historical VaR 99%', 'Max Drawdown'], 'ValuePct': [round(var95_historical*100,2), round(var95_parametric*100,2), round(var95_montecarlo*100,2), round(cvar95*100,2), round(var99_historical*100,2), round(max_drawdown*100,2)]})
risk_summary.to_csv('var_drawdown_summary.csv', index=False)
reloaded = pd.read_csv('var_drawdown_summary.csv')
reloaded.shape[0] == 6
Part 26: Real Sanity Check, VaR Ordering#
var99_historical < var95_historical
cvar95 <= var95_historical
Part 27: Real Sanity Check, Drawdown Bounds#
(drawdown <= 0).all()
Part 28: Real Comparison, VaR vs Real Max Drawdown#
round(abs(max_drawdown) / abs(var95_historical), 1)
Part 29: Real Number of Drawdown Episodes vs Real VaR Breaches#
underwater_starts, exceedances
Part 30: Real Recap Print#
print(f'Across {len(returns)} real trading days, Apple had a {round(var95_historical*100,2)}% one-day VaR, a {round(cvar95*100,2)}% Expected Shortfall, and a {round(max_drawdown*100,1)}% maximum drawdown that lasted up to {longest_streak} real consecutive underwater days.')
Wrap-Up: What You Learned#
- Historical, parametric, and Monte Carlo VaR are three genuinely different ways to estimate the real same downside threshold, and comparing them is a real honesty check.
- Conditional VaR, or Expected Shortfall, answers the real question VaR leaves open: how bad does it actually get once the threshold is breached.
- Backtesting VaR by counting real exceedances against the real theoretically expected rate is how you check whether a risk model is actually well-calibrated.
- Drawdown and VaR measure genuinely different things, VaR is about a real single bad day, drawdown is about a real sustained decline from a prior peak.
- Underwater period length and real recovery time turn an abstract percentage into a real, human sense of how long a loss actually took to live through.
- Next video: the Domain 2 capstone, building one real multi-asset portfolio report that pulls together everything covered across this whole finance domain.
Found this useful?
All lessons, notebooks and datasets here are free. If they helped you, a coffee keeps new lessons coming.



