First models on a time-split dataset, scored on the months they never saw.
Open the interactive binning explorer → Move the scorecard's splits yourself and watch the weight of evidence change.
The SecureBank case in EN.705.603 at Johns Hopkins asks for a card-fraud model that reaches 70 percent precision and 70 percent recall on unseen months. I ran this as the model selection pass. I picked two offline metrics tied to the requirements, trained three models on the same chronological split, and read the comparison for where the signal lives. Codex reviewed the first version adversarially and I reworked the leakage paths, the ablations, and the claims it flagged.
I score every model on average precision over the whole ranking and on precision and recall at one operating point. Average precision is my model selection number, since it does not depend on a threshold I have not chosen yet. The operating point is the number I'd show the bank. The alert threshold is set on the validation month at the lowest score that keeps precision at 70 percent, which is the analyst capacity constraint from my earlier posts turned into a rule. A model that never reaches 70 percent on November gets no qualifying operating point; I score it at a fixed 10 alerts a day instead and say so.
The split is the one I argued for in the dataset design week. I train on January 2019 through October 2020, choose the threshold on November, and score December once. The models train on the full training frame with balanced class weights; I kept the 10 percent downsample from my first pass as an ablation and it costs the boosted model 0.03 of average precision. Validation and test stay at the fraud rate among labelled rows, because precision on a rebalanced set is a number the bank never sees. The 10 percent of transactions with no label are out of every metric; their median amount and night share match the labelled rows, which is the only check I have that they are missing at random.
| Partition | Rows | Fraud | Fraud rate |
|---|---|---|---|
| Train (Jan 2019 to Oct 2020) | 1,313,360 | 6,099 | 0.46% |
| Validation (Nov 2020) | 57,919 | 202 | 0.35% |
| Test (Dec 2020) | 111,593 | 195 | 0.17% |
December doubles the volume and keeps the fraud count flat, so the base rate halves. Every precision number I report below was harder to earn than the validation month suggested.
I built seventeen features, all computable at authorization. The amount bands and the gap sentinel were fixed before any fitting. The velocity features use every transaction, including the unlabelled ones, but only rows with a strictly earlier timestamp. The scorecard adds one cross bin, category by amount band.
Numeric: log_amt, amt_vs_prior_mean, n_prev_1h, n_prev_24h, log_secs_since_prev, first_txn, hour_sin, hour_cos, night, dow_sin, dow_cos, month_sin, month_cos, age, log_city_pop, home_merchant_km. Categorical: category, and cat_amt (category by amount band) in the scorecard.
| amount band | fraud rate | fraud rows | rows |
|---|---|---|---|
| $0 to $50 | 0.201% | 1,379 | 685,955 |
| $50 to $200 | 0.034% | 190 | 565,058 |
| $200 to $300 | 1.827% | 598 | 32,723 |
| $300 to $500 | 7.294% | 1,065 | 14,601 |
| $500 to $800 | 9.262% | 762 | 8,227 |
| $800 to $1200 | 45.549% | 2,067 | 4,538 |
| $1200 to $up | 1.683% | 38 | 2,258 |
I fit a logistic regression on the standardized features, a weight-of-evidence scorecard (optbinning bins each numeric feature on the training rows, the category and cross-bin columns get a smoothed WOE per level, and a logistic regression fits on the WOE columns), and a gradient boosting classifier.
| model | AP | 95% interval | precision | 95% interval | recall | 95% interval | alerts per day | dollar recall | reached 70% on Nov |
|---|---|---|---|---|---|---|---|---|---|
| Gradient boosting | 0.828 | 0.76 to 0.88 | 0.567 | 0.44 to 0.66 | 0.841 | 0.79 to 0.89 | 9.3 | 0.94 | yes |
| WOE scorecard | 0.563 | 0.46 to 0.65 | 0.496 | 0.36 to 0.60 | 0.579 | 0.51 to 0.65 | 7.4 | 0.77 | yes |
| Logistic regression | 0.105 | 0.06 to 0.15 | 0.175 | 0.11 to 0.24 | 0.538 | 0.45 to 0.62 | 19.4 | 0.83 | no |
| Model | Dec AP | Precision | Recall | Alerts/day | Dollar recall | Reached 70% precision on Nov |
|---|---|---|---|---|---|---|
| Gradient boosting | 0.83 | 57% | 84% | 9 | 94% | yes |
| WOE scorecard | 0.56 | 50% | 58% | 7 | 77% | yes |
| Logistic regression | 0.10 | 18% | 54% | 19 | 83% | no, scored at 10 alerts a day |
| model | precision at the point | recall at the point | score threshold | oracle recall at 70% precision | qualifying point |
|---|---|---|---|---|---|
| Gradient boosting | 0.567 | 0.841 | 0.877 | 0.79 | yes |
| WOE scorecard | 0.496 | 0.579 | 0.995 | 0.48 | yes |
| Logistic regression | 0.175 | 0.538 | 0.979 | 0.00 | no, 10 alerts a day |
Gradient boosting reaches average precision 0.83 and, at the November threshold, 57 percent precision with 84 percent recall on 9 alerts a day, catching 94 percent of the fraud dollars. The scorecard reaches 0.56 and the raw logistic regression 0.10, and the logistic regression never qualifies. The whiskers are card-cluster bootstrap intervals with the model and threshold held fixed, so they cover the December card mix and nothing else. The precision miss against the 70 percent target has the halved base rate as one plausible cause; ROC AUC barely moves from November to December (0.999 to 0.998), but average precision drops from 0.93 to 0.83 and I have not separated prevalence from drift in the fraud mix.
| feature group | AP drop | sd over 10 repeats |
|---|---|---|
| amount (log_amt, amt_vs_prior_mean) | 0.919 | 0.004 |
| category | 0.811 | 0.016 |
| hour (hour_sin, hour_cos, night) | 0.315 | 0.017 |
| cardholder (age, log_city_pop) | 0.130 | 0.012 |
| velocity (n_prev_1h, n_prev_24h, log_secs_since_prev, first_txn) | 0.058 | 0.007 |
| weekday and month | 0.004 | 0.001 |
| home_merchant_km | -0.001 | 0.001 |
Shuffling feature groups together on November shows what the boosted model relies on. Amount costs 0.92 of average precision, category 0.81, hour 0.31, and nothing else more than 0.13. That is model reliance, not a statement about how the features combine, so I tested the combination directly.
| feature set | December AP | November AP | December ROC AUC |
|---|---|---|---|
| amount only | 0.022 | 0.043 | 0.921 |
| category only | 0.003 | 0.009 | 0.684 |
| amount plus category, additive | 0.019 | 0.033 | 0.923 |
| amount by category cross bin only | 0.369 | 0.513 | 0.966 |
| main effects scorecard | 0.105 | 0.209 | 0.965 |
| scorecard with cross bin | 0.563 | 0.654 | 0.987 |
| gradient boosting, 10 percent legitimate downsample | 0.795 | 0.922 | 0.997 |
| gradient boosting, full training frame | 0.828 | 0.927 | 0.998 |
On the training rows, a $300 to $500 purchase runs 100 percent fraud in grocery_pos and 0 percent in travel, so the same amount means different things in different categories. The ablation makes the point in numbers. A scorecard on amount alone scores 0.02 on December, category alone 0.003, the two added together 0.02, and the single category-by-amount cross bin 0.37. All eleven main effects with category reach 0.11; adding the cross bin lifts that to 0.56. The tree model learns the same interaction and the rest on its own and scores 0.83. That gap is what I took from the exercise.
| feature | information value |
|---|---|
| cat_amt | 8.290 |
| amt | 4.022 |
| amt_vs_prior_mean | 3.320 |
| hour | 1.855 |
| category | 0.572 |
| log_secs_since_prev | 0.479 |
| n_prev_1h | 0.363 |
| n_prev_24h | 0.137 |
| age | 0.039 |
| month | 0.039 |
| dow | 0.022 |
| city_pop | 0.006 |
| home_merchant_km | 0.001 |
Open the interactive binning explorer → See the bins behind the scorecard and move the splits yourself.
| model | night precision | night recall | night alerts | day precision | day recall | day alerts |
|---|---|---|---|---|---|---|
| Gradient boosting | 0.58 | 0.90 | 254 | 0.49 | 0.55 | 35 |
| WOE scorecard | 0.50 | 0.66 | 219 | 0.44 | 0.13 | 9 |
| Logistic regression | 0.33 | 0.58 | 284 | 0.03 | 0.32 | 316 |
On the training rows 85 percent of the fraud sits on the 23 percent of volume between 22:00 and 04:00, a 1.7 percent rate against 0.09 percent by day. Gradient boosting holds 90 percent recall at night and 55 percent in the day on the same threshold. The scorecard raises 9 daytime alerts in the whole month. A model that only works at night is a model a fraudster learns to beat by waiting until morning, so I'd report the daytime slice to the bank separately.
Method notes: scikit-learn 1.9, optbinning 1.0, one run of 115 seconds on a laptop. Thresholds and bins come from the training and validation months only. Intervals resample cardholders, 1000 draws, model and threshold fixed. The oracle recall at 70 percent precision in the tables uses December labels and is an upper bound, not an operating result.