SuperModelingFactory 0.5.2__tar.gz → 0.5.3__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 (112) hide show
  1. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/Core/Binning_Tool.py +1 -2
  2. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/Core/ODPS_Tool.py +97 -1
  3. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/Core/sample_weight_utils.py +6 -0
  4. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/Eval/Model_Eval_Tool.py +43 -4
  5. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/Eval/Model_Eval_Tool.pyi +3 -3
  6. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/Eval/weighted_eval_utils.py +3 -1
  7. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/Model/LRM_Tool.py +45 -6
  8. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/Model/LRM_Tool.pyi +2 -2
  9. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/Pipeline/_common.py +36 -2
  10. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/Pipeline/credit_model.py +16 -1
  11. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/Pipeline/reject_inference.py +16 -1
  12. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/Pipeline/score_consistency_uat.py +0 -2
  13. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/Sample/Reject_Infer.py +50 -1
  14. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/Sample/Reject_Infer.pyi +1 -1
  15. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/__init__.py +1 -1
  16. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/_utils/__init__.py +2 -1
  17. supermodelingfactory-0.5.3/Modeling_Tool/_utils/nan_guard.py +31 -0
  18. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/PKG-INFO +2 -2
  19. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/README.md +1 -1
  20. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/SuperModelingFactory.egg-info/PKG-INFO +2 -2
  21. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/SuperModelingFactory.egg-info/SOURCES.txt +1 -0
  22. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/pyproject.toml +1 -1
  23. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/setup.py +1 -1
  24. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/ExcelMaster/ExcelFormatTool.py +0 -0
  25. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/ExcelMaster/ExcelMaster.py +0 -0
  26. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/ExcelMaster/Template.py +0 -0
  27. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/ExcelMaster/Utility.py +0 -0
  28. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/ExcelMaster/__init__.py +0 -0
  29. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/LICENSE +0 -0
  30. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/MANIFEST.in +0 -0
  31. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/Core/Binning_Tool.pyi +0 -0
  32. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/Core/Check_DuckDB_Compatibility.py +0 -0
  33. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/Core/Json_Data_Converter.py +0 -0
  34. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/Core/Model_Registry_Tool.py +0 -0
  35. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/Core/Parallel_Engine.py +0 -0
  36. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/Core/Parallel_ODPS_Manager.py +0 -0
  37. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/Core/Proc_Compare.py +0 -0
  38. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/Core/Slope_Tool.py +0 -0
  39. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/Core/Slope_Tool.pyi +0 -0
  40. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/Core/XOR_Encryptor.py +0 -0
  41. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/Core/XOR_Encryptor.pyi +0 -0
  42. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/Core/__init__.py +0 -0
  43. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/Core/kDataFrame.py +0 -0
  44. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/Core/kDataFrame.pyi +0 -0
  45. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/Core/utils.py +0 -0
  46. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/Eval/Evaluation_Tool.py +0 -0
  47. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/Eval/Evaluation_Tool.pyi +0 -0
  48. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/Eval/__init__.py +0 -0
  49. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/Eval/evaluate_model.py +0 -0
  50. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/Eval/evaluate_model.pyi +0 -0
  51. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/Explainability/Coalition_Structure.py +0 -0
  52. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/Explainability/Model_Explainer.py +0 -0
  53. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/Explainability/__init__.py +0 -0
  54. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/Feature/Distribution_Tool.py +0 -0
  55. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/Feature/Distribution_Tool.pyi +0 -0
  56. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/Feature/Feature_Insights.py +0 -0
  57. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/Feature/Feature_Insights.pyi +0 -0
  58. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/Feature/Feature_Screen.py +0 -0
  59. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/Feature/PSI_Tool.py +0 -0
  60. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/Feature/PSI_Tool.pyi +0 -0
  61. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/Feature/WOE_Engine_Feature_Patch.py +0 -0
  62. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/Feature/Weighted_Screen.py +0 -0
  63. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/Feature/__init__.py +0 -0
  64. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/Model/Backward_Tool.py +0 -0
  65. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/Model/Backward_Tool.pyi +0 -0
  66. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/Model/GBM_Search_Tool.py +0 -0
  67. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/Model/GBM_Tool.py +0 -0
  68. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/Model/GBM_Tool.pyi +0 -0
  69. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/Model/__init__.py +0 -0
  70. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/Pipeline/__init__.py +0 -0
  71. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/Pipeline/feature_validation.py +0 -0
  72. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/Pipeline/mock_sample.py +0 -0
  73. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/Pipeline/orchestrator.py +0 -0
  74. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/Pipeline/sample_analysis.py +0 -0
  75. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/Pipeline/score_comparison.py +0 -0
  76. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/Pipeline/screening_artifact.py +0 -0
  77. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/Sample/Distribution_Adaptation.py +0 -0
  78. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/Sample/Distribution_Adaptation.pyi +0 -0
  79. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/Sample/Sample_Split.py +0 -0
  80. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/Sample/Sample_Split.pyi +0 -0
  81. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/Sample/__init__.py +0 -0
  82. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/UAT/UAT_Consistency_Checker.py +0 -0
  83. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/UAT/__init__.py +0 -0
  84. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/WOE/WOE_Adapter.py +0 -0
  85. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/WOE/WOE_Adapter.pyi +0 -0
  86. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/WOE/WOE_Master.py +0 -0
  87. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/WOE/WOE_Master.pyi +0 -0
  88. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/WOE/WOE_Monotone_Binner.py +0 -0
  89. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/WOE/WOE_Monotone_Binner.pyi +0 -0
  90. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/WOE/WOE_Plot_Tool.py +0 -0
  91. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/WOE/WOE_Plot_Tool.pyi +0 -0
  92. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/WOE/WOE_Report_Builder.py +0 -0
  93. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/WOE/WOE_Report_Builder.pyi +0 -0
  94. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/WOE/WOE_Tool.py +0 -0
  95. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/WOE/WOE_Tool.pyi +0 -0
  96. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/WOE/__init__.py +0 -0
  97. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/WOE/plot_woe_tool.py +0 -0
  98. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/WOE/plot_woe_tool.pyi +0 -0
  99. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/_utils/robust.py +0 -0
  100. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/_utils/sentinels.py +0 -0
  101. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/ref_font/KaiTi.ttf +0 -0
  102. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/ref_font/WeiRuanYaHei.ttf +0 -0
  103. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/ref_font/__init__.py +0 -0
  104. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Modeling_Tool/ref_font/simsun.ttc +0 -0
  105. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Report/Report_Tool.py +0 -0
  106. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/Report/__init__.py +0 -0
  107. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/SuperModelingFactory.egg-info/dependency_links.txt +0 -0
  108. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/SuperModelingFactory.egg-info/not-zip-safe +0 -0
  109. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/SuperModelingFactory.egg-info/requires.txt +0 -0
  110. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/SuperModelingFactory.egg-info/top_level.txt +0 -0
  111. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/requirements.txt +0 -0
  112. {supermodelingfactory-0.5.2 → supermodelingfactory-0.5.3}/setup.cfg +0 -0
@@ -367,8 +367,7 @@ class NumVarBinning:
367
367
  if modify:
368
368
  self.cut_points = self.modify_cutpoints(df, points)
369
369
  else:
370
- points.sort()
371
- self.cut_points = points
370
+ self.cut_points = sorted(points)
372
371
  self.bins = len(self.cut_points) - 1
373
372
 
374
373
  binning_series = pd.cut(df[self.var_name], self.cut_points, right=False)
@@ -39,6 +39,41 @@ def _make_related_odps_table_name(table_name, suffix):
39
39
  return related_bare, related_bare
40
40
 
41
41
 
42
+ def _parse_odps_partition_spec(partition):
43
+ if isinstance(partition, dict):
44
+ return [(str(k).strip(), str(v).strip().strip("'\"")) for k, v in partition.items()]
45
+ pairs = []
46
+ for item in str(partition).split(","):
47
+ if not item.strip():
48
+ continue
49
+ if "=" not in item:
50
+ raise ValueError(f"Invalid ODPS partition spec item {item!r}; expected key=value.")
51
+ key, value = item.split("=", 1)
52
+ pairs.append((key.strip(), value.strip().strip("'\"")))
53
+ if not pairs:
54
+ raise ValueError("partition must contain at least one key=value pair.")
55
+ return pairs
56
+
57
+
58
+ def _format_odps_partition_spec(pairs, quoted=False):
59
+ parts = []
60
+ for key, value in pairs:
61
+ safe_value = str(value).replace("'", "''")
62
+ if quoted:
63
+ parts.append(f"{key}='{safe_value}'")
64
+ else:
65
+ parts.append(f"{key}={safe_value}")
66
+ return ",".join(parts)
67
+
68
+
69
+ def _make_related_odps_partition_spec(partition, suffix):
70
+ pairs = _parse_odps_partition_spec(partition)
71
+ related = list(pairs)
72
+ key, value = related[-1]
73
+ related[-1] = (key, f"{value}{suffix}")
74
+ return _format_odps_partition_spec(related, quoted=False)
75
+
76
+
42
77
  class ODPSRunner(object):
43
78
  _wide_schema_patch_lock = threading.RLock()
44
79
  _wide_schema_patch_ref_count = 0
@@ -392,7 +427,21 @@ class ODPSRunner(object):
392
427
 
393
428
  logger.info(f'<<<< 完成数据入表{table_name}: shape={df.shape} >>>>')
394
429
 
395
- def insert_df(self, df, table_name, overwrite=True, partition=None):
430
+ def _partition_exists(self, table, partition):
431
+ if hasattr(table, "exist_partition"):
432
+ return bool(table.exist_partition(partition))
433
+ try:
434
+ table.get_partition(partition)
435
+ return True
436
+ except Exception:
437
+ return False
438
+
439
+ def _rename_partition(self, table_name, source_partition, target_partition):
440
+ src = _format_odps_partition_spec(_parse_odps_partition_spec(source_partition), quoted=True)
441
+ dst = _format_odps_partition_spec(_parse_odps_partition_spec(target_partition), quoted=True)
442
+ self.o.run_sql(f"ALTER TABLE {table_name} PARTITION ({src}) RENAME TO PARTITION ({dst});")
443
+
444
+ def insert_df(self, df, table_name, overwrite=True, partition=None, atomic=True):
396
445
  """将数据集插入至mc已存在的表中.
397
446
 
398
447
  Parameters
@@ -411,6 +460,53 @@ class ODPSRunner(object):
411
460
  df.loc[:, "py_inserttime"] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
412
461
 
413
462
  if bool(partition):
463
+ if overwrite and atomic:
464
+ ts = datetime.now().strftime('%Y%m%d%H%M%S%f')
465
+ staging_partition = _make_related_odps_partition_spec(partition, f"__staging_{ts}")
466
+ backup_partition = _make_related_odps_partition_spec(partition, f"__old_{ts}")
467
+ target_exists = self._partition_exists(t, partition)
468
+
469
+ for stale in (staging_partition, backup_partition):
470
+ try:
471
+ t.delete_partition(stale, if_exists=True)
472
+ except Exception as cleanup_exc: # noqa: BLE001
473
+ logger.warning(
474
+ f"insert_df: failed to cleanup partition {stale} before atomic write: {cleanup_exc!r}"
475
+ )
476
+
477
+ try:
478
+ with t.open_writer(partition=staging_partition, create_partition=True) as writer:
479
+ writer.write(df.values.tolist())
480
+ if target_exists:
481
+ self._rename_partition(table_name, partition, backup_partition)
482
+ self._rename_partition(table_name, staging_partition, partition)
483
+ except Exception:
484
+ if target_exists:
485
+ try:
486
+ if not self._partition_exists(t, partition) and self._partition_exists(t, backup_partition):
487
+ self._rename_partition(table_name, backup_partition, partition)
488
+ except Exception as restore_exc: # noqa: BLE001
489
+ logger.error(
490
+ f"insert_df: failed to restore original partition {partition} "
491
+ f"from {backup_partition}: {restore_exc!r}. Manual recovery may be required."
492
+ )
493
+ try:
494
+ t.delete_partition(staging_partition, if_exists=True)
495
+ except Exception: # noqa: BLE001
496
+ pass
497
+ raise
498
+
499
+ if target_exists:
500
+ try:
501
+ t.delete_partition(backup_partition, if_exists=True)
502
+ except Exception as cleanup_exc: # noqa: BLE001
503
+ logger.warning(
504
+ f"insert_df: atomic swap succeeded but failed to drop backup partition "
505
+ f"{backup_partition}: {cleanup_exc!r}. Safe to delete manually."
506
+ )
507
+ logger.info('<<<< insert_df atomic partition write done: shape={0} >>>>'.format(df.shape))
508
+ return
509
+
414
510
  if overwrite:
415
511
  t.delete_partition(partition, if_exists=True)
416
512
  with t.open_writer(partition=partition, create_partition=True) as writer:
@@ -52,6 +52,12 @@ def validate_sample_weight(weights, expected_len=None):
52
52
  raise ValueError("sample_weight must be finite (no NaN/inf).")
53
53
  if np.any(w < 0):
54
54
  raise ValueError("sample_weight must be non-negative.")
55
+ total = float(np.sum(w))
56
+ if total <= 0:
57
+ raise ValueError(
58
+ "sample_weight sum must be > 0; got sum={0}. "
59
+ "All-zero weights make weighted metrics undefined.".format(total)
60
+ )
55
61
  return w
56
62
 
57
63
 
@@ -1,5 +1,6 @@
1
1
  import logging
2
2
  import os
3
+ import warnings
3
4
  import numpy as np
4
5
  import pandas as pd
5
6
  from Modeling_Tool.Core.Binning_Tool import get_bin_range_list, super_binning
@@ -2246,8 +2247,9 @@ class PerformanceEvaluator:
2246
2247
  self.weight_col = weight_col
2247
2248
  self.datasets = {}
2248
2249
  self.dataset_weight_cols = {}
2250
+ self.evaluate_status = None
2249
2251
 
2250
- def add_dataset(self, name, data, weight_col = None):
2252
+ def add_dataset(self, name, data, weight_col = None, overwrite = False):
2251
2253
  """
2252
2254
  添加数据集。
2253
2255
 
@@ -2263,6 +2265,17 @@ class PerformanceEvaluator:
2263
2265
  self
2264
2266
  返回自身以便链式调用
2265
2267
  """
2268
+ if name in self.datasets and not overwrite:
2269
+ raise KeyError(
2270
+ f"Dataset name {name!r} already exists. Use overwrite=True to replace it, "
2271
+ "or pass a different dataset name."
2272
+ )
2273
+ if name in self.datasets and overwrite:
2274
+ warnings.warn(
2275
+ f"PerformanceEvaluator.add_dataset overwriting existing dataset {name!r}.",
2276
+ RuntimeWarning,
2277
+ stacklevel=2,
2278
+ )
2266
2279
  self.datasets[name] = data
2267
2280
  self.dataset_weight_cols[name] = weight_col
2268
2281
  return self
@@ -2304,16 +2317,38 @@ class PerformanceEvaluator:
2304
2317
  ``fig_save_path`` 自动按标签加后缀 (如 ``perf.png`` → ``perf_<label>.png``)。
2305
2318
  """
2306
2319
  if len(self.datasets) == 0:
2320
+ self.evaluate_status = "no_datasets"
2307
2321
  return pd.DataFrame([])
2308
2322
 
2309
2323
  if self.scr_name is None and self.model is None and self.feature_cols is None:
2310
- return -1
2324
+ self.evaluate_status = "compute_failed"
2325
+ warnings.warn(
2326
+ "PerformanceEvaluator.evaluate() now returns an empty DataFrame instead of -1; "
2327
+ "inspect .evaluate_status for the failure reason.",
2328
+ DeprecationWarning,
2329
+ stacklevel=2,
2330
+ )
2331
+ return pd.DataFrame([])
2311
2332
 
2312
2333
  if self.scr_name is None and self.model is None:
2313
- return -2
2334
+ self.evaluate_status = "compute_failed"
2335
+ warnings.warn(
2336
+ "PerformanceEvaluator.evaluate() now returns an empty DataFrame instead of -2; "
2337
+ "inspect .evaluate_status for the failure reason.",
2338
+ DeprecationWarning,
2339
+ stacklevel=2,
2340
+ )
2341
+ return pd.DataFrame([])
2314
2342
 
2315
2343
  if self.scr_name is None and self.feature_cols is None:
2316
- return -3
2344
+ self.evaluate_status = "compute_failed"
2345
+ warnings.warn(
2346
+ "PerformanceEvaluator.evaluate() now returns an empty DataFrame instead of -3; "
2347
+ "inspect .evaluate_status for the failure reason.",
2348
+ DeprecationWarning,
2349
+ stacklevel=2,
2350
+ )
2351
+ return pd.DataFrame([])
2317
2352
 
2318
2353
  active_weight_col = weight_col or self.weight_col
2319
2354
  has_dataset_weight = any(v is not None for v in self.dataset_weight_cols.values())
@@ -2369,6 +2404,7 @@ class PerformanceEvaluator:
2369
2404
  )
2370
2405
  if rpt_save_path:
2371
2406
  fnl_df.to_csv(rpt_save_path, index=False)
2407
+ self.evaluate_status = "ok" if not fnl_df.empty else "empty_input"
2372
2408
  return fnl_df
2373
2409
 
2374
2410
  # ── 多 y 标签支持: tgt_name 为 list/tuple 时, 逐标签评估后纵向拼接 ──
@@ -2412,6 +2448,7 @@ class PerformanceEvaluator:
2412
2448
  _ipy_display(fnl_df)
2413
2449
  if rpt_save_path:
2414
2450
  fnl_df.to_csv(rpt_save_path, index = False)
2451
+ self.evaluate_status = "ok" if not fnl_df.empty else "empty_input"
2415
2452
  return fnl_df
2416
2453
 
2417
2454
  def _get_score(data):
@@ -2472,6 +2509,7 @@ class PerformanceEvaluator:
2472
2509
  }
2473
2510
 
2474
2511
  if len(eval_datasets) == 0:
2512
+ self.evaluate_status = "empty_input"
2475
2513
  return pd.DataFrame([])
2476
2514
 
2477
2515
  model_eval_result_df = evaluate_performance(
@@ -2570,4 +2608,5 @@ class PerformanceEvaluator:
2570
2608
  if rpt_save_path:
2571
2609
  fnl_df.to_csv(rpt_save_path, index = False)
2572
2610
 
2611
+ self.evaluate_status = "ok" if isinstance(fnl_df, pd.DataFrame) and not fnl_df.empty else "empty_input"
2573
2612
  return fnl_df
@@ -32,6 +32,6 @@ class GainsTableCalculator:
32
32
  def calculate(self, grp_name = None, min_data_size = 100, grp_colname = None, sync_range = True, retSummary = False, withSummary = False, wholeGroup = False, add_func = None): ...
33
33
 
34
34
  class PerformanceEvaluator:
35
- def __init__(self, tgt_name, scr_name = None, model = None, feature_cols = None, dist_bins = 20, pct_bins = 10, precision = 5, min_bin_prop = 0.05, include_missing = False, equal_freq = True, chi2_method = False, init_equi_bins = 1000, chi2_p = 0.9, tree_binning = False, random_state = 42): ...
36
- def add_dataset(self, name, data): ...
37
- def evaluate(self, oot_grp_name = None, min_data_size = 100, grp_colname = None, fig_save_path = None, rpt_save_path = None, to_show = False, display = True, gains_table = False, benchmark_dataset = None): ...
35
+ def __init__(self, tgt_name, scr_name = None, model = None, feature_cols = None, dist_bins = 20, pct_bins = 10, precision = 5, min_bin_prop = 0.05, include_missing = False, equal_freq = True, chi2_method = False, init_equi_bins = 1000, chi2_p = 0.9, tree_binning = False, random_state = 42, weight_col = None): ...
36
+ def add_dataset(self, name, data, weight_col = None, overwrite = False): ...
37
+ def evaluate(self, oot_grp_name = None, min_data_size = 100, grp_colname = None, fig_save_path = None, rpt_save_path = None, to_show = False, display = True, gains_table = False, benchmark_dataset = None, weight_col = None): ...
@@ -14,6 +14,7 @@ import pandas as pd
14
14
  from sklearn.metrics import precision_recall_curve, roc_auc_score, roc_curve
15
15
 
16
16
  from Modeling_Tool.Core.sample_weight_utils import resolve_sample_weight
17
+ from Modeling_Tool._utils.robust import smf_logger
17
18
 
18
19
 
19
20
  def resolve_weights(data=None, weight_col=None, sample_weight=None, expected_len=None, wgt=None, wgt_col=None):
@@ -43,7 +44,8 @@ def safe_weighted_average(values, weights=None):
43
44
  def safe_auc(y_true, y_score, sample_weight=None):
44
45
  try:
45
46
  return float(roc_auc_score(y_true, y_score, sample_weight=sample_weight))
46
- except Exception:
47
+ except (ValueError, ZeroDivisionError) as exc:
48
+ smf_logger.record_and_continue("auc", exc, stage="weighted_eval.safe_auc")
47
49
  return np.nan
48
50
 
49
51
 
@@ -6,6 +6,7 @@ import pandas as pd
6
6
  from sklearn.linear_model import LogisticRegression
7
7
  import logging
8
8
  from Modeling_Tool.Core.sample_weight_utils import resolve_sample_weight
9
+ from Modeling_Tool._utils.nan_guard import warn_if_nan_ratio_exceeds
9
10
 
10
11
  logger = logging.getLogger(__name__)
11
12
 
@@ -252,6 +253,33 @@ def compute_bic(model, x, y, sample_weight=None):
252
253
  return bic
253
254
 
254
255
 
256
+ def _prepare_nan_handled_frame(data, nan_handling="fillna_median", warn_threshold=0.05, context=""):
257
+ """Return a numeric frame with explicit NaN handling for selection stats."""
258
+ allowed = {"fillna_0", "fillna_mean", "fillna_median", "drop_rows", "raise"}
259
+ if nan_handling not in allowed:
260
+ raise ValueError(f"nan_handling must be one of {sorted(allowed)}; got {nan_handling!r}")
261
+
262
+ frame = data.copy()
263
+ for col in frame.columns:
264
+ warn_if_nan_ratio_exceeds(frame[col], threshold=warn_threshold, context=f"{context}.{col}")
265
+
266
+ if nan_handling == "raise":
267
+ if frame.isna().any().any():
268
+ missing_cols = frame.columns[frame.isna().any()].tolist()
269
+ raise ValueError(f"{context}: NaN values present in columns {missing_cols}")
270
+ return frame
271
+ if nan_handling == "drop_rows":
272
+ return frame.dropna(axis=0)
273
+ if nan_handling == "fillna_0":
274
+ return frame.fillna(0)
275
+
276
+ if nan_handling == "fillna_mean":
277
+ fill_values = frame.mean(numeric_only=True).fillna(0)
278
+ else:
279
+ fill_values = frame.median(numeric_only=True).fillna(0)
280
+ return frame.fillna(fill_values).fillna(0)
281
+
282
+
255
283
  class FeatureSelectionAnalyzer:
256
284
  """
257
285
  Feature selection analyzer using statistical tests.
@@ -284,7 +312,7 @@ class FeatureSelectionAnalyzer:
284
312
  self.selected_features_ = None
285
313
  self.chi2_results_ = None
286
314
 
287
- def chi2_selection(self, data, feature_cols, target_col):
315
+ def chi2_selection(self, data, feature_cols, target_col, nan_handling="fillna_median", nan_warn_threshold=0.05):
288
316
  """
289
317
  Select features using chi-squared test.
290
318
 
@@ -305,8 +333,13 @@ class FeatureSelectionAnalyzer:
305
333
  from sklearn.feature_selection import chi2
306
334
  from sklearn.preprocessing import MinMaxScaler
307
335
 
308
- x = data[feature_cols].fillna(0)
309
- y = data[target_col]
336
+ x = _prepare_nan_handled_frame(
337
+ data[feature_cols],
338
+ nan_handling=nan_handling,
339
+ warn_threshold=nan_warn_threshold,
340
+ context="FeatureSelectionAnalyzer.chi2_selection",
341
+ )
342
+ y = data.loc[x.index, target_col]
310
343
 
311
344
  scaler = MinMaxScaler()
312
345
  x_scaled = scaler.fit_transform(x)
@@ -324,7 +357,7 @@ class FeatureSelectionAnalyzer:
324
357
  self.selected_features_ = results.loc[results['selected'], 'feature'].tolist()
325
358
  return results
326
359
 
327
- def compute_vif(self, data):
360
+ def compute_vif(self, data, nan_handling="fillna_median", nan_warn_threshold=0.05):
328
361
  """
329
362
  Compute Variance Inflation Factor (VIF) for multicollinearity detection.
330
363
 
@@ -340,9 +373,15 @@ class FeatureSelectionAnalyzer:
340
373
  """
341
374
  from statsmodels.stats.outliers_influence import variance_inflation_factor
342
375
 
343
- x = data.fillna(0).values
376
+ work = _prepare_nan_handled_frame(
377
+ data,
378
+ nan_handling=nan_handling,
379
+ warn_threshold=nan_warn_threshold,
380
+ context="FeatureSelectionAnalyzer.compute_vif",
381
+ )
382
+ x = work.values
344
383
  vif_data = pd.DataFrame({
345
- 'feature': data.columns,
384
+ 'feature': work.columns,
346
385
  'VIF': [variance_inflation_factor(x, i) for i in range(x.shape[1])]
347
386
  }).sort_values('VIF', ascending=False).reset_index(drop=True)
348
387
 
@@ -26,8 +26,8 @@ def compute_bic(model, x, y): ...
26
26
 
27
27
  class FeatureSelectionAnalyzer:
28
28
  def __init__(self, significance_level = 0.05): ...
29
- def chi2_selection(self, data, feature_cols, target_col): ...
30
- def compute_vif(self, data): ...
29
+ def chi2_selection(self, data, feature_cols, target_col, nan_handling = "fillna_median", nan_warn_threshold = 0.05): ...
30
+ def compute_vif(self, data, nan_handling = "fillna_median", nan_warn_threshold = 0.05): ...
31
31
  def correlation_filter(self, data, threshold = 0.8): ...
32
32
 
33
33
  class LRMaster:
@@ -285,7 +285,12 @@ def get_raw_model(model: Any) -> Any:
285
285
  return model
286
286
 
287
287
 
288
- def predict_positive(model: Any, data: pd.DataFrame, feature_cols: list[str]) -> np.ndarray:
288
+ def predict_positive(
289
+ model: Any,
290
+ data: pd.DataFrame,
291
+ feature_cols: list[str],
292
+ warn_nan: bool = True,
293
+ ) -> np.ndarray:
289
294
  raw_model = get_raw_model(model)
290
295
  x = data[feature_cols]
291
296
 
@@ -338,7 +343,7 @@ def predict_positive(model: Any, data: pd.DataFrame, feature_cols: list[str]) ->
338
343
  )
339
344
 
340
345
  finite_mask = np.isfinite(result)
341
- if not finite_mask.all():
346
+ if warn_nan and not finite_mask.all():
342
347
  n_bad = int((~finite_mask).sum())
343
348
  warnings.warn(
344
349
  f"predict_positive: {n_bad}/{n_rows} predictions are NaN/Inf "
@@ -349,6 +354,35 @@ def predict_positive(model: Any, data: pd.DataFrame, feature_cols: list[str]) ->
349
354
  return result
350
355
 
351
356
 
357
+ def predict_positive_many(
358
+ model: Any,
359
+ datasets: Mapping[str, pd.DataFrame],
360
+ feature_cols: list[str],
361
+ ) -> tuple[dict[str, np.ndarray], dict[str, int]]:
362
+ """Predict multiple datasets and emit one aggregate NaN/Inf warning."""
363
+ preds: dict[str, np.ndarray] = {}
364
+ stats: dict[str, int] = {}
365
+ total_bad = 0
366
+ total_rows = 0
367
+ for name, data in datasets.items():
368
+ pred = predict_positive(model, data, feature_cols, warn_nan=False)
369
+ bad = int((~np.isfinite(pred)).sum())
370
+ stats[str(name)] = bad
371
+ total_bad += bad
372
+ total_rows += len(pred)
373
+ preds[str(name)] = pred
374
+
375
+ if total_bad:
376
+ detail = ", ".join(f"{name}={count}" for name, count in stats.items() if count)
377
+ warnings.warn(
378
+ f"NaN/Inf predictions coerced or returned: {detail}; "
379
+ f"total {total_bad}/{total_rows}.",
380
+ RuntimeWarning,
381
+ stacklevel=2,
382
+ )
383
+ return preds, stats
384
+
385
+
352
386
  def add_dataset_with_optional_weight(
353
387
  evaluator: Any,
354
388
  name: str,
@@ -6,6 +6,7 @@ from typing import Any, Literal
6
6
 
7
7
  import numpy as np
8
8
  import pandas as pd
9
+ import warnings
9
10
 
10
11
  from ._common import (
11
12
  add_dataset_with_optional_weight,
@@ -175,6 +176,7 @@ class CreditModelPipeline:
175
176
 
176
177
  def __init__(self, config: CreditModelPipelineConfig | None = None):
177
178
  self.config = config or CreditModelPipelineConfig()
179
+ self.predict_positive_nan_stats: dict[str, dict[str, int]] = {}
178
180
  self._validate_gbm_feature_source_config()
179
181
 
180
182
  def run(self, data: pd.DataFrame) -> CreditModelPipelineResult:
@@ -1101,10 +1103,23 @@ class CreditModelPipeline:
1101
1103
  equal_freq=True,
1102
1104
  )
1103
1105
  eval_splits = {**splits, **model_extra}
1106
+ nan_stats: dict[str, int] = {}
1104
1107
  for ds_name, df in eval_splits.items():
1105
1108
  scored = df.copy()
1106
1109
  scored[f"pred_{name}"] = self._predict_model_positive(name, wrapper, scored, feature_cols)
1110
+ nan_stats[str(ds_name)] = int((~np.isfinite(scored[f"pred_{name}"].to_numpy(dtype=float))).sum())
1107
1111
  add_dataset_with_optional_weight(evaluator, ds_name, scored, weight_col=cfg.weight_col)
1112
+ self.predict_positive_nan_stats[str(name)] = nan_stats
1113
+ total_bad = sum(nan_stats.values())
1114
+ if total_bad:
1115
+ total_rows = sum(len(df) for df in eval_splits.values())
1116
+ detail = ", ".join(f"{k}={v}" for k, v in nan_stats.items() if v)
1117
+ warnings.warn(
1118
+ f"{name}: NaN/Inf predictions detected across evaluation datasets: "
1119
+ f"{detail}; total {total_bad}/{total_rows}.",
1120
+ RuntimeWarning,
1121
+ stacklevel=2,
1122
+ )
1108
1123
  fig_save_path = None
1109
1124
  if cfg.write_outputs and cfg.plot_outputs:
1110
1125
  fig_save_path = str(Path(cfg.output_dir) / "figs" / "perf" / f"perf_{name}.png")
@@ -1121,7 +1136,7 @@ class CreditModelPipeline:
1121
1136
  if self._warm_start_requested_for(model_name) and model_name in {"lgb", "xgb"}:
1122
1137
  init_score = self._get_warm_start_init_score(model_name, data)
1123
1138
  return wrapper.predict_with_base_margin(data[feature_cols], init_score, return_prob=True)
1124
- return predict_positive(wrapper, data, feature_cols)
1139
+ return predict_positive(wrapper, data, feature_cols, warn_nan=False)
1125
1140
 
1126
1141
  def _will_run_explainability(self) -> bool:
1127
1142
  cfg = self.config
@@ -128,6 +128,7 @@ class RejectInferencePipeline:
128
128
 
129
129
  def __init__(self, config: RejectInferencePipelineConfig | None = None):
130
130
  self.config = config or RejectInferencePipelineConfig()
131
+ self.predict_positive_nan_stats: dict[str, dict[str, int]] = {}
131
132
 
132
133
  def run(self, data: pd.DataFrame) -> RejectInferencePipelineResult:
133
134
  cfg = self.config
@@ -718,8 +719,22 @@ class RejectInferencePipeline:
718
719
 
719
720
  train_eval = train[train[cfg.approved_col] == 1].copy()
720
721
  eval_sets = {"train": train_eval, "validation": val.copy(), "oot": oot.copy()}
722
+ nan_stats: dict[str, int] = {}
721
723
  for ds in eval_sets.values():
722
- ds["pred_prob"] = predict_positive(model, ds, feature_cols)
724
+ ds["pred_prob"] = predict_positive(model, ds, feature_cols, warn_nan=False)
725
+ for ds_name, ds in eval_sets.items():
726
+ nan_stats[ds_name] = int((~np.isfinite(ds["pred_prob"].to_numpy(dtype=float))).sum())
727
+ self.predict_positive_nan_stats[str(method)] = nan_stats
728
+ total_bad = sum(nan_stats.values())
729
+ if total_bad:
730
+ total_rows = sum(len(ds) for ds in eval_sets.values())
731
+ detail = ", ".join(f"{k}={v}" for k, v in nan_stats.items() if v)
732
+ warnings.warn(
733
+ f"{method}: NaN/Inf predictions detected across RI evaluation datasets: "
734
+ f"{detail}; total {total_bad}/{total_rows}.",
735
+ RuntimeWarning,
736
+ stacklevel=2,
737
+ )
723
738
 
724
739
  evaluator = PerformanceEvaluator(
725
740
  tgt_name=cfg.target_col,
@@ -242,8 +242,6 @@ class ScoreConsistencyUATPipeline:
242
242
  coerced_count = int((as_num.notna() & original_notna).sum())
243
243
  parse_ratio = coerced_count / original_count
244
244
  if mode == "aggressive":
245
- if coerced_count == 0:
246
- continue
247
245
  lost = original_count - coerced_count
248
246
  if lost > 0:
249
247
  _logger.warning(
@@ -28,6 +28,8 @@ import numpy as np
28
28
  from typing import Union, Optional, List, Dict, Any, Tuple
29
29
  from abc import ABC, abstractmethod
30
30
 
31
+ from Modeling_Tool._utils.nan_guard import warn_if_nan_ratio_exceeds
32
+
31
33
 
32
34
  class RejectInferrer(ABC):
33
35
  """
@@ -81,6 +83,45 @@ class RejectInferrer(ABC):
81
83
 
82
84
  def _rng(self) -> np.random.Generator:
83
85
  return np.random.default_rng(self.random_state)
86
+
87
+ def _filter_nan_bad_probability(
88
+ self,
89
+ df_rejected: pd.DataFrame,
90
+ p_bad: pd.Series,
91
+ score_col: str,
92
+ nan_score_policy: str,
93
+ ) -> Tuple[pd.DataFrame, pd.Series]:
94
+ if nan_score_policy not in {"drop", "raise", "fill_0.5"}:
95
+ raise ValueError("nan_score_policy must be one of 'drop', 'raise', or 'fill_0.5'")
96
+
97
+ stats = warn_if_nan_ratio_exceeds(
98
+ p_bad,
99
+ threshold=0.01,
100
+ context="reject inference bad probability",
101
+ )
102
+ n_nan = int(stats["n_nan"])
103
+ if n_nan == 0:
104
+ return df_rejected, p_bad
105
+
106
+ n_total = int(stats["n_total"])
107
+ if nan_score_policy == "raise":
108
+ raise ValueError(
109
+ f"Reject inference bad probability contains {n_nan}/{n_total} NaN values "
110
+ f"from score_col={score_col!r}."
111
+ )
112
+ if nan_score_policy == "fill_0.5":
113
+ return df_rejected, p_bad.fillna(0.5)
114
+
115
+ keep = p_bad.notna()
116
+ dropped = int((~keep).sum())
117
+ warnings.warn(
118
+ f"Reject inference dropped {dropped}/{n_total} rejected rows with NaN bad probability "
119
+ f"from score_col={score_col!r}. Set nan_score_policy='fill_0.5' to reproduce "
120
+ f"the legacy 50/50 behaviour.",
121
+ RuntimeWarning,
122
+ stacklevel=2,
123
+ )
124
+ return df_rejected.loc[keep].copy(), p_bad.loc[keep]
84
125
 
85
126
  @abstractmethod
86
127
  def infer(self, df_approved: pd.DataFrame,
@@ -315,12 +356,14 @@ class FuzzyAugmentInferrer(RejectInferrer):
315
356
  weight_factor: float = 1.0,
316
357
  score_direction: str = 'high_good',
317
358
  random_state: Optional[int] = None,
359
+ nan_score_policy: str = "drop",
318
360
  ):
319
361
  """
320
362
  Initialize FuzzyAugmentInferrer.
321
363
  """
322
364
  super().__init__(target_col, score_col, score_direction=score_direction, random_state=random_state)
323
365
  self.weight_factor = weight_factor
366
+ self.nan_score_policy = nan_score_policy
324
367
 
325
368
  def infer(self, df_approved: pd.DataFrame,
326
369
  df_rejected: pd.DataFrame,
@@ -347,7 +390,13 @@ class FuzzyAugmentInferrer(RejectInferrer):
347
390
  df_approved_copy = df_approved.copy()
348
391
  df_approved_copy['_weight'] = 1.0
349
392
 
350
- p_bad = self._bad_probability(df_rejected[score_col]).fillna(0.5)
393
+ p_bad = self._bad_probability(df_rejected[score_col])
394
+ df_rejected, p_bad = self._filter_nan_bad_probability(
395
+ df_rejected,
396
+ p_bad,
397
+ score_col=score_col,
398
+ nan_score_policy=self.nan_score_policy,
399
+ )
351
400
  bad_copy = df_rejected.copy()
352
401
  bad_copy[self.target_col] = 1
353
402
  bad_copy['_weight'] = p_bad.to_numpy(dtype=float) * float(self.weight_factor)
@@ -31,7 +31,7 @@ class HardCutoffInferrer(RejectInferrer):
31
31
  def infer(self, df_approved: pd.DataFrame, df_rejected: pd.DataFrame, score_col: Optional[str] = None) -> pd.DataFrame: ...
32
32
 
33
33
  class FuzzyAugmentInferrer(RejectInferrer):
34
- def __init__(self, target_col: str = 'target', score_col: str = 'score', weight_factor: float = 1.0, score_direction: str = 'high_good', random_state: Optional[int] = None): ...
34
+ def __init__(self, target_col: str = 'target', score_col: str = 'score', weight_factor: float = 1.0, score_direction: str = 'high_good', random_state: Optional[int] = None, nan_score_policy: str = "drop"): ...
35
35
  def infer(self, df_approved: pd.DataFrame, df_rejected: pd.DataFrame, score_col: Optional[str] = None) -> pd.DataFrame: ...
36
36
 
37
37
  class ParcelingInferrer(RejectInferrer):
@@ -1,7 +1,7 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  __author__ = "Jingkai Sun"
4
- __version__ = "0.5.2"
4
+ __version__ = "0.5.3"
5
5
 
6
6
  from ._utils import SMF_MISSING_BIN
7
7
 
@@ -2,5 +2,6 @@
2
2
 
3
3
  from .robust import smf_logger, iv_guard
4
4
  from .sentinels import SMF_MISSING_BIN
5
+ from .nan_guard import warn_if_nan_ratio_exceeds
5
6
 
6
- __all__ = ["smf_logger", "iv_guard", "SMF_MISSING_BIN"]
7
+ __all__ = ["smf_logger", "iv_guard", "SMF_MISSING_BIN", "warn_if_nan_ratio_exceeds"]
@@ -0,0 +1,31 @@
1
+ # encoding: utf-8
2
+ """Shared NaN warning helpers."""
3
+ from __future__ import annotations
4
+
5
+ import warnings
6
+
7
+ import numpy as np
8
+ import pandas as pd
9
+
10
+
11
+ def warn_if_nan_ratio_exceeds(values, threshold: float = 0.05, context: str = "") -> dict:
12
+ """Warn when the NaN/Inf share in ``values`` exceeds ``threshold``.
13
+
14
+ Returns a small stats dict so callers can surface the same information in
15
+ reports without recomputing it.
16
+ """
17
+ arr = pd.to_numeric(pd.Series(values), errors="coerce")
18
+ n_total = int(len(arr))
19
+ n_nan = int((~np.isfinite(arr.to_numpy(dtype=float))).sum()) if n_total else 0
20
+ ratio = n_nan / n_total if n_total else 0.0
21
+ if n_total and ratio > float(threshold):
22
+ label = f"{context}: " if context else ""
23
+ warnings.warn(
24
+ f"{label}{n_nan}/{n_total} ({ratio:.1%}) values are NaN/Inf.",
25
+ RuntimeWarning,
26
+ stacklevel=2,
27
+ )
28
+ return {"n_total": n_total, "n_nan": n_nan, "nan_ratio": ratio}
29
+
30
+
31
+ __all__ = ["warn_if_nan_ratio_exceeds"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: SuperModelingFactory
3
- Version: 0.5.2
3
+ Version: 0.5.3
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
@@ -265,7 +265,7 @@ em.close_workbook()
265
265
 
266
266
  ## 版本
267
267
 
268
- - **Version**: 0.5.2
268
+ - **Version**: 0.5.3
269
269
  - **Author**: Jingkai Sun
270
270
 
271
271
  ## 许可证
@@ -208,7 +208,7 @@ em.close_workbook()
208
208
 
209
209
  ## 版本
210
210
 
211
- - **Version**: 0.5.2
211
+ - **Version**: 0.5.3
212
212
  - **Author**: Jingkai Sun
213
213
 
214
214
  ## 许可证
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: SuperModelingFactory
3
- Version: 0.5.2
3
+ Version: 0.5.3
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
@@ -265,7 +265,7 @@ em.close_workbook()
265
265
 
266
266
  ## 版本
267
267
 
268
- - **Version**: 0.5.2
268
+ - **Version**: 0.5.3
269
269
  - **Author**: Jingkai Sun
270
270
 
271
271
  ## 许可证
@@ -93,6 +93,7 @@ Modeling_Tool/WOE/__init__.py
93
93
  Modeling_Tool/WOE/plot_woe_tool.py
94
94
  Modeling_Tool/WOE/plot_woe_tool.pyi
95
95
  Modeling_Tool/_utils/__init__.py
96
+ Modeling_Tool/_utils/nan_guard.py
96
97
  Modeling_Tool/_utils/robust.py
97
98
  Modeling_Tool/_utils/sentinels.py
98
99
  Modeling_Tool/ref_font/KaiTi.ttf
@@ -12,7 +12,7 @@ build-backend = "setuptools.build_meta"
12
12
 
13
13
  [project]
14
14
  name = "SuperModelingFactory"
15
- version = "0.5.2"
15
+ version = "0.5.3"
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.5.2"),
18
+ version=os.environ.get("SMF_VERSION", "0.5.3"),
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",