Air Quality AI
Real-time monitoring + 72h forecasts for Skopje
An intelligent air quality monitoring system covering 9 stations across Skopje, with AI-powered forecasts up to 72 hours ahead using a custom BiLSTM neural network trained on data from 2020 to 2026. Includes interactive maps, historical analytics, notifications, and PDF/CSV export.
SCREENSHOTS
FEATURES
Interactive Map
9 monitoring stations across Skopje shown on Leaflet.js map. Click any station for live readings, history, and forecasts.
BiLSTM Forecasting
Custom Bidirectional LSTM neural network in TensorFlow/Keras predicts PM2.5, PM10, NO₂, O₃ up to 72 hours ahead.
Historical Analytics
Charts (Chart.js) for trends, comparisons between stations, and air quality patterns over years 2020 – 2026.
Notifications
Email alerts for when pollution exceeds WHO/EU thresholds, with custom subscription per station and pollutant.
CSV & PDF Export
Export historical readings and forecast data as CSV for further analysis, or as PDF reports for sharing.
Educational Content
Explains health impact thresholds, AQI scale, and what each pollutant means for ordinary citizens.
ARCHITECTURE
Hourly cron job fetches the latest readings from the public air quality API, stores them in Postgres, and triggers re-inference of the BiLSTM model on the rolling 72h window. The forecast is cached and served via REST to the frontend, which renders charts and map overlays.
TECH STACK
BACKEND
- Python
3.12 - Django
5.x - TensorFlow
2.x - Keras
3.x - NumPy / Pandas
- scikit-learn
- Celery / cron (scheduled fetch)
FRONTEND
- HTML5 / CSS3
- JavaScript (vanilla)
- Bootstrap
5.x - Leaflet.js (interactive maps)
- Chart.js (analytics charts)
DATA & ML
- PostgreSQL (Supabase)
- BiLSTM architecture
- Training dataset:
2020 — 2026 - Pollutants: PM2.5, PM10, NO₂, O₃
- Forecast horizon:
72 hours - 9 monitoring stations
INFRASTRUCTURE
- Render.com (hosting)
- Supabase (DB)
- GitHub (CI/CD)
- WeasyPrint / ReportLab (PDF)
- SMTP (email alerts)
ML MODEL DETAILS
Architecture
Bidirectional LSTM with 2 stacked layers, 128 units each. Dropout 0.2 for regularization. Dense output layer with 72 timesteps × 4 pollutants.
Training Data
6 years of hourly readings (~525,000 samples per station). Standardized with MinMaxScaler. Train/val/test split 70/15/15.
Input Features
Pollutant concentrations, temperature, humidity, wind, hour-of-day, day-of-week, season. Sliding window of 72 past hours predicts next 72 hours.
Performance
MAE ~5.3 μg/m³ for PM2.5 at 24h horizon, degrading to ~12 μg/m³ at 72h horizon. Outperforms naïve last-value baselines by ~40%.
API ENDPOINTS
STATIONS & READINGS
GET /api/stations/ site stanici (9 vkupno)
GET /api/stations/<id>/ detali za stanica
GET /api/stations/<id>/readings/ istorija (?from, ?to)
GET /api/stations/<id>/forecast/ 72h prognoza
ANALYTICS
GET /api/analytics/trends/ godi[š]ni trendovi
GET /api/analytics/comparison/ sporedba megju stanici
GET /api/analytics/aqi/ AQI skala + thresholds
EXPORTS
GET /api/export/csv/?station=X&from=Y&to=Z
GET /api/export/pdf/?station=X&from=Y&to=Z
USER / NOTIFICATIONS
POST /api/auth/register/
POST /api/auth/login/
POST /api/subscriptions/ dodaj alert
DELETE /api/subscriptions/<id>/ otka[ž]i alert
CHALLENGES SOLVED
Missing data in historical readings
Stations sometimes have hours/days of missing readings (sensor maintenance, outages). Applied forward-fill within 6h gaps, linear interpolation within 24h, and excluded samples with longer gaps from training.
Model degradation at longer horizons
Naïve LSTM had MAE of ~25 μg/m³ at 72h — too inaccurate. Switching to Bidirectional LSTM with bigger window (72h input → 72h output as multi-output) reduced error by ~55%.
Cold start on Render free tier
Loading a 50MB Keras model on each cold start took 15+ seconds. Solved by lazy-loading the model on first inference, keeping it in memory after.
Real-time + ML in one Django app
Inference takes 1-3 seconds. Built async endpoints with thread pool for forecast generation; cached results for 1 hour to avoid recomputation on repeat requests.