Session 3 of 26 Part I: Valuation Foundations — Chapter 3 of 18
Part I: Valuation Foundations Chapter 3 of 18 Session 3 Python Excel

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

📋
Two Paths, One Destination — Choose Your Tool

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.

Python: yfinance + pandas Excel: Screener.in export + formulas

🔍 Opening Challenge — The 100-Company Data Race

Your team must compute revenue growth, margins, ROIC, and leverage for 100 Indian companies — by the end of the day. Method A: open 100 annual reports and type numbers into Excel (about 40 hours). Method B: one Python script that does it in 10 minutes.
Your Task: In 30 seconds, list what data you would need to pull for each company to compute its value drivers. Then turn to a partner — how many items did you both list? You have 2 minutes.
📋 Stable content — Reviewed: June 2026

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:

LibraryPurposeInstall 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
📝
Note on Jupyter vs Script: For exploratory analysis, Jupyter Notebook (or JupyterLab) is ideal — you can see results inline after each cell. For production pipelines that you will reuse across companies, write .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.
Your Turn — Verify Your Setup ⏱ 3 min

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.

X Excel Path — Your Setup Instead Excel

No installation needed. You need only three things:

  1. Excel (or Google Sheets — everything here works in both; Google Sheets is free).
  2. 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.
  3. A new blank workbook named ValueDriver_Analysis.xlsx with 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:

ExchangeSuffixExample
National Stock Exchange (NSE).NSRELIANCE.NS, TCS.NS, INFY.NS
Bombay Stock Exchange (BSE).BORELIANCE.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
Important: The 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.
Your Turn — Predict Before You Run ⏱ 2 min

Before running the code, predict the output:

  1. For yf.Ticker("TCS.NS").info.get('longName'), what will Python print?
  2. For yf.Ticker("RELIANCE.NS").info.get('sector'), what sector do you expect?
  3. 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.

⚡ Quick Think (30 seconds): Why is the .NS suffix preferred over .BO for most Indian stocks? Hint: think about data completeness and liquidity — check the paragraph above if unsure.
X Excel Path — Get the Same Data from Screener.in Excel

Instead of yf.Ticker("RELIANCE.NS"), Excel users get identical data (actually deeper history — 10+ years) through Screener.in:

  1. Go to screener.in and search the company (e.g., "Asian Paints").
  2. On the company page you will see the Profit & Loss, Balance Sheet, and Cash Flow tables — the same three statements yfinance returns.
  3. 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).
  4. 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.

💡
Pro Tip: In Google Sheets, you can pull live prices with =GOOGLEFINANCE("NSE:ASIANPAINT","price") — no account needed for market data. Combine with Screener.in fundamentals and you have a complete dataset without any code.

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.

💡
Pro Tip: Use 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.
Your Turn — Explore the DataFrame ⏱ 5 min

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 — the DataFrame is transposed relative to how statements appear in annual reports.

True or False: The most recent fiscal year is the rightmost column of ticker.financials.

Reveal
FALSE — the most recent year is the LEFTMOST column. This is why pct_change(periods=-1) is needed for growth rates.

True or False: Every company reports exactly the same line items in ticker.financials.

Reveal
FALSE — field names vary by company and some fields are missing. This is why safe_get() fallback logic matters.
X Excel Path — Lay Out the Statements the Same Way Excel

In Python, yfinance gives you a transposed DataFrame (years as columns). Replicate this exact layout in Excel so both tracks stay comparable:

ABCD
1Line ItemFY2021FY2022FY2023→ older→newer
2Sales21,15925,39428,375
3Operating Profit3,7724,2544,313
4Net Profit2,5333,0583,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)
Code Challenge — Write It Yourself ⏱ 4 min

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.

X Excel Path — The Same Value Drivers in Formulas Excel

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:

RowContains
2Sales
3Operating Profit (OP)
4Net Profit
5Total Debt (Borrowings)
6Cash & Investments
7Net Worth / Equity
8Total Assets

1. Revenue growth (the Excel version of compute_revenue_growth):

' In C9 (growth for FY2022, the year in column C):
= (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):

' Operating Margin % (row 10):
= C3 / C2 * 100
' Net Margin % (row 11):
= C4 / C2 * 100

3. ROIC (compute_roic) — the row layout trick: First add helper rows:

' NOPAT (row 12) = OP × (1 − 25% tax):
= 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:

' Debt-to-Equity (row 15):
= 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)
Important: This is a preliminary ROIC calculation. A rigorous ROIC requires adjustments for operating leases, non-operating assets, and the exact tax rate from the financials. We will refine this in Chapter 5. The yfinance line-item names also vary slightly across companies — you may need to inspect 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.

X Excel Path — Your "Pipeline" Is a Well-Structured Workbook Excel

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:

  1. Sheet 1: "Data" — raw pasted numbers from Screener.in. Never touch this sheet after pasting; it is your "yfinance".
  2. 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 drivers DataFrame.
  3. Sheet 3: "Summary" — the latest-year snapshot: one column per company, one row per driver (built in the next Excel Path).
  4. 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")
Your Turn — Run the Pipeline ⏱ 4 min

Run fetch_and_analyze("TCS.NS") and answer:

  1. What is TCS's latest ROIC? (should be very high for a debt-free IT company)
  2. What is its Debt-to-Equity? (should be near zero)
  3. 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?

🔍 Debug Challenge — Find the Bug ⏱ 3 min

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)
🌎
Real World: When you run this comparison on Indian IT companies, you will typically find that TCS and Infosys lead on operating margins (24–26%), while mid-tier firms show lower but sometimes faster-growing margins. The ROIC comparison is particularly revealing — companies with ROIC consistently above 30% in the IT sector (TCS, Infosys) command premium P/E multiples because the market recognizes their sustainable competitive advantage. We will quantify this relationship in Chapters 15 and 16.
X Excel Path — Build the Comparison with Cross-Sheet References Excel

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:

  1. On the Summary sheet, put company names in row 1: B1: TCS, C1: INFY, D1: WIPRO
  2. Put driver labels in column A: Revenue Growth %, Op Margin %, ROIC %, D/E, Asset Turnover.
  3. 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:
' In Summary!B2 (TCS revenue growth):
=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.

Your Turn — Extend the Comparison ⏱ 5 min

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-Pair-Share — Why Similar Margins, Different ROIC? ⏱ 3 min

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)
Your Turn — Make It Yours ⏱ 4 min

Customize the plot_value_drivers_trend function:

  1. Change the sns.set_style("darkgrid") to "whitegrid" — what changes visually?
  2. Change the ROIC bar colors: currently green if >15%, red otherwise. Change the threshold to 10% (WACC proxy).
  3. 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').

X Excel Path — The Same Three Charts, Built by Selection Excel

Excel's chart engine replaces matplotlib. Because your data is already laid out in rows-and-years, each chart is select → insert → format:

  1. Growth trend line (the Python Chart 1): On a company's Drivers sheet, select the Revenue Growth row (including year labels) → Insert → Line Chart.
  2. 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.
  3. 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:

IssueCauseSolution
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.
🔍 Debug Challenge — Fix the KeyError ⏱ 4 min

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:

  1. Identify when the KeyError happens.
  2. 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!

X Excel Path — Your Errors Are #DIV/0! and #N/A (Here's the Safe-Get Equivalent) Excel

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 = try/except — replace any error with a blank:
=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.
📋 Stable content — Reviewed: June 2026

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:

SourceAccess MethodStrengthsLimitations
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
🎨 Match the Source ⏱ 2 min

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

  1. Set up your environment: Install yfinance, pandas, numpy, and matplotlib. (Excel track: create the 4-sheet workbook per the Section 3.5 Excel Path.)
  2. 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.
  3. Write the fetch_and_analyze() function from Section 3.5, adding error handling for missing line items. (Excel track: build the Drivers sheet formulas with IFERROR wrappers per Section 3.8's Excel Path.)
  4. Compute value drivers for all 4 companies and compile a comparison table for the latest fiscal year.
  5. 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)
  6. 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?
  7. Write a 200-word summary of which company appears to be the strongest value creator based on the data and why.
  8. 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

1

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.

2

Automated value driver computation — growth rates, margins, ROIC, efficiency, and leverage — transforms raw financials into actionable valuation inputs in seconds, not hours.

3

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.

4

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.

5

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?

📝 Quick Reflection ⏱ 1 min

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).