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,1418 @@
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "32f00772",
6
+ "metadata": {
7
+ "papermill": {
8
+ "duration": 0.003366,
9
+ "end_time": "2026-02-02T13:04:20.028364",
10
+ "exception": false,
11
+ "start_time": "2026-02-02T13:04:20.024998",
12
+ "status": "completed"
13
+ },
14
+ "tags": []
15
+ },
16
+ "source": [
17
+ "# Chapter 11: Scoring, Validation & Explanations\n",
18
+ "\n",
19
+ "End-to-end scoring pipeline with holdout validation, model comparison, adversarial\n",
20
+ "validation, SHAP explanations, and error analysis.\n",
21
+ "\n",
22
+ "**Sections:**\n",
23
+ "1. Run Scoring\n",
24
+ "2. Summary Metrics\n",
25
+ "3. Model Comparison Grid\n",
26
+ "4. Adversarial Pipeline Validation\n",
27
+ "5. Transformation Validation\n",
28
+ "6. Model Explanations (SHAP)\n",
29
+ "7. Customer Browser\n",
30
+ "8. Error Analysis\n",
31
+ "9. Export Results"
32
+ ]
33
+ },
34
+ {
35
+ "cell_type": "code",
36
+ "execution_count": null,
37
+ "id": "3dd9c433",
38
+ "metadata": {
39
+ "execution": {
40
+ "iopub.execute_input": "2026-02-02T13:04:20.034460Z",
41
+ "iopub.status.busy": "2026-02-02T13:04:20.034309Z",
42
+ "iopub.status.idle": "2026-02-02T13:04:20.383463Z",
43
+ "shell.execute_reply": "2026-02-02T13:04:20.382527Z"
44
+ },
45
+ "papermill": {
46
+ "duration": 0.353477,
47
+ "end_time": "2026-02-02T13:04:20.384447",
48
+ "exception": false,
49
+ "start_time": "2026-02-02T13:04:20.030970",
50
+ "status": "completed"
51
+ },
52
+ "tags": []
53
+ },
54
+ "outputs": [],
55
+ "source": [
56
+ "from customer_retention.analysis.notebook_progress import track_and_export_previous\n",
57
+ "track_and_export_previous(\"11_scoring_validation.ipynb\")\n",
58
+ "\n",
59
+ "import sys\n",
60
+ "from pathlib import Path\n",
61
+ "\n",
62
+ "from customer_retention.core.config.experiments import EXPERIMENTS_DIR, FINDINGS_DIR"
63
+ ]
64
+ },
65
+ {
66
+ "cell_type": "code",
67
+ "execution_count": null,
68
+ "id": "2df62fbd",
69
+ "metadata": {
70
+ "execution": {
71
+ "iopub.execute_input": "2026-02-02T13:04:20.394211Z",
72
+ "iopub.status.busy": "2026-02-02T13:04:20.394015Z",
73
+ "iopub.status.idle": "2026-02-02T13:04:20.399431Z",
74
+ "shell.execute_reply": "2026-02-02T13:04:20.398670Z"
75
+ },
76
+ "papermill": {
77
+ "duration": 0.013077,
78
+ "end_time": "2026-02-02T13:04:20.400068",
79
+ "exception": false,
80
+ "start_time": "2026-02-02T13:04:20.386991",
81
+ "status": "completed"
82
+ },
83
+ "tags": []
84
+ },
85
+ "outputs": [],
86
+ "source": [
87
+ "# Discover the generated pipeline directory\n",
88
+ "generated_dir = Path(\"../generated_pipelines/local\")\n",
89
+ "pipeline_dirs = sorted(generated_dir.glob(\"*/config.py\"))\n",
90
+ "if not pipeline_dirs:\n",
91
+ " raise FileNotFoundError(\n",
92
+ " f\"No generated pipeline found under {generated_dir}. Run notebook 10 first.\"\n",
93
+ " )\n",
94
+ "PIPELINE_DIR = pipeline_dirs[-1].parent\n",
95
+ "sys.path.insert(0, str(PIPELINE_DIR))\n",
96
+ "\n",
97
+ "from config import (\n",
98
+ " PIPELINE_NAME, TARGET_COLUMN, RECOMMENDATIONS_HASH, MLFLOW_TRACKING_URI,\n",
99
+ " FEAST_REPO_PATH, FEAST_FEATURE_VIEW, FEAST_ENTITY_KEY, FEAST_TIMESTAMP_COL,\n",
100
+ " PRODUCTION_DIR, EXPERIMENTS_DIR as GEN_EXPERIMENTS_DIR,\n",
101
+ " get_feast_data_path, get_gold_path, ARTIFACTS_PATH,\n",
102
+ ")\n",
103
+ "\n",
104
+ "print(f\"Pipeline: {PIPELINE_NAME}\")\n",
105
+ "print(f\"Pipeline dir: {PIPELINE_DIR}\")\n",
106
+ "print(f\"Experiments dir: {GEN_EXPERIMENTS_DIR}\")\n",
107
+ "print(f\"Recommendations hash: {RECOMMENDATIONS_HASH}\")"
108
+ ]
109
+ },
110
+ {
111
+ "cell_type": "markdown",
112
+ "id": "cdb7a547",
113
+ "metadata": {
114
+ "papermill": {
115
+ "duration": 0.002345,
116
+ "end_time": "2026-02-02T13:04:20.404863",
117
+ "exception": false,
118
+ "start_time": "2026-02-02T13:04:20.402518",
119
+ "status": "completed"
120
+ },
121
+ "tags": []
122
+ },
123
+ "source": [
124
+ "## 11.1 Run Scoring"
125
+ ]
126
+ },
127
+ {
128
+ "cell_type": "code",
129
+ "execution_count": null,
130
+ "id": "72417bd4",
131
+ "metadata": {
132
+ "execution": {
133
+ "iopub.execute_input": "2026-02-02T13:04:20.410359Z",
134
+ "iopub.status.busy": "2026-02-02T13:04:20.410203Z",
135
+ "iopub.status.idle": "2026-02-02T13:04:23.668644Z",
136
+ "shell.execute_reply": "2026-02-02T13:04:23.668162Z"
137
+ },
138
+ "papermill": {
139
+ "duration": 3.263488,
140
+ "end_time": "2026-02-02T13:04:23.670606",
141
+ "exception": false,
142
+ "start_time": "2026-02-02T13:04:20.407118",
143
+ "status": "completed"
144
+ },
145
+ "tags": []
146
+ },
147
+ "outputs": [],
148
+ "source": [
149
+ "import numpy as np\n",
150
+ "import pandas as pd\n",
151
+ "import mlflow\n",
152
+ "import mlflow.sklearn\n",
153
+ "import mlflow.xgboost\n",
154
+ "import xgboost as xgb\n",
155
+ "from feast import FeatureStore\n",
156
+ "from customer_retention.transforms import TransformExecutor, ArtifactStore\n",
157
+ "from customer_retention.generators.pipeline_generator.models import (\n",
158
+ " PipelineTransformationType, TransformationStep,\n",
159
+ ")\n",
160
+ "from config import EXCLUDED_SOURCES\n",
161
+ "\n",
162
+ "_registry = ArtifactStore.from_manifest(Path(ARTIFACTS_PATH) / \"manifest.yaml\")\n",
163
+ "_executor = TransformExecutor()\n",
164
+ "\n",
165
+ "# Import encoding/scaling steps from gold module\n",
166
+ "sys.path.insert(0, str(PIPELINE_DIR / \"gold\"))\n",
167
+ "from gold_features import ENCODINGS, SCALINGS, load_gold\n",
168
+ "\n",
169
+ "ORIGINAL_COLUMN = f\"original_{TARGET_COLUMN}\"\n",
170
+ "PREDICTIONS_PATH = PRODUCTION_DIR / \"data\" / \"scoring\" / \"predictions.parquet\"\n",
171
+ "\n",
172
+ "# Set tracking URI\n",
173
+ "mlflow.set_tracking_uri(MLFLOW_TRACKING_URI)\n",
174
+ "\n",
175
+ "\n",
176
+ "# --- Load holdout (from gold path which retains original_target, not Feast which excludes it) ---\n",
177
+ "features_df = load_gold()\n",
178
+ "\n",
179
+ "if ORIGINAL_COLUMN not in features_df.columns:\n",
180
+ " raise ValueError(\n",
181
+ " f\"No holdout found (column '{ORIGINAL_COLUMN}' missing). \"\n",
182
+ " \"Holdout must be created in silver layer BEFORE gold layer feature computation.\"\n",
183
+ " )\n",
184
+ "\n",
185
+ "scoring_mask = features_df[TARGET_COLUMN].isna() & features_df[ORIGINAL_COLUMN].notna()\n",
186
+ "scoring_df = features_df[scoring_mask].copy()\n",
187
+ "print(f\"Found {len(scoring_df):,} holdout records for scoring\")\n",
188
+ "\n",
189
+ "\n",
190
+ "# --- Feast features (fallback to parquet) ---\n",
191
+ "feast_path = Path(FEAST_REPO_PATH)\n",
192
+ "if (feast_path / \"feature_store.yaml\").exists():\n",
193
+ " try:\n",
194
+ " store = FeatureStore(repo_path=str(feast_path))\n",
195
+ " entity_df = scoring_df[[FEAST_ENTITY_KEY, FEAST_TIMESTAMP_COL]].copy()\n",
196
+ " exclude_cols = {FEAST_ENTITY_KEY, FEAST_TIMESTAMP_COL, TARGET_COLUMN, ORIGINAL_COLUMN}\n",
197
+ " feature_cols = [\n",
198
+ " c for c in scoring_df.columns\n",
199
+ " if c not in exclude_cols and not c.startswith(\"original_\")\n",
200
+ " ]\n",
201
+ " feature_refs = [f\"{FEAST_FEATURE_VIEW}:{col}\" for col in feature_cols]\n",
202
+ " result_df = store.get_online_features(\n",
203
+ " features=feature_refs,\n",
204
+ " entity_rows=[{FEAST_ENTITY_KEY: eid} for eid in scoring_df[FEAST_ENTITY_KEY]]\n",
205
+ " ).to_df()\n",
206
+ " result_df[ORIGINAL_COLUMN] = scoring_df[ORIGINAL_COLUMN].values\n",
207
+ " result_df[FEAST_ENTITY_KEY] = scoring_df[FEAST_ENTITY_KEY].values\n",
208
+ " scoring_features = result_df\n",
209
+ " print(\"Loaded features from Feast\")\n",
210
+ " except Exception as e:\n",
211
+ " print(f\"Feast retrieval failed ({e}), using parquet\")\n",
212
+ " scoring_features = scoring_df\n",
213
+ "else:\n",
214
+ " print(\"Feast not initialized, using parquet directly\")\n",
215
+ " scoring_features = scoring_df\n",
216
+ "\n",
217
+ "\n",
218
+ "# --- Load best model ---\n",
219
+ "client = mlflow.tracking.MlflowClient()\n",
220
+ "experiment = client.get_experiment_by_name(PIPELINE_NAME)\n",
221
+ "if not experiment:\n",
222
+ " raise ValueError(f\"Experiment {PIPELINE_NAME} not found\")\n",
223
+ "\n",
224
+ "\n",
225
+ "def _find_best_parent_run(client, experiment_id):\n",
226
+ " runs = client.search_runs(\n",
227
+ " experiment_ids=[experiment_id],\n",
228
+ " filter_string=f\"tags.recommendations_hash = '{RECOMMENDATIONS_HASH}'\",\n",
229
+ " order_by=[\"metrics.best_roc_auc DESC\"], max_results=1,\n",
230
+ " )\n",
231
+ " if not runs:\n",
232
+ " runs = client.search_runs(\n",
233
+ " experiment_ids=[experiment_id],\n",
234
+ " order_by=[\"metrics.best_roc_auc DESC\"], max_results=1,\n",
235
+ " )\n",
236
+ " if not runs:\n",
237
+ " raise ValueError(\"No runs found\")\n",
238
+ " return runs[0]\n",
239
+ "\n",
240
+ "\n",
241
+ "parent_run = _find_best_parent_run(client, experiment.experiment_id)\n",
242
+ "best_model_tag = parent_run.data.tags.get(\"best_model\", \"random_forest\")\n",
243
+ "model_name = f\"model_{best_model_tag}\"\n",
244
+ "if RECOMMENDATIONS_HASH:\n",
245
+ " model_name = f\"{model_name}_{RECOMMENDATIONS_HASH}\"\n",
246
+ "\n",
247
+ "child_runs = client.search_runs(\n",
248
+ " experiment_ids=[experiment.experiment_id],\n",
249
+ " filter_string=f\"tags.mlflow.parentRunId = '{parent_run.info.run_id}'\",\n",
250
+ ")\n",
251
+ "model_run = next(\n",
252
+ " (c for c in child_runs if c.info.run_name == best_model_tag), parent_run\n",
253
+ ")\n",
254
+ "model_uri = f\"runs:/{model_run.info.run_id}/{model_name}\"\n",
255
+ "print(f\"Loading model: {model_uri}\")\n",
256
+ "loader = mlflow.xgboost if best_model_tag == \"xgboost\" else mlflow.sklearn\n",
257
+ "model = loader.load_model(model_uri)\n",
258
+ "\n",
259
+ "\n",
260
+ "# --- Prepare features (TransformExecutor, NOT LabelEncoder) ---\n",
261
+ "def prepare_features(df):\n",
262
+ " df = df.copy()\n",
263
+ " drop_cols = [FEAST_ENTITY_KEY, FEAST_TIMESTAMP_COL, ORIGINAL_COLUMN, TARGET_COLUMN]\n",
264
+ " df = df.drop(columns=[c for c in drop_cols if c in df.columns], errors=\"ignore\")\n",
265
+ " df = df.drop(columns=[c for c in df.columns if c.startswith(\"original_\")], errors=\"ignore\")\n",
266
+ " df = _executor.apply_all(df, ENCODINGS + SCALINGS, fit_mode=False, artifact_store=_registry)\n",
267
+ " return df.select_dtypes(include=[\"int64\", \"float64\", \"int32\", \"float32\"]).fillna(0)\n",
268
+ "\n",
269
+ "\n",
270
+ "X = prepare_features(scoring_features)\n",
271
+ "y_true = scoring_features[ORIGINAL_COLUMN].values\n",
272
+ "\n",
273
+ "# --- Predict ---\n",
274
+ "print(\"Generating predictions...\")\n",
275
+ "if hasattr(model, \"predict_proba\"):\n",
276
+ " y_proba = model.predict_proba(X)[:, 1]\n",
277
+ "else:\n",
278
+ " y_proba = model.predict(xgb.DMatrix(X, feature_names=list(X.columns)))\n",
279
+ "y_pred = (y_proba >= 0.5).astype(int)\n",
280
+ "\n",
281
+ "# --- Metrics ---\n",
282
+ "from sklearn.metrics import roc_auc_score, accuracy_score, precision_score, recall_score, f1_score\n",
283
+ "\n",
284
+ "metrics = {\n",
285
+ " \"accuracy\": accuracy_score(y_true, y_pred),\n",
286
+ " \"precision\": precision_score(y_true, y_pred, zero_division=0),\n",
287
+ " \"recall\": recall_score(y_true, y_pred, zero_division=0),\n",
288
+ " \"f1\": f1_score(y_true, y_pred, zero_division=0),\n",
289
+ " \"roc_auc\": roc_auc_score(y_true, y_proba) if len(np.unique(y_true)) > 1 else 0.0,\n",
290
+ "}\n",
291
+ "print(\"\\nValidation Metrics (vs original values):\")\n",
292
+ "for name, value in metrics.items():\n",
293
+ " print(f\" {name}: {value:.4f}\")\n",
294
+ "\n",
295
+ "# --- Save predictions ---\n",
296
+ "predictions_df = pd.DataFrame({\n",
297
+ " FEAST_ENTITY_KEY: scoring_df[FEAST_ENTITY_KEY].values,\n",
298
+ " \"prediction\": y_pred,\n",
299
+ " \"probability\": y_proba,\n",
300
+ " \"actual\": y_true,\n",
301
+ " \"correct\": (y_pred == y_true).astype(int),\n",
302
+ "})\n",
303
+ "PREDICTIONS_PATH.parent.mkdir(parents=True, exist_ok=True)\n",
304
+ "predictions_df.to_parquet(PREDICTIONS_PATH, index=False)\n",
305
+ "print(f\"\\nPredictions saved: {PREDICTIONS_PATH}\")\n",
306
+ "print(f\"Correct: {predictions_df['correct'].sum():,}/{len(predictions_df):,} ({predictions_df['correct'].mean():.1%})\")"
307
+ ]
308
+ },
309
+ {
310
+ "cell_type": "markdown",
311
+ "id": "8b5eb9d4",
312
+ "metadata": {
313
+ "papermill": {
314
+ "duration": 0.003123,
315
+ "end_time": "2026-02-02T13:04:23.676730",
316
+ "exception": false,
317
+ "start_time": "2026-02-02T13:04:23.673607",
318
+ "status": "completed"
319
+ },
320
+ "tags": []
321
+ },
322
+ "source": [
323
+ "## 11.2 Summary Metrics"
324
+ ]
325
+ },
326
+ {
327
+ "cell_type": "code",
328
+ "execution_count": null,
329
+ "id": "5d2880c0",
330
+ "metadata": {
331
+ "execution": {
332
+ "iopub.execute_input": "2026-02-02T13:04:23.683183Z",
333
+ "iopub.status.busy": "2026-02-02T13:04:23.683045Z",
334
+ "iopub.status.idle": "2026-02-02T13:04:23.691221Z",
335
+ "shell.execute_reply": "2026-02-02T13:04:23.690765Z"
336
+ },
337
+ "papermill": {
338
+ "duration": 0.012164,
339
+ "end_time": "2026-02-02T13:04:23.691775",
340
+ "exception": false,
341
+ "start_time": "2026-02-02T13:04:23.679611",
342
+ "status": "completed"
343
+ },
344
+ "tags": []
345
+ },
346
+ "outputs": [],
347
+ "source": [
348
+ "from sklearn.metrics import (\n",
349
+ " accuracy_score, precision_score, recall_score,\n",
350
+ " f1_score, roc_auc_score, confusion_matrix,\n",
351
+ ")\n",
352
+ "\n",
353
+ "y_true = predictions_df[\"actual\"]\n",
354
+ "y_pred = predictions_df[\"prediction\"]\n",
355
+ "y_proba = predictions_df[\"probability\"]\n",
356
+ "\n",
357
+ "metrics = {\n",
358
+ " \"Accuracy\": accuracy_score(y_true, y_pred),\n",
359
+ " \"Precision\": precision_score(y_true, y_pred, zero_division=0),\n",
360
+ " \"Recall\": recall_score(y_true, y_pred, zero_division=0),\n",
361
+ " \"F1 Score\": f1_score(y_true, y_pred, zero_division=0),\n",
362
+ " \"ROC-AUC\": roc_auc_score(y_true, y_proba) if len(np.unique(y_true)) > 1 else 0.0,\n",
363
+ "}\n",
364
+ "\n",
365
+ "print(\"\\n=== Scoring Validation Metrics ===\")\n",
366
+ "for name, value in metrics.items():\n",
367
+ " print(f\" {name}: {value:.4f}\")\n",
368
+ "\n",
369
+ "cm = confusion_matrix(y_true, y_pred)\n",
370
+ "print(f\"\\nConfusion Matrix:\")\n",
371
+ "print(f\" TN={cm[0,0]:,} FP={cm[0,1]:,}\")\n",
372
+ "print(f\" FN={cm[1,0]:,} TP={cm[1,1]:,}\")"
373
+ ]
374
+ },
375
+ {
376
+ "cell_type": "code",
377
+ "execution_count": null,
378
+ "id": "aeb3bf7c",
379
+ "metadata": {
380
+ "execution": {
381
+ "iopub.execute_input": "2026-02-02T13:04:23.698098Z",
382
+ "iopub.status.busy": "2026-02-02T13:04:23.698000Z",
383
+ "iopub.status.idle": "2026-02-02T13:04:24.120245Z",
384
+ "shell.execute_reply": "2026-02-02T13:04:24.119760Z"
385
+ },
386
+ "papermill": {
387
+ "duration": 0.426255,
388
+ "end_time": "2026-02-02T13:04:24.120967",
389
+ "exception": false,
390
+ "start_time": "2026-02-02T13:04:23.694712",
391
+ "status": "completed"
392
+ },
393
+ "tags": []
394
+ },
395
+ "outputs": [],
396
+ "source": [
397
+ "import matplotlib.pyplot as plt\n",
398
+ "from sklearn.metrics import roc_curve\n",
399
+ "\n",
400
+ "fig, axes = plt.subplots(1, 2, figsize=(12, 4))\n",
401
+ "\n",
402
+ "# ROC curve\n",
403
+ "fpr, tpr, _ = roc_curve(y_true, y_proba)\n",
404
+ "axes[0].plot(fpr, tpr, \"b-\", lw=2, label=f\"ROC (AUC={metrics['ROC-AUC']:.3f})\")\n",
405
+ "axes[0].plot([0, 1], [0, 1], \"k--\", lw=1)\n",
406
+ "axes[0].set_xlabel(\"False Positive Rate\")\n",
407
+ "axes[0].set_ylabel(\"True Positive Rate\")\n",
408
+ "axes[0].set_title(\"ROC Curve\")\n",
409
+ "axes[0].legend()\n",
410
+ "\n",
411
+ "# Probability distribution\n",
412
+ "axes[1].hist(y_proba[y_true == 0], bins=30, alpha=0.5, label=\"Actual=0\", color=\"blue\")\n",
413
+ "axes[1].hist(y_proba[y_true == 1], bins=30, alpha=0.5, label=\"Actual=1\", color=\"red\")\n",
414
+ "axes[1].axvline(x=0.5, color=\"black\", linestyle=\"--\", label=\"Threshold\")\n",
415
+ "axes[1].set_xlabel(\"Predicted Probability\")\n",
416
+ "axes[1].set_ylabel(\"Count\")\n",
417
+ "axes[1].set_title(\"Probability Distribution\")\n",
418
+ "axes[1].legend()\n",
419
+ "\n",
420
+ "plt.tight_layout()\n",
421
+ "plt.show()"
422
+ ]
423
+ },
424
+ {
425
+ "cell_type": "markdown",
426
+ "id": "afc8d09d",
427
+ "metadata": {
428
+ "papermill": {
429
+ "duration": 0.003519,
430
+ "end_time": "2026-02-02T13:04:24.127927",
431
+ "exception": false,
432
+ "start_time": "2026-02-02T13:04:24.124408",
433
+ "status": "completed"
434
+ },
435
+ "tags": []
436
+ },
437
+ "source": [
438
+ "## 11.3 Model Comparison Grid\n",
439
+ "\n",
440
+ "Compare all trained models (Logistic Regression, Random Forest, XGBoost) on the holdout set.\n",
441
+ "\n",
442
+ "**Grid Layout:**\n",
443
+ "- **Row 1**: Confusion matrices (counts and percentages)\n",
444
+ "- **Row 2**: ROC curves with AUC scores\n",
445
+ "- **Row 3**: Precision-Recall curves with PR-AUC scores"
446
+ ]
447
+ },
448
+ {
449
+ "cell_type": "code",
450
+ "execution_count": null,
451
+ "id": "865ea5f6",
452
+ "metadata": {
453
+ "execution": {
454
+ "iopub.execute_input": "2026-02-02T13:04:24.135466Z",
455
+ "iopub.status.busy": "2026-02-02T13:04:24.135183Z",
456
+ "iopub.status.idle": "2026-02-02T13:04:24.234063Z",
457
+ "shell.execute_reply": "2026-02-02T13:04:24.233694Z"
458
+ },
459
+ "papermill": {
460
+ "duration": 0.103605,
461
+ "end_time": "2026-02-02T13:04:24.234812",
462
+ "exception": false,
463
+ "start_time": "2026-02-02T13:04:24.131207",
464
+ "status": "completed"
465
+ },
466
+ "tags": []
467
+ },
468
+ "outputs": [],
469
+ "source": [
470
+ "from sklearn.metrics import (\n",
471
+ " roc_curve, precision_recall_curve, average_precision_score,\n",
472
+ " confusion_matrix, roc_auc_score, f1_score, precision_score,\n",
473
+ " recall_score, accuracy_score,\n",
474
+ ")\n",
475
+ "from IPython.display import display, HTML\n",
476
+ "\n",
477
+ "mlflow.set_tracking_uri(MLFLOW_TRACKING_URI)\n",
478
+ "client = mlflow.tracking.MlflowClient()\n",
479
+ "experiment = client.get_experiment_by_name(PIPELINE_NAME)\n",
480
+ "\n",
481
+ "# Prepare features for scoring using TransformExecutor (NOT LabelEncoder)\n",
482
+ "X_holdout = prepare_features(scoring_features)\n",
483
+ "y_actual = predictions_df[\"actual\"].values\n",
484
+ "\n",
485
+ "# Get all logged models\n",
486
+ "logged_models = client.search_logged_models(experiment_ids=[experiment.experiment_id])\n",
487
+ "\n",
488
+ "# Load all 3 model types\n",
489
+ "model_types = [\"logistic_regression\", \"random_forest\", \"xgboost\"]\n",
490
+ "model_display_names = [\"Logistic Regression\", \"Random Forest\", \"XGBoost\"]\n",
491
+ "loaded_models = {}\n",
492
+ "model_predictions = {}\n",
493
+ "\n",
494
+ "for model_type, display_name in zip(model_types, model_display_names):\n",
495
+ " model_name_pattern = f\"model_{model_type}\"\n",
496
+ " if RECOMMENDATIONS_HASH:\n",
497
+ " model_name_pattern = f\"{model_name_pattern}_{RECOMMENDATIONS_HASH}\"\n",
498
+ "\n",
499
+ " matching_model = None\n",
500
+ " for lm in logged_models:\n",
501
+ " if lm.name == model_name_pattern:\n",
502
+ " if matching_model is None or lm.creation_timestamp > matching_model.creation_timestamp:\n",
503
+ " matching_model = lm\n",
504
+ "\n",
505
+ " if matching_model:\n",
506
+ " try:\n",
507
+ " if \"xgboost\" in model_type:\n",
508
+ " m = mlflow.xgboost.load_model(matching_model.model_uri)\n",
509
+ " dmatrix = xgb.DMatrix(X_holdout, feature_names=list(X_holdout.columns))\n",
510
+ " yp = m.predict(dmatrix)\n",
511
+ " else:\n",
512
+ " m = mlflow.sklearn.load_model(matching_model.model_uri)\n",
513
+ " yp = m.predict_proba(X_holdout)[:, 1]\n",
514
+ "\n",
515
+ " y_p = (yp > 0.5).astype(int)\n",
516
+ " loaded_models[display_name] = m\n",
517
+ " model_predictions[display_name] = {\"y_pred\": y_p, \"y_proba\": yp}\n",
518
+ " print(f\"Loaded {display_name}: ROC-AUC = {roc_auc_score(y_actual, yp):.4f}\")\n",
519
+ " except Exception as e:\n",
520
+ " print(f\"Could not load {display_name}: {e}\")\n",
521
+ "\n",
522
+ "print(f\"\\nLoaded {len(loaded_models)} models for comparison\")"
523
+ ]
524
+ },
525
+ {
526
+ "cell_type": "code",
527
+ "execution_count": null,
528
+ "id": "e8152776",
529
+ "metadata": {
530
+ "execution": {
531
+ "iopub.execute_input": "2026-02-02T13:04:24.242988Z",
532
+ "iopub.status.busy": "2026-02-02T13:04:24.242860Z",
533
+ "iopub.status.idle": "2026-02-02T13:04:24.582353Z",
534
+ "shell.execute_reply": "2026-02-02T13:04:24.581773Z"
535
+ },
536
+ "papermill": {
537
+ "duration": 0.344716,
538
+ "end_time": "2026-02-02T13:04:24.582901",
539
+ "exception": false,
540
+ "start_time": "2026-02-02T13:04:24.238185",
541
+ "status": "completed"
542
+ },
543
+ "tags": []
544
+ },
545
+ "outputs": [],
546
+ "source": [
547
+ "# Model Comparison Grid (3 columns x 3 rows)\n",
548
+ "n_models = len(model_predictions)\n",
549
+ "if n_models > 0:\n",
550
+ " fig, axes = plt.subplots(3, n_models, figsize=(5 * n_models, 12))\n",
551
+ " if n_models == 1:\n",
552
+ " axes = axes.reshape(-1, 1)\n",
553
+ "\n",
554
+ " colors = [\"#1f77b4\", \"#ff7f0e\", \"#2ca02c\"]\n",
555
+ "\n",
556
+ " for col_idx, (name, preds) in enumerate(model_predictions.items()):\n",
557
+ " y_p = preds[\"y_pred\"]\n",
558
+ " yp = preds[\"y_proba\"]\n",
559
+ " color = colors[col_idx % len(colors)]\n",
560
+ "\n",
561
+ " # Row 1: Confusion Matrix\n",
562
+ " cm = confusion_matrix(y_actual, y_p)\n",
563
+ " ax = axes[0, col_idx]\n",
564
+ " ax.imshow(cm, cmap=\"Blues\")\n",
565
+ " ax.set_xticks([0, 1])\n",
566
+ " ax.set_yticks([0, 1])\n",
567
+ " ax.set_xticklabels([\"Pred 0\", \"Pred 1\"])\n",
568
+ " ax.set_yticklabels([\"Actual 0\", \"Actual 1\"])\n",
569
+ " for i in range(2):\n",
570
+ " for j in range(2):\n",
571
+ " pct = cm[i, j] / cm.sum() * 100\n",
572
+ " ax.text(j, i, f\"{cm[i, j]}\\n({pct:.1f}%)\", ha=\"center\", va=\"center\",\n",
573
+ " color=\"white\" if cm[i, j] > cm.max() / 2 else \"black\", fontsize=10)\n",
574
+ " acc = accuracy_score(y_actual, y_p)\n",
575
+ " ax.set_title(f\"{name}\\nAccuracy: {acc:.3f}\", fontsize=11, fontweight=\"bold\")\n",
576
+ "\n",
577
+ " # Row 2: ROC Curve\n",
578
+ " ax = axes[1, col_idx]\n",
579
+ " fpr, tpr, _ = roc_curve(y_actual, yp)\n",
580
+ " auc = roc_auc_score(y_actual, yp)\n",
581
+ " ax.plot(fpr, tpr, color=color, lw=2, label=f\"AUC = {auc:.4f}\")\n",
582
+ " ax.plot([0, 1], [0, 1], \"k--\", lw=1, alpha=0.5)\n",
583
+ " ax.fill_between(fpr, tpr, alpha=0.2, color=color)\n",
584
+ " ax.set_xlabel(\"False Positive Rate\")\n",
585
+ " ax.set_ylabel(\"True Positive Rate\")\n",
586
+ " ax.set_title(\"ROC Curve\", fontsize=10)\n",
587
+ " ax.legend(loc=\"lower right\")\n",
588
+ " ax.grid(True, alpha=0.3)\n",
589
+ "\n",
590
+ " # Row 3: Precision-Recall Curve\n",
591
+ " ax = axes[2, col_idx]\n",
592
+ " precision_vals, recall_vals, _ = precision_recall_curve(y_actual, yp)\n",
593
+ " pr_auc = average_precision_score(y_actual, yp)\n",
594
+ " ax.plot(recall_vals, precision_vals, color=color, lw=2, label=f\"PR-AUC = {pr_auc:.4f}\")\n",
595
+ " baseline = y_actual.sum() / len(y_actual)\n",
596
+ " ax.axhline(y=baseline, color=\"gray\", linestyle=\"--\", lw=1, label=f\"Baseline = {baseline:.2f}\")\n",
597
+ " ax.fill_between(recall_vals, precision_vals, alpha=0.2, color=color)\n",
598
+ " ax.set_xlabel(\"Recall\")\n",
599
+ " ax.set_ylabel(\"Precision\")\n",
600
+ " ax.set_title(\"Precision-Recall Curve\", fontsize=10)\n",
601
+ " ax.legend(loc=\"lower left\")\n",
602
+ " ax.grid(True, alpha=0.3)\n",
603
+ "\n",
604
+ " plt.suptitle(\"Model Comparison Grid: Holdout Set Performance\",\n",
605
+ " fontsize=14, fontweight=\"bold\", y=1.02)\n",
606
+ " plt.tight_layout()\n",
607
+ " plt.show()\n",
608
+ "else:\n",
609
+ " print(\"No models loaded for comparison\")"
610
+ ]
611
+ },
612
+ {
613
+ "cell_type": "code",
614
+ "execution_count": null,
615
+ "id": "5e641fbf",
616
+ "metadata": {
617
+ "execution": {
618
+ "iopub.execute_input": "2026-02-02T13:04:24.591754Z",
619
+ "iopub.status.busy": "2026-02-02T13:04:24.591626Z",
620
+ "iopub.status.idle": "2026-02-02T13:04:24.625306Z",
621
+ "shell.execute_reply": "2026-02-02T13:04:24.624779Z"
622
+ },
623
+ "papermill": {
624
+ "duration": 0.039099,
625
+ "end_time": "2026-02-02T13:04:24.625974",
626
+ "exception": false,
627
+ "start_time": "2026-02-02T13:04:24.586875",
628
+ "status": "completed"
629
+ },
630
+ "tags": []
631
+ },
632
+ "outputs": [],
633
+ "source": [
634
+ "# Summary metrics table for all models\n",
635
+ "if model_predictions:\n",
636
+ " comparison_results = []\n",
637
+ " for name, preds in model_predictions.items():\n",
638
+ " y_p = preds[\"y_pred\"]\n",
639
+ " yp = preds[\"y_proba\"]\n",
640
+ " comparison_results.append({\n",
641
+ " \"Model\": name,\n",
642
+ " \"ROC-AUC\": roc_auc_score(y_actual, yp),\n",
643
+ " \"PR-AUC\": average_precision_score(y_actual, yp),\n",
644
+ " \"F1-Score\": f1_score(y_actual, y_p),\n",
645
+ " \"Precision\": precision_score(y_actual, y_p, zero_division=0),\n",
646
+ " \"Recall\": recall_score(y_actual, y_p, zero_division=0),\n",
647
+ " \"Accuracy\": accuracy_score(y_actual, y_p),\n",
648
+ " })\n",
649
+ "\n",
650
+ " comparison_df = pd.DataFrame(comparison_results).set_index(\"Model\")\n",
651
+ " print(\"\\n\" + \"=\" * 70)\n",
652
+ " print(\"MODEL COMPARISON SUMMARY (Holdout Set)\")\n",
653
+ " print(\"=\" * 70)\n",
654
+ " display(\n",
655
+ " comparison_df.style\n",
656
+ " .highlight_max(axis=0, props=\"background-color: #2e7d32; color: white\")\n",
657
+ " .format(\"{:.4f}\")\n",
658
+ " )\n",
659
+ "\n",
660
+ " best_model_name = comparison_df[\"ROC-AUC\"].idxmax()\n",
661
+ " best_auc = comparison_df.loc[best_model_name, \"ROC-AUC\"]\n",
662
+ " print(f\"\\nBest Model: {best_model_name} (ROC-AUC = {best_auc:.4f})\")"
663
+ ]
664
+ },
665
+ {
666
+ "cell_type": "markdown",
667
+ "id": "7cc7e27d",
668
+ "metadata": {
669
+ "papermill": {
670
+ "duration": 0.003359,
671
+ "end_time": "2026-02-02T13:04:24.633357",
672
+ "exception": false,
673
+ "start_time": "2026-02-02T13:04:24.629998",
674
+ "status": "completed"
675
+ },
676
+ "tags": []
677
+ },
678
+ "source": [
679
+ "## 11.4 Adversarial Pipeline Validation\n",
680
+ "\n",
681
+ "Validate that scoring pipeline produces identical features to training for holdout entities.\n",
682
+ "This catches transformation inconsistencies (e.g., scalers re-fit, encoders handling unseen values differently)."
683
+ ]
684
+ },
685
+ {
686
+ "cell_type": "code",
687
+ "execution_count": null,
688
+ "id": "77e2ea51",
689
+ "metadata": {
690
+ "execution": {
691
+ "iopub.execute_input": "2026-02-02T13:04:24.640766Z",
692
+ "iopub.status.busy": "2026-02-02T13:04:24.640645Z",
693
+ "iopub.status.idle": "2026-02-02T13:04:24.669286Z",
694
+ "shell.execute_reply": "2026-02-02T13:04:24.668810Z"
695
+ },
696
+ "papermill": {
697
+ "duration": 0.033239,
698
+ "end_time": "2026-02-02T13:04:24.669913",
699
+ "exception": false,
700
+ "start_time": "2026-02-02T13:04:24.636674",
701
+ "status": "completed"
702
+ },
703
+ "tags": []
704
+ },
705
+ "outputs": [],
706
+ "source": [
707
+ "gold_features = load_gold()\n",
708
+ "\n",
709
+ "holdout_mask = gold_features[ORIGINAL_COLUMN].notna()\n",
710
+ "holdout_gold = gold_features[holdout_mask].copy()\n",
711
+ "print(f\"Holdout entities for validation: {holdout_mask.sum():,}\")\n",
712
+ "\n",
713
+ "# Compare scoring features vs gold features for holdout records\n",
714
+ "scoring_entity_ids = set(scoring_features[FEAST_ENTITY_KEY].values)\n",
715
+ "gold_holdout = holdout_gold[holdout_gold[FEAST_ENTITY_KEY].isin(scoring_entity_ids)]\n",
716
+ "\n",
717
+ "exclude_cols = {FEAST_ENTITY_KEY, \"event_timestamp\", TARGET_COLUMN, ORIGINAL_COLUMN}\n",
718
+ "compare_cols = [\n",
719
+ " c for c in gold_holdout.columns\n",
720
+ " if c not in exclude_cols and not c.startswith(\"original_\")\n",
721
+ "]\n",
722
+ "\n",
723
+ "print(\"\\n\" + \"=\" * 60)\n",
724
+ "print(\"ADVERSARIAL PIPELINE VALIDATION\")\n",
725
+ "print(\"=\" * 60)\n",
726
+ "\n",
727
+ "mismatches = []\n",
728
+ "for col in compare_cols:\n",
729
+ " if col in scoring_features.columns and col in gold_holdout.columns:\n",
730
+ " g_vals = gold_holdout[col].values\n",
731
+ " s_vals = scoring_features.reindex(gold_holdout.index)[col].values\n",
732
+ " if pd.api.types.is_numeric_dtype(gold_holdout[col]):\n",
733
+ " delta = np.abs(g_vals.astype(float) - s_vals.astype(float))\n",
734
+ " max_delta = np.nanmax(delta) if len(delta) > 0 else 0\n",
735
+ " if max_delta > 1e-6:\n",
736
+ " mismatches.append({\"feature\": col, \"max_delta\": max_delta})\n",
737
+ "\n",
738
+ "if not mismatches:\n",
739
+ " print(\"\\nPASSED: Scoring features match training features\")\n",
740
+ "else:\n",
741
+ " print(f\"\\nFAILED: {len(mismatches)} features with drift\")\n",
742
+ " display(pd.DataFrame(mismatches).sort_values(\"max_delta\", ascending=False))"
743
+ ]
744
+ },
745
+ {
746
+ "cell_type": "markdown",
747
+ "id": "de444011",
748
+ "metadata": {
749
+ "papermill": {
750
+ "duration": 0.003852,
751
+ "end_time": "2026-02-02T13:04:24.678592",
752
+ "exception": false,
753
+ "start_time": "2026-02-02T13:04:24.674740",
754
+ "status": "completed"
755
+ },
756
+ "tags": []
757
+ },
758
+ "source": [
759
+ "## 11.5 Transformation Validation\n",
760
+ "\n",
761
+ "Use `validate_feature_transformation()` from the validation module to verify\n",
762
+ "encoding/scaling consistency between training and scoring."
763
+ ]
764
+ },
765
+ {
766
+ "cell_type": "code",
767
+ "execution_count": null,
768
+ "id": "a2dc6490",
769
+ "metadata": {
770
+ "execution": {
771
+ "iopub.execute_input": "2026-02-02T13:04:24.686472Z",
772
+ "iopub.status.busy": "2026-02-02T13:04:24.686358Z",
773
+ "iopub.status.idle": "2026-02-02T13:04:24.728303Z",
774
+ "shell.execute_reply": "2026-02-02T13:04:24.727841Z"
775
+ },
776
+ "papermill": {
777
+ "duration": 0.04693,
778
+ "end_time": "2026-02-02T13:04:24.728951",
779
+ "exception": false,
780
+ "start_time": "2026-02-02T13:04:24.682021",
781
+ "status": "completed"
782
+ },
783
+ "tags": []
784
+ },
785
+ "outputs": [],
786
+ "source": [
787
+ "from customer_retention.stages.validation import validate_feature_transformation\n",
788
+ "\n",
789
+ "# Training features = non-holdout, scoring features = holdout\n",
790
+ "training_mask = gold_features[ORIGINAL_COLUMN].isna()\n",
791
+ "training_subset = gold_features[training_mask].copy()\n",
792
+ "scoring_subset = gold_features[~training_mask].copy()\n",
793
+ "\n",
794
+ "report = validate_feature_transformation(\n",
795
+ " training_df=training_subset,\n",
796
+ " scoring_df=scoring_subset,\n",
797
+ " transform_fn=prepare_features,\n",
798
+ " entity_column=FEAST_ENTITY_KEY,\n",
799
+ " verbose=True,\n",
800
+ ")\n",
801
+ "\n",
802
+ "if report.passed:\n",
803
+ " print(\"Transformation validation PASSED\")\n",
804
+ "else:\n",
805
+ " print(f\"Transformation validation FAILED: {len(report.feature_mismatches)} mismatches\")"
806
+ ]
807
+ },
808
+ {
809
+ "cell_type": "markdown",
810
+ "id": "b79d4809",
811
+ "metadata": {
812
+ "papermill": {
813
+ "duration": 0.003454,
814
+ "end_time": "2026-02-02T13:04:24.736988",
815
+ "exception": false,
816
+ "start_time": "2026-02-02T13:04:24.733534",
817
+ "status": "completed"
818
+ },
819
+ "tags": []
820
+ },
821
+ "source": [
822
+ "## 11.6 Model Explanations (SHAP)"
823
+ ]
824
+ },
825
+ {
826
+ "cell_type": "code",
827
+ "execution_count": null,
828
+ "id": "76fce665",
829
+ "metadata": {
830
+ "execution": {
831
+ "iopub.execute_input": "2026-02-02T13:04:24.746062Z",
832
+ "iopub.status.busy": "2026-02-02T13:04:24.745943Z",
833
+ "iopub.status.idle": "2026-02-02T13:04:25.182792Z",
834
+ "shell.execute_reply": "2026-02-02T13:04:25.182312Z"
835
+ },
836
+ "papermill": {
837
+ "duration": 0.442072,
838
+ "end_time": "2026-02-02T13:04:25.183386",
839
+ "exception": false,
840
+ "start_time": "2026-02-02T13:04:24.741314",
841
+ "status": "completed"
842
+ },
843
+ "tags": []
844
+ },
845
+ "outputs": [],
846
+ "source": [
847
+ "import shap\n",
848
+ "\n",
849
+ "# Load best model for SHAP\n",
850
+ "mlflow.set_tracking_uri(MLFLOW_TRACKING_URI)\n",
851
+ "client = mlflow.tracking.MlflowClient()\n",
852
+ "\n",
853
+ "experiment = client.get_experiment_by_name(PIPELINE_NAME)\n",
854
+ "runs = client.search_runs(\n",
855
+ " experiment_ids=[experiment.experiment_id],\n",
856
+ " order_by=[\"metrics.best_roc_auc DESC\"],\n",
857
+ " max_results=1,\n",
858
+ ")\n",
859
+ "parent_run = runs[0]\n",
860
+ "\n",
861
+ "best_model_tag = parent_run.data.tags.get(\"best_model\", \"random_forest\")\n",
862
+ "model_name = f\"model_{best_model_tag}\"\n",
863
+ "if RECOMMENDATIONS_HASH:\n",
864
+ " model_name = f\"{model_name}_{RECOMMENDATIONS_HASH}\"\n",
865
+ "\n",
866
+ "child_runs = client.search_runs(\n",
867
+ " experiment_ids=[experiment.experiment_id],\n",
868
+ " filter_string=f\"tags.mlflow.parentRunId = '{parent_run.info.run_id}'\",\n",
869
+ ")\n",
870
+ "model_run = next((c for c in child_runs if c.info.run_name == best_model_tag), parent_run)\n",
871
+ "\n",
872
+ "model_uri = f\"runs:/{model_run.info.run_id}/{model_name}\"\n",
873
+ "print(f\"Loading model: {model_uri}\")\n",
874
+ "if best_model_tag == \"xgboost\":\n",
875
+ " model = mlflow.xgboost.load_model(model_uri)\n",
876
+ "else:\n",
877
+ " model = mlflow.sklearn.load_model(model_uri)\n",
878
+ "print(f\"Model type: {type(model).__name__}\")"
879
+ ]
880
+ },
881
+ {
882
+ "cell_type": "code",
883
+ "execution_count": null,
884
+ "id": "3464d0e1",
885
+ "metadata": {
886
+ "execution": {
887
+ "iopub.execute_input": "2026-02-02T13:04:25.192330Z",
888
+ "iopub.status.busy": "2026-02-02T13:04:25.192081Z",
889
+ "iopub.status.idle": "2026-02-02T13:04:25.197408Z",
890
+ "shell.execute_reply": "2026-02-02T13:04:25.197016Z"
891
+ },
892
+ "papermill": {
893
+ "duration": 0.010186,
894
+ "end_time": "2026-02-02T13:04:25.197861",
895
+ "exception": false,
896
+ "start_time": "2026-02-02T13:04:25.187675",
897
+ "status": "completed"
898
+ },
899
+ "tags": []
900
+ },
901
+ "outputs": [],
902
+ "source": [
903
+ "# Prepare features for SHAP using TransformExecutor (NOT LabelEncoder)\n",
904
+ "X = prepare_features(scoring_features)\n",
905
+ "feature_names = list(X.columns)\n",
906
+ "print(f\"Prepared {len(feature_names)} features for SHAP analysis\")"
907
+ ]
908
+ },
909
+ {
910
+ "cell_type": "code",
911
+ "execution_count": null,
912
+ "id": "4e0595d4",
913
+ "metadata": {
914
+ "execution": {
915
+ "iopub.execute_input": "2026-02-02T13:04:25.207529Z",
916
+ "iopub.status.busy": "2026-02-02T13:04:25.207410Z",
917
+ "iopub.status.idle": "2026-02-02T13:04:34.310396Z",
918
+ "shell.execute_reply": "2026-02-02T13:04:34.309912Z"
919
+ },
920
+ "papermill": {
921
+ "duration": 9.108894,
922
+ "end_time": "2026-02-02T13:04:34.310996",
923
+ "exception": false,
924
+ "start_time": "2026-02-02T13:04:25.202102",
925
+ "status": "completed"
926
+ },
927
+ "tags": []
928
+ },
929
+ "outputs": [],
930
+ "source": [
931
+ "# Create SHAP explainer\n",
932
+ "print(\"Creating SHAP explainer (may take a moment)...\")\n",
933
+ "\n",
934
+ "background_size = min(100, len(X))\n",
935
+ "background = shap.sample(X, background_size)\n",
936
+ "\n",
937
+ "if hasattr(model, \"predict_proba\"):\n",
938
+ " explainer = shap.Explainer(model.predict_proba, background, feature_names=feature_names)\n",
939
+ "else:\n",
940
+ " explainer = shap.Explainer(model, background, feature_names=feature_names)\n",
941
+ "\n",
942
+ "print(\"Computing SHAP values...\")\n",
943
+ "shap_values = explainer(X)\n",
944
+ "print(f\"SHAP values computed for {len(shap_values)} records\")"
945
+ ]
946
+ },
947
+ {
948
+ "cell_type": "code",
949
+ "execution_count": null,
950
+ "id": "f3be6aab",
951
+ "metadata": {
952
+ "execution": {
953
+ "iopub.execute_input": "2026-02-02T13:04:34.320580Z",
954
+ "iopub.status.busy": "2026-02-02T13:04:34.320441Z",
955
+ "iopub.status.idle": "2026-02-02T13:04:34.531044Z",
956
+ "shell.execute_reply": "2026-02-02T13:04:34.530558Z"
957
+ },
958
+ "papermill": {
959
+ "duration": 0.216039,
960
+ "end_time": "2026-02-02T13:04:34.531635",
961
+ "exception": false,
962
+ "start_time": "2026-02-02T13:04:34.315596",
963
+ "status": "completed"
964
+ },
965
+ "tags": []
966
+ },
967
+ "outputs": [],
968
+ "source": [
969
+ "# Use positive class SHAP values if multi-output\n",
970
+ "if len(shap_values.shape) == 3:\n",
971
+ " shap_vals = shap_values[:, :, 1] # Positive class\n",
972
+ "else:\n",
973
+ " shap_vals = shap_values\n",
974
+ "\n",
975
+ "plt.figure(figsize=(10, 8))\n",
976
+ "shap.summary_plot(shap_vals, X, feature_names=feature_names, show=False, max_display=20)\n",
977
+ "plt.title(\"Feature Importance (SHAP Summary)\")\n",
978
+ "plt.tight_layout()\n",
979
+ "plt.show()"
980
+ ]
981
+ },
982
+ {
983
+ "cell_type": "code",
984
+ "execution_count": null,
985
+ "id": "e7daa7dc",
986
+ "metadata": {
987
+ "execution": {
988
+ "iopub.execute_input": "2026-02-02T13:04:34.542356Z",
989
+ "iopub.status.busy": "2026-02-02T13:04:34.542238Z",
990
+ "iopub.status.idle": "2026-02-02T13:04:34.548179Z",
991
+ "shell.execute_reply": "2026-02-02T13:04:34.547705Z"
992
+ },
993
+ "papermill": {
994
+ "duration": 0.011839,
995
+ "end_time": "2026-02-02T13:04:34.548851",
996
+ "exception": false,
997
+ "start_time": "2026-02-02T13:04:34.537012",
998
+ "status": "completed"
999
+ },
1000
+ "tags": []
1001
+ },
1002
+ "outputs": [],
1003
+ "source": [
1004
+ "# Mean absolute SHAP values\n",
1005
+ "mean_shap = np.abs(shap_vals.values).mean(axis=0)\n",
1006
+ "importance_df = pd.DataFrame({\n",
1007
+ " \"feature\": feature_names,\n",
1008
+ " \"importance\": mean_shap,\n",
1009
+ "}).sort_values(\"importance\", ascending=False)\n",
1010
+ "\n",
1011
+ "print(\"Top 15 Most Important Features:\")\n",
1012
+ "display(importance_df.head(15))"
1013
+ ]
1014
+ },
1015
+ {
1016
+ "cell_type": "markdown",
1017
+ "id": "30791e92",
1018
+ "metadata": {
1019
+ "papermill": {
1020
+ "duration": 0.004667,
1021
+ "end_time": "2026-02-02T13:04:34.559340",
1022
+ "exception": false,
1023
+ "start_time": "2026-02-02T13:04:34.554673",
1024
+ "status": "completed"
1025
+ },
1026
+ "tags": []
1027
+ },
1028
+ "source": [
1029
+ "## 11.7 Customer Browser"
1030
+ ]
1031
+ },
1032
+ {
1033
+ "cell_type": "code",
1034
+ "execution_count": null,
1035
+ "id": "cd8460ea",
1036
+ "metadata": {
1037
+ "execution": {
1038
+ "iopub.execute_input": "2026-02-02T13:04:34.571206Z",
1039
+ "iopub.status.busy": "2026-02-02T13:04:34.571077Z",
1040
+ "iopub.status.idle": "2026-02-02T13:04:34.575971Z",
1041
+ "shell.execute_reply": "2026-02-02T13:04:34.575461Z"
1042
+ },
1043
+ "papermill": {
1044
+ "duration": 0.012058,
1045
+ "end_time": "2026-02-02T13:04:34.576704",
1046
+ "exception": false,
1047
+ "start_time": "2026-02-02T13:04:34.564646",
1048
+ "status": "completed"
1049
+ },
1050
+ "tags": []
1051
+ },
1052
+ "outputs": [],
1053
+ "source": [
1054
+ "# Create combined dataset for browsing\n",
1055
+ "browser_df = predictions_df.merge(\n",
1056
+ " scoring_features[[FEAST_ENTITY_KEY] + feature_names],\n",
1057
+ " on=FEAST_ENTITY_KEY,\n",
1058
+ " how=\"left\",\n",
1059
+ ")\n",
1060
+ "\n",
1061
+ "print(f\"Customer browser ready with {len(browser_df):,} records\")\n",
1062
+ "print(f\"\\nPrediction Distribution:\")\n",
1063
+ "print(f\" Predicted Positive: {(browser_df['prediction'] == 1).sum():,}\")\n",
1064
+ "print(f\" Predicted Negative: {(browser_df['prediction'] == 0).sum():,}\")\n",
1065
+ "print(f\"\\nCorrect Predictions: {browser_df['correct'].sum():,}/{len(browser_df):,} ({browser_df['correct'].mean():.1%})\")"
1066
+ ]
1067
+ },
1068
+ {
1069
+ "cell_type": "code",
1070
+ "execution_count": null,
1071
+ "id": "d14e806a",
1072
+ "metadata": {
1073
+ "execution": {
1074
+ "iopub.execute_input": "2026-02-02T13:04:34.586759Z",
1075
+ "iopub.status.busy": "2026-02-02T13:04:34.586648Z",
1076
+ "iopub.status.idle": "2026-02-02T13:04:34.590204Z",
1077
+ "shell.execute_reply": "2026-02-02T13:04:34.589718Z"
1078
+ },
1079
+ "papermill": {
1080
+ "duration": 0.01003,
1081
+ "end_time": "2026-02-02T13:04:34.591530",
1082
+ "exception": false,
1083
+ "start_time": "2026-02-02T13:04:34.581500",
1084
+ "status": "completed"
1085
+ },
1086
+ "tags": []
1087
+ },
1088
+ "outputs": [],
1089
+ "source": [
1090
+ "def show_customer(idx: int):\n",
1091
+ " \"\"\"Display details and SHAP explanation for a single customer.\"\"\"\n",
1092
+ " row = browser_df.iloc[idx]\n",
1093
+ " entity_id = row[FEAST_ENTITY_KEY]\n",
1094
+ "\n",
1095
+ " print(f\"=== Customer {entity_id} ===\")\n",
1096
+ " print(f\"Prediction: {int(row['prediction'])} (probability: {row['probability']:.3f})\")\n",
1097
+ " print(f\"Actual: {int(row['actual'])}\")\n",
1098
+ " print(f\"Correct: {'Yes' if row['correct'] else 'No'}\")\n",
1099
+ " print()\n",
1100
+ "\n",
1101
+ " feature_vals = X.iloc[idx]\n",
1102
+ " if len(shap_values.shape) == 3:\n",
1103
+ " customer_shap = shap_values[idx, :, 1].values\n",
1104
+ " else:\n",
1105
+ " customer_shap = shap_values[idx].values\n",
1106
+ "\n",
1107
+ " feature_impact = pd.DataFrame({\n",
1108
+ " \"feature\": feature_names,\n",
1109
+ " \"value\": feature_vals.values,\n",
1110
+ " \"shap_impact\": customer_shap,\n",
1111
+ " }).sort_values(\"shap_impact\", key=abs, ascending=False)\n",
1112
+ "\n",
1113
+ " print(\"Top Contributing Features:\")\n",
1114
+ " display(feature_impact.head(10))\n",
1115
+ "\n",
1116
+ " # Waterfall plot\n",
1117
+ " plt.figure(figsize=(10, 6))\n",
1118
+ " if len(shap_values.shape) == 3:\n",
1119
+ " shap.plots.waterfall(shap_values[idx, :, 1], max_display=10, show=False)\n",
1120
+ " else:\n",
1121
+ " shap.plots.waterfall(shap_values[idx], max_display=10, show=False)\n",
1122
+ " plt.title(f\"SHAP Explanation for Customer {entity_id}\")\n",
1123
+ " plt.tight_layout()\n",
1124
+ " plt.show()"
1125
+ ]
1126
+ },
1127
+ {
1128
+ "cell_type": "code",
1129
+ "execution_count": null,
1130
+ "id": "fd6aaaee",
1131
+ "metadata": {
1132
+ "execution": {
1133
+ "iopub.execute_input": "2026-02-02T13:04:34.601322Z",
1134
+ "iopub.status.busy": "2026-02-02T13:04:34.601195Z",
1135
+ "iopub.status.idle": "2026-02-02T13:04:34.992570Z",
1136
+ "shell.execute_reply": "2026-02-02T13:04:34.992096Z"
1137
+ },
1138
+ "papermill": {
1139
+ "duration": 0.396932,
1140
+ "end_time": "2026-02-02T13:04:34.993166",
1141
+ "exception": false,
1142
+ "start_time": "2026-02-02T13:04:34.596234",
1143
+ "status": "completed"
1144
+ },
1145
+ "tags": []
1146
+ },
1147
+ "outputs": [],
1148
+ "source": [
1149
+ "# Show first 3 customers\n",
1150
+ "print(\"Showing first 3 customers:\\n\")\n",
1151
+ "for i in range(min(3, len(browser_df))):\n",
1152
+ " show_customer(i)\n",
1153
+ " print(\"\\n\" + \"=\" * 60 + \"\\n\")"
1154
+ ]
1155
+ },
1156
+ {
1157
+ "cell_type": "code",
1158
+ "execution_count": null,
1159
+ "id": "35a9e063",
1160
+ "metadata": {
1161
+ "execution": {
1162
+ "iopub.execute_input": "2026-02-02T13:04:35.005568Z",
1163
+ "iopub.status.busy": "2026-02-02T13:04:35.005433Z",
1164
+ "iopub.status.idle": "2026-02-02T13:04:35.008375Z",
1165
+ "shell.execute_reply": "2026-02-02T13:04:35.007656Z"
1166
+ },
1167
+ "papermill": {
1168
+ "duration": 0.009752,
1169
+ "end_time": "2026-02-02T13:04:35.008902",
1170
+ "exception": false,
1171
+ "start_time": "2026-02-02T13:04:34.999150",
1172
+ "status": "completed"
1173
+ },
1174
+ "tags": []
1175
+ },
1176
+ "outputs": [],
1177
+ "source": [
1178
+ "# Look up by entity ID\n",
1179
+ "def lookup_customer(entity_id):\n",
1180
+ " \"\"\"Find and display a customer by their entity ID.\"\"\"\n",
1181
+ " mask = browser_df[FEAST_ENTITY_KEY] == entity_id\n",
1182
+ " if not mask.any():\n",
1183
+ " print(f\"Customer {entity_id} not found in scoring set\")\n",
1184
+ " return\n",
1185
+ " idx = browser_df[mask].index[0]\n",
1186
+ " x_idx = browser_df.index.get_loc(idx)\n",
1187
+ " show_customer(x_idx)\n",
1188
+ "\n",
1189
+ "\n",
1190
+ "# Example: lookup_customer(12345)\n",
1191
+ "print(\"Available entity IDs (first 10):\")\n",
1192
+ "print(browser_df[FEAST_ENTITY_KEY].head(10).tolist())"
1193
+ ]
1194
+ },
1195
+ {
1196
+ "cell_type": "markdown",
1197
+ "id": "7a10e90a",
1198
+ "metadata": {
1199
+ "papermill": {
1200
+ "duration": 0.005938,
1201
+ "end_time": "2026-02-02T13:04:35.020817",
1202
+ "exception": false,
1203
+ "start_time": "2026-02-02T13:04:35.014879",
1204
+ "status": "completed"
1205
+ },
1206
+ "tags": []
1207
+ },
1208
+ "source": [
1209
+ "## 11.8 Error Analysis"
1210
+ ]
1211
+ },
1212
+ {
1213
+ "cell_type": "code",
1214
+ "execution_count": null,
1215
+ "id": "2cee8606",
1216
+ "metadata": {
1217
+ "execution": {
1218
+ "iopub.execute_input": "2026-02-02T13:04:35.033732Z",
1219
+ "iopub.status.busy": "2026-02-02T13:04:35.033622Z",
1220
+ "iopub.status.idle": "2026-02-02T13:04:35.036439Z",
1221
+ "shell.execute_reply": "2026-02-02T13:04:35.036131Z"
1222
+ },
1223
+ "papermill": {
1224
+ "duration": 0.010077,
1225
+ "end_time": "2026-02-02T13:04:35.037185",
1226
+ "exception": false,
1227
+ "start_time": "2026-02-02T13:04:35.027108",
1228
+ "status": "completed"
1229
+ },
1230
+ "tags": []
1231
+ },
1232
+ "outputs": [],
1233
+ "source": [
1234
+ "# Analyze misclassified customers\n",
1235
+ "incorrect = browser_df[browser_df[\"correct\"] == 0]\n",
1236
+ "print(f\"Misclassified customers: {len(incorrect):,}\")\n",
1237
+ "\n",
1238
+ "# False positives (predicted 1, actual 0)\n",
1239
+ "fp = incorrect[incorrect[\"prediction\"] == 1]\n",
1240
+ "print(f\" False Positives: {len(fp):,}\")\n",
1241
+ "\n",
1242
+ "# False negatives (predicted 0, actual 1)\n",
1243
+ "fn = incorrect[incorrect[\"prediction\"] == 0]\n",
1244
+ "print(f\" False Negatives: {len(fn):,}\")"
1245
+ ]
1246
+ },
1247
+ {
1248
+ "cell_type": "code",
1249
+ "execution_count": null,
1250
+ "id": "988b6a72",
1251
+ "metadata": {
1252
+ "execution": {
1253
+ "iopub.execute_input": "2026-02-02T13:04:35.048794Z",
1254
+ "iopub.status.busy": "2026-02-02T13:04:35.048684Z",
1255
+ "iopub.status.idle": "2026-02-02T13:04:35.171216Z",
1256
+ "shell.execute_reply": "2026-02-02T13:04:35.170804Z"
1257
+ },
1258
+ "papermill": {
1259
+ "duration": 0.129762,
1260
+ "end_time": "2026-02-02T13:04:35.172645",
1261
+ "exception": false,
1262
+ "start_time": "2026-02-02T13:04:35.042883",
1263
+ "status": "completed"
1264
+ },
1265
+ "tags": []
1266
+ },
1267
+ "outputs": [],
1268
+ "source": [
1269
+ "# Example false positive\n",
1270
+ "if len(fp) > 0:\n",
1271
+ " print(\"\\n=== Example False Positive ===\")\n",
1272
+ " fp_idx = browser_df.index.get_loc(fp.index[0])\n",
1273
+ " show_customer(fp_idx)"
1274
+ ]
1275
+ },
1276
+ {
1277
+ "cell_type": "code",
1278
+ "execution_count": null,
1279
+ "id": "7ce2bae4",
1280
+ "metadata": {
1281
+ "execution": {
1282
+ "iopub.execute_input": "2026-02-02T13:04:35.186493Z",
1283
+ "iopub.status.busy": "2026-02-02T13:04:35.186355Z",
1284
+ "iopub.status.idle": "2026-02-02T13:04:35.446048Z",
1285
+ "shell.execute_reply": "2026-02-02T13:04:35.445342Z"
1286
+ },
1287
+ "papermill": {
1288
+ "duration": 0.267906,
1289
+ "end_time": "2026-02-02T13:04:35.446873",
1290
+ "exception": false,
1291
+ "start_time": "2026-02-02T13:04:35.178967",
1292
+ "status": "completed"
1293
+ },
1294
+ "tags": []
1295
+ },
1296
+ "outputs": [],
1297
+ "source": [
1298
+ "# Example false negative\n",
1299
+ "if len(fn) > 0:\n",
1300
+ " print(\"\\n=== Example False Negative ===\")\n",
1301
+ " fn_idx = browser_df.index.get_loc(fn.index[0])\n",
1302
+ " show_customer(fn_idx)"
1303
+ ]
1304
+ },
1305
+ {
1306
+ "cell_type": "markdown",
1307
+ "id": "773e6df6",
1308
+ "metadata": {
1309
+ "papermill": {
1310
+ "duration": 0.007096,
1311
+ "end_time": "2026-02-02T13:04:35.460861",
1312
+ "exception": false,
1313
+ "start_time": "2026-02-02T13:04:35.453765",
1314
+ "status": "completed"
1315
+ },
1316
+ "tags": []
1317
+ },
1318
+ "source": [
1319
+ "## 11.9 Export Results"
1320
+ ]
1321
+ },
1322
+ {
1323
+ "cell_type": "code",
1324
+ "execution_count": null,
1325
+ "id": "99fec76d",
1326
+ "metadata": {
1327
+ "execution": {
1328
+ "iopub.execute_input": "2026-02-02T13:04:35.474943Z",
1329
+ "iopub.status.busy": "2026-02-02T13:04:35.474800Z",
1330
+ "iopub.status.idle": "2026-02-02T13:04:35.487191Z",
1331
+ "shell.execute_reply": "2026-02-02T13:04:35.486629Z"
1332
+ },
1333
+ "papermill": {
1334
+ "duration": 0.020645,
1335
+ "end_time": "2026-02-02T13:04:35.487797",
1336
+ "exception": false,
1337
+ "start_time": "2026-02-02T13:04:35.467152",
1338
+ "status": "completed"
1339
+ },
1340
+ "tags": []
1341
+ },
1342
+ "outputs": [],
1343
+ "source": [
1344
+ "# Export detailed results with feature importance\n",
1345
+ "output_dir = GEN_EXPERIMENTS_DIR / \"data\" / \"scoring\"\n",
1346
+ "output_dir.mkdir(parents=True, exist_ok=True)\n",
1347
+ "\n",
1348
+ "# Save global feature importance\n",
1349
+ "importance_df.to_csv(output_dir / \"feature_importance.csv\", index=False)\n",
1350
+ "print(f\"Feature importance saved to {output_dir / 'feature_importance.csv'}\")\n",
1351
+ "\n",
1352
+ "top_features = importance_df.head(10)[\"feature\"].tolist()\n",
1353
+ "shap_by_entity = pd.DataFrame({FEAST_ENTITY_KEY: scoring_features[FEAST_ENTITY_KEY].values})\n",
1354
+ "for feat in top_features:\n",
1355
+ " feat_idx = feature_names.index(feat)\n",
1356
+ " if len(shap_values.shape) == 3:\n",
1357
+ " shap_by_entity[f\"shap_{feat}\"] = shap_values[:, feat_idx, 1].values\n",
1358
+ " else:\n",
1359
+ " shap_by_entity[f\"shap_{feat}\"] = shap_values[:, feat_idx].values\n",
1360
+ "\n",
1361
+ "detailed_df = predictions_df.merge(shap_by_entity, on=FEAST_ENTITY_KEY, how=\"left\")\n",
1362
+ "detailed_df.to_parquet(output_dir / \"predictions_with_shap.parquet\", index=False)\n",
1363
+ "print(f\"Detailed predictions with SHAP saved to {output_dir / 'predictions_with_shap.parquet'}\")\n"
1364
+ ]
1365
+ },
1366
+ {
1367
+ "cell_type": "markdown",
1368
+ "id": "91811812",
1369
+ "metadata": {
1370
+ "papermill": {
1371
+ "duration": 0.006688,
1372
+ "end_time": "2026-02-02T13:04:35.501600",
1373
+ "exception": false,
1374
+ "start_time": "2026-02-02T13:04:35.494912",
1375
+ "status": "completed"
1376
+ },
1377
+ "tags": []
1378
+ },
1379
+ "source": [
1380
+ "> **Save Reminder:** Save this notebook (Ctrl+S / Cmd+S) before running the next one.\n",
1381
+ "> The next notebook will automatically export this notebook's HTML documentation from the saved file."
1382
+ ]
1383
+ }
1384
+ ],
1385
+ "metadata": {
1386
+ "kernelspec": {
1387
+ "display_name": "Python 3",
1388
+ "language": "python",
1389
+ "name": "python3"
1390
+ },
1391
+ "language_info": {
1392
+ "codemirror_mode": {
1393
+ "name": "ipython",
1394
+ "version": 3
1395
+ },
1396
+ "file_extension": ".py",
1397
+ "mimetype": "text/x-python",
1398
+ "name": "python",
1399
+ "nbconvert_exporter": "python",
1400
+ "pygments_lexer": "ipython3",
1401
+ "version": "3.12.4"
1402
+ },
1403
+ "papermill": {
1404
+ "default_parameters": {},
1405
+ "duration": 18.648212,
1406
+ "end_time": "2026-02-02T13:04:38.124707",
1407
+ "environment_variables": {},
1408
+ "exception": null,
1409
+ "input_path": "/Users/Vital/python/CustomerRetention/exploration_notebooks/11_scoring_validation.ipynb",
1410
+ "output_path": "/Users/Vital/python/CustomerRetention/exploration_notebooks/11_scoring_validation.ipynb",
1411
+ "parameters": {},
1412
+ "start_time": "2026-02-02T13:04:19.476495",
1413
+ "version": "2.6.0"
1414
+ }
1415
+ },
1416
+ "nbformat": 4,
1417
+ "nbformat_minor": 5
1418
+ }