Projects

Built, measured, and written down.

Each project leads with a summary you can read in ten seconds. Open Read more for the technical decisions, the methodology and the results — including the parts that did not work.

01Machine learning2025

Fraud Detection System

An end-to-end fraud-detection system trained and evaluated on more than 13 million card transactions — from imbalanced-data handling and feature design through to threshold selection, model evaluation and a containerised app serving the model in production.

  • Python
  • XGBoost
  • Optuna
  • MLflow
  • pandas
  • SQL
  • FastAPI
  • Docker
Read moreShow less

The problem

Card fraud is a needle-in-a-haystack problem, and the haystack is the point. In the test set, 2,562 of 1,691,468 transactions are fraudulent — a rate of about 0.15%. A model that predicts “legitimate” for every single transaction is 99.85% accurate and worth nothing, which is why accuracy never appears in this project’s reporting.

The asymmetry runs the other way too. A missed fraud costs the chargeback; a false positive declines a real customer at a real till. Neither error is free, they are not free in the same way, and no metric collapses them into one number honestly. So the work was less about squeezing out a score than about being explicit on what was being traded for what.

Approach

The data is the Caixabank Tech synthetic transactions dataset — transactions, cardholders and cards across the 2010s, joined and explored in SQL before any model was fitted.

Feature engineering stayed deliberately close to the transaction: hour, day of week, month and day; days since the account opened; days until the card expires; years since the last PIN change; years since retirement. The date arithmetic doubles as a data-quality filter — rows where a card had already expired but recorded no error, or where a transaction predated its own account opening, are generation artefacts and get dropped. Eight categorical columns (merchant category, merchant city, card brand and type, error codes, and so on) are kept as native categoricals rather than one-hot expanded, since the model handles them directly.

The split is a stratified 80/20 hold-out, stratified so the fraud rate is preserved on both sides — with 0.15% positives, an unstratified split leaves the test fraud rate to chance.

Technical decisions

  • PR-AUC as the tuning objective, not ROC-AUC — the final model scores 0.9900 ROC-AUC, which sounds excellent and means very little here. ROC-AUC rewards ranking the 1.69M negatives correctly, and at this imbalance that is nearly free. PR-AUC (0.7998) only measures the part anyone cares about. Optuna optimised average precision directly, and XGBoost’s own eval_metric was set to aucpr so early stopping agreed with the objective.
  • scale_pos_weight tuned as a hyperparameter, not resampling — no SMOTE and no undersampling. Reweighting the loss leaves the real class prior untouched, so the predicted probabilities stay interpretable and threshold selection remains a meaningful exercise instead of an artefact of a rebalanced training set. The weight was searched over 1–100 rather than pinned at the imbalance ratio, letting the data decide how hard to push.
  • Optuna with a TPE sampler over a 50% stratified subsample — 15 trials of 3-fold stratified CV, with early stopping (50 rounds against a 200-tree ceiling) deciding the tree count per fold. Subsampling for the search and refitting on the full training set was the compromise that made tuning fit the compute available; it is also the most obvious place where more budget would buy more performance.
  • MLflow around every run — parameters, metrics and the serialised model logged per run, so the artefact that ended up behind the API is traceable to the run that produced it rather than to a pickle of unknown provenance.

Results

Evaluated on the held-out 1,691,468 transactions:

Metric Value
PR-AUC 0.7998
ROC-AUC 0.9900
Recall (fraud) 0.854
Precision (fraud) 0.263
F1 (fraud) 0.402

The model catches 85.4% of fraud. It also flags roughly four transactions for every one that is genuinely fraudulent — at production scale that is a lot of friction pushed onto legitimate customers, and the 14.6% it misses is a lot of fraud left on the table. Both numbers are real and neither is hidden here.

What that trade-off should be is not a modelling question. Where the threshold belongs depends on the cost of a chargeback against the cost of a declined customer, and setting it is a conversation with fraud analysts and risk owners, not something to be inferred from a validation curve.

From model to service

The model is not left as a notebook artefact. The pipeline is refactored into importable modules with separated concerns — load, preprocess, tune, train, evaluate — and served behind a FastAPI endpoint with Pydantic validation, with a Gradio front end so the model can be interrogated without writing code. The whole thing is containerised and deployed to Hugging Face Spaces. The Phase 1 SQL exploration also became a public Tableau dashboard, which is a different audience than this page and a useful thing to have had to build.

What I would do differently

The leakage is the honest headline. Several engineered features — avg_amount_cli, trans_count_cli, years_since_pin_change — are computed across the entire dataset before splitting, so each training row carries a trace of information from the test period. The 0.80 PR-AUC is therefore an optimistic estimate, and I would not defend it as a clean out-of-sample number. The correct fix is to compute those aggregates inside the training fold only, or as time-windowed features that can only look backwards from each transaction. That is the first thing I would rebuild.

The split should be temporal. A random hold-out on data spanning a decade lets the model train on the future and predict the past. Fraud patterns drift; a train-on-past, test-on-future split would be harder to score well on and far more indicative of live behaviour.

Tuning was under-resourced. 15 trials on half the training data is a sanity-check budget, not a search. The reported PR-AUC is a floor on what this feature set supports, not a ceiling.

Cardholder attributes are static snapshots. Age, yearly income and per-capita income are recorded as of dataset creation, not as of the transaction — so a 2012 transaction carries 2024’s income figure. It is a property of the source data rather than something the pipeline introduced, but it limits how much any conclusion about income and fraud can be trusted.

02Theoretical research2025

Deterministic Randomness Extraction for Quantum Random Number Generation with Partial Trust

Peer-reviewed research on randomness extraction in quantum random number generators — certifying how much true randomness a device produces under assumptions weak enough to be defensible in practice, and doing it without spending randomness to get randomness.

  • Semidefinite programming
  • NumPy
  • Optuna
Read moreShow less

Why this matters

Every encrypted message you send rests on a number nobody can guess. If an attacker can predict the randomness behind your keys, the cryptography above it is decoration — the algorithm can be perfect and the system still broken.

The catch is that randomness is not something you can inspect. A sequence that looks random passes every statistical test you throw at it whether it came from a quantum process or from a counter run through a good hash function that an adversary knows the seed of. “Looks random” and “is unpredictable to my adversary” are different properties, and only the second one is worth anything.

Quantum mechanics offers a way out, because quantum measurement outcomes are unpredictable as a matter of physics rather than as a matter of ignorance. But that only helps if the device really is doing the quantum thing it claims. So the field’s real question is not “how do I build a random number generator” but “how do I certify one, using assumptions I would be willing to defend when the device was built by someone I do not fully trust.”

The setting

A quantum random number generator’s raw output is not clean randomness. It is biased, correlated, and partly known to whoever might be listening. An extractor is the post-processing step that turns that into shorter, near-perfect randomness.

Standard extractors have an awkward requirement: they need a small amount of randomness to work — a seed. Which means a random number generator needs random numbers to produce random numbers. Deterministic extractors need no seed at all, which breaks the circularity. Classical information theory says they cannot exist for arbitrary sources — but they can if you know something about the source’s structure.

Foreman and Masanes showed this for the device-independent setting, where you assume almost nothing about the hardware and certify randomness from observed correlations alone. That regime is the strongest possible position to argue from, and also the hardest to build: it needs entanglement and loophole-free Bell violations, which is demanding equipment for a component that is supposed to be cheap.

Most deployable generators are prepare-and-measure instead: one device prepares a quantum state, another measures it. Simpler hardware, but you can no longer certify anything from correlations alone — you have to trust something. The interesting question is how little you can get away with trusting.

Contribution

We port the deterministic-extractor construction to the prepare-and-measure scenario, and prove it holds under three different trust assumptions: partial trust in the state preparation, partial trust in the measurement, and a semi-device-independent setting where all you assume is a bound on how much the prepared states overlap.

That last one is the practically interesting case. An overlap bound is a mild, physically checkable assumption about the hardware — much weaker than characterising the device, and much cheaper than going fully device-independent. It is roughly the least you can assume while still getting a proof.

Result

Simulating the protocol on a new family of experimentally relevant behaviours, we get positive key rates from about 7×10³ rounds.

That number is the point. Extraction results are often asymptotic — true in the limit of infinitely many measurement rounds, and silent about any real device, which runs a finite number. Seven thousand rounds is a quantity of data a real experiment produces quickly, so the guarantee applies to hardware rather than to an idealisation of it. Combined with needing no seed, it means a device can certify its own output starting from nothing.

The paper

Deterministic randomness extraction for quantum random number generation with partial trust — Pablo Tikas Pueyo, Tomás Fernández Martos, Gabriel Senno. arXiv:2512.08900, submitted December 2025, revised February 2026. Builds on C. Foreman and L. Masanes, Quantum 9, 1654 (2025).

03Data analysis2024

Most Streamed Tracks

An exploratory data analysis of the most-streamed songs on Spotify in 2023 — what the audio features actually correlate with, and which of the popular claims about them survive contact with the data.

  • Python
  • pandas
  • matplotlib
  • seaborn
Read moreShow less

The question

There is a comfortable story about streaming: make it danceable, make it upbeat, make it energetic, and the numbers follow. Spotify publishes audio features for every track — danceability, valence, energy, acousticness, instrumentalness, liveness, speechiness, BPM — so the story is testable rather than merely repeatable.

So: among the most-streamed tracks of 2023, do a song’s audio features predict how well it does?

Approach

953 tracks, cleaned to 857 — 95 rows carried a null key, and one row had metadata written into the streams field where a number belonged. Dropping nulls outright is the wrong default in general and I would impute here today; on a dataset this size, for this question, it was an acceptable shortcut and is flagged as one.

The more consequential decision was what to treat as the outcome. Total streams is the obvious choice and a bad one: tracks released earlier have had longer to accumulate, and a Taylor Swift release starts with an audience that has nothing to do with the track. So three outcomes get carried through the whole analysis instead of one:

  • streams — total accumulated plays, biased by release date and by fame.
  • in_spotify_charts — peak chart presence, which is closer to how a track performed at the time and largely neutralises the release-date advantage.
  • in_spotify_playlists — how many playlists a track was saved into, the closest thing available to a deliberate signal from listeners rather than from exposure.

What the data showed

No audio feature correlates with success. Not one. Across all three outcomes, no audio feature reaches |r| = 0.12:

Feature vs streams vs charts vs playlists
Danceability −0.101 0.030 −0.103
Valence −0.043 0.037 −0.022
Energy −0.030 0.105 0.040
Acousticness 0.011 −0.064 −0.056
Speechiness −0.113 −0.098 −0.090
BPM −0.002 0.039 −0.019

Danceability is the one most often named as a driver, and its correlation with total streams is negative.

What does correlate is placement. Playlist presence against streams is r = 0.788, and Apple playlists against streams is 0.774 — an order of magnitude above anything the audio features manage. Chart positions correlate strongly across platforms too (Spotify against Deezer 0.582, against Apple 0.552), which says the platforms are largely measuring the same underlying exposure rather than each surfacing something different.

Release year is the strongest non-placement signal, and it points the way you would expect from an accumulating counter: −0.222 against streams and −0.388 against playlist presence. Older tracks have simply had more time. That is a property of the measurement, not of the music.

The distributions are real, and they are not evidence. Top tracks do skew danceable (median 70), energetic (median 66) and almost entirely non-instrumental (median instrumentalness of 0). Major mode outnumbers minor 474 to 383, and C# is the most common key at 115 tracks against D#’s 30. Every one of those is a true statement about the sample. None of them is a claim about what causes success — see below.

The caveat that eats the findings

This dataset is a list of the most-streamed tracks of 2023. Every row already won. Selecting on the outcome and then correlating against it is range restriction, and range restriction pushes correlations toward zero mechanically — so the near-zero audio-feature correlations here are evidence that among hits, sound does not separate the bigger hits from the smaller ones. They are not evidence that audio features do not matter for becoming a hit in the first place. Answering that needs the songs that failed, and this dataset does not contain a single one.

The same logic disposes of the distribution findings. Top tracks are danceable and non-instrumental — but with no comparison group, there is no way to tell whether that reflects what listeners reward or simply what the industry releases. Instrumentalness has a median of exactly 0; a variable with almost no variance cannot correlate with anything.

And the direction of the one strong result is genuinely ambiguous. Playlist presence and streams move together at r = 0.79, but playlists drive plays and plays drive playlist inclusion, and both are fed by promotion. Correlation this strong is an invitation to a causal question, not an answer to one.