Price Multiples: P/E, P/B & PEG Ratios
Value companies by comparison — compute and interpret price multiples, identify genuine peers with statistical filters, and understand the fundamental drivers behind every multiple.
Learning Objectives
- Understand the philosophy behind relative valuation and when it complements or contradicts a DCF
- Compute and interpret P/E, P/B, and PEG ratios — their drivers, biases, and appropriate use cases
- Identify comparable companies using sector, size, growth, profitability, and leverage filters in Python
- Build a relative valuation model that derives a target price from peer group multiples
- Recognize when multiples mislead — and how to avoid the most common relative valuation traps
15.1 The Philosophy of Relative Valuation
Relative valuation values an asset by comparing it to how similar assets are priced in the market. Instead of asking "what are this company's future cash flows worth?" (the DCF question), it asks "what are investors paying for comparable companies right now?"
Relative valuation dominates practice. A survey of equity research reports found that over 85% use multiples as either the primary or secondary valuation method. Investment bankers price IPOs on multiples. Private equity firms make buyout decisions on multiples. The reasons are practical:
Strengths of Relative Valuation
- Simple, fast, and intuitive
- Reflects current market mood and sector sentiment
- Requires far fewer explicit assumptions than a DCF
- Easy to communicate: "Trades at 12x vs peers at 15x"
- Provides a market-based reality check on DCF results
Weaknesses of Relative Valuation
- If the peer group is mispriced, your valuation is wrong
- No two companies are truly identical — the "comparable" is always approximate
- Multiples ignore company-specific differences in growth, risk, and quality
- Market-wide overvaluation or undervaluation contaminates all multiples
- Easy to manipulate by cherry-picking peers
15.2 The P/E Ratio: The Ubiquitous Multiple
The Price-to-Earnings (P/E) ratio is the most widely used valuation multiple in the world. It is reported on every financial website, cited in every earnings call, and embedded in the collective consciousness of investors. But its ubiquity masks important subtleties.
P/E = Market Capitalization / Net Income
15.2.1 Which Earnings? Trailing vs Forward
| Variant | Earnings Used | Use Case |
|---|---|---|
| Trailing P/E | Last 4 quarters (TTM) or last fiscal year | Based on actual reported earnings — factual, but backward-looking |
| Forward P/E | Consensus analyst estimate for next 12 months | Forward-looking, but depends on analyst accuracy. Forward P/E is typically lower than trailing for growing companies. |
| Normalized P/E | Average earnings over 3–5 years or through-the-cycle earnings | Adjusts for cyclicality. Essential for cyclicals (metals, autos) where current earnings may be at a peak or trough. |
15.2.2 The Fundamental Drivers of P/E
P/E is not an independent number. It is determined by the same value drivers we studied throughout this course. The P/E ratio implied by a DCF is:
P/E = 1 / Ke × [1 + (ROE − Ke) / (Ke − g) × Retention Ratio]
This tells us P/E increases with: higher growth (g), higher ROE, lower risk (Ke). A company with a P/E of 30 is not "overvalued" if it has 25% ROE and 15% growth. A company with a P/E of 8 is not "cheap" if it has 5% ROE and 0% growth. The P/E must be interpreted relative to its fundamental drivers.
15.2.3 P/E in Python
import yfinance as yf
import pandas as pd
import numpy as np
def fetch_pe_and_drivers(ticker_symbol):
"""Fetch P/E ratio and its fundamental drivers for a company."""
ticker = yf.Ticker(ticker_symbol)
info = ticker.info
pe_trailing = info.get('trailingPE')
pe_forward = info.get('forwardPE')
eps_ttm = info.get('trailingEps')
growth = info.get('revenueGrowth', 0) * 100
roe = info.get('returnOnEquity', 0) * 100 if info.get('returnOnEquity') else None
beta = info.get('beta')
market_cap = info.get('marketCap', 0) / 1e7
return {
'ticker': ticker_symbol.replace('.NS', ''),
'company': info.get('longName', ticker_symbol),
'trailing_pe': pe_trailing,
'forward_pe': pe_forward,
'eps_ttm': eps_ttm,
'revenue_growth_pct': round(growth, 1),
'roe_pct': round(roe, 1) if roe else None,
'beta': round(beta, 2) if beta else None,
'market_cap_cr': round(market_cap, 0),
}
# --- Fetch P/E for a set of Indian companies ---
tickers = ['TCS.NS', 'INFY.NS', 'WIPRO.NS', 'HCLTECH.NS', 'TECHM.NS',
'ASIANPAINT.NS', 'TITAN.NS', 'RELIANCE.NS']
pe_data = []
for t in tickers:
try:
pe_data.append(fetch_pe_and_drivers(t))
except Exception as e:
print(f"Error for {t}: {e}")
pe_df = pd.DataFrame(pe_data).set_index('ticker')
print("=== P/E RATIOS — SELECT INDIAN COMPANIES ===")
print(pe_df.to_string())
15.2.4 P/E Interpretation Guide
| Sector | Typical Trailing P/E Range (India) | What Drives It |
|---|---|---|
| IT Services | 20–35x | High ROE (30–45%), moderate growth (8–15%), zero debt |
| Consumer Staples | 40–70x | Exceptional ROE (40–100% due to negative working capital), stable growth, low beta |
| Private Banks | 15–25x | Moderate ROE (12–18%), GDP-plus loan growth |
| Automotive | 15–30x | Cyclical earnings; use normalized P/E. Maruti trades at premium to Tata Motors. |
| Pharma | 20–40x | R&D-driven growth, patent cliff risk, US FDA regulatory risk |
| Metals & Mining | 5–15x | Commodity cycle; P/E is low at cycle peak (high E), high at trough (low E). Normalized P/E essential. |
| PSU / Utilities | 8–15x | Regulated returns, low growth, government ownership discount |
15.3 The P/B Ratio: Value in Assets
The Price-to-Book (P/B) ratio compares market value to the accounting book value of equity. It is most useful for financial institutions and asset-heavy companies where book value is a meaningful measure of economic worth — and least useful for asset-light, brand-driven companies where most value is intangible.
P/B = Market Capitalization / Shareholders' Equity
15.3.1 The Fundamental Driver of P/B
A company trades above book value (P/B > 1) only if it earns a return on equity (ROE) that exceeds its cost of equity (Ke). The higher the ROE relative to Ke, the higher the P/B. This is why Titan (ROE ~30%, P/B ~76x) commands a vastly higher P/B than a PSU bank (ROE ~8%, P/B ~0.8x).
15.3.2 When P/B Works (and When It Does Not)
| P/B Works Well For | P/B Fails For |
|---|---|
| Banks, NBFCs, insurance companies — assets and liabilities are mostly financial and marked-to-market or near-market | Technology companies — most value is in intellectual property and human capital, which does not appear on the balance sheet |
| Asset-heavy industrials (cement, steel) — book value is a reasonable floor for liquidation value | Consumer brands (Titan, Nestle, Asian Paints) — brand value is largely absent from book value |
| REITs, infrastructure holding companies | Pharma — patent portfolios and R&D pipelines are not on the books |
| Companies where ROE is stable and mean-reverting | Companies with significant goodwill from acquisitions — book value is inflated by purchase accounting |
15.3.3 P/B in Python
def fetch_pb_and_roe(ticker_symbol):
"""Fetch P/B ratio and ROE to validate the P/B = f(ROE) relationship."""
ticker = yf.Ticker(ticker_symbol)
info = ticker.info
pb = info.get('priceToBook')
roe = info.get('returnOnEquity', 0) * 100 if info.get('returnOnEquity') else None
return {'ticker': ticker_symbol.replace('.NS', ''),
'pb_ratio': round(pb, 1) if pb else None,
'roe_pct': round(roe, 1) if roe else None}
# --- Compare P/B vs ROE across sectors ---
stocks_for_pb = ['HDFCBANK.NS', 'ICICIBANK.NS', 'SBIN.NS', # Banks
'TCS.NS', 'ASIANPAINT.NS', 'TATASTEEL.NS', # IT, Consumer, Metals
'NTPC.NS', 'TITAN.NS']
pb_data = []
for t in stocks_for_pb:
try:
pb_data.append(fetch_pb_and_roe(t))
except Exception as e:
print(f"Error: {t}: {e}")
pb_df = pd.DataFrame(pb_data).set_index('ticker')
print("=== P/B vs ROE ===")
print(pb_df)
print(f"\nCorrelation (ROE, P/B): {pb_df['roe_pct'].corr(pb_df['pb_ratio']):.2f}")
print("→ P/B is driven by ROE. Higher ROE → Higher P/B.")
15.4 The PEG Ratio: Growth-Adjusted Valuation
The PEG ratio (Price/Earnings-to-Growth) divides the P/E ratio by the earnings growth rate. It addresses the most common objection to P/E-based comparisons: "Company A deserves a higher P/E because it is growing faster." PEG normalizes for growth.
A PEG of 1.0 is the traditional "fair value" benchmark — the company's P/E equals its growth rate. PEG < 1.0 suggests undervaluation; PEG > 1.0 suggests overvaluation. But this rule of thumb is crude. The true fair PEG depends on the company's risk and return profile — a high-ROE, low-risk company deserves a PEG > 1.0.
15.4.1 PEG in Python
def compute_peg(ticker_symbol):
"""Compute the PEG ratio = Trailing P/E / Earnings Growth Rate (%)."""
ticker = yf.Ticker(ticker_symbol)
info = ticker.info
pe = info.get('trailingPE')
# Earnings growth: use the average of historical and forward where available
earnings_growth = info.get('earningsGrowth', 0) * 100
if earnings_growth == 0:
earnings_growth = info.get('earningsQuarterlyGrowth', 0) * 100
peg = pe / earnings_growth if earnings_growth and earnings_growth > 0 else None
return {
'ticker': ticker_symbol.replace('.NS', ''),
'pe': round(pe, 1) if pe else None,
'earnings_growth_pct': round(earnings_growth, 1),
'peg': round(peg, 2) if peg else None,
}
# --- Compare PEG across companies ---
peg_tickers = ['TCS.NS', 'INFY.NS', 'ASIANPAINT.NS', 'TITAN.NS',
'RELIANCE.NS', 'TATASTEEL.NS']
peg_data = [compute_peg(t) for t in peg_tickers]
peg_df = pd.DataFrame(peg_data).set_index('ticker')
print("=== PEG RATIOS ===")
print(peg_df)
print(f"\nMedian PEG: {peg_df['peg'].median():.2f}")
print("PEG < 1.0 may indicate undervaluation relative to growth.")
print("PEG > 2.0 may indicate the market is pricing in even higher future growth.")
15.5 Finding Comparable Companies with Statistical Filters
The single most important step in relative valuation is selecting the right peer group. Cherry-picking peers is the easiest way to manipulate a multiples-based valuation. A disciplined approach uses objective, quantifiable filters that can be defended.
15.5.1 The Five-Filter Framework
| Filter | What It Screens | Example for TCS |
|---|---|---|
| 1. Sector/Industry | Same 2-digit or 3-digit NIC code, or same GICS sector | IT Services (NIC 62) |
| 2. Size | Revenue or Market Cap within 0.2x–5x of target | Revenue Rs. 30,000–2,50,000 Cr |
| 3. Growth | Revenue growth within ±10% of target's 3Y CAGR | 8–18% revenue growth |
| 4. Profitability | Operating margin within ±10% or ROE within ±15% | Operating margin 18–30% |
| 5. Leverage | D/E within 0–0.5 (for non-financials) | D/E < 0.3 (asset-light IT) |
15.5.2 Peer Selection in Python
def find_comparable_companies(target_ticker, candidate_tickers,
sector_filter=True, size_filter=True,
growth_filter=True, profitability_filter=True,
leverage_filter=True):
"""
Identify truly comparable companies using statistical filters.
Returns the filtered peer group with their key metrics.
"""
# Fetch target company data
target = yf.Ticker(target_ticker)
target_info = target.info
target_sector = target_info.get('sector', '')
target_industry = target_info.get('industry', '')
target_mcap = target_info.get('marketCap', 0) / 1e7
target_rev_growth = target_info.get('revenueGrowth', 0) * 100
target_op_margin = (target_info.get('operatingMargins', 0) * 100
if target_info.get('operatingMargins') else None)
print(f"Target: {target_info.get('longName', target_ticker)}")
print(f" Sector: {target_sector} | MCap: Rs.{target_mcap:,.0f}Cr | "
f"Rev Growth: {target_rev_growth:.1f}%")
# Fetch all candidates
peers = []
for tkr in candidate_tickers:
try:
t = yf.Ticker(tkr)
info = t.info
mcap = info.get('marketCap', 0) / 1e7
rev_g = info.get('revenueGrowth', 0) * 100
op_m = (info.get('operatingMargins', 0) * 100
if info.get('operatingMargins') else None)
de = info.get('debtToEquity', 0) / 100 if info.get('debtToEquity') else 0
pe = info.get('trailingPE')
pb = info.get('priceToBook')
# Apply filters
passes = True
reasons = []
if sector_filter:
if info.get('sector') != target_sector:
passes = False
reasons.append('Sector mismatch')
if size_filter and mcap > 0:
if not (target_mcap * 0.2 <= mcap <= target_mcap * 5):
passes = False
reasons.append(f'Size out of range')
if growth_filter and rev_g:
if abs(rev_g - target_rev_growth) > 10:
passes = False
reasons.append(f'Growth diff > 10%')
if profitability_filter and op_m and target_op_margin:
if abs(op_m - target_op_margin) > 10:
passes = False
reasons.append(f'Margin diff > 10%')
if leverage_filter and de:
if de > 1.5: # Exclude highly leveraged
passes = False
reasons.append(f'High leverage (D/E={de:.1f})')
peers.append({
'Ticker': tkr.replace('.NS', ''),
'Company': info.get('longName', tkr),
'Sector': info.get('sector', ''),
'MCap (Cr)': round(mcap, 0),
'Rev Growth %': round(rev_g, 1),
'Op Margin %': round(op_m, 1) if op_m else None,
'D/E': round(de, 2),
'Trailing P/E': round(pe, 1) if pe else None,
'P/B': round(pb, 1) if pb else None,
'Passes': passes,
'Reject Reason': '; '.join(reasons) if not passes else '✓'
})
except Exception as e:
print(f" Error fetching {tkr}: {e}")
peer_df = pd.DataFrame(peers)
filtered = peer_df[peer_df['Passes']]
print(f"\n Total candidates: {len(candidates)}")
print(f" Passed all filters: {len(filtered)}")
print(f" Rejected: {len(peer_df) - len(filtered)}")
print(f"\n === COMPARABLE PEERS ===")
print(filtered[['Ticker', 'Trailing P/E', 'P/B', 'Rev Growth %', 'Op Margin %']].to_string(index=False))
print(f"\n Median Trailing P/E: {filtered['Trailing P/E'].median():.1f}x")
print(f" Median P/B: {filtered['P/B'].median():.1f}x")
return filtered, peer_df
# --- Example: Find peers for Infosys ---
it_candidates = ['TCS.NS', 'INFY.NS', 'WIPRO.NS', 'HCLTECH.NS', 'TECHM.NS',
'LTIM.NS', 'PERSISTENT.NS', 'COFORGE.NS', 'MPHASIS.NS',
'SONATSOFTW.NS', 'ZENSARTECH.NS', 'TANLA.NS']
filtered_peers, all_peers = find_comparable_companies('INFY.NS', it_candidates)
15.6 From Peer Multiples to Target Price
With a validated peer group, the relative valuation is straightforward: apply the peer group's median multiple to the target company's corresponding fundamental.
Target Price (P/B method) = Peer Median P/B × Target BV/Share
def relative_valuation(target_ticker, peer_tickers):
"""
Perform a complete relative valuation.
1. Find comparable peers using statistical filters
2. Compute peer group median multiples
3. Apply to target company fundamentals
4. Derive target price and compare to DCF
"""
target = yf.Ticker(target_ticker)
t_info = target.info
t_name = t_info.get('longName', target_ticker)
t_price = t_info.get('currentPrice') or t_info.get('previousClose')
t_eps = t_info.get('trailingEps')
t_bv = t_info.get('bookValue')
# Find peers
filtered, _ = find_comparable_companies(target_ticker, peer_tickers)
# Peer multiples
median_pe = filtered['Trailing P/E'].median()
median_pb = filtered['P/B'].median()
# Target values
pe_target = median_pe * t_eps if t_eps and median_pe else None
pb_target = median_pb * t_bv if t_bv and median_pb else None
avg_target = np.nanmean([pe_target, pb_target]) if pe_target and pb_target else (pe_target or pb_target)
print(f"\n{'='*50}")
print(f" RELATIVE VALUATION: {t_name}")
print(f"{'='*50}")
print(f"\n Peer Group: {len(filtered)} companies")
print(f" Median P/E: {median_pe:.1f}x")
print(f" Median P/B: {median_pb:.1f}x")
print(f"\n Target Fundamentals:")
print(f" EPS (TTM): Rs. {t_eps:.2f}" if t_eps else " EPS: N/A")
print(f" BV/Share: Rs. {t_bv:.2f}" if t_bv else " BV/Share: N/A")
print(f"\n Implied Target Prices:")
if pe_target:
print(f" P/E Method: Rs. {pe_target:,.0f} "
f"({'↑' if pe_target > t_price else '↓'} "
f"{(pe_target/t_price - 1)*100:+.1f}% vs market)")
if pb_target:
print(f" P/B Method: Rs. {pb_target:,.0f} "
f"({'↑' if pb_target > t_price else '↓'} "
f"{(pb_target/t_price - 1)*100:+.1f}% vs market)")
if avg_target:
print(f" Average: Rs. {avg_target:,.0f}")
print(f"\n Current Price: Rs. {t_price:,.0f}")
print(f"\n NOTE: Relative valuation reflects market sentiment.")
print(f" Cross-check with DCF intrinsic value from Chapter 14.")
return {
'target': target_ticker,
'peers_used': len(filtered),
'median_pe': median_pe,
'median_pb': median_pb,
'pe_target_price': pe_target,
'pb_target_price': pb_target,
'avg_target_price': avg_target,
'current_price': t_price,
}
# --- Run relative valuation for Infosys ---
rel_val = relative_valuation('INFY.NS', it_candidates)
15.7 Common Multiples Traps and How to Avoid Them
Hands-On Project: Relative Valuation for Your Capstone Company
Perform a complete relative valuation of your capstone company using P/E, P/B, and PEG multiples. Identify a defensible peer group using the five statistical filters, compute the implied target price from peer multiples, and compare the result to your DCF intrinsic value from Chapter 14.
Steps
- Identify 10–15 candidate peers in the same sector as your capstone company. Use Screener.in or Yahoo Finance to build the list.
- Apply the five statistical filters from Section 15.5. Document which companies passed and which were rejected, with reasons.
- Compute peer median P/E and P/B. Use trailing P/E (TTM) for consistency. If the peer group has fewer than 4 companies after filtering, relax the filters incrementally and document why.
- Derive the implied target price from P/E and P/B. Average the two if both are available.
- Compute the PEG ratio for each peer and your target. Is your target's PEG above or below the peer median?
- Compare to DCF: Plot a bar chart showing: (a) Current market price, (b) DCF intrinsic value (from Ch14), (c) Relative valuation target (P/E method), (d) Relative valuation target (P/B method). Which method suggests the stock is most undervalued? Which is most conservative?
- Write a 200-word reconciliation: If the DCF and relative valuation give different answers, why? What assumption differences explain the gap? Which method do you trust more for this company and why?
View Solution / Walkthrough
Relative Valuation — Infosys (Illustrative)
# ================================================================
# COMPLETE RELATIVE VALUATION — Infosys
# ================================================================
# DCF value from Chapter 14 (hypothetical)
dcf_iv = 1850 # Rs. per share
# Run relative valuation
rel_val = relative_valuation('INFY.NS', it_candidates)
# Comparison chart
fig, ax = plt.subplots(figsize=(10, 6))
methods = ['Market\nPrice', 'DCF\n(Ch14)', 'P/E\nMethod', 'P/B\nMethod']
values = [
rel_val['current_price'],
dcf_iv,
rel_val['pe_target_price'],
rel_val['pb_target_price']
]
colors = ['gray', '#6c8cff', '#00c9a7', '#f0a040']
bars = ax.bar(methods, values, color=colors, edgecolor='white', linewidth=1.5)
for bar, val in zip(bars, values):
ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + max(values)*0.01,
f'Rs.{val:,.0f}', ha='center', fontweight='bold', fontsize=11)
if val != rel_val['current_price']:
pct = (val / rel_val['current_price'] - 1) * 100
ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() * 0.5,
f'{pct:+.0f}%', ha='center', color='white', fontweight='bold', fontsize=10)
ax.set_ylabel('Value Per Share (Rs.)')
ax.set_title('Valuation Method Comparison — Infosys', fontweight='bold', fontsize=14)
ax.grid(True, alpha=0.3, axis='y')
plt.tight_layout()
plt.show()
# Reconciliation
print(f"""
=== DCF vs RELATIVE VALUATION RECONCILIATION ===
DCF Intrinsic Value: Rs. {dcf_iv:,.0f}
Relative Valuation (Avg): Rs. {rel_val['avg_target_price']:,.0f}
Difference: Rs. {rel_val['avg_target_price'] - dcf_iv:,.0f}
Possible explanations for the gap:
1. The market (relative valuation) may be pricing in higher near-term
growth than my DCF terminal growth assumes.
2. The DCF's WACC of 10.6% may be conservative vs the market-implied
discount rate embedded in current peer multiples.
3. Peer multiples may incorporate an acquisition premium or scarcity
premium not captured in the DCF.
4. If DCF > Relative: the market may be undervaluing the company's
long-term competitive advantages that are captured in my DCF.
For Infosys, I place more weight on the DCF because:
• The IT services sector's multiples are sensitive to global tech
spending cycles and can be volatile.
• Infosys's value is primarily driven by long-term cash flow generation
(captured in DCF), not by asset value (P/B is less relevant for IT).
• The DCF forces explicit thinking about the sustainability of Infosys's
margins and growth, while multiples embed market sentiment.
""")
Key Takeaways
Multiples are driven by fundamentals. P/E is driven by growth, risk, and ROE. P/B is driven by ROE relative to Ke. Never compare multiples without comparing the fundamentals that explain them.
The peer group is the model. Statistical filters (sector, size, growth, profitability, leverage) produce a defensible peer set. Cherry-picking peers to hit a target price is manipulation.
PEG adjusts P/E for growth — but use normalized growth. A single year's earnings spike produces a misleadingly low PEG. Use 3–5 year average growth or the implied sustainable growth rate.
P/B works for banks and asset-heavy companies; it fails for asset-light, brand-driven businesses. Titan's P/B of 76x is not "overvalued" — it reflects that most of Titan's value is not on the balance sheet.
Relative valuation and DCF are complementary, not competing. When they agree, you have confirmation. When they disagree, investigate why — the gap reveals what the market believes that your DCF does not (or vice versa).
Test Your Understanding
1. A company has a P/E of 25, an ROE of 30%, and earnings growth of 20%. Its peer has a P/E of 15, ROE of 10%, and growth of 5%. Is the first company overvalued relative to the peer?
2. Why is the P/B ratio usually inappropriate for technology companies?
3. You need to value a cyclical steel company. Its current P/E is 6 (near a 10-year low). What is the most appropriate action?
4. In peer group selection, why is the median multiple preferred over the mean (simple average)?
5. Your DCF gives an intrinsic value of Rs. 1,200/share. Relative valuation (peer median P/E) gives Rs. 1,600/share. The current market price is Rs. 1,400. Which interpretation is most reasonable?