Learning Objectives

What You'll Learn Today

Section 1

Building a Full DCF Model in Python

From basic skeleton to production-grade valuation model

๐Ÿ’ญ
Think About It

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:

ComponentSession 19 (Skeleton)Session 20 (Full Model)
FCF ProjectionsHardcoded listCalculated from revenue growth + margin assumptions
WACCInput parameterCalculated via CAPM using market data
Terminal ValueGordon Growth onlyGordon Growth + Exit Multiple (cross-check)
SensitivityNoneAutomated 2-way tables + heatmaps
ScenariosNoneBull / Base / Bear with probability weighting
OutputPrint statementsProfessional 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.

Python โ€” Complete DCF Model Class
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()
Step 1: Calculating Beta from market data... Beta: 1.35 Step 2: Calculating WACC... Cost of Equity: 17.80% Cost of Debt: 9.00% Equity Weight: 72.3% Debt Weight: 27.7% WACC: 14.67% Step 4: Calculating Valuation... ============================================================ DCF VALUATION: TATAMOTORS.NS ============================================================ WACC: 14.67% Terminal Growth: 3.00% PV of FCFs: โ‚น 12,845.32 Cr Terminal Value (Gordon): โ‚น 1,45,230.00 Cr PV of Terminal Value: โ‚น 72,345.67 Cr Terminal Value (Exit): โ‚น 1,28,640.00 Cr PV of Terminal (Exit): โ‚น 64,128.50 Cr Enterprise Value: โ‚น 85,190.99 Cr Less: Net Debt: โ‚น 80,000.00 Cr Equity Value: โ‚น 5,190.99 Cr Shares Outstanding: 368.00 Cr Implied Share Price: โ‚น 14.11 ============================================================
Section 2

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 tableMax 2 variables; manual setupUnlimited variables; nested loops
SpeedSlow with large models (recalculates everything)Instant โ€” pure math, no recalc overhead
VisualizationConditional formatting (basic)Professional heatmaps with Matplotlib
Scenarios3 max (Scenario Manager)Unlimited โ€” just add more dictionaries
OutputStatic grid on a tabDataFrame 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.

Python โ€” Two-Way Sensitivity Table
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")
SENSITIVITY TABLE: Implied Share Price (โ‚น) ============================================================ WACC \ Growth 1.0% 2.0% 3.0% 4.0% 5.0% 10.0% โ‚น432 โ‚น528 โ‚น672 โ‚น912 โ‚น1,392 11.0% โ‚น312 โ‚น378 โ‚น468 โ‚น606 โ‚น840 12.0% โ‚น210 โ‚น252 โ‚น306 โ‚น378 โ‚น486 13.0% โ‚น120 โ‚น144 โ‚น174 โ‚น210 โ‚น264 14.0% โ‚น 36 โ‚น 48 โ‚น 60 โ‚น 72 โ‚น 84 15.0% โ‚น -42 โ‚น -30 โ‚น -18 โ‚น -6 โ‚น 6 16.0% โ‚น -114 โ‚น -102 โ‚น -90 โ‚น -78 โ‚น -66 17.0% โ‚น -180 โ‚น -168 โ‚น -156 โ‚น -144 โ‚น -132 18.0% โ‚น -240 โ‚น -228 โ‚น -216 โ‚น -204 โ‚น -192

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.

Python โ€” Scenario Analysis
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))
SCENARIO ANALYSIS โ€” Tata Motors DCF ================================================================================ Scenario Probability Revenue Y5 FCF Y5 EV Equity Share Price Prob-Weighted Price ๐Ÿ‚ Bull 25% 717366.0 40172.0 284523.0 204523.0 555.77 138.94 ๐Ÿ“Š Base 50% 636138.0 26717.0 167435.0 87435.0 237.60 118.80 ๐Ÿป Bear 25% 470544.0 10587.0 42678.0 -37322.0 -101.42 -25.36 WEIGHTED AVERAGE 100% - - - - - 232.38
Section 3

Professional Visualizations with Matplotlib

Creating publication-quality charts for valuation presentations

๐Ÿ“Š Essential Charts for Financial Modeling

Chart TypeUse CaseMatplotlib Function
Line ChartStock prices, moving averages, revenue trendsplt.plot()
Bar ChartRevenue comparison, peer multiples, FCF by yearplt.bar()
HeatmapSensitivity tables, correlation matricesplt.imshow()
WaterfallDCF value bridge: FCFs โ†’ TV โ†’ EV โ†’ EquityCustom (stacked bars)
Pie/DonutPortfolio allocation, revenue mixplt.pie()
HistogramReturn distribution, Monte Carlo outputsplt.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.

Python โ€” Waterfall Chart (DCF Value Bridge)
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")
Section 4

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:

Jupyter Notebook โ€” Recommended Structure
# ============================================
# 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]
๐Ÿ’กGolden Rule

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

Python โ€” Multi-Sheet Excel Report
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)
Python Lab

Hands-On Practice Exercises

๐Ÿ‹๏ธ Exercise 1: DCF Valuation of Infosys (30 min)

Objective: Build a complete DCF model for Infosys from scratch

AssumptionValue
TickerINFY.NS
Base Revenueโ‚น1,62,000 Cr
Revenue Growth Rates (5Y)[7%, 8%, 9%, 8%, 7%]
EBITDA Margin25%
FCF Conversion (of EBITDA)60%
Risk-Free Rate7%
Equity Risk Premium8%
Terminal Growth3%
Shares Outstanding414 Cr
Net Debt-โ‚น15,000 Cr (net cash)

Tasks:

  1. Calculate beta from 2-year weekly returns vs Nifty 50
  2. Calculate WACC
  3. Project 5-year FCFs
  4. Calculate EV, Equity Value, and implied share price
  5. Compare your implied price with the current market price โ€” is it overvalued or undervalued?
Python โ€” Infosys DCF
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:

  1. Use the sensitivity_analysis() function from Section 2
  2. Generate the heatmap using the code provided
  3. 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.

Key Modification

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:

  1. Fetch P/E, P/B, ROE, and Revenue Growth for all 5 banks using yfinance
  2. Create a 2ร—2 grid of bar charts (P/E, P/B, ROE%, Revenue Growth%)
  3. Add value labels on each bar
  4. Highlight the cheapest and most expensive bank
Python โ€” Bank Peer Dashboard
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:

  1. Combine all functions from today's session into a single valuation_report(ticker) function
  2. The function should: calculate beta, WACC, project FCFs, value the company, run sensitivity, and export to Excel
  3. Test with 3 different companies across sectors
Python โ€” Automated Valuation Report
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)
Quick Review

๐Ÿ“š Key Terms โ€” Click to Flip

Knowledge Check

Test Your Understanding

10 questions on Python Financial Modeling โ€“ II

Summary

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
๐Ÿ“šNext Session

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