Project Structure¶
Every folder and file has a single responsibility. Nothing is arbitrary.
Top-level¶
airsense-ml/
├── bruno/ # API client collection — executable requests, not docs
├── configs/ # YAML configuration — dataset and model parameters
├── data/ # Data files — tracked by DVC, never committed to git
├── docs/ # MkDocs source — this documentation
├── models/ # Trained model artifacts — tracked by DVC
├── notebooks/ # EDA exploration — never imported by src/
├── scripts/ # Dev tooling — interactive Makefile menus
├── src/ # Application code — the only folder that matters for the system
├── tests/ # Test suite
├── .env.example # Environment variable template
├── Dockerfile # Multi-stage production build
├── Makefile # All project commands
└── pyproject.toml # Package definition and dependencies
src/ — Application Code¶
src/
├── api/ # HTTP layer — FastAPI only
│ ├── adapters/ # Translates between API schemas and ML pipeline
│ │ └── prediction_adapter.py
│ ├── schemas/ # Pydantic request/response contracts
│ │ ├── prediction.py # PredictionRequest, PredictionResponse
│ │ ├── batch.py # BatchPredictionRequest, BatchPredictionResponse
│ │ ├── health.py # HealthResponse
│ │ └── model_info.py # ModelInfoResponse
│ ├── v1/ # Versioned route handlers
│ │ ├── predict.py # POST /v1/predict, POST /v1/predict/batch
│ │ ├── health.py # GET /v1/health
│ │ └── model_info.py # GET /v1/model/info
│ └── app.py # FastAPI app factory — wires everything together
│
├── core/ # Application foundation — base layer, no ML logic
│ ├── config.py # All settings via pydantic-settings
│ ├── logger.py # Loguru setup, stdlib interception
│ ├── api_lifespan.py # FastAPI lifespan — loads model at startup
│ └── __init__.py # bootstrap() — logger + warnings init
│
├── data/ # Data loading and validation
│ ├── loader.py # load_config, load_raw, drop_leakage
│ └── validator.py # validate_features, validate_no_nulls, validate_target_range
│
├── features/ # Feature engineering and transformation
│ ├── preprocessing.py # transform_target, load_and_clean orchestration
│ ├── feature_engineering.py # cyclical time encoding, aqi_capped flag
│ ├── encoding.py # sklearn numerical + categorical pipelines
│ └── pipeline.py # ColumnTransformer assembly from config
│
├── models/ # ML model logic
│ ├── registry.py # MODEL_MAP — all model classes in one place
│ ├── evaluate.py # compute_metrics → ModelMetrics (Pydantic)
│ ├── train.py # Training orchestration + MLflow logging
│ ├── predict.py # AQIPredictor — loads model, runs inference
│ └── tune.py # Optuna hyperparameter tuning
│
└── utils/ # Shared utilities
├── paths.py # PROJECT_ROOT, path helpers
├── warnings.py # Centralised warning suppression registry
└── model_results_display.py # Rich terminal tables for training output
configs/¶
configs/
├── delhi.yaml # Delhi-specific dataset config
│ # Controls: features, leakage cols, target, filepath
├── model_config.yaml # Training config
│ # Controls: models, hyperparams, splits, MLflow
└── model_config.prod.yaml # Production training config
# XGBoost + LightGBM only — memory-optimised
Adding a new city (e.g. Mumbai) requires only configs/mumbai.yaml — zero code changes.
data/¶
data/
├── raw/ # Original source data — never modified, DVC tracked
├── processed/ # Cleaned and validated data
└── features/ # Engineered feature sets (Parquet — planned)
Raw data is never committed to git. DVC pointer files (data/raw.dvc) are committed instead. Anyone cloning the repo runs dvc pull to get the data.
models/¶
models/
├── best_model.pkl # Best model from local training (Random Forest)
└── best_model_prod.pkl # Best memory-safe model for deployment (LightGBM)
Both are gitignored via *.pkl and tracked by DVC.
bruno/¶
bruno/
├── bruno.json # Collection manifest
├── health.bru # GET /v1/health
├── model_info.bru # GET /v1/model/info
├── predict_single.bru # POST /v1/predict — Delhi winter morning
├── predict_batch.bru # POST /v1/predict/batch — 3 stations
└── environments/
├── local.bru # baseUrl = http://localhost:8000
└── production.bru # baseUrl = https://your-deployed-url (gitignored)
What Goes Where — Decision Rules¶
| Content | Location | Reason |
|---|---|---|
| HTTP routes | src/api/v1/ |
Transport layer only |
| Request/response shapes | src/api/schemas/ |
Single contract source |
| Schema ↔ pipeline translation | src/api/adapters/ |
Decouples API from ML |
| Model loading + inference | src/models/predict.py |
ML layer, not API layer |
| Feature transformations | src/features/ |
Reused by train and inference |
| Column names, target | configs/*.yaml |
Config, not code |
| App settings, ports | src/core/config.py |
Single settings source |
| Exploration code | notebooks/ |
Never imported by src/ |
| Generated artifacts | models/, data/ |
DVC tracked, not git |