Model risk management framework for insurance pricing models
Project description
insurance-mrm
Model risk management framework for insurance pricing models.
The problem
Most UK insurers govern their pricing models with Word documents and Excel registers. A validation report is produced once at model launch, filed away, and referenced occasionally. Monitoring evidence (if it exists) lives in a separate tracker maintained by the pricing team. There is no audit trail connecting monitoring outputs to governance sign-offs.
This is not a criticism of any specific firm — it is a description of current practice across the market. The PRA's 2026 supervision priorities letter called out "gaps between assumed and realised profitability" for general insurers. PRA SS1/23 is expected to extend to insurers by 2026-2027. The current documentation approach will not survive a PRA model risk review.
insurance-mrm is the governance layer that sits on top of statistical validation and monitoring. It does not run statistical tests. Those are handled by insurance-validation (point-in-time validation reports) and insurance-monitoring (ongoing operational monitoring). What insurance-mrm provides is:
- A model card format with the fields a Model Risk Committee actually needs: assumptions register with risk ratings per assumption, explicit not-intended-for list, monitoring plan with named owner and trigger thresholds
- An objective risk tier scoring engine (0-100 composite score across 6 dimensions) that produces a defensible tier assignment with a verbose rationale string — not a judgment call
- A persistent model inventory backed by a plain JSON file, queryable by tier, status, owner, and review due date
- An executive committee report (HTML and JSON) that pulls together model identity, tier, validation status, monitoring summary, material assumptions, outstanding issues, and sign-off chain into a 2-3 page governance pack
Installation
pip install insurance-mrm
No mandatory dependencies beyond the standard library. Optional: insurance-validation and insurance-monitoring for richer integration.
Quickstart
from insurance_mrm import (
ModelCard, Assumption, Limitation,
ModelInventory, RiskTierScorer, GovernanceReport,
)
# 1. Build a model card
card = ModelCard(
model_id='motor-freq-tppd-v2',
model_name='Motor TPPD Frequency',
version='2.1.0',
model_class='pricing',
intended_use='Frequency pricing for private motor. Not for commercial motor, fleet, or reserving.',
not_intended_for=['Commercial motor', 'Fleet', 'Reserving', 'Capital'],
target_variable='claim_count',
distribution_family='Poisson',
model_type='CatBoost',
rating_factors=['driver_age', 'vehicle_age', 'annual_mileage', 'region'],
training_data_period=('2019-01-01', '2023-12-31'),
developer='Pricing Team',
champion_challenger_status='champion',
gwp_impacted=125_000_000,
customer_facing=True,
regulatory_use=False,
assumptions=[
Assumption(
description='Claim frequency stationarity since 2022',
risk='MEDIUM',
mitigation='Quarterly A/E monitoring, PSI alert at >0.25',
),
Assumption(
description='Region as proxy for road density and theft risk',
risk='LOW',
),
],
limitations=[
Limitation(
description='Performance degrades for vehicles >10 years (thin training data)',
impact='Higher prediction variance for this segment',
population_at_risk='~8% of book',
),
],
outstanding_issues=['VIF check on driver_age x annual_mileage pending'],
approved_by=['Chief Actuary', 'Model Risk Committee'],
approval_date='2024-10-15',
approval_conditions='Subject to quarterly A/E monitoring',
next_review_date='2025-10-15',
monitoring_owner='Sarah Ahmed, Head of Pricing Analytics',
monitoring_frequency='Quarterly',
monitoring_triggers={'psi_score': 0.25, 'ae_ratio_deviation': 0.10, 'gini_drop_pct': 0.05},
)
# 2. Score the risk tier
scorer = RiskTierScorer()
tier = scorer.score(
gwp_impacted=125_000_000,
model_complexity='high', # 'low' | 'medium' | 'high'
deployment_status='champion', # 'champion' | 'challenger' | 'shadow' | 'development' | 'retired'
regulatory_use=False,
external_data=False,
customer_facing=True,
validation_months_ago=6.0,
drift_triggers_last_year=0,
)
print(f"Tier {tier.tier} ({tier.tier_label}): {tier.score:.1f}/100")
print(tier.rationale)
# 3. Register in the inventory
inventory = ModelInventory('mrm_registry.json')
inventory.register(card, tier)
due = inventory.due_for_review(within_days=60)
for model in due:
print(f"{model['model_name']} — review due {model['next_review_date']}")
# 4. Generate the governance pack
report = GovernanceReport(
card=card,
tier=tier,
validation_results={
'overall_rag': 'GREEN',
'run_id': 'a1b2c3d4-uuid',
'run_date': '2024-10-01',
'gini': 0.42,
'ae_ratio': 1.01,
'psi_score': 0.07,
},
monitoring_results={
'period': '2025-Q3',
'ae_ratio': 1.02,
'psi_score': 0.06,
'recommendation': 'Continue',
},
)
report.save_html('motor_freq_mrm_pack.html')
Risk tier scoring
The scorer produces a 0-100 composite score across 6 dimensions. Weights sum to 100 and are configurable.
| Dimension | What it measures | Max pts (default) |
|---|---|---|
| Materiality | GWP influenced by the model | 25 |
| Complexity | Model architecture and feature count | 20 |
| Data quality | Use of external data sources | 10 |
| Validation coverage | Months since last independent validation | 10 |
| Drift history | Monitoring trigger events in last 12 months | 10 |
| Regulatory exposure | Production status + regulatory use + customer-facing pricing | 25 |
Tier thresholds (defaults):
| Score | Tier | Label | Review | Sign-off |
|---|---|---|---|---|
| >= 60 | 1 | Critical | Annual | Model Risk Committee |
| 30-59 | 2 | High | 18 months | Chief Actuary |
| < 30 | 3 | Medium | 24 months | Head of Pricing |
Override weights and thresholds at construction:
scorer = RiskTierScorer(
weights={
'materiality': 35, 'complexity': 20, 'data_quality': 5,
'validation_coverage': 15, 'drift_history': 10, 'regulatory_exposure': 15,
},
thresholds={1: 65, 2: 35, 3: 0},
)
Model inventory
inventory = ModelInventory('mrm_registry.json')
# Register
inventory.register(card, tier)
# Query
rows = inventory.list()
rows = inventory.list(status='champion')
rows = inventory.list(tier=1)
rows = inventory.list(owner='Sarah')
due = inventory.due_for_review(within_days=60)
overdue = inventory.overdue()
# Update after validation
inventory.update_validation(
model_id='motor-freq-tppd-v2',
validation_date='2025-10-01',
overall_rag='GREEN',
next_review_date='2026-10-01',
run_id='uuid-from-insurance-validation',
)
# Audit log
inventory.log_event(
model_id='motor-freq-tppd-v2',
event_type='monitoring_trigger',
description='PSI exceeded 0.25 on driver_age feature',
)
print(inventory.summary())
The inventory file is plain JSON and git-auditable. Treat it as source-controlled documentation.
Regulatory context
Designed with PRA SS1/23 Principles 1 and 5 in mind. SS1/23 is currently scoped to banks but is expected to extend to insurers by 2026-2027. The tier scoring rubric and model card fields align with SS1/23 expectations.
FCA Consumer Duty (PRIN 2A.9) requires firms to regularly evidence customer outcomes. The monitoring plan fields and trigger thresholds on the model card support this documentation requirement.
Part of the Burning Cost ecosystem
insurance-mrm is library 28 in the Burning Cost portfolio. It is the governance wrapper for:
insurance-validation— point-in-time technical validation reportsinsurance-monitoring— ongoing operational monitoring
A team using all three has a complete, auditable, PRA-aligned pricing model governance workflow in Python.
Licence
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file insurance_mrm-0.1.0.tar.gz.
File metadata
- Download URL: insurance_mrm-0.1.0.tar.gz
- Upload date:
- Size: 40.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.10.8 {"installer":{"name":"uv","version":"0.10.8","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2ea235443fbf069987d24c4fcf197b698940f88688eab4fb1e0631b5328c1830
|
|
| MD5 |
6ade215c34c983572fbf0c73621ec3f7
|
|
| BLAKE2b-256 |
cfb3b6ece442cfdf5426b62ce1071da1178a13d59fb727ccd449bfa197f17913
|
File details
Details for the file insurance_mrm-0.1.0-py3-none-any.whl.
File metadata
- Download URL: insurance_mrm-0.1.0-py3-none-any.whl
- Upload date:
- Size: 28.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.10.8 {"installer":{"name":"uv","version":"0.10.8","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1516dea9b8d55aa7e1dc79bbbe90e0b965d5aae80e94ca546814494737e862cd
|
|
| MD5 |
4f634285ae771df5b3caadf1ae9807f7
|
|
| BLAKE2b-256 |
e5b43e7b4ce5932fe485827ccac3d2a32324620e3dc8a740d71bc275ec207ae8
|