Treasury Data for AI
Garbage in, garbage out. Clean a messy treasury dataset before it touches a model.
- 01Treasury data sources, 5 SQL patterns and the EXTRACT > CLEAN > TRANSFORM > VALIDATE > LOAD pipeline
- 026 data quality checks and the 10 pandas commands that cover most treasury data prep
- 03Live build: clean 52 weeks of messy cash flow data and engineer 12 features
- A Python setup guide (or run it all in Claude)
- The messy and the clean dataset (CSV)
- Tested check and cleaning scripts
Garbage in, garbage out
The best model in the world cannot fix bad data. The often-quoted rule of thumb: around 80% of an AI project's time goes into data preparation, not modeling. Five ways bad treasury data breaks a model:
Missing values. 3 weeks of cash flow data missing. The model fills the gap with a guess. The forecast becomes fiction.
Duplicate entries. The same payment booked twice in the ERP. The model learns the duplicate as normal and over-forecasts payments.
Inconsistent formats. One bank sends DD/MM/YYYY, another MM/DD/YYYY. The model confuses 4 March with 3 April.
Outliers. A one-time EUR 5M acquisition payment. If it is not flagged, the model expects EUR 5M every quarter.
Stale data. Yesterday's FX rates used for today's exposure. The position is off by thousands.
Before you start: two ways to run the code
Modules 8 and 9 use Python. You do not need to become a programmer: AI writes the code, you check the results. Pick one route.
Use an AI assistant with code execution (for example Claude's code execution / analysis tool). Upload the CSV, paste the prompt, the AI writes and runs the Python and shows you the result.
Good for learning. Limits: nothing is stored between sessions, no connection to your systems, nothing can be scheduled. Use the course data, not your company's.
- Install Python 3.11 or later from python.org. On Windows, tick Add Python to PATH.
- Open a terminal (Command Prompt on Windows) and run the two commands below. If
pipis not recognized, usepy -m pip install … - In Jupyter: New › Python 3, paste the test cell, press Shift + Enter.
pip install pandas numpy matplotlib jupyter prophet scikit-learn openpyxl pytest jupyter notebook
import pandas as pd
print('Setup complete. Pandas version:', pd.__version__)Prefer Anaconda? It bundles Python, pandas and Jupyter, but not Prophet: add it with conda install -c conda-forge prophet. Check with IT first: Anaconda's terms require a paid licence for larger organizations. Miniforge is the free conda-forge alternative.
A new install today gives you pandas 3. Every script in this module is tested on pandas 2.3 and 3.0.
Five treasury data sources
Each source has its own format, refresh rate and quality issues. Your job is to turn all of them into one clean dataset.
MT940, MT942, CAMT.053, CAMT.054
SWIFT / EBICS / host-to-host / API
SAP FI tables (BKPF, BSEG), Oracle GL
SQL queries / RFC / OData API
Deal records, cash positions, exposures
API / CSV export / database views
FX rates, interest rates, yield curves
Bloomberg / ECB / Refinitiv feeds
Budgets, forecasts, AR/AP aging, payroll
Excel / SharePoint / Power Query
SQL for treasury: 5 patterns
You do not need to be a database administrator. These five patterns cover most treasury data extraction. Each one below was run against a test database.
-- 1. SELECT + WHERE: cash flows for one entity from January 2025 onward
SELECT date, amount, type
FROM cash_flows
WHERE entity = 'Entity_NL'
AND date >= '2025-01-01';
-- 2. GROUP BY + SUM: daily transactions into weekly totals
SELECT week_number, SUM(amount) AS total
FROM cash_flows
GROUP BY week_number
ORDER BY week_number;
-- 3. JOIN: bank lines matched to ERP entries by reference
SELECT b.date, b.amount, e.gl_account
FROM bank_statements b
JOIN erp_ledger e ON b.reference = e.reference;
-- 3b. LEFT JOIN: the bank lines with NO ERP match (your reconciliation breaks)
SELECT b.date, b.amount, b.reference
FROM bank_statements b
LEFT JOIN erp_ledger e ON b.reference = e.reference
WHERE e.reference IS NULL;
-- 4. CASE WHEN: categorize by description (LOWER makes it case-insensitive everywhere)
SELECT date, amount,
CASE
WHEN LOWER(description) LIKE '%payroll%' THEN 'Payroll'
WHEN LOWER(description) LIKE '%supplier%' THEN 'Supplier'
ELSE 'Other'
END AS category
FROM cash_flows;
-- 5. LAG: previous-day balance and the daily change
SELECT date, amount,
LAG(amount, 1) OVER (ORDER BY date) AS prev_day,
amount - LAG(amount, 1) OVER (ORDER BY date) AS daily_change
FROM cash_positions;
Two treasury traps. A plain JOIN silently drops bank lines with no ERP match, and those are exactly the items your reconciliation needs. Use the LEFT JOIN in 3b to list them.
LIKE is case-sensitive in some databases (PostgreSQL, for example), so "PAYROLL January" would fall into Other. Wrapping the column in LOWER() makes the rule behave the same everywhere.
The treasury data pipeline
Every dataset goes through the same five stages. Learn it once, apply it to every dataset.
This is ETL plus validation. VALIDATE is the step most teams skip, and it is the one an auditor asks about.
The 6 data quality checks
Run these on every dataset before it touches a model.
Missing values? Gaps in dates? Empty fields?
df.isnull().sum() · date range check
Duplicate rows? The same transaction twice?
df.duplicated().sum() · reference check
Mixed date formats? Currency codes vary?
df['date'].dtype · df['ccy'].unique()
Opening + transactions = closing? Totals match the source?
Balance validation · cross-reference
Extreme values? One-time items skewing the pattern?
df.describe() · IQR · z-score
How old is this data? When was the last refresh?
max(date) · data freshness
Python + pandas: the 10 commands you need
You are not becoming a data scientist. These 10 commands cover most treasury data preparation.
df = pd.read_csv('cash_flows.csv') # 1. load (pd.read_excel for .xlsx)
df.head() # 2. preview the first 5 rows
df.info() # 3. column types and non-null counts
df.describe() # 4. min, max, mean, std per column
df.isnull().sum() # 5. missing values per column
df = df.ffill() # 6. forward fill (or df.dropna())
df = df.drop_duplicates() # 7. remove exact duplicate rows
df['date'] = pd.to_datetime(df['date'], format='%d/%m/%Y') # 8. fix dates
weekly = df.groupby('week')['amount'].sum() # 9. aggregate
df['prev_week'] = df['amount'].shift(1) # 10. lag featureWatch out for old tutorials. df.fillna(method='ffill') appears in many guides. It was deprecated in pandas 2 and raises an error in pandas 3. Use df.ffill().
Command 8 names the format explicitly. dayfirst=True alone is only a hint. When one file mixes formats, pandas either stops with an error or guesses, and a guess is how 4 March becomes 3 April. The live build shows it.
Feature engineering for treasury
Features are the signals a model learns from. Better features, better forecast, even with a simple model.
Month, quarter, week of year, "does this week contain a month end or quarter end".
Previous week, previous 4 weeks, same week last year, rolling 4-week average.
Transaction type (payroll, supplier, customer, IC, tax, fees), one-hot encoded for ML models.
Payroll week, tax week, holiday, budget period. Events you already know about in advance.
Cumulative balance, net flow, inflow/outflow ratio, rolling volatility, deviation from the mean.
Match features to the grain of your data. On weekly data, "day of week" is the same for every row, and "is month end" is almost never true, so use "week contains a month end" instead. A feature that never changes teaches the model nothing.
The dataset: 52 weeks of messy cash flow data
Synthetic weekly cash flows for 2025, in EUR, with six problems planted on purpose. Download it and use it in both routes.
Receipts missing for 3 weeks. Weeks 15, 16, 17 are empty.
A duplicate row. Week 22 (a payroll week) appears twice.
Mixed date formats. Weeks 1–26 are DD/MM/YYYY, weeks 27–52 are YYYY-MM-DD.
A one-off. Week 38: a EUR 5.2M acquisition payment, funded the same week by a EUR 5.0M loan from the parent.
Inconsistent currency codes. EUR, eur and Euro, at random.
A sign error. Week 29: supplier payments booked as positive.
Run the 6 checks
Upload the CSV and paste this prompt (Route A), or run the script in Jupyter (Route B). Diagnose first, fix later.
I have uploaded treasury_cash_flows_messy.csv: 52 weeks of weekly cash flows in EUR. Columns: week_number, date, receipts, supplier_payments, payroll, tax, intercompany, other, net_flow, currency. Outflows are negative. Use Python (pandas) to run these 6 data quality checks and show the output of each. Do not fix anything yet. 1. Completeness: df.isnull().sum(). Which columns and weeks have missing values? 2. Uniqueness: df.duplicated().sum(). Which rows are duplicated? 3. Consistency: what type is the date column, which date formats appear, and what does df['currency'].unique() show? 4. Accuracy: add up the six flow columns per week and compare with net_flow. List the weeks that do not reconcile. 5. Outliers: flag any value more than 3 standard deviations from its column mean. List week and column. 6. Timeliness: what is the date range? Any gaps or repeated weeks in the weekly sequence?
Python script · tested on pandas 2.3 and 3.0
import pandas as pd
df = pd.read_csv('treasury_cash_flows_messy.csv')
FLOWS = ['receipts', 'supplier_payments', 'payroll', 'tax', 'intercompany', 'other']
# 1. Completeness
print(df.isnull().sum())
# 2. Uniqueness
print('Duplicate rows:', df.duplicated().sum())
# 3. Consistency
print('Date column type:', df['date'].dtype, '| sample:', df['date'].iloc[[0, -1]].tolist())
print('Currency codes:', df['currency'].unique())
# 4. Accuracy: do the flows add up to net_flow?
diff = df[FLOWS].sum(axis=1) - df['net_flow']
print('Weeks that do not reconcile:', df.loc[diff.abs() > 1, 'week_number'].tolist())
# 5. Outliers: more than 3 standard deviations from the column mean
z = (df[FLOWS] - df[FLOWS].mean()) / df[FLOWS].std()
for col in FLOWS:
weeks = df.loc[z[col].abs() > 3, 'week_number'].tolist()
if weeks:
print(f'Outliers in {col}: weeks {weeks}')
# 6. Timeliness: date range (after parsing, see the fix script)
What you should see · open after you run it
- Completeness: 3 missing values, all in receipts (weeks 15, 16, 17).
- Uniqueness: 1 duplicate row (week 22). The file has 53 rows, not 52.
- Consistency: dates are stored as text, in two formats. Three spellings of the currency: EUR, eur, Euro.
- Accuracy: weeks 15, 16, 17 (missing receipts) and 29 (sign error) do not reconcile to net_flow.
- Outliers: week 29 (supplier), week 38 (intercompany and other). Weeks 13, 26, 39 and 52 are flagged too: quarterly tax. That is a false positive. The test flags, the treasurer decides.
- Timeliness: 6 January to 29 December 2025. The duplicate shows up as a repeated week, not as a gap. Once it is removed, the sequence has no gaps.
Fix all six, then prove it
The prompt spells out how to fix each issue. Left to decide on its own, the AI tends to pick the two fixes that quietly corrupt the data.
Now fix the issues. Follow these rules exactly: 1. Duplicates: drop exact duplicate rows, keep the first. 2. Dates: the file mixes DD/MM/YYYY and YYYY-MM-DD. Parse each format explicitly (format='%d/%m/%Y' and format='%Y-%m-%d'). Never let pandas guess day versus month. Confirm every date parsed and the weeks are exactly 7 days apart. 3. Missing receipts (weeks 15-17): net_flow comes from the bank statement, so rebuild receipts = net_flow minus the other five flow columns. Do not interpolate. 4. Currency: standardize to 'EUR'. 5. Supplier payments are outflows: make every value negative. 6. Week 38 holds a EUR 5.2M acquisition payment and its EUR 5.0M intercompany funding. Add non_recurring=True for that week. Flag it, do not delete it. Then prove it: every week must now satisfy sum of flows = net_flow. Show df.info() and df.describe().
We tested the tempting shortcut: pd.to_datetime(df['date'], format='mixed', dayfirst=True). On pandas 3.0 it reads 2025-12-01 as 12 January. No error, no warning. Parse each format explicitly.
Interpolation missed the real receipts by EUR 33K–81K a week, and those weeks no longer reconciled. The net flow comes from the bank, so the gap can be rebuilt exactly: receipts = net_flow − the other flows. Estimate only what you cannot rebuild.
Python script · fixes, validation and features · tested
import pandas as pd
df = pd.read_csv('treasury_cash_flows_messy.csv')
FLOWS = ['receipts', 'supplier_payments', 'payroll', 'tax', 'intercompany', 'other']
# 1. Duplicates: drop exact duplicate rows, keep the first
df = df.drop_duplicates(keep='first').reset_index(drop=True)
# 2. Dates: parse each format explicitly. Never let pandas guess day vs month.
iso = pd.to_datetime(df['date'], format='%Y-%m-%d', errors='coerce')
dmy = pd.to_datetime(df['date'], format='%d/%m/%Y', errors='coerce')
df['date'] = iso.fillna(dmy)
assert df['date'].notna().all(), 'Some dates match neither format'
assert (df['date'].diff().dropna() == pd.Timedelta(days=7)).all(), 'Gap in the weekly sequence'
# 3. Missing receipts: net_flow comes from the bank, so rebuild receipts exactly
gap = df['receipts'].isna()
df.loc[gap, 'receipts'] = df.loc[gap, 'net_flow'] - df.loc[gap, FLOWS[1:]].sum(axis=1)
# 4. Currency codes
df['currency'] = df['currency'].str.strip().str.upper().replace({'EURO': 'EUR'})
# 5. Sign error: supplier payments are always outflows
df['supplier_payments'] = -df['supplier_payments'].abs()
# 6. One-off: flag, do not delete
df['non_recurring'] = df['week_number'] == 38
# Validate: every week must reconcile again
assert ((df[FLOWS].sum(axis=1) - df['net_flow']).abs() < 1).all(), 'Flows do not add up to net_flow'
# Features
week_end = df['date'] + pd.Timedelta(days=6)
df['month'] = df['date'].dt.month
df['quarter'] = df['date'].dt.quarter
df['week_of_year'] = df['date'].dt.isocalendar().week.astype(int)
df['contains_month_end'] = week_end.dt.month != df['date'].dt.month
df['contains_quarter_end'] = df['contains_month_end'] & df['month'].isin([3, 6, 9, 12])
df['prev_week_net'] = df['net_flow'].shift(1)
df['prev_4week_net'] = df['net_flow'].shift(4)
df['rolling_4week_avg'] = df['net_flow'].rolling(4).mean()
df['is_payroll_week'] = df['payroll'] != 0
df['is_tax_week'] = df['tax'] != 0
df['cumulative_balance'] = 2_450_000 + df['net_flow'].cumsum()
inflows = df['receipts'] + df[FLOWS[1:]].clip(lower=0).sum(axis=1)
outflows = -df[FLOWS[1:]].clip(upper=0).sum(axis=1)
df['inflow_outflow_ratio'] = (inflows / outflows).round(3)
df.info()
df.to_csv('treasury_cash_flows_clean.csv', index=False)
Add 12 features and save a model-ready dataset
The outlier is flagged, not deleted: the model should know week 38 happened, but not learn from it.
Now add 12 features for a forecasting model: 1. Time: month, quarter, week_of_year, contains_month_end, contains_quarter_end (does the week, Monday to Sunday, include a month end or quarter end?) 2. Lag: prev_week_net (shift 1), prev_4week_net (shift 4), rolling_4week_avg 3. Business: is_payroll_week (payroll is not zero), is_tax_week (tax is not zero) 4. Derived: cumulative_balance (starting from EUR 2,450,000), inflow_outflow_ratio Show the final DataFrame and save it as treasury_cash_flows_clean.csv.
Result: 52 rows, 10 original columns, the non_recurring flag and 12 features. Every week reconciles. The lag features are empty in the first rows (there is no week 0), which is expected.
This file feeds Module 9. If your AI output differs, compare it with ours:
↓ treasury_cash_flows_clean.csvThe Microsoft ecosystem for treasury data
For when you scale. Start with Python and pandas for cleaning, Power Query for extraction, Power BI for visuals. Add the rest as you grow.
Power Query. Extraction and transformation, built into Excel and Power BI. No code for basic ETL.
BeginnerPower BI. Dashboards on any data source. Scheduled refreshes. Share with the team.
BeginnerPower Automate. Workflow automation: run data pipelines on a schedule or an event, email alerts, file handling.
IntermediateMicrosoft Fabric. The unified data platform: lakehouse, data engineering, notebooks, ML. Enterprise scale.
AdvancedCopilot. The AI assistant inside Excel, Power BI and Power Automate. Data analysis in plain language.
BeginnerWhat you take away
Data quality is most of the work. The model is the easy part.
5 treasury data sources: banks, ERP, TMS, market data, internal.
5 SQL patterns, plus the LEFT JOIN that finds what did not match.
The EXTRACT > CLEAN > TRANSFORM > VALIDATE > LOAD pipeline.
6 data quality checks on every dataset. A flag is a question, not a verdict.
10 pandas commands, written for pandas 3.
Rebuild what you can, estimate only what you cannot. Parse dates explicitly.
Features that match the grain of your data: time, lag, category, business, derived.
You cleaned a treasury dataset from scratch.
Same six checks, same fixes, on every dataset you will ever feed a model. With real data it takes longer. The process is identical.
Next: Module 9, Building Cash Flow Forecasting Models. The clean dataset becomes a backtested 13-week forecast with three scenarios.
Built by a treasurer, for treasurers. · treasuryease.com