DEV Community

Bryan Hamilton
Bryan Hamilton

Posted on Originally published at thedjcalendar.com

I Predicted When DJs Will Tour Next With MySQL and a Stats Trick

Every fan asks the same question: "when is [my favorite DJ] coming back to my city?" For a long time, my answer as the guy running The DJ Calendar was basically "check the page and hope." Ticket alerts help, but they only fire after an announcement exists.

Then I realized I was already sitting on the answer. My site tracks upcoming shows for 99 DJs, and every show that passes becomes history. I had 629 past shows across 86 artists sitting in a JSON archive - venue, city, country, date. Touring is one of the most rhythmic things artists do. Festival season is festival season. Album cycles create bursts. The pattern is right there in the data.

So I built a tour predictor. No machine learning, no Python, no third-party API. Just PHP, one SQL table, and a 45-day clustering trick. It now publishes predictions like "our model estimates John Summit's next tour window opens May 2027 - 55% confidence" on his artist page. Here's exactly how it works, including the bug that taught me what "confidence" actually means.

The data I already had

The archive exists because of a different feature: a Tour History section on every artist page, built from past events. The join is the boring-but-critical part. Each event's slug starts with the artist's ID, so grouping shows by artist is a prefix match against the roster - not a foreign key, just string surgery that works.

// 629 past shows, 86 artists, one pass
foreach ($shows as $show) {
    $prefix = explode('-', $show['slug'])[0];
    if (isset($roster[$prefix])) {
        $history[$prefix][] = array(
            'ymd'    => $show['ymd'],
            'city'   => $show['city'],
            'country'=> $show['country'],
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

One rule up front: an artist needs at least 3 shows before the predictor will even look at them. Two data points can't make a rhythm, and I'd rather show nothing than show nonsense. On my 99-artist roster, 38 get skipped for exactly this reason.

The stats trick: shows are clusters, not points

Here's the core insight. A DJ's year doesn't look like a steady stream of dates. It looks like bursts: a two-week European club run, a festival season, an Ibiza residency, a break. Between bursts there are gaps, and those gaps are the signal.

So the first thing the predictor does is compress 47 shows into "tours" - clusters of shows where each show starts within 45 days of the previous one. One pass, no k-means, no elbow charts:

$clusters = array();
$current  = array($shows[0]);
for ($i = 1; $i < count($shows); $i++) {
    $prevEnd = strtotime(end($current)['ymd']);
    $thisStart = strtotime($shows[$i]['ymd']);
    if (($thisStart - $prevEnd) / 86400 <= 45) {
        $current[] = $shows[$i];          // same tour
    } else {
        $clusters[] = $current;           // gap - new tour
        $current = array($shows[$i]);
    }
}
$clusters[] = $current;
Enter fullscreen mode Exit fullscreen mode

Why 45 days? Tuned by eyeballing the data. Long enough that a festival weekend plus its club afterparties count as one tour. Short enough that a summer tour and a winter tour don't smear into one blob. If I were serving jazz residencies instead of EDM tours, I'd retune this number.

From clusters to an actual date

With clusters in hand, the prediction is almost embarrassingly simple:

  • A "gap" is the distance from the end of one cluster to the start of the next.
  • The predicted next window = the end of the last cluster + the average gap.
  • Clamp it so it can never land in the past - a prediction of "last month" is worse than no prediction.
INSERT INTO artist_tour_patterns
    (artist_id, prediction_next_month, prediction_confidence, prediction_reasoning, last_calculated)
VALUES (?, ?, ?, ?, NOW())
ON DUPLICATE KEY UPDATE
    prediction_next_month   = VALUES(prediction_next_month),
    prediction_confidence   = VALUES(prediction_confidence),
    prediction_reasoning    = VALUES(prediction_reasoning),
    last_calculated         = VALUES(last_calculated);
Enter fullscreen mode Exit fullscreen mode

That upsert matters more than it looks. The predictions live in one table keyed by artist slug, and a backstage button runs the whole 99-artist refresh in one pass. Artist pages never run the math - they read the stored row. The heavy work happens when I say so, not on every page view.

Scoring it honestly: the confidence stack

A date without a confidence number is just a guess with good posture. The score is a stack of small, explainable bonuses:

$conf = 20;                                   // it made a prediction at all
$conf += min(40, count($gaps) * 15);          // observed gaps = rhythm evidence
$conf += min(20, $showCount * 2);             // more shows, more signal
$conf += min(15, (int)$avgClusterSize * 3);   // dense clusters = real tours
if (count($clusters) === 1) {
    $conf = min($conf, 55);                   // one cluster = no rhythm observed
}
$conf = min($conf, 95);                       // never claim certainty
Enter fullscreen mode Exit fullscreen mode

Run John Summit through it: 47 shows, all in one giant cluster, zero gaps. That's 20 + 0 + 20 + 15 = 55, capped at 55. His page says "May 2027 - 55% confidence," and the reasoning line reads "Based on 47 recent shows across 1 tour cluster primarily in Europe/Global and North America."

Notice what the stack rewards: not raw show count - repetition of the rhythm. Each observed gap is the pattern happening again. That's the evidence, and evidence is the only thing a confidence number should be made of.

The bug that taught me the difference

The first version of that stack had a bug, and it was the instructive kind: the gap bonus paid out per cluster, not per gap. An artist with 3 clusters has only 2 observable gaps between them - the third cluster's "gap" is in the future, unobserved. Counting clusters gave unearned points for a rhythm that had never repeated yet.

The reasoning text lied too. It said "1 recent shows" for an artist who had actually played 15 recent shows across 3 clusters, because it counted the last cluster's size instead of the totals. After the fix, the same artist's confidence dropped, the text became true, and the high-confidence tier on my predictions page shrank from 51 artists to 27. That drop was the fix working.

The lesson generalizes past this project: if you can't walk through the arithmetic that produced your confidence number, it's a lie with a percent sign attached.

The edge case that keeps it honest

Constant tourers are the best test. Some DJs simply never stop - 47 dates, no 45-day break anywhere. They collapse into a single cluster, produce zero gaps, and get capped at 55.

The model refusing to fake certainty about the hardest case is my favorite property of the whole system. If an artist never pauses, there is no rhythm to learn. Saying "55% and here's why" beats inventing "87%" from vibes.

Serving it without slowing the site down

My artist pages are deliberately fast - fully JSON-driven, no database on the hot path, LCP around 1.2 seconds. I didn't want to give that up for a paragraph of text.

The compromise: the prediction lives in one MySQL row keyed by artist slug. The artist page does a single primary-key lookup inside a try/catch - if the database hiccups, the page renders exactly as before, the blurb just stays quiet. There's also a public hub that ranks every prediction by confidence, which doubles as the place all those artist-page links point to.

Two rules for presenting it: always say "our model estimates," never "he will tour," and always show the reasoning line next to the number. A prediction with visible reasoning is a conversation; one without is a fortune cookie.

When this approach falls apart

Being honest about the limits, because they're real:

  • Fewer than 3 shows - nothing to learn, don't predict.
  • Dense residencies - a weekly Ibiza residency is one fat cluster with no internal structure. The model shrugs, correctly.
  • Life events - album drops, visa problems, a residency renewal. No amount of history predicts a surprise. The confidence cap of 95 is a permanent reminder.
  • The 45-day constant - it's tuned to how electronic artists tour. Different genre, different constant.

It's a heuristic wearing a lab coat, and I'm fine with that. It turns 629 rows of history into 61 useful, honest predictions - with 38 artists correctly left alone - and it recalibrates itself every time new shows land in the archive.

The whole thing is live: every artist page on The DJ Calendar shows a "Tour Radar" box when a prediction exists, and the full ranked list is at thedjcalendar.com/tour-predictions/.


If you enjoyed this, I've written about the rest of the site's guts too: a login form that beat twenty years of PHP, and a search engine with zero server calls.

Top comments (0)