seasonal-forecast
v1.2.0
Published
Zero-dependency demand forecasting for seasonal businesses. Pure statistics, no ML, no cloud costs.
Maintainers
Readme
seasonal-forecast
Zero-dependency demand forecasting for seasonal businesses. Pure statistics, no ML, no cloud costs.
Built and battle-tested at TourOperation.com — powering demand predictions for tourism operators processing thousands of bookings across multiple years.
Why?
Most forecasting libraries are either:
- Too heavy — TensorFlow, Prophet, ARIMA models requiring Python, GPU, or cloud APIs
- Too generic — Built for stock prices or IoT, not seasonal businesses
seasonal-forecast is purpose-built for businesses where:
- Demand follows weekly patterns (weekdays vs weekends)
- Demand follows seasonal patterns (summer vs winter)
- You have 1-5 years of historical data
- You need predictions now, not after training a model
Features
| Feature | Description |
|---------|-------------|
| DOY-Window Forecasting | Compares same time-of-year across all historical years using a configurable day-of-year window |
| 3-Tier Historical Fallback | DOY → Month → Seasonal ratio — always produces a value, even with sparse data |
| DOW-Aware | Respects day-of-week patterns (e.g., restaurants closed Mondays, tours only on weekends) |
| Recency Weighting | Recent years count more than older ones (2024 data matters more than 2021) |
| Completion Rate Prediction | "15 people booked 7 days out → predicted final: 38 people" based on historical booking velocity |
| Season Multipliers | Define high/low seasons with custom multipliers |
| Demand Matrix | Grid view of items × dates with color-coded demand levels |
| Anomaly Detection | Z-score based detection with severity classification (mild/moderate/extreme) |
| Time Series Decomposition | Additive decomposition into trend, seasonal, and residual components |
| CSV/JSON Import | Auto-detecting importers with flexible column mapping and date format handling |
| State Serialization | Export/import forecaster state for caching patterns to file or database |
| Last Year Comparison | DOW-aware comparison with same period last year |
| Forecast Accuracy | Score predictions with MAPE, RMSE, MAE, bias, and hit rate metrics |
| Multi-Item Correlation | Pearson correlation to identify complementary and substitute products |
| Holiday Calendar | Built-in holiday presets (US, EU, TR, international) with proximity matching |
| Incremental Updates | Stream new data without full reload via addRecords() |
| CLI Tool | Command-line interface for quick analysis without writing code |
| Zero Dependencies | Pure TypeScript, works in Node.js, Bun, Deno, and browsers |
Install
npm install seasonal-forecastQuick Start
import { SeasonalForecaster } from 'seasonal-forecast';
const forecaster = new SeasonalForecaster({
windowDays: 3, // ±3 days around target day-of-year
minDataPoints: 3, // minimum data points to produce a forecast
});
// Load your historical data
forecaster.loadHistory([
{ itemId: 'sunset-cruise', date: '2023-06-15', quantity: 42 },
{ itemId: 'sunset-cruise', date: '2023-06-16', quantity: 38 },
{ itemId: 'sunset-cruise', date: '2024-06-14', quantity: 45 },
{ itemId: 'sunset-cruise', date: '2024-06-15', quantity: 51 },
// ... years of daily data
]);
// Get a single forecast
const forecast = forecaster.forecast('sunset-cruise', '2026-06-15');
console.log(forecast);
// {
// itemId: 'sunset-cruise',
// date: '2026-06-15',
// predicted: 48,
// confidenceLow: 34,
// confidenceHigh: 62,
// confidence: 'high',
// method: 'doy_weighted',
// factors: { base: 46.2, window: '+-3d', ... }
// }Core Concepts
1. Pattern Analysis
Analyzes your entire dataset to extract demand patterns per item:
const patterns = forecaster.analyze();
for (const pattern of patterns) {
console.log(`${pattern.itemId}:`);
console.log(` Average: ${pattern.overallAverage} per day`);
console.log(` DOW coefficients: ${pattern.dowCoefficients}`);
// [0.6, 1.1, 1.0, 0.9, 1.0, 1.2, 1.4]
// Sun Mon Tue Wed Thu Fri Sat
console.log(` YoY trend: ${pattern.yoyTrend > 0 ? '+' : ''}${(pattern.yoyTrend * 100).toFixed(0)}%`);
}2. Historical Averages (3-Tier Fallback)
Gets the best available historical comparison for any date:
const avg = forecaster.historicalAverage('sunset-cruise', '2026-07-20');
// { average: 44.3, tier: 'doy', years: 3 }
//
// Tiers:
// 'doy' — Same DOW within ±10 day-of-year window (most accurate)
// 'month' — Same DOW within same month (reliable fallback)
// 'seasonal' — DOW seasonal ratio × annual average (always available)3. Completion Rate Prediction
Predicts final demand from current bookings based on historical booking velocity:
forecaster.loadBookings([
{ itemId: 'sunset-cruise', serviceDate: '2025-07-20', bookedAt: '2025-07-01', quantity: 2 },
{ itemId: 'sunset-cruise', serviceDate: '2025-07-20', bookedAt: '2025-07-10', quantity: 4 },
// ... thousands of historical bookings
]);
const prediction = forecaster.predictFinal('sunset-cruise', '2026-07-20', 15);
// { predictedFinal: 38, confidence: 'high', completionRate: 0.394, milestone: 7, samples: 24 }4. Demand Matrix
Generate a grid view for operational planning:
const cells = forecaster.matrix(
['sunset-cruise', 'island-tour', 'diving'],
'2026-07-01',
'2026-07-14',
{ 'sunset-cruise-2026-07-01': 12, 'island-tour-2026-07-01': 8 }
);
for (const cell of cells) {
console.log(`${cell.itemId} on ${cell.date}: ${cell.color}`);
// sunset-cruise on 2026-07-01: orange
}5. Anomaly Detection
Identify unusual data points in historical data:
const anomalies = forecaster.detectAnomalies({ threshold: 2.0 });
for (const a of anomalies) {
console.log(`${a.date}: ${a.quantity} (expected ${a.expected}, ${a.severity} ${a.direction})`);
// 2024-06-15: 200 (expected 52.3, extreme high)
}6. Time Series Decomposition
Break down demand into trend, seasonal, and residual components:
const decompositions = forecaster.decompose();
for (const d of decompositions) {
console.log(`${d.itemId}: seasonal strength=${d.seasonalStrength}, trend strength=${d.trendStrength}`);
// Each point has: observed, trend, seasonal, residual
}7. Forecast Accuracy Scoring
Measure how well your forecasts match reality:
const forecasts = forecaster.forecastAll('2025-06-01', 30);
const actuals = [
{ itemId: 'sunset-cruise', date: '2025-06-01', quantity: 45 },
// ... actual outcomes
];
const report = forecaster.score(forecasts, actuals);
console.log(`MAPE: ${report.overall.mape}%`); // Mean Absolute Percentage Error
console.log(`RMSE: ${report.overall.rmse}`); // Root Mean Squared Error
console.log(`MAE: ${report.overall.mae}`); // Mean Absolute Error
console.log(`Bias: ${report.overall.bias}`); // Positive = over-predicting
console.log(`Hit rate: ${report.overall.hitRate}%`); // Within ±20% tolerance8. Multi-Item Correlation
Discover which items move together:
const result = forecaster.correlate({ minOverlap: 30 });
for (const pair of result.pairs) {
console.log(`${pair.itemA} ↔ ${pair.itemB}: ${pair.correlation} (${pair.relationship})`);
// sunset-cruise ↔ island-tour: 0.82 (strong_positive)
// sunset-cruise ↔ museum-pass: -0.45 (moderate_negative)
}9. Holiday Calendar
Flag dates near holidays that may affect demand:
import { HOLIDAY_PRESETS } from 'seasonal-forecast';
forecaster.setHolidays([
...HOLIDAY_PRESETS.international,
{ name: 'Local Festival', monthDay: '08-15', impact: 'high' },
]);
const matches = forecaster.checkHoliday('2026-12-25');
// [{ holiday: 'Christmas Day', distance: 0, impact: 'high', isExactMatch: true }]
// Annotate a date range
const annotations = forecaster.annotateHolidays('2026-12-20', '2027-01-05');10. Data Import
Import data from CSV or JSON with auto-detection:
import { fromCSV, fromJSON } from 'seasonal-forecast';
// CSV with auto-detected columns and delimiters
const records = fromCSV(csvString);
// JSON in various formats (array, date-keyed, nested)
const records2 = fromJSON(jsonData);11. Incremental Updates
Add data without full reload:
forecaster.loadHistory(historicalData);
// Later, add new data points
forecaster.addRecords([
{ itemId: 'sunset-cruise', date: '2026-03-23', quantity: 35 },
]);12. Season Multipliers
forecaster.setSeasons([
{ startMonthDay: '06-01', endMonthDay: '09-30', multiplier: 1.3 }, // Summer: +30%
{ startMonthDay: '12-01', endMonthDay: '02-28', multiplier: 0.5 }, // Winter: -50%
]);13. State Serialization
Cache analysis results for faster startup:
// Export state
const state = forecaster.exportState();
const json = toJSON(state);
fs.writeFileSync('forecast-cache.json', json);
// Import later
const newForecaster = new SeasonalForecaster();
newForecaster.importState(json);CLI
# Analyze patterns
seasonal-forecast analyze data.csv
# Generate forecasts
seasonal-forecast forecast data.csv --days 14 --item sunset-cruise
# Detect anomalies
seasonal-forecast anomalies data.csv --threshold 2.0
# Decompose time series
seasonal-forecast decompose data.csv
# Export state
seasonal-forecast export data.csv --format jsonStandalone Functions
All algorithms are also available as standalone functions:
import {
analyzePatterns,
generateForecasts,
buildHistoricalLookup,
buildCompletionRates,
predictFinal,
detectAnomalies,
decompose,
fromCSV,
fromJSON,
scoreAccuracy,
correlate,
matchHolidays,
HOLIDAY_PRESETS,
calcWeightedAvg,
classifyDemand,
getDoy,
doyDistance,
} from 'seasonal-forecast';How It Works
The DOY-Window Algorithm
Instead of complex ML models, we use a simple but powerful insight: the best predictor of demand on June 15 this year is demand around June 15 in previous years.
Target: June 15, 2026 (DOY 166)
Window: ±3 days → DOY 163-169
Year 2023: DOY 163=32, 165=41, 167=38 → avg=37.0 weight=1
Year 2024: DOY 164=39, 166=45, 168=42 → avg=42.0 weight=2
Year 2025: DOY 163=44, 165=48, 169=46 → avg=46.0 weight=3
Weighted average: (37×1 + 42×2 + 46×3) / (1+2+3) = 43.2
Apply DOW coefficient (Sunday=0.85): 43.2 × 0.85 = 36.7
Apply trend (+8% YoY): 36.7 × 1.08 = 39.6
→ Predicted: 40 passengersWhy Not ML?
| Approach | seasonal-forecast | ML (Prophet, etc.) |
|----------|-------------------|---------------------|
| Setup time | npm install | Python env, model training |
| Data needed | 1 year minimum | 2+ years recommended |
| Runtime cost | < 1ms per forecast | 100ms-10s per forecast |
| Cloud dependency | None | Often needs GPU/API |
| Explainability | Full factor breakdown | "The model says..." |
| Accuracy (seasonal) | Very good | Slightly better with 5+ years |
| Maintenance | Zero | Model retraining, drift monitoring |
For seasonal businesses with 1-5 years of data, the statistical approach performs within 5-10% of ML models while being orders of magnitude simpler.
Use Cases
- Tourism: Tour operators, activity providers, cruise lines
- Hospitality: Hotels, resorts, vacation rentals
- Restaurants: Table/cover forecasting, staff scheduling
- Retail: Seasonal product demand, inventory planning
- Events: Venue capacity planning, ticket sales forecasting
- Agriculture: Seasonal labor and resource planning
API Reference
SeasonalForecaster
| Method | Description |
|--------|-------------|
| loadHistory(records) | Load historical quantity data |
| addRecords(records) | Add data incrementally without full reload |
| loadBookings(bookings) | Load booking-level data for completion rates |
| setSeasons(seasons) | Define season multipliers |
| analyze() | Get pattern analysis for all items |
| getPattern(itemId) | Get pattern for a specific item |
| forecast(itemId, date) | Single-item single-date forecast |
| forecastAll(fromDate?, days?) | Bulk forecast for all items |
| historicalAverage(itemId, date) | 3-tier historical average |
| predictFinal(itemId, date, current) | Completion rate prediction |
| matrix(itemIds, from, to, actuals?) | Full demand matrix |
| classify(actual, historical, thresholds?) | Color classification |
| lastYearComparison(itemId, date) | Compare with same DOW last year |
| detectAnomalies(options?) | Find unusual data points |
| decompose(options?) | Trend/seasonal/residual decomposition |
| exportState() / importState(data) | Serialize/restore forecaster state |
| score(forecasts, actuals, options?) | Measure forecast accuracy (MAPE, RMSE, MAE) |
| correlate(options?) | Cross-item Pearson correlation |
| setHolidays(holidays) | Set holiday/event definitions |
| checkHoliday(date, proximity?) | Check date against holiday calendar |
| annotateHolidays(from, to, proximity?) | Annotate date range with holidays |
Types
See src/types.ts for full TypeScript definitions.
Contributing
Contributions are welcome! Please open an issue first to discuss what you'd like to change.
License
MIT — Built with care at TourOperation.com
