churnkit 0.75.0a1__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 (302) hide show
  1. churnkit-0.75.0a1.data/data/share/churnkit/exploration_notebooks/00_start_here.ipynb +647 -0
  2. churnkit-0.75.0a1.data/data/share/churnkit/exploration_notebooks/01_data_discovery.ipynb +1165 -0
  3. churnkit-0.75.0a1.data/data/share/churnkit/exploration_notebooks/01a_a_temporal_text_deep_dive.ipynb +961 -0
  4. churnkit-0.75.0a1.data/data/share/churnkit/exploration_notebooks/01a_temporal_deep_dive.ipynb +1690 -0
  5. churnkit-0.75.0a1.data/data/share/churnkit/exploration_notebooks/01b_temporal_quality.ipynb +679 -0
  6. churnkit-0.75.0a1.data/data/share/churnkit/exploration_notebooks/01c_temporal_patterns.ipynb +3305 -0
  7. churnkit-0.75.0a1.data/data/share/churnkit/exploration_notebooks/01d_event_aggregation.ipynb +1463 -0
  8. churnkit-0.75.0a1.data/data/share/churnkit/exploration_notebooks/02_column_deep_dive.ipynb +1430 -0
  9. churnkit-0.75.0a1.data/data/share/churnkit/exploration_notebooks/02a_text_columns_deep_dive.ipynb +854 -0
  10. churnkit-0.75.0a1.data/data/share/churnkit/exploration_notebooks/03_quality_assessment.ipynb +1639 -0
  11. churnkit-0.75.0a1.data/data/share/churnkit/exploration_notebooks/04_relationship_analysis.ipynb +1890 -0
  12. churnkit-0.75.0a1.data/data/share/churnkit/exploration_notebooks/05_multi_dataset.ipynb +1457 -0
  13. churnkit-0.75.0a1.data/data/share/churnkit/exploration_notebooks/06_feature_opportunities.ipynb +1624 -0
  14. churnkit-0.75.0a1.data/data/share/churnkit/exploration_notebooks/07_modeling_readiness.ipynb +780 -0
  15. churnkit-0.75.0a1.data/data/share/churnkit/exploration_notebooks/08_baseline_experiments.ipynb +979 -0
  16. churnkit-0.75.0a1.data/data/share/churnkit/exploration_notebooks/09_business_alignment.ipynb +572 -0
  17. churnkit-0.75.0a1.data/data/share/churnkit/exploration_notebooks/10_spec_generation.ipynb +1179 -0
  18. churnkit-0.75.0a1.data/data/share/churnkit/exploration_notebooks/11_scoring_validation.ipynb +1418 -0
  19. churnkit-0.75.0a1.data/data/share/churnkit/exploration_notebooks/12_view_documentation.ipynb +151 -0
  20. churnkit-0.75.0a1.dist-info/METADATA +229 -0
  21. churnkit-0.75.0a1.dist-info/RECORD +302 -0
  22. churnkit-0.75.0a1.dist-info/WHEEL +4 -0
  23. churnkit-0.75.0a1.dist-info/entry_points.txt +2 -0
  24. churnkit-0.75.0a1.dist-info/licenses/LICENSE +202 -0
  25. customer_retention/__init__.py +37 -0
  26. customer_retention/analysis/__init__.py +0 -0
  27. customer_retention/analysis/auto_explorer/__init__.py +62 -0
  28. customer_retention/analysis/auto_explorer/exploration_manager.py +470 -0
  29. customer_retention/analysis/auto_explorer/explorer.py +258 -0
  30. customer_retention/analysis/auto_explorer/findings.py +291 -0
  31. customer_retention/analysis/auto_explorer/layered_recommendations.py +485 -0
  32. customer_retention/analysis/auto_explorer/recommendation_builder.py +148 -0
  33. customer_retention/analysis/auto_explorer/recommendations.py +418 -0
  34. customer_retention/analysis/business/__init__.py +26 -0
  35. customer_retention/analysis/business/ab_test_designer.py +144 -0
  36. customer_retention/analysis/business/fairness_analyzer.py +166 -0
  37. customer_retention/analysis/business/intervention_matcher.py +121 -0
  38. customer_retention/analysis/business/report_generator.py +222 -0
  39. customer_retention/analysis/business/risk_profile.py +199 -0
  40. customer_retention/analysis/business/roi_analyzer.py +139 -0
  41. customer_retention/analysis/diagnostics/__init__.py +20 -0
  42. customer_retention/analysis/diagnostics/calibration_analyzer.py +133 -0
  43. customer_retention/analysis/diagnostics/cv_analyzer.py +144 -0
  44. customer_retention/analysis/diagnostics/error_analyzer.py +107 -0
  45. customer_retention/analysis/diagnostics/leakage_detector.py +394 -0
  46. customer_retention/analysis/diagnostics/noise_tester.py +140 -0
  47. customer_retention/analysis/diagnostics/overfitting_analyzer.py +190 -0
  48. customer_retention/analysis/diagnostics/segment_analyzer.py +122 -0
  49. customer_retention/analysis/discovery/__init__.py +8 -0
  50. customer_retention/analysis/discovery/config_generator.py +49 -0
  51. customer_retention/analysis/discovery/discovery_flow.py +19 -0
  52. customer_retention/analysis/discovery/type_inferencer.py +147 -0
  53. customer_retention/analysis/interpretability/__init__.py +13 -0
  54. customer_retention/analysis/interpretability/cohort_analyzer.py +185 -0
  55. customer_retention/analysis/interpretability/counterfactual.py +175 -0
  56. customer_retention/analysis/interpretability/individual_explainer.py +141 -0
  57. customer_retention/analysis/interpretability/pdp_generator.py +103 -0
  58. customer_retention/analysis/interpretability/shap_explainer.py +106 -0
  59. customer_retention/analysis/jupyter_save_hook.py +28 -0
  60. customer_retention/analysis/notebook_html_exporter.py +136 -0
  61. customer_retention/analysis/notebook_progress.py +60 -0
  62. customer_retention/analysis/plotly_preprocessor.py +154 -0
  63. customer_retention/analysis/recommendations/__init__.py +54 -0
  64. customer_retention/analysis/recommendations/base.py +158 -0
  65. customer_retention/analysis/recommendations/cleaning/__init__.py +11 -0
  66. customer_retention/analysis/recommendations/cleaning/consistency.py +107 -0
  67. customer_retention/analysis/recommendations/cleaning/deduplicate.py +94 -0
  68. customer_retention/analysis/recommendations/cleaning/impute.py +67 -0
  69. customer_retention/analysis/recommendations/cleaning/outlier.py +71 -0
  70. customer_retention/analysis/recommendations/datetime/__init__.py +3 -0
  71. customer_retention/analysis/recommendations/datetime/extract.py +149 -0
  72. customer_retention/analysis/recommendations/encoding/__init__.py +3 -0
  73. customer_retention/analysis/recommendations/encoding/categorical.py +114 -0
  74. customer_retention/analysis/recommendations/pipeline.py +74 -0
  75. customer_retention/analysis/recommendations/registry.py +76 -0
  76. customer_retention/analysis/recommendations/selection/__init__.py +3 -0
  77. customer_retention/analysis/recommendations/selection/drop_column.py +56 -0
  78. customer_retention/analysis/recommendations/transform/__init__.py +4 -0
  79. customer_retention/analysis/recommendations/transform/power.py +94 -0
  80. customer_retention/analysis/recommendations/transform/scale.py +112 -0
  81. customer_retention/analysis/visualization/__init__.py +15 -0
  82. customer_retention/analysis/visualization/chart_builder.py +2619 -0
  83. customer_retention/analysis/visualization/console.py +122 -0
  84. customer_retention/analysis/visualization/display.py +171 -0
  85. customer_retention/analysis/visualization/number_formatter.py +36 -0
  86. customer_retention/artifacts/__init__.py +3 -0
  87. customer_retention/artifacts/fit_artifact_registry.py +146 -0
  88. customer_retention/cli.py +93 -0
  89. customer_retention/core/__init__.py +0 -0
  90. customer_retention/core/compat/__init__.py +193 -0
  91. customer_retention/core/compat/detection.py +99 -0
  92. customer_retention/core/compat/ops.py +48 -0
  93. customer_retention/core/compat/pandas_backend.py +57 -0
  94. customer_retention/core/compat/spark_backend.py +75 -0
  95. customer_retention/core/components/__init__.py +11 -0
  96. customer_retention/core/components/base.py +79 -0
  97. customer_retention/core/components/components/__init__.py +13 -0
  98. customer_retention/core/components/components/deployer.py +26 -0
  99. customer_retention/core/components/components/explainer.py +26 -0
  100. customer_retention/core/components/components/feature_eng.py +33 -0
  101. customer_retention/core/components/components/ingester.py +34 -0
  102. customer_retention/core/components/components/profiler.py +34 -0
  103. customer_retention/core/components/components/trainer.py +38 -0
  104. customer_retention/core/components/components/transformer.py +36 -0
  105. customer_retention/core/components/components/validator.py +37 -0
  106. customer_retention/core/components/enums.py +33 -0
  107. customer_retention/core/components/orchestrator.py +94 -0
  108. customer_retention/core/components/registry.py +59 -0
  109. customer_retention/core/config/__init__.py +39 -0
  110. customer_retention/core/config/column_config.py +95 -0
  111. customer_retention/core/config/experiments.py +71 -0
  112. customer_retention/core/config/pipeline_config.py +117 -0
  113. customer_retention/core/config/source_config.py +83 -0
  114. customer_retention/core/utils/__init__.py +28 -0
  115. customer_retention/core/utils/leakage.py +85 -0
  116. customer_retention/core/utils/severity.py +53 -0
  117. customer_retention/core/utils/statistics.py +90 -0
  118. customer_retention/generators/__init__.py +0 -0
  119. customer_retention/generators/notebook_generator/__init__.py +167 -0
  120. customer_retention/generators/notebook_generator/base.py +55 -0
  121. customer_retention/generators/notebook_generator/cell_builder.py +49 -0
  122. customer_retention/generators/notebook_generator/config.py +47 -0
  123. customer_retention/generators/notebook_generator/databricks_generator.py +48 -0
  124. customer_retention/generators/notebook_generator/local_generator.py +48 -0
  125. customer_retention/generators/notebook_generator/project_init.py +174 -0
  126. customer_retention/generators/notebook_generator/runner.py +150 -0
  127. customer_retention/generators/notebook_generator/script_generator.py +110 -0
  128. customer_retention/generators/notebook_generator/stages/__init__.py +19 -0
  129. customer_retention/generators/notebook_generator/stages/base_stage.py +86 -0
  130. customer_retention/generators/notebook_generator/stages/s01_ingestion.py +100 -0
  131. customer_retention/generators/notebook_generator/stages/s02_profiling.py +95 -0
  132. customer_retention/generators/notebook_generator/stages/s03_cleaning.py +180 -0
  133. customer_retention/generators/notebook_generator/stages/s04_transformation.py +165 -0
  134. customer_retention/generators/notebook_generator/stages/s05_feature_engineering.py +115 -0
  135. customer_retention/generators/notebook_generator/stages/s06_feature_selection.py +97 -0
  136. customer_retention/generators/notebook_generator/stages/s07_model_training.py +176 -0
  137. customer_retention/generators/notebook_generator/stages/s08_deployment.py +81 -0
  138. customer_retention/generators/notebook_generator/stages/s09_monitoring.py +112 -0
  139. customer_retention/generators/notebook_generator/stages/s10_batch_inference.py +642 -0
  140. customer_retention/generators/notebook_generator/stages/s11_feature_store.py +348 -0
  141. customer_retention/generators/orchestration/__init__.py +23 -0
  142. customer_retention/generators/orchestration/code_generator.py +196 -0
  143. customer_retention/generators/orchestration/context.py +147 -0
  144. customer_retention/generators/orchestration/data_materializer.py +188 -0
  145. customer_retention/generators/orchestration/databricks_exporter.py +411 -0
  146. customer_retention/generators/orchestration/doc_generator.py +311 -0
  147. customer_retention/generators/pipeline_generator/__init__.py +26 -0
  148. customer_retention/generators/pipeline_generator/findings_parser.py +727 -0
  149. customer_retention/generators/pipeline_generator/generator.py +142 -0
  150. customer_retention/generators/pipeline_generator/models.py +166 -0
  151. customer_retention/generators/pipeline_generator/renderer.py +2125 -0
  152. customer_retention/generators/spec_generator/__init__.py +37 -0
  153. customer_retention/generators/spec_generator/databricks_generator.py +433 -0
  154. customer_retention/generators/spec_generator/generic_generator.py +373 -0
  155. customer_retention/generators/spec_generator/mlflow_pipeline_generator.py +685 -0
  156. customer_retention/generators/spec_generator/pipeline_spec.py +298 -0
  157. customer_retention/integrations/__init__.py +0 -0
  158. customer_retention/integrations/adapters/__init__.py +13 -0
  159. customer_retention/integrations/adapters/base.py +10 -0
  160. customer_retention/integrations/adapters/factory.py +25 -0
  161. customer_retention/integrations/adapters/feature_store/__init__.py +6 -0
  162. customer_retention/integrations/adapters/feature_store/base.py +57 -0
  163. customer_retention/integrations/adapters/feature_store/databricks.py +94 -0
  164. customer_retention/integrations/adapters/feature_store/feast_adapter.py +97 -0
  165. customer_retention/integrations/adapters/feature_store/local.py +75 -0
  166. customer_retention/integrations/adapters/mlflow/__init__.py +6 -0
  167. customer_retention/integrations/adapters/mlflow/base.py +32 -0
  168. customer_retention/integrations/adapters/mlflow/databricks.py +54 -0
  169. customer_retention/integrations/adapters/mlflow/experiment_tracker.py +161 -0
  170. customer_retention/integrations/adapters/mlflow/local.py +50 -0
  171. customer_retention/integrations/adapters/storage/__init__.py +5 -0
  172. customer_retention/integrations/adapters/storage/base.py +33 -0
  173. customer_retention/integrations/adapters/storage/databricks.py +76 -0
  174. customer_retention/integrations/adapters/storage/local.py +59 -0
  175. customer_retention/integrations/feature_store/__init__.py +47 -0
  176. customer_retention/integrations/feature_store/definitions.py +215 -0
  177. customer_retention/integrations/feature_store/manager.py +744 -0
  178. customer_retention/integrations/feature_store/registry.py +412 -0
  179. customer_retention/integrations/iteration/__init__.py +28 -0
  180. customer_retention/integrations/iteration/context.py +212 -0
  181. customer_retention/integrations/iteration/feedback_collector.py +184 -0
  182. customer_retention/integrations/iteration/orchestrator.py +168 -0
  183. customer_retention/integrations/iteration/recommendation_tracker.py +341 -0
  184. customer_retention/integrations/iteration/signals.py +212 -0
  185. customer_retention/integrations/llm_context/__init__.py +4 -0
  186. customer_retention/integrations/llm_context/context_builder.py +201 -0
  187. customer_retention/integrations/llm_context/prompts.py +100 -0
  188. customer_retention/integrations/streaming/__init__.py +103 -0
  189. customer_retention/integrations/streaming/batch_integration.py +149 -0
  190. customer_retention/integrations/streaming/early_warning_model.py +227 -0
  191. customer_retention/integrations/streaming/event_schema.py +214 -0
  192. customer_retention/integrations/streaming/online_store_writer.py +249 -0
  193. customer_retention/integrations/streaming/realtime_scorer.py +261 -0
  194. customer_retention/integrations/streaming/trigger_engine.py +293 -0
  195. customer_retention/integrations/streaming/window_aggregator.py +393 -0
  196. customer_retention/stages/__init__.py +0 -0
  197. customer_retention/stages/cleaning/__init__.py +9 -0
  198. customer_retention/stages/cleaning/base.py +28 -0
  199. customer_retention/stages/cleaning/missing_handler.py +160 -0
  200. customer_retention/stages/cleaning/outlier_handler.py +204 -0
  201. customer_retention/stages/deployment/__init__.py +28 -0
  202. customer_retention/stages/deployment/batch_scorer.py +106 -0
  203. customer_retention/stages/deployment/champion_challenger.py +299 -0
  204. customer_retention/stages/deployment/model_registry.py +182 -0
  205. customer_retention/stages/deployment/retraining_trigger.py +245 -0
  206. customer_retention/stages/features/__init__.py +73 -0
  207. customer_retention/stages/features/behavioral_features.py +266 -0
  208. customer_retention/stages/features/customer_segmentation.py +505 -0
  209. customer_retention/stages/features/feature_definitions.py +265 -0
  210. customer_retention/stages/features/feature_engineer.py +551 -0
  211. customer_retention/stages/features/feature_manifest.py +340 -0
  212. customer_retention/stages/features/feature_selector.py +239 -0
  213. customer_retention/stages/features/interaction_features.py +160 -0
  214. customer_retention/stages/features/temporal_features.py +243 -0
  215. customer_retention/stages/ingestion/__init__.py +9 -0
  216. customer_retention/stages/ingestion/load_result.py +32 -0
  217. customer_retention/stages/ingestion/loaders.py +195 -0
  218. customer_retention/stages/ingestion/source_registry.py +130 -0
  219. customer_retention/stages/modeling/__init__.py +31 -0
  220. customer_retention/stages/modeling/baseline_trainer.py +139 -0
  221. customer_retention/stages/modeling/cross_validator.py +125 -0
  222. customer_retention/stages/modeling/data_splitter.py +205 -0
  223. customer_retention/stages/modeling/feature_scaler.py +99 -0
  224. customer_retention/stages/modeling/hyperparameter_tuner.py +107 -0
  225. customer_retention/stages/modeling/imbalance_handler.py +282 -0
  226. customer_retention/stages/modeling/mlflow_logger.py +95 -0
  227. customer_retention/stages/modeling/model_comparator.py +149 -0
  228. customer_retention/stages/modeling/model_evaluator.py +138 -0
  229. customer_retention/stages/modeling/threshold_optimizer.py +131 -0
  230. customer_retention/stages/monitoring/__init__.py +37 -0
  231. customer_retention/stages/monitoring/alert_manager.py +328 -0
  232. customer_retention/stages/monitoring/drift_detector.py +201 -0
  233. customer_retention/stages/monitoring/performance_monitor.py +242 -0
  234. customer_retention/stages/preprocessing/__init__.py +5 -0
  235. customer_retention/stages/preprocessing/transformer_manager.py +284 -0
  236. customer_retention/stages/profiling/__init__.py +256 -0
  237. customer_retention/stages/profiling/categorical_distribution.py +269 -0
  238. customer_retention/stages/profiling/categorical_target_analyzer.py +274 -0
  239. customer_retention/stages/profiling/column_profiler.py +527 -0
  240. customer_retention/stages/profiling/distribution_analysis.py +483 -0
  241. customer_retention/stages/profiling/drift_detector.py +310 -0
  242. customer_retention/stages/profiling/feature_capacity.py +507 -0
  243. customer_retention/stages/profiling/pattern_analysis_config.py +513 -0
  244. customer_retention/stages/profiling/profile_result.py +212 -0
  245. customer_retention/stages/profiling/quality_checks.py +1632 -0
  246. customer_retention/stages/profiling/relationship_detector.py +256 -0
  247. customer_retention/stages/profiling/relationship_recommender.py +454 -0
  248. customer_retention/stages/profiling/report_generator.py +520 -0
  249. customer_retention/stages/profiling/scd_analyzer.py +151 -0
  250. customer_retention/stages/profiling/segment_analyzer.py +632 -0
  251. customer_retention/stages/profiling/segment_aware_outlier.py +265 -0
  252. customer_retention/stages/profiling/target_level_analyzer.py +217 -0
  253. customer_retention/stages/profiling/temporal_analyzer.py +388 -0
  254. customer_retention/stages/profiling/temporal_coverage.py +488 -0
  255. customer_retention/stages/profiling/temporal_feature_analyzer.py +692 -0
  256. customer_retention/stages/profiling/temporal_feature_engineer.py +703 -0
  257. customer_retention/stages/profiling/temporal_pattern_analyzer.py +636 -0
  258. customer_retention/stages/profiling/temporal_quality_checks.py +278 -0
  259. customer_retention/stages/profiling/temporal_target_analyzer.py +241 -0
  260. customer_retention/stages/profiling/text_embedder.py +87 -0
  261. customer_retention/stages/profiling/text_processor.py +115 -0
  262. customer_retention/stages/profiling/text_reducer.py +60 -0
  263. customer_retention/stages/profiling/time_series_profiler.py +303 -0
  264. customer_retention/stages/profiling/time_window_aggregator.py +376 -0
  265. customer_retention/stages/profiling/type_detector.py +382 -0
  266. customer_retention/stages/profiling/window_recommendation.py +288 -0
  267. customer_retention/stages/temporal/__init__.py +166 -0
  268. customer_retention/stages/temporal/access_guard.py +180 -0
  269. customer_retention/stages/temporal/cutoff_analyzer.py +235 -0
  270. customer_retention/stages/temporal/data_preparer.py +178 -0
  271. customer_retention/stages/temporal/point_in_time_join.py +134 -0
  272. customer_retention/stages/temporal/point_in_time_registry.py +148 -0
  273. customer_retention/stages/temporal/scenario_detector.py +163 -0
  274. customer_retention/stages/temporal/snapshot_manager.py +259 -0
  275. customer_retention/stages/temporal/synthetic_coordinator.py +66 -0
  276. customer_retention/stages/temporal/timestamp_discovery.py +531 -0
  277. customer_retention/stages/temporal/timestamp_manager.py +255 -0
  278. customer_retention/stages/transformation/__init__.py +13 -0
  279. customer_retention/stages/transformation/binary_handler.py +85 -0
  280. customer_retention/stages/transformation/categorical_encoder.py +245 -0
  281. customer_retention/stages/transformation/datetime_transformer.py +97 -0
  282. customer_retention/stages/transformation/numeric_transformer.py +181 -0
  283. customer_retention/stages/transformation/pipeline.py +257 -0
  284. customer_retention/stages/validation/__init__.py +60 -0
  285. customer_retention/stages/validation/adversarial_scoring_validator.py +205 -0
  286. customer_retention/stages/validation/business_sense_gate.py +173 -0
  287. customer_retention/stages/validation/data_quality_gate.py +235 -0
  288. customer_retention/stages/validation/data_validators.py +511 -0
  289. customer_retention/stages/validation/feature_quality_gate.py +183 -0
  290. customer_retention/stages/validation/gates.py +117 -0
  291. customer_retention/stages/validation/leakage_gate.py +352 -0
  292. customer_retention/stages/validation/model_validity_gate.py +213 -0
  293. customer_retention/stages/validation/pipeline_validation_runner.py +264 -0
  294. customer_retention/stages/validation/quality_scorer.py +544 -0
  295. customer_retention/stages/validation/rule_generator.py +57 -0
  296. customer_retention/stages/validation/scoring_pipeline_validator.py +446 -0
  297. customer_retention/stages/validation/timeseries_detector.py +769 -0
  298. customer_retention/transforms/__init__.py +47 -0
  299. customer_retention/transforms/artifact_store.py +50 -0
  300. customer_retention/transforms/executor.py +157 -0
  301. customer_retention/transforms/fitted.py +92 -0
  302. customer_retention/transforms/ops.py +148 -0
@@ -0,0 +1,204 @@
1
+ from dataclasses import dataclass
2
+ from enum import Enum
3
+ from typing import Optional
4
+
5
+ import numpy as np
6
+
7
+ from customer_retention.core.compat import Series
8
+
9
+
10
+ class OutlierDetectionMethod(str, Enum):
11
+ IQR = "iqr"
12
+ ZSCORE = "zscore"
13
+ MODIFIED_ZSCORE = "modified_zscore"
14
+ PERCENTILE = "percentile"
15
+
16
+
17
+ class OutlierTreatmentStrategy(str, Enum):
18
+ NONE = "none"
19
+ CAP_IQR = "cap_iqr"
20
+ CAP_PERCENTILE = "cap_percentile"
21
+ WINSORIZE = "winsorize"
22
+ CLIP = "clip"
23
+ LOG_TRANSFORM = "log_transform"
24
+ SQRT_TRANSFORM = "sqrt_transform"
25
+ DROP = "drop"
26
+ INDICATOR = "indicator"
27
+
28
+
29
+ @dataclass
30
+ class OutlierResult:
31
+ series: Series
32
+ method_used: OutlierDetectionMethod
33
+ strategy_used: OutlierTreatmentStrategy
34
+ outliers_detected: int
35
+ outliers_treated: int
36
+ lower_bound: Optional[float]
37
+ upper_bound: Optional[float]
38
+ outlier_mask: Optional[Series] = None
39
+ indicator_column: Optional[Series] = None
40
+ rows_dropped: int = 0
41
+ drop_mask: Optional[list[bool]] = None
42
+
43
+
44
+ class OutlierHandler:
45
+ def __init__(
46
+ self,
47
+ detection_method: OutlierDetectionMethod = OutlierDetectionMethod.IQR,
48
+ treatment_strategy: OutlierTreatmentStrategy = OutlierTreatmentStrategy.CAP_IQR,
49
+ iqr_multiplier: float = 1.5,
50
+ zscore_threshold: float = 3.0,
51
+ percentile_lower: float = 1,
52
+ percentile_upper: float = 99,
53
+ clip_min: Optional[float] = None,
54
+ clip_max: Optional[float] = None
55
+ ):
56
+ self.detection_method = detection_method
57
+ self.treatment_strategy = treatment_strategy
58
+ self.iqr_multiplier = iqr_multiplier
59
+ self.zscore_threshold = zscore_threshold
60
+ self.percentile_lower = percentile_lower
61
+ self.percentile_upper = percentile_upper
62
+ self.clip_min = clip_min
63
+ self.clip_max = clip_max
64
+ self._lower_bound: Optional[float] = None
65
+ self._upper_bound: Optional[float] = None
66
+ self._is_fitted = False
67
+
68
+ def detect(self, series: Series) -> OutlierResult:
69
+ clean = series.dropna()
70
+ lower, upper = self._compute_bounds(clean)
71
+ mask = (series < lower) | (series > upper)
72
+ mask = mask.fillna(False)
73
+
74
+ return OutlierResult(
75
+ series=series, method_used=self.detection_method,
76
+ strategy_used=self.treatment_strategy,
77
+ outliers_detected=int(mask.sum()), outliers_treated=0,
78
+ lower_bound=lower, upper_bound=upper, outlier_mask=mask
79
+ )
80
+
81
+ def fit(self, series: Series) -> "OutlierHandler":
82
+ clean = series.dropna()
83
+ self._lower_bound, self._upper_bound = self._compute_bounds(clean)
84
+ self._is_fitted = True
85
+ return self
86
+
87
+ def transform(self, series: Series) -> OutlierResult:
88
+ if not self._is_fitted:
89
+ raise ValueError("Handler not fitted. Call fit() or fit_transform() first.")
90
+ return self._apply_treatment(series, self._lower_bound, self._upper_bound)
91
+
92
+ def fit_transform(self, series: Series) -> OutlierResult:
93
+ self.fit(series)
94
+ return self._apply_treatment(series, self._lower_bound, self._upper_bound)
95
+
96
+ def _compute_bounds(self, clean: Series) -> tuple[float, float]:
97
+ if self.detection_method == OutlierDetectionMethod.IQR:
98
+ q1 = clean.quantile(0.25)
99
+ q3 = clean.quantile(0.75)
100
+ iqr = q3 - q1
101
+ return q1 - self.iqr_multiplier * iqr, q3 + self.iqr_multiplier * iqr
102
+
103
+ if self.detection_method == OutlierDetectionMethod.ZSCORE:
104
+ mean, std = clean.mean(), clean.std()
105
+ return mean - self.zscore_threshold * std, mean + self.zscore_threshold * std
106
+
107
+ if self.detection_method == OutlierDetectionMethod.MODIFIED_ZSCORE:
108
+ median = clean.median()
109
+ mad = np.abs(clean - median).median()
110
+ k = 1.4826
111
+ return median - 3.5 * k * mad, median + 3.5 * k * mad
112
+
113
+ if self.detection_method == OutlierDetectionMethod.PERCENTILE:
114
+ return clean.quantile(self.percentile_lower / 100), clean.quantile(self.percentile_upper / 100)
115
+
116
+ return clean.min(), clean.max()
117
+
118
+ def _apply_treatment(self, series: Series, lower: float, upper: float) -> OutlierResult:
119
+ mask = ((series < lower) | (series > upper)) & series.notna()
120
+ outliers_detected = int(mask.sum())
121
+ result_series = series.copy()
122
+
123
+ if self.treatment_strategy == OutlierTreatmentStrategy.NONE:
124
+ return OutlierResult(
125
+ series=result_series, method_used=self.detection_method,
126
+ strategy_used=self.treatment_strategy,
127
+ outliers_detected=outliers_detected, outliers_treated=0,
128
+ lower_bound=lower, upper_bound=upper, outlier_mask=mask
129
+ )
130
+
131
+ if self.treatment_strategy == OutlierTreatmentStrategy.INDICATOR:
132
+ indicator = mask.astype(int)
133
+ return OutlierResult(
134
+ series=result_series, method_used=self.detection_method,
135
+ strategy_used=self.treatment_strategy,
136
+ outliers_detected=outliers_detected, outliers_treated=0,
137
+ lower_bound=lower, upper_bound=upper, outlier_mask=mask,
138
+ indicator_column=indicator
139
+ )
140
+
141
+ if self.treatment_strategy == OutlierTreatmentStrategy.DROP:
142
+ return OutlierResult(
143
+ series=result_series, method_used=self.detection_method,
144
+ strategy_used=self.treatment_strategy,
145
+ outliers_detected=outliers_detected, outliers_treated=outliers_detected,
146
+ lower_bound=lower, upper_bound=upper, outlier_mask=mask,
147
+ rows_dropped=outliers_detected, drop_mask=mask.tolist()
148
+ )
149
+
150
+ if self.treatment_strategy in [OutlierTreatmentStrategy.CAP_IQR, OutlierTreatmentStrategy.WINSORIZE]:
151
+ result_series = result_series.clip(lower=lower, upper=upper)
152
+ return OutlierResult(
153
+ series=result_series, method_used=self.detection_method,
154
+ strategy_used=self.treatment_strategy,
155
+ outliers_detected=outliers_detected, outliers_treated=outliers_detected,
156
+ lower_bound=lower, upper_bound=upper, outlier_mask=mask
157
+ )
158
+
159
+ if self.treatment_strategy == OutlierTreatmentStrategy.CAP_PERCENTILE:
160
+ result_series = result_series.clip(lower=lower, upper=upper)
161
+ return OutlierResult(
162
+ series=result_series, method_used=self.detection_method,
163
+ strategy_used=self.treatment_strategy,
164
+ outliers_detected=outliers_detected, outliers_treated=outliers_detected,
165
+ lower_bound=lower, upper_bound=upper, outlier_mask=mask
166
+ )
167
+
168
+ if self.treatment_strategy == OutlierTreatmentStrategy.CLIP:
169
+ clip_lower = self.clip_min if self.clip_min is not None else lower
170
+ clip_upper = self.clip_max if self.clip_max is not None else upper
171
+ result_series = result_series.clip(lower=clip_lower, upper=clip_upper)
172
+ return OutlierResult(
173
+ series=result_series, method_used=self.detection_method,
174
+ strategy_used=self.treatment_strategy,
175
+ outliers_detected=outliers_detected, outliers_treated=outliers_detected,
176
+ lower_bound=clip_lower, upper_bound=clip_upper, outlier_mask=mask
177
+ )
178
+
179
+ if self.treatment_strategy == OutlierTreatmentStrategy.LOG_TRANSFORM:
180
+ if (series.dropna() < 0).any():
181
+ raise ValueError("Log transform requires non-negative values")
182
+ result_series = np.log1p(series)
183
+ return OutlierResult(
184
+ series=result_series, method_used=self.detection_method,
185
+ strategy_used=self.treatment_strategy,
186
+ outliers_detected=0, outliers_treated=0,
187
+ lower_bound=None, upper_bound=None
188
+ )
189
+
190
+ if self.treatment_strategy == OutlierTreatmentStrategy.SQRT_TRANSFORM:
191
+ result_series = np.sqrt(series)
192
+ return OutlierResult(
193
+ series=result_series, method_used=self.detection_method,
194
+ strategy_used=self.treatment_strategy,
195
+ outliers_detected=0, outliers_treated=0,
196
+ lower_bound=None, upper_bound=None
197
+ )
198
+
199
+ return OutlierResult(
200
+ series=result_series, method_used=self.detection_method,
201
+ strategy_used=self.treatment_strategy,
202
+ outliers_detected=outliers_detected, outliers_treated=0,
203
+ lower_bound=lower, upper_bound=upper, outlier_mask=mask
204
+ )
@@ -0,0 +1,28 @@
1
+ from .batch_scorer import BatchScorer, RiskSegment, ScoringConfig, ScoringResult
2
+ from .champion_challenger import (
3
+ ChampionChallenger,
4
+ ComparisonResult,
5
+ ModelRole,
6
+ PromotionCriteria,
7
+ RollbackManager,
8
+ RollbackPlan,
9
+ RollbackResult,
10
+ )
11
+ from .model_registry import ModelMetadata, ModelRegistry, ModelStage, RegistrationResult, ValidationResult
12
+ from .retraining_trigger import (
13
+ EvaluationResult,
14
+ RetrainingConfig,
15
+ RetrainingDecision,
16
+ RetrainingTrigger,
17
+ RetrainingTriggerType,
18
+ TriggerPriority,
19
+ )
20
+
21
+ __all__ = [
22
+ "ModelRegistry", "ModelStage", "ModelMetadata", "RegistrationResult", "ValidationResult",
23
+ "BatchScorer", "ScoringConfig", "ScoringResult", "RiskSegment",
24
+ "RetrainingTrigger", "RetrainingTriggerType", "TriggerPriority", "RetrainingDecision",
25
+ "RetrainingConfig", "EvaluationResult",
26
+ "ChampionChallenger", "ModelRole", "ComparisonResult", "PromotionCriteria",
27
+ "RollbackManager", "RollbackPlan", "RollbackResult"
28
+ ]
@@ -0,0 +1,106 @@
1
+ import time
2
+ from dataclasses import dataclass, field
3
+ from datetime import datetime
4
+ from typing import Any, List, Optional
5
+
6
+ from customer_retention.core.compat import DataFrame, pd
7
+ from customer_retention.core.components.enums import RiskSegment
8
+
9
+
10
+ @dataclass
11
+ class ScoringConfig:
12
+ model_name: str
13
+ model_stage: str = "Production"
14
+ feature_table: str = "customer_features"
15
+ output_table: str = "churn_predictions"
16
+ batch_size: int = 10000
17
+ parallelism: int = 8
18
+
19
+
20
+ @dataclass
21
+ class ScoringResult:
22
+ predictions: DataFrame
23
+ total_scored: int
24
+ scoring_duration_seconds: float
25
+ model_version: Optional[str] = None
26
+ feature_table_version: Optional[str] = None
27
+ errors: List[str] = field(default_factory=list)
28
+
29
+
30
+ class BatchScorer:
31
+ def __init__(self, model: Any, scaler: Any = None, threshold: float = 0.5,
32
+ model_version: Optional[str] = None, batch_size: int = 10000,
33
+ handle_nulls: str = "raise"):
34
+ self.model = model
35
+ self.scaler = scaler
36
+ self.threshold = threshold
37
+ self.model_version = model_version
38
+ self.batch_size = batch_size
39
+ self.handle_nulls = handle_nulls
40
+ self._segment_thresholds = {
41
+ RiskSegment.CRITICAL: 0.75,
42
+ RiskSegment.HIGH: 0.50,
43
+ RiskSegment.MEDIUM: 0.25,
44
+ RiskSegment.LOW: 0.0
45
+ }
46
+
47
+ def score(self, data: DataFrame, feature_columns: List[str],
48
+ id_column: str) -> ScoringResult:
49
+ start_time = time.time()
50
+ errors = []
51
+ missing_cols = [col for col in feature_columns if col not in data.columns]
52
+ if missing_cols:
53
+ raise ValueError(f"Missing feature columns: {missing_cols}")
54
+ features = data[feature_columns].copy()
55
+ if features.isnull().any().any():
56
+ if self.handle_nulls == "raise":
57
+ raise ValueError("Null values found in features")
58
+ elif self.handle_nulls == "fill_zero":
59
+ features = features.fillna(0)
60
+ elif self.handle_nulls == "fill_mean":
61
+ features = features.fillna(features.mean())
62
+ if self.scaler is not None:
63
+ features_scaled = self.scaler.transform(features)
64
+ else:
65
+ features_scaled = features.values
66
+ all_predictions = []
67
+ n_batches = (len(data) + self.batch_size - 1) // self.batch_size
68
+ for batch_idx in range(n_batches):
69
+ start_idx = batch_idx * self.batch_size
70
+ end_idx = min((batch_idx + 1) * self.batch_size, len(data))
71
+ batch_features = features_scaled[start_idx:end_idx]
72
+ batch_ids = data[id_column].iloc[start_idx:end_idx]
73
+ try:
74
+ probabilities = self.model.predict_proba(batch_features)[:, 1]
75
+ except Exception as e:
76
+ errors.append(f"Batch {batch_idx} error: {str(e)}")
77
+ continue
78
+ batch_df = pd.DataFrame({
79
+ "customer_id": batch_ids.values,
80
+ "churn_probability": probabilities,
81
+ "risk_segment": [self._assign_risk_segment(p) for p in probabilities],
82
+ "predicted_churn": (probabilities >= self.threshold).astype(int),
83
+ "score_timestamp": datetime.now()
84
+ })
85
+ if self.model_version:
86
+ batch_df["model_version"] = self.model_version
87
+ all_predictions.append(batch_df)
88
+ predictions_df = pd.concat(all_predictions, ignore_index=True) if all_predictions else pd.DataFrame()
89
+ duration = time.time() - start_time
90
+ return ScoringResult(
91
+ predictions=predictions_df,
92
+ total_scored=len(predictions_df),
93
+ scoring_duration_seconds=duration,
94
+ model_version=self.model_version,
95
+ errors=errors
96
+ )
97
+
98
+ def _assign_risk_segment(self, probability: float) -> str:
99
+ if probability >= self._segment_thresholds[RiskSegment.CRITICAL]:
100
+ return RiskSegment.CRITICAL.value
101
+ elif probability >= self._segment_thresholds[RiskSegment.HIGH]:
102
+ return RiskSegment.HIGH.value
103
+ elif probability >= self._segment_thresholds[RiskSegment.MEDIUM]:
104
+ return RiskSegment.MEDIUM.value
105
+ else:
106
+ return RiskSegment.LOW.value
@@ -0,0 +1,299 @@
1
+ import time
2
+ from dataclasses import dataclass, field
3
+ from datetime import datetime
4
+ from enum import Enum
5
+ from typing import Any, Dict, List, Optional
6
+
7
+ import numpy as np
8
+ from sklearn.metrics import auc, precision_recall_curve, precision_score, recall_score, roc_auc_score
9
+
10
+ from customer_retention.core.compat import DataFrame, Series
11
+
12
+
13
+ class ModelRole(Enum):
14
+ CHAMPION = "champion"
15
+ CHALLENGER = "challenger"
16
+ SHADOW = "shadow"
17
+
18
+
19
+ @dataclass
20
+ class PromotionCriteria:
21
+ min_pr_auc_improvement: float = 0.02
22
+ max_fairness_regression: float = 0.0
23
+ max_latency_ratio: float = 2.0
24
+ requires_validation_pass: bool = True
25
+ requires_business_approval: bool = True
26
+ requires_rollback_plan: bool = True
27
+
28
+
29
+ @dataclass
30
+ class ComparisonResult:
31
+ champion_metrics: Dict[str, Any]
32
+ challenger_metrics: Dict[str, Any]
33
+ pr_auc_improvement: float
34
+ recommendation: str
35
+ meets_promotion_criteria: bool
36
+ weighted_score_champion: Optional[float] = None
37
+ weighted_score_challenger: Optional[float] = None
38
+ fairness_comparison: Optional[Dict] = None
39
+ latency_comparison: Optional[Dict] = None
40
+ comparison_timestamp: datetime = field(default_factory=datetime.now)
41
+
42
+
43
+ @dataclass
44
+ class RollbackPlan:
45
+ current_model_name: str
46
+ current_version: str
47
+ rollback_model_name: str
48
+ rollback_version: str
49
+ estimated_duration_minutes: float = 5.0
50
+ steps: List[str] = field(default_factory=list)
51
+
52
+
53
+ @dataclass
54
+ class RollbackResult:
55
+ success: bool
56
+ from_version: str
57
+ to_version: str
58
+ duration_seconds: float
59
+ error: Optional[str] = None
60
+
61
+
62
+ class ChampionChallenger:
63
+ def __init__(self, weights: Optional[Dict[str, float]] = None,
64
+ promotion_criteria: Optional[PromotionCriteria] = None):
65
+ self.weights = weights or {
66
+ "pr_auc": 0.40,
67
+ "stability": 0.20,
68
+ "business_roi": 0.25,
69
+ "latency": 0.10,
70
+ "fairness": 0.05
71
+ }
72
+ self.promotion_criteria = promotion_criteria or PromotionCriteria()
73
+ self.champion = None
74
+ self.champion_name = None
75
+ self.champion_version = None
76
+ self.challenger = None
77
+ self.challenger_name = None
78
+ self.challenger_version = None
79
+ self.shadow_models: List[Dict] = []
80
+ self._comparison_history: List[ComparisonResult] = []
81
+
82
+ def set_champion(self, model: Any, model_name: str, version: str):
83
+ self.champion = model
84
+ self.champion_name = model_name
85
+ self.champion_version = version
86
+
87
+ def set_challenger(self, model: Any, model_name: str, version: str):
88
+ self.challenger = model
89
+ self.challenger_name = model_name
90
+ self.challenger_version = version
91
+
92
+ def add_shadow(self, model: Any, model_name: str, version: str):
93
+ self.shadow_models.append({
94
+ "model": model,
95
+ "name": model_name,
96
+ "version": version
97
+ })
98
+
99
+ def compare(self, X: DataFrame, y: Series,
100
+ protected_attribute: Optional[Series] = None,
101
+ include_stability: bool = False,
102
+ include_latency: bool = False) -> ComparisonResult:
103
+ champion_metrics = self._evaluate_model(self.champion, X, y, include_latency)
104
+ challenger_metrics = self._evaluate_model(self.challenger, X, y, include_latency)
105
+ if include_stability:
106
+ champion_metrics["cv_std"] = self._compute_stability(self.champion, X, y)
107
+ challenger_metrics["cv_std"] = self._compute_stability(self.challenger, X, y)
108
+ pr_auc_improvement = challenger_metrics["pr_auc"] - champion_metrics["pr_auc"]
109
+ fairness_comparison = None
110
+ if protected_attribute is not None:
111
+ fairness_comparison = self._compare_fairness(X, y, protected_attribute)
112
+ champion_metrics["fairness_metrics"] = fairness_comparison.get("champion")
113
+ challenger_metrics["fairness_metrics"] = fairness_comparison.get("challenger")
114
+ latency_comparison = None
115
+ if include_latency:
116
+ latency_comparison = {
117
+ "champion_ms": champion_metrics.get("latency_ms"),
118
+ "challenger_ms": challenger_metrics.get("latency_ms")
119
+ }
120
+ weighted_champion = self._compute_weighted_score(champion_metrics)
121
+ weighted_challenger = self._compute_weighted_score(challenger_metrics)
122
+ meets_criteria = self._check_promotion_criteria(
123
+ pr_auc_improvement, fairness_comparison, latency_comparison, champion_metrics, challenger_metrics
124
+ )
125
+ if meets_criteria and pr_auc_improvement >= self.promotion_criteria.min_pr_auc_improvement:
126
+ recommendation = "promote_challenger"
127
+ else:
128
+ recommendation = "keep_champion"
129
+ result = ComparisonResult(
130
+ champion_metrics=champion_metrics,
131
+ challenger_metrics=challenger_metrics,
132
+ pr_auc_improvement=pr_auc_improvement,
133
+ recommendation=recommendation,
134
+ meets_promotion_criteria=meets_criteria,
135
+ weighted_score_champion=weighted_champion,
136
+ weighted_score_challenger=weighted_challenger,
137
+ fairness_comparison=fairness_comparison,
138
+ latency_comparison=latency_comparison
139
+ )
140
+ self._comparison_history.append(result)
141
+ return result
142
+
143
+ def _evaluate_model(self, model: Any, X: DataFrame, y: Series,
144
+ include_latency: bool = False) -> Dict[str, Any]:
145
+ start_time = time.time()
146
+ y_prob = model.predict_proba(X)[:, 1]
147
+ latency_ms = (time.time() - start_time) * 1000
148
+ y_pred = (y_prob >= 0.5).astype(int)
149
+ precision, recall, _ = precision_recall_curve(y, y_prob)
150
+ pr_auc = auc(recall, precision)
151
+ roc_auc = roc_auc_score(y, y_prob)
152
+ metrics = {
153
+ "pr_auc": pr_auc,
154
+ "roc_auc": roc_auc,
155
+ "precision": precision_score(y, y_pred),
156
+ "recall": recall_score(y, y_pred)
157
+ }
158
+ if include_latency:
159
+ metrics["latency_ms"] = latency_ms
160
+ return metrics
161
+
162
+ def _compute_stability(self, model: Any, X: DataFrame, y: Series, n_splits: int = 5) -> float:
163
+ from sklearn.model_selection import cross_val_score
164
+ scores = cross_val_score(model, X, y, cv=n_splits, scoring="roc_auc")
165
+ return scores.std()
166
+
167
+ def _compare_fairness(self, X: DataFrame, y: Series,
168
+ protected: Series) -> Dict:
169
+ champion_probs = self.champion.predict_proba(X)[:, 1]
170
+ challenger_probs = self.challenger.predict_proba(X)[:, 1]
171
+ groups = protected.unique()
172
+ champion_fairness = {}
173
+ challenger_fairness = {}
174
+ for group in groups:
175
+ mask = protected == group
176
+ champion_fairness[group] = np.mean(champion_probs[mask])
177
+ challenger_fairness[group] = np.mean(challenger_probs[mask])
178
+ return {
179
+ "champion": champion_fairness,
180
+ "challenger": challenger_fairness
181
+ }
182
+
183
+ def _compute_weighted_score(self, metrics: Dict[str, Any]) -> float:
184
+ score = 0.0
185
+ if "pr_auc" in metrics:
186
+ score += self.weights.get("pr_auc", 0) * metrics["pr_auc"]
187
+ if "cv_std" in metrics:
188
+ stability_score = 1 - min(metrics["cv_std"] * 10, 1)
189
+ score += self.weights.get("stability", 0) * stability_score
190
+ return score
191
+
192
+ def _check_promotion_criteria(self, pr_auc_improvement: float,
193
+ fairness_comparison: Optional[Dict],
194
+ latency_comparison: Optional[Dict],
195
+ champion_metrics: Dict,
196
+ challenger_metrics: Dict) -> bool:
197
+ if pr_auc_improvement < self.promotion_criteria.min_pr_auc_improvement:
198
+ return False
199
+ if latency_comparison:
200
+ champ_latency = latency_comparison.get("champion_ms", 1)
201
+ chall_latency = latency_comparison.get("challenger_ms", 1)
202
+ if champ_latency and chall_latency:
203
+ if chall_latency > champ_latency * self.promotion_criteria.max_latency_ratio:
204
+ return False
205
+ return True
206
+
207
+ def score_with_shadow(self, X: DataFrame) -> Dict[str, Any]:
208
+ champion_preds = self.champion.predict_proba(X)[:, 1]
209
+ shadow_preds = {}
210
+ for shadow in self.shadow_models:
211
+ shadow_preds[shadow["name"]] = shadow["model"].predict_proba(X)[:, 1]
212
+ return {
213
+ "champion_predictions": champion_preds,
214
+ "shadow_predictions": shadow_preds,
215
+ "active_predictions": champion_preds
216
+ }
217
+
218
+ def get_comparison_history(self) -> List[ComparisonResult]:
219
+ return self._comparison_history.copy()
220
+
221
+ def generate_report(self) -> str:
222
+ if not self._comparison_history:
223
+ return "No comparisons performed"
224
+ latest = self._comparison_history[-1]
225
+ report = f"""Champion vs Challenger Report
226
+ =============================
227
+ Champion Metrics:
228
+ PR-AUC: {latest.champion_metrics.get('pr_auc', 'N/A'):.4f}
229
+ ROC-AUC: {latest.champion_metrics.get('roc_auc', 'N/A'):.4f}
230
+
231
+ Challenger Metrics:
232
+ PR-AUC: {latest.challenger_metrics.get('pr_auc', 'N/A'):.4f}
233
+ ROC-AUC: {latest.challenger_metrics.get('roc_auc', 'N/A'):.4f}
234
+
235
+ PR-AUC Improvement: {latest.pr_auc_improvement:.4f}
236
+ Meets Promotion Criteria: {latest.meets_promotion_criteria}
237
+ Recommendation: {latest.recommendation}
238
+ """
239
+ return report
240
+
241
+
242
+ class RollbackManager:
243
+ def __init__(self, notify_on_rollback: bool = True):
244
+ self.notify_on_rollback = notify_on_rollback
245
+
246
+ def create_plan(self, current_model_name: str, current_version: str,
247
+ rollback_model_name: str, rollback_version: str) -> RollbackPlan:
248
+ steps = [
249
+ "1. Stop scoring with current model",
250
+ "2. Load rollback model from registry",
251
+ "3. Validate rollback model",
252
+ "4. Switch production pointer to rollback model",
253
+ "5. Notify stakeholders",
254
+ "6. Document incident"
255
+ ]
256
+ return RollbackPlan(
257
+ current_model_name=current_model_name,
258
+ current_version=current_version,
259
+ rollback_model_name=rollback_model_name,
260
+ rollback_version=rollback_version,
261
+ estimated_duration_minutes=5.0,
262
+ steps=steps
263
+ )
264
+
265
+ def execute_rollback(self, model_name: str, from_version: str,
266
+ to_version: str) -> RollbackResult:
267
+ start_time = time.time()
268
+ try:
269
+ from customer_retention.stages.deployment.model_registry import ModelRegistry, ModelStage
270
+ registry = ModelRegistry()
271
+ registry.transition_stage(model_name, to_version, ModelStage.PRODUCTION)
272
+ registry.transition_stage(model_name, from_version, ModelStage.ARCHIVED)
273
+ if self.notify_on_rollback:
274
+ from customer_retention.stages.monitoring.alert_manager import Alert, AlertLevel, AlertManager
275
+ alert_manager = AlertManager()
276
+ alert = Alert(
277
+ alert_id=f"rollback_{datetime.now().strftime('%Y%m%d%H%M%S')}",
278
+ condition_id="ROLLBACK",
279
+ level=AlertLevel.CRITICAL,
280
+ message=f"Model rollback executed: {model_name} from v{from_version} to v{to_version}",
281
+ timestamp=datetime.now()
282
+ )
283
+ alert_manager.send_alert(alert)
284
+ duration = time.time() - start_time
285
+ return RollbackResult(
286
+ success=True,
287
+ from_version=from_version,
288
+ to_version=to_version,
289
+ duration_seconds=duration
290
+ )
291
+ except Exception as e:
292
+ duration = time.time() - start_time
293
+ return RollbackResult(
294
+ success=False,
295
+ from_version=from_version,
296
+ to_version=to_version,
297
+ duration_seconds=duration,
298
+ error=str(e)
299
+ )