Full Stack Software Engineer
Knokr Predictor
Festival Lineup Prediction System
Visit SiteThe Problem
Festival lineups follow patterns — genre affinity, geographic booking corridors, artist touring circuits, and cross-festival co-occurrence. Knokr's existing music discovery infrastructure (3.3M weighted artist connections, Louvain community detection, 1,400+ festival lineups, 52K artists) contained the signal to predict plausible future lineups. The question was whether that signal could be extracted on demand, per festival, without loading the entire dataset into memory or relying on traditional supervised learning.
What I Built
A two-service prediction system built end-to-end and iteratively improved: a Python prediction engine (FastAPI, asyncpg, Redis) that queries pre-computed graph data on demand, and a Next.js 16 frontend for browsing festivals, viewing current lineups, generating predictions, and regenerating for variety. Both services read from the shared Knokr PostgreSQL database, communicate via Redis job queue, and deploy independently on Railway.
The Engine — Three Iterations
Attempt 1: Gradient Boosting Classifier
Built a scikit-learn GradientBoostingClassifier trained on 112K samples across 1,373 festivals. Nine engineered features: appearance count, billing tier trajectory, genre overlap, geographic proximity, co-occurrence, recency, loyalty score, festival frequency, and scene membership. Used CalibratedClassifierCV for probability calibration. Cross-validation accuracy: 84%.
The co-occurrence feature caused data leakage — it directly encoded whether an artist shared festivals with the current lineup, perfectly predicting the training label. After fixing leakage, scene membership absorbed 65% of feature importance. Every artist in the same Louvain community got near-identical scores regardless of festival-specific fit. Training loaded 3.3M rows into memory, blocking startup for 90+ seconds. The model was abandoned.
Attempt 2: Graph-Based Connection Scoring
Replaced the ML model with direct ArtistConnection weight queries. For each lineup artist, found connected artists filtered by genre overlap. Added scene membership scoring and geographic multipliers. Results improved but were dominated by heavily-connected artists who appeared in every prediction regardless of festival identity — a rock festival and a folk festival with one shared artist got similar candidates. The scoring was purely connection-driven; festival context was a filter, not a driver.
Attempt 3: Festival-First Per-Artist Replacement
Inverted the approach: the festival defines the pool, not artist connections. The engine profiles the festival (type, tier, genre distribution from lineup, country distribution), then finds 20 similar festivals in a 90-day window sharing genres. Their lineups form the candidate pool — real artists booked at real festivals in the same season and genre space.
For each lineup artist, the engine finds replacement candidates from the pool at compatible rating levels (±1 of the artist's level, within the festival's tier range) with genre overlap. Candidates are scored by connection weight + genre depth multiplier (2x at full overlap) + country distribution match. Slots are allocated proportional to the lineup's geographic mix — a 30% Spanish lineup yields ~30% Spanish predictions.
A Redis-backed frequency penalty tracks how often each artist appears across predictions (24-hour TTL), applying a 10% penalty per appearance to prevent over-prediction. Scores are sqrt-flattened for more even distribution, then sampled with weighted random selection. Each request gets a fresh seed so regenerating produces a different lineup.
Classification System
Built an artist rating system (1-5 levels) auto-calculated from festival appearance count, with manual override via admin UI. 51,953 artists auto-rated. Integrated with the existing festival tier system (1-6, Local to Legendary). Bulk classification pages for both artists and festivals with inline button controls and autosave. Festival type auto-set from venue count (1 venue = standalone, 2+ = citywide). The classification data feeds directly into the prediction engine's per-artist level matching.
Connection Weight Signals
The ArtistConnection table captures eight types of relationships, each with a different weight maintained by the graph worker in Knokr Base:
| Signal | Weight |
|---|---|
| LOCAL_SCENE (city + region + genre) | 8 |
| SHARED_MEMBER (band members) | 7 |
| SAME_EVENT | 6 |
| SAME_FESTIVAL_DAY | 5 |
| SAME_VENUE | 4 |
| SAME_FESTIVAL | 3 |
| NATIONAL_SCENE (country + genre) | 2 |
| SAME_COUNTRY | 1 |
Current Accuracy
Based on manual review against known lineups:
| Metric | Current | Target |
|---|---|---|
| Genre relevance | ~75% | 90%+ |
| Artist plausibility | ~55% | 75%+ |
| Geographic fit | ~50% | 70%+ |
| Confidence calibration | Poor | Meaningful |
| Regeneration variety | Good | Good |
Key Technical Decisions
- No ML model in production — The graph data already encodes the features a classifier would learn. Querying it directly is faster, more transparent, and festival-specific. The scikit-learn model was built and validated before being replaced.
- Redis job queue — Decouples request from processing. Frontend submits a job, polls for completion. No HTTP timeouts on long-running predictions.
- Per-artist replacement — Each lineup slot gets its own candidate search scoped by genre and level, rather than one global pool ranked by a single score.
- Country-proportional sampling — Hard allocation by geographic distribution prevents international festivals from predicting 100% domestic acts.
- Separate repos, separate services — Engine and frontend deploy independently. No shared build, no coupling.
Next Steps
Confidence calibration is the primary gap — scores cluster at the extremes rather than distributing meaningfully. Rebooking avoidance will penalize artists from the most recent edition. Billing tier prediction will assign headliner/main/support/opener using artist level and festival tier data. Redis pool caching will store the candidate pool per festival so regeneration resamples without re-querying. A supervised ML layer is planned once classification data matures — training on graph scores as features rather than raw co-occurrence data.
Technology Stack
- Python
- FastAPI
- asyncpg
- Redis
- scikit-learn
- Next.js 16
- React 19
- TypeScript
- Prisma 6
- PostgreSQL
- HeroUI
- Tailwind CSS 4
- Railway