Indian Bank Transaction Categorizer (SetFit)
A SetFit text classifier that categorizes Indian bank/credit-card transaction narrations (UPI, NEFT, IMPS, ACH, POS) into everyday personal-finance spending categories.
It was built for a personal local-first finance tracker, after an explicit search turned up no public dataset that pairs real Indian narration formats with category labels — datasets either have real-looking Indian bank text with no labels (built for OCR/document-AI), or have category labels attached to generic Western merchant names with an "India" tag that doesn't reflect the actual UPI/NEFT/IMPS text shape. This model closes that specific gap using entirely synthetic, privacy-safe training data — see the companion training dataset, which also includes the generator script.
What's new in this version (v3)
Three categories were added, driven by real gaps found running this model in production:
Salary— split out ofIncome & Depositsso recurring paycheck income can be tracked separately from one-off credits (refunds, interest, cashback).Internal Transfer— money moved between two accounts the same person/household owns (e.g. "SELF FUND TRANSFER", "MOB/SELFFT/..."). Previously these either inflated spend/investment totals or scattered across unrelated categories.Loan Payment— loan EMI repayments. The training data specifically teaches the model to tell a loan EMI (numeric-reference-only narrations likeECS EMI 100826or<ref>_EMI_<date>) apart from a retail purchase paid via no-cost EMI (e.g.EMI Uniqlo), which stays categorized as the thing actually bought.
⚠️ Breaking change: model.predict(...) can now return 3 new label strings it never returned
before. If your code maps predictions through a fixed switch/dict keyed on the old 18 categories,
update it to handle (or explicitly ignore) Salary, Internal Transfer, and Loan Payment before
upgrading.
What it does
Classifies a transaction description into one of 21 categories:
Groceries · Eating Out · Kids Activities · Shopping & Clothing · Medicine & Pharmacy ·
Hospital & Medical · Utilities · Rent & Mortgage · Loan Payment · Transportation & Gas ·
Entertainment & Subscriptions · Travel · Insurance · Education · Personal Care ·
Investments & Savings Transfer · ATM & Cash · Fees & Interest · Salary ·
Income & Deposits · Internal Transfer
It handles two input styles, both seen during training:
- Bank-narration shapes, e.g.
UPI/Swiggy Instamart/swiggy123@ybl/Payment/HDFC,NEFT Dr-HDFC0000953-SURESH VERMA-Blinkit - Plain-language descriptions, e.g.
"paid electricity bill BESCOM online","SIP auto debit HDFC Flexicap fund"
Training data
970 rows, entirely synthetic, generated from:
- Real public brand names (Swiggy, Zomato, BigBasket, PharmEasy, Netflix, Zerodha, IRCTC, etc.) and real company names used only as employer examples (Infosys, TCS, Akamai, etc.) — these are just facts about which businesses/employers exist, not personal data.
- Randomly generated reference numbers, UPI VPAs, and person names (drawn from a generic name pool) — nothing here corresponds to any real person or real transaction.
- Templates mirror narration shapes reverse-engineered while building statement parsers for ICICI, HDFC, Axis, and Scapia (India), and while building a real-time bank-alert-email parser — not copied from any real statement or email content.
Nothing in this model was trained on real user transaction data. The generator script is included in the
[Budgeter repo] (synthetic_indian_generator.py) if you want to inspect or extend it.
Evaluation
| Test set | Accuracy | High-confidence wrong (≥0.6, would slip past a review gate) |
|---|---|---|
| Held-out synthetic (unseen seed, 370 rows) | 94.6% | 0 |
| Hand-crafted natural-language set, not from the generator's templates (30 rows) | 90.0% | 1 |
The held-out misses cluster almost entirely around one genuinely ambiguous boundary — "Interest Credit" /
"Dividend Credit" / "Cashback Credit" vs. Investments & Savings Transfer — and land at low confidence, so
in the source app these get flagged for human review rather than silently misfiled. Zero held-out misses are
confidently wrong, which matters more in practice than the raw accuracy number.
Honest limitation: this is trained entirely on synthetic data. It has not been evaluated against real bank statements (for privacy reasons, it never will be by the original author) — treat the numbers above as a reasonable proxy, not a guarantee, and validate against your own data before relying on it.
Usage
from setfit import SetFitModel
model = SetFitModel.from_pretrained("Sumeetgpt/indian-transaction-categorizer")
model.predict(["UPI/Swiggy Instamart/swiggy@ybl/Payment/HDFC"])
# ['Groceries']
probs = model.predict_proba(["UPI/Swiggy Instamart/swiggy@ybl/Payment/HDFC"])[0]
# use max(probs) as a confidence score; gate low-confidence predictions for human review
Model Details
- Model Type: SetFit
- Sentence Transformer body: sentence-transformers/all-MiniLM-L6-v2
- Classification head: LogisticRegression
- Training: contrastive fine-tuning, 2 epochs, 20 iterations, batch size 16
- License: MIT