How TicketSignal works
TicketSignal forecasts the final attendance of a live show from the tickets sold so far and the hours until the start. It finds past shows that stood in the same position at the same point in their sales and reports how they finished.
By Duncan Wold. Updated October 30, 2026.
The problem
Ticket sales for live events are back-loaded: most tickets sell late. A straight line through sales so far misleads, and a single average of past shows ignores both the room and the timing. Forty tickets sold means something different ten days out than two days out.
The question TicketSignal answers: of all past shows that had about 40 tickets sold about 48 hours before the start, how did they finish?
Where it came from
The method was built in 2018 at PianoFight, an independent San Francisco theater co-founded by the author, as a tool for its operations team. It was released as open source under the MIT license. PianoFight closed in March 2023.
TicketSignal reimplements the method as a Python API with a database and authentication. Porting it exposed five defects in the original JavaScript, such as a mistyped loop variable in the free-ticket filter and an operator-precedence error in the room filter. Each is fixed and documented in the source.
The data
| Room | Capacity used | Shows | Paid sales | Avg. final |
|---|---|---|---|---|
| main | 120 | 1,809 | 78,851 | 43.6 |
| second | 60 | 1,881 | 35,868 | 19.1 |
Included: completed, publicly listed shows that were not free RSVP events, counting paid tickets that were not cancelled or refunded.
Privacy: customer names and emails are dropped at import. Only each sale's timestamp and price are stored. The raw exports are not published and have never been committed to the code repository.
The method
- State. Room, tickets sold and hours until the start, with a tolerance box around them (default 2 tickets, 4 hours).
- Votes. Each past show in the same room whose sales passed through the box contributes its final attendance.
- Probabilities. Bayes' rule turns the counts into a probability for every possible final number. The prior is estimated from the data (empirical Bayes).
- Smoothing. A weighted window (default 10 tickets) removes spikes caused by single shows.
- Output. The point forecast is the distribution's mean. The interval expands symmetrically around it until it holds the requested probability (default 80%). Results never exceed room capacity.
P(final = n | state) = P(state | final = n) x P(final = n) / P(state)
state = (tickets sold +/- tolerance, hours before start +/- tolerance)
With too few matching shows, the service returns a warning or an error instead of a number.
A worked example
Main room, 40 tickets sold, 48 hours to go: 99 of the 1,809 main-room shows passed through that state.
Second room, 20 sold, 72 hours out: 36 tickets, 80% range 23 to 49, from 102 matching shows. Both computed on this data on October 30, 2026.
curl -G https://api.ticketsignal.io/v1/forecast \
-d stage=main \
-d tickets_sold=40 \
-d hours_until_start=48
Does it work? A back-test
Method. Walk-forward: each show is forecast using only shows that started before it. Forecasts at 7 days, 3 days, 2 days, 1 day and 6 hours before the start, with the live service's default settings and no tuning. A forecast is scored when the show had at least 5 tickets sold at that point. Result: 9,656 forecasts for 2,342 shows, February 2016 to February 2023. Baselines: room average, tickets sold so far, and tickets sold so far scaled by the historical ratio of final to sold-so-far at the same point (the ratio rule).
| Out | Forecasts | Error: TicketSignal | Error: best simple rule | 80% range held | Bias |
|---|---|---|---|---|---|
| 7 days | 1,388 | 12.8 | 22.9 | 81% | -2.3 |
| 3 days | 1,806 | 10.3 | 18.6 | 84% | -0.7 |
| 2 days | 1,972 | 9.1 | 15.7 | 85% | 0.0 |
| 1 day | 2,152 | 7.6 | 11.7 | 89% | +0.4 |
| 6 hours | 2,338 | 5.9 | 7.7 | 94% | +0.6 |
| All | 9,656 | 8.7 | 14.7 | 87% | -0.2 |
"Out" is the time before the start. Error is the mean absolute difference in tickets between forecast and actual final attendance. Bias is the mean of forecast minus actual; negative means too low. "Best simple rule" is the best of the three baselines at each time (the ratio rule in the "All" row).
- Error. 8.7 tickets against 14.7 for the ratio rule, 41% lower (95% range 38% to 43%, resampling whole shows). Against the best simple rule at each time: 44% lower at 7 and 3 days, 23% lower at 6 hours.
- Ranges. The 50%, 80% and 90% ranges held 58%, 87% and 93% of the time, slightly wide because an interval is built to hold at least the requested probability. The 80% range held 78% for the main room at 7 days and 81% with fewer than 10 matching shows.
- Bias. Mean error was -0.2 tickets; 53% of forecasts were too high and 44% too low. Forecasts lean low far out (-2.3 at 7 days) and high near the start (+0.6 at 6 hours, 58% too high). For comparison, tickets sold so far was too low 96% of the time, the room average was 14.5 tickets low on average, and the ratio rule had a mean error of +0.3 but was too low 55% of the time.
- Pull toward typical. Weak shows were over-forecast and strong shows under-forecast. Main room: the weakest quarter of shows (average 24 tickets) was over-forecast by 10.7; the strongest quarter (average 89) was under-forecast by 9.8. Second room: +4.4 and -6.1.
- Stability. The 80% range held between 85% and 91% in every year, including 2020 and 2021.
Scope. Retrospective, one venue, shows that took place only. Scored forecasts cover 61% of main-room shows at 7 days and 84% at 6 hours (second room: 30% and 67%), because only shows with at least 5 tickets sold are scored. Baselines are simple rules, not machine-learning models. The scripts are in the project repository.
Using the API
| Endpoint | Purpose |
|---|---|
| GET /v1/forecast | Final-attendance forecast. Parameters: stage, tickets_sold, hours_until_start; optional tix_delta, time_delta_hours, smooth_by, confidence. |
| GET /health | Liveness check with corpus size. |
| POST /v1/beta-signups | Join the beta list. |
The forecast response contains point_forecast, ci_low, ci_high, events_in_state, probabilities (every possible final number) and warnings. Anonymous use is rate limited per address; API keys, issued by hand, raise the limit. Reference: api.ticketsignal.io/docs. Specification (OpenAPI 3): api.ticketsignal.io/openapi.json.
How it is built
- Engine: Python port of the original logic, tested on synthetic shows with known answers plus regression tests for the fixed defects.
- API: FastAPI with strict validation; impossible requests return 422.
- Data: SQLAlchemy 2 and Alembic on PostgreSQL 16; the forecast corpus is held in memory.
- Security: API keys stored as SHA-256 hashes; roles (user, admin, superuser) enforced on the server; per-address and per-key rate limits; stricter limit and a honeypot field on the sign-up form.
- Operations: Docker Compose behind nginx with HTTPS. The database and API port are not exposed. Firewall allows SSH, HTTP and HTTPS only; password logins are disabled.
- Quality gate: 32 automated tests, linting and a container build on every push.
Limits
- One venue, two rooms, one historical period.
- Assumes the future resembles the past. No adjustment for season, day of week, performer, promotion or price.
- Sparse states give wide ranges and warnings.
- Forecasts regress toward typical attendance: weak shows are over-forecast and strong shows under-forecast.
- Accuracy is a range. Two days out, the typical miss in the main room is about 11 tickets on an average attendance of 44.
- Cancelled shows are not in the data.
- Rate limits are held in memory in a single process.
Links
Try the live demo API docs OpenAPI spec Original open-source tool