SuperModelingFactory 0.4.2__tar.gz → 0.5.0__tar.gz

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 (108) hide show
  1. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/Feature/PSI_Tool.py +97 -20
  2. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/WOE/WOE_Monotone_Binner.py +76 -0
  3. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/__init__.py +1 -1
  4. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/PKG-INFO +1 -1
  5. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/SuperModelingFactory.egg-info/PKG-INFO +1 -1
  6. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/pyproject.toml +1 -1
  7. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/setup.py +1 -1
  8. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/ExcelMaster/ExcelFormatTool.py +0 -0
  9. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/ExcelMaster/ExcelMaster.py +0 -0
  10. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/ExcelMaster/Template.py +0 -0
  11. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/ExcelMaster/Utility.py +0 -0
  12. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/ExcelMaster/__init__.py +0 -0
  13. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/LICENSE +0 -0
  14. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/MANIFEST.in +0 -0
  15. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/Core/Binning_Tool.py +0 -0
  16. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/Core/Binning_Tool.pyi +0 -0
  17. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/Core/Check_DuckDB_Compatibility.py +0 -0
  18. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/Core/Json_Data_Converter.py +0 -0
  19. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/Core/Model_Registry_Tool.py +0 -0
  20. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/Core/ODPS_Tool.py +0 -0
  21. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/Core/Parallel_Engine.py +0 -0
  22. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/Core/Parallel_ODPS_Manager.py +0 -0
  23. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/Core/Proc_Compare.py +0 -0
  24. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/Core/Slope_Tool.py +0 -0
  25. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/Core/Slope_Tool.pyi +0 -0
  26. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/Core/XOR_Encryptor.py +0 -0
  27. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/Core/XOR_Encryptor.pyi +0 -0
  28. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/Core/__init__.py +0 -0
  29. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/Core/kDataFrame.py +0 -0
  30. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/Core/kDataFrame.pyi +0 -0
  31. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/Core/sample_weight_utils.py +0 -0
  32. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/Core/utils.py +0 -0
  33. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/Eval/Evaluation_Tool.py +0 -0
  34. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/Eval/Evaluation_Tool.pyi +0 -0
  35. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/Eval/Model_Eval_Tool.py +0 -0
  36. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/Eval/Model_Eval_Tool.pyi +0 -0
  37. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/Eval/__init__.py +0 -0
  38. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/Eval/evaluate_model.py +0 -0
  39. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/Eval/evaluate_model.pyi +0 -0
  40. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/Eval/weighted_eval_utils.py +0 -0
  41. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/Explainability/Coalition_Structure.py +0 -0
  42. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/Explainability/Model_Explainer.py +0 -0
  43. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/Explainability/__init__.py +0 -0
  44. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/Feature/Distribution_Tool.py +0 -0
  45. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/Feature/Distribution_Tool.pyi +0 -0
  46. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/Feature/Feature_Insights.py +0 -0
  47. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/Feature/Feature_Insights.pyi +0 -0
  48. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/Feature/Feature_Screen.py +0 -0
  49. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/Feature/PSI_Tool.pyi +0 -0
  50. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/Feature/WOE_Engine_Feature_Patch.py +0 -0
  51. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/Feature/Weighted_Screen.py +0 -0
  52. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/Feature/__init__.py +0 -0
  53. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/Model/Backward_Tool.py +0 -0
  54. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/Model/Backward_Tool.pyi +0 -0
  55. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/Model/GBM_Search_Tool.py +0 -0
  56. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/Model/GBM_Tool.py +0 -0
  57. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/Model/GBM_Tool.pyi +0 -0
  58. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/Model/LRM_Tool.py +0 -0
  59. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/Model/LRM_Tool.pyi +0 -0
  60. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/Model/__init__.py +0 -0
  61. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/Pipeline/__init__.py +0 -0
  62. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/Pipeline/_common.py +0 -0
  63. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/Pipeline/credit_model.py +0 -0
  64. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/Pipeline/feature_validation.py +0 -0
  65. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/Pipeline/mock_sample.py +0 -0
  66. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/Pipeline/orchestrator.py +0 -0
  67. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/Pipeline/reject_inference.py +0 -0
  68. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/Pipeline/sample_analysis.py +0 -0
  69. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/Pipeline/score_comparison.py +0 -0
  70. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/Pipeline/score_consistency_uat.py +0 -0
  71. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/Pipeline/screening_artifact.py +0 -0
  72. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/Sample/Distribution_Adaptation.py +0 -0
  73. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/Sample/Distribution_Adaptation.pyi +0 -0
  74. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/Sample/Reject_Infer.py +0 -0
  75. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/Sample/Reject_Infer.pyi +0 -0
  76. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/Sample/Sample_Split.py +0 -0
  77. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/Sample/Sample_Split.pyi +0 -0
  78. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/Sample/__init__.py +0 -0
  79. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/UAT/UAT_Consistency_Checker.py +0 -0
  80. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/UAT/__init__.py +0 -0
  81. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/WOE/WOE_Adapter.py +0 -0
  82. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/WOE/WOE_Adapter.pyi +0 -0
  83. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/WOE/WOE_Master.py +0 -0
  84. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/WOE/WOE_Master.pyi +0 -0
  85. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/WOE/WOE_Monotone_Binner.pyi +0 -0
  86. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/WOE/WOE_Plot_Tool.py +0 -0
  87. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/WOE/WOE_Plot_Tool.pyi +0 -0
  88. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/WOE/WOE_Report_Builder.py +0 -0
  89. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/WOE/WOE_Report_Builder.pyi +0 -0
  90. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/WOE/WOE_Tool.py +0 -0
  91. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/WOE/WOE_Tool.pyi +0 -0
  92. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/WOE/__init__.py +0 -0
  93. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/WOE/plot_woe_tool.py +0 -0
  94. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/WOE/plot_woe_tool.pyi +0 -0
  95. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/ref_font/KaiTi.ttf +0 -0
  96. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/ref_font/WeiRuanYaHei.ttf +0 -0
  97. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/ref_font/__init__.py +0 -0
  98. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Modeling_Tool/ref_font/simsun.ttc +0 -0
  99. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/README.md +0 -0
  100. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Report/Report_Tool.py +0 -0
  101. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/Report/__init__.py +0 -0
  102. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/SuperModelingFactory.egg-info/SOURCES.txt +0 -0
  103. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/SuperModelingFactory.egg-info/dependency_links.txt +0 -0
  104. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/SuperModelingFactory.egg-info/not-zip-safe +0 -0
  105. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/SuperModelingFactory.egg-info/requires.txt +0 -0
  106. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/SuperModelingFactory.egg-info/top_level.txt +0 -0
  107. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/requirements.txt +0 -0
  108. {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.0}/setup.cfg +0 -0
@@ -58,7 +58,8 @@ class PSICalculator:
58
58
  equal_freq: bool = True,
59
59
  min_bin_prop: float = 0.05,
60
60
  content: float = 1e-6,
61
- precision: int = 5
61
+ precision: int = 5,
62
+ missing_policy: str = "include"
62
63
  ):
63
64
  """
64
65
  Initialize PSICalculator with configuration parameters.
@@ -75,12 +76,26 @@ class PSICalculator:
75
76
  Small value to prevent division by zero. Default is 1e-6.
76
77
  precision : int, optional
77
78
  Decimal precision for rounding. Default is 5.
79
+ missing_policy : {"drop", "include", "warn_and_drop"}, optional
80
+ How NaN rows are handled. Default in 0.5.0 is "include", which routes
81
+ NaN rows through a dedicated "__MISSING__" bin so missing-rate drift
82
+ contributes to the PSI. This is the recommended production behaviour
83
+ and became the default in 0.5.0 (previously "drop" in 0.4.2 for
84
+ numeric backward-compat). Pass "drop" to reproduce pre-0.5.0
85
+ numbers, or "warn_and_drop" for legacy numbers with a RuntimeWarning
86
+ naming the NaN counts.
78
87
  """
88
+ if missing_policy not in {"include", "drop", "warn_and_drop"}:
89
+ raise ValueError(
90
+ f"PSICalculator.__init__: missing_policy must be one of "
91
+ f"'include', 'drop', 'warn_and_drop'; got {missing_policy!r}."
92
+ )
79
93
  self.buckets = buckets
80
94
  self.equal_freq = equal_freq
81
95
  self.min_bin_prop = min_bin_prop
82
96
  self.content = content
83
97
  self.precision = precision
98
+ self.missing_policy = missing_policy
84
99
 
85
100
  # def _calculate_single_psi(
86
101
  # self,
@@ -418,7 +433,8 @@ class PSICalculator:
418
433
  varlist: List[str],
419
434
  group_by: Optional[str] = None,
420
435
  group_name: Optional[str] = None,
421
- return_details = False
436
+ return_details = False,
437
+ missing_policy: Optional[str] = None
422
438
  ) -> pd.DataFrame:
423
439
  """
424
440
  Calculate grouped PSI comparing two datasets, using expected as benchmark.
@@ -435,12 +451,18 @@ class PSICalculator:
435
451
  Column to group by in both datasets.
436
452
  group_name : str, optional
437
453
  Specific group column name for multi-group calculation.
454
+ missing_policy : {"drop", "include", "warn_and_drop"}, optional
455
+ How NaN rows are handled. If None (default), falls back to the value
456
+ configured on ``self.missing_policy``. Pass an explicit value to
457
+ override the class-level default for this call only. See
458
+ ``PSICalculator.__init__`` for the semantics of each mode.
438
459
 
439
460
  Returns
440
461
  -------
441
462
  pandas.DataFrame
442
463
  Grouped PSI results.
443
464
  """
465
+ effective_policy = missing_policy if missing_policy is not None else self.missing_policy
444
466
  return calculate_multigroup_psi_two_sets(
445
467
  expected_df = expected_df,
446
468
  actual_df = current_data,
@@ -452,7 +474,8 @@ class PSICalculator:
452
474
  content = self.content,
453
475
  precision = self.precision,
454
476
  group_name = group_name,
455
- return_details = return_details
477
+ return_details = return_details,
478
+ missing_policy = effective_policy
456
479
  )
457
480
 
458
481
 
@@ -469,7 +492,7 @@ def _calculate_single_psi(
469
492
  min_bin_prop: float = 0.05,
470
493
  content: float = 1e-6,
471
494
  precision: int = 5,
472
- missing_policy: str = "drop",
495
+ missing_policy: str = "include",
473
496
  ) -> Union[float, Tuple[float, pd.DataFrame]]:
474
497
  """
475
498
  Calculate Population Stability Index (PSI) for a single variable.
@@ -652,7 +675,8 @@ def calculate_psi(
652
675
  return_details: bool = False,
653
676
  min_bin_prop: float = 0.05,
654
677
  content: float = 1e-6,
655
- precision: int = 5
678
+ precision: int = 5,
679
+ missing_policy: str = "include"
656
680
  ) -> Union[float, pd.DataFrame, Tuple[Dict, Dict]]:
657
681
  """
658
682
  Calculate Population Stability Index (PSI) for a variable, optionally by groups.
@@ -682,6 +706,14 @@ def calculate_psi(
682
706
  Small value to avoid division by zero. Default is 1e-6.
683
707
  precision : int, optional
684
708
  Decimal precision. Default is 5.
709
+ missing_policy : {"drop", "include", "warn_and_drop"}, optional
710
+ How NaN rows are handled. Default in 0.5.0 is "include", which routes
711
+ NaN rows through a dedicated "__MISSING__" bin so missing-rate drift
712
+ contributes to the PSI. This is the recommended production behaviour
713
+ and became the default in 0.5.0 (previously "drop" in 0.4.2 for
714
+ numeric backward-compat). Pass "drop" to reproduce pre-0.5.0
715
+ numbers, or "warn_and_drop" for legacy numbers with a RuntimeWarning
716
+ naming the NaN counts.
685
717
 
686
718
  Returns
687
719
  -------
@@ -734,7 +766,8 @@ def calculate_psi(
734
766
  True,
735
767
  min_bin_prop,
736
768
  content,
737
- precision
769
+ precision,
770
+ missing_policy
738
771
  )
739
772
  results[group] = psi_value
740
773
  details_dict[group] = detail
@@ -747,7 +780,8 @@ def calculate_psi(
747
780
  False,
748
781
  min_bin_prop,
749
782
  content,
750
- precision
783
+ precision,
784
+ missing_policy
751
785
  )
752
786
 
753
787
  if return_details:
@@ -765,7 +799,8 @@ def calculate_psi(
765
799
  True,
766
800
  min_bin_prop,
767
801
  content,
768
- precision
802
+ precision,
803
+ missing_policy
769
804
  )
770
805
  else:
771
806
  return _calculate_single_psi(
@@ -776,7 +811,8 @@ def calculate_psi(
776
811
  False,
777
812
  min_bin_prop,
778
813
  content,
779
- precision
814
+ precision,
815
+ missing_policy
780
816
  )
781
817
 
782
818
 
@@ -791,7 +827,8 @@ def calculate_within_psi(
791
827
  min_bin_prop: float = 0.05,
792
828
  content: float = 1e-6,
793
829
  precision: int = 5,
794
- benchmark_display_name: Optional[str] = None
830
+ benchmark_display_name: Optional[str] = None,
831
+ missing_policy: str = "include"
795
832
  ) -> Union[pd.DataFrame, Dict]:
796
833
  """
797
834
  Calculate PSI values within a single dataset, comparing groups to a benchmark.
@@ -823,6 +860,14 @@ def calculate_within_psi(
823
860
  Decimal precision. Default is 5.
824
861
  benchmark_display_name : str, optional
825
862
  Custom name for benchmark in results.
863
+ missing_policy : {"drop", "include", "warn_and_drop"}, optional
864
+ How NaN rows are handled. Default in 0.5.0 is "include", which routes
865
+ NaN rows through a dedicated "__MISSING__" bin so missing-rate drift
866
+ contributes to the PSI. This is the recommended production behaviour
867
+ and became the default in 0.5.0 (previously "drop" in 0.4.2 for
868
+ numeric backward-compat). Pass "drop" to reproduce pre-0.5.0
869
+ numbers, or "warn_and_drop" for legacy numbers with a RuntimeWarning
870
+ naming the NaN counts.
826
871
 
827
872
  Returns
828
873
  -------
@@ -858,7 +903,8 @@ def calculate_within_psi(
858
903
  return_details=True,
859
904
  min_bin_prop=min_bin_prop,
860
905
  content=content,
861
- precision=precision
906
+ precision=precision,
907
+ missing_policy=missing_policy
862
908
  )
863
909
  res_dict[obs_value] = psi
864
910
  detail_dict[obs_value] = details
@@ -872,7 +918,8 @@ def calculate_within_psi(
872
918
  return_details=False,
873
919
  min_bin_prop=min_bin_prop,
874
920
  content=content,
875
- precision=precision
921
+ precision=precision,
922
+ missing_policy=missing_policy
876
923
  )
877
924
  res_dict[obs_value] = psi
878
925
 
@@ -895,7 +942,8 @@ def calculate_psi_within_dataset(
895
942
  buckets: int = 10,
896
943
  min_bin_prop: float = 0.05,
897
944
  content: float = 1e-6,
898
- precision: int = 5
945
+ precision: int = 5,
946
+ missing_policy: str = "include"
899
947
  ) -> pd.DataFrame:
900
948
  """
901
949
  Calculate PSI for multiple variables within a dataset, comparing groups to a benchmark.
@@ -923,6 +971,14 @@ def calculate_psi_within_dataset(
923
971
  Small value to avoid division by zero. Default is 1e-6.
924
972
  precision : int, optional
925
973
  Decimal precision. Default is 5.
974
+ missing_policy : {"drop", "include", "warn_and_drop"}, optional
975
+ How NaN rows are handled. Default in 0.5.0 is "include", which routes
976
+ NaN rows through a dedicated "__MISSING__" bin so missing-rate drift
977
+ contributes to the PSI. This is the recommended production behaviour
978
+ and became the default in 0.5.0 (previously "drop" in 0.4.2 for
979
+ numeric backward-compat). Pass "drop" to reproduce pre-0.5.0
980
+ numbers, or "warn_and_drop" for legacy numbers with a RuntimeWarning
981
+ naming the NaN counts.
926
982
 
927
983
  Returns
928
984
  -------
@@ -946,7 +1002,8 @@ def calculate_psi_within_dataset(
946
1002
  return_details=False,
947
1003
  min_bin_prop=min_bin_prop,
948
1004
  content=content,
949
- precision=precision
1005
+ precision=precision,
1006
+ missing_policy=missing_policy
950
1007
  ).sort_values([grp_name]).reset_index(drop=True)
951
1008
 
952
1009
  single_psi['var'] = var
@@ -964,7 +1021,8 @@ def calculate_multivar_psi_two_sets(
964
1021
  equal_freq: bool = True,
965
1022
  min_bin_prop: float = 0.05,
966
1023
  content: float = 1e-6,
967
- precision: int = 5
1024
+ precision: int = 5,
1025
+ missing_policy: str = "include"
968
1026
  ) -> pd.DataFrame:
969
1027
  """
970
1028
  Calculate PSI for multiple variables by comparing two different datasets.
@@ -992,6 +1050,14 @@ def calculate_multivar_psi_two_sets(
992
1050
  Small value to avoid division by zero. Default is 1e-6.
993
1051
  precision : int, optional
994
1052
  Decimal precision. Default is 5.
1053
+ missing_policy : {"drop", "include", "warn_and_drop"}, optional
1054
+ How NaN rows are handled. Default in 0.5.0 is "include", which routes
1055
+ NaN rows through a dedicated "__MISSING__" bin so missing-rate drift
1056
+ contributes to the PSI. This is the recommended production behaviour
1057
+ and became the default in 0.5.0 (previously "drop" in 0.4.2 for
1058
+ numeric backward-compat). Pass "drop" to reproduce pre-0.5.0
1059
+ numbers, or "warn_and_drop" for legacy numbers with a RuntimeWarning
1060
+ naming the NaN counts.
995
1061
 
996
1062
  Returns
997
1063
  -------
@@ -1011,7 +1077,8 @@ def calculate_multivar_psi_two_sets(
1011
1077
  target_col=var,
1012
1078
  group_by=group_by,
1013
1079
  buckets=buckets,
1014
- return_details=False
1080
+ return_details=False,
1081
+ missing_policy=missing_policy
1015
1082
  )
1016
1083
  if group_by is None:
1017
1084
  single_psi = pd.DataFrame([single_psi], columns=['psi'])
@@ -1151,7 +1218,8 @@ def calculate_multigroup_psi_two_sets(
1151
1218
  content: float = 1e-6,
1152
1219
  precision: int = 5,
1153
1220
  group_name: Optional[str] = None,
1154
- return_details: bool = False
1221
+ return_details: bool = False,
1222
+ missing_policy: str = "include"
1155
1223
  ) -> Union[pd.DataFrame, Dict[str, pd.DataFrame]]:
1156
1224
  """
1157
1225
  Calculate grouped PSI using expected DataFrame as benchmark, applied to actual DataFrame groups.
@@ -1181,6 +1249,12 @@ def calculate_multigroup_psi_two_sets(
1181
1249
  return_details : bool, optional
1182
1250
  是否返回详细分箱信息。若为 True,返回字典 {'psi': psi_df, 'details': details_df},
1183
1251
  details_df 包含列:['bin', 'expected_percent', 'actual_percent', 'psi_component', group_name, 'var']
1252
+ missing_policy : {"drop", "include", "warn_and_drop"}, optional
1253
+ NaN 行处理策略。0.5.0 起默认为 "include",将 NaN 行汇入独立的
1254
+ "__MISSING__" 桶,使缺失率漂移进入 PSI 计算;这是推荐的生产行为,
1255
+ 并在 0.5.0 成为默认值(0.4.2 为 "drop" 以保持数值向后兼容)。
1256
+ 传入 "drop" 可复现 0.5.0 之前的数值,或 "warn_and_drop" 保留旧数值
1257
+ 同时发出 RuntimeWarning 报告两侧 NaN 数量。
1184
1258
  """
1185
1259
  if group_name is not None:
1186
1260
  if actual_df[group_name].isna().sum() > 0:
@@ -1203,7 +1277,8 @@ def calculate_multigroup_psi_two_sets(
1203
1277
  return_details=True,
1204
1278
  min_bin_prop=min_bin_prop,
1205
1279
  content=content,
1206
- precision=precision
1280
+ precision=precision,
1281
+ missing_policy=missing_policy
1207
1282
  )
1208
1283
 
1209
1284
  psi_records.append({group_name: group, 'var': var, 'psi': psi_val})
@@ -1250,7 +1325,8 @@ def calculate_multigroup_psi_two_sets(
1250
1325
  return_details=True,
1251
1326
  min_bin_prop=min_bin_prop,
1252
1327
  content=content,
1253
- precision=precision
1328
+ precision=precision,
1329
+ missing_policy=missing_policy
1254
1330
  )
1255
1331
  psi_records.append({'var': var, 'psi': psi_val})
1256
1332
 
@@ -1285,7 +1361,8 @@ def calculate_multigroup_psi_two_sets(
1285
1361
  equal_freq=equal_freq,
1286
1362
  min_bin_prop=min_bin_prop,
1287
1363
  content=content,
1288
- precision=precision
1364
+ precision=precision,
1365
+ missing_policy=missing_policy
1289
1366
  )
1290
1367
 
1291
1368
  return group_psi
@@ -257,6 +257,11 @@ class MonotoneWOEBinner:
257
257
  # }}
258
258
  self._results: Dict[str, Any] = {}
259
259
  self._is_fitted = False
260
+ # N38 (0.5.0): populated by apply_woe when unseen categorical values are
261
+ # observed in transform data. Keys are feature names; each value is a
262
+ # dict with unseen_values/affected_rows/affected_frac/total_rows.
263
+ # Reset at the start of every apply_woe call.
264
+ self._unseen_category_stats: Dict[str, dict] = {}
260
265
 
261
266
  # ─────────────────────────────────────────────────────────────────
262
267
  # 内部工具
@@ -2047,6 +2052,7 @@ class MonotoneWOEBinner:
2047
2052
  data: pd.DataFrame,
2048
2053
  suffix: str = "_woe",
2049
2054
  inplace: bool = False,
2055
+ unseen_category_policy: str = "warn",
2050
2056
  ) -> pd.DataFrame:
2051
2057
  """
2052
2058
  将 data 中的特征原始数值转换为 WOE 值,添加 *_woe 列。
@@ -2066,14 +2072,39 @@ class MonotoneWOEBinner:
2066
2072
  data : 含原始特征列的 DataFrame
2067
2073
  suffix : WOE 列后缀,默认 "_woe"
2068
2074
  inplace : 是否在原 DataFrame 上操作(False = 返回副本)
2075
+ unseen_category_policy : {"warn", "raise", "silent"}, optional
2076
+ How to handle transform-time categorical values not seen at fit
2077
+ time. Default in 0.5.0 is ``"warn"``, which fills the unseen
2078
+ value with ``missing_woe`` (same as previous behaviour) but
2079
+ emits a ``RuntimeWarning`` per feature listing the unseen
2080
+ categories and affected row count, and populates
2081
+ ``self._unseen_category_stats`` for programmatic monitoring.
2082
+ Pass ``"raise"`` to fail loudly on first unseen category, or
2083
+ ``"silent"`` to reproduce the pre-0.5.0 fully-silent behaviour.
2084
+
2085
+ Attributes populated
2086
+ --------------------
2087
+ _unseen_category_stats : Dict[str, dict]
2088
+ Keys are feature names that saw at least one unseen category in
2089
+ the most recent ``apply_woe`` call. Each value is
2090
+ ``{"unseen_values": set, "affected_rows": int,
2091
+ "affected_frac": float, "total_rows": int}``. Reset at the
2092
+ start of every ``apply_woe`` call.
2069
2093
 
2070
2094
  Returns
2071
2095
  -------
2072
2096
  DataFrame,新增 {feat}{suffix} 列
2073
2097
  """
2098
+ if unseen_category_policy not in {"warn", "raise", "silent"}:
2099
+ raise ValueError(
2100
+ f"apply_woe: unseen_category_policy must be one of "
2101
+ f"'warn', 'raise', 'silent'; got {unseen_category_policy!r}."
2102
+ )
2074
2103
  self._check_fitted()
2075
2104
  df = data if inplace else data.copy()
2076
2105
  woe_outputs: Dict[str, np.ndarray] = {}
2106
+ # Reset per-call so callers can inspect stats from the *latest* run only.
2107
+ self._unseen_category_stats = {}
2077
2108
 
2078
2109
  for feat, vr in self._results.items():
2079
2110
  if feat not in df.columns:
@@ -2109,6 +2140,51 @@ class MonotoneWOEBinner:
2109
2140
 
2110
2141
  # ── 类别特征:按取值直接查 WOE,不做区间切分 ──
2111
2142
  if vr.get("is_categorical"):
2143
+ # N38 (0.5.0): detect transform-time categories that were not
2144
+ # seen at fit time so drift on categorical features is visible.
2145
+ if unseen_category_policy != "silent":
2146
+ fit_categories = set(vr.get("categories", []) or [])
2147
+ if fit_categories:
2148
+ observed = set(series.dropna().unique().tolist())
2149
+ unseen = observed - fit_categories
2150
+ if unseen:
2151
+ affected_mask = series.isin(unseen)
2152
+ affected_rows = int(affected_mask.sum())
2153
+ total_rows = int(len(series))
2154
+ stats = {
2155
+ "unseen_values": unseen,
2156
+ "affected_rows": affected_rows,
2157
+ "affected_frac": affected_rows / max(total_rows, 1),
2158
+ "total_rows": total_rows,
2159
+ }
2160
+ self._unseen_category_stats[feat] = stats
2161
+ try:
2162
+ unseen_preview = sorted(unseen)[:5]
2163
+ except TypeError:
2164
+ unseen_preview = list(unseen)[:5]
2165
+ more_suffix = "..." if len(unseen) > 5 else ""
2166
+ if unseen_category_policy == "raise":
2167
+ raise ValueError(
2168
+ f"apply_woe: feature {feat!r} has "
2169
+ f"{len(unseen)} unseen categories in "
2170
+ f"transform data: {unseen_preview}{more_suffix}. "
2171
+ f"{affected_rows}/{total_rows} rows affected "
2172
+ f"({stats['affected_frac']:.1%}). "
2173
+ f"Pass unseen_category_policy='warn' to fill "
2174
+ f"with missing_woe and warn, or 'silent' for "
2175
+ f"legacy behaviour."
2176
+ )
2177
+ else: # "warn"
2178
+ warnings.warn(
2179
+ f"apply_woe: feature {feat!r} has "
2180
+ f"{len(unseen)} unseen categories in "
2181
+ f"transform data: {unseen_preview}{more_suffix}. "
2182
+ f"Filling with missing_woe={feat_missing_woe}. "
2183
+ f"{affected_rows}/{total_rows} rows affected "
2184
+ f"({stats['affected_frac']:.1%}).",
2185
+ RuntimeWarning,
2186
+ stacklevel=2,
2187
+ )
2112
2188
  wt = vr["woe_table"]
2113
2189
  # 原始取值 → WOE(fit 路径,精确匹配;int/float 由 dict 等价性兼容)
2114
2190
  # refine_cate 聚类后,一个箱含多个类别(cat_members);逐成员展开建表
@@ -1,7 +1,7 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  __author__ = "Jingkai Sun"
4
- __version__ = "0.4.2"
4
+ __version__ = "0.5.0"
5
5
 
6
6
  # Core module - base data structures
7
7
  from .Core import (
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: SuperModelingFactory
3
- Version: 0.4.2
3
+ Version: 0.5.0
4
4
  Summary: Credit risk modeling factory: WOE binning, scorecards, LightGBM, Excel reporting.
5
5
  Home-page: https://github.com/Kyle-J-Sun/SuperModelingFactory
6
6
  Author: Kyle Sun
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: SuperModelingFactory
3
- Version: 0.4.2
3
+ Version: 0.5.0
4
4
  Summary: Credit risk modeling factory: WOE binning, scorecards, LightGBM, Excel reporting.
5
5
  Home-page: https://github.com/Kyle-J-Sun/SuperModelingFactory
6
6
  Author: Kyle Sun
@@ -12,7 +12,7 @@ build-backend = "setuptools.build_meta"
12
12
 
13
13
  [project]
14
14
  name = "SuperModelingFactory"
15
- version = "0.4.2"
15
+ version = "0.5.0"
16
16
  description = "Credit risk modeling factory: WOE binning, scorecards, LightGBM, Excel reporting."
17
17
  readme = "README.md"
18
18
  requires-python = ">=3.10"
@@ -15,7 +15,7 @@ def _read(path: str) -> str:
15
15
 
16
16
  setup(
17
17
  name="SuperModelingFactory",
18
- version=os.environ.get("SMF_VERSION", "0.4.2"),
18
+ version=os.environ.get("SMF_VERSION", "0.5.0"),
19
19
  description="Credit risk modeling factory: WOE binning, scorecards, LightGBM, Excel reporting.",
20
20
  long_description=_read("README.md"),
21
21
  long_description_content_type="text/markdown",