Add comprehensive CLI tools for regulatory affairs and quality management: 1. regulatory-affairs-head/scripts/regulatory_pathway_analyzer.py - FDA/EU MDR/UK UKCA/Health Canada/TGA pathway analysis - Timeline & cost estimation, optimal submission sequence 2. capa-officer/scripts/root_cause_analyzer.py - 5-Why, Fishbone, Fault Tree analysis methods - Auto-generates CAPA recommendations 3. risk-management-specialist/scripts/fmea_analyzer.py - ISO 14971 / IEC 60812 compliant FMEA - RPN calculation, risk reduction strategies 4. quality-manager-qmr/scripts/quality_effectiveness_monitor.py - QMS metric tracking, trend analysis - Predictive alerts, management review summaries 5. quality-documentation-manager/scripts/document_version_control.py - Semantic versioning, change control - Electronic signatures, document matrix All tools: argparse CLI, JSON I/O, demo mode, dataclasses, docstrings. Closes #238
483 lines
19 KiB
Python
483 lines
19 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
Quality Management System Effectiveness Monitor
|
||
|
||
Quantitatively assess QMS effectiveness using leading and lagging indicators.
|
||
Tracks trends, calculates control limits, and predicts potential quality issues
|
||
before they become failures. Integrates with CAPA and management review processes.
|
||
|
||
Supports metrics:
|
||
- Complaint rates, defect rates, rework rates
|
||
- Supplier performance
|
||
- CAPA effectiveness
|
||
- Audit findings trends
|
||
- Non-conformance statistics
|
||
|
||
Usage:
|
||
python quality_effectiveness_monitor.py --metrics metrics.csv --dashboard
|
||
python quality_effectiveness_monitor.py --qms-data qms_data.json --predict
|
||
python quality_effectiveness_monitor.py --interactive
|
||
"""
|
||
|
||
import argparse
|
||
import json
|
||
import csv
|
||
import sys
|
||
from dataclasses import dataclass, field, asdict
|
||
from typing import List, Dict, Optional, Tuple
|
||
from datetime import datetime, timedelta
|
||
from statistics import mean, stdev, median
|
||
import numpy as np
|
||
from scipy import stats
|
||
|
||
|
||
@dataclass
|
||
class QualityMetric:
|
||
"""A single quality metric data point."""
|
||
metric_id: str
|
||
metric_name: str
|
||
category: str
|
||
date: str
|
||
value: float
|
||
unit: str
|
||
target: float
|
||
upper_limit: float
|
||
lower_limit: float
|
||
trend_direction: str = "" # "up", "down", "stable"
|
||
sigma_level: float = 0.0
|
||
is_alert: bool = False
|
||
is_critical: bool = False
|
||
|
||
|
||
@dataclass
|
||
class QMSReport:
|
||
"""QMS effectiveness report."""
|
||
report_period: Tuple[str, str]
|
||
overall_effectiveness_score: float
|
||
metrics_count: int
|
||
metrics_in_control: int
|
||
metrics_out_of_control: int
|
||
critical_alerts: int
|
||
trends_analysis: Dict
|
||
predictive_alerts: List[Dict]
|
||
improvement_opportunities: List[Dict]
|
||
management_review_summary: str
|
||
|
||
|
||
class QMSEffectivenessMonitor:
|
||
"""Monitors and analyzes QMS effectiveness."""
|
||
|
||
SIGNAL_INDICATORS = {
|
||
"complaint_rate": {"unit": "per 1000 units", "target": 0, "upper_limit": 1.5},
|
||
"defect_rate": {"unit": "PPM", "target": 100, "upper_limit": 500},
|
||
"rework_rate": {"unit": "%", "target": 2.0, "upper_limit": 5.0},
|
||
"on_time_delivery": {"unit": "%", "target": 98, "lower_limit": 95},
|
||
"audit_findings": {"unit": "count/month", "target": 0, "upper_limit": 3},
|
||
"capa_closure_rate": {"unit": "% within target", "target": 100, "lower_limit": 90},
|
||
"supplier_defect_rate": {"unit": "PPM", "target": 200, "upper_limit": 1000}
|
||
}
|
||
|
||
def __init__(self):
|
||
self.metrics = []
|
||
|
||
def load_csv(self, csv_path: str) -> List[QualityMetric]:
|
||
"""Load metrics from CSV file."""
|
||
metrics = []
|
||
with open(csv_path, 'r', encoding='utf-8') as f:
|
||
reader = csv.DictReader(f)
|
||
for row in reader:
|
||
metric = QualityMetric(
|
||
metric_id=row.get('metric_id', ''),
|
||
metric_name=row.get('metric_name', ''),
|
||
category=row.get('category', 'General'),
|
||
date=row.get('date', ''),
|
||
value=float(row.get('value', 0)),
|
||
unit=row.get('unit', ''),
|
||
target=float(row.get('target', 0)),
|
||
upper_limit=float(row.get('upper_limit', 0)),
|
||
lower_limit=float(row.get('lower_limit', 0)),
|
||
)
|
||
metrics.append(metric)
|
||
self.metrics = metrics
|
||
return metrics
|
||
|
||
def calculate_sigma_level(self, metric: QualityMetric, historical_values: List[float]) -> float:
|
||
"""Calculate process sigma level based on defect rate."""
|
||
if metric.unit == "PPM" or "rate" in metric.metric_name.lower():
|
||
# For defect rates, DPMO = defects_per_million_opportunities
|
||
if historical_values:
|
||
avg_defect_rate = mean(historical_values)
|
||
if avg_defect_rate > 0:
|
||
dpmo = avg_defect_rate
|
||
# Simplified sigma conversion (actual uses 1.5σ shift)
|
||
sigma_map = {
|
||
330000: 1.0, 620000: 2.0, 110000: 3.0, 27000: 4.0,
|
||
6200: 5.0, 230: 6.0, 3.4: 6.0
|
||
}
|
||
# Rough sigma calculation
|
||
sigma = 6.0 - (dpmo / 1000000) * 10
|
||
return max(0.0, min(6.0, sigma))
|
||
return 0.0
|
||
|
||
def analyze_trend(self, values: List[float]) -> Tuple[str, float]:
|
||
"""Analyze trend direction and significance."""
|
||
if len(values) < 3:
|
||
return "insufficient_data", 0.0
|
||
|
||
x = list(range(len(values)))
|
||
y = values
|
||
|
||
# Linear regression
|
||
n = len(x)
|
||
sum_x = sum(x)
|
||
sum_y = sum(y)
|
||
sum_xy = sum(x[i] * y[i] for i in range(n))
|
||
sum_x2 = sum(xi * xi for xi in x)
|
||
|
||
slope = (n * sum_xy - sum_x * sum_y) / (n * sum_x2 - sum_x * sum_x) if (n * sum_x2 - sum_x * sum_x) != 0 else 0
|
||
|
||
# Determine trend direction
|
||
if slope > 0.01:
|
||
direction = "up"
|
||
elif slope < -0.01:
|
||
direction = "down"
|
||
else:
|
||
direction = "stable"
|
||
|
||
# Calculate R-squared
|
||
if slope != 0:
|
||
intercept = (sum_y - slope * sum_x) / n
|
||
y_pred = [slope * xi + intercept for xi in x]
|
||
ss_res = sum((y[i] - y_pred[i])**2 for i in range(n))
|
||
ss_tot = sum((y[i] - mean(y))**2 for i in range(n))
|
||
r2 = 1 - (ss_res / ss_tot) if ss_tot > 0 else 0
|
||
else:
|
||
r2 = 0
|
||
|
||
return direction, r2
|
||
|
||
def detect_alerts(self, metrics: List[QualityMetric]) -> List[Dict]:
|
||
"""Detect metrics that require attention."""
|
||
alerts = []
|
||
for metric in metrics:
|
||
# Check immediate control limit violation
|
||
if metric.upper_limit and metric.value > metric.upper_limit:
|
||
alerts.append({
|
||
"metric_id": metric.metric_id,
|
||
"metric_name": metric.metric_name,
|
||
"issue": "exceeds_upper_limit",
|
||
"value": metric.value,
|
||
"limit": metric.upper_limit,
|
||
"severity": "critical" if metric.category in ["Customer", "Regulatory"] else "high"
|
||
})
|
||
if metric.lower_limit and metric.value < metric.lower_limit:
|
||
alerts.append({
|
||
"metric_id": metric.metric_id,
|
||
"metric_name": metric.metric_name,
|
||
"issue": "below_lower_limit",
|
||
"value": metric.value,
|
||
"limit": metric.lower_limit,
|
||
"severity": "critical" if metric.category in ["Customer", "Regulatory"] else "high"
|
||
})
|
||
|
||
# Check for adverse trend (3+ points in same direction)
|
||
# Need to group by metric_name and check historical data
|
||
# Simplified: check trend_direction flag if set
|
||
if metric.trend_direction in ["up", "down"] and metric.sigma_level > 3:
|
||
alerts.append({
|
||
"metric_id": metric.metric_id,
|
||
"metric_name": metric.metric_name,
|
||
"issue": f"adverse_trend_{metric.trend_direction}",
|
||
"value": metric.value,
|
||
"severity": "medium"
|
||
})
|
||
|
||
return alerts
|
||
|
||
def predict_failures(self, metrics: List[QualityMetric], forecast_days: int = 30) -> List[Dict]:
|
||
"""Predict potential failures based on trends."""
|
||
predictions = []
|
||
|
||
# Group metrics by name to get time series
|
||
grouped = {}
|
||
for m in metrics:
|
||
if m.metric_name not in grouped:
|
||
grouped[m.metric_name] = []
|
||
grouped[m.metric_name].append(m)
|
||
|
||
for metric_name, metric_list in grouped.items():
|
||
if len(metric_list) < 5:
|
||
continue
|
||
|
||
# Sort by date
|
||
metric_list.sort(key=lambda m: m.date)
|
||
values = [m.value for m in metric_list]
|
||
|
||
# Simple linear extrapolation
|
||
x = list(range(len(values)))
|
||
y = values
|
||
n = len(x)
|
||
sum_x = sum(x)
|
||
sum_y = sum(y)
|
||
sum_xy = sum(x[i] * y[i] for i in range(n))
|
||
sum_x2 = sum(xi * xi for xi in x)
|
||
slope = (n * sum_xy - sum_x * sum_y) / (n * sum_x2 - sum_x * sum_x) if (n * sum_x2 - sum_x * sum_x) != 0 else 0
|
||
|
||
if slope != 0:
|
||
# Forecast next value
|
||
next_value = y[-1] + slope
|
||
target = metric_list[0].target
|
||
upper_limit = metric_list[0].upper_limit
|
||
|
||
if (target and next_value > target * 1.2) or (upper_limit and next_value > upper_limit * 0.9):
|
||
predictions.append({
|
||
"metric": metric_name,
|
||
"current_value": y[-1],
|
||
"forecast_value": round(next_value, 2),
|
||
"forecast_days": forecast_days,
|
||
"trend_slope": round(slope, 3),
|
||
"risk_level": "high" if upper_limit and next_value > upper_limit else "medium"
|
||
})
|
||
|
||
return predictions
|
||
|
||
def calculate_effectiveness_score(self, metrics: List[QualityMetric]) -> float:
|
||
"""Calculate overall QMS effectiveness score (0-100)."""
|
||
if not metrics:
|
||
return 0.0
|
||
|
||
scores = []
|
||
for m in metrics:
|
||
# Score based on distance to target
|
||
if m.target != 0:
|
||
deviation = abs(m.value - m.target) / max(abs(m.target), 1)
|
||
score = max(0, 100 - deviation * 100)
|
||
else:
|
||
# For metrics where lower is better (defects, etc.)
|
||
if m.upper_limit:
|
||
score = max(0, 100 - (m.value / m.upper_limit) * 100 * 0.5)
|
||
else:
|
||
score = 50 # Neutral if no target
|
||
scores.append(score)
|
||
|
||
# Penalize for alerts
|
||
alerts = self.detect_alerts(metrics)
|
||
penalty = len([a for a in alerts if a["severity"] in ["critical", "high"]]) * 5
|
||
return max(0, min(100, mean(scores) - penalty))
|
||
|
||
def identify_improvement_opportunities(self, metrics: List[QualityMetric]) -> List[Dict]:
|
||
"""Identify metrics with highest improvement potential."""
|
||
opportunities = []
|
||
for m in metrics:
|
||
if m.upper_limit and m.value > m.upper_limit * 0.8:
|
||
gap = m.upper_limit - m.value
|
||
if gap > 0:
|
||
improvement_pct = (gap / m.upper_limit) * 100
|
||
opportunities.append({
|
||
"metric": m.metric_name,
|
||
"current": m.value,
|
||
"target": m.upper_limit,
|
||
"gap": round(gap, 2),
|
||
"improvement_potential_pct": round(improvement_pct, 1),
|
||
"recommended_action": f"Reduce {m.metric_name} by at least {round(gap, 2)} {m.unit}",
|
||
"impact": "High" if m.category in ["Customer", "Regulatory"] else "Medium"
|
||
})
|
||
|
||
# Sort by improvement potential
|
||
opportunities.sort(key=lambda x: x["improvement_potential_pct"], reverse=True)
|
||
return opportunities[:10]
|
||
|
||
def generate_management_review_summary(self, report: QMSReport) -> str:
|
||
"""Generate executive summary for management review."""
|
||
summary = [
|
||
f"QMS EFFECTIVENESS REVIEW - {report.report_period[0]} to {report.report_period[1]}",
|
||
"",
|
||
f"Overall Effectiveness Score: {report.overall_effectiveness_score:.1f}/100",
|
||
f"Metrics Tracked: {report.metrics_count} | In Control: {report.metrics_in_control} | Alerts: {report.critical_alerts}",
|
||
""
|
||
]
|
||
|
||
if report.critical_alerts > 0:
|
||
summary.append("🔴 CRITICAL ALERTS REQUIRING IMMEDIATE ATTENTION:")
|
||
for alert in [a for a in report.predictive_alerts if a.get("risk_level") == "high"]:
|
||
summary.append(f" • {alert['metric']}: forecast {alert['forecast_value']} (from {alert['current_value']})")
|
||
summary.append("")
|
||
|
||
summary.append("📈 TOP IMPROVEMENT OPPORTUNITIES:")
|
||
for i, opp in enumerate(report.improvement_opportunities[:3], 1):
|
||
summary.append(f" {i}. {opp['metric']}: {opp['recommended_action']} (Impact: {opp['impact']})")
|
||
summary.append("")
|
||
|
||
summary.append("🎯 RECOMMENDED ACTIONS:")
|
||
summary.append(" 1. Address all high-severity alerts within 30 days")
|
||
summary.append(" 2. Launch improvement projects for top 3 opportunities")
|
||
summary.append(" 3. Review CAPA effectiveness for recurring issues")
|
||
summary.append(" 4. Update risk assessments based on predictive trends")
|
||
|
||
return "\n".join(summary)
|
||
|
||
def analyze(
|
||
self,
|
||
metrics: List[QualityMetric],
|
||
start_date: str = None,
|
||
end_date: str = None
|
||
) -> QMSReport:
|
||
"""Perform comprehensive QMS effectiveness analysis."""
|
||
in_control = 0
|
||
for m in metrics:
|
||
if not m.is_alert and not m.is_critical:
|
||
in_control += 1
|
||
|
||
out_of_control = len(metrics) - in_control
|
||
|
||
alerts = self.detect_alerts(metrics)
|
||
critical_alerts = len([a for a in alerts if a["severity"] in ["critical", "high"]])
|
||
|
||
predictions = self.predict_failures(metrics)
|
||
improvement_opps = self.identify_improvement_opportunities(metrics)
|
||
|
||
effectiveness = self.calculate_effectiveness_score(metrics)
|
||
|
||
# Trend analysis by category
|
||
trends = {}
|
||
categories = set(m.category for m in metrics)
|
||
for cat in categories:
|
||
cat_metrics = [m for m in metrics if m.category == cat]
|
||
if len(cat_metrics) >= 2:
|
||
avg_values = [mean([m.value for m in cat_metrics])] # Simplistic - would need time series
|
||
trends[cat] = {
|
||
"metric_count": len(cat_metrics),
|
||
"avg_value": round(mean([m.value for m in cat_metrics]), 2),
|
||
"alerts": len([a for a in alerts if any(m.metric_name == a["metric_name"] for m in cat_metrics)])
|
||
}
|
||
|
||
period = (start_date or metrics[0].date, end_date or metrics[-1].date) if metrics else ("", "")
|
||
|
||
report = QMSReport(
|
||
report_period=period,
|
||
overall_effectiveness_score=effectiveness,
|
||
metrics_count=len(metrics),
|
||
metrics_in_control=in_control,
|
||
metrics_out_of_control=out_of_control,
|
||
critical_alerts=critical_alerts,
|
||
trends_analysis=trends,
|
||
predictive_alerts=predictions,
|
||
improvement_opportunities=improvement_opps,
|
||
management_review_summary="" # Filled later
|
||
)
|
||
|
||
report.management_review_summary = self.generate_management_review_summary(report)
|
||
|
||
return report
|
||
|
||
|
||
def format_qms_report(report: QMSReport) -> str:
|
||
"""Format QMS report as text."""
|
||
lines = [
|
||
"=" * 80,
|
||
"QMS EFFECTIVENESS MONITORING REPORT",
|
||
"=" * 80,
|
||
f"Period: {report.report_period[0]} to {report.report_period[1]}",
|
||
f"Overall Score: {report.overall_effectiveness_score:.1f}/100",
|
||
"",
|
||
"METRIC STATUS",
|
||
"-" * 40,
|
||
f" Total Metrics: {report.metrics_count}",
|
||
f" In Control: {report.metrics_in_control}",
|
||
f" Out of Control: {report.metrics_out_of_control}",
|
||
f" Critical Alerts: {report.critical_alerts}",
|
||
"",
|
||
"TREND ANALYSIS BY CATEGORY",
|
||
"-" * 40,
|
||
]
|
||
|
||
for category, data in report.trends_analysis.items():
|
||
lines.append(f" {category}: {data['avg_value']} (alerts: {data['alerts']})")
|
||
|
||
if report.predictive_alerts:
|
||
lines.extend([
|
||
"",
|
||
"PREDICTIVE ALERTS (Next 30 days)",
|
||
"-" * 40,
|
||
])
|
||
for alert in report.predictive_alerts[:5]:
|
||
lines.append(f" ⚠ {alert['metric']}: {alert['current_value']} → {alert['forecast_value']} ({alert['risk_level']})")
|
||
|
||
if report.improvement_opportunities:
|
||
lines.extend([
|
||
"",
|
||
"TOP IMPROVEMENT OPPORTUNITIES",
|
||
"-" * 40,
|
||
])
|
||
for i, opp in enumerate(report.improvement_opportunities[:5], 1):
|
||
lines.append(f" {i}. {opp['metric']}: {opp['recommended_action']}")
|
||
|
||
lines.extend([
|
||
"",
|
||
"MANAGEMENT REVIEW SUMMARY",
|
||
"-" * 40,
|
||
report.management_review_summary,
|
||
"=" * 80
|
||
])
|
||
|
||
return "\n".join(lines)
|
||
|
||
|
||
def main():
|
||
parser = argparse.ArgumentParser(description="QMS Effectiveness Monitor")
|
||
parser.add_argument("--metrics", type=str, help="CSV file with quality metrics")
|
||
parser.add_argument("--qms-data", type=str, help="JSON file with QMS data")
|
||
parser.add_argument("--dashboard", action="store_true", help="Generate dashboard summary")
|
||
parser.add_argument("--predict", action="store_true", help="Include predictive analytics")
|
||
parser.add_argument("--output", choices=["text", "json"], default="text")
|
||
parser.add_argument("--interactive", action="store_true", help="Interactive mode")
|
||
|
||
args = parser.parse_args()
|
||
monitor = QMSEffectivenessMonitor()
|
||
|
||
if args.metrics:
|
||
metrics = monitor.load_csv(args.metrics)
|
||
report = monitor.analyze(metrics)
|
||
elif args.qms_data:
|
||
with open(args.qms_data) as f:
|
||
data = json.load(f)
|
||
# Convert to QualityMetric objects
|
||
metrics = [QualityMetric(**m) for m in data.get("metrics", [])]
|
||
report = monitor.analyze(metrics)
|
||
else:
|
||
# Demo data
|
||
demo_metrics = [
|
||
QualityMetric("M001", "Customer Complaint Rate", "Customer", "2026-03-01", 0.8, "per 1000", 1.0, 1.5, 0.5),
|
||
QualityMetric("M002", "Defect Rate PPM", "Quality", "2026-03-01", 125, "PPM", 100, 500, 0, trend_direction="down", sigma_level=4.2),
|
||
QualityMetric("M003", "On-Time Delivery", "Operations", "2026-03-01", 96.5, "%", 98, 0, 95, trend_direction="down"),
|
||
QualityMetric("M004", "CAPA Closure Rate", "Quality", "2026-03-01", 92.0, "%", 100, 0, 90, is_alert=True),
|
||
QualityMetric("M005", "Supplier Defect Rate", "Supplier", "2026-03-01", 450, "PPM", 200, 1000, 0, is_critical=True),
|
||
]
|
||
# Simulate time series
|
||
all_metrics = []
|
||
for i in range(30):
|
||
for dm in demo_metrics:
|
||
new_metric = QualityMetric(
|
||
metric_id=dm.metric_id,
|
||
metric_name=dm.metric_name,
|
||
category=dm.category,
|
||
date=f"2026-03-{i+1:02d}",
|
||
value=dm.value + (i * 0.1) if dm.metric_name == "Customer Complaint Rate" else dm.value,
|
||
unit=dm.unit,
|
||
target=dm.target,
|
||
upper_limit=dm.upper_limit,
|
||
lower_limit=dm.lower_limit
|
||
)
|
||
all_metrics.append(new_metric)
|
||
report = monitor.analyze(all_metrics)
|
||
|
||
if args.output == "json":
|
||
result = asdict(report)
|
||
print(json.dumps(result, indent=2))
|
||
else:
|
||
print(format_qms_report(report))
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|