← Back to portfolio
Data workbench · SQLite

The project numbers, queried in SQL

All three datasets loaded into one SQLite database, then queried with joins, CTEs and window functions. Every result below was produced by the query above it. To run it yourself: python sql/run.py, then open portfolio.db in any SQLite tool.

Files: schema.sql (tables) · queries.sql (these six queries) · run.py (builds the database from the CSVs and runs them).

Q1. Spend per card each month, and its month-on-month change

SELECT month,
       ROUND(spend_inr_lakh_cr * 1e12 / (cards_outstanding_mn * 1e6)) AS spend_per_card_inr,
       ROUND(100.0 * (spend_inr_lakh_cr / cards_outstanding_mn)
             / LAG(spend_inr_lakh_cr / cards_outstanding_mn) OVER (ORDER BY month) - 100, 1) AS mom_change_pct
FROM card_monthly
ORDER BY month;
monthspend_per_card_inrmom_change_pct
2026-0117,145—
2026-0215,038-12.3
2026-0318,46122.8
2026-0416,494-10.7
2026-0516,7631.6
2026-0616,530-1.4
2026-0716,9302.4
2026-0816,284-3.8
2026-0916,281-0

Q2. Net cards added each month (millions)

SELECT month,
       cards_outstanding_mn,
       ROUND(cards_outstanding_mn - LAG(cards_outstanding_mn) OVER (ORDER BY month), 2) AS net_added_mn
FROM card_monthly
ORDER BY month;
monthcards_outstanding_mnnet_added_mn
2026-01116.65—
2026-02117.71.05
2026-03118.630.93
2026-04119.440.81
2026-05120.51.06
2026-06121.61.1
2026-07122.861.26
2026-08124.051.19
2026-09125.31.25

Q3. Months where the card base grew but spend per card fell

WITH m AS (
  SELECT month, cards_outstanding_mn,
         spend_inr_lakh_cr / cards_outstanding_mn AS ppc,
         LAG(cards_outstanding_mn) OVER (ORDER BY month) AS prev_cards,
         LAG(spend_inr_lakh_cr / cards_outstanding_mn) OVER (ORDER BY month) AS prev_ppc
  FROM card_monthly
)
SELECT month,
       ROUND(cards_outstanding_mn - prev_cards, 2) AS cards_added_mn,
       ROUND(100.0 * ppc / prev_ppc - 100, 1)      AS spend_per_card_change_pct
FROM m
WHERE cards_outstanding_mn > prev_cards AND ppc < prev_ppc
ORDER BY month;
monthcards_added_mnspend_per_card_change_pct
2026-021.05-12.3
2026-040.81-10.7
2026-061.1-1.4
2026-081.19-3.8
2026-091.25-0

Q4. Issuer ranking by spend per card, April 2026

SELECT issuer,
       ROUND(spend_inr_cr * 1e7 / (cards_outstanding_mn * 1e6)) AS spend_per_card_inr,
       spend_yoy_pct,
       RANK() OVER (ORDER BY spend_inr_cr / cards_outstanding_mn DESC) AS rank_by_spend_per_card
FROM issuer_apr2026
ORDER BY rank_by_spend_per_card;
issuerspend_per_card_inrspend_yoy_pctrank_by_spend_per_card
HDFC Bank21,97712.341
SBI Card17,06028.982
ICICI Bank16,927-7.353

Q5. UPI average ticket and UPI value versus all card spend

SELECT u.month,
       ROUND(u.upi_value_inr_lakh_cr * 1e12 / (u.upi_volume_bn * 1e9)) AS upi_avg_ticket_inr,
       ROUND(u.upi_value_inr_lakh_cr / c.spend_inr_lakh_cr, 1)          AS upi_to_card_value_x
FROM upi_monthly u
LEFT JOIN card_monthly c ON c.month = u.month
ORDER BY u.month;
monthupi_avg_ticket_inrupi_to_card_value_x
2026-011,30614.2
2026-021,31615.2
2026-031,30413.5
2026-041,29914.7
2026-051,28914.8
2026-061,27314.4
2026-071,26314.4
2026-081,21714.8
2026-091,22014.4
2026-101,238—

Q6. Named complaint categories ranked, with running share (the residual 'All other grounds' is excluded)

SELECT category,
       share_pct,
       RANK() OVER (ORDER BY share_pct DESC) AS rnk,
       ROUND(SUM(share_pct) OVER (ORDER BY share_pct DESC ROWS UNBOUNDED PRECEDING), 2) AS running_share_pct
FROM ombudsman_fy25
WHERE category <> 'All other grounds'
ORDER BY rnk;
categoryshare_pctrnkrunning_share_pct
Loans and advances29.25129.25
Credit cards17.15246.4
Mobile / electronic banking16.86363.26
Deposit accounts16.84480.1