Suyash Sapre

Suyash Sapre

Research

Work I've contributed to in Dr. Ben Rachunok's group at NC State ISE, from January 2026.

R1

Google Maps Review Scraper — NC food access data

Why

Last semester Teresa Gorton and Dr. Rachunok were working on food desert research at NC State, and I got to help by building a Google Maps scraper. Most food desert research measures access in miles. They were looking at it through Penchansky & Thomas's five dimensions of access — availability, accessibility, accommodation, affordability, acceptability — measured through what people actually write in Google Maps reviews. That approach had been demonstrated for Raleigh; the job was to scale it to the whole state.

We ended up covering nearly 5,000 grocery stores across almost 600 NC cities, pulling in over 1.2 million reviews along the way.

How it works

Two stages. First, resolve a universe of stores: 451,296 Overture Maps POIs down to North Carolina, enriched to Google place_ids, fuzzy-matched and manually reviewed, then filtered from 424 raw Google categories down to the 13 that are actually grocery. Second, scrape reviews for the survivors — headless Playwright on Linux VMs, running continuously, restarting itself on crash and draining a batch until it's done.

Overture Maps US POIs                     451,296
 └─ NC filter                             16,680
    └─ GMaps enrichment (place_id)        7,858 matched + 6,607 unmatched
       └─ Outscraper second pass          21,840 results
          └─ Fuzzy match + manual review  4,905 matched / 1,605 rejected
             └─ Category filter           424 GMaps categories → 13 in, 411 out
                └─ Closed-store + place_id dedup
                   └─ Review scraping     1,201,237 reviews
StackPython · Playwright · SQLite (WAL) · Overture Maps · DuckDB · Tkinter · Linux VMs
↑ Index
R2

GeoMatch

Why

Working on the scraper, I kept running into the same problem: I had a list of stores from one source and a list from another, and I needed to know which rows were the same real place. They never match exactly.

"Food Lion #1542, 2410 Wade Ave Ste 100"
"Food Lion, Wade Avenue, Raleigh"
"FOOD LION 2410 WADE AVE RALEIGH NC 27607"

One store. Three records. Zero exact matches. A human can see they're the same in a second; a spreadsheet can't, and I had thousands of them. The existing options either wanted an API key — meaning the addresses leave your machine, which you can't do with someone else's research data — or depended on a C library that's painful to install. So I wrote one that runs offline and installs anywhere.

How it works

It never compares the two strings directly. It takes them apart first.

  • Clean up. Lowercase, strip punctuation, and expand the abbreviations the post office uses — Ave and Avenue, Ste and Suite, N and North — so both sides speak the same dialect.
  • Take it apart. Split each address into labelled pieces: house number, street name, suffix, unit, city, state, ZIP, and the business name.
  • Check the deal-breakers first. Some things can't disagree. Different ZIP, different house number, different state? Rejected immediately, no matter how similar the names look — this is the rule that catches a Dollar General matched to the wrong Dollar General. Matching on all three? Accepted immediately.
  • Then score what's left. Compare each piece separately — how close are the street names, the business names, the coordinates — and combine those into one number between 0 and 1 that means what it says: 0.9 should be right about 90% of the time.
  • Say why. Every pair comes back with a sentence explaining the verdict, like "accepted by rule: house number + street + ZIP/city all match."
StackPython ≥3.10 · pandas · RapidFuzz · scikit-learn · pgeocode · ONNX Runtime · pytest · Apache-2.0
Links Repository
↑ Index
Supply Chain

The end-to-end idea

I've been weirdly fascinated by retail supply chains for a while — how they actually work, and how you'd build the next generation of mathematical models behind resilient, efficient ones. The projects below are me working through that: learning the math, and learning how it's actually done.

Strategic     Where do facilities go?         →  01  facility location
Tactical      What goes on which truck?      →  02  consolidation
Operational   What route does it drive?      →  03  routing / TSP & VRP
Reverse       Where does it go coming back?  →  04  return routing
01

Facility Location

Why

At UPL I got to work on redesigning our 3PL warehouse network strategy — SKU placement across warehouses, import port allocation, and inventory positioning against seasonal demand. I did something similar in school, where our project advised the City of Goldsboro on fire station siting: 8 candidate sites, 2,724 demand points, drive-time and drive-distance matrices, and a hard 4-minute first-truck response standard.

The big takeaway is that "optimal" is a function of which objective you picked. Goldsboro's answer changes depending on whether you minimise total distance, minimise the worst-case response, or maximise the population covered inside 4 minutes. Same 8 candidates, different stations open. That's the whole point of this project.

How it works

Four families of model over the same candidate set, so the answers can be put side by side.

Cost-minimising

  • UFLP — uncapacitated fixed-charge location, exact ILP (PuLP/CBC). Minimise fixed cost plus total transport cost.
  • CFLP — adds capacity constraints per facility. Surfaces the capacity premium: the extra distance overflow demand has to travel once a facility's cap binds. Sweeps capacity levels to show where it starts to hurt and where it goes infeasible.
  • p-median — fix the facility count, minimise total weighted distance.

Coverage-driven

  • LSCP — minimum facilities to cover every demand point within threshold T.
  • MCLP — fix the budget, maximise demand covered within T. Reports the coverage neighbourhood distribution |Nᵢ(T)| so you can see which points only one facility can reach — those are the fragile ones.
  • p-center — minimax. Minimise the worst-case distance any customer travels. Reports the theoretical lower bound maxᵢ minⱼ dᵢⱼ and flags when the solution hits it, so you know it's provably optimal rather than just solver-optimal.

Heuristic

  • Greedy-Add — opens one facility at a time, each time picking whichever candidate saves the most, and stops when nothing saves anything.
  • Greedy-Drop — starts with everything open, closes the least useful.
  • Both benchmarked against the exact ILP: gap and runtime, side by side.

Multi-objective

  • Weighted-sum — normalise distance and time to [0,1], sweep w from 0 to 1, trace the Pareto frontier. Flags the exact weights where the open-facility set flips.
  • ε-constraint — minimise time subject to total distance ≤ ε. Gives the complete frontier rather than just the supported points.
RunsQuarterly to annually. A network redesign is a capital decision, so an hour of solve time costs nothing.
StackPython · PuLP / CBC · pandas · React · FastAPI · Leaflet · OSM Nominatim
↑ Index
02

Consolidation Centres & Cross-Dock Terminals

Why

I've always wondered how Lowe's and companies at that scale actually run consolidation centres — how freight from hundreds of suppliers gets pooled, and how anyone decides what goes on which truck. A consolidation centre makes two decisions and they interact. When do you release a truck? Holding freight fills trailers and cuts cost per hundredweight, but every shipment on the dock is a shipment not at the DC. What goes on the truck? A trailer has a weight ceiling and a cube ceiling, and freight that fills one leaves the other slack — so the second decision changes how many trailers the first one has to pay for.

So I built both on the same generated freight, priced everything off a realistic LTL rate structure, and asked where a consolidation centre stops paying for itself at all.

How it works

The freight cost model first, because it makes or breaks everything else. LTL freight is not linear in weight. A tariff quotes a rate per hundredweight that drops at published weight breaks, and the bracket's rate applies to the whole shipment — breaks at 500 / 1,000 / 2,000 / 5,000 / 10,000 / 20,000 lb, running from $42.00/cwt down to $9.60/cwt. On top of the staircase sits the over-declare rule: a shipment may be billed as if it weighed exactly a higher break point whenever the cheaper rate more than offsets the phantom weight. A 4,800 lb shipment moves as 5,000 lb and costs 23% less than its own bracket says. So the billed cost is the lower envelope of the staircase, not the staircase itself. Model freight as linear in weight and every conclusion about consolidation is worthless, because crossing a break is the whole point of pooling.

  • Dispatch. Freight accumulates and a policy decides each day whether to release: a fixed schedule, a fill threshold, or whichever comes first. The policies differ in a second way that matters more than the trigger — a fill-triggered release ships the full trailers and holds the remainder, while a time-triggered release ships everything, however empty. That asymmetry is what produces the cost-versus-dwell curve.
  • Load build. Vector bin packing, not 3D geometric packing: a shipment is a resource vector (weight, cube), with no orientation or stacking rule. That's the right abstraction for the question being asked — which ceiling binds. Four heuristics, including residual_fit, which picks the shipment whose resource vector has the highest cosine similarity with the trailer's remaining capacity. Since an empty 53' van's residual points at 45,000 lb / 3,800 ft³ — 11.8 lb/ft³ — maximising that cosine means picking freight whose density matches the density the trailer still has room for. It's the formal version of mixing dense and light freight on the same load.
  • Break-even. Landed cost of shipping every supplier direct to the DC, against pooling through a centre first, swept across supplier count, shipment size and geometry. Two choices deliberately made against consolidation: tenders are pooled by (supplier, day) so pooling can't "recover" an LTL penalty I invented, and every leg is priced at min(LTL, TL) so a supplier with enough freight to justify a truck buys one. Supplier→DC distance uses the law of cosines on a random bearing, so the centre is not assumed to be on the way.

One simplification worth stating, because it shapes two of the findings. The rate table scales linearly with distance; real tariffs taper the mileage factor, so real long-haul LTL is much cheaper per mile than this. With linear scaling and truckload at $2.75/mi, a truckload wins above ~1,100 lb on any lane — distance cancels out of both sides — where real crossovers sit at 10,000+ lb. The staircase here therefore prices small tenders, and the outbound leg is effectively a flat charge per trailer.

What it found

Waiting is worth a lot, and the price is a long tail. Shipping to a fill threshold instead of a clock costs $3.83/cwt against $7.44/cwt — −$3.61/cwt (95% CI [−3.74, −3.48], Cohen's d = −9.71), cheaper on all 30 paired freight streams — for 1.68 extra days of mean dwell. But the mean hides the real cost: under a pure fill rule the worst shipment waits 30.8 days while p95 dwell is only 6.0. A fill rule has no upper bound on how long an awkward shipment can sit in the remainder, because there is always something better to load. That's the argument for a hybrid, and it shows up in no average anywhere. The clock rule isn't even monotone: releasing every 7 days is 9.4% more expensive per cwt than every 5, because seven days of arrivals overshoot one trailer and spawn a near-empty second one every cycle.

The weight breaks do not drive the release decision, which is not what I expected. I'd assumed optimal fill thresholds would snap to weight breaks — plateaus and jumps rather than a smooth curve. They don't. At $2.75/mi truckload, only 1.0% of dispatched trailers were cheaper rated as LTL, so the outbound leg is a flat charge and cost per cwt is almost exactly proportional to 1/fill: the correlation across all 510 runs is 0.9993. There's nothing for a threshold to snap to. The staircase is real and it does bind — but on the inbound leg, where individual supplier tenders sit deep in the expensive brackets. Over-declaring binds on 32.7% of tenders and takes 4.6% off the total freight bill.

Which ceiling binds is set by the freight mix, and it flips. Plot every dispatched trailer by weight and cube utilisation and you get an L: trailers pin against one ceiling or the other, almost never both. Sweeping the light-freight share at constant total weight, the binding constraint flips at 43% light freight, where mean freight density is 11.9 lb/ft³ — against the trailer's own break-even density of 45,000/3,800 = 11.84 lb/ft³. That flip point isn't a fitted number; it's the trailer's geometry showing up in the data. It's also where trailer count is minimised, because it's the only mix where both ceilings can be reached at once.

Packer Trailers / 1,000 shipments Weight util Cube util
residual_fit — adaptive 22.73 0.901 0.856
ffd_scalarized(0.5) — fixed 50/50 blend 22.99 0.891 0.846
ffd_weight 26.27 0.780 0.740
ffd_cube 26.29 0.779 0.739

The adaptive packer saves 3.54 trailers per 1,000 shipments over sorting on weight alone (95% CI [2.99, 3.99], −13.5%, d = −2.47; 26 wins, 4 ties, 0 losses over 30 paired instances) and matches the LP lower bound ceil(max(ΣW/45000, ΣV/3800)) on 100% of instances — it cannot be improved on. But my own claim that no fixed rule could match it doesn't hold. A flat 50/50 blend of normalised weight and cube captures 93% of the gain: residual_fit beats it by 0.26 trailers per 1,000 (−1.1%, d = −0.26; 2 wins, 28 ties, 0 losses), and across the mix sweep the fixed blend actually wins one instance. Adapting to the residual is not wrong, it is barely distinguishable.

Fragmentation is a real cost and it lands entirely on the inbound leg. Going from a few dominant suppliers to all suppliers alike, at fixed total volume, drops mean tender weight from 1,306 lb to 1,029 lb and raises inbound cost by +$4.94/cwt (+19.3%, d = 1.71). Outbound fill is untouched at 0.98 — the centre absorbs fragmentation completely on the way out and pays for it completely on the way in.

And the break-even is driven by shipment size first, geography second, supplier count third — spreads of 107, 46 and 24 points of mean saving across the three axes. Supplier count acts almost entirely through tender size: at fixed volume, going from 10 to 100 suppliers cuts what each one tenders, and consolidation improves from +4.0% to +28.9%. It's the same mechanism, not a third one.

γ = supplier→centre / centre→DC Break-even tender weight
0.10 — suppliers clustered at the centre 2,100 – 3,950 lb
0.25 1,200 – 2,100 lb
0.50 780 – 1,200 lb
1.00 — centre no closer than the DC never wins in the swept range

The cost decomposition is the part that changed how I think about this. Averaged over the grid, the consolidated stack is inbound LTL $46.08/cwt, the centre's own fixed cost $54.67/cwt, outbound linehaul $4.38/cwt, handling $3.20/cwt. The fixed cost is the largest single line — larger than all the freight it handles — because at 7,400 lb/day this centre is grossly under-utilised. It's constant per cwt across the grid while direct-ship cost per cwt falls as tenders grow toward a truckload, so that, not the weight table, is what sets the crossover. A siting decision should start with throughput, not with rates.

All freight is synthetic. The rate table's structure — break points, the shape of the taper, the over-declare rule — is representative of published LTL tariffs, but no figure in it is claimed to be measured from any real carrier or shipper, and the linear mileage scaling is a documented simplification carried into the findings above. Nothing here is a rate quote. Every headline number is a paired comparison on identical freight streams with a 95% percentile-bootstrap CI, and every function that consumes randomness takes an explicit generator, so any figure regenerates exactly.

RunsDispatch decides daily, load build runs per outbound wave — so the budget is seconds, and no exact solver is warranted at this scale. The whole study reruns from scratch in about 30 seconds.
StackPython · NumPy · pandas · matplotlib · pytest
Scale40 suppliers · 90-day horizon · 510 runs · 30 seeds per experiment · 95 tests
Not forA load plan. Vector packing answers which ceiling binds; it says nothing about where a pallet physically goes. Dock door assignment, downstream inventory effects, and routing onward from the centre are all out of scope.
↑ Index
03

Routing — TSP & VRP Solvers

Why

The truck is loaded. Now what order does it visit the stops in, and how many trucks do you need? This is the operational layer of the stack above — the travelling salesman problem, then the vehicle routing problem that generalises it once capacity and multiple vehicles come in. I wanted to find out for myself where exact solving stops being worth the wait.

How it works

Three layers, run on the same instances so the answers are comparable.

  • Construction — nearest neighbour, nearest insertion, Clarke-Wright savings, sweep. Fast, no guarantees.
  • Improvement — 2-opt and Or-opt on the constructions, plus capacity-checked relocate and swap moves across routes, since per-route local search can never move work between vehicles.
  • Exact — both models are cutting-plane loops: solve, and if what comes back isn't a valid set of routes, add the constraint it violated and solve again. DFJ with lazy subtour elimination for the TSP; a two-index formulation with lazy capacity cuts (x(E(S)) ≤ |S| − ⌈d(S)/Q⌉) for the CVRP.

What it found

Benchmarked over 150 instances — sizes 10 to 100, 15 seeds each, uniform and clustered layouts. The same cutting-plane idea behaves completely differently on the two problems: DFJ proves a 150-node TSP optimal in about 21 seconds, while the CVRP model already fails to prove optimality on 43% of 15-customer instances inside a 45-second limit.

That gap isn't about size, it's about how well the relaxation describes the problem. Drop subtour constraints from a TSP and what's left is still tour-shaped, so a handful of cuts closes it. Drop capacity constraints from a CVRP and what's left barely resembles routing — the solver proposes overloaded trucks and each cut forbids exactly one violation, so cuts pile up faster than the bound moves. The practical read: for a TSP, just solve it exactly. For a CVRP past about fifteen customers, the exact model's real job is calibrating the heuristics on small instances so you can trust them on large ones.

The result I didn't expect: on the TSP, the better construction heuristic produces the worse final answer. Nearest insertion beats nearest neighbour by a wide margin on its own — 19.9% vs 24.2% above optimal at n=100 — but run identical local search on both and it reverses, to 4.47% and 3.22%. Nearest neighbour's flaw is a few long crossing edges, which is exactly what 2-opt removes; nearest insertion spreads its error thinly as slightly-wrong local orderings that no single move can fix. Judge a construction heuristic on what it leaves for the search, not on its own tour length.

RunsNightly. Routes are cut once a day, so the solver gets hours rather than seconds, which is exactly why exact solving is worth trying here.
StackPython · PuLP / CBC · NumPy · pandas · matplotlib · pytest
↑ Index
04

Return Routing Optimisation

Why

I returned a North Face jacket. It was unworn — wrong size, sent back within a few days — and it still got shipped across the country to a facility so somebody could open the box and decide it was resellable. Everything needed to make that call was knowable before the box moved.

Most retailers route every return to a central warehouse, inspect it there, and only then decide what to do with it. Deciding after the item has already travelled wastes the shipping and lets value decay; the industry loses an estimated 10–20% of a product's value this way. So: decide where a return goes before it ships, not after.

How it works

Three layers, fired at the moment the return label is generated.

  • L1 — predicts a probability distribution over the item's condition from label-time features only.
  • L2 — prices every route in expected dollars: EV(j) = Σₖ P(k) × [R(j,k) − T(j) − P(j,k)]
  • L3 — an integer program assigns the whole day's returns under every facility's capacity limit.

What it found

Policy Net recovery / return Cost / return A-items at full price
P0 — centralised (strawman) $39.46 $15.07 0%
PNEAR — nearest DC (baseline) $40.46 $13.63 0%
P1 — EV routing, mail only $41.57 $11.70 0%
P2 — EV routing + free in-store $45.66 $9.70 29.4%
Oracle — perfect condition info $46.75 $8.10 30.7%

+$5.20 recovered per return over the credible baseline — about $520,000/year at 100,000 returns. Paired over the same returns: 95% bootstrap CI [+$4.92, +$5.48], t = 36.3, Cohen's d = 0.57, with 61.9% of returns routed differently. Using label-time features alone, it captures 83% of the value a perfect-information router would achieve.

Most of that is not the machine learning, and it's worth saying so before anyone asks. The Layer-1 quality sweep doubles as a decomposition: at skill = 0 the classifier is switched off entirely and every return gets the population base rate, so whatever survives is earned by the channel change and the optimiser alone. Free in-store returns plus capacity-aware routing account for +$4.68 — 90% of the uplift. The trained condition classifier accounts for +$0.51 — 10%. Most of the money is a policy change and an assignment problem.

The second output is a capacity plan. The dual on each capacity constraint says what one more unit of daily throughput at that node is worth, which turns a router into a where-to-invest recommendation.

The North Face anecdote is motivation only — it contributed no data. The network, capacities, recovery rates, costs, and the returns themselves are synthetic; parameter ranges are anchored to published industry figures, and no value is claimed to be measured from any real company. Every figure above is scored against the item's true condition, which no model ever saw.

RunsAt label generation, per return — it has to answer in the time a web page takes to load. Only the assignment ILP batches, once a day.
StackPython · PuLP (ILP) · scikit-learn · multinomial logistic · bootstrap inference · LP duality · Monte Carlo
↑ Index
05

Supply Chain Risk Intelligence

Why

Disruptions show up in the news days before they show up in anyone's numbers. I wanted to see whether that gap could be turned into a usable signal — something that tells a supply chain team to look at a corridor or a port before the shipment is already late.

How it works

Multi-source news ingestion via NewsAPI, NLP and GPT risk scoring, corridor and port risk indices, executive heatmaps, and export to CSV, PDF or Power BI.

I'm rebuilding it, because the scoring layer is an LLM asked to rate a headline, and that is not a risk model — no calibration, no base rate, no way for it to be wrong in a measurable direction. The rebuild replaces it with event extraction against a defined taxonomy plus an explicit exposure map, so an index number traces back to a named supply node rather than to a vibe, and the whole thing can be scored against disruptions that actually happened.

RunsContinuously. News arrives whether or not you are ready for it.
StackPython · GPT · NLP · NewsAPI · Streamlit
Links Repository
Demo video
↑ Index
06

Commodity Stockout Risk Platform

Why

Commodity markets price scarcity for a living. If a raw material is about to get tight, the futures curve tends to know before a planner does — so I wanted to see how far you could get using market data as an early proxy for stockout risk.

How it works

Live OHLCV from Yahoo Finance, technical indicators (ATR, RSI, MACD), a unified 0–1 risk index, a Random Forest shock predictor, and GPT-generated summaries on top.

It's being reworked, because the premise is both the interesting part and the weak part: commodity price volatility is a leading signal for some disruptions and a lagging one for others, and this version doesn't distinguish. Until the index is validated against actual stockout events rather than price moves, it's a market dashboard wearing a supply chain label.

RunsDaily, after market close.
StackPython · Random Forest · Yahoo Finance · Streamlit
Links Repository
Demo video
↑ Index
07

PolicyPulse

Why

Say you run an online clothing shop. A big chunk of what you sell comes back, and shipping each return costs you money. So someone runs the numbers and says: start charging $9.95 to mail something back. On a spreadsheet it's obviously right — it saves millions a year.

Then you announce it, and people are angry about it online for a week, and some of them quietly never shop with you again. The spreadsheet had no way of seeing that coming. It knows what things cost. It has no idea how people feel. That's the gap this fills: before you announce a policy, get some sense of how people will take it.

You could just ask them — survey 500 customers, average the answers. The problem is that isn't how these things actually play out. What usually decides the outcome is one person with a big following posting about it, the bargain hunters piling on behind them, and the whole thing snowballing into something none of the individual answers predicted. A survey measures what people think on their own. This tries to measure what happens once they start talking to each other.

How it works

  • First, who does this actually hurt? Not everyone feels $9.95 the same way. If your usual order is $45, that's a fifth of what you spent and it stings. If your usual order is $165, you barely notice. So the model thinks in proportions, not dollars. It also assumes the pain levels off: a $50 fee isn't five times worse than a $10 one — past a point it's just no.
  • Then build a crowd. 500 imaginary customers in five types — loyal regulars, casual shoppers, bargain hunters, people with big followings, and sustainability-minded buyers. Each type spends differently, is annoyed more or less easily, and posts more or less often.
  • Announce it, and let them talk for 45 days. Every day each person reads ten posts and adjusts how they feel. Which ten is the part that matters: six come from accounts with a big following, because loud voices dominate any feed; two come from people like them, because we mostly hear from people who already agree with us; one is simply recent; and one is random, so nobody is completely sealed off. Then they might write something themselves, which everyone else can see tomorrow.
  • Keep "annoyed" and "leaving" as separate things. They aren't the same. Plenty of people think a fee is unfair and keep buying anyway. Tracking them apart is the difference between a useful answer and a useless one.
  • Finally, put a price on it. Feelings aren't a decision. So you feed in your own numbers — how many customers you have, what they spend, your margin, how often things come back — and it turns the mood into money: what the fee saves you, against what the customers you lost were worth, at every price point. The useful output isn't a single figure, it's the point where saving money starts costing you more than it saves.

Two versions, kept honest against each other. There are two simulators doing the same job. In the fast one, customers are just numbers following rules — it's free, it gives the same answer every time, and a full run takes half a second, so you can try hundreds of settings. In the slow one, every customer is a real AI that writes actual posts and replies to other people. It costs $174 and up per run and takes minutes, but it reads the actual wording of the announcement, which is the only way to check whether how you say something changes how it lands.

What it found

The bit that surprised me: casual shoppers turn out to be worth more than bargain hunters, even though far fewer of them leave. Bargain hunters complain the loudest and are the cheapest to lose. The customers who quietly stop coming back are the expensive ones.

Running both simulators is the point. If the expensive one agrees with the cheap one, you can trust the cheap one to do the exploring. On which customer types get hit hardest, they agree 80% of the time. They don't agree on the exact numbers, and I wouldn't expect them to — the fast version knows how everyone privately feels, while the AI version only ever sees what people chose to post, and people mostly post when they feel strongly. So the ordering is the thing to check, not the figures. On those, the two still disagree about which single group has it worst.

The most convincing moment wasn't a number. One of the AI customers, told nothing except that it shops on a budget, wrote this on its own:

"$12.95 return fee?? SERIOUSLY?? When my average order is $25–40, that's 30–50% of what I spent! …5 other retailers I use have FREE returns."

That's the same "judge the fee against what you usually spend" reasoning I'd hand-written into the fast version — arrived at independently, by something that had never seen my code. Two completely different methods landing on the same idea is worth more than any two numbers matching.

RunsOnce per decision, before launch. Minutes per run is fine, and that slack is what pays for the expensive engine.
StackPython · OASIS (CAMEL-AI) · Streamlit · Plotly · PyYAML · Claude · pytest
Scale500 customers · 45 simulated days · half a second per run · 132 tests
Links Repository
Good forWhich option is worse, which customers to worry about, and roughly how much worse it gets as the fee goes up.
Not forQuoting the percentages as predictions. It has never been tested against a policy that actually happened — H&M and Zara both brought in return fees in 2022 and the reaction is on public record, so that's the obvious next job. Until then the numbers underneath it are careful guesses, not measurements. It's a good way to argue about a decision, not yet a way to predict it.
↑ Index
Other fun projects
08

Garmin Wrapped

Why

Every year friends post their Spotify Wrapped; Garmin shows me records and a step-count bar chart — no story, no curation. So I built the narrative layer.

How it works

Rather than showing everything (a dashboard), it answers one question: what are the most interesting things your data revealed this year? Every candidate insight passes two gates before it reaches you — is there enough data to make this claim reliably, and is this fact actually worth one of your ten slides? The survivors are then ordered into a Freytag dramatic arc rather than by score.

  • Prophet-based decomposition for genuine anomaly detection, not "low step day".
  • Granger-gated cross-metric insights — sleep vs. next-day activity, tested before it's claimed.
  • Population comparisons anchored in peer-reviewed epidemiological studies; personality archetypes like Weekend Warrior, Steady Pacer, Cool Engine.
  • gpt-4o for climax slides, gpt-4o-mini for context slides, template fallback when there's no API key.
StackPython · SQLite · Prophet · Granger causality · Streamlit · gpt-4o
Demo video and slides
Garmin Wrapped slide 1 Garmin Wrapped slide 2 Garmin Wrapped slide 3 Garmin Wrapped slide 4 Garmin Wrapped slide 5 Garmin Wrapped slide 6 Garmin Wrapped slide 7 Garmin Wrapped slide 8

Click any slide to open it at full resolution.

↑ Index
09

WolfAttend

Why

A professor and I were talking and we kept coming back to the same complaint: taking attendance in a big lecture makes you choose between speed and honesty. Calling names burns ten minutes. Passing a sheet around means people sign for friends. Putting a code on the projector means it gets texted to whoever's still in bed. Every option is either slow or easy to cheat. We figured there had to be a better way, so I designed one and emailed it to the university.

How it works

Two signals, because neither one is enough on its own.

  • A code that expires. The instructor starts the session and a rotating code appears. Students enter it in under 15 seconds. On its own this is defeated by a screenshot.
  • Proof the phone is in the room. The campus WiFi already knows which access point every device is connected to, and which access point covers which room. Checking that log confirms physical presence without any new hardware, any app, or anyone scanning anything. On its own this is defeated by a VPN.

Together they close both holes: the code proves you're there now, the network proves you're there physically. It checks every device a student has registered, so leaving your laptop at home doesn't count against you, and the output is only ever present or flagged for review — it never marks anyone absent automatically. It lives inside the LMS, so there's nothing new to buy and nothing new for students to install.

LMS (Moodle → Canvas)  →  session start, code issue, roster write-back
Validation layer       →  code check × live device-to-access-point match
Network (Cisco ISE)    →  access-point-to-room map, device presence

What it found

I wrote up the full design and deployment plan and sent it to NC State's IT leadership, for funzies. They agreed secure attendance is a real problem, had a developer review it — and turned it down. NC State is migrating from Moodle to Canvas over the next two years, so there's no appetite for building anything new on Moodle, and they're already looking at secure attendance options for Canvas.

The one that actually matters: their developer pointed out that students mostly use cell data rather than campus WiFi. My whole presence signal assumes they're on the campus network. If they're not, the second factor doesn't exist.

We went back and forth on BLE signals and such, but this one seems like a problem I don't have the skills to solve with Claude Code.

StackSystems design · Moodle / Canvas LTI · Cisco ISE pxGrid · TOTP · RADIUS · FERPA
↑ Index
Tools
10

Payment Tracker

Why

I wanted my Excel budget dashboard to fill itself instead of me typing into it.

How it works

The trick is upstream of the code: I set every bank and card alert threshold to $0.01, so every single transaction emails itself to me. From there it's a parsing job — watch Gmail, pull out amount, direction, counterparty, method, card last-4 and memo, and write the row into the dashboard.

Each provider is a drop-in parser plugin rather than another branch in one function, so adding a bank means adding a file. It deduplicates on message ID and runs at 11pm via Task Scheduler.

StackPython · Gmail API · OAuth2 · openpyxl
↑ Index
11

YouTube Notes Taker

Why

I watch a lot of videos while I'm on the bike, and I wanted notes so I don't forget them.

How it works

It monitors a playlist, transcribes anything new, and writes a structured summary with takeaways and action items on a daily schedule. It prefers the free YouTube captions and only falls back to Whisper when they're missing; it's idempotent and fault-tolerant, and it tracks per-run API cost to a spreadsheet so I can see what the habit costs.

StackPython · yt-dlp · Whisper · OpenAI
Links Repository
↑ Index
12

Spotify Library Comparator

Why

I DJ off a local music library, but I find new tracks on Spotify. Working out which ones I hadn't downloaded yet meant scrolling two lists side by side.

How it works

It compares a Spotify playlist against the local folder and auto-generates a catch-up playlist of everything missing. Filenames never match streaming metadata cleanly, so matching is fuzzy — RapidFuzz over normalised artist–title pairs rather than string equality.

StackPython · Spotify API · OAuth · RapidFuzz
Links Repository
Demo video
↑ Index