pandasintermediate45 minPandas
Financial Reconciliation and Ledger Matching
Perform automated two-way financial ledger reconciliation in Pandas using outer joins, merge indicators, and discrepancy reporting.
The incident
- Company
- FinSure Lending
- Engineer
- Harish Nair, Reconciliation Analytics Specialist
- Trigger
- Month-end settlement between internal payment gateway ledger and external bank clearing statements showed an unexplained 14 lakh rupee discrepancy.
- Stakes
- Month-end audit failure and banking non-compliance penalties of 20 lakh rupees if unmatched transaction discrepancies remained unresolved.
On a Thursday morning, Harish Nair arrived at FinSure Lending, a digital consumer credit institution processing thousands of daily loan repayments. During the monthly financial closing cycle, the accounting department discovered an alarming 14 lakh rupee reconciliation imbalance between internal payment gateway transaction logs and the clearing bank official monthly settlement statement. With external financial auditors scheduled to arrive on Monday, leadership faced potential 20 lakh rupee regulatory non-compliance fines. Regulatory auditors required audited proof that every loan disbursement and borrower repayment matched corresponding banking clearing entries exactly.
The accounting operations team was overwhelmed: they had attempted manual VLOOKUP matching in Excel across hundreds of thousands of rows. When cell lookups failed due to missing reference numbers or slight payment gateway fee differences, accountants spent hours manually hunting down individual discrepancies, causing severe delays in monthly ledger sign-off. Spreadsheet lookup formulas crashed under high transaction volumes, creating blind spots that concealed missing deposits and unrecorded banking fees.
Harish took charge of developing an automated, reproducible financial reconciliation pipeline in Pandas. The internal_ledger recorded txn_id, amount, and status ('SETTLED', 'PENDING'), while bank_statement recorded reference and settled_amount. Crucially, the reconciliation model needed to identify three distinct exception categories: internal payments missing from the bank, bank deposits missing from internal records, and matched transactions with amount variances. Establishing a reproducible two-way reconciliation pipeline was essential to guarantee complete financial transparency across distributed banking interfaces.
Harish designed a full outer merge strategy in Pandas: pd.merge(internal, bank, left_on='txn_id', right_on='reference', how='outer', indicator=True). The indicator column automatically categorized every transaction into 'both' (matched), 'left_only' (internal only), or 'right_only' (bank only). For matched transactions, Harish calculated amount differences to detect unrecorded bank processing fees. The indicator flag enabled the analytics engine to isolate unbatched deposits and partial settlements without requiring complex nested filtering logic.
By afternoon, Harish executed the automated reconciliation script. The pipeline pinpointed the entire discrepancy: internal transaction TXN-103 (8,500 rupees) was still PENDING and had not reached the bank, transaction TXN-105 (31,000 rupees) was completely missing from the bank clearing batch, the bank had deposited an unlinked credit of 9,000 rupees (TXN-106), and TXN-102 suffered a 500-rupee gateway fee deduction. Isolating these distinct exception categories allowed the finance team to reconcile general ledger accounts with pinpoint mathematical accuracy.
Armed with Harish automated exception report, treasury operations recovered the missing 31,000 rupee batch and adjusted fee journal entries before auditors arrived. The CFO approved the monthly accounts on time, eliminating 20 lakh rupees in regulatory penalty risk and making Harish Pandas reconciliation model the company official month-end standard. Deploying automated Pandas reconciliation transformed a multi-day spreadsheet ordeal into a 10-second automated audit check, permanently protecting FinSure Lending from compliance penalties.
What you will learn
- Perform two-way ledger reconciliation using pd.merge() with how='outer' and indicator=True
- Isolate unmatched records across system boundaries using merge indicator filters ('left_only', 'right_only')
- Detect financial variance anomalies on matched transaction keys by computing column differences
- Generate executive reconciliation exception summaries detailing missing deposits and fee discrepancies
What you need first
Full Outer Merge with IndicatorPandas combining DataFrames
pd.merge(..., how='outer', indicator=True) retains all records from both datasets and adds a _merge column tagging row origins.
import pandas as pd
df1 = pd.DataFrame({'k': [1]})
df2 = pd.DataFrame({'k': [2]})
print(list(pd.merge(df1, df2, on='k', how='outer', indicator=True)['_merge']))['left_only', 'right_only']
Using inner merge for reconciliation, which discards unmatched transactions on both sides and conceals discrepancies.
Discrepancy and Variance LogicPandas filtering
Evaluating df['amount'] - df['settled_amount'] on matched rows flags transactions where processing fees or partial settlements occurred.
import pandas as pd
s1 = pd.Series([1000.0])
s2 = pd.Series([950.0])
print(float(s1 - s2))50.0
Comparing float amounts without rounding or tolerance thresholds, which can trigger false mismatches from binary floating-point precision.
Architecture and data flow
The data
Internal payment transaction ledger and external bank settlement statement.
internal_ledger: Internal gateway loan payment records
| txn_id | amount | status |
|---|---|---|
| TXN-101 | 15000 | SETTLED |
| TXN-102 | 22000 | SETTLED |
| TXN-103 | 8500 | PENDING |
| TXN-104 | 14000 | SETTLED |
| TXN-105 | 31000 | SETTLED |
bank_statement: External clearing bank statement deposits
| reference | settled_amount |
|---|---|
| TXN-101 | 15000 |
| TXN-102 | 21500 |
| TXN-104 | 14000 |
| TXN-106 | 9000 |
Step by step
Read each step, run its code, then check your output against the one shown.
Load internal ledger and bank statement DataFrames
We initialize the internal_ledger (5 transactions totaling 90,500.00 rupees) and external bank_statement (4 deposits totaling 59,500.00 rupees). Inspecting the raw data reveals clear differences: internal transaction TXN-105 is missing from the bank, bank deposit TXN-106 is missing from internal records, and TXN-102 has an amount discrepancy. In financial data engineering, verifying source dataset shapes establishes baseline reconciliation benchmarks before joining. Comparing source row counts and financial totals early ensures that subsequent merge operations account for every transactional dollar across complex payment ecosystems.
import pandas as pd internal = pd.DataFrame({ "txn_id": ["TXN-101", "TXN-102", "TXN-103", "TXN-104", "TXN-105"], "amount": [15000.0, 22000.0, 8500.0, 14000.0, 31000.0], "status": ["SETTLED", "SETTLED", "PENDING", "SETTLED", "SETTLED"] }) bank = pd.DataFrame({ "reference": ["TXN-101", "TXN-102", "TXN-104", "TXN-106"], "settled_amount": [15000.0, 21500.0, 14000.0, 9000.0] }) print("Internal records:", len(internal), f"Sum: {internal['amount'].sum():.2f}") print("Bank records:", len(bank), f"Sum: {bank['settled_amount'].sum():.2f}")Internal records: 5 Sum: 90500.00 Bank records: 4 Sum: 59500.00
Common mistakeFailing to check source row totals before merging, making it difficult to detect dropped records during join operations.
How you will knowYou will see 5 internal records totaling 90500.00 and 4 bank records totaling 59500.00.
Attempt naive inner merge join
Wrong turn first: this step shows the mistake before the fix
Here is the naive mistake: writing pd.merge(internal, bank, left_on='txn_id', right_on='reference', how='inner'). Running an inner join silently drops all unmatched records: TXN-103, TXN-105, and TXN-106 disappear completely from the output. In financial accounting, an inner join creates a false impression that all records match, completely concealing the 31,000 rupee missing bank deposit and failing audit review. Inner joins silently discard non-matching keys on both sides, masking reconciliation discrepancies and failing audit scrutiny and exposing institutions to severe regulatory penalties during annual external compliance audits.
import pandas as pd internal = pd.DataFrame({ "txn_id": ["TXN-101", "TXN-102", "TXN-103", "TXN-104", "TXN-105"], "amount": [15000.0, 22000.0, 8500.0, 14000.0, 31000.0], "status": ["SETTLED", "SETTLED", "PENDING", "SETTLED", "SETTLED"] }) bank = pd.DataFrame({ "reference": ["TXN-101", "TXN-102", "TXN-104", "TXN-106"], "settled_amount": [15000.0, 21500.0, 14000.0, 9000.0] }) inner_result = pd.merge(internal, bank, left_on="txn_id", right_on="reference", how="inner") print("Naive inner join row count:", len(inner_result)) print("Matched IDs:", list(inner_result["txn_id"]))Naive inner join row count: 3 Matched IDs: ['TXN-101', 'TXN-102', 'TXN-104']
Common mistakeUsing inner merge for financial reconciliation, which silently discards unmatched rows on both sides.
How you will knowObserve that the inner join returns only 3 rows, completely omitting unmatched transactions TXN-103, TXN-105, and TXN-106.
Execute full outer merge with indicator tracking
We resolve the missing record hazard by executing a full outer join with indicator=True: pd.merge(internal, bank, left_on='txn_id', right_on='reference', how='outer', indicator=True). The resulting _merge column explicitly labels the origin of each row: 'both' indicates matched transactions, 'left_only' identifies internal records missing from the bank, and 'right_only' identifies unlinked bank deposits. Outer joins with indicator tags provide transparent categorization of financial discrepancies across system boundaries. The indicator feature allows engineering teams to segment unmatched transactions into actionable operational queues for immediate accounting resolution.
import pandas as pd internal = pd.DataFrame({ "txn_id": ["TXN-101", "TXN-102", "TXN-103", "TXN-104", "TXN-105"], "amount": [15000.0, 22000.0, 8500.0, 14000.0, 31000.0], "status": ["SETTLED", "SETTLED", "PENDING", "SETTLED", "SETTLED"] }) bank = pd.DataFrame({ "reference": ["TXN-101", "TXN-102", "TXN-104", "TXN-106"], "settled_amount": [15000.0, 21500.0, 14000.0, 9000.0] }) merged = pd.merge(internal, bank, left_on="txn_id", right_on="reference", how="outer", indicator=True) print("Outer join row count:", len(merged)) print("Merge status breakdown:") for status, count in merged["_merge"].value_counts().items(): print(f" {status}: {count}")Outer join row count: 6 Merge status breakdown: both: 3 left_only: 2 right_only: 1
Common mistakeOmitting indicator=True in outer merges, which forces analysts to write manual null checks across multiple columns.
How you will knowThe output confirms 6 total rows: 3 matched in 'both', 2 in 'left_only', and 1 in 'right_only'.
Calculate amount variance on matched transaction records
For transactions present in both ledgers (_merge == 'both'), we evaluate financial variances by subtracting settled_amount from internal amount. On TXN-101 and TXN-104, the difference is exactly 0.00 rupees. On TXN-102, however, the bank credited 21,500.00 against an internal expectation of 22,000.00, isolating a 500.00 rupee discrepancy caused by payment gateway processing deductions. Calculating numerical variances on matched records highlights partial settlements and unrecorded processing fee deductions. Identifying these micro-discrepancies ensures that ledger balances agree to the exact penny before financial statements are signed.
import pandas as pd internal = pd.DataFrame({ "txn_id": ["TXN-101", "TXN-102", "TXN-103", "TXN-104", "TXN-105"], "amount": [15000.0, 22000.0, 8500.0, 14000.0, 31000.0], "status": ["SETTLED", "SETTLED", "PENDING", "SETTLED", "SETTLED"] }) bank = pd.DataFrame({ "reference": ["TXN-101", "TXN-102", "TXN-104", "TXN-106"], "settled_amount": [15000.0, 21500.0, 14000.0, 9000.0] }) merged = pd.merge(internal, bank, left_on="txn_id", right_on="reference", how="outer", indicator=True) matched = merged[merged["_merge"] == "both"].copy() matched["variance"] = matched["amount"] - matched["settled_amount"] for _, row in matched.iterrows(): print(f"Matched {row['txn_id']}: Expected={row['amount']:.2f}, Bank={row['settled_amount']:.2f}, Variance={row['variance']:.2f}")Matched TXN-101: Expected=15000.00, Bank=15000.00, Variance=0.00 Matched TXN-102: Expected=22000.00, Bank=21500.00, Variance=500.00 Matched TXN-104: Expected=14000.00, Bank=14000.00, Variance=0.00
Common mistakeFailing to check for partial payments or fee deductions on matched keys, assuming key presence guarantees dollar equality.
How you will knowOutput shows TXN-102 has a 500.00 rupee variance, while TXN-101 and TXN-104 have 0.00 variance.
Synthesize executive reconciliation exception report
In the final step, we assemble an executive exception summary categorizing all financial discrepancies. We flag missing bank deposits (TXN-105 for 31,000.00 rupees), unlinked bank credits (TXN-106 for 9,000.00 rupees), pending transactions (TXN-103 for 8,500.00 rupees), and fee deductions (TXN-102 for 500.00 rupees). This actionable report provides treasury teams with the exact items required for month-end audit sign-off. Classifying reconciliation exceptions by root cause equips accounting teams with the specific details needed to post adjusting journal entries and clear audits.
import pandas as pd internal = pd.DataFrame({ "txn_id": ["TXN-101", "TXN-102", "TXN-103", "TXN-104", "TXN-105"], "amount": [15000.0, 22000.0, 8500.0, 14000.0, 31000.0], "status": ["SETTLED", "SETTLED", "PENDING", "SETTLED", "SETTLED"] }) bank = pd.DataFrame({ "reference": ["TXN-101", "TXN-102", "TXN-104", "TXN-106"], "settled_amount": [15000.0, 21500.0, 14000.0, 9000.0] }) merged = pd.merge(internal, bank, left_on="txn_id", right_on="reference", how="outer", indicator=True) missing_in_bank = merged[merged["_merge"] == "left_only"] bank_only = merged[merged["_merge"] == "right_only"] matched = merged[merged["_merge"] == "both"].copy() matched["variance"] = matched["amount"] - matched["settled_amount"] fee_discrepancies = matched[matched["variance"] != 0] print(f"Missing in Bank: {list(missing_in_bank['txn_id'])} (Total: {missing_in_bank['amount'].sum():.2f})") print(f"Bank Only Credits: {list(bank_only['reference'])} (Total: {bank_only['settled_amount'].sum():.2f})") print(f"Fee Discrepancies: {list(fee_discrepancies['txn_id'])} (Variance: {fee_discrepancies['variance'].sum():.2f})")Missing in Bank: ['TXN-103', 'TXN-105'] (Total: 39500.00) Bank Only Credits: ['TXN-106'] (Total: 9000.00) Fee Discrepancies: ['TXN-102'] (Variance: 500.00)
Common mistakeFailing to segment exceptions by financial cause, handing accountants an undifferentiated list of discrepancies.
How you will knowThe output categorizes missing bank transactions (TXN-103, TXN-105), bank-only credits (TXN-106), and fee discrepancies (TXN-102).
Build it yourself
Write your solution below, run it in your browser, and pass every check.
What the result means
Interpretation
The reconciliation pipeline pinpoints the exact drivers of the financial closing imbalance. Out of 90,500.00 rupees in internal records, 39,500.00 rupees did not appear in the bank statement: TXN-103 (8,500.00) was still pending gateway settlement, while TXN-105 (31,000.00) represented an unbatched transmission. Furthermore, a 500.00 rupee fee deduction was isolated on TXN-102. Identifying these specific exceptions enabled treasury to resolve all items prior to audit review.
The message you would send
Hi Finance and Accounting Team, the automated Pandas reconciliation pipeline has identified all discrepancies between the internal ledger and bank statements. The 31,000 rupee missing deposit was traced to an unbatched settlement file, and the 500 rupee fee variance was documented for journal entry. All accounts are now reconciled for audit sign-off.
Put it on your CV
Resume or LinkedIn bullet
Built an automated financial reconciliation pipeline in Pandas utilizing full outer joins, merge indicators, and fee variance auditing.README for GitHub
# Financial Reconciliation and Ledger Matching
An enterprise financial data engineering pipeline in Pandas reconciling internal payment ledgers against bank clearing statements.
## Features
- Two-way ledger reconciliation via pd.merge(how='outer', indicator=True)
- Automated isolation of unmatched transactions across system boundaries
- Variance calculations on matched keys to detect hidden gateway fee deductions
- Executive audit exception reporting satisfying regulatory compliance standardsImplement fuzzy string matching using difflib to match bank references that contain truncated transaction IDs.