Bank statements from different banks don't share a layout. Some use separate Debit and Credit columns, others a single signed Amount, others "Paid in / Paid out". Without a script, each file has to be adjusted manually before the analysis starts. This script handles the normalisation and produces one flat table ready for the next step.
This script comes from real work: when I review a company's cash flows, I usually receive statements from several banks, each with its own export format. The sample files in this repository use synthetic data; real counterparties and amounts were replaced for confidentiality.
- Overview
- Who this is for
- Quick start
- Key features
- How it works
- Design decisions
- Project structure
- Getting started
- Input data requirements
- Supported formats
- Demo: input → output
- License
- Contact
- Input:
.xlsxstatement files from multiple banks, placed in theinput/folder. - Output:
merged_statements.xlsx- one sheet, six columns:txn_id,Date,Description,Debit,Credit,Source_File.
Tech stack. Python · pandas · openpyxl
Audience. Financial analysts and anyone who regularly works with transaction data from multiple banks.
I built this for my own work. When reviewing a company's cash flows, I collect statements from several bank accounts and usually receive a different export layout from each bank. The script consolidates them in one run and produces a flat table for counterparty normalisation, Power BI, or further review in Excel.
Useful when:
- Analysing cash flows across multiple accounts
- Reviewing transaction data from several banks together
- Preparing data for Power BI, Power Query, or pandas
# 1. Clone the repo
git clone https://github.com/IrinaTok11/multibank-transaction-review.git
cd multibank-transaction-review
# 2. Create and activate a virtual environment
python -m venv .venv
# Windows:
.venv\Scripts\activate
# macOS/Linux:
source .venv/bin/activate
# 3. Install dependencies
pip install -r requirements.txt
# 4. Place statement files in the input/ folder, then run
python merge_statements.pyResult: merged_statements.xlsx appears in the project root with all transactions merged and sorted by date.
- Header row is detected automatically - the script scans the first 30 rows and finds the one that best matches the known column names. Real bank statements often have 8-22 rows of letterhead before the transactions start.
- Format is identified by column name matching - no need to specify which bank or format each file belongs to.
- Single signed
Amountcolumn (positive = credit, negative = debit) is split intoDebit/Creditautomatically. - Date formats:
MM/DD/YYYY,DD.MM.YYYY,DD-Mon-YYYY(e.g.01-Jun-2023), plus a pandasdayfirst=Truefallback for other common patterns - parsed in sequence. All dates are written to the output in a single consistent format (DD/MM/YYYY), regardless of the source bank. Excel stores dates as numbers internally, so the display format is set explicitly per cell - every user sees the same result, independent of their regional Windows settings. - Currency symbols, thousand separators, and sign handling in amounts -
$1,234.56,€1.234,56,-$1,234.56,(1,234.56),50 000,00all parse correctly. - Empty
Debit/Creditcells are filled with0.00- no blanks in the output. - Each row gets a
txn_id- MD5 hash of date, amounts, description, and row index. Reproducible across runs on the same input. - Adding support for a new statement format is usually one dictionary entry in
BANK_COLUMN_MAPS. The currentSignedAmountsplit is already built into the script.
input/
├─ statement_bank_A.xlsx ← US layout: Debit / Credit columns
├─ statement_bank_B.xlsx ← US layout: single signed Amount
└─ statement_bank_C.xlsx ← UK layout: Paid in / Paid out
│
▼
merge_statements.py
│
For each file:
1. Scan first 30 rows → find header row
2. Load with correct header
3. Match columns → identify format
4. Rename to canonical schema
5. Normalize dates and amounts
6. Split signed Amount if needed
7. Drop zero-amount rows (balance lines, summary rows)
8. Tag with detected format name + source filename
│
▼
Concatenate → sort by Date → generate txn_id
│
▼
merged_statements.xlsx (one sheet: txn_id | Date | Description | Debit | Credit | Source_File)
Where this fits in the analysis workflow
Bank statements (various formats)
↓ merge_statements.py
merged_statements.xlsx - all transactions in one flat table
↓ counterparty normalisation (Python / SQL)
↓ Power BI - pivot analysis, cash flow review
Console output example (3 of the 6 supported formats shown):
Loading statement_bank_A.xlsx ...
header row: 8
OK statement_bank_A.xlsx (34 rows, format=DebitCredit)
Loading statement_bank_B.xlsx ...
header row: 14
OK statement_bank_B.xlsx (35 rows, format=SignedAmount)
Loading statement_bank_C.xlsx ...
header row: 22
OK statement_bank_C.xlsx (30 rows, format=PaidInOut)
-
Header detection by scoring. The script scans each row and counts how many cells match known column names from
BANK_COLUMN_MAPS. The row with the highest score is treated as the header. If two rows tie, the earlier one is used. A minimum score of 1 is enough to catch partial layouts and still skip most letterhead rows. -
Column map as a config dictionary. Format-specific logic is kept in
BANK_COLUMN_MAPSat the top of the file. Format identification uses a 40% column-name match threshold, tolerates spacing and capitalisation differences, and also requires a date column plus at least one amount column (debit,credit, or signedamount). This avoids false positives where only part of the layout matches. -
Canonical
amounttriggers automatic splitting. When the detected format isSignedAmount,split_signed_amount()moves negative values toDebitand positive values toCredit. After that, the main pipeline works only withdebitandcredit. -
Consistent date display across machines. Excel stores dates as serial numbers, and the visible format depends on each cell's
number_format, not on Windows regional settings. The script setsnumber_format = "DD/MM/YYYY"for every date cell in the output so the file opens with the same display across machines. -
Amounts stored as floats, not strings. All
Debit/Creditvalues land in Excel as numbers with#,##0.00format. DAX measures and Power Query steps can aggregate them directly. -
Zero-fill instead of blanks.
NaNin amount columns can lead to issues in pivot tables and DAX. Filling with0.00is a safer default for financial data. -
Six columns in output. The pipeline uses a
fmtcolumn internally for logging, but it is dropped before writing. The output is always:txn_id,Date,Description,Debit,Credit,Source_File. -
Overwrite behaviour on Windows.
merged_statements.xlsxis overwritten on each run if the file is closed. If it is open in Excel, Windows raises aPermissionError- close the file first.
multibank-transaction-review/
├─ input/ # place statement files here (not tracked)
├─ docs/
│ └─ assets/
│ ├─ pipeline_overview.png # pipeline diagram (inputs → merged output)
│ ├─ global_input.png # screenshot: DebitCredit format
│ ├─ northbridge_input.png # screenshot: SignedAmount format
│ ├─ uk_input.png # screenshot: PaidInOut format
│ └─ merged_statements.png # screenshot: merged output
├─ merge_statements.py # main script
├─ merged_statements.xlsx # example output (generated)
├─ requirements.txt
├─ .gitignore
├─ LICENSE
└─ README.md
-
Create and activate a virtual environment
python -m venv .venv # Windows: .venv\Scripts\activate # macOS/Linux: source .venv/bin/activate
-
Install dependencies
pip install -r requirements.txt
-
Place statement files in
input/Files must be.xlsxor.xls. The script ignores files starting with~(Excel temp files). If a legacy.xlsfile fails to load in your environment, convert it to.xlsxand re-run. -
Run
python merge_statements.py
-
Check the result
merged_statements.xlsxappears in the project root.
To use a different input folder or output path:
python merge_statements.py --input C:\path\to\statements\ --output C:\path\to\result.xlsx
Use
--strictto abort the run if any file fails to load (default behaviour: skip the failed file and continue processing the rest):python merge_statements.py --strict
The script finds the header row automatically, so no fixed layout is required. Minimum expectations:
- File format:
.xlsxor.xls - Columns that match a known format mapping (see Supported formats)
- A date column (
date) with dates in one of the supported formats - A counterparty/description column (
counterpartyorcounterparty_debit/counterparty_credit) - At least one amount column - either a
Debit/Creditpair or a single signedAmount
Rows where both Debit and Credit are zero are dropped - this removes opening-balance lines and account summary rows that survive header detection.
The script identifies each file by matching its column names against the configured mappings. Date formats (MM/DD/YYYY, DD.MM.YYYY, DD-Mon-YYYY, and pandas fallback) are tried automatically for every file - they are not tied to a specific layout. Currently handles the following amount column combinations:
| Format name | Amount columns | Notes |
|---|---|---|
DebitCredit |
Debit + Credit |
Standard US split layout |
DebitCredit_Extended |
Debit + Credit |
Same split layout, date column named Transaction Date |
SignedAmount |
Amount (signed, +/-) |
Single-column layout - split automatically |
PaidInOut |
Paid in (USD) + Paid out (USD) |
UK-style layout |
WithdrawalDeposit |
Withdrawals + Deposits |
Alternative US naming |
PayeePayerSplit |
Withdrawal Amount + Deposit Amount |
Split counterparty: separate Payee (debit) and Payer (credit) columns - merged into one Description field automatically |
To add a new layout, add one entry to BANK_COLUMN_MAPS in merge_statements.py. In the usual case, no function changes are needed; the existing SignedAmount split is already handled separately.
This project is available under the MIT License. See LICENSE.
IRINA TOKMIANINA - Financial/BI Analyst
LinkedIn: linkedin.com/in/tokmianina · Email: irinatokmianina@gmail.com




