ins-pricing 0.2.7__py3-none-any.whl → 0.2.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ins_pricing/CHANGELOG.md +179 -0
- ins_pricing/RELEASE_NOTES_0.2.8.md +344 -0
- ins_pricing/modelling/core/bayesopt/utils.py +2 -1
- ins_pricing/modelling/explain/shap_utils.py +209 -6
- ins_pricing/pricing/calibration.py +125 -1
- ins_pricing/pricing/factors.py +110 -1
- ins_pricing/production/preprocess.py +166 -0
- ins_pricing/setup.py +1 -1
- ins_pricing/tests/governance/__init__.py +1 -0
- ins_pricing/tests/governance/test_audit.py +56 -0
- ins_pricing/tests/governance/test_registry.py +128 -0
- ins_pricing/tests/governance/test_release.py +74 -0
- ins_pricing/tests/pricing/__init__.py +1 -0
- ins_pricing/tests/pricing/test_calibration.py +72 -0
- ins_pricing/tests/pricing/test_exposure.py +64 -0
- ins_pricing/tests/pricing/test_factors.py +156 -0
- ins_pricing/tests/pricing/test_rate_table.py +40 -0
- ins_pricing/tests/production/__init__.py +1 -0
- ins_pricing/tests/production/test_monitoring.py +350 -0
- ins_pricing/tests/production/test_predict.py +233 -0
- ins_pricing/tests/production/test_preprocess.py +339 -0
- ins_pricing/tests/production/test_scoring.py +311 -0
- ins_pricing/utils/profiling.py +377 -0
- ins_pricing/utils/validation.py +427 -0
- ins_pricing-0.2.9.dist-info/METADATA +149 -0
- {ins_pricing-0.2.7.dist-info → ins_pricing-0.2.9.dist-info}/RECORD +28 -12
- ins_pricing/CHANGELOG_20260114.md +0 -275
- ins_pricing/CODE_REVIEW_IMPROVEMENTS.md +0 -715
- ins_pricing-0.2.7.dist-info/METADATA +0 -101
- {ins_pricing-0.2.7.dist-info → ins_pricing-0.2.9.dist-info}/WHEEL +0 -0
- {ins_pricing-0.2.7.dist-info → ins_pricing-0.2.9.dist-info}/top_level.txt +0 -0
|
@@ -1,3 +1,33 @@
|
|
|
1
|
+
"""Production preprocessing utilities for applying training-time transformations.
|
|
2
|
+
|
|
3
|
+
This module provides functions for loading and applying preprocessing artifacts
|
|
4
|
+
that were saved during model training. It ensures that production data undergoes
|
|
5
|
+
the same transformations as training data.
|
|
6
|
+
|
|
7
|
+
Typical workflow:
|
|
8
|
+
1. Load preprocessing artifacts from training
|
|
9
|
+
2. Prepare raw features (type conversion, missing value handling)
|
|
10
|
+
3. Apply full preprocessing pipeline (one-hot encoding, scaling)
|
|
11
|
+
|
|
12
|
+
Example:
|
|
13
|
+
>>> from ins_pricing.production.preprocess import load_preprocess_artifacts, apply_preprocess_artifacts
|
|
14
|
+
>>>
|
|
15
|
+
>>> # Load artifacts saved during training
|
|
16
|
+
>>> artifacts = load_preprocess_artifacts("models/my_model/preprocess_artifacts.json")
|
|
17
|
+
>>>
|
|
18
|
+
>>> # Apply to new production data
|
|
19
|
+
>>> import pandas as pd
|
|
20
|
+
>>> raw_data = pd.read_csv("new_policies.csv")
|
|
21
|
+
>>> preprocessed = apply_preprocess_artifacts(raw_data, artifacts)
|
|
22
|
+
>>>
|
|
23
|
+
>>> # Now ready for model prediction
|
|
24
|
+
>>> predictions = model.predict(preprocessed)
|
|
25
|
+
|
|
26
|
+
Note:
|
|
27
|
+
Preprocessing artifacts must match the exact configuration used during training
|
|
28
|
+
to ensure consistency between training and production predictions.
|
|
29
|
+
"""
|
|
30
|
+
|
|
1
31
|
from __future__ import annotations
|
|
2
32
|
|
|
3
33
|
import json
|
|
@@ -8,6 +38,39 @@ import pandas as pd
|
|
|
8
38
|
|
|
9
39
|
|
|
10
40
|
def load_preprocess_artifacts(path: str | Path) -> Dict[str, Any]:
|
|
41
|
+
"""Load preprocessing artifacts from a JSON file.
|
|
42
|
+
|
|
43
|
+
Preprocessing artifacts contain all information needed to transform
|
|
44
|
+
raw production data the same way as training data, including:
|
|
45
|
+
- Feature names and types
|
|
46
|
+
- Categorical feature categories
|
|
47
|
+
- Numeric feature scaling parameters (mean, scale)
|
|
48
|
+
- One-hot encoding configuration
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
path: Path to the preprocessing artifacts JSON file
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
Dictionary containing preprocessing configuration and parameters:
|
|
55
|
+
- factor_nmes: List of feature column names
|
|
56
|
+
- cate_list: List of categorical feature names
|
|
57
|
+
- num_features: List of numeric feature names
|
|
58
|
+
- cat_categories: Dict mapping categorical features to their categories
|
|
59
|
+
- numeric_scalers: Dict with scaling parameters (mean, scale) per feature
|
|
60
|
+
- var_nmes: List of final column names after preprocessing
|
|
61
|
+
- drop_first: Whether first category was dropped in one-hot encoding
|
|
62
|
+
|
|
63
|
+
Raises:
|
|
64
|
+
ValueError: If the artifact file is not a valid JSON dictionary
|
|
65
|
+
FileNotFoundError: If the artifact file does not exist
|
|
66
|
+
|
|
67
|
+
Example:
|
|
68
|
+
>>> artifacts = load_preprocess_artifacts("models/xgb_model/preprocess.json")
|
|
69
|
+
>>> print(artifacts.keys())
|
|
70
|
+
dict_keys(['factor_nmes', 'cate_list', 'num_features', ...])
|
|
71
|
+
>>> print(artifacts['factor_nmes'])
|
|
72
|
+
['age', 'gender', 'region', 'vehicle_age']
|
|
73
|
+
"""
|
|
11
74
|
artifact_path = Path(path)
|
|
12
75
|
payload = json.loads(artifact_path.read_text(encoding="utf-8", errors="replace"))
|
|
13
76
|
if not isinstance(payload, dict):
|
|
@@ -16,6 +79,52 @@ def load_preprocess_artifacts(path: str | Path) -> Dict[str, Any]:
|
|
|
16
79
|
|
|
17
80
|
|
|
18
81
|
def prepare_raw_features(df: pd.DataFrame, artifacts: Dict[str, Any]) -> pd.DataFrame:
|
|
82
|
+
"""Prepare raw features for preprocessing by handling types and missing values.
|
|
83
|
+
|
|
84
|
+
This function performs initial data preparation:
|
|
85
|
+
1. Ensures all required features exist (adds missing columns with NA)
|
|
86
|
+
2. Converts numeric features to numeric type (coercing errors to 0)
|
|
87
|
+
3. Converts categorical features to proper categorical type
|
|
88
|
+
4. Applies category constraints from training data
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
df: Raw input DataFrame with policy/claim data
|
|
92
|
+
artifacts: Preprocessing artifacts from load_preprocess_artifacts()
|
|
93
|
+
Must contain: factor_nmes, cate_list, num_features, cat_categories
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
DataFrame with:
|
|
97
|
+
- Only feature columns (factor_nmes)
|
|
98
|
+
- Numeric features as float64
|
|
99
|
+
- Categorical features as object or Categorical
|
|
100
|
+
- Missing columns filled with NA
|
|
101
|
+
- Invalid numeric values filled with 0
|
|
102
|
+
|
|
103
|
+
Example:
|
|
104
|
+
>>> raw_df = pd.DataFrame({
|
|
105
|
+
... 'age': ['25', '30', 'invalid'],
|
|
106
|
+
... 'gender': ['M', 'F', 'X'],
|
|
107
|
+
... 'missing_feature': [1, 2, 3]
|
|
108
|
+
... })
|
|
109
|
+
>>> artifacts = {
|
|
110
|
+
... 'factor_nmes': ['age', 'gender', 'region'],
|
|
111
|
+
... 'num_features': ['age'],
|
|
112
|
+
... 'cate_list': ['gender', 'region'],
|
|
113
|
+
... 'cat_categories': {'gender': ['M', 'F'], 'region': ['North', 'South']}
|
|
114
|
+
... }
|
|
115
|
+
>>> prepared = prepare_raw_features(raw_df, artifacts)
|
|
116
|
+
>>> print(prepared.dtypes)
|
|
117
|
+
age float64
|
|
118
|
+
gender category
|
|
119
|
+
region object
|
|
120
|
+
>>> print(prepared['age'].tolist())
|
|
121
|
+
[25.0, 30.0, 0.0] # 'invalid' coerced to 0
|
|
122
|
+
|
|
123
|
+
Note:
|
|
124
|
+
- Missing numeric values are filled with 0 (not NaN)
|
|
125
|
+
- Unknown categories are kept as-is (handled later in one-hot encoding)
|
|
126
|
+
- Extra columns in input df are dropped
|
|
127
|
+
"""
|
|
19
128
|
factor_nmes = list(artifacts.get("factor_nmes") or [])
|
|
20
129
|
cate_list = list(artifacts.get("cate_list") or [])
|
|
21
130
|
num_features = set(artifacts.get("num_features") or [])
|
|
@@ -42,6 +151,63 @@ def prepare_raw_features(df: pd.DataFrame, artifacts: Dict[str, Any]) -> pd.Data
|
|
|
42
151
|
|
|
43
152
|
|
|
44
153
|
def apply_preprocess_artifacts(df: pd.DataFrame, artifacts: Dict[str, Any]) -> pd.DataFrame:
|
|
154
|
+
"""Apply complete preprocessing pipeline to production data.
|
|
155
|
+
|
|
156
|
+
This is the main preprocessing function that applies the full transformation
|
|
157
|
+
pipeline used during training:
|
|
158
|
+
1. Prepare raw features (via prepare_raw_features)
|
|
159
|
+
2. One-hot encode categorical features
|
|
160
|
+
3. Standardize numeric features using training statistics
|
|
161
|
+
4. Align columns to match training data exactly
|
|
162
|
+
|
|
163
|
+
The output is ready for model prediction and guaranteed to have the same
|
|
164
|
+
column structure as the training data.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
df: Raw input DataFrame with policy/claim data
|
|
168
|
+
artifacts: Complete preprocessing artifacts dictionary containing:
|
|
169
|
+
- factor_nmes: Feature names
|
|
170
|
+
- cate_list: Categorical feature names
|
|
171
|
+
- num_features: Numeric feature names
|
|
172
|
+
- cat_categories: Categorical feature categories
|
|
173
|
+
- numeric_scalers: Dict with 'mean' and 'scale' for each numeric feature
|
|
174
|
+
- var_nmes: Final column names after preprocessing
|
|
175
|
+
- drop_first: Whether to drop first category in one-hot encoding
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
Preprocessed DataFrame ready for model prediction with:
|
|
179
|
+
- One-hot encoded categorical features
|
|
180
|
+
- Standardized numeric features: (value - mean) / scale
|
|
181
|
+
- Exact column structure matching training data
|
|
182
|
+
- Missing columns filled with 0
|
|
183
|
+
- dtype: int8 for one-hot columns, float64 for numeric
|
|
184
|
+
|
|
185
|
+
Raises:
|
|
186
|
+
KeyError: If artifacts are missing required keys
|
|
187
|
+
|
|
188
|
+
Example:
|
|
189
|
+
>>> # Complete preprocessing pipeline
|
|
190
|
+
>>> artifacts = load_preprocess_artifacts("models/xgb/preprocess.json")
|
|
191
|
+
>>> raw_data = pd.DataFrame({
|
|
192
|
+
... 'age': [25, 30, 35],
|
|
193
|
+
... 'gender': ['M', 'F', 'M'],
|
|
194
|
+
... 'region': ['North', 'South', 'East']
|
|
195
|
+
... })
|
|
196
|
+
>>> processed = apply_preprocess_artifacts(raw_data, artifacts)
|
|
197
|
+
>>> print(processed.shape)
|
|
198
|
+
(3, 50) # More columns after one-hot encoding
|
|
199
|
+
>>> print(processed.columns[:5])
|
|
200
|
+
Index(['age', 'gender_F', 'gender_M', 'region_East', 'region_North'], dtype='object')
|
|
201
|
+
>>> # Age is now standardized
|
|
202
|
+
>>> print(processed['age'].tolist())
|
|
203
|
+
[-0.52, 0.0, 0.52] # Standardized values
|
|
204
|
+
|
|
205
|
+
Note:
|
|
206
|
+
- Categorical features not seen during training will be ignored (dropped in one-hot)
|
|
207
|
+
- Numeric features are standardized using training mean and std
|
|
208
|
+
- Output column order matches training data exactly
|
|
209
|
+
- Use this function for production scoring to ensure consistency
|
|
210
|
+
"""
|
|
45
211
|
cate_list = list(artifacts.get("cate_list") or [])
|
|
46
212
|
num_features = list(artifacts.get("num_features") or [])
|
|
47
213
|
var_nmes = list(artifacts.get("var_nmes") or [])
|
ins_pricing/setup.py
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Tests for the governance module."""
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Tests for audit logging module."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from ins_pricing.exceptions import GovernanceError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TestAuditLogging:
|
|
11
|
+
"""Test audit logging functionality."""
|
|
12
|
+
|
|
13
|
+
def test_log_model_action(self, tmp_path):
|
|
14
|
+
"""Test logging a model action."""
|
|
15
|
+
from ins_pricing.governance.audit import AuditLogger
|
|
16
|
+
|
|
17
|
+
logger = AuditLogger(audit_dir=tmp_path)
|
|
18
|
+
logger.log(
|
|
19
|
+
action="model_registered",
|
|
20
|
+
model_name="test_model",
|
|
21
|
+
user="test_user",
|
|
22
|
+
details={"version": "1.0.0"}
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
logs = logger.get_logs(model_name="test_model")
|
|
26
|
+
assert len(logs) > 0
|
|
27
|
+
assert logs[0]["action"] == "model_registered"
|
|
28
|
+
|
|
29
|
+
def test_get_audit_trail(self, tmp_path):
|
|
30
|
+
"""Test retrieving complete audit trail."""
|
|
31
|
+
from ins_pricing.governance.audit import AuditLogger
|
|
32
|
+
|
|
33
|
+
logger = AuditLogger(audit_dir=tmp_path)
|
|
34
|
+
|
|
35
|
+
# Log multiple actions
|
|
36
|
+
logger.log("registered", "model_a", "user1")
|
|
37
|
+
logger.log("trained", "model_a", "user1")
|
|
38
|
+
logger.log("deployed", "model_a", "user2")
|
|
39
|
+
|
|
40
|
+
trail = logger.get_audit_trail("model_a")
|
|
41
|
+
|
|
42
|
+
assert len(trail) == 3
|
|
43
|
+
assert trail[-1]["action"] == "deployed"
|
|
44
|
+
|
|
45
|
+
def test_filter_logs_by_date(self, tmp_path):
|
|
46
|
+
"""Test filtering audit logs by date range."""
|
|
47
|
+
from ins_pricing.governance.audit import AuditLogger
|
|
48
|
+
|
|
49
|
+
logger = AuditLogger(audit_dir=tmp_path)
|
|
50
|
+
logger.log("action1", "model", "user")
|
|
51
|
+
|
|
52
|
+
# Filter by today
|
|
53
|
+
today = datetime.now().date()
|
|
54
|
+
logs = logger.get_logs(start_date=today, end_date=today)
|
|
55
|
+
|
|
56
|
+
assert len(logs) > 0
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Tests for model registry module."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import pytest
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
|
|
8
|
+
from ins_pricing.exceptions import GovernanceError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.fixture
|
|
12
|
+
def sample_model_metadata():
|
|
13
|
+
"""Sample model metadata."""
|
|
14
|
+
return {
|
|
15
|
+
"model_name": "test_model_v1",
|
|
16
|
+
"version": "1.0.0",
|
|
17
|
+
"created_at": datetime.now().isoformat(),
|
|
18
|
+
"model_type": "xgboost",
|
|
19
|
+
"metrics": {"mse": 100.5, "r2": 0.85},
|
|
20
|
+
"features": ["age", "premium", "region"],
|
|
21
|
+
"author": "test_user"
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TestModelRegistry:
|
|
26
|
+
"""Test model registry functionality."""
|
|
27
|
+
|
|
28
|
+
def test_register_new_model(self, tmp_path, sample_model_metadata):
|
|
29
|
+
"""Test registering a new model."""
|
|
30
|
+
from ins_pricing.governance.registry import ModelRegistry
|
|
31
|
+
|
|
32
|
+
registry = ModelRegistry(registry_path=tmp_path / "registry.json")
|
|
33
|
+
registry.register(sample_model_metadata)
|
|
34
|
+
|
|
35
|
+
assert registry.exists(sample_model_metadata["model_name"])
|
|
36
|
+
|
|
37
|
+
def test_duplicate_registration_error(self, tmp_path, sample_model_metadata):
|
|
38
|
+
"""Test error on duplicate model registration."""
|
|
39
|
+
from ins_pricing.governance.registry import ModelRegistry
|
|
40
|
+
|
|
41
|
+
registry = ModelRegistry(registry_path=tmp_path / "registry.json")
|
|
42
|
+
registry.register(sample_model_metadata)
|
|
43
|
+
|
|
44
|
+
with pytest.raises(GovernanceError):
|
|
45
|
+
registry.register(sample_model_metadata) # Duplicate
|
|
46
|
+
|
|
47
|
+
def test_get_model_metadata(self, tmp_path, sample_model_metadata):
|
|
48
|
+
"""Test retrieving model metadata."""
|
|
49
|
+
from ins_pricing.governance.registry import ModelRegistry
|
|
50
|
+
|
|
51
|
+
registry = ModelRegistry(registry_path=tmp_path / "registry.json")
|
|
52
|
+
registry.register(sample_model_metadata)
|
|
53
|
+
|
|
54
|
+
metadata = registry.get(sample_model_metadata["model_name"])
|
|
55
|
+
|
|
56
|
+
assert metadata["version"] == "1.0.0"
|
|
57
|
+
assert "metrics" in metadata
|
|
58
|
+
|
|
59
|
+
def test_list_all_models(self, tmp_path):
|
|
60
|
+
"""Test listing all registered models."""
|
|
61
|
+
from ins_pricing.governance.registry import ModelRegistry
|
|
62
|
+
|
|
63
|
+
registry = ModelRegistry(registry_path=tmp_path / "registry.json")
|
|
64
|
+
registry.register({"model_name": "model_a", "version": "1.0.0"})
|
|
65
|
+
registry.register({"model_name": "model_b", "version": "2.0.0"})
|
|
66
|
+
|
|
67
|
+
models = registry.list_all()
|
|
68
|
+
|
|
69
|
+
assert len(models) == 2
|
|
70
|
+
assert any(m["model_name"] == "model_a" for m in models)
|
|
71
|
+
|
|
72
|
+
def test_update_model_metadata(self, tmp_path, sample_model_metadata):
|
|
73
|
+
"""Test updating model metadata."""
|
|
74
|
+
from ins_pricing.governance.registry import ModelRegistry
|
|
75
|
+
|
|
76
|
+
registry = ModelRegistry(registry_path=tmp_path / "registry.json")
|
|
77
|
+
registry.register(sample_model_metadata)
|
|
78
|
+
|
|
79
|
+
# Update metrics
|
|
80
|
+
registry.update(
|
|
81
|
+
sample_model_metadata["model_name"],
|
|
82
|
+
{"metrics": {"mse": 95.0, "r2": 0.87}}
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
updated = registry.get(sample_model_metadata["model_name"])
|
|
86
|
+
assert updated["metrics"]["mse"] == 95.0
|
|
87
|
+
|
|
88
|
+
def test_delete_model(self, tmp_path, sample_model_metadata):
|
|
89
|
+
"""Test deleting a model from registry."""
|
|
90
|
+
from ins_pricing.governance.registry import ModelRegistry
|
|
91
|
+
|
|
92
|
+
registry = ModelRegistry(registry_path=tmp_path / "registry.json")
|
|
93
|
+
registry.register(sample_model_metadata)
|
|
94
|
+
|
|
95
|
+
registry.delete(sample_model_metadata["model_name"])
|
|
96
|
+
|
|
97
|
+
assert not registry.exists(sample_model_metadata["model_name"])
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class TestModelVersioning:
|
|
101
|
+
"""Test model versioning functionality."""
|
|
102
|
+
|
|
103
|
+
def test_register_multiple_versions(self, tmp_path):
|
|
104
|
+
"""Test registering multiple versions of same model."""
|
|
105
|
+
from ins_pricing.governance.registry import ModelRegistry
|
|
106
|
+
|
|
107
|
+
registry = ModelRegistry(registry_path=tmp_path / "registry.json")
|
|
108
|
+
|
|
109
|
+
registry.register({"model_name": "my_model", "version": "1.0.0"})
|
|
110
|
+
registry.register({"model_name": "my_model", "version": "1.1.0"})
|
|
111
|
+
|
|
112
|
+
versions = registry.get_versions("my_model")
|
|
113
|
+
|
|
114
|
+
assert len(versions) == 2
|
|
115
|
+
assert "1.0.0" in versions
|
|
116
|
+
assert "1.1.0" in versions
|
|
117
|
+
|
|
118
|
+
def test_get_latest_version(self, tmp_path):
|
|
119
|
+
"""Test getting the latest version of a model."""
|
|
120
|
+
from ins_pricing.governance.registry import ModelRegistry
|
|
121
|
+
|
|
122
|
+
registry = ModelRegistry(registry_path=tmp_path / "registry.json")
|
|
123
|
+
registry.register({"model_name": "my_model", "version": "1.0.0"})
|
|
124
|
+
registry.register({"model_name": "my_model", "version": "2.0.0"})
|
|
125
|
+
|
|
126
|
+
latest = registry.get_latest("my_model")
|
|
127
|
+
|
|
128
|
+
assert latest["version"] == "2.0.0"
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Tests for model release module."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
|
|
7
|
+
from ins_pricing.exceptions import GovernanceError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TestModelRelease:
|
|
11
|
+
"""Test model release workflow."""
|
|
12
|
+
|
|
13
|
+
def test_create_release(self, tmp_path):
|
|
14
|
+
"""Test creating a new model release."""
|
|
15
|
+
from ins_pricing.governance.release import ReleaseManager
|
|
16
|
+
|
|
17
|
+
manager = ReleaseManager(release_dir=tmp_path)
|
|
18
|
+
release_id = manager.create_release(
|
|
19
|
+
model_name="test_model",
|
|
20
|
+
version="1.0.0",
|
|
21
|
+
artifacts=["model.pkl", "config.json"]
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
assert release_id is not None
|
|
25
|
+
assert manager.release_exists(release_id)
|
|
26
|
+
|
|
27
|
+
def test_get_release_info(self, tmp_path):
|
|
28
|
+
"""Test getting release information."""
|
|
29
|
+
from ins_pricing.governance.release import ReleaseManager
|
|
30
|
+
|
|
31
|
+
manager = ReleaseManager(release_dir=tmp_path)
|
|
32
|
+
release_id = manager.create_release(
|
|
33
|
+
model_name="test_model",
|
|
34
|
+
version="1.0.0"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
info = manager.get_release_info(release_id)
|
|
38
|
+
|
|
39
|
+
assert info["model_name"] == "test_model"
|
|
40
|
+
assert info["version"] == "1.0.0"
|
|
41
|
+
|
|
42
|
+
def test_promote_release(self, tmp_path):
|
|
43
|
+
"""Test promoting a release to production."""
|
|
44
|
+
from ins_pricing.governance.release import ReleaseManager
|
|
45
|
+
|
|
46
|
+
manager = ReleaseManager(release_dir=tmp_path)
|
|
47
|
+
release_id = manager.create_release(
|
|
48
|
+
model_name="test_model",
|
|
49
|
+
version="1.0.0"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
manager.promote_to_production(release_id)
|
|
53
|
+
|
|
54
|
+
info = manager.get_release_info(release_id)
|
|
55
|
+
assert info["status"] == "production"
|
|
56
|
+
|
|
57
|
+
def test_rollback_release(self, tmp_path):
|
|
58
|
+
"""Test rolling back a release."""
|
|
59
|
+
from ins_pricing.governance.release import ReleaseManager
|
|
60
|
+
|
|
61
|
+
manager = ReleaseManager(release_dir=tmp_path)
|
|
62
|
+
|
|
63
|
+
# Create and promote two releases
|
|
64
|
+
release1 = manager.create_release("test_model", "1.0.0")
|
|
65
|
+
manager.promote_to_production(release1)
|
|
66
|
+
|
|
67
|
+
release2 = manager.create_release("test_model", "2.0.0")
|
|
68
|
+
manager.promote_to_production(release2)
|
|
69
|
+
|
|
70
|
+
# Rollback to version 1.0.0
|
|
71
|
+
manager.rollback_to(release1)
|
|
72
|
+
|
|
73
|
+
current = manager.get_production_release("test_model")
|
|
74
|
+
assert current["version"] == "1.0.0"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Tests for the pricing module."""
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Tests for pricing calibration module."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pandas as pd
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@pytest.fixture
|
|
9
|
+
def sample_model_predictions():
|
|
10
|
+
"""Sample model predictions and actuals."""
|
|
11
|
+
np.random.seed(42)
|
|
12
|
+
return pd.DataFrame({
|
|
13
|
+
"actual_loss": np.random.exponential(500, 1000),
|
|
14
|
+
"predicted_loss": np.random.exponential(480, 1000),
|
|
15
|
+
"exposure": np.ones(1000),
|
|
16
|
+
"premium": np.random.uniform(200, 1000, 1000)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TestGlobalCalibration:
|
|
21
|
+
"""Test global calibration methods."""
|
|
22
|
+
|
|
23
|
+
def test_multiplicative_calibration(self, sample_model_predictions):
|
|
24
|
+
"""Test multiplicative calibration factor."""
|
|
25
|
+
from ins_pricing.pricing.calibration import calibrate_multiplicative
|
|
26
|
+
|
|
27
|
+
calibration_factor = calibrate_multiplicative(
|
|
28
|
+
actual=sample_model_predictions["actual_loss"],
|
|
29
|
+
predicted=sample_model_predictions["predicted_loss"],
|
|
30
|
+
weights=sample_model_predictions["exposure"]
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
assert isinstance(calibration_factor, (int, float, np.number))
|
|
34
|
+
assert calibration_factor > 0
|
|
35
|
+
|
|
36
|
+
def test_additive_calibration(self, sample_model_predictions):
|
|
37
|
+
"""Test additive calibration adjustment."""
|
|
38
|
+
from ins_pricing.pricing.calibration import calibrate_additive
|
|
39
|
+
|
|
40
|
+
calibration_adjustment = calibrate_additive(
|
|
41
|
+
actual=sample_model_predictions["actual_loss"],
|
|
42
|
+
predicted=sample_model_predictions["predicted_loss"],
|
|
43
|
+
weights=sample_model_predictions["exposure"]
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
assert isinstance(calibration_adjustment, (int, float, np.number))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class TestSegmentCalibration:
|
|
50
|
+
"""Test segment-specific calibration."""
|
|
51
|
+
|
|
52
|
+
def test_calibrate_by_segment(self):
|
|
53
|
+
"""Test calibration within segments."""
|
|
54
|
+
from ins_pricing.pricing.calibration import calibrate_by_segment
|
|
55
|
+
|
|
56
|
+
df = pd.DataFrame({
|
|
57
|
+
"segment": ["A", "B", "A", "B", "A"] * 200,
|
|
58
|
+
"actual": np.random.exponential(500, 1000),
|
|
59
|
+
"predicted": np.random.exponential(480, 1000),
|
|
60
|
+
"exposure": np.ones(1000)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
calibrated = calibrate_by_segment(
|
|
64
|
+
df,
|
|
65
|
+
actual_col="actual",
|
|
66
|
+
pred_col="predicted",
|
|
67
|
+
segment_col="segment",
|
|
68
|
+
weight_col="exposure"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
assert "calibration_factor" in calibrated.columns
|
|
72
|
+
assert len(calibrated["segment"].unique()) == 2
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Tests for pricing exposure calculation module."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pandas as pd
|
|
5
|
+
import pytest
|
|
6
|
+
from datetime import datetime, timedelta
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.fixture
|
|
10
|
+
def sample_policy_dates():
|
|
11
|
+
"""Sample policy with start and end dates."""
|
|
12
|
+
start_date = datetime(2023, 1, 1)
|
|
13
|
+
return pd.DataFrame({
|
|
14
|
+
"policy_id": range(100),
|
|
15
|
+
"start_date": [start_date + timedelta(days=i) for i in range(100)],
|
|
16
|
+
"end_date": [start_date + timedelta(days=i+365) for i in range(100)],
|
|
17
|
+
"premium": np.random.uniform(200, 1000, 100)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TestExposureCalculation:
|
|
22
|
+
"""Test exposure calculation functions."""
|
|
23
|
+
|
|
24
|
+
def test_calculate_policy_exposure_years(self, sample_policy_dates):
|
|
25
|
+
"""Test calculating exposure in years."""
|
|
26
|
+
from ins_pricing.pricing.exposure import compute_exposure
|
|
27
|
+
|
|
28
|
+
df = compute_exposure(
|
|
29
|
+
sample_policy_dates,
|
|
30
|
+
start_col="start_date",
|
|
31
|
+
end_col="end_date",
|
|
32
|
+
time_unit="years"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
assert "exposure" in df.columns
|
|
36
|
+
assert all(df["exposure"] > 0)
|
|
37
|
+
assert all(df["exposure"] <= 1.1) # Roughly 1 year
|
|
38
|
+
|
|
39
|
+
def test_calculate_policy_exposure_days(self, sample_policy_dates):
|
|
40
|
+
"""Test calculating exposure in days."""
|
|
41
|
+
from ins_pricing.pricing.exposure import compute_exposure
|
|
42
|
+
|
|
43
|
+
df = compute_exposure(
|
|
44
|
+
sample_policy_dates,
|
|
45
|
+
start_col="start_date",
|
|
46
|
+
end_col="end_date",
|
|
47
|
+
time_unit="days"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
assert all(df["exposure"] >= 365)
|
|
51
|
+
assert all(df["exposure"] <= 366)
|
|
52
|
+
|
|
53
|
+
def test_partial_period_exposure(self):
|
|
54
|
+
"""Test exposure for partial periods."""
|
|
55
|
+
from ins_pricing.pricing.exposure import compute_exposure
|
|
56
|
+
|
|
57
|
+
df = pd.DataFrame({
|
|
58
|
+
"start_date": [datetime(2023, 1, 1)],
|
|
59
|
+
"end_date": [datetime(2023, 6, 30)] # 6 months
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
result = compute_exposure(df, "start_date", "end_date", time_unit="years")
|
|
63
|
+
|
|
64
|
+
assert 0.48 < result["exposure"].iloc[0] < 0.52 # Roughly 0.5 years
|