Data & AI · CS + Data Science, UW–Madison · built at FHI Heat

Data & AI systems that make decisions —
and prove they're right.

I built FHI Heat's data warehouse from scratch, then most of the tools that ran on it.

~15 siloed systems across the US & EU — Shopify, Amazon (Seller, Vendor, Ads), TikTok Shop & Ads, Meta & Google Ads, Google Analytics & Search Console, SAP, ShipStation, Attentive, Euka — unified into one SQL source of truth with nightly ETL. On that foundation I built a creator-discovery pipeline, an ad-spend decision engine (100% out-of-sample accuracy), an n8n automation, and the executive dashboard leadership ran on.

1data warehouse, built from scratch
15+systems unified (US & EU)
6tools I built on top of it
100%out-of-sample decision accuracy
experience · two engagements
FHI Heat
AI & Data Engineer · promoted from intern
Built the data warehouse from scratch, then most of the ML & BI tools that ran on it.
UW–Madison · People and Robots Lab × Kamp Lab
Voice-AI research · co-authored
Co-authoring the voice layer of SteMy, a voice-first AI assistant for bench scientists.
◆ NIH R01 — scored highly, funding anticipated
skills & tech stack

Technical skills — each one tied to a shipped project

Core: Python · SQL · LangGraph · dbt · BigQuery · Power BI · Next.js · machine learning · causal inference.

AI / LLM

LangGraph agents
RAG · Chroma
RAGAS eval
Claude / OpenAI

Data / Eng

multi-source ETL
dimensional modeling
dbt · BigQuery
Python · SQL

ML / Rigor

buy-intent scoring
causal inference
OOS backtesting
guardrails · eval

BI / Automation

Power BI · DAX
Next.js dashboards
n8n workflows
star-schema modeling
data platform & architecture

The data platform — everything ran on the warehouse I built

Sources → the warehouse → what it powered (solid), plus public rebuilds modeled on it (dashed) and one separate research-lab project. Click any node to jump to that project.

the sale · financeaffiliate growth engineexec visibilitypublic rebuilds · modeled on the warehouseShopifyAmazonAmazon VendorAmazon AdsTikTok ShopTikTok AdsMetaGoogle AdsGA4GSCGMCSAPShipStationAttentiveEukathe auditbuild-vs-buycreator inteldecision enginen8n alerts▸▸exec dashNL→SQLdbtwarehouse · built from scratchSEPARATE ENGAGEMENT — UW PEOPLE & ROBOTS × KAMP LAB · VOICE AI · NIH R01voice AIresearch lab
selected work · by discipline

Every project — what I built, how it works, the impact

The warehouse I built from scratch, and every tool on top of it. Expand any card for the full “how it works.”

View as
Data engineering — the warehouse, built from scratch
The single SQL source of truth every tool below runs on.
WAREHOUSE · ETLSOURCESshopify✓amazon✓tiktok✓meta ads✓google✓sap b1✓WAREHOUSEraw→stg→martsfact_orderssku · spend · shipdim_productsku · channel · dateNIGHTLY PYTHON ETL~15 SOURCES → 1

Multi-source data warehouse — built from scratch

15+ systems → 1
unified warehouse, built from scratch
Data engineeringETL / ELTDimensional modelingData integrationSQLPython
Read how it worksHide details

Designed and built FHI Heat's SQL warehouse from zero — a dbt-style raw → staging → marts model (hundreds of tables/views) unifying ~15 source systems across the US & EU via nightly Python ETL: every commerce, ad, search, ERP, fulfillment, and marketing platform the company runs on. The single source of truth every downstream tool queries.

How it works
  • Sources (~15, US & EU) — commerce: Shopify, Amazon (Seller + Vendor Central), TikTok Shop. Ads: Amazon, TikTok, Meta, Google. Search & analytics: Google Analytics (GA4), Search Console, Merchant Center. Plus SAP B1 (ERP), ShipStation, Attentive, and Euka — each via its own API.
  • Scale & schema — a dbt-style raw → staging → marts model: ~380 tables / ~120 views across 6 schemas, tens of millions of rows (SAP invoice lines and Attentive events alone run into the millions), conformed onto shared dimensions via a cross-marketplace SKU bridge that resolves one physical product across every channel; USD/EUR normalization; nightly Python ETL.
  • Reliability & reconciliation — incremental idempotent upserts, schema-drift handling, snapshot de-duplication, and data-quality checks; cross-source reconciliation (ad spend across platforms vs. the SAP ledger, Amazon Seller vs. Vendor, GA4 vs. orders) into one revenue-and-margin view.
Shopify · Amazon · TikTok · Meta · Google · SAP · ShipStation · Attentive · Euka → nightly Python ETL · raw→staging→marts → one conformed SQL schema
Applied ML, LLM agents & automation — the affiliate growth engine
One end-to-end system, three parts: discovery finds & scores creators → the decision engine calls scale / cut → n8n alerts on every change.
CREATOR DISCOVERYCREATOR DISCOVERYBUY-INTENT#CREATORSCOREINTENT10.90HIGH20.79MED30.68MED40.57LOW50.46LOW60.35LOWanti-bot CDP scrape → transparent intent lexicon → rank

① findCreator-discovery & buy-intent pipeline

vendor-replacing
creator discovery + buy-intent
Web scraping (anti-bot / CDP)PythonPlaywrightData pipelinesNLP heuristics
Read how it worksHide details

To replace a third-party creator-intelligence vendor, I built a standalone pipeline that scrapes TikTok's public creator, video, and comment data — the two things no official API returns: creators outside your own shop (for cold affiliate outreach) and raw comment text scored for buy-intent. It feeds the affiliate outreach team and the decision engine below.

How it works
  • Beating bot-detection (the core problem) — TikTok's comment/video endpoints are heavily defended; signed APIs and headless/headful Playwright all returned 'detecting a bot,' on datacenter and clean residential IPs alike. The breakthrough: the block is on the automated browser itself (navigator.webdriver + launch flags), not the IP — so I drive a genuine Chrome as a normal OS process and attach over the DevTools Protocol (CDP), letting TikTok's own JS sign the requests.
  • Robust capture — read response bodies via raw CDP (Playwright returns empty for the signed endpoints); paginate comments by scrolling the last one into view and expanding reply threads (the naive method silently capped at ~40); a fresh browser context per video so one bad video can't wedge a run (verified 250 comments in ~86s).
  • Explainable scoring & compliance — buy-intent is a transparent regex lexicon (high/med/low label + category, auditable by design — not a black box); public data only, polite pacing, and an opt-out path that deletes stored data and blocks future scraping.
drive real Chrome over CDP (beat bot-detection) → capture comments + replies → buy-intent lexicon → ranked prospects
DECISION ENGINECAMPAIGN DECISIONS100% OOSCAMPAIGNROASCALLSCALETUNECUTSCALECUTbreak-evenROAS vs margin break-even · self-scores its own calls (DiD)

② decideAffiliate ad-spend decision engine

100% OOS
affiliate decision engine · shipped
LLM agents (LangGraph)RAG (Chroma)Causal inference (DiD)Closed-loop self-scoringUnit economicsOut-of-sample backtest
Read how it worksHide details

Synthetic rebuild, live at the demo below. The desk, the campaign-verdict logic, the closed-loop self-scoring, the RAG copilot & the UX are mine; the creator-ranking model was a colleague’s. Every figure is fabricated.

Overview — the daily action queue across campaigns, spark timing & sampling; each card carries the exact move + evidence chips (synthetic 6-section desk)
Overview — the daily action queue across campaigns, spark timing & sampling; each card carries the exact move + evidence chips (synthetic 6-section desk)
Ads Machine — GMV Max campaign verdicts (scale / cut / tune / retarget) against each campaign’s own break-even ROAS
Ads Machine — GMV Max campaign verdicts (scale / cut / tune / retarget) against each campaign’s own break-even ROAS
Proof no dashboard gives you — did the picks actually cause lift? A pre-registered holdout + matched-control difference-in-differences + a rank-IC recommender scorecard
Proof no dashboard gives you — did the picks actually cause lift? A pre-registered holdout + matched-control difference-in-differences + a rank-IC recommender scorecard
Ask across everything — a RAG copilot over comments, captions & live campaign verdicts at once; the cross-source question a seller dashboard can’t answer
Ask across everything — a RAG copilot over comments, captions & live campaign verdicts at once; the cross-source question a seller dashboard can’t answer
Money — unit economics, creator LTV, a margin×cost sensitivity grid & the controlled-test (holdout) proof
Money — unit economics, creator LTV, a margin×cost sensitivity grid & the controlled-test (holdout) proof

The operator desk that told FHI's affiliate managers what to do each morning: which TikTok GMV Max campaigns to scale, cut, or tune, when to spark an organic winner, and which creators to sample next. A deterministic rules layer makes every campaign call (ROAS vs. margin breakeven); an LLM copilot only narrates the verdict and cites the audience evidence. It closes its own loop — every campaign recommendation is archived and later graded (difference-in-differences vs. unchanged campaigns) against what actually happened, then tomorrow's actions re-rank by dollar impact. Shipped and in use (a live page + a weekly one-click report). Honest scope: the upstream creator-ranking/scoring model was a colleague's; my work is the decision desk, the campaign-verdict logic, the closed-loop self-scoring, the RAG copilot, and the full operator UX. Because the production system is private, I rebuilt the whole desk public, live, and open-source (demo + repo below) — so you can click through every view and read the code yourself, on fully synthetic data.

How it works
  • Architecture — three live sources (a 138K-creator roster, the SQL warehouse, and scraped comment text) → a deterministic decision layer (plain SQL + pandas: campaign ROAS vs. margin breakeven → cut/tune/scale; creator rank + eligibility → sample) → a RAG layer that explains each verdict and cites its evidence. 'Code decides, the LLM narrates' — the 138K creators stay in SQL and JOIN at answer time; only comments are embedded (MiniLM → Chroma).
  • Campaign decisions, validated — an out-of-sample backtest recomputes every verdict from pre-cutoff data and measures realized outcomes after, with no look-ahead: 100% profitable/unprofitable classification across four cutoff windows. The 'keep' side rested on few campaigns, so I lean on the robust 'stop' side. (The creator-ranking model's own lift validation was the colleague's work; my desk surfaces its ranks behind eligibility + control guardrails.)
  • The desk's guardrails (mine) — it surfaces the ranking model's ordinal ranks only (never sizes spend from them), hard-excludes protected matched-control and already-seeded creators before any pick is shown, and gates on a 0-video eligibility rule (a large share of 'active' creators had posted nothing). The RAG copilot is RAGAS-evaluated, and a drift signal can hold the daily run. The scoring model itself — feature design, the exploration bandit, retraining — was the colleague's, not mine.
  • Closes its own loop (self-scoring) — the engine archives each day's recommendations and later grades them against outcomes with zero look-ahead and zero manual input: a difference-in-differences check against unchanged campaigns (as control) for whether an action actually moved the number, an adoption/direction check (was the call followed?), and a daily re-validation of its own timing claims that gates alerts on sample size. Every recommended action is ranked by estimated dollar impact per month on one common scale, so the queue is prioritized by money at stake — not noise. Reported platform ROI is treated as total-channel (it claims organic + affiliate orders too) — rank with it, never size spend from it.
SQL warehouse → LangGraph decision engine → out-of-sample dollar backtest → self-scoring loop
N8N · OPS ALERTSschedfetchdiffalertstopno changeschedule → fetch → diff vs last run → alert only on changeerror-handler branch · webhook from env vardaily 08:00

③ alertn8n alerting automation

auto-alerts
no-code automation (n8n)
Workflow automation (n8n)MLOps / monitoringChange detectionLow-code integration
Read how it worksHide details
The n8n workflow canvas — schedule → diff → alert (public repo)
The n8n workflow canvas — schedule → diff → alert (public repo)

The models are only useful if someone acts on them, so I built the alerting layer as an n8n workflow: a daily schedule fans out to four watchers — new high-intent creators in the sampling queue, campaign verdicts flipping, model drift, and the 8–14-day 'spark' window on videos — each polling the deployed engine, diffing against the last run, and alerting only when something changed.

How it works
  • A daily schedule trigger → 4 conditional branches, each hitting the deployed engine's endpoints, diffing the result against a persisted snapshot, and posting a chat alert (Slack/Discord) only on change — no alert fatigue — plus a dedicated error-handler workflow.
  • A light MLOps touch — one branch polls the deployed model's /drift health endpoint, so the same layer flags model degradation, not just business events.
  • Secrets stay out of the flow (webhook read from an env var); the public version runs against a synthetic engine, the internal twin adds a branch relaying the live /alerts.
scheduled trigger → diff vs last run → alert on change → Slack
Financial analytics, BI & decision support
Warehouse-powered decisions — a diligence reconciliation, a build-vs-buy call, and the executive dashboard leadership ran on.
RECONCILIATIONRECONCILIATIONMATCHEDCARRIERSHIPPINGLEDGERΔwallet 1✓wallet 2✓wallet 3✓wallet 4✓570K+ records reconciled line-by-lineto ≈0.5% variance — a bookkeeping mis-class, not missing money

Six-figure books reconciliation (M&A diligence)

→ 0.5% variance
six-figure discrepancy, reconciled
Data reconciliationForensic analysisSQLETL / API integrationFinancial data
Read how it worksHide details

During acquisition diligence, the buyer flagged a large apparent shipping discrepancy — the ERP showed far more paid to the postal carrier than shipping labels were actually bought for. Working solo in the warehouse, I reconciled 570K+ shipping records against the SAP payment ledger and proved it was a bookkeeping mis-classification, not missing money: shipping is prepaid through two carrier 'wallets' that were both being booked under a single vendor. Counting both wallets and both payment streams, spend matched payments to ~0.5% over three years — and the correction was booked.

How it works
  • Cross-source forensic reconciliation — the ShipStation API gives ground truth on every label bought (by carrier and prepaid wallet); the SAP ledger gives what was paid and how it was booked. I joined 'spent' vs. 'paid' month-by-month and in total across both wallets and both payment streams.
  • Root cause — one vendor code was hiding two carriers; only putting shipping ground truth next to the ERP exposed the mapping. I also traced a mid-window vendor switch that had booked some shipping as software subscriptions, and separated booking-lag timing from true variance.
  • Result — 573,866 label records over three years reconciled to a 0.97 month-to-month correlation and ~0.5% residual variance, delivered as a color-coded proof workbook (problem/solution, monthly proof, line-item evidence) an outside diligence team could follow.
570K shipping records → reconciled vs ERP ledger → six-figure gap → bookkeeping fix
BUILD-VS-BUYPER-SKU COST — BUILD VS BUYcost / orderIN-HOUSEFBTVERDICTDO NOT ADOPT~11.6K orders priced individually; FBT wins on only ~16%delivery-speed natural experiment → no traffic lift

Fulfilled-by-TikTok build-vs-buy analysis

a firm "no"
build-vs-buy, backed by data
Cost modelingCausal inference (natural experiment)Decision analysisSQL / API dataData viz
Read how it worksHide details

TikTok pitched its Fulfilled-by-TikTok program (they warehouse & ship your inventory at a higher fee) with claims that faster shipping converts better and gets more traffic. Instead of trusting the brochure, I priced every fee and every claim against FHI's actual first-party data and recommended against blanket adoption — with a per-SKU strategy: don't convert the light-weight brushes (~95% of volume, structurally costlier under FBT), pilot the heavy tools where FBT actually saves, and make TikTok underwrite the unverifiable 'free traffic' claim. Delivered as an interactive dashboard for a non-technical decision-maker.

How it works
  • Per-SKU rate-card simulation — priced 11,593 fee-bearing orders individually (each against its real ShipStation weight and the exact FBT tier table), not a blended average that would hide FBT saving on heavy items but losing on light ones. FBT beat the in-house label on only ~16% of orders.
  • Robustness — TikTok revised the rate card twice mid-analysis; I verified each change only touched 16–50 lb / multi-unit tiers, proving every FHI-relevant tier (≤5 lb) was unchanged, so the verdict survived both revisions.
  • Causal check on the claims — a fast-vs-slow delivery-state natural experiment showed customers didn't punish slower shipping (repeat rate moved ~0.2 pts across a 3.7→6.0-day spread), and I priced the 'free traffic' claim against real ad CAC to reframe it as a concession to extract.
per-SKU rate-card simulation → natural experiment on shipping speed → recommend NO → cost avoided
EXECUTIVE BIEXECUTIVE OVERVIEWBY CHANNEL ▾REVENUE↑MARGIN↑AOV↑AD SPEND↑⚠ FLAGACTIONScut · burner setscale · winnerwatch · stockoutreframed display → decision · caught a data-quality bug

Executive BI dashboard

display → decision
exec BI app (Next.js)
BI / decision dashboardsNext.js / ReactPerformance / cachingProd deploy & authData-quality / SQL
Read how it worksHide details

Below is a public Power BI rebuild on synthetic data — the production dashboard described above was a private Next.js / MSSQL app.

Power BI rebuild · executive overview (synthetic data)
Power BI rebuild · executive overview (synthetic data)
Media spend
Media spend
Customers
Customers
Inventory
Inventory
Creators
Creators
Search terms
Search terms
Conversion
Conversion
Cash
Cash
Star-schema model + DAX
Star-schema model + DAX

The executive BI web app FHI's CEO and operators ran on — daily revenue, margin, channel, inventory, and ad views where every panel ends in a ranked 'Actions' band (cut / fix / scale / watch) plus an alert layer (stockouts, ROAS collapse, creator decay). It reframed the dashboard from display to decision, and surfaced what the DTC-only reports missed: a major wholesale channel, and a large slice of ad spend with no measurable attribution.

How it works
  • Next.js 15 (App Router) over the MSSQL warehouse, panels built only on the marts layer. A cross-channel 'Product 360' rolls one physical product across Amazon, Shopify, TikTok, SAP wholesale, inventory, and ads via the canonical SKU bridge.
  • Performance — heavy views (a ~3.6M-row SAP scan ran ~45s cold) made instant with a two-level cache (server + browser), a global mutex so heavy queries never run concurrently, warm-on-boot + background refresh, and deadlock auto-retry.
  • Production ops — bcrypt + signed-cookie auth with per-IP lockout, and a Windows push-to-live deploy pipeline (robocopy mirror, next build with automatic .next-prev rollback on failure, behind a Cloudflare Tunnel) at ~80s per cycle over ~400 commits.
  • Data-quality — caught a bug where a marketplace's intraday API snapshots were fanning out as duplicate sales (inflating that channel's revenue) and rebuilt the metric from deduplicated source.
warehouse marts → Next.js decision dashboard + Actions bands → production deploy (~80s push-to-live)
Public rebuilds & live deployments — analytics engineering + LLM tooling (synthetic data · public repos)
NL→SQL AGENTASK YOUR WAREHOUSEREAD-ONLY›revenue by channel last 30 daysSELECT channel, SUM(revenue)FROM marts.fct_dailyWHERE date > … GROUP BY 1LIMIT 100 -- forcedRESULT⛔ "delete all orders"blocked — read-only guardrole-scoped views

NL→SQL analytics agent (governed)

5 roles
governed NL→SQL agent · live
LLM agents (LangGraph)Text-to-SQLGuardrails / governanceEvaluation
Read how it worksHide details

One warehouse the whole company can ask in plain English — from an intern to the CEO — each role scoped to exactly what it's cleared to see, every answer grounded in a single source of truth. A LangGraph agent turns the question into governed SQL (read-only, self-repairing) with role-based access enforced in three layers: a schema-scoped prompt, a guard, and per-role DuckDB views (hidden columns physically absent). Public rebuild on synthetic data modeled on my warehouse; live demo with five role logins (ask it to 'delete all orders' and it refuses).

How it works
  • LangGraph agent: NL → SQL → execute → chart, with a guardrail node (read-only SELECT only, forced LIMIT, blocks DML) and a self-repair loop (catch SQL error → feed it back → retry).
  • Role-based access in three layers: a schema-scoped prompt, a guard that rejects out-of-scope columns, and per-role DuckDB views as a physical backstop (hidden columns literally absent).
  • DuckDB engine, Streamlit UI, and an evaluation harness — on synthetic data modeled on my real warehouse schema.
plain-English question → LangGraph → guardrailed read-only SQL → chart + answer, or a refusal
DBT DOCS · LINEAGEMODELSstg_ordersstg_linesdim_productfct_dailyfct_channelLINEAGE13/13 TESTS ✓rawstagingstagingmartmartnot_null · unique · relationships · accepted_valuesdbt-bigquery · CI on every push

dbt + BigQuery analytics warehouse

13/13 tests
dbt + BigQuery, CI-tested
Analytics engineering (dbt)BigQueryData testingCI/CD
Read how it worksHide details
dbt docs — the model lineage DAG (synthetic data · public repo)
dbt docs — the model lineage DAG (synthetic data · public repo)

A retail warehouse modeled in dbt on BigQuery (raw → staging → marts) with data tests, lineage docs, and GitHub Actions CI running the full build + 13 tests on every push. Public rebuild on synthetic data — the named-stack, reproducible version of my production modeling.

How it works
  • dbt on BigQuery: raw → staging (clean and type) → marts (fact_daily_revenue, dim_product with unit margin, channel rollups).
  • Data tests (not_null, unique, relationships, accepted_values) plus auto-generated lineage docs.
  • GitHub Actions CI runs the full dbt build and tests in an isolated dataset on every push (service-account auth).
raw seeds → staging → marts (dbt) → 13 data tests, green in CI

Live deployments

3 public apps
deployed + reachable
DeploymentCI/CDGitHub Pages · Streamlit · Render
Read how it worksHide details

Three public apps you can hit right now — the NL→SQL agent (Streamlit), the decision-engine rebuild (Render), and the dbt warehouse with green CI — plus this site. The status bar pings them every 30 minutes; free-tier demos nap when idle and wake on first click.

Streamlit · NL→SQL agent → Render · decision-engine rebuild → GitHub Actions · dbt CI
Voice-AI research — People and Robots Lab × Kamp Lab (UW–Madison) (ongoing · co-authoring · NIH R01 scored highly)
STEMY · VOICESTEMY — HANDS-FREE● LIVElistening…“log day 3 · replicate 2 · 4.2 mV”open Day 3, replicate 2 ✓CONFIRM BEFORE WRITINGset O₂ = 5% on Day 3, replicate 2 — say yes?YESNOrealtime · webrtc ~700ms

Voice-first lab assistant (SteMy)

NIH R01
People and Robots Lab × Kamp Lab · UW–Madison · ongoing
Voice AI (OpenAI Realtime)Function-callingHuman-in-the-loop safetyReal-time systemsResearch engineering
Read how it worksHide details

SteMy is a hands-free voice AI lab partner for stem-cell researchers. Turning stem cells into heart cells is notoriously irreproducible batch-to-batch — and researchers are hands-busy at the hood during time-sensitive windows, with dozens of variables to track. I build and lead the voice-interaction layer that lets them capture and recall the right experimental variable at the right moment, hands-free. A separate, ongoing UW–Madison research engagement with the People and Robots Lab (Prof. Bilge Mutlu) × the Kamp Lab (Dr. Timothy Kamp); part of an NIH R01 funded by the National Institutes of Health.

How it works
  • Domain-tuned voice UX — protocol- and study-day-aware (it prompts for the variables that matter at that differentiation stage), with lab-reality speech: biology-vocabulary biasing, number normalization, anaphora memory ('that replicate'), hands-free wake-phrase + barge-in, and confidence-gated auto-execute with a clarification fallback.
  • Architecture — full-stack (React 18 + Vite / Python + FastAPI + SQLite); real-time voice via OpenAI's Realtime API over a persistent WebRTC session (~600–900ms), with server-mediated tools: ephemeral session tokens, a per-page tool catalog, and one executor enforcing auth + telemetry. Migrated from a ~1.8–3s turn-based pipeline without a rewrite; the hard state lives in the backend (slot-filling, a navigate_to tool) so the LLM has one narrow job per turn.
  • Safety & shipping — a spoken confirmation gate before any action that changes an experiment record; shipped in phases behind a runtime feature flag (roll back from the browser, no redeploy), covered by Vitest + HTTP smoke-test suites that caught a stale-session bug pre-release.
  • Scope & funding — I build and lead the voice/tools layer; teammates build the agent and the reproducibility 'brain.' Co-author on the methods paper; part of an NIH R01 funded by the National Institutes of Health.
OpenAI Realtime (full-duplex speech) → voice → backend function calls → spoken confirmation before any action