Credit Card Fraud Detector
Scores credit-card transactions for fraud. The default model, smote_rf, is a random forest trained on
SMOTE-oversampled data; a Keras MLP (keras_mlp) is included as an alternative. Both were trained on the
ULB credit-card fraud dataset. On the held-out test
split (56,746 transactions, 95 frauds), the default model catches 69 of 95 frauds and raises 6 false alarms,
for a PR-AUC of 0.802 at its tuned threshold of 0.57 (run of 2026-09-25, metrics.json).
Model
- Input: transactions with the Kaggle columns
Time, V1..V28, Amount, as a pandas DataFrame, a CSV path, CSV text, a list of row dicts or{"rows": [...]}. Other columns, such as the labelClass, are ignored. - Preprocessing (
model.to_features):V1..V28are used as-is (the bank released them already PCA-transformed).AmountandTimeare scaled with aRobustScaler(median/IQR fit on the training split,scaler.joblib). This gives 30 float32 features. smote_rf(default,model.joblib):RandomForestClassifierwith 100 trees and scikit-learn defaults. It was fit on training rows that SMOTE balanced to 50/50. SMOTE only runs at training time, so the file holds just the fitted forest. P(fraud) is the forest's vote share. A row is flagged when P(fraud) >= 0.57, the threshold that maximised F1 on the validation split.keras_mlp(alternative,model.keras): Dense 30-64-32-16-1 with ReLU, dropout 0.3 and a sigmoid head (4,609 parameters, float32). It was trained with balanced class weights and early stopping. The class weights push its probabilities up, so its tuned threshold is higher (0.937).config.jsonholds the input schema, each model's tuned threshold and validation PR-AUC, the default model and the library versions.model.pyloads it all (model.load(path).predict(...)).
Output of predict(data, model="best", threshold=None):
{"model": "smote_rf", "threshold": 0.57, "n_rows": 10, "n_flagged": 3,
"rows": [{"row": 0, "fraud_probability": 0.98, "is_fraud": true}, ...]}
threshold=None uses the model's tuned threshold. Pass your own threshold to trade recall against false
alarms.
Usage
Python
from huggingface_hub import hf_hub_download, snapshot_download
import sys
path = snapshot_download("shalev396/credit-card-fraud")
sys.path.insert(0, path)
import model
predictor = model.load(path, device="cpu") # CPU only (scikit-learn + TensorFlow)
csv = hf_hub_download("shalev396/credit-card-fraud", "examples/mixed_transactions.csv", repo_type="space")
print(predictor.predict(csv)) # or a DataFrame / CSV text / list of row dicts
print(predictor.predict(csv, model="keras_mlp", threshold=0.5))
Space API (free, public): the Space exposes
/predict with inputs (csv_file, model, threshold) and outputs [result, seconds, device]; see its README
for curl, @gradio/client and gradio_client examples.
Inference Endpoint: this repo has a handler.py, so it can be deployed as a dedicated API from the
model page (Deploy → Inference Endpoints, CPU hardware is enough; dependencies in requirements.txt).
Request body:
{"inputs": "Time,V1,V2,...,V28,Amount\n<one line of 30 numbers per transaction>\n",
"parameters": {"model": "best", "threshold": 0.57}}
inputs can also be a list of row dicts or {"rows": [...]}. Both parameters are optional. The response is
the predict dict above.
Training
- Data: 284,807 transactions made by European cardholders over two days in September 2013, collected by
Worldline and the ULB Machine Learning Group. Of these, 492 are frauds (0.17 %). The data is downloaded
from Kaggle
mlg-ulb/creditcardfraud, with OpenMLdata_id=1597as a fallback. Exact duplicate rows are dropped so that a transaction cannot appear in two splits, which leaves 283,726 rows and 473 frauds. - Split: stratified 60/20/20 into train / validation / test: 170,235 / 56,745 / 56,746 rows with 284 / 94 / 95 frauds.
- Selection (the test split never influences a choice): for each model, the decision threshold is set to maximise F1 on validation. The default model is the one with the highest validation PR-AUC (average precision). The best scikit-learn variant and the MLP are exported. Test metrics are then computed once for every variant.
- Hardware / time: CPU only. Fitting all 12 candidates took 383 s in this run (6 threads on a desktop
CPU shared with other jobs; 15.6 min wall time for the whole notebook); an earlier 20-thread run of the
old
train.pypipeline took about 3 minutes end to end. - Full code: training/ (the notebook drives every step) · Colab
Experiments
Twelve candidates, all fit on the same train split and scored on the same features:
| Group | Variants | Idea |
|---|---|---|
| Baselines | baseline_logreg, baseline_rf | imbalanced data as-is |
| NearMiss-1 undersampling | nearmiss_{logreg,knn,dtree,rf,svc} | keep only the legit rows closest to frauds (the RBF SVM is Platt-calibrated) |
| SMOTE oversampling | smote_{logreg,knn,dtree,rf} | synthesise frauds up to a 50/50 balance |
| Neural net | keras_mlp | Dense 30-64-32-16-1, balanced class weights, early stopping on a slice of the train split |
Resampling runs inside an imblearn Pipeline, so it only touches the rows a model is fit on. All variants,
ranked by validation PR-AUC (the selection metric). Each variant's test metrics are at its own tuned
threshold; the deployed model is in bold:
| variant | val PR-AUC | threshold | test PR-AUC | test ROC-AUC | test F1 | test precision | test recall |
|---|---|---|---|---|---|---|---|
| smote_rf (deployed) | 0.8839 | 0.570 | 0.8016 | 0.9598 | 0.8118 | 0.9200 | 0.7263 |
| baseline_rf | 0.8769 | 0.330 | 0.7983 | 0.9189 | 0.8161 | 0.8987 | 0.7474 |
| baseline_logreg | 0.8024 | 0.142 | 0.6939 | 0.9558 | 0.7727 | 0.8395 | 0.7158 |
| smote_logreg | 0.7658 | 1.000 | 0.7179 | 0.9582 | 0.7977 | 0.8846 | 0.7263 |
| keras_mlp (exported alternative) | 0.7333 | 0.937 | 0.6838 | 0.9365 | 0.7912 | 0.8276 | 0.7579 |
| smote_knn | 0.6819 | 1.000 | 0.5449 | 0.9100 | 0.7071 | 0.6796 | 0.7368 |
| nearmiss_rf | 0.5952 | 1.000 | 0.5771 | 0.8855 | 0.7262 | 0.8356 | 0.6421 |
| nearmiss_svc | 0.4163 | 1.000 | 0.3554 | 0.9233 | 0.4397 | 0.6739 | 0.3263 |
| smote_dtree | 0.3649 | 1.000 | 0.2683 | 0.8360 | 0.5000 | 0.3975 | 0.6737 |
| nearmiss_logreg | 0.0585 | 1.000 | 0.0474 | 0.8966 | 0.1348 | 0.0906 | 0.2632 |
| nearmiss_knn | 0.0055 | 1.000 | 0.0047 | 0.7811 | 0.0104 | 0.0052 | 0.8211 |
| nearmiss_dtree | 0.0023 | 1.000 | 0.0022 | 0.6168 | 0.0044 | 0.0022 | 0.9368 |

- Why PR-AUC: frauds are 0.17 % of rows, so ROC-AUC barely separates the models.
smote_logregscores a test ROC-AUC of 0.958, close tosmote_rfat 0.960, but its PR-AUC is 0.718 against 0.802.nearmiss_logregscores a ROC-AUC of 0.897 with a PR-AUC of 0.047. NearMiss discards all but a few hundred legit rows, and its precision collapses. - The selection margin is small:
smote_rfbeatsbaseline_rfon validation PR-AUC by 0.884 to 0.877, which is within noise for 94 validation frauds. On test the two models are equivalent: PR-AUC 0.802 vs 0.798 and F1 0.812 vs 0.816. - The MLP stops early after 7 epochs (best early-stopping PR-AUC at epoch 2) and ends below both random forests and plain logistic regression on validation PR-AUC (0.733).
- What changed from the earlier version of this project: the original pipeline ranked models by
test ROC-AUC at a fixed 0.5 threshold. It picked
smote_logreg, which had precision 0.059 and F1 0.111 on its test split, while its plain random forest had F1 0.874 and PR-AUC 0.861. Those numbers come from a documented earlier run of the old pipeline, which used an 80/20 split with duplicates kept and no validation split. They are not directly comparable with the table above, which trains on 60 % of the rows and has no duplicate leakage.
Evaluation
Default model (smote_rf) on the test split, at its tuned threshold of 0.57:
| metric (test) | value |
|---|---|
| pr_auc | 0.8016 |
| roc_auc | 0.9598 |
| f1 | 0.8118 |
| precision | 0.9200 |
| recall | 0.7263 |
Confusion matrix on the test split: 69 of 95 frauds caught (recall 0.726), 6 false alarms among 56,651 legit transactions (precision 0.920).

Limitations
- Only works on this dataset's feature space.
V1..V28come from an unpublished PCA of undisclosed bank features, so the model cannot score raw transactions from any other source. - Old, narrow data. Two days of European card traffic from 2013. Fraud patterns drift, and nothing here was validated on later data.
- Few positives. There are 95 test frauds, so each missed fraud moves recall by about 1 percentage point. Treat the metrics as estimates with wide error bars.
- Scores are not calibrated probabilities. Forest votes come from SMOTE-balanced data, and the MLP
was trained with class weights. Use
fraud_probabilityfor ranking and thresholding only. - Max-F1 is one operating point. When a missed fraud and a false alarm have different costs, choose
your own threshold (the
thresholdargument). Timecounts seconds since the first transaction in the two-day window, so it means nothing for new data.- Educational portfolio project. Not for real payment decisions.
Dataset credit: Dal Pozzolo, Caelen, Johnson and Bontempi, Calibrating Probability with Undersampling for Unbalanced Classification, IEEE CIDM 2015 (Worldline and ULB Machine Learning Group).