Value Drivers & Financial Data Analytics with Python
Extract financial data programmatically, automate ratio computation, and identify the economic drivers of value — all in Python.
Learning Objectives
- Set up a Python environment for financial data analysis (yfinance, pandas, numpy)
- Fetch historical financial data for Indian companies from Yahoo Finance using yfinance
- Extract and navigate financial statements (income statement, balance sheet, cash flow) programmatically
- Compute key value drivers — growth rates, margins, ROIC, and efficiency ratios — from raw data
- Build a reusable Python pipeline that automates the financial extraction and value driver computation workflow
- Excel track: Perform the identical analysis in Excel using Screener.in data, cell formulas, and charts — no coding required
Every computation in this session is shown in two ways: the primary Python path (marked Python) for automation and scale, and a parallel Excel path (marked with green Excel Path boxes) for students who prefer spreadsheets or do not yet code. Both produce the same numbers — pick the track that suits you, or do both and cross-check. Excel users should follow along with the green boxes; Python users can skip them.
🔍 Opening Challenge — The 100-Company Data Race
3.1 Setting Up Your Python Environment
Before we write a single line of analysis code, we need the right tools installed. The core libraries for financial data analytics are mature, well-documented, and free. Here is what you need:
| Library | Purpose | Install Command |
|---|---|---|
| yfinance | Download historical market data and financial statements from Yahoo Finance. Supports Indian stocks with the .NS (NSE) and .BO (BSE) suffixes. |
pip install yfinance |
| pandas | Data manipulation and analysis — DataFrames, time series, merging, grouping, and financial computations. | pip install pandas |
| numpy | Numerical computing — array operations, statistical functions, and financial math (NPV, IRR). | pip install numpy |
| matplotlib | Data visualization — line charts, bar charts, and histograms for exploratory analysis. | pip install matplotlib |
| seaborn | Statistical visualizations built on matplotlib — heatmaps, pair plots, and distribution charts. | pip install seaborn |
You can install all of them at once:
pip install yfinance pandas numpy matplotlib seaborn
.py scripts. We will use the notebook style for this chapter's examples, but every code block can be saved as a script. Install Jupyter with pip install jupyterlab.
Run this in Jupyter or a script. Record the versions:
import yfinance as yf
import pandas as pd
import numpy as np
import matplotlib
import seaborn
print(f"yfinance: {yf.__version__}")
print(f"pandas: {pd.__version__}")
print(f"numpy: {np.__version__}")
print(f"matplotlib: {matplotlib.__version__}")
print(f"seaborn: {seaborn.__version__}")
Success criterion: No ModuleNotFoundError. If you get an error, reinstall the missing library and rerun. Help a neighbor who is stuck.
No installation needed. You need only three things:
- Excel (or Google Sheets — everything here works in both; Google Sheets is free).
- A free Screener.in account (screener.in) — our data source. It provides 10+ years of standardized Indian company financials, exportable to Excel with one click.
- A new blank workbook named
ValueDriver_Analysis.xlsxwith one worksheet per company you will analyze.
Excel vs Python at a glance: Excel wins on visibility (you see every number) and zero learning curve; Python wins on automation (100 companies in one run) and repeatability. For a single company, Excel is equally valid — the analysis logic is identical, only the tool changes.
3.2 How yfinance Works with Indian Stocks
yfinance is a Python wrapper around Yahoo Finance's API. It lets you download historical price data, financial statements, dividend history, and corporate actions — all into pandas DataFrames. For Indian stocks, the critical detail is the ticker suffix:
| Exchange | Suffix | Example |
|---|---|---|
| National Stock Exchange (NSE) | .NS | RELIANCE.NS, TCS.NS, INFY.NS |
| Bombay Stock Exchange (BSE) | .BO | RELIANCE.BO, TCS.BO |
The .NS suffix is generally preferred because NSE data on Yahoo Finance tends to be more complete and liquid. Here is the basic pattern for creating a Ticker object and fetching data:
import yfinance as yf
import pandas as pd
import numpy as np
# Create a Ticker object for an Indian stock
ticker = yf.Ticker("RELIANCE.NS")
# Get basic company info
info = ticker.info
print(f"Company: {info.get('longName')}")
print(f"Sector: {info.get('sector')}")
print(f"Market Cap: {info.get('marketCap'):,} INR")
print(f"P/E Ratio: {info.get('trailingPE')}")
# Fetch historical price data — last 5 years
hist = ticker.history(period="5y")
print(hist.head())
# Columns: Open, High, Low, Close, Volume, Dividends, Stock Splits
ticker.info dictionary contains dozens of fields, but not all are populated for every stock. Indian stocks may have gaps in fields like trailingPegRatio or recommendationMean. Always check if a field exists before using it. Also, yfinance data is not real-time — it is delayed by 15–20 minutes for NSE stocks, and financial statement data is updated only after companies file their quarterly/annual reports.
Before running the code, predict the output:
- For
yf.Ticker("TCS.NS").info.get('longName'), what will Python print? - For
yf.Ticker("RELIANCE.NS").info.get('sector'), what sector do you expect? - Why does the market cap print with
:and,formatting — what does:in the f-string do?
Then run it and compare. If your prediction was wrong, note why.
.NS suffix preferred over .BO for most Indian stocks? Hint: think about data completeness and liquidity — check the paragraph above if unsure.
Instead of yf.Ticker("RELIANCE.NS"), Excel users get identical data (actually deeper history — 10+ years) through Screener.in:
- Go to screener.in and search the company (e.g., "Asian Paints").
- On the company page you will see the Profit & Loss, Balance Sheet, and Cash Flow tables — the same three statements yfinance returns.
- Scroll below the "Export to Excel" section — or copy the tables directly: select the table on the page, copy, and paste into Excel (use Paste Special → Text if formatting comes out odd).
- Repeat for each company, one worksheet per company, named by ticker (e.g.,
ASIANPAINT).
What Screener.in gives you for free: Sales, Expenses, OPM (operating margin — already computed!), Net Profit, EPS, Debt, Cash & Investments, Book Value, ROCE, ROE — 10 years, standardized across companies. The pre-computed ratios replace several of our Python functions.
3.3 Fetching Financial Statements Programmatically
yfinance provides three properties that return the financial statements as pandas DataFrames. Each has a transposed structure: columns are fiscal years, and rows are line items.
3.3.1 The Income Statement
# Fetch annual income statement
income_stmt = ticker.financials # or ticker.income_stmt
print(income_stmt.columns) # Fiscal year-end dates
print(income_stmt.index[:10]) # First 10 line items
# Key line items we care about:
# Total Revenue, Cost of Revenue, Gross Profit,
# Operating Income (EBIT), Net Income, EBITDA
The income statement is the starting point for computing revenue growth, margins, and operating profitability. Each column is a fiscal year, and the years run left to right (most recent on the left).
3.3.2 The Balance Sheet
# Fetch annual balance sheet
balance_sheet = ticker.balance_sheet
print(balance_sheet.columns)
# Key line items:
# Total Assets, Total Debt, Cash and Cash Equivalents,
# Total Equity, Current Assets, Current Liabilities,
# Accounts Receivable, Inventory, Accounts Payable
The balance sheet gives us invested capital, net debt, working capital components, and book value of equity.
3.3.3 The Cash Flow Statement
# Fetch annual cash flow statement
cash_flow = ticker.cashflow
print(cash_flow.columns)
# Key line items:
# Operating Cash Flow, Capital Expenditure,
# Free Cash Flow, Dividends Paid,
# Issuance/Repayment of Debt, Repurchase of Stock
The cash flow statement provides operating cash flow, capex, and free cash flow — the raw ingredients for DCF valuation.
ticker.quarterly_financials, ticker.quarterly_balance_sheet, and ticker.quarterly_cashflow for quarterly data. Quarterly data is useful for tracking within-year trends and building trailing-twelve-month (TTM) metrics. TTM = sum of the last 4 quarters — it gives a more current picture than the last annual report.
Fetch and explore the income statement for any Indian company you choose. Answer these questions by running code:
import yfinance as yf
ticker = yf.Ticker("ASIANPAINT.NS")
income_stmt = ticker.financials
# 1. What is the shape of the DataFrame?
print(income_stmt.shape)
# 2. What are the column labels (fiscal year-end dates)?
print(list(income_stmt.columns))
# 3. How many line items (rows) are there?
print(len(income_stmt))
# 4. What are the first 5 line items?
print(list(income_stmt.index[:5]))
# 5. Does the line item 'Total Revenue' exist?
print('Total Revenue' in income_stmt.index)
Then swap companies with your neighbor and repeat. Compare: do different companies have the same line items available?
🔗 Before You Move On — 3 Quick Checks
True or False: In yfinance, ticker.financials has fiscal years as columns and line items as rows.
Reveal
True or False: The most recent fiscal year is the rightmost column of ticker.financials.
Reveal
pct_change(periods=-1) is needed for growth rates.True or False: Every company reports exactly the same line items in ticker.financials.
Reveal
safe_get() fallback logic matters.In Python, yfinance gives you a transposed DataFrame (years as columns). Replicate this exact layout in Excel so both tracks stay comparable:
| A | B | C | D | … | |
|---|---|---|---|---|---|
| 1 | Line Item | FY2021 | FY2022 | FY2023 | → older→newer |
| 2 | Sales | 21,159 | 25,394 | 28,375 | … |
| 3 | Operating Profit | 3,772 | 4,254 | 4,313 | … |
| 4 | Net Profit | 2,533 | 3,058 | 3,195 | … |
Key conventions to follow all session:
- Rows = line items, columns = fiscal years (oldest left → newest right — the opposite of yfinance, so growth formulas stay intuitive).
- Keep one worksheet per company, and put the company name in cell
A1. - Create a separate Summary sheet where formulas from all company sheets roll up (we build this in the Section 3.6 Excel Path).
Values in Rs. Crore throughout — Screener.in exports are already in crores, so no unit conversion is needed.
3.4 Computing Key Value Drivers from the Data
With the raw financial data in pandas DataFrames, we can now compute the value drivers we identified in Chapter 1. The goal is to write clean, reusable functions that work for any company.
3.4.1 Revenue Growth Rate
def compute_revenue_growth(income_stmt):
"""Compute year-over-year revenue growth rates."""
revenue = income_stmt.loc['Total Revenue'] # or 'Revenue'
# Revenue is a Series with fiscal years as index
growth_rates = revenue.pct_change(periods=-1) * 100
# pct_change(periods=-1) computes (this_year - last_year) / last_year
# Negative periods because columns go newest → oldest
return growth_rates.dropna()
revenue_growth = compute_revenue_growth(income_stmt)
print("Revenue Growth Rates (%):")
print(revenue_growth)
3.4.2 Profit Margins
def compute_margins(income_stmt):
"""Compute gross, operating, and net margins."""
revenue = income_stmt.loc['Total Revenue']
gross_profit = income_stmt.loc['Gross Profit']
ebit = income_stmt.loc['Operating Income'] # or 'EBIT'
net_income = income_stmt.loc['Net Income']
margins = pd.DataFrame({
'Gross Margin (%)': (gross_profit / revenue) * 100,
'Operating Margin (%)': (ebit / revenue) * 100,
'Net Margin (%)': (net_income / revenue) * 100
})
return margins.round(2)
margins = compute_margins(income_stmt)
print(margins)
Challenge 1 — Predict: In compute_revenue_growth, why is pct_change(periods=-1) used instead of the default pct_change()? Write your answer in one sentence before looking at the answer.
Challenge 2 — Write a function that returns the Net Margin for any company, handling the case where the 'Net Income' line is missing:
def compute_net_margin(income_stmt):
# Revenue = income_stmt.loc['Total Revenue']
# Net income line may be 'Net Income' or 'Net Income Common Stockholders'
# Return net margin as a Series
...
Hint
Use a safe_get-style helper, or check income_stmt.index to see what line items exist. Try 'Net Income' in income_stmt.index first.
Every Python function in this section has a one-line Excel equivalent. Assume the layout from the previous Excel Path: years across columns B:G (oldest→newest), and these row anchors:
| Row | Contains |
|---|---|
| 2 | Sales |
| 3 | Operating Profit (OP) |
| 4 | Net Profit |
| 5 | Total Debt (Borrowings) |
| 6 | Cash & Investments |
| 7 | Net Worth / Equity |
| 8 | Total Assets |
1. Revenue growth (the Excel version of compute_revenue_growth):
= (C2 - B2) / B2 ' newest − prior, divided by prior
' Or use the built-in, then drag right across all years:
= GROWTH isn't needed — simply drag C9 to D9:G9
2. Margins (compute_margins):
= C3 / C2 * 100
' Net Margin % (row 11):
= C4 / C2 * 100
3. ROIC (compute_roic) — the row layout trick: First add helper rows:
= C3 * (1 - 0.25)
' Invested Capital (row 13) = Equity + Debt − Cash:
= C7 + C5 - C6
' ROIC % (row 14):
= C12 / C13 * 100
4. Leverage and efficiency — the same pattern:
= C5 / C7
' Asset Turnover (row 16):
= C2 / C8
' FCF Yield (row 17) — needs OCF (row 18) and Capex (row 19) rows from the Cash Flow sheet:
= (C18 - C19) / MarketCap_Cell * 100
Golden rule: write each formula once in the newest-year column, then drag right — Excel updates the column references automatically, exactly like the pandas functions operate across all years at once. Use Ctrl + Shift + → then drag to copy across fast.
3.4.3 Return on Invested Capital (ROIC)
ROIC is the most important value driver. We compute it as NOPAT divided by Invested Capital. For a preliminary calculation from yfinance data:
def compute_roic(income_stmt, balance_sheet):
"""Compute preliminary ROIC from financial statement data."""
ebit = income_stmt.loc['Operating Income']
# Estimate tax rate — yfinance sometimes has this, otherwise use 25%
tax_rate = 0.25
nopat = ebit * (1 - tax_rate)
# Invested Capital = Total Equity + Total Debt - Cash
total_equity = balance_sheet.loc['Total Equity Gross Minority Interest']
total_debt = balance_sheet.loc['Total Debt']
cash = balance_sheet.loc['Cash And Cash Equivalents']
invested_capital = total_equity + total_debt - cash
roic = (nopat / invested_capital) * 100
return roic.round(2)
roic = compute_roic(income_stmt, balance_sheet)
print("ROIC (%):")
print(roic)
income_stmt.index to find the exact key for each line item.
3.4.4 Efficiency Ratios
def compute_efficiency_ratios(income_stmt, balance_sheet):
"""Compute asset turnover and working capital ratios."""
revenue = income_stmt.loc['Total Revenue']
total_assets = balance_sheet.loc['Total Assets']
receivables = balance_sheet.loc.get('Accounts Receivable', None)
inventory = balance_sheet.loc.get('Inventory', None)
payables = balance_sheet.loc.get('Accounts Payable', None)
ratios = pd.DataFrame({
'Asset Turnover': (revenue / total_assets).round(2),
})
if receivables is not None:
ratios['Receivable Turnover'] = (revenue / receivables).round(2)
ratios['Days Receivable'] = (365 / ratios['Receivable Turnover']).round(0)
if inventory is not None:
ratios['Inventory Turnover'] = (revenue / inventory).round(2)
ratios['Days Inventory'] = (365 / ratios['Inventory Turnover']).round(0)
return ratios
efficiency = compute_efficiency_ratios(income_stmt, balance_sheet)
print(efficiency)
3.4.5 Leverage Ratios
def compute_leverage_ratios(balance_sheet):
"""Compute debt ratios."""
total_debt = balance_sheet.loc['Total Debt']
total_equity = balance_sheet.loc['Total Equity Gross Minority Interest']
total_assets = balance_sheet.loc['Total Assets']
ratios = pd.DataFrame({
'Debt-to-Equity': (total_debt / total_equity).round(2),
'Debt-to-Assets': (total_debt / total_assets).round(2),
})
return ratios
leverage = compute_leverage_ratios(balance_sheet)
print(leverage)
3.4.6 Free Cash Flow Yield
def compute_fcf_yield(cash_flow, market_cap):
"""Compute Free Cash Flow Yield."""
operating_cf = cash_flow.loc['Operating Cash Flow']
capex = cash_flow.loc['Capital Expenditure'] # Usually negative
free_cash_flow = operating_cf + capex # capex is negative, so this subtracts
fcf_yield = (free_cash_flow / market_cap) * 100
return fcf_yield.round(2)
# market_cap from ticker.info['marketCap'] for the most recent year
fcf_yield = compute_fcf_yield(cash_flow, info['marketCap'])
print("FCF Yield (%):")
print(fcf_yield)
3.5 The Complete Value Driver Pipeline
Now we combine everything into a single, reusable function. Given a ticker symbol, it returns a comprehensive DataFrame of value drivers across all available fiscal years.
The Python fetch_and_analyze() function hides data-fetch and computation in one call. In Excel, the equivalent is a disciplined workbook structure you build once and reuse for every company:
- Sheet 1: "Data" — raw pasted numbers from Screener.in. Never touch this sheet after pasting; it is your "yfinance".
- Sheet 2: "Drivers" — all formulas from the Section 3.4 Excel Path, referencing Data with cross-sheet formulas like =Data!C2. This sheet is your
driversDataFrame. - Sheet 3: "Summary" — the latest-year snapshot: one column per company, one row per driver (built in the next Excel Path).
- Sheet 4: "Charts" — trend lines and comparison charts (Section 3.7 Excel Path).
Why separation matters: when you paste next year's Screener.in data into "Data", every downstream formula and chart updates automatically — the same reuse benefit the Python function gives. Mixing raw data and formulas in one sheet is the #1 cause of broken Excel models.
def fetch_and_analyze(ticker_symbol):
"""
Fetch financial data for an Indian company and compute
all key value drivers.
Parameters
----------
ticker_symbol : str
Yahoo Finance ticker (e.g., 'RELIANCE.NS', 'TCS.NS')
Returns
-------
dict containing DataFrames of growth, margins, ROIC,
efficiency, leverage, and FCF yield.
"""
ticker = yf.Ticker(ticker_symbol)
info = ticker.info
# --- Fetch Statements ---
is_df = ticker.financials # Income Statement
bs_df = ticker.balance_sheet # Balance Sheet
cf_df = ticker.cashflow # Cash Flow Statement
mkt_cap = info.get('marketCap', None)
# --- Compute Value Drivers ---
results = {}
# Revenue & Earnings Growth
if 'Total Revenue' in is_df.index:
rev = is_df.loc['Total Revenue']
results['Revenue Growth (%)'] = rev.pct_change(periods=-1) * 100
results['Revenue (Cr)'] = rev / 1e7 # Convert to crores
if 'Net Income' in is_df.index:
ni = is_df.loc['Net Income']
results['Net Income Growth (%)'] = ni.pct_change(periods=-1) * 100
# Margins
if 'Total Revenue' in is_df.index and 'Gross Profit' in is_df.index:
results['Gross Margin (%)'] = (is_df.loc['Gross Profit'] / is_df.loc['Total Revenue']) * 100
if 'Total Revenue' in is_df.index and 'Operating Income' in is_df.index:
results['Operating Margin (%)'] = (is_df.loc['Operating Income'] / is_df.loc['Total Revenue']) * 100
if 'Total Revenue' in is_df.index and 'Net Income' in is_df.index:
results['Net Margin (%)'] = (is_df.loc['Net Income'] / is_df.loc['Total Revenue']) * 100
# ROIC (preliminary)
if all(x in is_df.index for x in ['Operating Income']) and \
all(x in bs_df.index for x in ['Total Equity Gross Minority Interest', 'Total Debt', 'Cash And Cash Equivalents']):
ebit = is_df.loc['Operating Income']
nopat = ebit * 0.75 # assuming 25% tax
ic = bs_df.loc['Total Equity Gross Minority Interest'] + bs_df.loc['Total Debt'] - bs_df.loc['Cash And Cash Equivalents']
results['ROIC (%)'] = (nopat / ic) * 100
# Efficiency
if 'Total Revenue' in is_df.index and 'Total Assets' in bs_df.index:
results['Asset Turnover'] = is_df.loc['Total Revenue'] / bs_df.loc['Total Assets']
# Leverage
if 'Total Debt' in bs_df.index and 'Total Equity Gross Minority Interest' in bs_df.index:
results['Debt-to-Equity'] = bs_df.loc['Total Debt'] / bs_df.loc['Total Equity Gross Minority Interest']
# FCF Yield
if mkt_cap and 'Operating Cash Flow' in cf_df.index and 'Capital Expenditure' in cf_df.index:
fcf = cf_df.loc['Operating Cash Flow'] + cf_df.loc['Capital Expenditure']
results['FCF Yield (%)'] = (fcf / mkt_cap) * 100
# --- Compile into a single DataFrame ---
drivers_df = pd.DataFrame(results).T # Transpose: metrics as rows, years as columns
drivers_df = drivers_df.round(2)
drivers_df = drivers_df.sort_index(axis=1) # Sort years chronologically
return {
'drivers': drivers_df,
'info': info,
'income_stmt': is_df,
'balance_sheet': bs_df,
'cash_flow': cf_df
}
# --- Usage ---
result = fetch_and_analyze("TCS.NS")
print("=== Value Drivers for TCS ===")
print(result['drivers'])
print(f"\nCompany: {result['info'].get('longName')}")
print(f"Sector: {result['info'].get('sector')}")
print(f"Market Cap: {result['info'].get('marketCap'):,} INR")
Run fetch_and_analyze("TCS.NS") and answer:
- What is TCS's latest ROIC? (should be very high for a debt-free IT company)
- What is its Debt-to-Equity? (should be near zero)
- Which metric in the drivers table surprised you the most? Why?
Now swap in YOUR chosen company (e.g., "ASIANPAINT.NS", "TATASTEEL.NS") and compare. Which company has the higher ROIC? Which has the higher FCF yield?
This code has a bug. Find it before running:
# Intended: compute revenue growth for each fiscal year
revenue = income_stmt.loc['Total Revenue']
growth = revenue.pct_change() # default periods=1 — but columns are newest-first!
print("Revenue Growth Rates:")
print(growth)
Why is this WRONG? Hints: What order are the columns in? What does pct_change() default to? Discuss with your partner, then state the fix.
Reveal the fix
Columns are newest-first, so default pct_change() computes (older − newest) / newest. Use pct_change(periods=-1) to compute (newest − older) / older correctly.
3.6 Multi-Company Value Driver Comparison
One of the most powerful applications of programmatic analysis is comparing value drivers across multiple companies simultaneously. The following function fetches data for a list of tickers and produces a comparison table for the most recent year:
def compare_companies(tickers, labels=None):
"""
Fetch and compare value drivers across multiple companies
for the most recent fiscal year.
Parameters
----------
tickers : list of str
Yahoo Finance tickers
labels : list of str, optional
Display names (defaults to ticker symbols)
Returns
-------
DataFrame with companies as columns and value drivers as rows
"""
if labels is None:
labels = [t.replace('.NS', '').replace('.BO', '') for t in tickers]
all_drivers = {}
for ticker, label in zip(tickers, labels):
try:
result = fetch_and_analyze(ticker)
# Get the most recent year column
latest_year = result['drivers'].columns[0]
all_drivers[label] = result['drivers'][latest_year]
print(f"✓ {label} — data fetched")
except Exception as e:
print(f"✗ {label} — error: {e}")
comparison = pd.DataFrame(all_drivers)
return comparison
# --- Compare top Indian IT companies ---
it_companies = {
'TCS.NS': 'TCS',
'INFY.NS': 'Infosys',
'WIPRO.NS': 'Wipro',
'HCLTECH.NS': 'HCL Tech',
'TECHM.NS': 'Tech Mahindra'
}
comparison = compare_companies(
list(it_companies.keys()),
list(it_companies.values())
)
print("\n=== IT Sector Value Driver Comparison (Latest Year) ===")
print(comparison)
The Python compare_companies() loop becomes your Summary sheet. One column per company, one row per driver — all pulled live from each company's Drivers sheet:
- On the Summary sheet, put company names in row 1:
B1: TCS,C1: INFY,D1: WIPRO… - Put driver labels in column A: Revenue Growth %, Op Margin %, ROIC %, D/E, Asset Turnover.
- Pull the latest-year value with a cross-sheet reference. If each company's Drivers sheet holds ROIC in row 14 and FY2025 in column G:
=TCS!G9
' In Summary!C2 (INFY revenue growth):
=INFY!G9
' Then add the peer MEDIAN in the last column (this is your peer group benchmark):
=MEDIAN(B2:D2)
One formula, dragged once: enter =B1-style references across a row, then select the row and drag down — every driver updates for every company at once. This Summary sheet IS your comparison DataFrame, and =MEDIAN() replaces the .median() we use in Python.
Modify the it_companies dictionary to run a comparison across a DIFFERENT sector. For example:
banks = {
'HDFCBANK.NS': 'HDFC Bank',
'ICICIBANK.NS': 'ICICI Bank',
'SBIN.NS': 'SBI',
'KOTAKBANK.NS': 'Kotak',
'AXISBANK.NS': 'Axis'
}
Answer: Do banks behave like IT companies? What happens to ROIC and FCF Yield for banks? Why?
Tip: Banks have a different business model — their "debt" is deposits, so leverage ratios behave differently. You will learn the correct way to value financial firms later.
Think: TCS and Infosys have similar operating margins (~24–26%). Yet TCS's ROIC often exceeds Infosys's. How can two companies with nearly identical margins have different ROIC?
Pair: Discuss. Recall: ROIC = NOPAT / Invested Capital and ROIC = Margin × Capital Turnover. What drives the difference?
Share: 2 pairs share — what is the missing variable?
3.7 Visualizing Value Drivers with matplotlib and seaborn
Numbers in tables are precise; charts reveal patterns. Let us create visualizations that make value driver trends immediately obvious.
3.7.1 Trend Charts for a Single Company
import matplotlib.pyplot as plt
import seaborn as sns
sns.set_style("darkgrid")
plt.rcParams['figure.figsize'] = (12, 5)
def plot_value_drivers_trend(result, company_name):
"""Plot revenue growth, margins, and ROIC over time."""
drivers = result['drivers']
fig, axes = plt.subplots(1, 3, figsize=(16, 5))
# Revenue & Net Income Growth
ax = axes[0]
growth_metrics = ['Revenue Growth (%)', 'Net Income Growth (%)']
for metric in growth_metrics:
if metric in drivers.index:
ax.plot(drivers.columns, drivers.loc[metric], marker='o', label=metric)
ax.set_title(f'{company_name}: Growth Trends')
ax.legend()
ax.axhline(y=0, color='red', linestyle='--', alpha=0.5)
ax.tick_params(axis='x', rotation=45)
# Margins
ax = axes[1]
margin_metrics = ['Gross Margin (%)', 'Operating Margin (%)', 'Net Margin (%)']
for metric in margin_metrics:
if metric in drivers.index:
ax.plot(drivers.columns, drivers.loc[metric], marker='s', label=metric)
ax.set_title('Margin Trends')
ax.legend()
ax.tick_params(axis='x', rotation=45)
# ROIC
ax = axes[2]
if 'ROIC (%)' in drivers.index:
ax.bar(range(len(drivers.columns)), drivers.loc['ROIC (%)'],
color=['#6c8cff' if v > 15 else '#e0556a' for v in drivers.loc['ROIC (%)']])
ax.set_xticks(range(len(drivers.columns)))
ax.set_xticklabels([str(d).split('-')[0] for d in drivers.columns], rotation=45)
ax.set_title('ROIC (%) — Green = >15%')
ax.axhline(y=15, color='green', linestyle='--', alpha=0.7, label='15% threshold')
ax.legend()
plt.tight_layout()
plt.show()
# Usage
result = fetch_and_analyze("ASIANPAINT.NS")
plot_value_drivers_trend(result, "Asian Paints")
3.7.2 Sector Comparison Heatmap
def plot_sector_heatmap(comparison_df):
"""Create a heatmap comparing value drivers across companies."""
plt.figure(figsize=(14, 6))
sns.heatmap(comparison_df, annot=True, fmt='.1f', cmap='RdYlGn',
center=0, linewidths=0.5, cbar_kws={'label': 'Value'})
plt.title('Sector Value Driver Comparison')
plt.tight_layout()
plt.show()
# plot_sector_heatmap(comparison)
Customize the plot_value_drivers_trend function:
- Change the
sns.set_style("darkgrid")to"whitegrid"— what changes visually? - Change the ROIC bar colors: currently green if >15%, red otherwise. Change the threshold to 10% (WACC proxy).
- Add a line for Revenue Growth to the second (margins) chart.
Bonus: Run it for a company of your choice and save the figure with plt.savefig('my_drivers.png').
Excel's chart engine replaces matplotlib. Because your data is already laid out in rows-and-years, each chart is select → insert → format:
- Growth trend line (the Python Chart 1): On a company's Drivers sheet, select the Revenue Growth row (including year labels) → Insert → Line Chart.
- Margins trend (Chart 2): Select the Op Margin and Net Margin rows together (hold Ctrl to select non-adjacent rows) → Insert → Line Chart. Both series appear automatically.
- ROIC bars vs threshold (Chart 3): Select the ROIC row → Insert → Column Chart. To color bars green above 10% like the Python version: click a bar → right-click → Format Data Series → Fill → Invert if Negative, or simpler — add a helper row =IF(C14>10, C14, "") and chart that series in green beside the base series.
Growth vs ROIC scatter (the value creation map): on the Summary sheet, select the Revenue Growth row and the ROIC row (use Ctrl for both) → Insert → Scatter (X Y) → then right-click a point → Add Data Labels, and label with company names from row 1. This is your quadrant map — add axis lines at 0% growth and 10% ROIC via Chart Elements → Axes → More Axis Options to set the crossing value.
3.8 Handling Common Data Issues with Indian Stocks
Real-world financial data is messy. Here are the most common issues you will encounter when fetching Indian company data and how to handle them:
| Issue | Cause | Solution |
|---|---|---|
| Missing line items | yfinance uses generic field names; not all companies report all fields, and field names vary | Use df.index.tolist() to inspect available fields. Write fallback logic: try multiple possible field names. |
| Stale financials | Yahoo Finance updates financial statements with a delay; most recent quarter may be missing | Check the date of the latest column. If it is more than 6 months old, supplement with quarterly data: ticker.quarterly_financials. |
| Currency confusion | yfinance reports Indian stocks in INR, but some fields may appear in crores or lakhs inconsistently | Verify magnitudes. If "Total Revenue" shows 5.8e11 for Reliance, it is in raw INR (~Rs. 5,80,000 crore). Divide by 1e7 for crores, 1e5 for lakhs. |
| Ministry of Corporate Affairs (MCA) vs exchange filings | Indian companies file with both MCA and stock exchanges; yfinance may not always have the latest filing | For critical analysis, cross-check yfinance data against the company's investor relations page or Screener.in. Use yfinance for rapid prototyping, not final analysis. |
| Rate limiting | Yahoo Finance may throttle requests if you make too many too quickly | Add time.sleep(2) between ticker requests when looping over many companies. |
| Corporate actions | Splits, bonuses, and rights issues distort historical price data and per-share metrics | yfinance automatically adjusts historical prices for splits. For fundamental data, manually check for dilutive events in the period under analysis. |
This code crashes for many companies. Why — and how do you fix it?
# This sometimes raises a KeyError!
revenue = income_stmt.loc['Total Revenue']
net_income = income_stmt.loc['Net Income']
net_margin = (net_income / revenue) * 100
print(net_margin)
With your partner:
- Identify when the
KeyErrorhappens. - Rewrite the code using a
safe_get()helper that tries multiple field names.
Hint
Some companies report Net Income as 'Net Income Common Stockholders'. The safe_get pattern from Section 3.5 already solves this — reuse it!
Python raises KeyError; Excel shows #N/A (missing line) and #DIV/0! (zero denominator). The Excel safe_get is a pair of functions you should wrap around every ratio:
=IFERROR(C4/C2*100, "")
' IFNA handles missing line items pulled with lookups:
=IFNA(VLOOKUP("Net Profit", Data!A:M, 3, FALSE), "")
Excel-specific data issues matching the table above:
- Missing/stale financials → Screener.in updates within days of filings — usually fresher than yfinance. Just re-paste the Data sheet.
- Currency confusion → Screener.in is always in Rs. Cr — one less unit trap than raw yfinance (which returns absolute rupees).
- Hand-paste errors (the Excel-specific risk!) → after each paste, spot-check two known numbers against the website; better, use Screener.in's Export to Excel button rather than copy-paste.
- Drag mistakes → the Excel version of a code bug. Always press Ctrl + ` (show formulas) and scan the row before trusting dragged results.
3.9 Beyond yfinance: Alternative Data Sources for Indian Companies
yfinance is excellent for prototyping and learning, but production-grade valuation work often requires more reliable and detailed data sources. Here are alternatives:
| Source | Access Method | Strengths | Limitations |
|---|---|---|---|
| Screener.in | Web scraping (be respectful) or manual Excel export | Clean, standardized Indian financials going back 10+ years; pre-computed ratios | No official API; terms restrict automated access |
| BSE/NSE Websites | Direct download of XBRL/PDF filings | Authoritative source; complete filing history | Manual; format varies across companies and years |
| NSE Python Library (nsetools) | pip install nsetools |
Real-time quotes, index data, top gainers/losers | Limited to market data, no financial statements |
| CMIE Prowess | Paid subscription with API access | Gold standard for Indian financial data; standardized across 50,000+ companies; 25+ years of history | Expensive; typically accessed through university subscriptions |
| Capitaline | Paid subscription, Excel export | Comprehensive Indian company database with standardized formats | Paid; no programmatic API in basic tier |
Which data source for each task? Match and justify with a partner.
- Prototype a model quickly, free, no account → ___ (yfinance)
- Production-grade standardized data for 50,000+ Indian firms → ___ (CMIE Prowess)
- Authoritative, complete filing history → ___ (BSE/NSE filings)
- Clean, pre-computed ratios going back 10+ years → ___ (Screener.in)
Check your answers
yfinance → prototype; CMIE Prowess → production; BSE/NSE → authoritative filings; Screener.in → quick ratios. In practice, you will use different sources at different stages.
Hands-On Project: Build Your Value Driver Dashboard
In this project, you will build a Python script that fetches financial data for any Indian company and produces a comprehensive value driver summary — growth rates, margins, ROIC, efficiency, and leverage — along with visualizations. This script will be the foundation you reuse throughout the course.
Excel Option Not comfortable with Python yet? Complete the identical project in Excel: export the 4 companies from Screener.in into the workbook structure from the Excel Path boxes (Data → Drivers → Summary → Charts sheets), compute every driver with the formulas from Section 3.4's Excel Path, and build the 3 charts per the Section 3.7 Excel Path. Your deliverable is the workbook instead of the script — the analysis and insights required are exactly the same. (Encouraged: try the Python version for ONE company and Excel for the rest — you will appreciate both tools' strengths.)
Steps
- Set up your environment: Install yfinance, pandas, numpy, and matplotlib. (Excel track: create the 4-sheet workbook per the Section 3.5 Excel Path.)
- Choose 4 Indian companies from different sectors — for example, TCS (IT), Asian Paints (Consumer), Reliance (Conglomerate), and HDFC Bank (Banking). Note: banks report financials differently; HDFC Bank will test your error handling.
- Write the
fetch_and_analyze()function from Section 3.5, adding error handling for missing line items. (Excel track: build the Drivers sheet formulas withIFERRORwrappers per Section 3.8's Excel Path.) - Compute value drivers for all 4 companies and compile a comparison table for the latest fiscal year.
- Create visualizations:
- A line chart showing revenue and net income growth trends for each company
- A grouped bar chart comparing operating margins across all 4 companies
- A scatter plot of Revenue Growth vs ROIC (label each point with the company name)
- Interpret the scatter plot: Companies in the top-right quadrant (high growth + high ROIC) are value creators. Companies in the bottom-right (high growth + low ROIC) are potential value destroyers. Which quadrant do your 4 companies fall into?
- Write a 200-word summary of which company appears to be the strongest value creator based on the data and why.
- Group Sharing (NEW): In groups of 4, each person presents their scatter plot and one key insight in 2 minutes. The group votes on the most surprising finding. One group will be asked to share their winning insight with the whole class.
View Solution / Walkthrough
Complete Solution Script
import yfinance as yf
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import time
import warnings
warnings.filterwarnings('ignore')
sns.set_style("darkgrid")
plt.rcParams['figure.figsize'] = (14, 5)
# ============================================================
# PART 1: Core fetch-and-analyze function (with error handling)
# ============================================================
def safe_loc(df, candidates):
"""Try multiple possible field names and return the first match."""
if isinstance(candidates, str):
candidates = [candidates]
for c in candidates:
if c in df.index:
return df.loc[c]
return None
def fetch_and_analyze_v2(ticker_symbol):
"""Robust version with fallback field names for Indian stocks."""
ticker = yf.Ticker(ticker_symbol)
info = ticker.info
is_df = ticker.financials
bs_df = ticker.balance_sheet
cf_df = ticker.cashflow
mkt_cap = info.get('marketCap', np.nan)
results = {}
# Revenue
rev = safe_loc(is_df, ['Total Revenue', 'Revenue', 'Operating Revenue'])
if rev is not None:
results['Revenue (Cr)'] = rev / 1e7
results['Revenue Growth (%)'] = rev.pct_change(periods=-1) * 100
# Net Income
ni = safe_loc(is_df, ['Net Income', 'Net Income Common Stockholders'])
if ni is not None:
results['Net Income Growth (%)'] = ni.pct_change(periods=-1) * 100
# EBIT
ebit = safe_loc(is_df, ['Operating Income', 'EBIT', 'Operating Revenue'])
if ebit is not None and rev is not None:
results['Operating Margin (%)'] = (ebit / rev) * 100
# Gross Profit
gp = safe_loc(is_df, ['Gross Profit'])
if gp is not None and rev is not None:
results['Gross Margin (%)'] = (gp / rev) * 100
# Net Margin
if ni is not None and rev is not None:
results['Net Margin (%)'] = (ni / rev) * 100
# ROIC
equity = safe_loc(bs_df, ['Total Equity Gross Minority Interest',
'Stockholders Equity',
'Total Equity'])
debt = safe_loc(bs_df, ['Total Debt', 'Long Term Debt'])
cash = safe_loc(bs_df, ['Cash And Cash Equivalents',
'Cash Cash Equivalents And Short Term Investments'])
if ebit is not None and equity is not None and debt is not None and cash is not None:
nopat = ebit * 0.75
ic = equity + debt - cash
results['ROIC (%)'] = (nopat / ic) * 100
# Efficiency
assets = safe_loc(bs_df, ['Total Assets'])
if rev is not None and assets is not None:
results['Asset Turnover'] = rev / assets
# Leverage
if debt is not None and equity is not None:
results['Debt-to-Equity'] = debt / equity
# FCF Yield
ocf = safe_loc(cf_df, ['Operating Cash Flow'])
capex = safe_loc(cf_df, ['Capital Expenditure',
'Capital Expenditure Reported',
'Purchase Of Property Plant And Equipment'])
if ocf is not None and capex is not None and not np.isnan(mkt_cap):
fcf = ocf + capex # capex is negative
results['FCF Yield (%)'] = (fcf / mkt_cap) * 100
drivers_df = pd.DataFrame(results).T.round(2)
drivers_df = drivers_df.sort_index(axis=1)
return {'drivers': drivers_df, 'info': info,
'income_stmt': is_df, 'balance_sheet': bs_df, 'cash_flow': cf_df}
# ============================================================
# PART 2: Multi-company comparison
# ============================================================
companies = {
'TCS.NS': 'Tata Consultancy Services',
'ASIANPAINT.NS': 'Asian Paints',
'RELIANCE.NS': 'Reliance Industries',
'HDFCBANK.NS': 'HDFC Bank'
}
all_results = {}
for ticker, name in companies.items():
try:
all_results[name] = fetch_and_analyze_v2(ticker)
print(f"✓ {name}")
except Exception as e:
print(f"✗ {name}: {e}")
time.sleep(1.5) # Rate limiting
# Build comparison table (latest year)
latest_data = {}
for name, result in all_results.items():
latest_col = result['drivers'].columns[0]
latest_data[name] = result['drivers'][latest_col]
comparison = pd.DataFrame(latest_data)
print("\n=== Cross-Sector Value Driver Comparison ===")
print(comparison.round(2))
# ============================================================
# PART 3: Visualizations
# ============================================================
fig, axes = plt.subplots(2, 2, figsize=(16, 12))
# Chart 1: Operating Margin comparison (bar)
ax = axes[0, 0]
margins = comparison.loc['Operating Margin (%)'] if 'Operating Margin (%)' in comparison.index else []
if len(margins) > 0:
colors = ['#6c8cff' if v > 15 else '#e0556a' for v in margins]
ax.bar(margins.index, margins.values, color=colors)
ax.set_title('Operating Margin Comparison')
ax.set_ylabel('Operating Margin (%)')
ax.axhline(y=15, color='green', linestyle='--', alpha=0.5, label='15% benchmark')
ax.legend()
ax.tick_params(axis='x', rotation=30)
# Chart 2: Revenue Growth vs ROIC (scatter)
ax = axes[0, 1]
if 'Revenue Growth (%)' in comparison.index and 'ROIC (%)' in comparison.index:
x = comparison.loc['Revenue Growth (%)']
y = comparison.loc['ROIC (%)']
ax.scatter(x, y, s=200, c='#6c8cff', edgecolors='white', linewidth=2)
for i, name in enumerate(comparison.columns):
ax.annotate(name, (x.iloc[i], y.iloc[i]),
textcoords="offset points", xytext=(8, 5), fontsize=9)
ax.axhline(y=15, color='green', linestyle='--', alpha=0.4)
ax.axvline(x=0, color='red', linestyle='--', alpha=0.4)
ax.set_xlabel('Revenue Growth (%)')
ax.set_ylabel('ROIC (%)')
ax.set_title('Growth vs ROIC: Who Creates Value?')
# Quadrant labels
ax.text(0.98, 0.98, 'Value Creators', transform=ax.transAxes,
ha='right', va='top', fontsize=10, color='green')
ax.text(0.02, 0.02, 'Value Destroyers', transform=ax.transAxes,
ha='left', va='bottom', fontsize=10, color='red')
# Chart 3: Revenue trend lines
ax = axes[1, 0]
for name, result in all_results.items():
rev = result['drivers'].loc['Revenue (Cr)'] if 'Revenue (Cr)' in result['drivers'].index else None
if rev is not None:
years = [str(d).split('-')[0] for d in rev.index]
ax.plot(years, rev.values, marker='o', label=name, linewidth=2)
ax.set_title('Revenue Trend (Rs. Crore)')
ax.legend(fontsize=8)
ax.tick_params(axis='x', rotation=45)
# Chart 4: ROIC trend lines
ax = axes[1, 1]
for name, result in all_results.items():
roic_data = result['drivers'].loc['ROIC (%)'] if 'ROIC (%)' in result['drivers'].index else None
if roic_data is not None:
years = [str(d).split('-')[0] for d in roic_data.index]
ax.plot(years, roic_data.values, marker='s', label=name, linewidth=2)
ax.set_title('ROIC Trend (%)')
ax.axhline(y=15, color='green', linestyle='--', alpha=0.5)
ax.legend(fontsize=8)
ax.tick_params(axis='x', rotation=45)
plt.suptitle('Corporate Valuation — Value Driver Analysis', fontsize=16, fontweight='bold', y=1.01)
plt.tight_layout()
plt.show()
# ============================================================
# PART 4: Interpretation
# ============================================================
print("""
=== INTERPRETATION GUIDE ===
Growth vs ROIC Quadrants:
- Top-Right (High Growth + High ROIC): VALUE CREATORS
These companies grow while earning returns above their cost of capital.
Example: Asian Paints typically shows 15-20% revenue growth with 30%+ ROIC.
- Top-Left (Low/Negative Growth + High ROIC): CASH COWS
Mature businesses generating strong returns but with limited growth.
They should return cash to shareholders via dividends/buybacks.
- Bottom-Right (High Growth + Low ROIC): VALUE DESTROYERS
Fast-growing but earning below cost of capital. Growth destroys value.
Common in startups and capital-intensive industries in early phases.
- Bottom-Left (Low Growth + Low ROIC): TURNAROUND CANDIDATES
May require restructuring, divestitures, or management change.
""")
Key Insights from a Typical Run:
- Asian Paints usually appears in the top-right: 15–20% revenue growth with 30%+ ROIC. It is a textbook value creator with a wide moat.
- TCS shows moderate growth (8–12%) with very high ROIC (40%+). It is a cash cow that also grows — a rare and valuable combination.
- Reliance is more volatile — growth depends on the investment cycle (Jio, Retail, O2C). ROIC varies significantly by segment, making sum-of-parts essential.
- HDFC Bank may error on ROIC because banking financials are structured differently. Banks use ROE and ROA rather than ROIC. We will handle financial sector valuation in later chapters.
Key Takeaways
yfinance + pandas gives you programmatic access to financial statements for any Indian stock using the .NS suffix. This eliminates manual data entry and enables analysis at scale.
Automated value driver computation — growth rates, margins, ROIC, efficiency, and leverage — transforms raw financials into actionable valuation inputs in seconds, not hours.
Error handling is essential. yfinance field names vary across companies and some fields are missing entirely. Always use fallback logic and validate your data before analysis.
The Growth vs ROIC scatter plot is the single most powerful diagnostic in valuation. Companies in the top-right quadrant create value; those in the bottom-right destroy it.
yfinance is for prototyping. For production valuation work, supplement with Screener.in, CMIE Prowess, or direct company filings. The code structure remains the same — only the data source changes.
Test Your Understanding
1. What is the correct yfinance ticker suffix for an NSE-listed Indian stock?
2. In the compute_roic() function, why is cash subtracted from the invested capital calculation?
3. A company has a revenue growth rate of 18% but an ROIC of 6% when its WACC is 10%. Which quadrant of the Growth vs ROIC scatter plot does it belong in, and what does this mean?
4. Why is it important to use time.sleep() when fetching data for multiple companies in a loop?
5. When you inspect ticker.financials and find that a line item you need is missing, what is the best approach?
Which part of the Python workflow felt hardest — fetching data, computing ratios, or visualizing? Write it down. In the next sessions we will build on this pipeline for ratio analysis (Session 4) and ROIC (Session 5).