ds@mcpanalytics.aiEmail your data and your question and get an answer about your own data, with the method shown.Email your data and question. Get an answer.

Expense Anomaly Detection

By MCP Analytics Team | Published , updated

Duplicate vendor payments, expense report padding, miscoded general ledger (GL) entries, and fraud may be hard to find in transaction data by line-by-line review. This analysis applies machine learning anomaly detection to your expense data, flagging the transactions that deviate most from normal patterns and giving you a ranked shortlist to investigate instead of an ocean of line items.

Worked Example (Simulated): Which Expense Rows Stand Out?

Question: Which simulated expense rows differ most from the usual pattern?

For a single numeric column, a robust z-score is a simple baseline.

Inputs: Simulated data, seed 17. The script makes 20 rows with amount, days since the prior payment, and weekday (Monday is 1). Seventeen are regular rows: amounts near 100 (kept between 65 and 135), gaps of 5 to 9 days, weekdays 1 to 5. Three rows are planted: T18 (245, 1, 7), T19 (38, 1, 1) and T20 (100, 30, 7).

Method: The script fits an Isolation Forest model with a fixed seed, so the run can be repeated. It sets contamination=0.15, which asks the model to flag the top 15 percent of rows. On 20 rows that is 3 rows, whatever the data look like. It prints the flagged rows, highest score first.

import numpy as np
from sklearn.ensemble import IsolationForest

SEED = 17
rng = np.random.default_rng(SEED)
# Simulated regular transactions: amounts near 100 and payment gaps near 7 days.
regular_amounts = np.clip(rng.normal(100, 12, size=17), 65, 135)
regular_gaps = rng.integers(5, 10, size=17)
regular_days = rng.integers(1, 6, size=17)
X = np.column_stack([regular_amounts, regular_gaps, regular_days])
# Add three simulated transactions with unusual combinations.
X = np.vstack([X, [245, 1, 7], [38, 1, 1], [100, 30, 7]])
ids = [f"T{i:02d}" for i in range(1, len(X) + 1)]
model = IsolationForest(n_estimators=200, contamination=0.15, random_state=SEED)
model.fit(X)
anomaly_scores = -model.decision_function(X)
pred = model.predict(X)
flagged = np.flatnonzero(pred == -1)
ranked = sorted(flagged, key=lambda i: anomaly_scores[i], reverse=True)
print(f"Simulated data, seed {SEED}")
print(
    f"Rows: {len(X)}; features: amount, days since prior payment, "
    "weekday (Monday=1)"
)
print(f"Flagged rows: {len(flagged)}")
print("ID   amount  gap_days  weekday  anomaly_score")
for i in ranked:
    print(
        f"{ids[i]}  {X[i,0]:6.1f}  {int(X[i,1]):8d}  {int(X[i,2]):7d}  "
        f"{anomaly_scores[i]:.3f}"
    )

Printed output:

Simulated data, seed 17
Rows: 20; features: amount, days since prior payment, weekday (Monday=1)
Flagged rows: 3
ID   amount  gap_days  weekday  anomaly_score
T20   100.0        30        7  0.174
T18   245.0         1        7  0.163
T19    38.0         1        1  0.121

Result: The run flags the three planted rows, in the order T20, T18, T19. The count of three comes from the setting, not from the model deciding there were three. The planted rows were built to stand apart: T19's amount of 38 is below the regular range, and T18 and T20 fall on weekday 7, which never occurs in the regular rows, so the odd weekday is part of what the model saw. This shows the method ranking rows that were built to be odd. It is not a detection rate. Check flagged rows against invoices and payment records before treating them as problems.

Limit: These rows are made up. A flag is a prompt to review a record, not proof of fraud.

Sources: Isolation Forest was published by Fei Tony Liu, Kai Ming Ting and Zhi-Hua Zhou in 2008 (IEEE International Conference on Data Mining). Isolation Forest.

The Problem With Threshold-Based Rules

Most companies that try to catch unusual expenses use simple rules: flag anything over $10,000, flag any vendor payment within 5 days of a previous payment to the same vendor, flag expense reports that exceed the monthly average by 2x. These rules catch the obvious cases: a $50,000 payment to an unknown vendor will trigger every time. But they miss the subtle ones and generate too many false positives on legitimate transactions.

To find the rows in your own data that do not fit the usual pattern, try the free anomaly detection tool.

A duplicate payment of $2,300 to a vendor who normally receives $2,300 monthly looks perfectly normal to a threshold rule. An expense report that bills $180 for dinner every Tuesday night (slightly above the $150 average but never exceeding the $200 threshold) is $30 over that average each time, and no rule catches it. A vendor who gradually increases invoice amounts by 3% per month is invisible until someone audits the full history.

Machine learning anomaly detection works differently. Instead of looking at one dimension at a time (amount, frequency, vendor), it evaluates transactions across all dimensions simultaneously. A payment might look normal on amount alone and normal on frequency alone, but the combination of that amount, to that vendor, on that day of the week, in that department, is unusual relative to the full pattern of normal transactions. The algorithm, called Isolation Forest, finds these multi-dimensional outliers that single-dimension rules miss.

How Isolation Forest Works (Without the Math)

The core idea is intuitive: unusual things are easier to describe than normal things. If you played a game of 20 questions about a transaction, a normal payment ("$2,300, to Office Depot, on the 15th, from the admin department") would take many questions to isolate from the crowd of similar transactions. But an unusual payment ("$2,300, to a vendor we have never paid before, on a Sunday, from the engineering department") can be isolated in just a few questions.

The algorithm builds many random trees. Rows that a few random splits can isolate get higher anomaly scores. Rows that take many splits to isolate, because they look like everything else, get lower scores. The scores are averaged over all the trees.

You do not need to tell the model what fraud looks like. You do not need labeled examples of "good" and "bad" transactions. You do not need to configure rules or set thresholds (beyond specifying roughly what percentage of transactions you expect to be anomalous). The algorithm learns what "normal" looks like from your data and flags everything that deviates.

What Kinds of Anomalies It Catches

The key advantage over rules is that the model catches combinations that no single rule would flag. A $900 payment is not unusual. A payment to Vendor X is not unusual. But a $900 payment to Vendor X from Department Y on a Saturday. That specific combination might score as highly anomalous because it has never occurred before.

Who This Is For

Controllers, accounts payable managers, and internal auditors at companies with $5M to $100M in revenue: large enough to process meaningful transaction volume (500+ per month) but too small for enterprise fraud detection software. Industries with significant operational spending benefit the most: construction, healthcare, professional services, manufacturing, and hospitality. Companies with distributed purchasing authority (multiple cost centers, field offices, or project-based billing) are particularly vulnerable because no single person sees all transactions.

A common alternative is periodic manual sampling: the controller pulls a random sample of transactions each month and reviews them. This analysis can instead rank transactions for review. Check flagged rows against invoices, payment records, and known outcomes. This page does not show that the method finds more problems than random sampling.

What Data You Need

A CSV export from your general ledger, accounts payable system, or expense management platform (QuickBooks, Xero, Expensify, Concur, Brex). You need at least two numeric columns that characterize each transaction:

Additional numeric features that significantly improve detection:

The model accepts 2-20 numeric features. More dimensions give it more ways to distinguish anomalies from normal transactions. Categorical columns like vendor name or GL code should be excluded from the feature mapping. The model requires numeric inputs only. Use those columns for investigation after the anomalies are flagged.

More rows give the model a clearer picture of what is normal. The example above uses 20 made-up rows only to show the steps.

How to Read the Report

Anomaly Score Distribution: a histogram showing scores across all transactions. Normal transactions cluster at low scores; anomalies sit in the right tail. A gap between score groups is a pattern in the results, not proof that the model is right. Check flagged transactions against records and known outcomes. A gradual tail with no clear gap means the boundary is fuzzy and you should interpret borderline cases with caution.

Top Anomalies Table: this is your action list. The 20-50 most anomalous transactions ranked by score, with all feature values shown. For each transaction, you can see exactly why it was flagged: unusual amount, unusual timing, unusual vendor frequency, or some combination. Start your investigation here.

Feature Importance, which dimensions drove the anomaly scores most. If "transaction_amount" dominates, your anomalies are primarily unusually large or small payments. If multiple features contribute roughly equally, the anomalies are multi-dimensional. They look unusual across several characteristics simultaneously, which often indicates more sophisticated patterns.

Normal vs. Anomaly Comparison: side-by-side statistics showing how anomalous transactions differ from normal ones. Maybe anomalous transactions are 10x the average amount. Maybe they have normal amounts but come from vendors with unusually low payment frequency. This comparison table makes the differences concrete.

Building a Review Workflow

Monthly Cadence

Run the analysis at month-end on the full month's GL export. Review the top 20 anomalies. Most will have innocent explanations: a legitimate one-time purchase, a vendor who changed billing frequency, a correctly coded but unusual project expense. Flag the ones that do not have obvious explanations for deeper investigation.

Prioritize by Score

Anomaly scores are continuous, not binary. Scores are tool specific. Use the report's score definition and ranking, then check flagged rows against records and known outcomes. Start with the highest scores and work down. You will quickly develop a sense for what score level separates "worth investigating" from "probably fine" in your specific data.

Track False Positives

Keep a log of which flagged transactions turned out to be legitimate. Over time, this helps you tune the contamination parameter, if the model flags 5% of transactions but only 1% are actually problematic, reduce the contamination setting from 5 to 2. This narrows the investigation list and reduces reviewer fatigue.

When to Use Something Else

Your turn

Bring your own data and the question you actually need answered.

MCP AnalyticsData science by email Send us your data and your question, and we will reply. ds@mcpanalytics.ai