What You'll Learn Today
Building a Full DCF Model in Python
From basic skeleton to production-grade valuation model
In Session 19, we built a DCF skeleton function. What additional components do we need to make it a complete, professional-grade DCF model?
Think about: WACC calculation from market data, multiple scenarios, sensitivity tables, and professional output formatting
๐ From Skeleton to Full Model
In Session 19, we created a dcf_valuation() function that took hardcoded FCFs. Now we'll build the complete pipeline:
| Component | Session 19 (Skeleton) | Session 20 (Full Model) |
|---|---|---|
| FCF Projections | Hardcoded list | Calculated from revenue growth + margin assumptions |
| WACC | Input parameter | Calculated via CAPM using market data |
| Terminal Value | Gordon Growth only | Gordon Growth + Exit Multiple (cross-check) |
| Sensitivity | None | Automated 2-way tables + heatmaps |
| Scenarios | None | Bull / Base / Bear with probability weighting |
| Output | Print statements | Professional DataFrame + Excel export |
โ๏ธ Worked Example 1: DCF Valuation of Tata Motors
Given: Tata Motors financial data and market assumptions.
Calculate: (i) Projected FCFs for 5 years. (ii) WACC via CAPM. (iii) Terminal value (both methods). (iv) Enterprise & equity value. (v) Implied share price.
import pandas as pd
import numpy as np
import yfinance as yf
class DCFModel:
"""Full DCF valuation model for any company."""
def __init__(self, ticker, risk_free_rate=0.07, market_premium=0.08,
terminal_growth=0.03, projection_years=5):
self.ticker = ticker
self.rf = risk_free_rate # India 10Y govt bond ~7%
self.market_premium = market_premium # Equity risk premium ~8%
self.g_term = terminal_growth # 3% perpetual growth
self.n_years = projection_years
self.stock = yf.Ticker(ticker)
def calculate_beta(self):
"""Calculate beta from 2-year weekly returns vs Nifty 50."""
# Download stock and Nifty data
stock_data = self.stock.history(period="2y")
nifty = yf.Ticker("^NSEI").history(period="2y")
# Weekly returns
stock_weekly = stock_data['Close'].resample('W').last().pct_change().dropna()
nifty_weekly = nifty['Close'].resample('W').last().pct_change().dropna()
# Align dates
aligned = pd.concat([stock_weekly, nifty_weekly], axis=1, join='inner')
aligned.columns = ['stock', 'market']
# Beta = Cov(stock, market) / Var(market)
cov_matrix = aligned.cov()
self.beta = cov_matrix.loc['stock', 'market'] / cov_matrix.loc['market', 'market']
return self.beta
def calculate_wacc(self, tax_rate=0.2517):
"""Calculate WACC using CAPM for cost of equity."""
# Cost of Equity (CAPM)
self.cost_of_equity = self.rf + self.beta * self.market_premium
# Simplified: assume cost of debt = risk-free + 2% spread
self.cost_of_debt = self.rf + 0.02
# Get market cap and debt from yfinance
info = self.stock.info
market_cap = info.get('marketCap', 0)
total_debt = info.get('totalDebt', 0)
if market_cap == 0:
# Fallback: use 70% equity, 30% debt
equity_weight = 0.70
else:
total_capital = market_cap + total_debt
equity_weight = market_cap / total_capital if total_capital > 0 else 0.70
debt_weight = 1 - equity_weight
self.wacc = (equity_weight * self.cost_of_equity +
debt_weight * self.cost_of_debt * (1 - tax_rate))
print(f" Beta: {self.beta:.3f}")
print(f" Cost of Equity: {self.cost_of_equity*100:.2f}%")
print(f" Cost of Debt: {self.cost_of_debt*100:.2f}%")
print(f" Equity Weight: {equity_weight*100:.1f}%")
print(f" Debt Weight: {debt_weight*100:.1f}%")
print(f" WACC: {self.wacc*100:.2f}%")
return self.wacc
def project_fcfs(self, base_ebitda, ebitda_margin, growth_rates,
capex_pct, dnwc_pct, tax_rate=0.2517, da_pct=0.03):
"""Project Free Cash Flows from revenue assumptions."""
projections = []
for i, growth in enumerate(growth_rates):
year = i + 1
revenue = base_ebitda * (1 + growth)
ebitda = revenue * ebitda_margin
ebit = ebitda - (revenue * da_pct) # EBIT after D&A
tax = ebit * tax_rate
nopat = ebit - tax
capex = revenue * capex_pct
dnwc = revenue * dnwc_pct
fcf = nopat + (revenue * da_pct) - capex - dnwc # Add back D&A
projections.append({
'Year': year,
'Revenue': round(revenue, 2),
'EBITDA': round(ebitda, 2),
'EBIT': round(ebit, 2),
'Tax': round(tax, 2),
'NOPAT': round(nopat, 2),
'CapEx': round(capex, 2),
'dNWC': round(dnwc, 2),
'FCF': round(fcf, 2)
})
self.projections = pd.DataFrame(projections)
self.fcfs = self.projections['FCF'].tolist()
return self.projections
def value(self, shares_outstanding, net_debt=0, exit_multiple=None):
"""Calculate Enterprise Value, Equity Value, and Per-Share Value."""
# PV of projected FCFs
pv_fcfs = [fcf / (1 + self.wacc)**t for t, fcf in enumerate(self.fcfs, 1)]
sum_pv_fcfs = sum(pv_fcfs)
# Terminal Value โ Gordon Growth
terminal_fcf = self.fcfs[-1] * (1 + self.g_term)
tv_gordon = terminal_fcf / (self.wacc - self.g_term)
pv_tv_gordon = tv_gordon / (1 + self.wacc)**self.n_years
# Terminal Value โ Exit Multiple (if provided)
if exit_multiple:
tv_multiple = self.projections['EBITDA'].iloc[-1] * exit_multiple
pv_tv_multiple = tv_multiple / (1 + self.wacc)**self.n_years
else:
tv_multiple = None
pv_tv_multiple = None
# Use Gordon Growth as primary
ev = sum_pv_fcfs + pv_tv_gordon
equity_value = ev - net_debt
price_per_share = equity_value / shares_outstanding
self.results = {
'sum_pv_fcfs': sum_pv_fcfs,
'tv_gordon': tv_gordon,
'pv_tv_gordon': pv_tv_gordon,
'tv_multiple': tv_multiple,
'pv_tv_multiple': pv_tv_multiple,
'enterprise_value': ev,
'net_debt': net_debt,
'equity_value': equity_value,
'shares': shares_outstanding,
'price_per_share': price_per_share,
'pv_fcfs': pv_fcfs
}
return self.results
def summary(self):
"""Print professional valuation summary."""
r = self.results
print("=" * 60)
print(f"DCF VALUATION: {self.ticker}")
print("=" * 60)
print(f"WACC: {self.wacc*100:.2f}%")
print(f"Terminal Growth: {self.g_term*100:.2f}%")
print(f"")
print(f"PV of FCFs: โน{r['sum_pv_fcfs']:>12,.2f} Cr")
print(f"Terminal Value (Gordon): โน{r['tv_gordon']:>12,.2f} Cr")
print(f"PV of Terminal Value: โน{r['pv_tv_gordon']:>12,.2f} Cr")
if r['tv_multiple']:
print(f"Terminal Value (Exit): โน{r['tv_multiple']:>12,.2f} Cr")
print(f"PV of Terminal (Exit): โน{r['pv_tv_multiple']:>12,.2f} Cr")
print(f"")
print(f"Enterprise Value: โน{r['enterprise_value']:>12,.2f} Cr")
print(f"Less: Net Debt: โน{r['net_debt']:>12,.2f} Cr")
print(f"Equity Value: โน{r['equity_value']:>12,.2f} Cr")
print(f"Shares Outstanding: {r['shares']:>12,.2f} Cr")
print(f"Implied Share Price: โน{r['price_per_share']:>12,.2f}")
print("=" * 60)
print("\nProjected FCFs:")
print(self.projections[['Year','Revenue','EBITDA','FCF']].to_string(index=False))
# ========================================
# RUN THE MODEL
# ========================================
model = DCFModel("TATAMOTORS.NS")
print("Step 1: Calculating Beta from market data...")
beta = model.calculate_beta()
print("\nStep 2: Calculating WACC...")
wacc = model.calculate_wacc()
print("\nStep 3: Projecting FCFs...")
# Assumptions for Tata Motors (โน Cr)
projections = model.project_fcfs(
base_ebitda=160000, # ~โน1,60,000 Cr base EBITDA
ebitda_margin=0.12, # 12% EBITDA margin
growth_rates=[0.08, 0.10, 0.12, 0.10, 0.08], # Revenue growth
capex_pct=0.05, # CapEx = 5% of revenue
dnwc_pct=0.01, # Change in NWC = 1% of revenue
da_pct=0.03 # D&A = 3% of revenue
)
print("\nStep 4: Calculating Valuation...")
results = model.value(
shares_outstanding=368, # 368 Cr shares
net_debt=80000, # โน80,000 Cr net debt
exit_multiple=6.0 # 6x EV/EBITDA exit multiple
)
print("\n")
model.summary()
Automated Sensitivity Analysis
Replacing Excel Data Tables with Python โ faster, more flexible, and fully reproducible
๐ Why Python Beats Excel for Sensitivity
| Feature | ๐ Excel Data Tables | ๐ Python |
|---|---|---|
| Two-way table | Max 2 variables; manual setup | Unlimited variables; nested loops |
| Speed | Slow with large models (recalculates everything) | Instant โ pure math, no recalc overhead |
| Visualization | Conditional formatting (basic) | Professional heatmaps with Matplotlib |
| Scenarios | 3 max (Scenario Manager) | Unlimited โ just add more dictionaries |
| Output | Static grid on a tab | DataFrame exportable to Excel / image |
โ๏ธ Worked Example 2: Two-Way Sensitivity + Heatmap
Task: Create a sensitivity table varying WACC (10%โ18%) and terminal growth rate (1%โ5%). Display as a heatmap.
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
def sensitivity_analysis(fcfs, wacc_range, growth_range, net_debt, shares):
"""
Two-way sensitivity: Share Price vs WACC and Terminal Growth.
"""
results = []
for wacc in wacc_range:
row = {}
for g_term in growth_range:
# PV of FCFs
pv_fcfs = sum(f / (1 + wacc)**t for t, f in enumerate(fcfs, 1))
# Terminal Value (Gordon Growth)
terminal_fcf = fcfs[-1] * (1 + g_term)
tv = terminal_fcf / (wacc - g_term)
pv_tv = tv / (1 + wacc)**len(fcfs)
# Equity Value & Share Price
ev = pv_fcfs + pv_tv
equity = ev - net_debt
price = equity / shares
row[f"{g_term*100:.1f}%"] = round(price, 2)
results.append(row)
df = pd.DataFrame(results, index=[f"{w*100:.1f}%" for w in wacc_range])
df.index.name = "WACC \\ Growth"
return df
# Define ranges
wacc_range = np.arange(0.10, 0.185, 0.01) # 10% to 18%
growth_range = np.arange(0.01, 0.055, 0.01) # 1% to 5%
# Using Tata Motors FCFs from Section 1
fcfs = [8640.0, 11616.0, 15360.0, 16128.0, 15360.0] # Example FCFs
sensitivity = sensitivity_analysis(
fcfs=fcfs,
wacc_range=wacc_range,
growth_range=growth_range,
net_debt=80000,
shares=368
)
print("SENSITIVITY TABLE: Implied Share Price (โน)")
print("=" * 60)
print(sensitivity.to_string())
# ========================================
# HEATMAP VISUALIZATION
# ========================================
fig, ax = plt.subplots(figsize=(10, 7))
# Convert to numpy array for heatmap
data = sensitivity.values.astype(float)
im = ax.imshow(data, cmap='RdYlGn', aspect='auto')
ax.set_xticks(range(len(sensitivity.columns)))
ax.set_xticklabels(sensitivity.columns, fontsize=10)
ax.set_yticks(range(len(sensitivity.index)))
ax.set_yticklabels(sensitivity.index, fontsize=10)
ax.set_xlabel('Terminal Growth Rate', fontsize=12, fontweight='bold')
ax.set_ylabel('WACC', fontsize=12, fontweight='bold')
ax.set_title('DCF Sensitivity: Implied Share Price (โน)\nTata Motors',
fontsize=14, fontweight='bold', pad=15)
# Add value annotations
for i in range(len(sensitivity.index)):
for j in range(len(sensitivity.columns)):
val = data[i, j]
color = 'white' if abs(val) > 300 else 'black'
ax.text(j, i, f'โน{val:.0f}', ha='center', va='center',
fontsize=9, fontweight='bold', color=color)
plt.colorbar(im, label='Share Price (โน)', shrink=0.8)
plt.tight_layout()
plt.savefig('sensitivity_heatmap.png', dpi=150, bbox_inches='tight')
plt.show()
print("\nโ
Heatmap saved as sensitivity_heatmap.png")
Key Insight: The sensitivity table instantly shows that the model is highly sensitive to WACC. At 15%+ WACC, the implied price turns negative โ meaning the company isn't generating enough return to cover its cost of capital. This is the kind of insight that takes minutes in Python but requires manual cell-by-cell recalculation in Excel.
โ๏ธ Worked Example 3: Bull / Base / Bear Scenario Analysis
Task: Run 3 scenarios with different growth and margin assumptions, then probability-weight the results.
import pandas as pd
def scenario_dcf(scenarios, wacc=0.1467, g_term=0.03, net_debt=80000, shares=368):
"""Run multiple DCF scenarios and probability-weight them."""
results = []
for name, params in scenarios.items():
# Project FCFs
fcfs = []
base = params['base_revenue']
for i in range(5):
base = base * (1 + params['growth_rates'][i])
ebitda = base * params['ebitda_margin']
fcf = ebitda * params['fcf_conversion'] # Simplified
fcfs.append(fcf)
# DCF Calculation
pv_fcfs = sum(f / (1 + wacc)**t for t, f in enumerate(fcfs, 1))
tv = fcfs[-1] * (1 + g_term) / (wacc - g_term)
pv_tv = tv / (1 + wacc)**5
ev = pv_fcfs + pv_tv
equity = ev - net_debt
price = equity / shares
results.append({
'Scenario': name,
'Probability': f"{params['probability']*100:.0f}%",
'Revenue Y5': round(base, 0),
'FCF Y5': round(fcfs[-1], 0),
'EV': round(ev, 0),
'Equity': round(equity, 0),
'Share Price': round(price, 2),
'Prob-Weighted Price': round(price * params['probability'], 2)
})
df = pd.DataFrame(results)
# Add probability-weighted average
weighted_avg = df['Prob-Weighted Price'].sum()
total_row = pd.DataFrame([{
'Scenario': 'WEIGHTED AVERAGE',
'Probability': '100%',
'Revenue Y5': '-',
'FCF Y5': '-',
'EV': '-',
'Equity': '-',
'Share Price': '-',
'Prob-Weighted Price': round(weighted_avg, 2)
}])
df = pd.concat([df, total_row], ignore_index=True)
return df
# Define scenarios
scenarios = {
'๐ Bull': {
'base_revenue': 450000,
'growth_rates': [0.12, 0.15, 0.18, 0.15, 0.12],
'ebitda_margin': 0.14,
'fcf_conversion': 0.40,
'probability': 0.25
},
'๐ Base': {
'base_revenue': 420000,
'growth_rates': [0.08, 0.10, 0.12, 0.10, 0.08],
'ebitda_margin': 0.12,
'fcf_conversion': 0.35,
'probability': 0.50
},
'๐ป Bear': {
'base_revenue': 400000,
'growth_rates': [0.03, 0.04, 0.05, 0.04, 0.03],
'ebitda_margin': 0.09,
'fcf_conversion': 0.25,
'probability': 0.25
}
}
results = scenario_dcf(scenarios)
print("SCENARIO ANALYSIS โ Tata Motors DCF")
print("=" * 80)
print(results.to_string(index=False))
Professional Visualizations with Matplotlib
Creating publication-quality charts for valuation presentations
๐ Essential Charts for Financial Modeling
| Chart Type | Use Case | Matplotlib Function |
|---|---|---|
| Line Chart | Stock prices, moving averages, revenue trends | plt.plot() |
| Bar Chart | Revenue comparison, peer multiples, FCF by year | plt.bar() |
| Heatmap | Sensitivity tables, correlation matrices | plt.imshow() |
| Waterfall | DCF value bridge: FCFs โ TV โ EV โ Equity | Custom (stacked bars) |
| Pie/Donut | Portfolio allocation, revenue mix | plt.pie() |
| Histogram | Return distribution, Monte Carlo outputs | plt.hist() |
โ๏ธ Worked Example 4: DCF Value Bridge + Peer Comparison Charts
Task: Create (i) a waterfall chart showing the DCF value bridge, and (ii) a peer comparison bar chart.
import matplotlib.pyplot as plt
import numpy as np
# ========================================
# CHART 1: Waterfall โ DCF Value Bridge
# ========================================
def waterfall_chart(categories, values, title="DCF Value Bridge"):
"""Create a professional waterfall chart."""
fig, ax = plt.subplots(figsize=(12, 6))
# Calculate cumulative positions
cumulative = [0]
for v in values[:-1]:
cumulative.append(cumulative[-1] + v)
# Colors: green for positive, red for negative, blue for totals
colors = []
for i, v in enumerate(values):
if i == 0: # First item (PV of FCFs)
colors.append('#2563EB')
elif i == len(values) - 1: # Last item (total)
colors.append('#1E3A5F')
elif v >= 0:
colors.append('#10B981')
else:
colors.append('#EF4444')
# Draw bars
bottoms = []
for i in range(len(values)):
if i == 0:
bottoms.append(0)
elif i == len(values) - 1:
bottoms.append(0)
else:
bottoms.append(cumulative[i] if values[i] >= 0 else cumulative[i] + values[i])
bars = ax.bar(categories, [abs(v) for v in values], bottom=bottoms,
color=colors, edgecolor='white', linewidth=1.5, width=0.6)
# Add value labels
for i, (bar, val) in enumerate(zip(bars, values)):
y_pos = bar.get_y() + bar.get_height() + 500
label = f'โน{val:,.0f}' if val > 0 else f'-โน{abs(val):,.0f}'
ax.text(bar.get_x() + bar.get_width()/2, y_pos, label,
ha='center', va='bottom', fontweight='bold', fontsize=10)
# Connector lines
for i in range(len(values) - 2):
x1 = i + 0.3
x2 = i + 0.7
y = cumulative[i + 1]
ax.plot([x1, x2], [y, y], color='gray', linewidth=1, linestyle='--')
ax.set_title(title, fontsize=14, fontweight='bold', pad=15)
ax.set_ylabel('Value (โน Cr)', fontsize=12)
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.grid(axis='y', alpha=0.3)
plt.tight_layout()
plt.savefig('dcf_waterfall.png', dpi=150, bbox_inches='tight')
plt.show()
# DCF Value Bridge data
categories = ['PV of\nFCFs', 'PV of Terminal\nValue', 'Enterprise\nValue', 'Net\nDebt', 'Equity\nValue']
values = [12845, 72346, 0, -80000, 0]
# Recalculate: EV = PV FCFs + PV TV, Equity = EV - Debt
values[2] = values[0] + values[1] # EV
values[4] = values[2] + values[3] # Equity Value
waterfall_chart(categories, values, "DCF Value Bridge โ Tata Motors (โน Cr)")
# ========================================
# CHART 2: Peer Comparison โ Bar Chart
# ========================================
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
peers = ['Tata Motors', 'M&M', 'Maruti', 'Ashok Leyland']
colors = ['#2563EB', '#10B981', '#F59E0B', '#EF4444']
# P/E Ratio
pe_ratios = [28.5, 32.1, 35.8, 24.2]
axes[0].barh(peers, pe_ratios, color=colors)
axes[0].set_title('P/E Ratio', fontweight='bold')
axes[0].set_xlabel('x')
for i, v in enumerate(pe_ratios):
axes[0].text(v + 0.3, i, f'{v:.1f}x', va='center', fontweight='bold')
# EV/EBITDA
ev_ebitda = [8.2, 14.5, 12.3, 7.8]
axes[1].barh(peers, ev_ebitda, color=colors)
axes[1].set_title('EV/EBITDA', fontweight='bold')
axes[1].set_xlabel('x')
for i, v in enumerate(ev_ebitda):
axes[1].text(v + 0.2, i, f'{v:.1f}x', va='center', fontweight='bold')
# Revenue Growth %
growth = [15.2, 18.5, 12.1, 22.8]
axes[2].barh(peers, growth, color=colors)
axes[2].set_title('Revenue Growth %', fontweight='bold')
axes[2].set_xlabel('%')
for i, v in enumerate(growth):
axes[2].text(v + 0.3, i, f'{v:.1f}%', va='center', fontweight='bold')
for ax in axes:
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.grid(axis='x', alpha=0.3)
fig.suptitle('Auto Sector โ Comparable Company Analysis',
fontsize=14, fontweight='bold', y=1.02)
plt.tight_layout()
plt.savefig('peer_comparison.png', dpi=150, bbox_inches='tight')
plt.show()
print("โ
Charts saved: dcf_waterfall.png, peer_comparison.png")
Jupyter Notebook Workflows & Best Practices
Organizing your work for reproducibility and collaboration
๐ The Professional Notebook Structure
A well-organized notebook follows this structure โ just like a well-organized Excel model has separate tabs for inputs, calculations, and outputs:
# ============================================
# Cell 1: TITLE & DESCRIPTION (Markdown)
# ============================================
# # DCF Valuation: Tata Motors
# **Author:** [Your Name]
# **Date:** 2025-04-30
# **Purpose:** Full DCF model with sensitivity analysis
# ============================================
# Cell 2: IMPORTS
# ============================================
import pandas as pd
import numpy as np
import yfinance as yf
import matplotlib.pyplot as plt
# ============================================
# Cell 3: INPUTS & ASSUMPTIONS (all in one place!)
# ============================================
TICKER = "TATAMOTORS.NS"
RISK_FREE_RATE = 0.07
EQUITY_RISK_PREMIUM = 0.08
TERMINAL_GROWTH = 0.03
PROJECTION_YEARS = 5
TAX_RATE = 0.2517
SHARES_OUTSTANDING = 368 # Cr
NET_DEBT = 80000 # Cr
# Growth assumptions
GROWTH_RATES = [0.08, 0.10, 0.12, 0.10, 0.08]
EBITDA_MARGIN = 0.12
CAPEX_PCT = 0.05
DNWC_PCT = 0.01
# ============================================
# Cell 4โ6: CALCULATIONS (functions)
# ============================================
# [Your DCF functions go here]
# ============================================
# Cell 7: RESULTS & OUTPUT
# ============================================
# [Display results, print summary]
# ============================================
# Cell 8: SENSITIVITY ANALYSIS
# ============================================
# [Sensitivity tables and heatmaps]
# ============================================
# Cell 9: VISUALIZATIONS
# ============================================
# [Charts]
# ============================================
# Cell 10: EXPORT TO EXCEL
# ============================================
# [Export all results]
All assumptions in Cell 3. Never hardcode values inside calculation cells. This is the Python equivalent of having a dedicated "Assumptions" tab in Excel. If someone wants to change the growth rate from 8% to 10%, they change ONE cell โ not hunt through 20 cells of code.
๐ Exporting Professional Reports to Excel
import pandas as pd
from openpyxl import load_workbook
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
def export_dcf_report(filename, projections, sensitivity, scenarios):
"""Export complete DCF analysis to a formatted Excel file."""
# Write DataFrames to Excel
with pd.ExcelWriter(filename, engine='openpyxl') as writer:
projections.to_excel(writer, sheet_name='Projections', index=False)
sensitivity.to_excel(writer, sheet_name='Sensitivity')
scenarios.to_excel(writer, sheet_name='Scenarios', index=False)
# Format the workbook
wb = load_workbook(filename)
# Define styles
header_font = Font(bold=True, color='FFFFFF', size=11)
header_fill = PatternFill(start_color='1E3A5F', end_color='1E3A5F', fill_type='solid')
number_format = '#,##0.00'
thin_border = Border(
left=Side(style='thin'),
right=Side(style='thin'),
top=Side(style='thin'),
bottom=Side(style='thin')
)
# Format each sheet
for sheet_name in wb.sheetnames:
ws = wb[sheet_name]
# Format header row
for cell in ws[1]:
cell.font = header_font
cell.fill = header_fill
cell.alignment = Alignment(horizontal='center')
# Auto-adjust column widths
for column in ws.columns:
max_length = 0
for cell in column:
if cell.value:
max_length = max(max_length, len(str(cell.value)))
ws.column_dimensions[column[0].column_letter].width = max_length + 4
wb.save(filename)
print(f"โ
Report exported: {filename}")
# Usage:
# export_dcf_report('tata_motors_dcf.xlsx', projections, sensitivity, scenarios)
Hands-On Practice Exercises
๐๏ธ Exercise 1: DCF Valuation of Infosys (30 min)
Objective: Build a complete DCF model for Infosys from scratch
| Assumption | Value |
|---|---|
| Ticker | INFY.NS |
| Base Revenue | โน1,62,000 Cr |
| Revenue Growth Rates (5Y) | [7%, 8%, 9%, 8%, 7%] |
| EBITDA Margin | 25% |
| FCF Conversion (of EBITDA) | 60% |
| Risk-Free Rate | 7% |
| Equity Risk Premium | 8% |
| Terminal Growth | 3% |
| Shares Outstanding | 414 Cr |
| Net Debt | -โน15,000 Cr (net cash) |
Tasks:
- Calculate beta from 2-year weekly returns vs Nifty 50
- Calculate WACC
- Project 5-year FCFs
- Calculate EV, Equity Value, and implied share price
- Compare your implied price with the current market price โ is it overvalued or undervalued?
import pandas as pd
import numpy as np
import yfinance as yf
# Reuse the DCFModel class from Section 1
model = DCFModel("INFY.NS", risk_free_rate=0.07, market_premium=0.08, terminal_growth=0.03)
# Step 1: Beta
beta = model.calculate_beta()
# Step 2: WACC
wacc = model.calculate_wacc()
# Step 3: Project FCFs
projections = model.project_fcfs(
base_ebitda=162000,
ebitda_margin=0.25,
growth_rates=[0.07, 0.08, 0.09, 0.08, 0.07],
capex_pct=0.04,
dnwc_pct=0.02,
da_pct=0.04
)
# Step 4: Value
results = model.value(shares_outstanding=414, net_debt=-15000, exit_multiple=15.0)
# Step 5: Compare with market price
current_price = yf.Ticker("INFY.NS").history(period="1d")['Close'].iloc[-1]
upside = (results['price_per_share'] / current_price - 1) * 100
model.summary()
print(f"\nCurrent Market Price: โน{current_price:.2f}")
print(f"Upside/Downside: {upside:+.2f}%")
verdict = "UNDERVALUED โ
" if upside > 10 else "OVERVALUED โ ๏ธ" if upside < -10 else "FAIRLY VALUED โ๏ธ"
print(f"Verdict: {verdict}")
๐๏ธ Exercise 2: Two-Way Sensitivity + Heatmap (20 min)
Objective: Create a sensitivity table for Infosys varying WACC (9%โ17%) and terminal growth (1%โ5%). Generate a professional heatmap.
Tasks:
- Use the
sensitivity_analysis()function from Section 2 - Generate the heatmap using the code provided
- Identify the range of WACC/growth combinations where Infosys is undervalued vs. its current price
Adapt the sensitivity_analysis() and heatmap code from Section 2 using Infosys FCFs and assumptions.
Replace Tata Motors FCFs with Infosys projected FCFs. Change net_debt=-15000 (cash) and shares=414. The heatmap will show a much higher share price range since Infosys is a high-margin IT company with net cash.
๐๏ธ Exercise 3: Sector Peer Dashboard (20 min)
Objective: Create a multi-chart peer comparison dashboard for 5 Indian banks
Banks: HDFC Bank, SBI, ICICI Bank, Kotak Mahindra, Axis Bank
Tasks:
- Fetch P/E, P/B, ROE, and Revenue Growth for all 5 banks using yfinance
- Create a 2ร2 grid of bar charts (P/E, P/B, ROE%, Revenue Growth%)
- Add value labels on each bar
- Highlight the cheapest and most expensive bank
import pandas as pd
import yfinance as yf
import matplotlib.pyplot as plt
banks = {
'HDFC Bank': 'HDFCBANK.NS',
'SBI': 'SBIN.NS',
'ICICI Bank': 'ICICIBANK.NS',
'Kotak': 'KOTAKBANK.NS',
'Axis Bank': 'AXISBANK.NS'
}
data = []
for name, ticker in banks.items():
info = yf.Ticker(ticker).info
data.append({
'Bank': name,
'P/E': info.get('trailingPE', 0),
'P/B': info.get('priceToBook', 0),
'ROE %': (info.get('returnOnEquity', 0) or 0) * 100,
'Rev Growth %': (info.get('revenueGrowth', 0) or 0) * 100
})
df = pd.DataFrame(data)
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
metrics = ['P/E', 'P/B', 'ROE %', 'Rev Growth %']
colors = ['#2563EB', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6']
for idx, metric in enumerate(metrics):
ax = axes[idx // 2][idx % 2]
bars = ax.bar(df['Bank'], df[metric], color=colors, edgecolor='white', linewidth=1)
ax.set_title(metric, fontweight='bold', fontsize=12)
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.grid(axis='y', alpha=0.3)
# Value labels
for bar, val in zip(bars, df[metric]):
ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.5,
f'{val:.1f}', ha='center', fontweight='bold', fontsize=10)
# Highlight cheapest (lowest P/E or P/B) or best (highest ROE)
if metric in ['P/E', 'P/B']:
min_idx = df[metric].idxmin()
bars[min_idx].set_edgecolor('green')
bars[min_idx].set_linewidth(3)
elif metric == 'ROE %':
max_idx = df[metric].idxmax()
bars[max_idx].set_edgecolor('green')
bars[max_idx].set_linewidth(3)
plt.suptitle('Indian Banking Sector โ Peer Comparison',
fontsize=16, fontweight='bold')
plt.tight_layout()
plt.savefig('bank_peers.png', dpi=150, bbox_inches='tight')
plt.show()
๐๏ธ Exercise 4 (Advanced): Automated Valuation Report (20 min)
Objective: Create a script that takes any ticker and generates a complete Excel report with DCF, sensitivity, and peer comparison
Tasks:
- Combine all functions from today's session into a single
valuation_report(ticker)function - The function should: calculate beta, WACC, project FCFs, value the company, run sensitivity, and export to Excel
- Test with 3 different companies across sectors
def valuation_report(ticker, base_revenue, margin, growth_rates,
shares, net_debt, sector_peers=None):
"""Generate complete DCF valuation report for any company."""
print(f"\n{'='*60}")
print(f"VALUATION REPORT: {ticker}")
print(f"{'='*60}")
# 1. Build model
model = DCFModel(ticker)
model.calculate_beta()
model.calculate_wacc()
# 2. Project FCFs
model.project_fcfs(
base_ebitda=base_revenue,
ebitda_margin=margin,
growth_rates=growth_rates,
capex_pct=0.05, dnwc_pct=0.02, da_pct=0.03
)
# 3. Value
model.value(shares_outstanding=shares, net_debt=net_debt)
model.summary()
# 4. Sensitivity
sens = sensitivity_analysis(
fcfs=model.fcfs,
wacc_range=np.arange(0.08, 0.18, 0.02),
growth_range=np.arange(0.01, 0.06, 0.01),
net_debt=net_debt, shares=shares
)
# 5. Export
filename = f"{ticker.replace('.NS','')}_valuation.xlsx"
export_dcf_report(filename, model.projections, sens,
pd.DataFrame([model.results]))
return model
# Test across sectors
valuation_report("TCS.NS", 240000, 0.26, [0.08, 0.09, 0.10, 0.09, 0.08],
shares=380, net_debt=-10000)
valuation_report("RELIANCE.NS", 900000, 0.18, [0.10, 0.12, 0.14, 0.12, 0.10],
shares=677, net_debt=120000)
valuation_report("TATAMOTORS.NS", 420000, 0.12, [0.08, 0.10, 0.12, 0.10, 0.08],
shares=368, net_debt=80000)
๐ Key Terms โ Click to Flip
Test Your Understanding
10 questions on Python Financial Modeling โ II
Key Takeaways
๐ What We Covered Today
- Built a full DCF model class in Python with automated beta calculation, WACC via CAPM, FCF projection, and dual terminal value methods
- Created two-way sensitivity tables that replace Excel Data Tables โ with professional heatmaps via Matplotlib
- Ran Bull/Base/Bear scenario analysis with probability-weighted average valuation
- Created professional charts: waterfall (DCF value bridge), bar charts (peer comparison), and heatmaps
- Learned Jupyter notebook best practices: all assumptions in one cell, functions for calculations, professional structure
- Exported results to formatted Excel reports with OpenPyXL styling
- Built an automated valuation pipeline that generates a complete report from just a ticker symbol
Session 21: AI-Enhanced Forecasting
We'll move beyond manual assumptions to machine learning-powered forecasting: linear regression for revenue prediction, time-series decomposition, and feature engineering for financial models. Read: Lรณpez de Prado, Ch. 3