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
- Compute and interpret EV/EBITDA, EV/Sales, EV/EBIT, and EV/Invested Capital multiples
- Understand why EV-based multiples are superior to equity multiples for cross-company comparisons with different capital structures
- Apply industry-specific multiples (EV/Capacity, EV/Subscriber) for sectors where standard multiples fail
- Build a statistical analysis of peer multiples — distributions, outliers, and percentile ranges
- Create an interactive peer comparison dashboard using Python (Plotly) for visual storytelling
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.
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.
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:
- Higher growth — more future EBITDA to discount
- Higher ROIC — more value created per unit of reinvestment
- Lower WACC — future cash flows are discounted less
- Lower tax rate — more of EBITDA flows through to cash flow
- Lower reinvestment needs — more of EBITDA becomes free cash flow
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
| Sector | Typical EV/EBITDA Range (India) | Key Driver of Variation |
|---|---|---|
| IT Services | 12–22x | Revenue growth, margins (TCS premium over mid-tier) |
| Consumer Staples | 25–45x | Brand strength, revenue growth visibility |
| Consumer Discretionary | 18–35x | Growth, ROIC (Titan, Asian Paints at premium) |
| Pharma | 12–25x | US FDA compliance, patent pipeline, R&D productivity |
| Cement | 10–18x | Capacity growth, regional pricing power, cost position |
| Automotive | 8–18x | Product cycle, EV transition exposure, export mix |
| Telecom | 5–10x | ARPU trend, spectrum liabilities, competitive intensity |
| Metals & Mining | 4–8x | Commodity 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.
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.
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:
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.
| Sector | Specialized Multiple | Why Standard Multiples Fail |
|---|---|---|
| Banking / NBFC | P/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. |
| Insurance | P/Embedded Value (P/EV), P/B | Embedded value captures the present value of in-force policies plus net asset value. |
| Telecom | EV/Subscriber, EV/Tower | Revenue per subscriber and infrastructure per tower drive value more directly than EBITDA. |
| Retail | EV/Store, EV/Sq Ft, Revenue/Sq Ft | Store-level economics drive value. Two retailers with the same EBITDA but different store counts have different growth trajectories. |
| Cement / Steel | EV/Tonne (capacity), EV/MT | Commodity businesses; value is driven by capacity × margin per tonne. Replacement cost is a key anchor. |
| Oil & Gas | EV/Barrel of Reserves, EV/Daily Production | Reserves are the primary asset. EBITDA is a function of commodity prices and does not capture reserve life. |
| Power / Utilities | EV/MW (capacity), P/B (regulated book) | Regulated utilities earn a fixed return on regulated equity; capacity drives revenue potential. |
| Real Estate | P/NAV (Net Asset Value), EV/Sq Ft | NAV captures the fair value of land bank and development projects; earnings are lumpy. |
| E-commerce / SaaS | EV/Revenue, EV/Gross Profit, EV/GMV | Most are loss-making at the EBITDA level. Revenue or gross profit multiples are the primary valuation tool. |
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
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
- Compute EV and all EV multiples for your capstone company and 8–12 peer companies using the pipeline.
- 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?
- Check for sector-appropriate industry-specific multiples (Section 16.4). If your sector has one, compute it for all peers.
- 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.
- Derive the relative valuation target price range (Low-Mid-High) from the peer multiples.
- 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
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.
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.
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.
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.
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?