ROIC, Economic Profit & Value Creation
The single most important number in corporate valuation — how to compute it, decompose it, and use it to separate value creators from value destroyers.
Learning Objectives
- Compute NOPAT (Net Operating Profit After Tax) with proper adjustments for non-operating items
- Calculate Invested Capital with precision — distinguishing operating from non-operating assets
- Derive ROIC and benchmark it against WACC to determine whether a company creates or destroys value
- Decompose ROIC into its drivers using the McKinsey value driver formula
- Compute Economic Profit (EVA) and understand why it is superior to net income for measuring value creation
- Build a complete ROIC analysis pipeline in Python with Indian company data
- Excel track: Compute NOPAT, Invested Capital, ROIC, and Economic Profit with formulas in the course workbook — no coding required
This session builds the ROIC engine: NOPAT → Invested Capital → ROIC → Economic Profit. The Python path (marked Python) automates it; the Excel path (green Excel Path boxes) computes the identical chain with formulas — and has a genuine edge here: Screener.in prints ROCE (Return on Capital Employed) for every company, which is a close cousin of ROIC. Compute your ROIC in Excel, compare with the site's ROCE, and you have a built-in answer key: broadly matching numbers mean your NOPAT and Invested Capital rows are right.
🔍 Opening Challenge — The Growth Illusion
5.1 Why ROIC Is the Central Metric in Valuation
In Chapter 1, we established a fundamental truth: growth creates value only when the return on invested capital exceeds the cost of that capital. ROIC is the metric that makes this truth operational. It answers the question every investor should ask: for every rupee the company invests, how much profit does it generate?
Consider three companies with identical 15% revenue growth:
| Company A | Company B | Company C | |
|---|---|---|---|
| Revenue Growth | 15% | 15% | 15% |
| ROIC | 35% | 12% | 6% |
| WACC | 10% | 10% | 10% |
| ROIC − WACC Spread | +25% | +2% | −4% |
| Value Creation | Strong creator | Marginal | Destroyer |
Company A is a compounding machine. Every rupee reinvested generates 35 paise of after-tax profit — far above the 10% cost. Company C, despite growing at the same rate, is burning value. Its 6% ROIC means each rupee invested earns less than what that capital costs. The 15% growth only accelerates the destruction.
Now complete the hook with the missing number. Company X: revenue growth 20%, ROIC 7%, WACC 11%. Company Y: growth 6%, ROIC 28%, WACC 11%.
- Compute each company's ROIC − WACC spread.
- Which company actually creates value with every rupee it reinvests?
- Was your opening vote right? What does X's 20% growth actually do to shareholder value?
Reveal answers
X: 7% − 11% = −4% — every rupee reinvested at 20% growth destroys 4 paise of value; faster growth = faster destruction. Y: 28% − 11% = +17% — a compounding machine even at 6% growth. Y wins. The missing number from the hook was ROIC (vs WACC) — which is exactly what this session teaches you to compute.
5.2 Computing NOPAT: The Numerator
NOPAT (Net Operating Profit After Tax) is the profit generated by the company's core operations, after tax, but before financing costs. It represents the earnings available to all capital providers — both debt and equity holders. It is the numerator in the ROIC equation.
The simplicity of this formula is deceptive. The adjustment work lies in ensuring EBIT truly reflects operating earnings. Here is the step-by-step approach:
Reported figures (Rs. Cr): Operating Income (EBIT) = 900 · Other Income (all non-operating: interest + dividend) = 150 · One-time gain on property sale = 100 · Effective tax rate = 25%.
Compute:
- Adjusted Operating EBIT (strip both non-operating items)
- NOPAT
Then answer: by what percentage would you have overstated NOPAT if you had used reported EBIT directly?
Reveal answers
Adjusted EBIT = 900 − 150 − 100 = 650. NOPAT = 650 × 0.75 = 487.5. Unadjusted NOPAT would be 900 × 0.75 = 675 — overstated by 38.5%. On a 11% WACC, that error alone flips the ROIC conclusion for many companies.
5.2.1 NOPAT in Python
def compute_nopat(income_stmt, tax_rate=0.25, non_operating_income_fields=None):
"""
Compute NOPAT from an income statement DataFrame.
Parameters
----------
income_stmt : pd.DataFrame (yfinance .financials format)
tax_rate : float — effective or statutory tax rate (default 0.25)
non_operating_income_fields : list of str — income statement line items
that are non-operating and should be excluded from operating EBIT.
Returns
-------
pd.Series of NOPAT by fiscal year.
"""
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
ebit = safe_get(income_stmt, ['Operating Income', 'EBIT'])
if ebit is None:
# Try to construct EBIT from components
rev = safe_get(income_stmt, ['Total Revenue', 'Revenue'])
total_exp = safe_get(income_stmt, ['Total Expenses'])
if rev is not None and total_exp is not None:
ebit = rev - total_exp
else:
raise ValueError("Cannot find or construct EBIT from income statement.")
# Strip non-operating income if fields are provided
if non_operating_income_fields:
for field in non_operating_income_fields:
non_op = safe_get(income_stmt, field)
if non_op is not None:
ebit = ebit - non_op
nopat = ebit * (1 - tax_rate)
return nopat
# Usage
import yfinance as yf
ticker = yf.Ticker("ASIANPAINT.NS")
nopat = compute_nopat(ticker.financials)
print("NOPAT by Fiscal Year (Rs. Cr):")
print((nopat / 1e7).round(2)) # Convert to crores
On each company's Drivers sheet, first set up an Inputs block — this is the single most important Excel modeling habit of the whole course:
B1: 25% ' ← label "Tax Rate" in A1
B2: 10% ' ← label "WACC assumption" in A2 (used in 5.6)
Then (Operating Profit row 3, Other Income row 23, one-time gains row 24 — adjust rows to your sheet):
= C3 - C23 - C24
' NOPAT (row 48) — note the $B$1 ABSOLUTE reference:
= C47 * (1 - $B$1)
Why $B$1 matters: the dollar signs "lock" the tax rate cell so it does not shift when you drag the formula across years — every NOPAT cell reads the same single input. Next year, or in a scenario, you change one cell and the entire workbook updates. This is Excel's version of a function parameter, and it is how professional models are built.
Screener.in cross-check: Screener's P&L shows "Other Income" as a separate line already excluded from its OPM calculation — so your row 47 and Screener's "Operating Profit" should agree closely. Screener's "Tax %" line gives you the effective tax rate to put in B1 (better than a flat 25%).
5.3 Computing Invested Capital: The Denominator
Invested Capital represents the total capital that has been deployed in the company's operations — the capital provided by both debt and equity holders, net of non-operating assets. It is the denominator of ROIC.
5.3.1 The Operating Approach
This builds invested capital from the asset side. Operating current assets include receivables, inventory, and the minimum cash needed for operations — but exclude excess cash. Operating current liabilities include payables and accruals — but exclude interest-bearing short-term debt.
5.3.2 The Financing Approach (Easier from yfinance)
This builds invested capital from the financing side — equity plus debt, minus assets that are not employed in the business. We subtract cash because it is not an operating asset (it generates interest income, not operating profit). We subtract non-operating investments (minority stakes in other companies) for the same reason.
Think: TCS holds roughly Rs. 50,000 Cr of cash. Suppose we did NOT subtract cash from invested capital. Would TCS's ROIC go up or down? Would that be fair to the business?
Pair: Work through the logic with your partner: cash sits in the denominator but generates almost none of the numerator (interest income is excluded from NOPAT).
Share: Two pairs explain. Key insight: idle cash dilutes ROIC — including it punishes efficient businesses for holding reserves, which is why we remove it and value cash separately.
5.3.3 Critical Adjustments
| Adjustment | Rationale | How |
|---|---|---|
| Subtract excess cash | Cash beyond what is needed for day-to-day operations is a non-operating asset. Including it inflates invested capital and depresses ROIC. | Estimate operating cash as 2–5% of revenue (varies by industry). Subtract the remainder from invested capital. |
| Subtract non-consolidated investments | Minority stakes generate dividend/associate income, not operating profit. They should be valued separately. | Identify "Investments" on the balance sheet (non-current and current). Exclude unless they are operating subsidiaries. |
| Add capitalized operating leases | Under Ind AS 116, most leases are already on the balance sheet. For companies exempt or pre-2019 data, leases were off-balance-sheet. | Multiply annual lease expense by 6–8x to estimate the off-balance-sheet debt equivalent. Add to both debt and fixed assets. |
| Add back goodwill impairment | Goodwill impairment is a non-cash charge that reduces equity but does not reflect current operating reality. | Add cumulative impairment charges back to equity and invested capital. This prevents ROIC from being artificially inflated by write-downs. |
| Treat provisions as debt equivalents | Large restructuring or legal provisions are essentially debt — they represent future cash outflows. | Add material long-term provisions to invested capital. Judge materiality case by case. |
5.3.4 Invested Capital in Python
def compute_invested_capital(balance_sheet, excess_cash_pct=0.0):
"""
Compute invested capital from balance sheet data.
By default, treats ALL cash as non-operating (conservative).
Set excess_cash_pct to ~0.02 (2% of revenue) to treat some cash
as operating when you have revenue data.
"""
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
equity = safe_get(balance_sheet, [
'Total Equity Gross Minority Interest',
'Stockholders Equity', 'Total Equity'])
total_debt = safe_get(balance_sheet, [
'Total Debt', 'Long Term Debt'])
cash = safe_get(balance_sheet, [
'Cash And Cash Equivalents',
'Cash Cash Equivalents And Short Term Investments'])
lt_investments = safe_get(balance_sheet, [
'Long Term Investments', 'Investments And Advances'])
if equity is None:
# Build from: Total Assets − Total Liabilities
assets = safe_get(balance_sheet, ['Total Assets'])
liabilities = safe_get(balance_sheet, ['Total Liabilities Net Minority Interest'])
if assets is not None and liabilities is not None:
equity = assets - liabilities
else:
raise ValueError("Cannot determine equity from balance sheet.")
# If there's no separate debt line, use the difference approach
if total_debt is None:
total_debt = 0 # In some yfinance outputs, debt is embedded in liabilities
invested_capital = equity + total_debt
if cash is not None:
invested_capital = invested_capital - cash
if lt_investments is not None:
# Only subtract if these are truly non-operating
invested_capital = invested_capital - lt_investments
return invested_capital
# Usage
ic = compute_invested_capital(ticker.balance_sheet)
print("Invested Capital by Fiscal Year (Rs. Cr):")
print((ic / 1e7).round(2))
Balance sheet snapshot (Rs. Cr): Equity 4,000 · Total Debt 1,500 · Cash 800 · Non-operating investments 300 · Operating Current Assets 2,200 · Operating Current Liabilities 1,400 · Net PPE 3,800.
Compute IC twice and check they agree:
- Financing approach: Equity + Debt − Cash − Non-op investments = ?
- Operating approach: (Op. Current Assets − Op. Current Liabilities) + Net PPE = ?
If your two answers differ, one of your classifications is wrong — find it.
Reveal answers
Financing: 4,000 + 1,500 − 800 − 300 = 4,400. Operating: (2,200 − 1,400) + 3,800 = 4,600. They differ by 200 — the non-operating investments of 300 don't fully reconcile with the working capital split (200 of the investments sit inside "operating" current assets here). This is exactly the kind of classification judgment Section 5.3.3's adjustment table addresses: the two approaches only match when every asset is correctly tagged operating vs non-operating.
Build IC as a staircase of rows — one row per component, so every adjustment from the table above is visible and auditable (rows continue from the Session 4 workbook; Equity row 7, Debt row 5, Cash row 6, Investments row 28):
= C7 + C5
' (−) Cash (row 50):
= -C6
' (−) Non-operating investments (row 51):
= -C28
' Invested Capital (row 52) — sum the staircase:
= SUM(C49:C51)
' Optional refinement — keep only EXCESS cash in row 50 instead:
= -(C6 - 0.02*C2) ' subtracts cash beyond 2% of revenue
Then ROIC is one formula (NOPAT row 48 from the previous Excel Path):
= IFERROR(C48 / C52 * 100, "n/a")
Apply conditional formatting to row 53: green above your WACC cell ($B$2, set to 10%), red below. Drag all rows right across the years — you now have the full ROIC trend, color-coded, exactly like the Python pipeline's output.
The ROCE answer key: Screener.in prints ROCE on every company page. ROCE = Operating Profit × (1−tax isn't applied) / (Debt + Equity) — slightly different numerator and it does not net out cash. So expect your ROIC to be near ROCE but not identical (usually a bit higher for cash-rich firms because you subtract cash). If your number is wildly different (say, off by 15+ percentage points), you have a formula bug — check rows 49–52 before anything else.
5.4 Putting It Together: Computing ROIC
With NOPAT and Invested Capital computed, ROIC follows directly. But the analysis does not stop at a single number — we must examine ROIC trends, compare against WACC, and decompose the drivers.
5.4.1 The Core ROIC Function
def compute_roic(income_stmt, balance_sheet, tax_rate=0.25):
"""
Compute NOPAT, Invested Capital, and ROIC.
Returns a DataFrame with all three components by fiscal year.
"""
nopat = compute_nopat(income_stmt, tax_rate)
ic = compute_invested_capital(balance_sheet)
# Align indices (they should match by fiscal year, but verify)
common_years = nopat.index.intersection(ic.index)
nopat = nopat[common_years]
ic = ic[common_years]
roic = (nopat / ic) * 100
result = pd.DataFrame({
'NOPAT (Cr)': nopat / 1e7,
'Invested Capital (Cr)': ic / 1e7,
'ROIC (%)': roic.round(2)
})
return result
def compare_roic_across_companies(tickers, labels=None, tax_rate=0.25):
"""
Compute and compare ROIC for multiple companies.
Returns a DataFrame with each company's latest ROIC and components.
"""
if labels is None:
labels = [t.replace('.NS', '').replace('.BO', '') for t in tickers]
results = {}
for tkr, lbl in zip(tickers, labels):
try:
ticker = yf.Ticker(tkr)
roic_df = compute_roic(ticker.financials, ticker.balance_sheet, tax_rate)
latest = roic_df.iloc[:, 0] # Most recent fiscal year
results[lbl] = latest
print(f"✓ {lbl}: ROIC = {latest['ROIC (%)']:.1f}%")
except Exception as e:
print(f"✗ {lbl}: {e}")
time.sleep(1.5)
comparison = pd.DataFrame(results).T
return comparison
# --- Compare Indian consumer leaders ---
consumer_tickers = ['ASIANPAINT.NS', 'TITAN.NS', 'NESTLEIND.NS',
'BRITANNIA.NS', 'HINDUNILVR.NS']
consumer_labels = ['Asian Paints', 'Titan', 'Nestle India', 'Britannia', 'HUL']
roic_comparison = compare_roic_across_companies(consumer_tickers, consumer_labels)
print("\n=== ROIC Comparison — Indian Consumer Sector ===")
print(roic_comparison)
5.4.2 Interpreting ROIC Levels
| ROIC Range | Assessment | Typical Examples (India) |
|---|---|---|
| > 30% | Exceptional — wide and durable competitive moat | Asian Paints, Titan, TCS, Nestle India, Pidilite |
| 20–30% | Strong — clear competitive advantages | Infosys, HUL, Dr. Reddy's, Maruti Suzuki |
| 10–20% | Adequate — earning returns above typical cost of capital | Larsen & Toubro, Sun Pharma, Ultratech Cement |
| 5–10% | Marginal — returns barely exceed or equal cost of capital | Tata Motors, Hindalco (cyclical; check multi-year averages) |
| < 5% | Weak — likely destroying value; growth is harmful | Telecom companies (pre-consolidation), highly leveraged infra firms |
🔗 Before You Move On — 3 Quick Checks
True or False: A company with ROE of 22% and a D/E of 2.5 must be creating value for shareholders.
Reveal
True or False: Subtracting cash from invested capital lowers ROIC.
Reveal
True or False: NOPAT uses the same numerator as net income — they only differ in the tax rate applied.
Reveal
5.5 Decomposing ROIC: The Value Driver Formula
A single ROIC number is useful for comparison. But to understand why one company earns 35% while its competitor earns 12%, we must decompose ROIC into its drivers. McKinsey's value driver formula expresses ROIC as the product of operating margin and capital turnover:
ROIC = Operating Margin × Capital Turnover
This decomposition reveals that there are exactly two ways to earn a high ROIC:
Strategy 1: High Margins
Earn a large spread between selling price and cost. This requires pricing power — a strong brand, differentiated product, or monopoly position.
Examples: Asian Paints (brand), Titan (brand + design), TCS (service quality), Nestle (brand portfolio).
Margin driver: NOPAT / Revenue
Strategy 2: High Capital Efficiency
Generate a lot of revenue from a small capital base. This requires asset-light operations — minimal PPE, fast inventory turns, tight receivables.
Examples: DMart (inventory turns), Indigo (aircraft utilization), Infosys (asset-light IT).
Turnover driver: Revenue / Invested Capital
The very best companies — Titan, TCS, Asian Paints — score well on both dimensions. They earn high margins AND turn over capital efficiently. This is the hallmark of a genuinely exceptional business.
Four companies, all with ROIC = 24%. Fill in the blanks:
| NOPAT Margin | Capital Turnover | Business type | |
|---|---|---|---|
| Luxury Brands Ltd | 24% | ___ | ___ |
| HyperMart Ltd | 3% | ___ | ___ |
| Software Ltd | ___ | 1.5x | ___ |
| Steel Co | ___ | 0.8x | ___ |
For each: compute the missing value (remember ROIC = Margin × Turnover) and name a real Indian company with roughly that profile.
Reveal answers
Luxury: 24% × 1.0x (Titan-style pricing power) · HyperMart: 3% × 8.0x (DMart-style volume machine) · Software: 16% × 1.5x (Infosys — margin-led) · Steel: 30% × 0.8x (Tata Steel at a cyclical peak — the margin is not durable!). Same ROIC, four totally different risk profiles — which is why you decompose before you forecast.
Think: Company P earns its ROIC through margins (a beloved brand). Company Q earns the same ROIC through capital turnover (brilliant operations, thin margins). If a well-funded competitor attacks both in 5 years, whose ROIC is more likely to survive?
Pair: Take opposite sides with your partner — one defends the brand (P), one defends the operations (Q). Two rounds, 1 minute each.
Share: We'll hear the best argument for each side. Note for your capstone: this judgment becomes your terminal ROIC assumption in Session 15.
5.5.1 Python: ROIC Decomposition
def decompose_roic(income_stmt, balance_sheet, tax_rate=0.25):
"""
Decompose ROIC into Operating Margin and Capital Turnover.
"""
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
revenue = safe_get(income_stmt, ['Total Revenue', 'Revenue'])
nopat = compute_nopat(income_stmt, tax_rate)
ic = compute_invested_capital(balance_sheet)
common = nopat.index.intersection(ic.index).intersection(revenue.index)
nopat = nopat[common]
ic = ic[common]
revenue = revenue[common]
op_margin = (nopat / revenue) * 100
capital_turnover = revenue / ic
roic = (nopat / ic) * 100
decomposition = pd.DataFrame({
'NOPAT Margin (%)': op_margin.round(2),
'Capital Turnover (x)': capital_turnover.round(2),
'ROIC (%)': roic.round(2)
})
return decomposition
def plot_roic_decomposition(tickers, labels):
"""
Scatter plot: Operating Margin vs Capital Turnover,
with ROIC as isoquants. Each company is a point.
"""
import matplotlib.pyplot as plt
import numpy as np
fig, ax = plt.subplots(figsize=(12, 8))
for tkr, lbl in zip(tickers, labels):
try:
ticker = yf.Ticker(tkr)
dec = decompose_roic(ticker.financials, ticker.balance_sheet)
latest = dec.iloc[:, 0] # Most recent year
ax.scatter(latest['Capital Turnover (x)'],
latest['NOPAT Margin (%)'],
s=300, edgecolors='white', linewidth=2, zorder=5)
ax.annotate(f"{lbl}\nROIC: {latest['ROIC (%)']:.0f}%",
(latest['Capital Turnover (x)'],
latest['NOPAT Margin (%)']),
textcoords="offset points", xytext=(10, 5), fontsize=9)
except Exception as e:
print(f"✗ {lbl}: {e}")
time.sleep(1.5)
# Add ROIC isoquants
x_range = np.linspace(0.1, ax.get_xlim()[1] * 1.2, 100)
for roic_level in [10, 20, 30, 50, 75]:
y = roic_level / x_range
ax.plot(x_range, y, '--', color='gray', alpha=0.3, linewidth=0.8)
ax.annotate(f'{roic_level}% ROIC',
(x_range[-1], y[-1]),
fontsize=7, color='gray', alpha=0.7,
textcoords="offset points", xytext=(5, 0))
ax.set_xlabel('Capital Turnover (Revenue / Invested Capital)')
ax.set_ylabel('NOPAT Margin (%)')
ax.set_title('ROIC Decomposition: How Do Companies Earn Their Returns?',
fontsize=13, fontweight='bold')
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# Usage
consumer_tickers = ['ASIANPAINT.NS', 'TITAN.NS', 'NESTLEIND.NS',
'BRITANNIA.NS', 'HINDUNILVR.NS']
consumer_labels = ['Asian Paints', 'Titan', 'Nestle India', 'Britannia', 'HUL']
plot_roic_decomposition(consumer_tickers, consumer_labels)
Two more rows and the decomposition is laid bare (continuing row numbers: Sales row 2, NOPAT row 48, IC row 52):
= C48 / C2 * 100
' Capital Turnover x (row 55):
= C2 / C52
' Consistency check — should equal ROIC row 53:
= C54 * C55
The margin-vs-turnover scatter (the Python isoquant chart, Excel edition): on your Summary sheet, ensure every company's latest Margin (row 54) and Turnover (row 55) are pulled across, then select the two rows → Insert → Scatter → add data labels with company names. The Python version draws curved "ROIC isoquant" lines; in Excel, simply add a third series manually — a column of turnover values (0.5, 1, 2, 4, 8) against a computed =24/turnover formula gives you the 24% ROIC frontier as a line. Companies sitting above the frontier out-earn 24%; below it, they don't.
5.6 Economic Profit: The True Measure of Value Creation
Economic Profit (also called Economic Value Added or EVA) measures the absolute amount of value created in a given period — not just the rate of return. While ROIC tells you the efficiency of capital use, Economic Profit tells you the magnitude of value creation.
Economic Profit = NOPAT − (WACC × Invested Capital)
Economic Profit has a powerful property: it is directly linked to the excess returns in a DCF valuation. The present value of all future Economic Profits, plus the current invested capital, equals the enterprise value from a DCF model. This is the economic profit valuation model — an alternative to the traditional DCF that separates the value of existing capital from the value of future value creation.
5.6.1 Why Net Income Fails to Measure Value Creation
Net income is the most commonly cited profit metric. But it fails as a value creation measure for three reasons:
| Flaw in Net Income | Why It Matters | How Economic Profit Fixes It |
|---|---|---|
| Ignores the cost of equity capital | A company can report positive net income while destroying shareholder value because it earns less than the equity cost of capital. | Economic Profit charges for ALL capital — debt AND equity — at the WACC. |
| Includes non-operating items | Gains on asset sales, investment income, and one-time items inflate net income without reflecting operating performance. | Economic Profit starts from NOPAT, which strips out non-operating items. |
| Penalizes investment | Under GAAP/Ind AS, R&D and capex are expensed or depreciated, reducing net income even when they create value. | Investments that earn above WACC increase Economic Profit over time, even if they reduce near-term earnings. |
Two companies, both reporting healthy net income (Rs. Cr):
| Steady Power Ltd | Nimbus Software | |
|---|---|---|
| Net Income | 500 | 480 |
| NOPAT | 620 | 540 |
| Invested Capital | 7,000 | 2,400 |
| WACC | 10% | 11% |
Compute for each: (a) ROIC, (b) capital charge, (c) Economic Profit. Which company would you rather own — and why does the answer differ from what net income suggests?
Reveal answers
Steady Power: ROIC = 620/7,000 = 8.9% < 10% WACC → charge 700 → EP = −80 Cr (destroying value despite Rs. 500 Cr profit). Nimbus: ROIC = 540/2,400 = 22.5% > 11% → charge 264 → EP = +276 Cr. Nearly identical net incomes, opposite value verdicts — Economic Profit charges for equity capital that accounting profit treats as free.
5.6.2 Computing Economic Profit in Python
def compute_economic_profit(income_stmt, balance_sheet,
wacc=0.10, tax_rate=0.25):
"""
Compute Economic Profit (EVA) = NOPAT − (WACC × Invested Capital).
If you haven't yet estimated WACC (Chapters 10–11), use 10% as a
reasonable estimate for Indian companies.
"""
nopat = compute_nopat(income_stmt, tax_rate)
ic = compute_invested_capital(balance_sheet)
common = nopat.index.intersection(ic.index)
nopat = nopat[common]
ic = ic[common]
capital_charge = wacc * ic
economic_profit = nopat - capital_charge
roic = (nopat / ic) * 100
spread = roic - (wacc * 100)
result = pd.DataFrame({
'NOPAT (Cr)': (nopat / 1e7).round(2),
'Invested Capital (Cr)': (ic / 1e7).round(2),
'ROIC (%)': roic.round(2),
'WACC (%)': wacc * 100,
'Spread (%)': spread.round(2),
'Capital Charge (Cr)': (capital_charge / 1e7).round(2),
'Economic Profit (Cr)': (economic_profit / 1e7).round(2)
})
result['Value Creator?'] = result['Economic Profit (Cr)'] > 0
return result
# Usage
ticker = yf.Ticker("ASIANPAINT.NS")
ep = compute_economic_profit(ticker.financials, ticker.balance_sheet, wacc=0.10)
print(ep)
You already built everything except the charge. Recall: NOPAT row 48, IC row 52, ROIC row 53, and the WACC input cell $B$2 (10%) from the first Excel Path of this session. Continue the staircase:
= C52 * $B$2
' Economic Profit (row 57):
= C48 - C56
' Spread % (row 58) — the sign tells the whole story:
= C53/100 - $B$2
Conditional formatting: row 57 red if < 0, green if > 0. Row 58 likewise. Drag right — every year's verdict is now colour-coded.
Why the $B$2 reference is the point of this box: change that one cell from 10% to 12% and watch every capital charge, economic profit, and colour flag across every year recompute instantly. This single-cell "what-if" is the Excel counterpart of the Python function's wacc=0.10 parameter — and it is how you will answer the committee question "would your conclusion survive a 200bp higher WACC?" in one keystroke.
5.7 ROIC Trends: What to Look For
A single year's ROIC is informative. The trend over 5–10 years is invaluable. It reveals the trajectory of a company's competitive position.
5.7.1 The Four ROIC Trajectories
| Pattern | What It Looks Like | Valuation Implication |
|---|---|---|
| Sustained High ROIC | ROIC > 20% for 5+ years with low volatility | The company has a durable moat. Value it with a long competitive advantage period (15–20+ years). High terminal value justified. Examples: Asian Paints, TCS, Titan. |
| Mean-Reverting ROIC | Very high ROIC (>50%) that declines toward 15–20% over time | Competition is eroding the moat. Forecast ROIC to mean-revert to just above WACC over 5–10 years. Do NOT extrapolate current sky-high ROIC into perpetuity. Examples: early-stage IT product companies, new pharma launches. |
| Improving ROIC | ROIC rising from below WACC to above WACC | The company is in turnaround. Early years may show value destruction; later years show value creation. The DCF must capture the inflection. Examples: Tata Motors post-2021 recovery, cement companies post-demand revival. |
| Chronically Low ROIC | ROIC < 8% for 5+ years with no improvement trend | The business is structurally challenged. Unless there is a credible turnaround catalyst (new management, regulatory change, industry consolidation), value it at or below book value. Growth assumptions should be minimal. |
Match each 5-year ROIC path to a pattern (and guess the real company):
- 18% → 19% → 18% → 20% → 19% → ? pattern — and name an Indian company that looks like this
- 4% → 6% → 9% → 13% → 16% → ? pattern
- 52% → 44% → 35% → 28% → 24% → ? pattern — what must your valuation assume about the future?
- 6% → 5% → 7% → 4% → 5% → ? pattern — what is the one thing that could change the story?
Pair: Compare matches. For pattern 3, discuss: would you extrapolate 24% forever, or keep fading it — and until when?
Reveal answers
1) Sustained High — Asian Paints / TCS. 2) Improving — Tata Motors post-2021 (or a cement recovery). 3) Mean-Reverting — fade the ROIC toward ~15% over the forecast; extrapolating 24% forever double-counts a shrinking moat. 4) Chronically Low — a PSU utility or pre-consolidation telecom; the catalyst is consolidation, regulation, or new management — without one, value near book.
5.7.2 Visualizing ROIC Trajectories
def plot_roic_trajectory(ticker_symbol, company_name, wacc=0.10):
"""Plot ROIC trend over time with WACC benchmark."""
ticker = yf.Ticker(ticker_symbol)
roic_df = compute_roic(ticker.financials, ticker.balance_sheet)
fig, axes = plt.subplots(1, 2, figsize=(16, 6))
# Chart 1: ROIC trend
ax = axes[0]
years = [str(d).split('-')[0] for d in roic_df.columns]
roic_vals = roic_df.loc['ROIC (%)']
# Color bars: green if > WACC, red if not
colors = ['#00c9a7' if v > wacc*100 else '#e0556a' for v in roic_vals]
ax.bar(years, roic_vals, color=colors, edgecolor='white', linewidth=0.5)
ax.axhline(y=wacc*100, color='orange', linestyle='--', linewidth=2,
label=f'WACC = {wacc*100:.0f}%')
ax.set_title(f'{company_name}: ROIC Trend')
ax.set_ylabel('ROIC (%)')
ax.legend()
ax.tick_params(axis='x', rotation=45)
# Chart 2: NOPAT and Invested Capital growth
ax = axes[1]
nopat_growth = roic_df.loc['NOPAT (Cr)'].pct_change(periods=-1) * 100
ic_growth = roic_df.loc['Invested Capital (Cr)'].pct_change(periods=-1) * 100
x = range(len(years) - 1)
w = 0.35
ax.bar([i - w/2 for i in x], nopat_growth.dropna(), w,
label='NOPAT Growth %', color='#6c8cff')
ax.bar([i + w/2 for i in x], ic_growth.dropna(), w,
label='IC Growth %', color='#f0a040')
ax.set_xticks(x)
ax.set_xticklabels(years[1:], rotation=45)
ax.set_title('NOPAT Growth vs Invested Capital Growth')
ax.axhline(y=0, color='red', linestyle='--', alpha=0.5)
ax.legend()
ax.set_ylabel('Growth Rate (%)')
# Annotation: if NOPAT grows faster than IC, ROIC is expanding
plt.suptitle(f'{company_name} — ROIC Analysis',
fontsize=14, fontweight='bold', y=1.02)
plt.tight_layout()
plt.show()
Your ROIC row 53 already holds 5–10 years across columns — the trajectory is sitting there; make it visible:
- Sparkline: select the ROIC row → Insert → Sparklines → Line → location: the row-label cell in column A. Each company sheet now shows its ROIC story as a tiny trend icon — flat-high, rising, fading, or flat-low is visible at a glance across the whole workbook.
- Trajectory tag — one formula that classifies the pattern automatically (newest year in G, first year in B, and 5 columns minimum):
IF(G53>B53*1.5, "Improving",
IF(G53<B53*0.7, "Mean-Reverting",
"Check / Chronically Low")))
Put this tag beside every company's sparkline. It is a crude classifier — always eyeball the sparkline too — but it turns a 10-company review from a number-hunt into a one-screen scan, exactly what the Python trajectory plots deliver.
5.8 ROIC Benchmarks Across Indian Industries
ROIC varies systematically by industry. Comparing a steel company's ROIC to a software company's is meaningless. The following benchmarks provide context for evaluating whether a company's ROIC is exceptional within its sector:
| Sector | Median ROIC Range | Top Quartile | Key Driver |
|---|---|---|---|
| IT Services | 25–45% | >45% (TCS) | Negative working capital + asset-light model |
| Consumer Staples | 20–40% | >40% (Nestle, HUL) | Brand pricing power + high turnover |
| Consumer Discretionary | 18–35% | >35% (Titan, Asian Paints) | Brand + distribution moat |
| Pharma | 12–22% | >22% (Divis, Laurus) | R&D productivity + patent protection |
| Cement | 8–16% | >16% (Shree Cement) | Low-cost production + regional pricing power |
| Automotive | 8–18% | >18% (Maruti, Bajaj Auto) | Scale + product mix + export margins |
| Telecom | 3–8% | >8% (Bharti Airtel, Jio) | ARPU growth + tower infrastructure sharing |
| Metals & Mining | 3–12% (cyclical) | >12% (Hindalco Novelis) | Global commodity prices + cost position |
| Power / Utilities | 5–10% | >10% (NTPC) | Regulated returns + plant load factor |
| PSU Banks | N/A (use ROA/ROE) | N/A | Net Interest Margin + asset quality |
Which sector does each ROIC profile belong to? Close the table first!
- Median ROIC 30%, driven by negative working capital
- Median ROIC 5%, driven by regulated returns and plant load factor
- Median ROIC 10% but highly cyclical, top quartile driven by cost position
- Median ROIC 16%, key driver = R&D productivity
Pair: Compare answers, then the harder question — for profile 3, why is a "median ROIC" almost meaningless, and what should you use instead?
Check your answers
1) IT Services · 2) Power/Utilities · 3) Metals & Mining · 4) Pharma. For metals: ROIC swings with the commodity cycle, so use through-the-cycle average ROIC (5–10 years), never a single-year median — a peak-year 14% can coexist with a trough-year 2%.
Hands-On Project: The ROIC Value Creation Map
Build a comprehensive ROIC analysis for 8–10 Indian companies across at least 3 sectors. Produce a "value creation map" — a scatter plot of ROIC vs Revenue Growth — that identifies which companies are genuine value creators, which are value destroyers, and which are in transition.
Excel Option Complete the identical project in Excel: one worksheet per company with the full formula chain from this session's Excel Path boxes (Inputs block → NOPAT → IC staircase → ROIC → Economic Profit → decomposition rows), rolled into a Summary sheet with ROIC, growth, spread, and economic profit per company. Build the Value Creation Map as a scatter chart (ROIC vs growth, labelled), set the vertical axis to cross at 10% (WACC) and horizontal at 0% to draw the quadrants, and use bubble size via a third series if you want market cap scaling. Validation: each company's ROIC should sit in the same ballpark as Screener.in's printed ROCE — if any is wildly off, audit that company's IC staircase first.
Steps
- Select 8–10 companies across at least 3 sectors. Include a mix: some known for high ROIC (TCS, Asian Paints), some in capital-intensive sectors (Tata Steel, NTPC), and one that might be a value destroyer.
- Compute ROIC for each company using the
compute_roic()pipeline. Use multiple years to get an average ROIC, not just the latest year. - Compute revenue growth (3-year CAGR) for each company.
- Build the Value Creation Map: a scatter plot with Revenue Growth on the x-axis and ROIC on the y-axis. Size each bubble by market capitalization. Add a horizontal line at WACC (10%) and a vertical line at 0% growth to create four quadrants.
- Classify each company:
- Top-Right: Value Creators (High Growth + ROIC > WACC)
- Top-Left: Cash Cows (Low Growth + ROIC > WACC)
- Bottom-Right: Value Destroyers (High Growth + ROIC < WACC)
- Bottom-Left: Stagnant (Low Growth + ROIC < WACC)
- Write a 300-word analysis: Which quadrant do most of your companies fall into? What does this tell you about value creation in the Indian corporate sector? Identify one company whose position surprised you and explain why.
- Gallery Walk (NEW): Pin or screen-share your Value Creation Map. Half the class stands by their maps; the other half circulates for 5 minutes asking one question per map: "defend this company's quadrant." Then swap roles. The two most-debated placements get replayed for the whole class.
View Solution / Walkthrough
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import yfinance as yf
import time
# --- Collect ROIC and Growth Data ---
companies = {
'TCS.NS': 'TCS',
'ASIANPAINT.NS': 'Asian Paints',
'TITAN.NS': 'Titan',
'HINDUNILVR.NS': 'HUL',
'NESTLEIND.NS': 'Nestle India',
'RELIANCE.NS': 'Reliance',
'TATASTEEL.NS': 'Tata Steel',
'NTPC.NS': 'NTPC',
'BHARTIARTL.NS': 'Bharti Airtel',
'MARUTI.NS': 'Maruti Suzuki'
}
results_list = []
WACC = 0.10
for ticker, name in companies.items():
try:
t = yf.Ticker(ticker)
info = t.info
# ROIC (average of available years)
roic_df = compute_roic(t.financials, t.balance_sheet)
avg_roic = roic_df.loc['ROIC (%)'].mean()
# 3-Year Revenue CAGR
rev = t.financials.loc['Total Revenue']
rev = rev.sort_index() # Oldest first
if len(rev) >= 4:
years = len(rev) - 1
cagr = ((rev.iloc[-1] / rev.iloc[-4]) ** (1/3) - 1) * 100
else:
cagr = rev.pct_change(periods=-1).mean() * 100
mkt_cap = info.get('marketCap', np.nan)
results_list.append({
'Company': name,
'Avg ROIC (%)': round(avg_roic, 1),
'3Y Rev CAGR (%)': round(cagr, 1),
'Market Cap (Cr)': mkt_cap / 1e7,
'Spread (%)': round(avg_roic - WACC*100, 1),
'Value Creator': avg_roic > WACC*100
})
print(f"✓ {name}: ROIC={avg_roic:.1f}%, Growth={cagr:.1f}%")
except Exception as e:
print(f"✗ {name}: {e}")
time.sleep(1.5)
results_df = pd.DataFrame(results_list)
results_df = results_df.set_index('Company')
print("\n=== Value Creation Summary ===")
print(results_df)
# --- Value Creation Map ---
fig, ax = plt.subplots(figsize=(14, 10))
for company in results_df.index:
row = results_df.loc[company]
size = max(row['Market Cap (Cr)'] / 50000, 80) # Scale bubble size
color = '#00c9a7' if row['Value Creator'] else '#e0556a'
ax.scatter(row['3Y Rev CAGR (%)'], row['Avg ROIC (%)'],
s=size, c=color, edgecolors='white', linewidth=2,
alpha=0.8, zorder=5)
ax.annotate(company,
(row['3Y Rev CAGR (%)'], row['Avg ROIC (%)']),
textcoords="offset points", xytext=(8, 6), fontsize=10,
fontweight='bold')
# Quadrant lines
ax.axhline(y=WACC*100, color='orange', linestyle='--', linewidth=2,
label=f'WACC = {WACC*100:.0f}%')
ax.axvline(x=0, color='gray', linestyle='--', linewidth=1.5,
label='Zero Growth')
# Quadrant labels
x_max = ax.get_xlim()[1]
y_max = ax.get_ylim()[1]
ax.text(x_max*0.95, y_max*0.95, 'VALUE CREATORS\n(High Growth + ROIC > WACC)',
ha='right', va='top', fontsize=11, fontweight='bold', color='#00c9a7',
bbox=dict(boxstyle='round', facecolor='black', alpha=0.6))
ax.text(x_max*0.05, y_max*0.95, 'CASH COWS\n(Low Growth + ROIC > WACC)',
ha='left', va='top', fontsize=11, fontweight='bold', color='#00c9a7',
bbox=dict(boxstyle='round', facecolor='black', alpha=0.6))
ax.text(x_max*0.95, WACC*100*0.2, 'VALUE DESTROYERS\n(High Growth + ROIC < WACC)',
ha='right', va='bottom', fontsize=11, fontweight='bold', color='#e0556a',
bbox=dict(boxstyle='round', facecolor='black', alpha=0.6))
ax.text(x_max*0.05, WACC*100*0.2, 'STAGNANT\n(Low Growth + ROIC < WACC)',
ha='left', va='bottom', fontsize=11, fontweight='bold', color='#e0556a',
bbox=dict(boxstyle='round', facecolor='black', alpha=0.6))
ax.set_xlabel('3-Year Revenue CAGR (%)', fontsize=12)
ax.set_ylabel('Average ROIC (%)', fontsize=12)
ax.set_title('Indian Corporate Value Creation Map\n(Bubble Size = Market Cap)',
fontsize=15, fontweight='bold')
ax.legend(loc='lower left', fontsize=9)
ax.grid(True, alpha=0.2)
plt.tight_layout()
plt.show()
# --- Summary Stats ---
n_creators = results_df['Value Creator'].sum()
n_total = len(results_df)
print(f"\n{n_creators}/{n_total} companies earn ROIC > WACC ({n_creators/n_total*100:.0f}%)")
print(f"Average Spread: {results_df['Spread (%)'].mean():.1f}%")
Key Insights from a Typical Run:
- Value Creators (Top-Right): Asian Paints, Titan, and TCS typically occupy this quadrant — high ROIC with sustained growth. These are the compounding machines of Indian industry.
- Cash Cows (Top-Left): HUL and Nestle India often appear here — exceptional ROIC but mature growth. The valuation question is: can they sustain the ROIC, and how much of earnings should be returned to shareholders?
- Cyclical/Transitional: Tata Steel and NTPC typically show ROIC near or below WACC, with growth driven by commodity cycles and capex cycles respectively. Their position on the map shifts with the cycle.
- Surprise candidate: Bharti Airtel's ROIC has improved from sub-5% (pre-2019) to 10%+ as the telecom sector consolidated to a 3-player market. It is a case study in how industry structure determines ROIC — not just company-specific advantages.
Key Takeaways
ROIC is the single most important number in valuation. It determines whether growth creates or destroys value. A company growing at 20% with ROIC below WACC is burning shareholder wealth.
NOPAT and Invested Capital must be carefully adjusted. The raw financial statements mix operating and non-operating items. Your job is to isolate the operating core.
Economic Profit = (ROIC − WACC) × Invested Capital. It is the absolute rupee amount of value created. Positive net income does not guarantee positive economic profit.
ROIC = NOPAT Margin × Capital Turnover. High ROIC comes from either pricing power (high margins) or capital efficiency (high turnover). The best companies have both.
The ROIC trend matters more than a single year's value. Is ROIC sustained, mean-reverting, improving, or chronically low? The trajectory determines the terminal value assumption in your DCF.
Test Your Understanding
1. A company has EBIT of Rs. 500 Cr, an effective tax rate of 25%, and no non-operating income. Its equity is Rs. 2,000 Cr, total debt is Rs. 1,000 Cr, and cash is Rs. 400 Cr. What is its ROIC?
2. How can a company report positive net income but negative economic profit?
3. In the McKinsey ROIC decomposition, ROIC = NOPAT Margin × Capital Turnover. A company has an ROIC of 30%, with a NOPAT Margin of 15%. What is its Capital Turnover?
4. Why is excess cash subtracted from invested capital when computing ROIC?
5. A company's ROIC has declined from 28% to 16% over 5 years while its revenue grew at 18% annually. Invested capital grew at 30% annually. What does this pattern suggest?
Name one company you now suspect is a value destroyer despite healthy profits. Write down its ROIC, its likely WACC, and the sign of its economic profit. Keep this note — it is a candidate capstone company, and Session 6's dashboard will let you show its ROIC-vs-WACC gap visually to a boardroom audience.