agent skill · reconciliation · pandas
Apply Master Classifications from a Mapping Table
Classify transactions (accounts, departments, report lines) from a single master mapping table, with duplicate/inconsistency checks and an unmatched-exception list.
When to use this skill
- You need to tag a trial balance or transaction file with categories — expense type, department, financial-statement line — from a lookup table.
- The same account keeps getting classified three different ways across months, and year-over-year analysis has stopped making sense.
- The current “process” is a VLOOKUP against whichever
Classification_FINAL_FINAL.xlsxhappens to be newest on the shared drive.
The fix: one master mapping file, validated before use (no duplicate keys, no conflicting categories), merged with indicator=True so every unmapped account lands on an exception list instead of silently becoming blank.
Inputs it expects
- Transaction file: e.g., a trial balance with
Account_num,Account_desc,Amount. - Master mapping table: one Excel sheet, one row per account, with
Account_num,Category,Subcategory,Report_Line, and aYear_created(or effective-date) column for the audit trail. - Keys in the same format on both sides — account numbers as strings, consistently padded, no stray spaces.
- An agreed rule for conflicts: usually “most recent classification wins.”
Steps
- Load the mapping library and check it for trouble before using it: accounts appearing more than once, and accounts whose category differs between rows.
- Export any inconsistencies for team review — those are governance findings, not code problems.
- Deduplicate to one row per account (keep the most recent by
Year_created). - Record the transaction row count and total amount — your control totals.
- Left-merge transactions against the clean library with
indicator=Trueandvalidate='many_to_one'. - Split matched vs. unmatched. Summarize unmatched accounts by transaction count and total dollars (biggest dollars first) and export for classification.
- Tie out totals, then publish the classified file.
Code
import pandas as pd
# 1. Load transactions and the master mapping library
txns = pd.read_excel('trial_balance_december.xlsx') # Account_num, Account_desc, Amount
library = pd.read_excel('Master_Classifications.xlsx',
sheet_name='Mappings', keep_default_na=False)
for df in (txns, library):
df['Account_num'] = df['Account_num'].astype(str).str.strip()
rows_in = len(txns)
amount_in = txns['Amount'].sum()
# 2. Validate the library: same account, different categories?
cat_counts = library.groupby('Account_num')['Category'].nunique()
inconsistent = cat_counts[cat_counts > 1]
if len(inconsistent) > 0:
library[library['Account_num'].isin(inconsistent.index)] \
.sort_values(['Account_num', 'Year_created']) \
.to_excel('REVIEW_inconsistent_classifications.xlsx', index=False)
print(f"WARNING: {len(inconsistent)} accounts have conflicting classifications — exported for review")
# 3. Deduplicate: most recent classification wins
library_clean = library.sort_values(['Account_num', 'Year_created']) \
.drop_duplicates(subset='Account_num', keep='last')
# 4. Merge — keep every transaction, label match status, forbid fan-out
classified = pd.merge(
txns,
library_clean[['Account_num', 'Category', 'Subcategory', 'Report_Line']],
on='Account_num',
how='left',
indicator=True,
validate='many_to_one'
)
matched = (classified['_merge'] == 'both').sum()
unmatched = (classified['_merge'] == 'left_only').sum()
print(f"Transactions: {rows_in:,} | Mapped: {matched:,} "
f"({matched / rows_in:.1%}) | Unmapped: {unmatched:,}")
# 5. Exception list: unmapped accounts, largest dollars first
if unmatched > 0:
(classified[classified['_merge'] == 'left_only']
.groupby(['Account_num', 'Account_desc'])['Amount']
.agg(Transaction_count='count', Total_amount='sum')
.reset_index()
.sort_values('Total_amount', key=abs, ascending=False)
.to_excel('EXCEPTIONS_unmapped_accounts.xlsx', index=False))
classified.drop(columns='_merge').to_excel('trial_balance_CLASSIFIED.xlsx', index=False)
Validation (control totals)
- Rows in vs. rows out:
len(classified) == rows_in. Withvalidate='many_to_one'and a deduplicated library this cannot drift — if it does, the library still has duplicate keys. - Matched + unmatched = total:
matched + unmatched == rows_in. Report the coverage percentage each period; it should trend toward 100%. - Amount tie-out:
classified['Amount'].sum() == amount_into the penny, and mapped amount + unmapped amount = total amount. Also tieclassified.groupby('Category')['Amount'].sum()plus the unmapped bucket back to the original total. - Library sanity: after dedup,
library_clean['Account_num'].is_uniquemust beTrue.
Exceptions to surface
- Every account in
EXCEPTIONS_unmapped_accounts.xlsx— new or mistyped accounts with no classification. Review largest absolute dollars first; a $2M unmapped account matters more than fifty $10 ones. - Every account in
REVIEW_inconsistent_classifications.xlsx— the same account classified differently across years. A human must confirm whether it was an intentional reclassification (document it) or a typo/judgment drift (correct the library). - Mappings never used by any transaction (
right_onlyif you re-run withhow='outer', or an anti-join): stale library entries worth pruning. - Near-duplicate category names in the library (
"Personnel"vs"Personel") — checklibrary['Category'].unique()and reconcile spellings before they split your subtotals.