Last checked
Use the public PaySim dataset, keep only TRANSFER and CASH_OUT rows, skip the leaky after-balance columns, split by time and train a gradient boosting model with class weights. Then choose a score threshold by adding up the shillings missed and the cost of false alarms.
McTaba runs coding and data bootcamps in Kenya. This guide is written to be useful whether or not you ever join one.
Real M-Pesa fraud data is private, and rightly so. The public stand-in is PaySim: a simulator, built from logs of a mobile money service, that produces transactions with fraud added. You can download it from the PaySim page on Kaggle (checked on 3 November 2026; a free Kaggle account is needed to download). The authors describe the simulator in a 2016 paper, "PaySim: A financial mobile money simulator for fraud detection", by Lopez-Rojas, Elmir and Axelsson.
This is one of the projects on our data science career map. M-Pesa fraud detection is also a capstone option in our Data Science to AI Engineering programme.
Each row has a step (one hour of the simulated month), a type, an amount, origin and destination balances, and an isFraud label. The data is synthetic, so any score you get here says nothing about real M-Pesa. It teaches the method.
| Step | Why it matters |
|---|---|
| Check how rare fraud is | Rare labels make accuracy useless |
| Keep TRANSFER and CASH_OUT | The fraud pattern is takeover, move, cash out |
| Drop the after-balances | They leak the answer |
| Split by time | Tomorrow's fraud must be tested on yesterday's training |
We teach metrics for imbalanced fraud data in phase 4 of the Data Science to AI Engineering programme.
Print the share of fraud first. It is a tiny fraction of rows. A model that says "not fraud" every time would score above 99 percent accuracy and catch nothing. That is why we ignore accuracy here.
Read the dataset notes on Kaggle. They explain that fraud transactions are cancelled, so the balance columns after the transaction can give the answer away. Use only what a system knows before it approves the payment.
import pandas as pd
from sklearn.ensemble import HistGradientBoostingClassifier
df = pd.read_csv("PS_20174392719_1491204439457_log.csv")
print(df["isFraud"].mean())
df = df[df["type"].isin(["TRANSFER", "CASH_OUT"])].copy()
df["hour"] = df["step"] % 24
df["is_transfer"] = (df["type"] == "TRANSFER").astype(int)
df["pct_of_balance"] = df["amount"] / (df["oldbalanceOrg"] + 1)
cols = ["amount", "hour", "is_transfer",
"oldbalanceOrg", "pct_of_balance"]
train, test = df[df["step"] <= 600], df[df["step"] > 600]
model = HistGradientBoostingClassifier(
class_weight="balanced", random_state=0)
model.fit(train[cols], train["isFraud"])
p = model.predict_proba(test[cols])[:, 1]
The file name may differ on your download, so check it. class_weight="balanced" makes each rare fraud row count for more during training. The time split trains on the first 600 hours and tests on the rest. The feature pct_of_balance says what share of the account the payment drains: takeovers tend to empty it.
Get weekly developer tips
Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.
No spam. Unsubscribe anytime.
Precision asks: of the payments we blocked, how many were fraud? Recall asks: of all the fraud, how much did we catch? Both are counts. A fraud team thinks in shillings, so convert.
Imagine each wrongly blocked payment costs KES 100 in staff time and an angry call. A missed fraud costs the full amount. The code adds both for four thresholds.
FALSE_ALARM = 100 # an assumption: change it to your own
fraud = test["isFraud"] == 1
rows = []
for t in [0.5, 0.8, 0.95, 0.99]:
flag = p >= t
missed = test.loc[~flag & fraud, "amount"].sum()
alarms = int((flag & ~fraud).sum())
rows.append({
"threshold": t,
"precision": (flag & fraud).sum() / max(flag.sum(), 1),
"recall": (flag & fraud).sum() / fraud.sum(),
"kes_missed": round(missed),
"false_alarms": alarms,
"total_cost": round(missed + alarms * FALSE_ALARM),
})
print(pd.DataFrame(rows).round(3))
Read the table from the top. A low threshold blocks more, so recall rises and false alarms rise too. A high threshold is quiet but lets large frauds through. Pick the row with the lowest total cost, then ask whether the alarms it makes are a number your team can actually review in a day.
Now imagine you raise the alarm cost to KES 10,000. Run it again. The cheapest row moves, because each false alarm now hurts more than a missed small fraud. Always show the cost line with the threshold.
For signals taken from a card and wallet gateway, see fraud detection signals on payment data. For another scored-people project, read how to build a chama credit score. To serve the model to other systems, follow deploying a machine learning model with FastAPI. If you want to practise on other public data, see Kenyan open datasets.
Want to build this with mentors and show it at a demo day? See the Data Science to AI Engineering programme: 5 months full time or 8 months part time, KES 140,000, or KES 120,000 if paid in full upfront, next intake 11 January 2027.
KES 140,000 (KES 120,000 upfront)
Data Science to AI Engineering: 5 months full time or 8 months part time, hybrid in Nairobi. Fraud, credit and M-Pesa data from Week 1, and every model deployed. Starts 11 January 2027.
See the Data Science programme