Projects

Selected work

Machine learning, data, and the occasional adventure on the web. Each project taught me something I couldn't have learned any other way.

Data Engineering

Dataset Drift Detector (driftlog)

Public datasets change between releases and nobody writes it down. This writes the changelog for them.

The question. Public datasets get republished every year, and between releases columns get renamed, categories get recoded, units switch. Publishers rarely document any of it. Analysts rebuild on the new file and the numbers move, with no way to tell whether the world changed or the file did. Can a tool write the changelog they never did?

What I built. A Python CLI that compares successive releases of any dataset and flags schema and value changes: rename detection, new or recoded categories, and drift measured with PSI and KS tests. It writes a plain-English changelog and a self-contained HTML report. 334 tests.

What it found. Across 9 yearly releases of the US Department of Education’s IPEDS institutional directory, 2016 to 2024, it found 33 breaking changes. On Our World in Data’s CO2 dataset it caught the real switch from tonnes to million tonnes in November 2021.

33
Breaking changes found
PythonPandasPyArrowpytest

source private

Machine Learning

Lyrics Genre Detection

Can you tell a song's genre from its words alone?

The question. We recognise genre by how a song sounds. But how much of it is already there in the words alone? A chance to point machine learning at something I actually love.

What I built. I cleaned a dataset of song lyrics, then used TF-IDF to score how important each word is to a song relative to every other song, which turns text into numbers a model can read. A Random Forest classifier learned genre from those scores.

What I learned. Some genres have a vocabulary of their own and the model spots them easily. Others overlap so heavily in language that lyrics alone are not enough to tell them apart.

Pythonscikit-learnPandasTF-IDF

View source ↗

Data Engineering

SQL Sales Analytics

Analytical SQL on 1.55 million rows of Brazilian e-commerce data, and four optimization attempts, two of which failed.

The question. What can real transaction data tell you if you ask it properly? I used the Olist dataset: 99,441 orders from 96,096 customers and 3,095 sellers, 1,550,922 rows across 9 tables, and set out to answer business questions rather than just demonstrate syntax.

What I built. A PostgreSQL 17 database loaded by a Python script that streams the CSVs in with COPY and checks every table against known row counts, so a bad download fails loudly instead of quietly giving wrong answers. On top of that, five query files covering ranking (RANK, DENSE_RANK, NTILE), trend analysis with LAG and moving averages, running totals, multi-step CTEs including a recursive one, and five-table joins.

What the data said. Being late wrecks your reviews and being early buys you nothing: ten days early scores 4.32 stars, one day early scores 4.23, but four days late drops you to 1.97 and pushes one-star reviews from 9% to 71%. Separately, 28 sellers out of 2,970 bring in a quarter of all revenue, and 97% of customers never order twice.

What I learned. The findings depend entirely on getting the details right. customer_unique_id is the person while customer_id is issued fresh per order, so grouping by the obvious column makes repeat buyers vanish. Delivery dates are stored at midnight, so comparing timestamps rather than dates mislabels 1,292 on-time orders as late. On optimization I tried four changes and measured each with EXPLAIN (ANALYZE, BUFFERS) over five runs: two worked, two the query planner ignored outright. The failures were the useful part. An index does nothing for a query that reads half the table, and the biggest win was not an index at all but noticing the same 19,015 centroids were being recomputed from a million rows on every single run.

11x faster
Best query speed-up
PostgreSQLSQLPythonWindow Functions

View source ↗

Tooling

Edwing: Productivity Browser Extension

Small tools to make online learning calmer and more focused.

The problem. Studying online means competing with everything else the browser is offering you. I wanted small tools that help without nagging.

What I built. Three browser extensions in JavaScript: a focused search for learning content, a Pomodoro timer with lo-fi music, and a playback-speed controller for video lessons.

The result. Little frictions removed, one at a time. Exactly the kind of tooling I wanted while studying.

JavaScriptBrowser APIs

View source ↗

Data Engineering

HMDA Lending Analytics

Loaded 39.8 million real mortgage records into PostgreSQL, then measured and fixed the slowest queries. The worst one got 177x faster.

The question. At work I handle regulated healthcare data. Would the same rigor hold up in a different regulated domain, at real scale, if I built the entire pipeline myself? I chose the CFPB’s public HMDA mortgage data, 2022 through 2024, because it is big enough to force genuine engineering decisions.

What I built. A normalized PostgreSQL schema holding 39,793,397 mortgage applications, loaded by an idempotent Python pipeline. Rather than dropping bad rows silently, it counts and logs every cleaning decision. That mattered: HMDA hides undocumented placeholder values, including the literal strings "NA" and "Exempt" and a numeric 1111 code, all of which the loader has to catch by name.

Then twelve analytical queries covering window functions, multi-step CTEs, every join type and GROUPING SETS, each one opening with the business question it answers.

The optimization. I profiled the four heaviest queries with EXPLAIN (ANALYZE, BUFFERS), added a materialized view, composite indexes and year-range partitioning, then measured again. One query fell from 5,659ms to 32ms. That is 177x, measured rather than estimated.

What I learned. The real risks were never in the SQL. They were in the assumptions between steps. A missing index turned a routine reload into a multi-hour job. One query looked like a failed optimization until VACUUM ANALYZE refreshed its statistics. A materialized view built one step too early quietly detached from the table it was meant to summarize. None of that surfaces until you run the whole thing end to end and check the numbers against reality.

177x faster
Query speed-up
PostgreSQLSQLPythonTableau Public

View source ↗