Projects

What I build
when nobody's
asking me to.

I build these because I like building. Outside of work, on my own time — mostly to learn something I haven't used yet. I like picking a new tool or method and seeing what it's actually good for, not just what the docs promise.

Placeholder — parks trip-planner output (an itinerary, agent handoff log, or eval report)
PARKS PLANNERMULTI-AGENT
Placeholder — a forecasting chart from the Iowa liquor demand project
FORECASTINGIOWA LIQUOR
Agent projects
Multi-agent systems
01

National parks trip planner

Built

Four single-purpose agents (seasonality, logistics, budget, itinerary) with typed Pydantic handoffs, no orchestration framework, and three distinct memory types (episodic, procedural, session).

Problem
Multi-agent systems are usually described in the abstract — no system to point to and say “here’s where that handoff actually breaks.”
Approach
Four single-purpose agents — seasonality, logistics, budget, itinerary — each returning a typed schema, never free text. Hand-rolled orchestration: every handoff is a plain function call I can log and inspect, no framework in between. Three memory types kept deliberately distinct: episodic (trips I’ve actually taken, seeded from this site’s parks page), procedural (standing preferences, refined on correction), and session (current run only).
Outcome
An eval suite across four dimensions — constraint satisfaction, feasibility, consistency, preference alignment — that catches a real failure mode: the model “remembering” something in context without acting on it. Tested and passing offline before a single live API call.
4 single-purpose agents 3 memory types 4 eval dimensions
Claude APIPythonPydanticAgent memory
02

Closet & wardrobe recommender

Built

Two stages over one closet: three agents (cataloguing, weather, stylist) that turn a cataloged closet and a forecast into ranked outfit suggestions, and a compatibility graph plus purchase optimizer that ranks candidate buys by how much new outfit variety each one actually unlocks.

Problem
Outfit recommendation and purchase advice are usually solved separately, and the naive version of “what should I buy next” is actively misleading — a second pair of jeans you already own creates dozens of valid item combinations, indistinguishable by raw count from a genuinely useful purchase.
Approach
Three single-purpose agents for stage 1 — cataloguing, weather, stylist — sharing one interface, run(input) → output, and one schema, ClosetItem, that forbids extra fields so a compatibility score can never get written back onto an item record. The weather agent is split three ways: a deterministic API fetch, a rules-based warmth window, and an LLM call reserved for ambiguous days only — a dry 26°C day gets no API call, a day with scattered showers and an 11°C swing does. Stage 2 runs the same closet through pairwise compatibility scoring (color, formality, pattern, gated by hard rules like a formality gap ≥3), enumerates every valid outfit as a clique in that graph, then ranks purchase candidates by outfits gained — discounted to distinct style signatures, not raw combinations, so a duplicate doesn’t read as variety.
Outcome
143 tests, offline, none touching the network. The one that matters most builds a closet with a deliberate structural hole — a formal top and formal shoes with no bottom formal enough to join them — and asserts that formal trousers, which bridge the two, outrank a duplicate pair of jeans: recommended vs. redundant, even though the duplicate alone still produces real combinations. The live paths — recommend and suggest-buy against a real closet and a live API — haven’t produced a run report yet.
3 single-purpose agents 143 tests, offline 2 independent pipelines
Claude APIPythonPydanticComputer visionOptimization
03

Scalable agent evaluation framework

Built

A typed adapter contract for any agent exposing run(input) → output, pluggable scorers — deterministic and LLM-as-judge side by side — and a paired bootstrap regression test on score deltas instead of a raw before/after diff.

Problem
Every agent project I built needed evaluation, and I kept rewriting the same scoring logic from scratch each time — deterministic checks tangled up with LLM-as-judge calls, with no way to tell a real regression from noise.
Approach
A typed adapter contract — a name and a run() — so the framework never learns how an agent works, only that it can call it and get typed output back. Scorers ship as factories, not hardcoded checks: seven deterministic factories (schema validation, numeric bounds, set coverage, sequence integrity, duplicate detection) plus an LLM-as-judge scorer, both behind the same interface, running concurrently under an async runner that holds a semaphore around judge calls so a large suite doesn’t open hundreds of connections at once. Regression detection runs a paired bootstrap on score deltas rather than a raw before/after diff, because eval scores are bounded, skewed, and small-n — exactly where a t-test’s assumptions fail.
Outcome
Its own test suite is real: 80 tests, offline, passing in under a second — including six constructed score-set pairs with known ground truth, such as the same effect size correctly called NO_SIGNIFICANT_CHANGE at n=8 and REGRESSION at n=400. The self-contained toy-agent example runs end-to-end and writes real, diffable run reports. Not yet validated against a live agent — wiring it into the parks planner above, in place of that project’s one-off eval_suite.py, is next.
7 scorer factories 80 tests, offline 3-valued verdict
PythonasyncioPydanticLLM-as-judgeBootstrap statistics
ML projects
Applied ML
04

Time series demand forecasting

Built

A tiered naive → Holt-Winters → global LightGBM production stack wrapped in split-conformal prediction intervals, plus a second Prophet / LightGBM / SARIMAX / Ridge-ensemble bake-off at monthly granularity — both validated on a synthetic fixture with known ground truth before being applied to 73,482 real weekly transactions across Iowa’s top 25 liquor retailers.

Problem
A point forecast alone isn’t actionable, and a model that wins on one lucky train/test split is demonstrating variance, not skill — the only honest test is scoring across many rolling backtest folds, including ones spanning a real demand shock. The dataset also has real gaps (no price, promotion, or macro data), and beating the naive and seasonal-naive baselines was never guaranteed.
Approach
Ran two pipelines to the same validation standard: a production stack (naive → Holt-Winters → global LightGBM → conformal intervals) across 200 store × liquor-type series, rolling-origin backtested over 6 folds spanning the 2020 shock, and an exploration bake-off of five models (naive, Prophet, LightGBM, SARIMAX, Ridge ensemble) across 8 categories, walk-forward validated on a genuinely unseen 12-month holdout.
Outcome
Naive seasonal won 6 of 8 categories on the exploration holdout — Prophet, LightGBM, SARIMAX, and their ensemble were all fairly tried and lost. The production pipeline told the opposite story at weekly granularity, where Holt-Winters beat naive by a real margin (0.47 vs. 0.59 WAPE) with conformal intervals landing at 90.5% empirical coverage against a 90% target.
73K transactions analyzed 6/8 categories naive wins 90.5% interval coverage
PythonLightGBMProphetSARIMAXHolt-WintersConformal prediction
05

Customer journey attribution

Built

Multi-touch attribution — Markov removal-effect and a data-driven LightGBM + SHAP model, benchmarked against classic heuristics — validated on 8,000 synthetic journeys with known ground truth before being applied to 267,084 real journeys from the public GA4 e-commerce dataset.

Problem
Allocating marketing budget across channels requires knowing which touchpoints along a customer’s path actually drove the conversion — without that, spend defaults to last-click or intuition and high-value channels go underfunded. But conversions carry no ground truth for what caused them, so the attribution method itself has to be validated before its numbers can guide those decisions.
Approach
Ran seven attribution methods across two datasets: 8,000 simulated journeys with known channel effects, for ground-truth validation, and 267,084 real GA4 journeys (1.63% empirical conversion rate). Each method was scored on true-effect rank recovery (Spearman ρ), calibration, bootstrap stability, and agreement between the Markov and data-driven approaches.
Outcome
Only the data-driven model recovered the true channel ranking on the simulator (ρ = 0.79); every heuristic scored ≈0, and Markov removal-effect scored −0.18 — anti-correlated, confounded by channel frequency, despite being perfectly calibrated and bootstrap-stable (stability ≠ correctness). On real data the two trusted methods agreed only moderately (ρ = 0.49): heuristics and Markov rank channels by touchpoint volume, while the data-driven model piles credit onto the catch-all “other” bucket — a likely data-quality artifact given the dataset’s obfuscation, not a causal signal; those real-data numbers shouldn’t be trusted until “other” is decomposed. 83% of journeys are single-touch, so multi-touch attribution only applies to the remaining ~17% of traffic.
267K journeys analyzed 7 methods compared ρ 0.79 rank recovery
PythonpandasLightGBMSHAPMarkov chainsSVG data viz