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.
Files changed (31) hide show
  1. ins_pricing/CHANGELOG.md +179 -0
  2. ins_pricing/RELEASE_NOTES_0.2.8.md +344 -0
  3. ins_pricing/modelling/core/bayesopt/utils.py +2 -1
  4. ins_pricing/modelling/explain/shap_utils.py +209 -6
  5. ins_pricing/pricing/calibration.py +125 -1
  6. ins_pricing/pricing/factors.py +110 -1
  7. ins_pricing/production/preprocess.py +166 -0
  8. ins_pricing/setup.py +1 -1
  9. ins_pricing/tests/governance/__init__.py +1 -0
  10. ins_pricing/tests/governance/test_audit.py +56 -0
  11. ins_pricing/tests/governance/test_registry.py +128 -0
  12. ins_pricing/tests/governance/test_release.py +74 -0
  13. ins_pricing/tests/pricing/__init__.py +1 -0
  14. ins_pricing/tests/pricing/test_calibration.py +72 -0
  15. ins_pricing/tests/pricing/test_exposure.py +64 -0
  16. ins_pricing/tests/pricing/test_factors.py +156 -0
  17. ins_pricing/tests/pricing/test_rate_table.py +40 -0
  18. ins_pricing/tests/production/__init__.py +1 -0
  19. ins_pricing/tests/production/test_monitoring.py +350 -0
  20. ins_pricing/tests/production/test_predict.py +233 -0
  21. ins_pricing/tests/production/test_preprocess.py +339 -0
  22. ins_pricing/tests/production/test_scoring.py +311 -0
  23. ins_pricing/utils/profiling.py +377 -0
  24. ins_pricing/utils/validation.py +427 -0
  25. ins_pricing-0.2.9.dist-info/METADATA +149 -0
  26. {ins_pricing-0.2.7.dist-info → ins_pricing-0.2.9.dist-info}/RECORD +28 -12
  27. ins_pricing/CHANGELOG_20260114.md +0 -275
  28. ins_pricing/CODE_REVIEW_IMPROVEMENTS.md +0 -715
  29. ins_pricing-0.2.7.dist-info/METADATA +0 -101
  30. {ins_pricing-0.2.7.dist-info → ins_pricing-0.2.9.dist-info}/WHEEL +0 -0
  31. {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
@@ -3,7 +3,7 @@ from setuptools import setup, find_packages
3
3
 
4
4
  setup(
5
5
  name="ins_pricing",
6
- version="0.2.7",
6
+ version="0.2.9",
7
7
  description="Reusable modelling, pricing, governance, and reporting utilities.",
8
8
  author="meishi125478",
9
9
  license="Proprietary",
@@ -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