A daily traffic total is easy to compare after midnight and impossible to observe while the day is still running. At noon, the available count mixes two different questions: how busy is this day, and what fraction of a typical day has normally happened by noon? A useful intraday forecast separates those questions instead of extrapolating the current count as if traffic arrived uniformly over 24 hours.
The model below is deliberately small. It combines a smoothed baseline from completed days with a historical hourly profile. It is suitable as a transparent benchmark and often more useful than a complicated model that has never been backtested. It is not a substitute for a full forecasting system when campaigns, incidents, bot traffic, or strong calendar effects dominate the signal.
Separate Daily Level from Intraday Shape
Let the completed daily totals be
\[ D_1,D_2,\ldots,D_n. \]
Their current level is represented by an exponentially smoothed baseline. Starting with \(B_1=D_1\), update it after each completed day by
\[ B_k=(1-\alpha)B_{k-1}+\alpha D_k, \qquad 0<\alpha\leq 1. \]
A larger \(\alpha\) reacts quickly to recent changes; a smaller value produces a steadier baseline. The weighting mechanism and its exponentially decaying memory are derived in the exponential smoothing discussion. The coefficient is a tuning parameter here. It does not become a Kalman gain merely because both updates happen to be recursive.
The historical hourly profile supplies the second component. Let \(p_i\) be the expected share of daily traffic in hour \(i\), normalized so that \(\sum_{i=1}^{24}p_i=1\). After \(h\) completed hours, the expected cumulative share is
\[ q_h=\sum_{i=1}^{h}p_i. \]
Estimate this profile from complete, comparable days. Normalize each day by its own total before averaging the hourly shares; otherwise one unusually large day controls both the level and the shape. Separate profiles by weekday when Monday and Saturday behave differently, and assign events to one explicit time zone before extracting an hour. Daylight-saving transitions need their own policy rather than silently producing a 23- or 25-hour day.
Forecast the Remaining Day
Let \(C_h\) be today's cumulative traffic after \(h\) completed hours. The baseline predicts that a fraction \(1-q_h\) of an ordinary day remains. Adding that expected remainder to what has already happened gives
\[ \widehat{T}_h = C_h + (1-q_h)B. \]
This equation has a direct operational meaning: preserve every visit already observed, then fill only the unseen part of the day from the historical baseline. At the start of the day it returns approximately \(B\); at the end, where \(q_{24}=1\), it returns the observed total exactly.
Another route starts with the naive pace projection \(P_h=C_h/q_h\), then gives that projection weight \(q_h\) and the baseline weight \(1-q_h\):
\[ \begin{array}{rl} \widehat{T}_h &=q_hP_h+(1-q_h)B\\ &=q_h\dfrac{C_h}{q_h}+(1-q_h)B\\ &=C_h+(1-q_h)B. \end{array} \]
The simplified form avoids dividing by a tiny \(q_h\) in the first hours. The underlying assumption is still strong: today's remaining traffic is expected to follow the baseline even when today's observed traffic is already unusually high or low. Treat it as a benchmark, not as an automatic claim that the current anomaly will disappear.
Forecast Explorer
The fixed example below uses seven completed daily totals, one historical hourly profile, and one held-out day. Move the cutoff through the day. The pace projection is volatile while little traffic has accumulated; the remainder model changes more gradually and converges to the held-out day's final total as observations replace assumptions.
Reference Implementation
The functions keep the completed-day input intact and define the cutoff as a count of completed hours. For a partially observed current hour, either exclude it or use a finer profile; treating half an hour as a complete hour introduces a systematic boundary bias.
function exponentialBaseline(completedDailyTotals, alpha) {
if (completedDailyTotals.length === 0) {
throw new RangeError('At least one completed day is required.');
}
if (!(alpha > 0 && alpha <= 1)) {
throw new RangeError('alpha must be in (0, 1].');
}
return completedDailyTotals.slice(1).reduce(
(estimate, total) => (1 - alpha) * estimate + alpha * total,
completedDailyTotals[0]
);
}
function cumulativeShare(hourlyProfile, completedHours) {
if (!Number.isInteger(completedHours) || completedHours < 0 || completedHours > hourlyProfile.length) {
throw new RangeError('completedHours is outside the profile.');
}
const totalWeight = hourlyProfile.reduce((sum, value) => sum + value, 0);
if (!(totalWeight > 0)) {
throw new RangeError('The profile must have positive total weight.');
}
return hourlyProfile
.slice(0, completedHours)
.reduce((sum, value) => sum + value, 0) / totalWeight;
}
function forecastDay({ completedDailyTotals, hourlyProfile, observedSoFar, completedHours, alpha }) {
if (!(observedSoFar >= 0)) {
throw new RangeError('observedSoFar must be non-negative.');
}
const baseline = exponentialBaseline(completedDailyTotals, alpha);
const share = cumulativeShare(hourlyProfile, completedHours);
return {
baseline,
share,
paceProjection: share === 0 ? null : observedSoFar / share,
forecast: observedSoFar + (1 - share) * baseline
};
} Validation and Uncertainty
Choose \(\alpha\), profile groups, and any later refinements with a walk-forward backtest. For each historical target day, fit the baseline and profile using only earlier days, hide observations after hour \(h\), and compare \(\widehat{T}_h\) with the known final total. Repeat this for every cutoff hour. Random train-test splitting leaks future behavior into the past and gives an unrealistically optimistic result.
Report mean absolute error (MAE) by cutoff hour, not only one aggregate score. Also report signed mean error to expose persistent over- or under-forecasting. A useful benchmark comparison includes the smoothed daily baseline \(B\) by itself and the pace projection \(C_h/q_h\); extra complexity is justified only when it beats these simple baselines out of sample.
A point forecast hides operational risk. Store the walk-forward residuals \(e_{d,h}=T_d-\widehat{T}_{d,h}\) separately for each cutoff hour. Empirical residual quantiles, for example the 10th and 90th percentiles, turn the point estimate into an 80% prediction interval. Coverage should then be checked on later data: roughly 80% of final totals should fall inside a nominal 80% interval.
When the Model Breaks
- Profile drift: product changes, seasonality, or a new audience can invalidate old hourly shares.
- Calendar effects: weekdays, holidays, launches, and campaigns need separate features or profiles.
- Instrumentation changes: consent rules, caching, and analytics migrations can create artificial jumps.
- Non-human traffic: crawlers and attacks should be filtered or modeled as a separate process.
- Early cutoffs: low counts make all current-pace evidence noisy; prediction intervals should be widest there.
Once the benchmark fails consistently, a dynamic regression, state-space model, or count model can add weekday, campaign, trend, and uncertainty terms explicitly. Keeping this transparent forecast in the evaluation remains valuable: it reveals whether the larger model learns real structure or merely fits historical noise.