A Power BI fraud-flagging dashboard built on a synthetic 25,155-row corporate expense ledger with deliberately planted anomalies — duplicate charges, over-limit spend, self-approvals, round-number patterns, and spending spikes. Rule-based risk scoring surfaces the ~20% of transactions worth a second look, ranks employees by risk, and exposes segregation-of-duties gaps.
Live interactive showcase → (add your Netlify/portfolio link here)
| Dataset | 25,155 synthetic expense transactions, Jan 2025 – Jun 2026 |
| Tool | Power BI Desktop (DAX, Power Query / M) |
| Report pages | Overview · Investigation List · Employee Risk |
| Total expenses | ₱4.67M |
| Flagged transactions | 5,018 (19.95%) |
| High-risk transactions (Score 3+) | 74 |
| Employees with Score 3+ | 31 |
| Employees with self-approved expenses | 34 |
The dashboard scores every transaction against seven independent fraud rules (duplicate, over-limit, self-approved, round-number, round-number pattern, weekend submission, spending spike, rapid cluster) and rolls them into a single Risk Score, filterable live via a disconnected threshold table.
Add screenshots here:
screenshots/overview.png,screenshots/investigation-list.png,screenshots/employee-risk.png
Overview — KPI cards, monthly expense trend, flags by department, expenses by category Investigation List — full flagged-transaction table with a Score 1+/2+/3+ button slicer Employee Risk — company-wide risk ranking + self-approval (segregation-of-duties) table
Star schema: one fact table, one disconnected parameter table, and four measure-only tables kept separate by purpose rather than dumped into one bucket.
Validated_Expense (fact)
├── Expense ID, Amount, Risk_Score
├── Flag_Duplicate, Flag_OverLimit, Flag_SelfApproved
├── Flag_RoundNumber, Flag_RoundNumberPattern
├── Flag_Weekend, Flag_SpendingSpike, Flag_RapidCluster
└── Employee, Manager, Department, Vendor, Category, Payment Method
RiskThreshold (disconnected)
└── Label, MinScore → powers the Score 1+/2+/3+ button slicer
Core KPI Measures Flag Measures for KPI Cards
├── Total Expenses ├── Flag Count – Duplicate ├── Expense/Flagged MoM %
├── % Flagged ├── Flag Count – Over Limit ├── MoM Color (inverted)
├── Avg Risk Score ├── Flag Count – Self Approved └── MoM Icon + Label
└── High Risk (Score 3+) └── Total Flags (All Types)
Department/Employee Breakdown
├── Employees With Any Flag
├── Employees With Score 3+
└── Employees with Self-Approval
KPI cards are built manually — background shapes + text boxes + Card visuals + MoM % cards + line-chart sparklines layered together — rather than relying on a single visual type, for full control over spacing and conditional formatting.
Real trade-offs made during the build, not just a feature list.
Flag rate calibration: 38% → ~19% The first pass of rules flagged over a third of all spend — unusable for an investigator. Brought it down by excluding certain categories from the weekend rule, adding a buffer above the policy limit before triggering "over limit," and running a Python distribution analysis to set the spending-spike window empirically instead of guessing a round threshold.
Expense ID stays in the table on purpose Power BI tables silently collapse rows that are identical across all visible columns. Two duplicate transactions — same employee, vendor, amount, date — would merge into one row and understate the risk score. Keeping a unique ID column in any audit/transaction table is non-negotiable.
RANKX / ALLSELECTED trap
ALL(Employee) doesn't clear filter context introduced by other active slicers, so the ranking measure was silently ranking within department instead of company-wide. Switching to ALLSELECTED scoped to the Employee column fixed true global ranking while still respecting the department/manager slicers.
Inverted MoM conditional formatting For a risk metric, an increase is the bad outcome — so every MoM card on this dashboard flips Power BI's default color logic: red means "up," green means "down."
Risk Score ≥ 2 as the operational threshold High enough precision to be actionable for a reviewer, not so restrictive that true positives slip through. The disconnected RiskThreshold table lets anyone toggle 1+/2+/3+ live instead of hard-coding one cutoff.
A page was cut, not shipped broken
An early Trend page used DATESMTD anchored to TODAY(). The dataset ends June 2026, so "month-to-date" was silently comparing against a real-world today with no data behind it — producing a phantom partial-month dip. Shipping a broken KPI is worse than shipping one fewer page, so the Trend page was removed rather than patched around.
- Power BI Desktop — DAX measures, calculated columns, disconnected tables, conditional formatting via field value
- Power Query / M — data transformation and audit rule implementation
- Python — distribution analysis for empirical threshold tuning
- pbixray — extracting DAX measures, schema, and relationships for documentation
Expense_Audit/
├── Expense_Audit.pbix
├── screenshots/
│ ├── overview.png
│ ├── investigation-list.png
│ └── employee-risk.png
└── README.md
Ryan Seguiro — Data Analytics GitHub @nayR02 · LinkedIn · Portfolio (add link)