Files
claude-skills-reference/ra-qm-team/quality-manager-qmr/scripts/quality_effectiveness_monitor.py
sudabg 059f91f1a4 feat: add 5 production Python tools for RA/QM skills (#238)
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
2026-03-13 22:26:03 +08:00

483 lines
19 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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()