Portreeve
explainer · 5 Sept 202613 min read

What credit card fraud detection datasets can't teach you

The public credit card fraud detection datasets model the bank's problem, not a SaaS checkout. What is in each one, and how to build the dataset you need.

You ate a wave of card testing or a batch of chargebacks last week, someone said "train a model", and now you are searching for credit card fraud detection datasets. You will find four within ten minutes. All four are real work by serious people, and none of them contain your problem.

The problem is not class imbalance, which you can read about anywhere. It is label mismatch: what each dataset's authors were paid to detect, and why a card tester hitting your checkout produces a row that every one of them would mark legitimate, or never record at all.

The four datasets everyone finds

ULB / Worldline, on Kaggle. The Credit Card Fraud Detection dataset was published by the Machine Learning Group at Université Libre de Bruxelles from a collaboration with Worldline, a European payment processor. The Kaggle page describes it as two days of European card transactions from September 2013, with 492 frauds out of 284,807 transactions, so 0.172% positive. The paper it cites (Dal Pozzolo, Caelen, Johnson and Bontempi, 2015) is about calibrating probabilities after undersampling, with the dataset as one of its test cases. The label means the issuer or processor flagged the transaction as fraud after the fact.

IEEE-CIS, from Vesta. The 2019 Kaggle competition used data from Vesta, a payment guarantee provider for large e-commerce merchants. Vesta's own data description thread defines the label: a reported chargeback on the card is fraud, transactions after it linked by account, email or billing address are fraud, and anything not reported within 120 days is legitimate.

PaySim. A simulator by Lopez-Rojas, Elmir and Axelsson, described in a 2016 paper as generating mobile money transactions "based on a sample of real transactions extracted from the logs of a mobile money service implemented in an African country". Transactions are cash-in, cash-out, debit, payment and transfer. The fraud label is injected by the simulator.

Sparkov. Brandon Harris's Sparkov_Data_Generation builds fake customers and merchants with the Faker library and drives them with JSON demographic profiles. The profiles directory contains a fraud_*.json counterpart for each demographic, which is the entire definition of fraud in the set. The Kaggle export covers 1,000 customers and 800 merchants over 2019 and 2020.

Two real, two synthetic. One models a card issuer's view, one a payment guarantor's, one an African mobile wallet, one a Python author's imagination. Hold that list against what you are trying to catch.

The ULB set answers the bank's question

Every feature in the ULB set except Time and Amount is a principal component. The Kaggle page says the original features were PCA-transformed for confidentiality, so V1 through V28 are linear mixtures of columns you will never see. You cannot tell which one was the merchant category, which one was the country, or whether the cardholder's history was in there at all.

That works for what the authors needed, a benchmark for comparing classifiers under extreme class imbalance. It makes the set worthless for feature engineering, because you cannot engineer features you cannot name.

More important is what the label encodes. A fraud flag at a processor like Worldline means a real cardholder's card was used by someone else and the purchase was later identified as unauthorized. A classifier trained on it learns to answer "does this purchase look like the person who owns the card". That is the bank's question, and the bank has the card's full history and eats the loss when the answer is wrong. You care whether the account being created is going to pay you, abuse your free tier, or chargeback in 60 days.

The 99.9% accuracy number deserves one paragraph, because you will see it in every notebook. With 0.172% positives, a model that predicts "legitimate" for every row scores 99.83%. Hayat and Magnier's 2025 critique of the methods published on this dataset goes further: they show a minimal neural network with deliberate data leakage reaching 99.9% recall, and argue that published results are routinely inflated by preprocessing before the train/test split and by ignoring time order. The Kaggle page itself recommends area under the precision-recall curve instead of accuracy. If someone hands you a 99.9% figure from this set, they have told you nothing about the model and something about their pipeline.

The IEEE-CIS fraud detection dataset is closer, and still not yours

The Vesta data is the one worth understanding, because it is the closest a public set gets to an online checkout. It has an identity table with DeviceType, DeviceInfo, and 38 anonymised id_ columns. It has P_emaildomain and R_emaildomain for purchaser and recipient email domains. It has card1 through card6, address fields, and hundreds of Vesta-engineered velocity and timedelta counters.

Three things break it for you.

The columns are anonymised. card1 is an integer that is probably a BIN-like identifier, C1 through C14 are counts of something, D1 through D15 are time deltas from something. The winning team got there by reconstructing a client identifier out of card1, addr1 and D1, then aggregating everything by it. You would be doing archaeology on someone else's schema instead of looking at your own.

The base rate and mix belong to Vesta's merchants. The Fraud Dataset Benchmark paper from Amazon puts the training split at 3.5% fraud, 20 times the ULB rate and well above what a subscription business sees at checkout. A model trained at 3.5% will have its threshold calibrated for a world you do not live in. The same paper's limitations section says it plainly: the datasets "do not carry any information about real fraud", and using a model trained on them to make decisions "could lead to false accusations".

And the label is a chargeback. isFraud=1 requires a reported chargeback, and anything unreported after 120 days becomes isFraud=0. The thread acknowledges that fraud a cardholder never noticed is labelled legitimate. That gap is exactly where your attackers live.

Synthetic credit card fraud datasets model the author's imagination

PaySim's paper is candid about its scope. The authors write that "the injection of malicious fraud behaviour and the application of different fraud detection methods are outside the scope of this paper and are the topics for further work". The fraud in the released dataset is a later addition: agents that take control of a customer's account and empty it by transfer and cash-out. Whatever the fraud column captures, it is a mobile wallet with a transaction menu of five verbs, none of which is "signup" or "start trial".

Sparkov is more direct about it. Fraud is a profile file. fraud_adults_2550_female_urban.json describes how a fraudulent transaction stream for that demographic behaves: transactions per day, day-of-week distribution, amount distributions per merchant category. The generator draws from those distributions and labels the output is_fraud=1. A model trained on it learns Brandon Harris's priors about fraud, faithfully, and nothing else.

Synthetic sets have one legitimate use for you: exercising a pipeline. If you need a million rows with the right column types to test that your feature store, your streaming job and your model server do not fall over, generate them. Never take a threshold, a feature importance, or a recall number from them into production. The thing you would be measuring is how well you recovered the generator.

The label you need does not exist in any of them

Consider what a card tester does to a SaaS checkout.

They have a list of a few thousand stolen card numbers and want to know which are live. The cheapest way to find out is to attach each card to a customer without charging it. Stripe's SetupIntent exists to "set up and save your customer's card without immediately collecting a payment". Stripe's own card testing guide says card setup is "a method preferred by fraudulent actors, because card validation and authorizations during card setup don't typically show up on cardholder statements". The tester gets an issuer response per card. The cardholder sees nothing.

The second-cheapest way is a ladder of small charges, $1.03 here, $0.87 there, the kind of amount nobody disputes. Most fail, which is the point. The few that succeed get refunded by you or ignored by the cardholder. During the 2022 wave Stripe reported blocking more than 20 million card testing attempts a day at the peak. The card testing post walks the ladder itself; what matters here is where those attempts land in a dataset.

In the ULB set, a $0 authorization is not a transaction and does not appear. A successful $1.03 charge that nobody reported is Class=0. In IEEE-CIS, no chargeback within 120 days means isFraud=0, so a probe ladder is labelled clean by definition, and a SetupIntent never enters a transaction table at all. PaySim has no card-validation verb. Sparkov's fraud profiles were written by hand and do not contain one.

Free-tier and trial abuse is worse. Someone spinning up 400 accounts on disposable email domains to farm your AI credits never touches a card. There is no transaction, no processor, and no row in any credit card fraud dataset, real or synthetic, because the entire category of dataset is indexed on a card payment happening.

Your two most expensive abuse patterns are structurally absent from these sets: one is labelled legitimate because nobody complains, the other is missing because no money moved. A classifier trained on any of them, tuned to any recall you like, will wave both through. This is why "download the Kaggle set, train XGBoost, ship it" is bad advice for a software business. The model will not be bad at your problem. It will be solving a different one.

Your event log is the dataset

The dataset with the right features and the right definition of fraud already exists, or will as soon as you start writing it down. It is your own event log, and the label is whatever cost you money.

Record five moments. At signup: email, the hashed device fingerprint from the browser, the IP and what kind of IP it is (residential, datacenter, VPN exit), the timestamp. At trial_start: the same, plus which plan. At checkout_attempt: the processor's card fingerprint, the card's funding type, the amount in minor units, the outcome from the processor, and whether this device has presented other cards. At trial_convert: whether the trial became paid. At login: whether the device matches anything in the account's history.

Hash the identity keys. You do not need the raw email or the raw device string to count how many accounts share one; you need a stable key. Never store card numbers. The processor's fingerprint is what links two accounts using the same card, and it is all you will ever need.

Then label backwards from the money. Every charge.dispute.created webhook from your processor is a chargeback label on the checkout event that created the charge, and by cluster linkage on the signup that created the account. Each of those cost you a $15 dispute fee on Stripe whether or not you win, so do not throw the label away. Every refund you issued because the charge looked wrong is a confirmed_abuse label. Every account you banned by hand is one. Every time you flagged something and it turned out to be a real customer, that is a false_positive label, and it is the most valuable one you have, because no public set can give it to you. This is the same shape as Vesta's rule, except the linkage keys and the fraud definition are yours.

Portreeve is the tool I built for this loop. One call at each of the five events returns allow, review or block with reason codes, and the feedback API lets you report confirmed_abuse, false_positive or chargeback on any event, idempotent per event and outcome, so the labelling above is a single line after the fact. Because review never blocks the end user and a later deny arrives by signed webhook, flagging is free to your customers, and a confirmed label marks every account linked to that one across hashed email, device, card, phone and payer wallet. The event payload reference covers the fields above, and the free tier screens 1,000 events a month with no card.

Whether or not you use a service, start writing the log today. Six months from now the argument about whether to train a model will be settled by the data, and you will only have the data if you started collecting it before you needed it.

One thing the ML-first advice gets backwards: at your volume, rules will beat the model for at least the first year. A rule like "more than three distinct card fingerprints from one device in an hour" encodes the mechanism directly, needs zero labelled examples, and is legible when a customer emails asking why they were blocked. A model needs a few thousand positive labels before it stops memorising the last attack, and if you have a few thousand confirmed abuse events, you have bigger problems than model selection. Until then, velocity checks keyed on device, card and cluster rather than IP are the highest-return code you can write, because shared egress makes per-IP counts lie in both directions. The reason codes reference is one catalogue of which counters are worth keeping, whether you use the service or not.

When a model starts to make sense

There is a point where the log outgrows rules. Your rule file has more than a few dozen entries and you are afraid to delete any. False-positive labels are arriving at a rate that suggests two rules are fighting. The attacks have adapted to the thresholds and you are chasing them weekly. At that point your own labelled events, with your own features and your own base rate, are the only training set that makes sense.

The public datasets still earn a place then, as reading material. IEEE-CIS is a catalogue of feature ideas: counts per card per window, time since first seen for each identity key, email domain frequency, device-to-account cardinality. The Dal Pozzolo paper explains what undersampling does to your predicted probabilities and how to correct the threshold afterwards. Hayat and Magnier are a checklist of leakage mistakes. None of them are training data for you.

Keep the rules running underneath the model. Rules encode mechanisms that hold regardless of traffic mix; the model learns the mix. When the mix shifts, and it will the first time you land on a fraud forum's list of soft targets, the rules stop the bleeding while you relabel.

If you want the event log and the labelling loop without building either, create a free Portreeve account. No card, 1,000 screened events a month, and every verdict you feed back becomes a labelled row you own.

← Back to all posts