SuperModelingFactory 0.5.10__tar.gz → 0.6.1__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 (113) hide show
  1. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/Core/Proc_Compare.py +189 -36
  2. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/Core/kDataFrame.py +8 -16
  3. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/Core/utils.py +10 -7
  4. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/Eval/Model_Eval_Tool.py +26 -46
  5. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/Eval/evaluate_model.py +48 -19
  6. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/Eval/weighted_eval_utils.py +13 -2
  7. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/Explainability/Coalition_Structure.py +5 -5
  8. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/Explainability/Model_Explainer.py +106 -26
  9. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/Feature/Distribution_Tool.py +105 -29
  10. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/Feature/Distribution_Tool.pyi +3 -3
  11. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/Feature/Feature_Insights.py +82 -36
  12. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/Feature/Feature_Screen.py +18 -4
  13. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/Feature/PSI_Tool.py +244 -0
  14. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/Feature/PSI_Tool.pyi +1 -1
  15. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/Feature/WOE_Engine_Feature_Patch.py +181 -61
  16. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/Feature/Weighted_Screen.py +94 -20
  17. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/Pipeline/feature_validation.py +327 -98
  18. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/Pipeline/field_meta.py +10 -0
  19. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/Pipeline/sample_analysis.py +124 -101
  20. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/Pipeline/score_consistency_uat.py +2 -0
  21. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/Sample/Sample_Split.py +32 -3
  22. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/Sample/Sample_Split.pyi +1 -0
  23. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/UAT/UAT_Consistency_Checker.py +73 -50
  24. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/WOE/WOE_Adapter.py +45 -6
  25. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/WOE/WOE_Adapter.pyi +1 -0
  26. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/WOE/WOE_Monotone_Binner.py +30 -9
  27. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/WOE/WOE_Monotone_Binner.pyi +1 -1
  28. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/WOE/WOE_Tool.py +82 -35
  29. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/__init__.py +1 -1
  30. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/PKG-INFO +2 -2
  31. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/README.md +1 -1
  32. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/SuperModelingFactory.egg-info/PKG-INFO +2 -2
  33. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/pyproject.toml +1 -1
  34. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/setup.py +1 -1
  35. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/ExcelMaster/ExcelFormatTool.py +0 -0
  36. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/ExcelMaster/ExcelMaster.py +0 -0
  37. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/ExcelMaster/Template.py +0 -0
  38. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/ExcelMaster/Utility.py +0 -0
  39. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/ExcelMaster/__init__.py +0 -0
  40. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/LICENSE +0 -0
  41. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/MANIFEST.in +0 -0
  42. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/Core/Binning_Tool.py +0 -0
  43. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/Core/Binning_Tool.pyi +0 -0
  44. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/Core/Check_DuckDB_Compatibility.py +0 -0
  45. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/Core/Json_Data_Converter.py +0 -0
  46. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/Core/Model_Registry_Tool.py +0 -0
  47. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/Core/ODPS_Tool.py +0 -0
  48. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/Core/Parallel_Engine.py +0 -0
  49. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/Core/Parallel_ODPS_Manager.py +0 -0
  50. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/Core/Slope_Tool.py +0 -0
  51. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/Core/Slope_Tool.pyi +0 -0
  52. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/Core/XOR_Encryptor.py +0 -0
  53. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/Core/XOR_Encryptor.pyi +0 -0
  54. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/Core/__init__.py +0 -0
  55. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/Core/kDataFrame.pyi +0 -0
  56. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/Core/sample_weight_utils.py +0 -0
  57. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/Eval/Evaluation_Tool.py +0 -0
  58. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/Eval/Evaluation_Tool.pyi +0 -0
  59. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/Eval/Model_Eval_Tool.pyi +0 -0
  60. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/Eval/__init__.py +0 -0
  61. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/Eval/evaluate_model.pyi +0 -0
  62. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/Explainability/__init__.py +0 -0
  63. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/Feature/Feature_Insights.pyi +0 -0
  64. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/Feature/__init__.py +0 -0
  65. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/Model/Backward_Tool.py +0 -0
  66. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/Model/Backward_Tool.pyi +0 -0
  67. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/Model/GBM_Search_Tool.py +0 -0
  68. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/Model/GBM_Tool.py +0 -0
  69. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/Model/GBM_Tool.pyi +0 -0
  70. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/Model/LRM_Tool.py +0 -0
  71. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/Model/LRM_Tool.pyi +0 -0
  72. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/Model/__init__.py +0 -0
  73. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/Pipeline/__init__.py +0 -0
  74. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/Pipeline/_common.py +0 -0
  75. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/Pipeline/credit_model.py +0 -0
  76. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/Pipeline/mock_sample.py +0 -0
  77. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/Pipeline/orchestrator.py +0 -0
  78. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/Pipeline/reject_inference.py +0 -0
  79. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/Pipeline/score_comparison.py +0 -0
  80. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/Pipeline/screening_artifact.py +0 -0
  81. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/Sample/Distribution_Adaptation.py +0 -0
  82. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/Sample/Distribution_Adaptation.pyi +0 -0
  83. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/Sample/Reject_Infer.py +0 -0
  84. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/Sample/Reject_Infer.pyi +0 -0
  85. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/Sample/__init__.py +0 -0
  86. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/UAT/__init__.py +0 -0
  87. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/WOE/WOE_Master.py +0 -0
  88. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/WOE/WOE_Master.pyi +0 -0
  89. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/WOE/WOE_Plot_Tool.py +0 -0
  90. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/WOE/WOE_Plot_Tool.pyi +0 -0
  91. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/WOE/WOE_Report_Builder.py +0 -0
  92. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/WOE/WOE_Report_Builder.pyi +0 -0
  93. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/WOE/WOE_Tool.pyi +0 -0
  94. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/WOE/__init__.py +0 -0
  95. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/WOE/plot_woe_tool.py +0 -0
  96. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/WOE/plot_woe_tool.pyi +0 -0
  97. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/_utils/__init__.py +0 -0
  98. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/_utils/nan_guard.py +0 -0
  99. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/_utils/robust.py +0 -0
  100. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/_utils/sentinels.py +0 -0
  101. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/ref_font/KaiTi.ttf +0 -0
  102. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/ref_font/WeiRuanYaHei.ttf +0 -0
  103. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/ref_font/__init__.py +0 -0
  104. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Modeling_Tool/ref_font/simsun.ttc +0 -0
  105. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Report/Report_Tool.py +0 -0
  106. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/Report/__init__.py +0 -0
  107. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/SuperModelingFactory.egg-info/SOURCES.txt +0 -0
  108. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/SuperModelingFactory.egg-info/dependency_links.txt +0 -0
  109. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/SuperModelingFactory.egg-info/not-zip-safe +0 -0
  110. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/SuperModelingFactory.egg-info/requires.txt +0 -0
  111. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/SuperModelingFactory.egg-info/top_level.txt +0 -0
  112. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/requirements.txt +0 -0
  113. {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.1}/setup.cfg +0 -0
@@ -34,6 +34,7 @@ class ProcCompareConfig:
34
34
  chunk_size: int = 200_000
35
35
  n_partitions: int = 16
36
36
  backend: CompareBackend = "sequential"
37
+ compare_block_size: int = 64
37
38
 
38
39
  numeric_tol: float = 1e-8
39
40
  numeric_rtol: float = 0.0
@@ -107,6 +108,8 @@ class ProcCompareEngine:
107
108
  raise ValueError("chunk_size must be a positive integer.")
108
109
  if cfg.n_partitions <= 0:
109
110
  raise ValueError("n_partitions must be a positive integer.")
111
+ if cfg.compare_block_size <= 0:
112
+ raise ValueError("compare_block_size must be a positive integer.")
110
113
  if cfg.numeric_tol < 0 or cfg.numeric_rtol < 0 or cfg.datetime_tol_seconds < 0:
111
114
  raise ValueError("tolerance values must be non-negative.")
112
115
  if cfg.top_n <= 0:
@@ -294,44 +297,54 @@ class ProcCompareEngine:
294
297
  mismatch_frames = []
295
298
  mismatch_cols_by_row: dict[int, list[str]] = {}
296
299
 
297
- for col in compare_cols:
298
- left_col = f"{col}_left"
299
- right_col = f"{col}_right"
300
- comp = self._compare_series(col, merged[left_col], merged[right_col], common_mask)
301
- mismatch_mask = comp["mismatch"]
302
- n_mismatch = int(mismatch_mask.sum())
303
- if n_mismatch:
304
- mismatch_indexes = merged.index[mismatch_mask]
305
- for idx in mismatch_indexes:
300
+ common_values = common_mask.to_numpy(dtype=bool)
301
+ n_compared = int(common_values.sum())
302
+ block_size = int(self.config.compare_block_size)
303
+ for start in range(0, len(compare_cols), block_size):
304
+ block_cols = compare_cols[start : start + block_size]
305
+ comp = self._compare_column_block(merged, block_cols, common_values)
306
+ mismatch_matrix = comp["mismatch"]
307
+ one_null_matrix = comp["one_null"]
308
+ both_null_matrix = comp["both_null"]
309
+ diff_matrix = comp["diff"]
310
+ abs_diff_matrix = comp["abs_diff"]
311
+ left_values = comp["left_values"]
312
+ right_values = comp["right_values"]
313
+
314
+ for col_idx, col in enumerate(block_cols):
315
+ mismatch_positions = np.flatnonzero(mismatch_matrix[:, col_idx])
316
+ n_mismatch = int(len(mismatch_positions))
317
+ for idx in mismatch_positions:
306
318
  mismatch_cols_by_row.setdefault(int(idx), []).append(col)
307
- if self.config.detail_mode != "none":
308
- detail = merged.loc[mismatch_mask, key_cols].copy()
319
+ if n_mismatch and self.config.detail_mode != "none":
320
+ detail = merged.iloc[mismatch_positions][key_cols].copy()
309
321
  detail["column"] = col
310
- detail["left_value"] = merged.loc[mismatch_mask, left_col].to_numpy()
311
- detail["right_value"] = merged.loc[mismatch_mask, right_col].to_numpy()
312
- detail["diff"] = comp["diff"].loc[mismatch_mask].to_numpy()
313
- detail["abs_diff"] = comp["abs_diff"].loc[mismatch_mask].to_numpy()
322
+ detail["left_value"] = left_values[mismatch_positions, col_idx]
323
+ detail["right_value"] = right_values[mismatch_positions, col_idx]
324
+ detail["diff"] = diff_matrix[mismatch_positions, col_idx]
325
+ detail["abs_diff"] = abs_diff_matrix[mismatch_positions, col_idx]
314
326
  mismatch_frames.append(detail)
315
327
 
316
- n_compared = int(common_mask.sum())
317
- n_one_null = int(comp["one_null"].sum())
318
- n_both_null = int(comp["both_null"].sum())
319
- n_equal = n_compared - n_mismatch
320
- column_rows.append(
321
- {
322
- "column": col,
323
- "n_compared": n_compared,
324
- "n_equal": n_equal,
325
- "n_mismatch": n_mismatch,
326
- "pct_mismatch": round(n_mismatch / n_compared * 100, 6) if n_compared else 0.0,
327
- "n_one_side_null": n_one_null,
328
- "n_both_null": n_both_null,
329
- "mean_diff": comp["diff"].mean(skipna=True),
330
- "max_abs_diff": comp["abs_diff"].max(skipna=True),
331
- "_diff_sum": comp["diff"].sum(skipna=True),
332
- "_diff_count": int(comp["diff"].notna().sum()),
333
- }
334
- )
328
+ diff_values = diff_matrix[:, col_idx]
329
+ abs_diff_values = abs_diff_matrix[:, col_idx]
330
+ diff_valid = np.isfinite(diff_values)
331
+ diff_sum = float(np.nansum(diff_values)) if diff_valid.any() else 0.0
332
+ diff_count = int(diff_valid.sum())
333
+ column_rows.append(
334
+ {
335
+ "column": col,
336
+ "n_compared": n_compared,
337
+ "n_equal": n_compared - n_mismatch,
338
+ "n_mismatch": n_mismatch,
339
+ "pct_mismatch": round(n_mismatch / n_compared * 100, 6) if n_compared else 0.0,
340
+ "n_one_side_null": int(one_null_matrix[:, col_idx].sum()),
341
+ "n_both_null": int(both_null_matrix[:, col_idx].sum()),
342
+ "mean_diff": diff_sum / diff_count if diff_count else np.nan,
343
+ "max_abs_diff": float(np.nanmax(abs_diff_values)) if np.isfinite(abs_diff_values).any() else np.nan,
344
+ "_diff_sum": diff_sum,
345
+ "_diff_count": diff_count,
346
+ }
347
+ )
335
348
 
336
349
  for idx, cols in mismatch_cols_by_row.items():
337
350
  row_summary.loc[idx, "n_cell_mismatch"] = len(cols)
@@ -375,6 +388,146 @@ class ProcCompareEngine:
375
388
  duplicate_key_summary=duplicate_key_summary,
376
389
  )
377
390
 
391
+ def _compare_column_block(
392
+ self,
393
+ merged: pd.DataFrame,
394
+ columns: list[str],
395
+ common_mask: np.ndarray,
396
+ ) -> dict[str, np.ndarray]:
397
+ """Compare a bounded field block with vectorized type-specific kernels."""
398
+ n_rows = len(merged)
399
+ n_cols = len(columns)
400
+ left_series = [self._normalize_series_missing(merged[f"{col}_left"]) for col in columns]
401
+ right_series = [self._normalize_series_missing(merged[f"{col}_right"]) for col in columns]
402
+ left_values = np.column_stack([series.to_numpy(dtype=object) for series in left_series])
403
+ right_values = np.column_stack([series.to_numpy(dtype=object) for series in right_series])
404
+ mismatch = np.zeros((n_rows, n_cols), dtype=bool)
405
+ one_null = np.zeros((n_rows, n_cols), dtype=bool)
406
+ both_null = np.zeros((n_rows, n_cols), dtype=bool)
407
+ diff = np.full((n_rows, n_cols), np.nan, dtype=float)
408
+ abs_diff = np.full((n_rows, n_cols), np.nan, dtype=float)
409
+
410
+ datetime_idx = [
411
+ idx
412
+ for idx, col in enumerate(columns)
413
+ if self._should_compare_datetime(col, left_series[idx], right_series[idx])
414
+ ]
415
+ datetime_set = set(datetime_idx)
416
+ numeric_idx = [
417
+ idx
418
+ for idx in range(n_cols)
419
+ if idx not in datetime_set
420
+ and self._should_compare_numeric(left_series[idx], right_series[idx])
421
+ ]
422
+ numeric_set = set(numeric_idx)
423
+ string_idx = [
424
+ idx
425
+ for idx in range(n_cols)
426
+ if idx not in datetime_set and idx not in numeric_set
427
+ ]
428
+ common = common_mask[:, None]
429
+
430
+ if datetime_idx:
431
+ left_dt = np.column_stack(
432
+ [self._datetime_seconds(left_series[idx]) for idx in datetime_idx]
433
+ )
434
+ right_dt = np.column_stack(
435
+ [self._datetime_seconds(right_series[idx]) for idx in datetime_idx]
436
+ )
437
+ block_diff = left_dt - right_dt
438
+ block_abs = np.abs(block_diff)
439
+ left_null = ~np.isfinite(left_dt)
440
+ right_null = ~np.isfinite(right_dt)
441
+ tolerances = np.asarray(
442
+ [self._column_datetime_tol(columns[idx]) for idx in datetime_idx],
443
+ dtype=float,
444
+ )
445
+ block_one = (left_null ^ right_null) & common
446
+ block_both = left_null & right_null & common
447
+ block_mismatch = (block_abs > tolerances[None, :]) | block_one
448
+ if not self.config.both_null_equal:
449
+ block_mismatch |= block_both
450
+ block_mismatch &= common
451
+ diff[:, datetime_idx] = block_diff
452
+ abs_diff[:, datetime_idx] = block_abs
453
+ one_null[:, datetime_idx] = block_one
454
+ both_null[:, datetime_idx] = block_both
455
+ mismatch[:, datetime_idx] = block_mismatch
456
+
457
+ if numeric_idx:
458
+ left_num = np.column_stack(
459
+ [pd.to_numeric(left_series[idx], errors="coerce").to_numpy(dtype=float, na_value=np.nan) for idx in numeric_idx]
460
+ )
461
+ right_num = np.column_stack(
462
+ [pd.to_numeric(right_series[idx], errors="coerce").to_numpy(dtype=float, na_value=np.nan) for idx in numeric_idx]
463
+ )
464
+ block_diff = left_num - right_num
465
+ block_abs = np.abs(block_diff)
466
+ left_null = ~np.isfinite(left_num)
467
+ right_null = ~np.isfinite(right_num)
468
+ tolerances = np.asarray(
469
+ [self._column_numeric_tol(columns[idx]) for idx in numeric_idx],
470
+ dtype=float,
471
+ )
472
+ limit = tolerances[:, 0][None, :] + tolerances[:, 1][None, :] * np.abs(right_num)
473
+ block_one = (left_null ^ right_null) & common
474
+ block_both = left_null & right_null & common
475
+ block_mismatch = (block_abs > limit) | block_one
476
+ if not self.config.both_null_equal:
477
+ block_mismatch |= block_both
478
+ block_mismatch &= common
479
+ diff[:, numeric_idx] = block_diff
480
+ abs_diff[:, numeric_idx] = block_abs
481
+ one_null[:, numeric_idx] = block_one
482
+ both_null[:, numeric_idx] = block_both
483
+ mismatch[:, numeric_idx] = block_mismatch
484
+
485
+ if string_idx:
486
+ left_null = np.column_stack([left_series[idx].isna().to_numpy() for idx in string_idx])
487
+ right_null = np.column_stack([right_series[idx].isna().to_numpy() for idx in string_idx])
488
+ left_text = np.column_stack(
489
+ [left_series[idx].astype("string").fillna("").astype(str).to_numpy() for idx in string_idx]
490
+ )
491
+ right_text = np.column_stack(
492
+ [right_series[idx].astype("string").fillna("").astype(str).to_numpy() for idx in string_idx]
493
+ )
494
+ block_one = (left_null ^ right_null) & common
495
+ block_both = left_null & right_null & common
496
+ block_mismatch = (left_text != right_text) | block_one
497
+ if self.config.both_null_equal:
498
+ block_mismatch &= ~block_both
499
+ else:
500
+ block_mismatch |= block_both
501
+ block_mismatch &= common
502
+ one_null[:, string_idx] = block_one
503
+ both_null[:, string_idx] = block_both
504
+ mismatch[:, string_idx] = block_mismatch
505
+
506
+ return {
507
+ "mismatch": mismatch,
508
+ "one_null": one_null,
509
+ "both_null": both_null,
510
+ "diff": diff,
511
+ "abs_diff": abs_diff,
512
+ "left_values": left_values,
513
+ "right_values": right_values,
514
+ }
515
+
516
+ @staticmethod
517
+ def _datetime_seconds(series: pd.Series) -> np.ndarray:
518
+ values = ProcCompareEngine._to_datetime_series(series)
519
+ missing = values.isna().to_numpy()
520
+ result = values.astype("int64").to_numpy(dtype=float) / 1_000_000_000.0
521
+ result[missing] = np.nan
522
+ return result
523
+
524
+ @staticmethod
525
+ def _to_datetime_series(series: pd.Series) -> pd.Series:
526
+ try:
527
+ return pd.to_datetime(series, errors="coerce", format="mixed")
528
+ except (TypeError, ValueError):
529
+ return pd.to_datetime(series, errors="coerce")
530
+
378
531
  def _compare_series(
379
532
  self,
380
533
  col: str,
@@ -391,8 +544,8 @@ class ProcCompareEngine:
391
544
  abs_diff = pd.Series(np.nan, index=left.index, dtype="float64")
392
545
 
393
546
  if self._should_compare_datetime(col, left, right):
394
- left_dt = pd.to_datetime(left, errors="coerce")
395
- right_dt = pd.to_datetime(right, errors="coerce")
547
+ left_dt = self._to_datetime_series(left)
548
+ right_dt = self._to_datetime_series(right)
396
549
  diff = (left_dt - right_dt).dt.total_seconds()
397
550
  abs_diff = diff.abs()
398
551
  tol = self._column_datetime_tol(col)
@@ -57,13 +57,7 @@ class kSeries(Series):
57
57
  """
58
58
  Scale the model scores (for internt segment of MCI model)
59
59
  """
60
- data = self.copy()
61
- def app_func(x):
62
- scores = x * 1.112
63
- if (scores > 0.9999999):
64
- return 0.9999999
65
- return scores
66
- return data.apply(app_func)
60
+ return (self * 1.112).clip(upper=0.9999999)
67
61
 
68
62
  def proc_freq(self) -> pd.DataFrame:
69
63
  """
@@ -128,8 +122,12 @@ class kDataFrame(DataFrame):
128
122
  data = self
129
123
  else:
130
124
  data = self.copy()
131
- import re
132
- data[vintage_colname] = data[by].apply(lambda x: re.search("\d{4}-\d{2}", x).group().replace('-', ''))
125
+ data[vintage_colname] = (
126
+ data[by]
127
+ .astype("string")
128
+ .str.extract(r"(\d{4}-\d{2})", expand=False)
129
+ .str.replace("-", "", regex=False)
130
+ )
133
131
  if inplace:
134
132
  self = data
135
133
  return data
@@ -165,13 +163,7 @@ class kDataFrame(DataFrame):
165
163
  """
166
164
  Scale the model scores (for internt segment of MCI model)
167
165
  """
168
- data = self.copy()
169
- def app_func(x):
170
- scores = x * 1.112
171
- if (scores > 0.9999999):
172
- return 0.9999999
173
- return scores
174
- return data[pb_score].apply(app_func)
166
+ return (self[pb_score] * 1.112).clip(upper=0.9999999)
175
167
 
176
168
  def proc_freq(self, var: str):
177
169
  """
@@ -342,8 +342,12 @@ def convert_to_vintage(data, vintage_colname = 'VINTAGE', by = 'TRAN_TMS', retur
342
342
  >>> df = pd.DataFrame({'TRAN_TMS': ['2025-03-15 10:00:00', '2025-03-20 11:00:00']})
343
343
  >>> convert_to_vintage(df)
344
344
  """
345
- import re
346
- data[vintage_colname] = data[by].apply(lambda x: re.search("\\d{4}-\\d{2}", x).group().replace('-', ''))
345
+ data[vintage_colname] = (
346
+ data[by]
347
+ .astype("string")
348
+ .str.extract(r"(\d{4}-\d{2})", expand=False)
349
+ .str.replace("-", "", regex=False)
350
+ )
347
351
 
348
352
  if return_kDF:
349
353
  return kDataFrame(data)
@@ -1052,10 +1056,9 @@ def bool_to_str(data):
1052
1056
  >>> bool_to_str(df)
1053
1057
  """
1054
1058
  dfc = data.copy()
1055
- type_dict = data.dtypes.to_dict()
1056
- for k,v in type_dict.items():
1057
- if str(v).lower() == 'bool':
1058
- dfc = dfc.astype({k: str})
1059
+ bool_cols = [col for col in data.columns if pd.api.types.is_bool_dtype(data[col])]
1060
+ if bool_cols:
1061
+ dfc[bool_cols] = dfc[bool_cols].astype(str)
1059
1062
  return dfc
1060
1063
 
1061
1064
 
@@ -2670,4 +2673,4 @@ def get_feature_names_batch(models, model_type=None):
2670
2673
  elif isinstance(models, list):
2671
2674
  return [get_feature_names(model, model_type=model_type) for model in models]
2672
2675
  else:
2673
- raise TypeError("models参数应为dict或list类型")
2676
+ raise TypeError("models参数应为dict或list类型")
@@ -88,51 +88,31 @@ def _get_gains_table_scr(data, score, dep, nbins = 10, precision = 5,
88
88
  bin_colnames = ("_bin_num", "_bin_range"),
89
89
  ascending = ascending)
90
90
 
91
- def _compute_gains_tmp(group):
92
- # 计算各项指标
93
- min_val = group[score].min()
94
- max_val = group[score].max()
95
- # NOTE: pandas 2.3+ 起 groupby(...).apply(func) 传入 func 的 group
96
- # DataFrame 不再包含分组列,不能再用 group['_bin_num']。
97
- # len(group) 与原 .count() 数值等价(_bin_num 为分组主键,不为 NaN)。
98
- n = len(group)
99
- avg_score = group[score].mean()
100
- unique_score = group[score].nunique()
101
-
102
- # dep相关
103
- dep_vals = group[dep]
104
-
105
- grand_total = res.shape[0]
106
- grand_perf_cnt = res[dep].count()
107
- grand_total_bad = res[dep].sum()
108
- grand_total_good = (grand_perf_cnt - grand_total_bad)
109
-
110
- perf_cnt = dep_vals.count()
111
- n_bad = (dep_vals == 1).sum()
112
- n_good = (dep_vals == 0).sum()
113
- avg_bad = n_bad / dep_vals.count() if dep_vals.count() > 0 else 0 # 避免除以0
114
- avg_good = n_good / dep_vals.count() if dep_vals.count() > 0 else 0
115
-
116
- lift = avg_bad / res[dep].mean()
117
- prop = n / grand_total
118
-
119
- # 返回Series
120
- return pd.Series({
121
- 'MIN': min_val,
122
- 'MAX': max_val,
123
- 'N': n,
124
- 'PROP': prop,
125
- 'PERF_CNT': perf_cnt,
126
- 'AVG_SCORE': avg_score,
127
- 'UNIQUE_SCORE': unique_score,
128
- 'AVG_BAD': avg_bad,
129
- 'AVG_GOOD': avg_good,
130
- 'N_BAD': n_bad,
131
- 'N_GOOD': n_good,
132
- 'LIFT': lift
133
- })
134
-
135
- gains_table = res.groupby(["_bin_num", "_bin_range"], dropna=False).apply(_compute_gains_tmp)
91
+ res = res.copy()
92
+ res["_smf_bad_ind"] = res[dep].eq(1).astype(np.int64)
93
+ res["_smf_good_ind"] = res[dep].eq(0).astype(np.int64)
94
+ grouped = res.groupby(["_bin_num", "_bin_range"], dropna=False)
95
+ gains_table = grouped.agg(
96
+ MIN=(score, "min"),
97
+ MAX=(score, "max"),
98
+ N=(dep, "size"),
99
+ PERF_CNT=(dep, "count"),
100
+ AVG_SCORE=(score, "mean"),
101
+ UNIQUE_SCORE=(score, "nunique"),
102
+ N_BAD=("_smf_bad_ind", "sum"),
103
+ N_GOOD=("_smf_good_ind", "sum"),
104
+ )
105
+ perf_denom = gains_table["PERF_CNT"].replace(0, np.nan)
106
+ gains_table["PROP"] = gains_table["N"] / max(len(res), 1)
107
+ gains_table["AVG_BAD"] = (gains_table["N_BAD"] / perf_denom).fillna(0.0)
108
+ gains_table["AVG_GOOD"] = (gains_table["N_GOOD"] / perf_denom).fillna(0.0)
109
+ gains_table["LIFT"] = gains_table["AVG_BAD"] / res[dep].mean()
110
+ gains_table = gains_table[
111
+ [
112
+ "MIN", "MAX", "N", "PROP", "PERF_CNT", "AVG_SCORE",
113
+ "UNIQUE_SCORE", "AVG_BAD", "AVG_GOOD", "N_BAD", "N_GOOD", "LIFT",
114
+ ]
115
+ ]
136
116
 
137
117
  gains_table["BAD_PCT_IN_EACH_BIN"] = gains_table["N_BAD"] / gains_table["N_BAD"].sum()
138
118
  gains_table["GOOD_PCT_IN_EACH_BIN"] = gains_table["N_GOOD"] / gains_table["N_GOOD"].sum()
@@ -146,7 +126,7 @@ def _get_gains_table_scr(data, score, dep, nbins = 10, precision = 5,
146
126
 
147
127
 
148
128
  gains_table["TRUE_BAD_SHIFT"] = (gains_table['AVG_BAD'].shift(1) / gains_table['AVG_BAD'] - 1) if not ascending else (gains_table['AVG_BAD'] / gains_table['AVG_BAD'].shift(1) - 1)
149
- gains_table["RANK_ORDER_BUMP"] = gains_table["TRUE_BAD_SHIFT"].apply(lambda x: 1 if x < 0 else 0)
129
+ gains_table["RANK_ORDER_BUMP"] = gains_table["TRUE_BAD_SHIFT"].lt(0).astype(int)
150
130
 
151
131
  gains_table["WOE"] = calc_woe(data = gains_table, bad_pct = "BAD_PCT_IN_EACH_BIN", good_pct = "GOOD_PCT_IN_EACH_BIN")
152
132
  gains_table["IV"] = calc_iv(data = gains_table, bad_pct = "BAD_PCT_IN_EACH_BIN", good_pct = "GOOD_PCT_IN_EACH_BIN")
@@ -648,25 +648,46 @@ def __agg(df):
648
648
  sum_score=pd.NamedAgg(column='y_score', aggfunc='sum'),
649
649
  ).reset_index()
650
650
  df_agg[['n', 'sum_true', 'sum_score']] = df_agg[['n', 'sum_true', 'sum_score']].fillna(0)
651
- df_agg.loc[:, 'proportion'] = [x / N for x in df_agg['n']]
652
- df_agg.loc[:, 'capture_rate'] = [x / N1 for x in df_agg['sum_true']]
651
+ df_agg.loc[:, 'proportion'] = np.divide(
652
+ df_agg['n'].to_numpy(dtype=float),
653
+ float(N),
654
+ out=np.full(len(df_agg), np.nan),
655
+ where=N > 0,
656
+ )
657
+ df_agg.loc[:, 'capture_rate'] = np.divide(
658
+ df_agg['sum_true'].to_numpy(dtype=float),
659
+ float(N1),
660
+ out=np.full(len(df_agg), np.nan),
661
+ where=N1 > 0,
662
+ )
653
663
 
654
664
  if 'y_group' in df_agg.columns:
655
- gs = list(set(df_agg['y_group']))
656
- for g in gs:
657
- g_idx = (df_agg['y_group'] == g)
658
- df_agg.loc[g_idx, 'cumsum_n'] = np.cumsum(df_agg.loc[g_idx, 'n'])
659
- df_agg.loc[g_idx, 'cumsum_proportion'] = np.cumsum(df_agg.loc[g_idx, 'proportion'])
660
- df_agg.loc[g_idx, 'cumsum_true'] = np.cumsum(df_agg.loc[g_idx, 'sum_true'])
661
- df_agg.loc[g_idx, 'cumsum_score'] = np.cumsum(df_agg.loc[g_idx, 'sum_score'])
665
+ cumulative = df_agg.groupby('y_group', sort=False)[
666
+ ['n', 'proportion', 'sum_true', 'sum_score']
667
+ ].cumsum()
668
+ cumulative.columns = [
669
+ 'cumsum_n', 'cumsum_proportion', 'cumsum_true', 'cumsum_score'
670
+ ]
671
+ df_agg.loc[:, cumulative.columns] = cumulative.to_numpy()
662
672
  else:
663
673
  df_agg.loc[:, 'cumsum_n'] = np.cumsum(df_agg['n'])
664
674
  df_agg.loc[:, 'cumsum_proportion'] = np.cumsum(df_agg['proportion'])
665
675
  df_agg.loc[:, 'cumsum_true'] = np.cumsum(df_agg['sum_true'])
666
676
  df_agg.loc[:, 'cumsum_score'] = np.cumsum(df_agg['sum_score'])
667
677
 
668
- df_agg.loc[:, 'cumavg_true'] = [x / y if y>0 else np.nan for x,y in zip(df_agg['cumsum_true'], df_agg['cumsum_n'])]
669
- df_agg.loc[:, 'cumavg_score'] = [x / y if y>0 else np.nan for x,y in zip(df_agg['cumsum_score'], df_agg['cumsum_n'])]
678
+ cumulative_n = df_agg['cumsum_n'].to_numpy(dtype=float)
679
+ df_agg.loc[:, 'cumavg_true'] = np.divide(
680
+ df_agg['cumsum_true'].to_numpy(dtype=float),
681
+ cumulative_n,
682
+ out=np.full(len(df_agg), np.nan),
683
+ where=cumulative_n > 0,
684
+ )
685
+ df_agg.loc[:, 'cumavg_score'] = np.divide(
686
+ df_agg['cumsum_score'].to_numpy(dtype=float),
687
+ cumulative_n,
688
+ out=np.full(len(df_agg), np.nan),
689
+ where=cumulative_n > 0,
690
+ )
670
691
 
671
692
  columns = group_cols + ['min_score', 'max_score', 'n', 'proportion', 'sum_true', 'sum_score', 'avg_true', 'avg_score', 'capture_rate',
672
693
  'cumsum_n', 'cumsum_proportion', 'cumsum_true', 'cumsum_score', 'cumavg_true', 'cumavg_score', ]
@@ -794,20 +815,28 @@ def calc_equid_pct(y_true, y_score, y_group=None, bins=10, ascending=True, sampl
794
815
  binsize = int(size / bins) # 向下取整
795
816
  indices = np.argsort(y_score) if ascending else np.argsort(y_score)[::-1]
796
817
 
797
- thresholds = np.array([0]*size)
798
- thresholds_percentile = [100 * (i+1) / bins for i in range(bins)]
799
- for i in range(bins):
800
- s = i*binsize
801
- e = (i+1)*binsize if i < bins-1 else np.max([size, (i+1)*binsize]) # 严格排序
802
- thresholds[indices[s:e]] = thresholds_percentile[i]
803
- if bool(y_group):
818
+ # Keep the historical integer percentile labels while assigning them in
819
+ # one pass. For bin counts that do not divide 100, the legacy code
820
+ # truncated labels during assignment to its integer array.
821
+ thresholds = np.zeros(size, dtype=int)
822
+ if binsize > 0:
823
+ sorted_bin = np.minimum(np.arange(size) // binsize, bins - 1)
824
+ else:
825
+ sorted_bin = np.full(size, bins - 1, dtype=int)
826
+ thresholds[indices] = 100.0 * (sorted_bin + 1) / bins
827
+ if y_group is not None:
804
828
  df = pd.DataFrame({'y_true': y_true, 'y_score': y_score, 'y_group': y_group, 'thresholds': thresholds})
805
829
  else:
806
830
  df = pd.DataFrame({'y_true': y_true, 'y_score': y_score, 'thresholds': thresholds})
807
831
  pct_df = __agg(df)
808
832
 
809
833
  avg_true = np.mean(y_true)
810
- pct_df['lift'] = [x / avg_true for x in pct_df['cumavg_true']]
834
+ pct_df['lift'] = np.divide(
835
+ pct_df['cumavg_true'].to_numpy(dtype=float),
836
+ float(avg_true),
837
+ out=np.full(len(pct_df), np.nan),
838
+ where=avg_true != 0,
839
+ )
811
840
  pct_df['gain'] = np.cumsum(pct_df['capture_rate'])
812
841
 
813
842
  return pct_df
@@ -124,6 +124,7 @@ def get_gains_table(data, dep, score, nbins=10, weight_col=None, weighted_binnin
124
124
  df["_w"] = weight
125
125
  df["_bad_w"] = weight * y
126
126
  df["_good_w"] = weight * (1.0 - y)
127
+ df["_score_w"] = weight * s
127
128
  grouped = df.groupby(["_bin_num", "_bin_range"], sort=True, dropna=False)
128
129
  out = grouped.agg(
129
130
  MIN=(score, "min"),
@@ -133,9 +134,19 @@ def get_gains_table(data, dep, score, nbins=10, weight_col=None, weighted_binnin
133
134
  PERF_CNT=("_w", "sum"),
134
135
  N_BAD=("_bad_w", "sum"),
135
136
  N_GOOD=("_good_w", "sum"),
136
- AVG_SCORE=(score, lambda x: safe_weighted_average(x, df.loc[x.index, "_w"])),
137
+ SCORE_W=("_score_w", "sum"),
138
+ SCORE_COUNT=(score, "count"),
137
139
  UNIQUE_SCORE=(score, "nunique"),
138
140
  )
141
+ out["AVG_SCORE"] = out["SCORE_W"] / out["N"].replace(0, np.nan)
142
+ out.loc[out["SCORE_COUNT"].ne(out["N_RAW"]), "AVG_SCORE"] = np.nan
143
+ out = out.drop(columns=["SCORE_W", "SCORE_COUNT"])
144
+ out = out[
145
+ [
146
+ "MIN", "MAX", "N", "N_RAW", "PERF_CNT", "N_BAD",
147
+ "N_GOOD", "AVG_SCORE", "UNIQUE_SCORE",
148
+ ]
149
+ ]
139
150
 
140
151
  total_weight = float(out["N"].sum()) or 1.0
141
152
  total_bad = float(out["N_BAD"].sum()) or 1.0
@@ -155,7 +166,7 @@ def get_gains_table(data, dep, score, nbins=10, weight_col=None, weighted_binnin
155
166
  out["KS"] = out["KS_PER_BIN"]
156
167
  out["LIFT"] = out["AVG_BAD"] / overall_bad_rate if overall_bad_rate else np.nan
157
168
  out["TRUE_BAD_SHIFT"] = out["AVG_BAD"].shift(1) / out["AVG_BAD"] - 1
158
- out["RANK_ORDER_BUMP"] = out["TRUE_BAD_SHIFT"].apply(lambda x: 1 if x < 0 else 0)
169
+ out["RANK_ORDER_BUMP"] = out["TRUE_BAD_SHIFT"].lt(0).astype(int)
159
170
  with np.errstate(divide="ignore", invalid="ignore"):
160
171
  out["WOE"] = np.log(out["BAD_PCT_IN_EACH_BIN"] / out["GOOD_PCT_IN_EACH_BIN"])
161
172
  out["WOE"] = out["WOE"].replace([np.inf, -np.inf], 0).fillna(0)
@@ -8,6 +8,7 @@ accepted by ``shap.maskers.Partition``.
8
8
  """
9
9
  from __future__ import annotations
10
10
 
11
+ from collections import Counter
11
12
  from typing import Dict, Iterable, List, Mapping, Optional, Sequence
12
13
 
13
14
  import numpy as np
@@ -248,7 +249,7 @@ def validate_groups(groups: Mapping[str, Sequence[str]], features: Sequence[str]
248
249
  feature_set = set(features)
249
250
  missing = feature_set - set(all_assigned)
250
251
  unknown = set(all_assigned) - feature_set
251
- duplicated = {feat for feat in all_assigned if all_assigned.count(feat) > 1}
252
+ duplicated = {feat for feat, count in Counter(all_assigned).items() if count > 1}
252
253
  ok = not missing and not duplicated and not unknown
253
254
  if not ok and raise_error:
254
255
  parts = []
@@ -316,10 +317,9 @@ def groups_to_shap_clustering(
316
317
 
317
318
  for feats in groups.values():
318
319
  idxs = [feat_idx[feat] for feat in feats if feat in feat_idx]
319
- for i in idxs:
320
- for j in idxs:
321
- if i != j:
322
- dist_mat[i, j] = float(intra_dist)
320
+ if idxs:
321
+ dist_mat[np.ix_(idxs, idxs)] = float(intra_dist)
322
+ dist_mat[idxs, idxs] = 0.0
323
323
 
324
324
  condensed = squareform(dist_mat, checks=False)
325
325
  return linkage(condensed, method="complete")