Commit ·
91a9dcd
1
Parent(s): b4fadea
Dependencies fix
Browse files- README.md +8 -0
- app/api/routes.py +10 -0
- app/monitoring/alerts.py +0 -1
- app/monitoring/drift.py +18 -0
- app/monitoring/governance.py +66 -0
- app/utils/alerts.py +28 -0
- reports/evidently/drift_report.html +0 -0
- requirements-dev.txt +11 -4
- requirements.txt +11 -4
README.md
CHANGED
|
@@ -11,7 +11,15 @@ license: mit
|
|
| 11 |
|
| 12 |
# Under Construction
|
| 13 |
|
|
|
|
| 14 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
# Repo Structure
|
| 17 |
|
|
|
|
| 11 |
|
| 12 |
# Under Construction
|
| 13 |
|
| 14 |
+
py -3.9 -m venv .venv
|
| 15 |
|
| 16 |
+
.venv\Scripts\activate
|
| 17 |
+
|
| 18 |
+
python -m pip install --upgrade pip
|
| 19 |
+
|
| 20 |
+
pip install -r requirements.txt
|
| 21 |
+
|
| 22 |
+
uvicorn app.main:app --reload
|
| 23 |
|
| 24 |
# Repo Structure
|
| 25 |
|
app/api/routes.py
CHANGED
|
@@ -6,6 +6,7 @@ from app.inference.predictor import Predictor
|
|
| 6 |
from app.core.logging import log_prediction
|
| 7 |
from app.monitoring.data_loader import load_production_data
|
| 8 |
from app.monitoring.drift import run_drift_check
|
|
|
|
| 9 |
|
| 10 |
|
| 11 |
router = APIRouter()
|
|
@@ -38,3 +39,12 @@ def run_drift():
|
|
| 38 |
"status": "drift_check_completed",
|
| 39 |
"report_path": report_path
|
| 40 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
from app.core.logging import log_prediction
|
| 7 |
from app.monitoring.data_loader import load_production_data
|
| 8 |
from app.monitoring.drift import run_drift_check
|
| 9 |
+
import pandas as pd
|
| 10 |
|
| 11 |
|
| 12 |
router = APIRouter()
|
|
|
|
| 39 |
"status": "drift_check_completed",
|
| 40 |
"report_path": report_path
|
| 41 |
}
|
| 42 |
+
|
| 43 |
+
@router.get("/monitoring/run")
|
| 44 |
+
def monitoring_run():
|
| 45 |
+
# Example: load some data
|
| 46 |
+
current_data = pd.read_csv("data/current.csv")
|
| 47 |
+
reference_data = pd.read_csv("data/reference.csv")
|
| 48 |
+
|
| 49 |
+
alerts = run_drift_check(current_data, reference_data, model_version="v1")
|
| 50 |
+
return {"alerts": alerts}
|
app/monitoring/alerts.py
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
# threshold evaluation
|
|
|
|
|
|
app/monitoring/drift.py
CHANGED
|
@@ -4,6 +4,7 @@ import os
|
|
| 4 |
import pandas as pd
|
| 5 |
from evidently.report import Report
|
| 6 |
from evidently.metric_preset import DataDriftPreset
|
|
|
|
| 7 |
|
| 8 |
REFERENCE_DATA_PATH = "models/v1/reference_data.csv"
|
| 9 |
REPORT_DIR = "reports/evidently"
|
|
@@ -27,3 +28,20 @@ def run_drift_check(current_df: pd.DataFrame):
|
|
| 27 |
report.save_html(REPORT_PATH)
|
| 28 |
|
| 29 |
return REPORT_PATH
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
import pandas as pd
|
| 5 |
from evidently.report import Report
|
| 6 |
from evidently.metric_preset import DataDriftPreset
|
| 7 |
+
from app.monitoring.governance import Governance
|
| 8 |
|
| 9 |
REFERENCE_DATA_PATH = "models/v1/reference_data.csv"
|
| 10 |
REPORT_DIR = "reports/evidently"
|
|
|
|
| 28 |
report.save_html(REPORT_PATH)
|
| 29 |
|
| 30 |
return REPORT_PATH
|
| 31 |
+
|
| 32 |
+
# Thresholds configuration
|
| 33 |
+
thresholds = {
|
| 34 |
+
"psi": 0.2,
|
| 35 |
+
"accuracy_drop": 0.05,
|
| 36 |
+
"f1": 0.7
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
governance = Governance(thresholds=thresholds)
|
| 40 |
+
|
| 41 |
+
def run_drift_check(current_data, reference_data, model_version="v1"):
|
| 42 |
+
report = Report(metrics=[DataDriftPreset()])
|
| 43 |
+
report.run(current_data=current_data, reference_data=reference_data)
|
| 44 |
+
|
| 45 |
+
# Governance check
|
| 46 |
+
alerts = governance.check_metrics(report.as_dict(), model_version=model_version)
|
| 47 |
+
return alerts
|
app/monitoring/governance.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# This file implements threshold checking, governance signals logging, and notifications.
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import logging
|
| 5 |
+
from datetime import datetime
|
| 6 |
+
from app.utils.alerts import send_email_alert, send_slack_alert
|
| 7 |
+
import os
|
| 8 |
+
|
| 9 |
+
os.makedirs("logs", exist_ok=True)
|
| 10 |
+
logger = logging.getLogger("governance")
|
| 11 |
+
logger.setLevel(logging.INFO)
|
| 12 |
+
handler = logging.FileHandler("logs/governance_alerts.log")
|
| 13 |
+
formatter = logging.Formatter('%(asctime)s | %(levelname)s | %(message)s')
|
| 14 |
+
handler.setFormatter(formatter)
|
| 15 |
+
logger.addHandler(handler)
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class Governance:
|
| 19 |
+
def __init__(self, thresholds: dict):
|
| 20 |
+
"""
|
| 21 |
+
thresholds example:
|
| 22 |
+
{
|
| 23 |
+
"psi": 0.2,
|
| 24 |
+
"accuracy_drop": 0.05,
|
| 25 |
+
"f1": 0.7
|
| 26 |
+
}
|
| 27 |
+
"""
|
| 28 |
+
self.thresholds = thresholds
|
| 29 |
+
|
| 30 |
+
def check_metrics(self, report_dict: dict, model_version: str):
|
| 31 |
+
alerts = []
|
| 32 |
+
|
| 33 |
+
# Example: data drift
|
| 34 |
+
psi = report_dict.get("metrics", {}).get("DataDriftPreset", {}).get("result", {}).get("dataset_drift", 0)
|
| 35 |
+
if psi > self.thresholds.get("psi", 0.2):
|
| 36 |
+
alerts.append(f"Data drift detected (PSI={psi})")
|
| 37 |
+
|
| 38 |
+
# Example: classification performance
|
| 39 |
+
f1 = report_dict.get("metrics", {}).get("ClassificationPreset", {}).get("result", {}).get("f1_score", 1.0)
|
| 40 |
+
if f1 < self.thresholds.get("f1", 0.7):
|
| 41 |
+
alerts.append(f"F1 drop detected (F1={f1})")
|
| 42 |
+
|
| 43 |
+
# Example: regression accuracy
|
| 44 |
+
accuracy_drop = report_dict.get("metrics", {}).get("RegressionPreset", {}).get("result", {}).get("accuracy_drop", 0)
|
| 45 |
+
if accuracy_drop > self.thresholds.get("accuracy_drop", 0.05):
|
| 46 |
+
alerts.append(f"Accuracy drop detected ({accuracy_drop})")
|
| 47 |
+
|
| 48 |
+
# Log alerts
|
| 49 |
+
for alert in alerts:
|
| 50 |
+
self.log_alert(alert, model_version)
|
| 51 |
+
|
| 52 |
+
# Optional notifications
|
| 53 |
+
for alert in alerts:
|
| 54 |
+
send_email_alert(alert)
|
| 55 |
+
send_slack_alert(alert)
|
| 56 |
+
|
| 57 |
+
return alerts
|
| 58 |
+
|
| 59 |
+
@staticmethod
|
| 60 |
+
def log_alert(message: str, model_version: str):
|
| 61 |
+
log_entry = {
|
| 62 |
+
"timestamp": datetime.utcnow().isoformat(),
|
| 63 |
+
"model_version": model_version,
|
| 64 |
+
"alert": message
|
| 65 |
+
}
|
| 66 |
+
logger.info(json.dumps(log_entry))
|
app/utils/alerts.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Helper functions for sending notifications.
|
| 2 |
+
|
| 3 |
+
import smtplib
|
| 4 |
+
from email.message import EmailMessage
|
| 5 |
+
import requests
|
| 6 |
+
|
| 7 |
+
def send_email_alert(message: str):
|
| 8 |
+
# Configure your SMTP settings here
|
| 9 |
+
try:
|
| 10 |
+
email = EmailMessage()
|
| 11 |
+
email.set_content(message)
|
| 12 |
+
email["Subject"] = "ML Governance Alert"
|
| 13 |
+
email["From"] = "ml.alerts@example.com"
|
| 14 |
+
email["To"] = "ops-team@example.com"
|
| 15 |
+
|
| 16 |
+
with smtplib.SMTP("localhost") as smtp:
|
| 17 |
+
smtp.send_message(email)
|
| 18 |
+
except Exception as e:
|
| 19 |
+
print(f"Failed to send email alert: {e}")
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def send_slack_alert(message: str):
|
| 23 |
+
# Slack webhook URL
|
| 24 |
+
webhook_url = "https://hooks.slack.com/services/XXXX/YYYY/ZZZZ"
|
| 25 |
+
try:
|
| 26 |
+
requests.post(webhook_url, json={"text": message})
|
| 27 |
+
except Exception as e:
|
| 28 |
+
print(f"Failed to send Slack alert: {e}")
|
reports/evidently/drift_report.html
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
requirements-dev.txt
CHANGED
|
@@ -1,5 +1,12 @@
|
|
| 1 |
evidently==0.4.15
|
| 2 |
-
fastapi
|
| 3 |
-
uvicorn
|
| 4 |
-
pandas
|
| 5 |
-
scikit-learn
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
evidently==0.4.15
|
| 2 |
+
fastapi>=0.100.0,<0.130.0
|
| 3 |
+
uvicorn>=0.21.1,<0.40.0
|
| 4 |
+
pandas>=1.5.0,<2.0.0
|
| 5 |
+
scikit-learn==1.6.1
|
| 6 |
+
pydantic==1.10.12
|
| 7 |
+
plotly
|
| 8 |
+
numpy<2.0.0
|
| 9 |
+
requests
|
| 10 |
+
scipy>=1.10.0,<2.0.0
|
| 11 |
+
python-multipart>=0.0.6
|
| 12 |
+
typing-extensions>=4.0.0
|
requirements.txt
CHANGED
|
@@ -1,5 +1,12 @@
|
|
| 1 |
evidently==0.4.15
|
| 2 |
-
fastapi
|
| 3 |
-
uvicorn
|
| 4 |
-
pandas
|
| 5 |
-
scikit-learn
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
evidently==0.4.15
|
| 2 |
+
fastapi>=0.100.0,<0.130.0
|
| 3 |
+
uvicorn>=0.21.1,<0.40.0
|
| 4 |
+
pandas>=1.5.0,<2.0.0
|
| 5 |
+
scikit-learn==1.6.1
|
| 6 |
+
pydantic==1.10.12
|
| 7 |
+
plotly
|
| 8 |
+
numpy<2.0.0
|
| 9 |
+
requests
|
| 10 |
+
scipy>=1.10.0,<2.0.0
|
| 11 |
+
python-multipart>=0.0.6
|
| 12 |
+
typing-extensions>=4.0.0
|