Skip to content

Architecture Overview

AirSense ML is structured as a layered ML system — each layer has a single responsibility and strict import direction.


System Overview

┌─────────────────────────────────────────────────────┐
│                   Data Layer                         │
│   Raw CSV → Loader → Validator → Feature Engineering │
└─────────────────────┬───────────────────────────────┘
┌─────────────────────▼───────────────────────────────┐
│                 Training Layer                       │
│   sklearn Pipeline → 7 Models → MLflow → Best Model │
└─────────────────────┬───────────────────────────────┘
┌─────────────────────▼───────────────────────────────┐
│                  Inference Layer                     │
│   AQIPredictor → PredictionAdapter → Response       │
└─────────────────────┬───────────────────────────────┘
┌─────────────────────▼───────────────────────────────┐
│                    API Layer                         │
│   FastAPI → /v1 Routes → Pydantic Schemas           │
└─────────────────────┬───────────────────────────────┘
┌─────────────────────▼───────────────────────────────┐
│                 Container Layer                      │
│   Docker (multi-stage, linux/amd64) → Docker Hub    │
└─────────────────────────────────────────────────────┘

Dependency Direction

The most important architectural rule — imports flow strictly downward. No layer imports from a layer above it:

Level 0 — imports nothing from src
    src/core/config.py
    src/core/logger.py
    src/utils/paths.py

Level 1 — imports from Level 0 only
    src/utils/warnings.py
    src/core/__init__.py

Level 2 — imports from Level 0 + 1
    src/data/loader.py
    src/data/validator.py
    src/features/*

Level 3 — imports from Level 0, 1, 2
    src/models/*

Level 4 — imports from everything below
    src/api/*

Violating this rule creates circular imports. Every circular import we encountered during development was caused by a Level N module importing from Level N+1.


Key Design Patterns

Config-driven pipeline

No hardcoded column names, model parameters, or file paths in code. Everything flows from YAML:

configs/delhi.yaml          → data pipeline
configs/model_config.yaml   → training pipeline
.env.dev / .env.prod        → application settings

Adding a new city requires only a new YAML file — zero code changes.

Adapter pattern (API ↔ Inference)

PredictionRequest (API schema) never touches AQIPredictor directly. The adapter translates between them:

PredictionRequest → PredictionAdapter.to_dataframe() → AQIPredictor
AQIPredictor      → PredictionAdapter.to_response()  → PredictionResponse

This means the API contract and the ML pipeline can evolve independently.

sklearn Pipeline objects

Feature transformations are encapsulated in sklearn Pipeline and ColumnTransformer objects — not applied as raw functions. This means:

  • The same transformations are guaranteed at training and inference time
  • No training/serving skew
  • The full pipeline (features + model) is serialised as a single artifact

Singleton predictor

AQIPredictor is instantiated once at API startup via FastAPI's lifespan context manager and stored on app.state. All requests share the same loaded model — no per-request model loading overhead.


Data Flow — Training

1. load_config("configs/delhi.yaml")
2. load_raw(config)                  — reads CSV
3. engineer_base_features(df)        — cyclical encoding, aqi_capped
4. drop_leakage(df, config)          — removes PM2.5, NO2 etc
5. validate_features(df, config)     — raises if columns missing
6. transform_target(df)              — log1p(AQI), separate X and y
7. train_test_split                  — 80/10/10 train/val/test
8. build_feature_pipeline(config)    — ColumnTransformer
9. Pipeline(features + model).fit()  — for each of 7 models
10. compute_metrics(val, test)       — RMSE, R², MAE, RMSLE
11. mlflow.log_metrics/params/model  — per run
12. joblib.dump(best_pipeline)       — models/best_model.pkl

Data Flow — Inference

1. POST /v1/predict (JSON body)
2. Pydantic validates PredictionRequest
3. request.app.state.predictor.predict(body)
4. PredictionAdapter.to_dataframe(request)   — single-row DataFrame
5. engineer_base_features(df, target="aqi")  — same as training
6. pipeline.predict(df)                      — log-scale prediction
7. inverse_transform_target(prediction)      — expm1 → AQI scale
8. PredictionAdapter.to_response(aqi, version)
9. PredictionResponse returned as JSON

Configuration Architecture

pyproject.toml          — package metadata, dependency groups
configs/delhi.yaml      — dataset-specific: features, leakage, target
configs/model_config.yaml — training: models, hyperparams, MLflow, artifacts
.env.example            — template for all environment variables
.env.dev                — local overrides (gitignored)
.env.prod               — production overrides (gitignored)
src/core/config.py      — pydantic-settings reads .env, exposes settings singleton

All runtime configuration is accessible via from src.core import settings.