Learning Project: An automated data quality monitoring system that detects when your data starts behaving differently than expected.
Imagine you have a weather prediction model that was trained on historical data. Over time, the patterns in your incoming data might changeโmaybe due to climate change, sensor upgrades, or seasonal variations. This change is called data drift, and it can silently break your ML models!
This project solves that problem by:
- ๐ Automatically monitoring new data files as they arrive
- ๐ Comparing new data against a "reference" baseline to detect unusual changes
- ๐จ Alerting you when drift is detected via cloud dashboards
- ๐งน Cleaning up processed files to keep your system organized
Real-world use case: A data science team receives weekly weather data and needs to ensure it matches expected patterns before retraining their forecasting models. This system runs automatically every day at 4 PM, checking for issues without manual intervention.
| Technology | Version | Purpose & Why It's Used |
|---|---|---|
| Python 3.12 | Latest | The backbone languageโchosen for its rich data science ecosystem |
| Apache Airflow 3.1.3 | Latest | Workflow orchestration engineโschedules and manages our monitoring pipeline. Think of it as a smart cron job on steroids that handles dependencies, retries, and monitoring |
| Evidently AI 0.7.17 | Latest | ML monitoring libraryโperforms statistical tests to detect data drift. It's like a health check for your datasets, running tests like Kolmogorov-Smirnov for numerical features |
| PostgreSQL 16 | Latest | Metadata databaseโAirflow uses this to track task states, execution history, and workflow metadata |
| Redis 7.2 | Stable | Message brokerโenables distributed task execution across multiple workers in Celery |
| Docker & Docker Compose | - | Containerizationโensures everyone runs the exact same environment, eliminating "works on my machine" issues |
| Pandas | via Evidently | Data manipulationโloads and transforms CSV files for analysis |
Architecture Choice: We use CeleryExecutor (not LocalExecutor) because it allows horizontal scalingโyou can add more workers to handle increased load.
sample-evidently-dag/
โ
โโโ ๐ dags/ # Airflow DAG definitions (your workflow logic)
โ โโโ monitoring-dag.py # โญ THE MAIN FILE - defines the drift monitoring workflow
โ
โโโ ๐ data/ # Data storage (mounted into Docker containers)
โ โโโ reference/ # Baseline "good" data for comparison
โ โ โโโ weather-reference-sample.csv
โ โโโ data-drift/ # Incoming data files to monitor
โ โโโ week1.csv # Simulated weekly data drops
โ โโโ week2.csv
โ โโโ week3.csv
โ
โโโ ๐ logs/ # Airflow execution logs (auto-generated)
โ โโโ (task logs appear here after runs)
โ
โโโ ๐ plugins/ # Custom Airflow plugins (currently empty)
โ โโโ (extend Airflow functionality here)
โ
โโโ ๐ config/ # Airflow configuration files
โ โโโ airflow.cfg # (auto-generated by docker-compose)
โ
โโโ ๐ณ docker-compose.yaml # Multi-container orchestration (8 services!)
โ # Defines: postgres, redis, scheduler, worker, api-server, dag-processor, triggerer, init
โ
โโโ ๐ณ Dockerfile # Custom Airflow image with Evidently installed
โ
โโโ ๐ฆ requirements.txt # Python dependencies (just Evidently for now)
โ
โโโ ๐ .gitignore # Ignores logs and temporary files
โ
โโโ ๐ README.md # You are here! ๐
-
/dags: The heart of your project. Each Python file here defines a workflow (DAG = Directed Acyclic Graph). Ourmonitoring-dag.pyis the only DAG and contains all the monitoring logic. -
/data/reference: Contains "baseline" data representing normal/expected distributions. All new data is compared against this reference. -
/data/data-drift: Drop new CSV files here to trigger the monitoring workflow. The system watches this folder and processes files matchingweek*.csv. -
/logs: Airflow automatically writes detailed logs here. Check these first when debugging! -
Docker Compose Services (8 containers working together):
postgres- Stores Airflow's metadataredis- Message queue for task distributionairflow-apiserver- Web UI (access atlocalhost:8080)airflow-scheduler- Decides when to run tasksairflow-dag-processor- Parses DAG filesairflow-worker- Executes tasksairflow-triggerer- Handles async event-based tasksairflow-init- One-time setup (creates admin user, folders, etc.)
What happens: The system continuously checks for new data files.
Journey through the code:
- Start:
PythonSensortask (detect_file) runs every 30 seconds - Function:
_detect_file()inmonitoring-dag.py(lines 123-171)glob.glob("/opt/airflow/data/data-drift/week*.csv")
- Logic:
- If no files โ Returns
Falseโ Sensor keeps waiting - If file found โ Selects newest (by creation time) โ Pushes filename to XCom (Airflow's inter-task messaging) โ Returns
True
- If no files โ Returns
- Result: DAG continues to next task
Key Learning: Sensors are different from operators! They poll until a condition is met, blocking the workflow until success.
What happens: New data is loaded, compared to reference data, and analyzed for drift.
Journey through the code:
graph TD
A[detect_data_drift Task] -->|Pull filename from XCom| B[_detect_data_drift function]
B -->|Call| C[_load_files function]
C -->|Load CSV| D[Reference Data]
C -->|Load CSV| E[Current Data]
D --> F[Evidently Report]
E --> F
F -->|Run Statistical Tests| G{Dataset Drift Detected?}
G -->|Yes| H[Return 'data_drift_detected']
G -->|No| I[Return 'no_data_drift_detected']
H --> J[Trigger Alert Branch]
I --> K[Continue Normal Operation]
style G fill:#ff9999
style H fill:#ffcccc
style I fill:#ccffcc
Step-by-step breakdown:
-
Pull Data (
_detect_data_drift, line 183):data_logs_filename = ti.xcom_pull(task_ids="detect_file", key="data_logs_filename")
โ ๏ธ Airflow 3.x Note: Must specifytask_idsparameter (changed from 2.x!) -
Load Data (
_load_files, line 78):- Loads reference CSV from
/data/reference/ - Loads current CSV (the file we just detected)
- Converts datetime columns
- Drops unnecessary columns (
count) - Returns as Evidently
Datasetobjects
- Loads reference CSV from
-
Create Report (line 206):
data_drift_run = Report([DataDriftPreset()])
DataDriftPreset()includes multiple statistical tests:- Kolmogorov-Smirnov test for numerical features (temp, humidity, windspeed)
- Chi-square test for categorical features (season, holiday, workingday)
- Dataset-level summary (overall drift score)
-
Run Analysis (line 215):
data_drift_result = data_drift_run.run( current_data=data_logs, reference_data=reference )
-
Check Results (line 226):
if report["metrics"][0]["value"]["count"] > 0: return "data_drift_detected" # Branch to alert task else: return "no_data_drift_detected" # Branch to no-op task
Key Learning: This uses a BranchPythonOperatorโthe return value determines which downstream task(s) execute. It's Airflow's way of implementing if/else logic in workflows!
What happens: If drift is detected, a detailed report is generated and uploaded to Evidently Cloud.
Journey through the code:
-
Task Execution (
data_drift_detectedtask, line 377):- Only runs if the branch operator returned
"data_drift_detected"
- Only runs if the branch operator returned
-
Credential Check (
_data_drift_detected, line 255):if not EVIDENTLY_CLOUD_TOKEN or not EVIDENTLY_CLOUD_PROJECT_ID: raise ValueError("Credentials not configured!")
- These come from Airflow Variables (set in Admin > Variables in UI)
-
Connect to Cloud (line 263):
ws = CloudWorkspace(token=EVIDENTLY_CLOUD_TOKEN, url="https://app.evidently.cloud") project = ws.get_project(EVIDENTLY_CLOUD_PROJECT_ID)
-
Generate & Upload Report (lines 280-297):
data_drift_report = Report([DataDriftPreset()]) data_drift_result = data_drift_report.run(current_data=data_logs, reference_data=reference) ws.add_run(project_id=project.id, run=data_drift_result, include_data=True)
include_data=Trueuploads raw data for deep analysis in the cloud UI
-
Cleanup (
clean_filetask, line 320):- Removes the processed CSV file (optional, controlled by task dependencies)
Full Workflow Visualization:
graph LR
A[detect_file<br/>Sensor] -->|File Found| B[detect_data_drift<br/>Branch]
B -->|Drift Found| C[data_drift_detected<br/>Python]
B -->|No Drift| D[no_data_drift_detected<br/>Empty]
C --> E[clean_file<br/>Python]
D --> E
E --> F[end<br/>Empty]
style A fill:#fff4e6
style B fill:#ffe6e6
style C fill:#ffcccc
style D fill:#e6ffe6
style E fill:#e6f3ff
style F fill:#f0f0f0
Key Learning: Notice the trigger rules! The clean_file task has trigger_rule="none_failed_min_one_success" (line 407), meaning it runs after either branch completes successfully. Without this, it would only run after both branches (which never happens with branching).
File: monitoring-dag.py, lines 156 & 188
What to learn: Airflow 3.x changed how tasks share data!
# โ OLD WAY (Airflow 2.x) - No longer works!
data_logs_filename = context["task_instance"].xcom_pull(key="data_logs_filename")
# โ
NEW WAY (Airflow 3.x) - Must specify source task
data_logs_filename = context["task_instance"].xcom_pull(
task_ids="detect_file", # Which task pushed this value?
key="data_logs_filename"
)Why it matters: This makes data lineage explicitโyou can trace where each value came from, improving debugging and clarity.
File: monitoring-dag.py, lines 123-171
What to learn: How to implement event-driven workflows instead of rigid schedules.
detect_file = PythonSensor(
task_id="detect_file",
python_callable=_detect_file,
poke_interval=30, # Check every 30 seconds
timeout=600, # Give up after 10 minutes
mode="poke" # Block a worker while waiting
)Design Pattern: The sensor keeps calling _detect_file() until it returns True. This is more efficient than running the entire DAG every 30 seconds hoping a file exists!
Alternative modes:
poke(current): Blocks a worker slot but responds instantly when condition is metreschedule: Frees up the worker between checks but adds slight delay
File: monitoring-dag.py, lines 196-200 & 255-260
What to learn: Always validate external inputs!
# After pulling from XCom
if not data_logs_filename:
raise ValueError(
"No data file found! The detect_file sensor should have pushed a filename to XCom. "
"Check that the sensor task completed successfully and pushed the value."
)
# Before using API credentials
if not EVIDENTLY_CLOUD_TOKEN or not EVIDENTLY_CLOUD_PROJECT_ID:
raise ValueError(
"Evidently Cloud credentials not configured! "
"Please set EVIDENTLY_CLOUD_TOKEN and EVIDENTLY_CLOUD_PROJECT_ID "
"in Airflow Variables (Admin > Variables)"
)Why this matters: These checks provide actionable error messages instead of cryptic stack traces. When a student (or your future self!) encounters an error, the message explains exactly what to fix.
Bonus: Notice how the code uses print() statements throughout (lines 147, 158, 194, etc.) for observabilityโthese appear in Airflow logs and help debug production issues.
Make sure you have installed:
- ๐ณ Docker Desktop (or Docker Engine + Docker Compose)
- ๐ป At least 4GB RAM and 10GB disk space available
- ๐ Internet connection (to download images)
# Navigate to the project directory
cd sample-evidently-dag
# Verify files are present
ls -la
# You should see: docker-compose.yaml, Dockerfile, dags/, data/, etc.# Build the image with Evidently installed
docker-compose build
# This takes 3-5 minutes on first run
# It downloads the base Airflow image and installs evidently==0.7.17# Create necessary folders and initialize the database
docker-compose up airflow-init
# Wait for the message: "airflow-init_1 exited with code 0"# Start the entire Airflow cluster (8 containers)
docker-compose up -d
# Check that all services are running
docker-compose ps
# Wait 1-2 minutes for all health checks to pass-
Open your browser: http://localhost:8080
-
Login with default credentials:
- Username:
airflow - Password:
airflow
- Username:
-
You should see the
monitoring_dagin the DAGs list!
To see drift reports in the cloud:
-
Sign up for Evidently Cloud: https://app.evidently.cloud
-
Create a project and copy your:
- API Token
- Project ID
-
In Airflow UI, go to Admin > Variables
-
Add two new variables:
- Key:
EVIDENTLY_CLOUD_TOKEN, Value:<your_token> - Key:
EVIDENTLY_CLOUD_PROJECT_ID, Value:<your_project_id>
- Key:
Option A: Manual Trigger (Recommended for Learning)
- In the Airflow UI, click on
monitoring_dag - Toggle the DAG ON (switch in top-left)
- Click the
โถ๏ธ Play button (top-right) to trigger manually
Option B: Automatic Schedule
- The DAG runs automatically daily at 4:00 PM (cron:
0 16 * * *) - The sensor will wait until a file matching
week*.csvappears in/data/data-drift/
-
Graph View: See the workflow structure
- Click the DAG name โ "Graph" tab
- Watch tasks turn from white โ yellow (running) โ green (success) or red (failed)
-
Task Logs: Debug and learn
- Click any task box โ "Log" button
- Read the
print()statements we added for learning!
-
Monitor Progress:
detect_filesensor will wait for a file (check every 30 seconds)detect_data_driftanalyzes the data- Branch splits based on drift detection
clean_fileremoves the processed file
# The project comes with test files already!
# Verify they exist:
ls -la data/data-drift/
# You should see: week1.csv, week2.csv, week3.csv
# If the DAG is running and watching, it will process these files
# To reprocess them, just copy them back:
cp data/data-drift/week1.csv data/data-drift/week_test.csv
# The sensor will detect week_test.csv and trigger the workflow!# Stop all containers
docker-compose down
# Remove volumes (this deletes all Airflow dataโuse with caution!)
docker-compose down -vSolution: DAGs are parsed every 30 seconds. Wait a minute, or check for Python syntax errors:
docker-compose exec airflow-dag-processor airflow dags listSolution: The custom image wasn't built. Run:
docker-compose build --no-cache
docker-compose up -d --force-recreateSolution: The XCom value wasn't found. Check:
- Did the
detect_filesensor complete successfully? - Are there files matching
week*.csvin/data/data-drift/? - Check sensor logs for XCom push confirmation
Solution: Add variables in Airflow UI (Admin > Variables):
EVIDENTLY_CLOUD_TOKENEVIDENTLY_CLOUD_PROJECT_ID
Or comment out the cloud upload task if testing locally.
Once you're comfortable with this project:
- Modify the Schedule: Change line 328 from
"0 16 * * *"to run more frequently - Add More Data: Create
week4.csv,week5.csvwith different distributions - Add Email Alerts: Use Airflow's
EmailOperatorto send notifications when drift is detected - Custom Drift Metrics: Replace
DataDriftPreset()with specific metrics from Evidently - Database Integration: Instead of CSV files, read from a PostgreSQL or MongoDB database
- Add Tests: Create unit tests for the
_load_files()and_detect_data_drift()functions
- Apache Airflow Docs: https://airflow.apache.org/docs/
- Evidently AI Docs: https://docs.evidentlyai.com/
- Airflow 3.x Migration Guide: https://airflow.apache.org/docs/apache-airflow/stable/migration-guide-3.0.html
- Data Drift Explained: https://www.evidentlyai.com/blog/ml-monitoring-data-drift
After completing this project, you should understand:
- โ How to build production-grade data pipelines with Airflow
- โ The importance of data quality monitoring in ML systems
- โ How sensors enable event-driven workflows
- โ Using branching for conditional logic in DAGs
- โ XCom for inter-task communication
- โ Docker Compose for multi-service orchestration
- โ Statistical tests for drift detection (KS test, Chi-square)
- โ Defensive programming with validation and error handling
Happy Learning! ๐ Questions? Check the code commentsโthey're there to help you!