Sessions 17–18 of 26 Part VI: Relative Valuation — Chapter 16 of 18
Part VI: Relative Valuation Chapter 16 of 18 Sessions 17–18 Python

EV Multiples & Peer Comparison Dashboard

Master enterprise-value-based multiples — EV/EBITDA, EV/Sales, EV/IC — and build an interactive peer comparison dashboard that brings relative valuation to life.

Learning Objectives

Session 17 — EV/EBITDA & Other Multiples

16.1 Why EV-Based Multiples Are Superior for Cross-Company Comparison

In Chapter 15, we used P/E and P/B — equity multiples. These have a fundamental flaw: they are affected by capital structure. Two identical companies with different debt levels will have different P/E ratios even if their operations are identical. Enterprise value (EV) multiples solve this problem by using a numerator (EV) that is independent of capital structure and a denominator (EBITDA, EBIT, Sales) that is pre-interest and thus also capital-structure-neutral.

Equity Multiples (P/E, P/B)

Numerator: Market Cap (equity only)
Denominator: Net Income, Book Value (after interest)
Problem: Both numerator and denominator are affected by debt. A leveraged company with high interest expense may show a deceptively high P/E.

Enterprise Value Multiples (EV/EBITDA, EV/Sales)

Numerator: EV = Market Cap + Debt − Cash
Denominator: EBITDA, EBIT, Sales (before interest)
Advantage: Capital-structure-neutral. Compares operating performance, not financing choices.

💡
Pro Tip: For relative valuation of non-financial companies, EV/EBITDA should be your primary multiple. It is capital-structure-neutral, unaffected by depreciation policy (unlike P/E which is affected by D&A), and widely used by practitioners. P/E is a useful supplement, especially for communication ("the stock trades at 20x earnings"), but EV/EBITDA is the professional's workhorse.

16.2 EV/EBITDA: The Workhorse Enterprise Multiple

EV/EBITDA is the most widely used enterprise value multiple in practice. It is the default multiple for M&A transactions, leveraged buyout (LBO) analysis, and cross-border comparisons. Its dominance stems from the properties of EBITDA: it approximates operating cash flow, is unaffected by capital structure and depreciation policy, and is widely available.

EV/EBITDA = (Market Cap + Total Debt + Preferred + Minority Interest − Cash) / EBITDA

16.2.1 The Fundamental Drivers of EV/EBITDA

Like all multiples, EV/EBITDA is driven by fundamentals. A company deserves a higher EV/EBITDA if it has:

import yfinance as yf
import pandas as pd
import numpy as np

def compute_ev_and_multiples(ticker_symbol):
    """
    Compute Enterprise Value and key EV-based multiples for a company.

    Returns EV, EV/EBITDA, EV/EBIT, EV/Sales, EV/IC.
    """
    ticker = yf.Ticker(ticker_symbol)
    info = ticker.info

    # Market Cap
    mkt_cap = info.get('marketCap', 0) / 1e7  # Rs. Crore

    # Balance sheet items
    bs = ticker.balance_sheet
    def safe_get(df, candidates):
        for c in ([candidates] if isinstance(candidates, str) else candidates):
            if c in df.index:
                return df.loc[c]
        return None

    total_debt = safe_get(bs, ['Total Debt', 'Long Term Debt'])
    total_debt = total_debt.iloc[0] / 1e7 if total_debt is not None else 0
    cash = safe_get(bs, ['Cash And Cash Equivalents'])
    cash = cash.iloc[0] / 1e7 if cash is not None else 0
    minority = safe_get(bs, ['Minority Interest'])
    minority = minority.iloc[0] / 1e7 if minority is not None else 0

    # Income statement items
    is_df = ticker.financials
    ebitda = safe_get(is_df, ['EBITDA'])
    ebit = safe_get(is_df, ['Operating Income', 'EBIT'])
    revenue = safe_get(is_df, ['Total Revenue', 'Revenue'])

    ebitda_val = ebitda.iloc[0] / 1e7 if ebitda is not None else None
    ebit_val = ebit.iloc[0] / 1e7 if ebit is not None else None
    revenue_val = revenue.iloc[0] / 1e7 if revenue is not None else None

    # Enterprise Value
    ev = mkt_cap + total_debt + minority - cash

    # Multiples
    multiples = {
        'Ticker': ticker_symbol.replace('.NS', ''),
        'Company': info.get('longName', ticker_symbol)[:30],
        'Market Cap': round(mkt_cap, 0),
        'Total Debt': round(total_debt, 0),
        'Cash': round(cash, 0),
        'Enterprise Value': round(ev, 0),
    }

    if ebitda_val and ebitda_val > 0:
        multiples['EV/EBITDA'] = round(ev / ebitda_val, 1)
    if ebit_val and ebit_val > 0:
        multiples['EV/EBIT'] = round(ev / ebit_val, 1)
    if revenue_val and revenue_val > 0:
        multiples['EV/Sales'] = round(ev / revenue_val, 1)

    # EV/Invested Capital
    equity = safe_get(bs, ['Total Equity Gross Minority Interest', 'Stockholders Equity'])
    if equity is not None:
        ic = equity.iloc[0] / 1e7 + total_debt - cash
        if ic > 0:
            multiples['EV/IC'] = round(ev / ic, 1)

    return multiples


# --- Compute EV multiples for a sector ---
it_companies = ['TCS.NS', 'INFY.NS', 'WIPRO.NS', 'HCLTECH.NS', 'TECHM.NS',
                'LTIM.NS', 'PERSISTENT.NS', 'COFORGE.NS']
ev_data = []
for t in it_companies:
    try:
        ev_data.append(compute_ev_and_multiples(t))
    except Exception as e:
        print(f"Error for {t}: {e}")

ev_df = pd.DataFrame(ev_data).set_index('Ticker')

print("=== EV MULTIPLES — INDIAN IT SERVICES ===")
cols = ['EV/EBITDA', 'EV/EBIT', 'EV/Sales', 'EV/IC']
print(ev_df[cols].to_string())

# Sector statistics
print(f"\n=== SECTOR STATISTICS ===")
for col in cols:
    if col in ev_df.columns:
        data = ev_df[col].dropna()
        print(f"  {col:<15}: Median={data.median():.1f}x, "
              f"Mean={data.mean():.1f}x, "
              f"P25={data.quantile(0.25):.1f}x, "
              f"P75={data.quantile(0.75):.1f}x")

16.2.2 Sector Benchmarks for EV/EBITDA

SectorTypical EV/EBITDA Range (India)Key Driver of Variation
IT Services12–22xRevenue growth, margins (TCS premium over mid-tier)
Consumer Staples25–45xBrand strength, revenue growth visibility
Consumer Discretionary18–35xGrowth, ROIC (Titan, Asian Paints at premium)
Pharma12–25xUS FDA compliance, patent pipeline, R&D productivity
Cement10–18xCapacity growth, regional pricing power, cost position
Automotive8–18xProduct cycle, EV transition exposure, export mix
Telecom5–10xARPU trend, spectrum liabilities, competitive intensity
Metals & Mining4–8xCommodity cycle position, cost curve position

16.3 EV/Sales and EV/EBIT: When EBITDA Is Not Enough

16.3.1 EV/Sales: For Companies That Don't Have Earnings

EV/Sales (or EV/Revenue) is used when a company has negative or negligible EBITDA — common for high-growth startups, early-stage companies, and turnaround situations. It is the bluntest of the EV multiples (revenue ignores all cost structure differences) but sometimes the only usable one.

EV/Sales = Enterprise Value / Revenue

Limitations: EV/Sales ignores profitability entirely. Two companies with identical revenue but one with 30% EBITDA margins and the other with 5% should not trade at the same EV/Sales. Always pair EV/Sales with a profitability metric.

16.3.2 EV/EBIT: When Depreciation Matters

EV/EBIT is a stricter multiple than EV/EBITDA because it accounts for depreciation — a real economic cost. For capital-intensive industries (cement, steel, telecom), EV/EBIT may be more appropriate than EV/EBITDA because depreciation is material and ignoring it overstates cash generation capacity.

EV/EBIT = Enterprise Value / EBIT

16.3.3 EV/Invested Capital: The ROIC Connection

EV/Invested Capital (EV/IC) directly links relative valuation to the ROIC framework we built in Chapter 5. The relationship is:

EV/IC = (ROIC − g) / (WACC − g)

A company with ROIC > WACC should trade at EV/IC > 1.0. A company with ROIC < WACC should trade at EV/IC < 1.0. This is the same value creation logic from Chapter 5, expressed as a multiple.

# --- Compare EV/IC vs ROIC across companies ---
print("\n=== EV/IC vs ROIC — The Value Creation Link ===")
for _, row in ev_df.iterrows():
    tkr = row.name
    ev_ic = row.get('EV/IC')
    if ev_ic:
        ticker = yf.Ticker(tkr + '.NS')
        roe = ticker.info.get('returnOnEquity', 0) * 100
        # Approximate ROIC from ROE (simplified)
        approx_roic = roe * 0.75  # rough adjustment for leverage
        value_creator = '✓ CREATOR' if ev_ic > 1.0 else '✗ DESTROYER'
        print(f"  {tkr:<12}: EV/IC = {ev_ic:.1f}x, ROE ≈ {roe:.0f}%,  "
              f"→ {value_creator}")

16.4 Industry-Specific Multiples

Some sectors require specialized multiples because standard financial metrics (EBITDA, EBIT) do not capture the key value driver. These industry-specific multiples are widely used in practice and often appear in M&A transaction comps.

SectorSpecialized MultipleWhy Standard Multiples Fail
Banking / NBFCP/B, P/E (equity multiples are standard because debt is operating)EV is meaningless for banks — debt is not financing, it is the raw material (deposits). Use P/B and P/E.
InsuranceP/Embedded Value (P/EV), P/BEmbedded value captures the present value of in-force policies plus net asset value.
TelecomEV/Subscriber, EV/TowerRevenue per subscriber and infrastructure per tower drive value more directly than EBITDA.
RetailEV/Store, EV/Sq Ft, Revenue/Sq FtStore-level economics drive value. Two retailers with the same EBITDA but different store counts have different growth trajectories.
Cement / SteelEV/Tonne (capacity), EV/MTCommodity businesses; value is driven by capacity × margin per tonne. Replacement cost is a key anchor.
Oil & GasEV/Barrel of Reserves, EV/Daily ProductionReserves are the primary asset. EBITDA is a function of commodity prices and does not capture reserve life.
Power / UtilitiesEV/MW (capacity), P/B (regulated book)Regulated utilities earn a fixed return on regulated equity; capacity drives revenue potential.
Real EstateP/NAV (Net Asset Value), EV/Sq FtNAV captures the fair value of land bank and development projects; earnings are lumpy.
E-commerce / SaaSEV/Revenue, EV/Gross Profit, EV/GMVMost are loss-making at the EBITDA level. Revenue or gross profit multiples are the primary valuation tool.
📝
Note: Industry-specific multiples are not alternatives to standard multiples — they are supplements. A telecom analyst should report EV/EBITDA AND EV/Subscriber. A cement analyst should report EV/EBITDA AND EV/Tonne. The industry-specific multiple provides a sanity check on the standard multiple. If EV/Tonne says the company is cheap but EV/EBITDA says it is expensive, one of them is missing something.
Session 18 — Peer Comparison Dashboard

16.5 Statistical Analysis of Peer Multiples

Peer multiples are not a single number — they form a distribution. Understanding that distribution is essential. A target company trading at the peer median means something very different from trading at the 90th percentile.

import matplotlib.pyplot as plt
import seaborn as sns
from scipy import stats

def analyze_multiple_distribution(ev_df, multiple_col='EV/EBITDA',
                                    target_ticker='INFY'):
    """
    Statistical analysis of a peer multiple distribution.

    Plots histogram, box plot, and shows percentile ranks.
    """
    data = ev_df[multiple_col].dropna()
    target_val = data.get(target_ticker) if target_ticker in data.index else None

    fig, axes = plt.subplots(1, 3, figsize=(18, 5))

    # 1. Histogram with KDE
    ax = axes[0]
    ax.hist(data, bins=min(len(data), 12), color='#6c8cff', alpha=0.7,
            edgecolor='white', linewidth=1)
    sns.kdeplot(data, ax=ax, color='#00c9a7', linewidth=2)
    if target_val:
        ax.axvline(x=target_val, color='#e0556a', linestyle='--', linewidth=2.5,
                   label=f'{target_ticker}: {target_val:.1f}x')
    ax.axvline(x=data.median(), color='#f0a040', linestyle='-', linewidth=1.5,
               label=f'Median: {data.median():.1f}x')
    ax.set_xlabel(multiple_col)
    ax.set_title(f'{multiple_col} Distribution', fontweight='bold')
    ax.legend(fontsize=9)

    # 2. Horizontal bar chart — ranked
    ax = axes[1]
    ranked = data.sort_values()
    colors = ['#e0556a' if i == ranked.index.get_loc(target_ticker) else '#6c8cff'
              for i in ranked.index] if target_ticker in ranked.index else ['#6c8cff']*len(ranked)
    ax.barh(ranked.index, ranked.values, color=colors, edgecolor='white')
    if target_val and target_ticker in ranked.index:
        ax.axvline(x=target_val, color='#e0556a', linestyle='--', linewidth=2)
    ax.axvline(x=data.median(), color='#f0a040', linestyle='-', linewidth=1.5)
    ax.set_xlabel(multiple_col)
    ax.set_title('Peer Ranking', fontweight='bold')

    # 3. Percentile analysis
    ax = axes[3] if len(axes) > 3 else None  # We only have 3

    # Print statistics
    print(f"\n=== {multiple_col} — PEER DISTRIBUTION ANALYSIS ===")
    print(f"  N:               {len(data)}")
    print(f"  Mean:            {data.mean():.1f}x")
    print(f"  Median:          {data.median():.1f}x")
    print(f"  Std Dev:         {data.std():.1f}x")
    print(f"  Min / Max:       {data.min():.1f}x / {data.max():.1f}x")
    print(f"  P25 / P75:       {data.quantile(0.25):.1f}x / {data.quantile(0.75):.1f}x")

    if target_val:
        pct_rank = stats.percentileofscore(data, target_val)
        print(f"\n  Target ({target_ticker}): {target_val:.1f}x")
        print(f"  Percentile Rank:  {pct_rank:.0f}th percentile")
        if pct_rank < 25:
            print(f"  → Trading at a DISCOUNT to peers — potentially undervalued")
        elif pct_rank > 75:
            print(f"  → Trading at a PREMIUM to peers — growth/quality premium must be justified")
        else:
            print(f"  → Trading in line with peers")

    # Outlier detection (IQR method)
    Q1, Q3 = data.quantile(0.25), data.quantile(0.75)
    IQR = Q3 - Q1
    outliers = data[(data < Q1 - 1.5 * IQR) | (data > Q3 + 1.5 * IQR)]
    if len(outliers) > 0:
        print(f"\n  Outliers detected (IQR method):")
        for ticker, val in outliers.items():
            print(f"    {ticker}: {val:.1f}x")

    # 3. Box plot
    ax = axes[2]
    box_data = [data.values]
    bp = ax.boxplot(box_data, patch_artist=True, widths=0.4)
    bp['boxes'][0].set_facecolor('#6c8cff')
    bp['boxes'][0].set_alpha(0.6)
    # Scatter individual points
    y = np.random.normal(1, 0.02, len(data))
    ax.scatter(data.values, y, alpha=0.7, s=60, color='#6c8cff', edgecolors='white')
    if target_val:
        ax.scatter([target_val], [1], s=200, color='#e0556a', zorder=10,
                   edgecolors='white', linewidth=2)
        ax.annotate(target_ticker, (target_val, 1),
                   textcoords="offset points", xytext=(10, 15),
                   fontweight='bold', color='#e0556a')
    ax.set_xlabel(multiple_col)
    ax.set_title('Box Plot with Individual Points', fontweight='bold')
    ax.set_yticks([])

    plt.suptitle(f'Peer Multiple Analysis — {multiple_col}',
                 fontweight='bold', fontsize=14, y=1.02)
    plt.tight_layout()
    plt.show()

    return data


# --- Analyze EV/EBITDA distribution ---
ev_ebitda_data = analyze_multiple_distribution(ev_df, 'EV/EBITDA', 'INFY')

16.6 Interactive Peer Comparison Dashboard with Plotly

A static screenshot is worth a thousand numbers. An interactive dashboard — where stakeholders can hover, click, zoom, and filter — is worth a thousand screenshots. Plotly makes this possible in Python with a few lines of code.

import plotly.graph_objects as go
import plotly.express as px
from plotly.subplots import make_subplots

def build_peer_comparison_dashboard(ev_df, target_ticker='INFY', sector='IT Services'):
    """
    Build an interactive peer comparison dashboard using Plotly.

    Includes:
      - EV/EBITDA bar chart with benchmark lines
      - Growth vs EV/EBITDA scatter (bubble chart)
      - Multiples comparison table (heatmap-style)
      - Peer percentile gauge for the target
    """
    # Fetch growth data
    growth_data = {}
    for tkr in ev_df.index:
        try:
            info = yf.Ticker(tkr + '.NS').info
            growth_data[tkr] = info.get('revenueGrowth', 0) * 100
        except:
            growth_data[tkr] = 0

    ev_df['Revenue Growth %'] = ev_df.index.map(growth_data)

    target_val = ev_df.loc[target_ticker, 'EV/EBITDA'] if target_ticker in ev_df.index else None
    median_val = ev_df['EV/EBITDA'].median()

    # --- Create Dashboard ---
    fig = make_subplots(
        rows=2, cols=2,
        subplot_titles=(
            'EV/EBITDA by Company', 'Growth vs EV/EBITDA',
            'Multiple Comparison Heatmap', 'Peer Percentile Gauge'
        ),
        specs=[[{'type': 'bar'}, {'type': 'scatter'}],
               [{'type': 'table'}, {'type': 'indicator'}]],
        vertical_spacing=0.12, horizontal_spacing=0.10
    )

    # 1. EV/EBITDA Bar Chart
    colors_bar = ['#e0556a' if t == target_ticker else '#6c8cff' for t in ev_df.index]
    fig.add_trace(
        go.Bar(x=ev_df.index, y=ev_df['EV/EBITDA'], marker_color=colors_bar,
               text=ev_df['EV/EBITDA'].round(1), textposition='outside',
               name='EV/EBITDA'),
        row=1, col=1
    )
    fig.add_hline(y=median_val, line_dash='dash', line_color='#f0a040',
                  annotation_text=f'Median: {median_val:.1f}x', row=1, col=1)

    # 2. Growth vs EV/EBITDA Scatter
    valid = ev_df[ev_df['Revenue Growth %'].notna() & ev_df['EV/EBITDA'].notna()]
    fig.add_trace(
        go.Scatter(x=valid['Revenue Growth %'], y=valid['EV/EBITDA'],
                   mode='markers+text', text=valid.index,
                   textposition='top center',
                   marker=dict(
                       size=valid['Enterprise Value'] / valid['Enterprise Value'].max() * 60 + 10,
                       color=['#e0556a' if t == target_ticker else '#6c8cff' for t in valid.index],
                       line=dict(color='white', width=1.5)
                   ),
                   name='Companies'),
        row=1, col=2
    )
    fig.update_xaxes(title_text='Revenue Growth (%)', row=1, col=2)
    fig.update_yaxes(title_text='EV/EBITDA (x)', row=1, col=2)

    # 3. Multiples Comparison Table (heatmap-style using a table)
    table_cols = ['EV/EBITDA', 'EV/EBIT', 'EV/Sales', 'EV/IC']
    table_data = ev_df[table_cols].round(1)
    # Color-code: green = above median, red = below median
    fig.add_trace(
        go.Table(
            header=dict(values=['Ticker'] + table_cols,
                       fill_color='#1c1c2e', font=dict(color='#6c8cff', size=11),
                       align='left'),
            cells=dict(values=[table_data.index.tolist()] +
                              [table_data[c].tolist() for c in table_cols],
                       fill_color='#161625', font=dict(color='#e4e4ed', size=10),
                       align='left'),
        ),
        row=2, col=1
    )

    # 4. Percentile Gauge for Target
    if target_val:
        pct_rank = stats.percentileofscore(ev_df['EV/EBITDA'].dropna(), target_val)
        fig.add_trace(
            go.Indicator(
                mode='gauge+number+delta',
                value=target_val,
                delta={'reference': median_val, 'relative': False,
                       'increasing.color': '#e0556a', 'decreasing.color': '#00c9a7'},
                gauge={
                    'axis': {'range': [ev_df['EV/EBITDA'].min(),
                                        ev_df['EV/EBITDA'].max()]},
                    'bar': {'color': '#e0556a' if target_val > median_val else '#00c9a7'},
                    'steps': [
                        {'range': [ev_df['EV/EBITDA'].min(), ev_df['EV/EBITDA'].quantile(0.25)],
                         'color': '#00c9a733'},
                        {'range': [ev_df['EV/EBITDA'].quantile(0.25), ev_df['EV/EBITDA'].quantile(0.75)],
                         'color': '#6c8cff33'},
                        {'range': [ev_df['EV/EBITDA'].quantile(0.75), ev_df['EV/EBITDA'].max()],
                         'color': '#e0556a33'},
                    ],
                    'threshold': {
                        'line': {'color': '#f0a040', 'width': 2},
                        'thickness': 0.75, 'value': median_val
                    }
                },
                title={'text': f'{target_ticker} EV/EBITDA
' f'' f'Percentile: {pct_rank:.0f}th | ' f'Median: {median_val:.1f}x'} ), row=2, col=2 ) fig.update_layout( title=dict(text=f'{sector} — Peer Comparison Dashboard', font=dict(size=18, color='#e4e4ed')), height=900, width=1400, template='plotly_dark', showlegend=False, paper_bgcolor='#0f0f1a', plot_bgcolor='#0f0f1a', font=dict(color='#e4e4ed'), ) fig.show() return fig # --- Build the interactive dashboard --- # dashboard = build_peer_comparison_dashboard(ev_df, target_ticker='INFY') # dashboard.show() # Opens in browser
🌎
Real World: The interactive Plotly dashboard is far more effective than static charts in stakeholder meetings. When an investment committee member asks "What if we compare Infosys to only the top 3 IT companies?", you can filter with one click and the entire dashboard updates. This responsiveness builds credibility — it shows you have mastered the data, not just memorized a few numbers. For the capstone project, include at least one Plotly dashboard alongside your static DCF exhibits.

16.7 The Complete Relative Valuation Pipeline

Combine Chapters 15 and 16 into a single relative valuation pipeline that computes both equity and enterprise multiples, identifies peers, and produces the target price range:

def complete_relative_valuation(target_ticker, candidate_peers,
                                 use_median=True):
    """
    Complete relative valuation combining equity and EV multiples.

    Returns:
      - Peer group with all multiples
      - Target implied prices from each multiple
      - Valuation range (low, mid, high)
      - Dashboard
    """
    # Fetch target data
    target = yf.Ticker(target_ticker)
    t_info = target.info
    t_price = t_info.get('currentPrice') or t_info.get('previousClose')

    # Compute EV multiples for all candidates
    all_ev = []
    for tkr in candidate_peers:
        try:
            all_ev.append(compute_ev_and_multiples(tkr))
        except:
            pass
    ev_df = pd.DataFrame(all_ev).set_index('Ticker')

    # Also fetch P/E and P/B (from Chapter 15 functions)
    pe_values, pb_values = {}, {}
    for tkr in ev_df.index:
        try:
            info = yf.Ticker(tkr + '.NS').info
            pe_values[tkr] = info.get('trailingPE')
            pb_values[tkr] = info.get('priceToBook')
        except:
            pass

    ev_df['P/E'] = ev_df.index.map(pe_values)
    ev_df['P/B'] = ev_df.index.map(pb_values)

    # Peer median multiples
    agg_func = 'median' if use_median else 'mean'
    peer_medians = ev_df.agg(agg_func)

    # Target fundamentals
    t_eps = t_info.get('trailingEps')
    t_bv = t_info.get('bookValue')
    t_ebitda = None  # Compute from financials
    t_sales = None

    # Implied prices from each multiple
    implied = {}
    if t_eps and peer_medians.get('P/E'):
        implied['P/E Target'] = round(peer_medians['P/E'] * t_eps, 0)
    if t_bv and peer_medians.get('P/B'):
        implied['P/B Target'] = round(peer_medians['P/B'] * t_bv, 0)

    values = list(implied.values())
    if values:
        low_val = np.percentile(values, 25)
        high_val = np.percentile(values, 75)
        mid_val = np.median(values)

        print(f"\n{'='*55}")
        print(f"  RELATIVE VALUATION SUMMARY: "
              f"{t_info.get('longName', target_ticker)}")
        print(f"{'='*55}")
        print(f"\n  Peer Group: {len(ev_df)} companies")
        print(f"\n  Peer Median Multiples:")
        for col in ['EV/EBITDA', 'EV/EBIT', 'EV/Sales', 'P/E', 'P/B']:
            if col in peer_medians:
                print(f"    {col:<15}: {peer_medians[col]:.1f}x")

        print(f"\n  Implied Target Prices:")
        for method, price in implied.items():
            upside = (price / t_price - 1) * 100
            print(f"    {method:<15}: Rs. {price:,.0f} ({upside:+.1f}%)")

        print(f"\n  Valuation Range:")
        print(f"    Low:  Rs. {low_val:,.0f}")
        print(f"    Mid:  Rs. {mid_val:,.0f}")
        print(f"    High: Rs. {high_val:,.0f}")
        print(f"    Current Market Price: Rs. {t_price:,.0f}")

    return {'ev_df': ev_df, 'peer_medians': peer_medians,
            'implied_prices': implied, 'target_price': t_price}


# --- Run complete relative valuation ---
rel_val_complete = complete_relative_valuation('INFY.NS', it_companies)

Hands-On Project: EV Multiples Analysis and Peer Dashboard

Complete the EV multiples analysis for your capstone company. Compute EV/EBITDA, EV/EBIT, EV/Sales, and EV/IC for your peer group. Build the statistical distribution analysis and the interactive Plotly dashboard. Produce a final relative valuation summary with a target price range.

Steps

  1. Compute EV and all EV multiples for your capstone company and 8–12 peer companies using the pipeline.
  2. Analyze the EV/EBITDA distribution: Where does your target rank? Is it at a premium or discount? Can the premium be justified by superior growth, ROIC, or margins?
  3. Check for sector-appropriate industry-specific multiples (Section 16.4). If your sector has one, compute it for all peers.
  4. Build the Plotly interactive dashboard (Section 16.6). Ensure it includes at minimum: EV/EBITDA bar chart, Growth vs EV/EBITDA scatter, multiples comparison table, and the percentile gauge.
  5. Derive the relative valuation target price range (Low-Mid-High) from the peer multiples.
  6. Triangulate with DCF: Create a single summary table showing: Current Market Price, DCF Intrinsic Value (Ch14), P/E Target (Ch15), P/B Target (Ch15), EV/EBITDA Implied Value (Ch16). Which methods cluster together? Which are outliers?
View Solution / Walkthrough

Valuation Triangulation — Infosys (Illustrative)

# ================================================================
# FINAL VALUATION TRIANGULATION
# ================================================================

# Values from DCF (Ch14) and Relative Valuation (Ch15-16)
dcf_iv = 1850
pe_target = 1920
pb_target = 1680
ev_ebitda_implied = 1780  # EV/EBITDA × target EBITDA → EV → Equity → per share
market_price = 1580

fig, ax = plt.subplots(figsize=(12, 7))
methods = ['Current\nMarket Price', 'DCF\nIntrinsic Value',
           'P/E\nMethod', 'P/B\nMethod', 'EV/EBITDA\nMethod']
values = [market_price, dcf_iv, pe_target, pb_target, ev_ebitda_implied]
colors = ['#a0a0b8', '#6c8cff', '#00c9a7', '#f0a040', '#e0556a']

bars = ax.bar(methods, values, color=colors, edgecolor='white', linewidth=2)

# Annotations
for bar, val, method in zip(bars, values, methods):
    pct = (val / market_price - 1) * 100
    ax.text(bar.get_x() + bar.get_width()/2, val + 20,
            f'Rs.{val:,.0f}\n({pct:+.0f}%)',
            ha='center', fontweight='bold', fontsize=10)

# Add a range band
all_vals = [pe_target, pb_target, ev_ebitda_implied, dcf_iv]
ax.axhspan(min(all_vals), max(all_vals), alpha=0.08, color='#6c8cff')
ax.text(4.3, (min(all_vals) + max(all_vals))/2,
        f'Valuation Range:\nRs.{min(all_vals):,.0f} – {max(all_vals):,.0f}',
        ha='left', va='center', fontsize=11, fontweight='bold', color='#6c8cff')

ax.set_ylabel('Value Per Share (Rs.)')
ax.set_title('Valuation Triangulation — Multiple Methods, One Answer',
             fontweight='bold', fontsize=14)
ax.grid(True, alpha=0.3, axis='y')
plt.tight_layout()
plt.show()

print(f"""
=== VALUATION TRIANGULATION SUMMARY ===

  Method            Value/Share    vs Market
  ─────────────────────────────────────────
  Market Price      Rs.{market_price:,.0f}         —
  DCF               Rs.{dcf_iv:,.0f}        {((dcf_iv/market_price)-1)*100:+.0f}%
  P/E Method        Rs.{pe_target:,.0f}        {((pe_target/market_price)-1)*100:+.0f}%
  P/B Method        Rs.{pb_target:,.0f}        {((pb_target/market_price)-1)*100:+.0f}%
  EV/EBITDA Method  Rs.{ev_ebitda_implied:,.0f}        {((ev_ebitda_implied/market_price)-1)*100:+.0f}%

  VALUATION RANGE: Rs. {min(all_vals):,.0f} – {max(all_vals):,.0f}
  MIDPOINT:        Rs. {np.median(all_vals):,.0f}

  All methods suggest the stock is {'undervalued' if all(v > market_price for v in all_vals) else 'overvalued' if all(v < market_price for v in all_vals) else 'mixed — investigate further'}.

  Preferred method: [Insert your reasoning based on company characteristics]
""")

Key Takeaways

1

EV/EBITDA is the workhorse enterprise multiple. It is capital-structure-neutral, depreciation-policy-neutral, and widely used in M&A, LBO, and cross-border analysis. For non-financial companies, it should be your primary relative valuation multiple.

2

EV/Sales and EV/EBIT have specific use cases. EV/Sales for companies without earnings. EV/EBIT for capital-intensive industries where depreciation is a real economic cost that EBITDA ignores.

3

Industry-specific multiples reveal what standard multiples miss. EV/Subscriber, EV/Tonne, P/NAV — these capture sector-specific value drivers that EBITDA does not. Use them as supplements, not replacements.

4

The peer multiple distribution matters more than a single median. Know where your target ranks — 25th percentile means discount; 75th percentile means premium. The premium must be justified by superior fundamentals.

5

Triangulate DCF and relative valuation. When multiple methods converge, you have a defensible answer. When they diverge, the gap is where analytical insight lives — investigate why.

Test Your Understanding

1. Why is EV/EBITDA preferred over P/E for comparing companies with different capital structures?

2. A cement company has EV/EBITDA of 8x and EV/Tonne of $120. Its peer trades at EV/EBITDA of 12x but EV/Tonne of $110. What does this divergence suggest?

3. When is EV/Sales the most appropriate multiple to use?

4. A company trades at EV/IC of 0.7x. What does this imply about its value creation?

5. Your target company's EV/EBITDA is at the 85th percentile of its peer group. What must you demonstrate to justify this premium?