Plugins & Custom Doctors
ModelDoctor's architecture allows you to inject custom domain-specific diagnostic rules by creating a Custom Doctor. This is useful for compliance checks, domain-specific quality gates, or proprietary business rules that the built-in Doctors do not cover.
The BaseDoctor Contract
All custom doctors must inherit from modeldoctor.core.base_doctor.BaseDoctor and implement the following:
Required Class Attributes
| Attribute | Type | Description |
|---|---|---|
name |
str |
A unique string identifier (e.g., "ComplianceDoctor"). |
dimension |
str |
The diagnostic category for grouping findings (e.g., "Compliance"). |
priority |
int |
Execution order — lower numbers run first. Built-in doctors use 10–80. |
Required Methods
| Method | Signature | Description |
|---|---|---|
examine |
(self, context: EvaluationContext) -> Diagnosis |
The core analysis method. Called once per diagnosis run. |
Minimal Working Example
from modeldoctor.core.base_doctor import BaseDoctor
from modeldoctor.core.context import EvaluationContext
from modeldoctor.models.diagnosis import Diagnosis
from modeldoctor.models.enums import Severity
class ComplianceDoctor(BaseDoctor):
name = "ComplianceDoctor"
dimension = "Fairness & Compliance"
priority = 90 # Run after built-in doctors
# Protected attributes to check for
SENSITIVE_FEATURES = {"age", "gender", "race"}
def examine(self, context: EvaluationContext) -> Diagnosis:
diagnosis = self._new_diagnosis()
if not context.feature_names:
return diagnosis
detected = [f for f in context.feature_names if f in self.SENSITIVE_FEATURES]
if detected:
finding = self._finding(
title="Sensitive Attribute in Feature Set",
severity=Severity.WARNING,
explanation=(
f"The following features are classified as sensitive: "
f"{', '.join(detected)}. "
f"Training with protected attributes may violate fairness constraints."
),
tags=["compliance", "fairness"],
)
diagnosis.findings.append(finding)
diagnosis.dimension_score = 40.0
diagnosis.passed = False
return diagnosis
Registering and Injecting Your Doctor
To use your custom Doctor, inject it into the DoctorRegistry and pass it to the pipeline:
from sklearn.ensemble import RandomForestClassifier
from modeldoctor.core.registry import DoctorRegistry
from modeldoctor.core.pipeline import DiagnosticPipeline
from modeldoctor.core.context import EvaluationContext
from modeldoctor import ModelDoctorConfig
import modeldoctor as md
# Build a registry with all default doctors plus your custom one
registry = DoctorRegistry.default()
registry.register(ComplianceDoctor)
# Use md.diagnose() for the full pipeline (prescriptions + health score)
# Your custom doctor will appear in the registry
report = md.diagnose(model, X_train, y_train, X_test, y_test)
Using the EvidenceBuilder (Advanced)
For more sophisticated doctors that need weighted, normalized evidence scoring, use EvidenceBuilder directly instead of creating Finding objects manually. This ensures your doctor's findings are scored consistently with the built-in risk and confidence pipeline.
from modeldoctor.core.base_doctor import BaseDoctor
from modeldoctor.core.context import EvaluationContext
from modeldoctor.models.diagnosis import Diagnosis
class AdvancedDoctor(BaseDoctor):
name = "AdvancedDoctor"
dimension = "Custom"
priority = 85
def examine(self, context: EvaluationContext) -> Diagnosis:
diagnosis = self._new_diagnosis()
builder = self._evidence_builder()
# Example: flag large inference latency
latency_ms = context.inference_latency_ms
if latency_ms > 500:
builder.add(
name="Inference Latency",
measured_value=latency_ms,
weight="High",
normalized_score=min(latency_ms / 2000.0, 1.0),
expected_range="<= 500ms"
)
return self._finalize(diagnosis, builder)
Best Practices
- Fail gracefully: If your doctor requires a specific model type (e.g., a neural network), check
context.modelfirst and return an emptyDiagnosiswithdimension_score = 100.0if unsupported. - Use
_finding()factory: The_finding()helper onBaseDoctorensures finding objects have all required fields populated correctly. - Use priority > 80: Keep custom doctors at priority 85–99 to ensure they run after built-in ones.
- Avoid expensive computation: Access pre-computed properties on
context(e.g.,context.feature_importances) rather than recomputing them.