SuperModelingFactory 0.5.5__tar.gz → 0.5.6__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.5 → supermodelingfactory-0.5.6}/Modeling_Tool/Core/ODPS_Tool.py +64 -22
  2. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/Core/Proc_Compare.py +64 -17
  3. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/Feature/Weighted_Screen.py +5 -3
  4. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/Pipeline/_common.py +57 -0
  5. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/Pipeline/credit_model.py +15 -0
  6. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/Pipeline/feature_validation.py +5 -7
  7. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/Pipeline/field_meta.py +28 -1
  8. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/Pipeline/reject_inference.py +200 -49
  9. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/Pipeline/sample_analysis.py +17 -2
  10. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/Pipeline/score_comparison.py +101 -13
  11. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/__init__.py +1 -1
  12. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/PKG-INFO +2 -2
  13. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/README.md +1 -1
  14. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/SuperModelingFactory.egg-info/PKG-INFO +2 -2
  15. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/pyproject.toml +1 -1
  16. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/setup.py +1 -1
  17. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/ExcelMaster/ExcelFormatTool.py +0 -0
  18. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/ExcelMaster/ExcelMaster.py +0 -0
  19. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/ExcelMaster/Template.py +0 -0
  20. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/ExcelMaster/Utility.py +0 -0
  21. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/ExcelMaster/__init__.py +0 -0
  22. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/LICENSE +0 -0
  23. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/MANIFEST.in +0 -0
  24. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/Core/Binning_Tool.py +0 -0
  25. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/Core/Binning_Tool.pyi +0 -0
  26. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/Core/Check_DuckDB_Compatibility.py +0 -0
  27. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/Core/Json_Data_Converter.py +0 -0
  28. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/Core/Model_Registry_Tool.py +0 -0
  29. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/Core/Parallel_Engine.py +0 -0
  30. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/Core/Parallel_ODPS_Manager.py +0 -0
  31. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/Core/Slope_Tool.py +0 -0
  32. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/Core/Slope_Tool.pyi +0 -0
  33. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/Core/XOR_Encryptor.py +0 -0
  34. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/Core/XOR_Encryptor.pyi +0 -0
  35. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/Core/__init__.py +0 -0
  36. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/Core/kDataFrame.py +0 -0
  37. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/Core/kDataFrame.pyi +0 -0
  38. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/Core/sample_weight_utils.py +0 -0
  39. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/Core/utils.py +0 -0
  40. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/Eval/Evaluation_Tool.py +0 -0
  41. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/Eval/Evaluation_Tool.pyi +0 -0
  42. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/Eval/Model_Eval_Tool.py +0 -0
  43. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/Eval/Model_Eval_Tool.pyi +0 -0
  44. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/Eval/__init__.py +0 -0
  45. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/Eval/evaluate_model.py +0 -0
  46. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/Eval/evaluate_model.pyi +0 -0
  47. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/Eval/weighted_eval_utils.py +0 -0
  48. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/Explainability/Coalition_Structure.py +0 -0
  49. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/Explainability/Model_Explainer.py +0 -0
  50. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/Explainability/__init__.py +0 -0
  51. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/Feature/Distribution_Tool.py +0 -0
  52. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/Feature/Distribution_Tool.pyi +0 -0
  53. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/Feature/Feature_Insights.py +0 -0
  54. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/Feature/Feature_Insights.pyi +0 -0
  55. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/Feature/Feature_Screen.py +0 -0
  56. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/Feature/PSI_Tool.py +0 -0
  57. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/Feature/PSI_Tool.pyi +0 -0
  58. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/Feature/WOE_Engine_Feature_Patch.py +0 -0
  59. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/Feature/__init__.py +0 -0
  60. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/Model/Backward_Tool.py +0 -0
  61. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/Model/Backward_Tool.pyi +0 -0
  62. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/Model/GBM_Search_Tool.py +0 -0
  63. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/Model/GBM_Tool.py +0 -0
  64. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/Model/GBM_Tool.pyi +0 -0
  65. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/Model/LRM_Tool.py +0 -0
  66. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/Model/LRM_Tool.pyi +0 -0
  67. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/Model/__init__.py +0 -0
  68. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/Pipeline/__init__.py +0 -0
  69. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/Pipeline/mock_sample.py +0 -0
  70. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/Pipeline/orchestrator.py +0 -0
  71. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/Pipeline/score_consistency_uat.py +0 -0
  72. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/Pipeline/screening_artifact.py +0 -0
  73. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/Sample/Distribution_Adaptation.py +0 -0
  74. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/Sample/Distribution_Adaptation.pyi +0 -0
  75. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/Sample/Reject_Infer.py +0 -0
  76. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/Sample/Reject_Infer.pyi +0 -0
  77. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/Sample/Sample_Split.py +0 -0
  78. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/Sample/Sample_Split.pyi +0 -0
  79. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/Sample/__init__.py +0 -0
  80. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/UAT/UAT_Consistency_Checker.py +0 -0
  81. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/UAT/__init__.py +0 -0
  82. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/WOE/WOE_Adapter.py +0 -0
  83. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/WOE/WOE_Adapter.pyi +0 -0
  84. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/WOE/WOE_Master.py +0 -0
  85. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/WOE/WOE_Master.pyi +0 -0
  86. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/WOE/WOE_Monotone_Binner.py +0 -0
  87. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/WOE/WOE_Monotone_Binner.pyi +0 -0
  88. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/WOE/WOE_Plot_Tool.py +0 -0
  89. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/WOE/WOE_Plot_Tool.pyi +0 -0
  90. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/WOE/WOE_Report_Builder.py +0 -0
  91. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/WOE/WOE_Report_Builder.pyi +0 -0
  92. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/WOE/WOE_Tool.py +0 -0
  93. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/WOE/WOE_Tool.pyi +0 -0
  94. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/WOE/__init__.py +0 -0
  95. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/WOE/plot_woe_tool.py +0 -0
  96. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/WOE/plot_woe_tool.pyi +0 -0
  97. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/_utils/__init__.py +0 -0
  98. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/_utils/nan_guard.py +0 -0
  99. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/_utils/robust.py +0 -0
  100. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/_utils/sentinels.py +0 -0
  101. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/ref_font/KaiTi.ttf +0 -0
  102. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/ref_font/WeiRuanYaHei.ttf +0 -0
  103. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/ref_font/__init__.py +0 -0
  104. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Modeling_Tool/ref_font/simsun.ttc +0 -0
  105. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Report/Report_Tool.py +0 -0
  106. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/Report/__init__.py +0 -0
  107. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/SuperModelingFactory.egg-info/SOURCES.txt +0 -0
  108. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/SuperModelingFactory.egg-info/dependency_links.txt +0 -0
  109. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/SuperModelingFactory.egg-info/not-zip-safe +0 -0
  110. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/SuperModelingFactory.egg-info/requires.txt +0 -0
  111. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/SuperModelingFactory.egg-info/top_level.txt +0 -0
  112. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/requirements.txt +0 -0
  113. {supermodelingfactory-0.5.5 → supermodelingfactory-0.5.6}/setup.cfg +0 -0
@@ -281,17 +281,31 @@ class ODPSRunner(object):
281
281
 
282
282
  @staticmethod
283
283
  def cre_table_schema(df, partition_name=None):
284
-
285
- dtypes = df.dtypes
286
- isnum_dtypes = [pd.api.types.is_numeric_dtype(x) for x in dtypes]
287
- isint_dtypes = [pd.api.types.is_integer_dtype(x) for x in dtypes]
288
-
289
284
  table_columns = []
290
285
  table_partitions = []
291
- for i in range(len(df.columns)):
292
- col = df.columns[i]
293
- col_type = "string" if not isnum_dtypes[i] else "float"
294
- col_type = "bigint" if isint_dtypes[i] else col_type
286
+ for col, dtype in df.dtypes.items():
287
+ if pd.api.types.is_complex_dtype(dtype) or pd.api.types.is_timedelta64_dtype(dtype):
288
+ raise TypeError(
289
+ f"Unsupported dtype for ODPS schema inference: column={col!r}, dtype={dtype!s}."
290
+ )
291
+ if pd.api.types.is_bool_dtype(dtype):
292
+ col_type = "boolean"
293
+ elif pd.api.types.is_integer_dtype(dtype):
294
+ col_type = "bigint"
295
+ elif pd.api.types.is_float_dtype(dtype):
296
+ col_type = "float" if str(dtype).lower() == "float32" else "double"
297
+ elif pd.api.types.is_datetime64_any_dtype(dtype):
298
+ col_type = "datetime"
299
+ elif (
300
+ pd.api.types.is_string_dtype(dtype)
301
+ or pd.api.types.is_object_dtype(dtype)
302
+ or isinstance(dtype, pd.CategoricalDtype)
303
+ ):
304
+ col_type = "string"
305
+ else:
306
+ raise TypeError(
307
+ f"Unsupported dtype for ODPS schema inference: column={col!r}, dtype={dtype!s}."
308
+ )
295
309
  if col == partition_name:
296
310
  table_partitions.append(Partition(name=col, type=col_type))
297
311
  else:
@@ -304,6 +318,28 @@ class ODPSRunner(object):
304
318
 
305
319
  return table_schema
306
320
 
321
+ @staticmethod
322
+ def _to_odps_records(df):
323
+ """Return Python records with pandas missing values converted to ODPS NULL."""
324
+ object_df = df.astype(object)
325
+ object_df = object_df.where(pd.notna(object_df), None)
326
+ return object_df.values.tolist()
327
+
328
+ def _execute_sql_blocking(self, sql):
329
+ """Execute DDL and wait until the remote ODPS instance has completed."""
330
+ execute_sql = getattr(self.o, "execute_sql", None)
331
+ if callable(execute_sql):
332
+ return execute_sql(sql)
333
+
334
+ run_sql = getattr(self.o, "run_sql", None)
335
+ if not callable(run_sql):
336
+ raise AttributeError("ODPS client must provide execute_sql() or run_sql().")
337
+ instance = run_sql(sql)
338
+ wait_for_success = getattr(instance, "wait_for_success", None)
339
+ if callable(wait_for_success):
340
+ wait_for_success()
341
+ return instance
342
+
307
343
  def upload_df(self, df, table_name, table_schema=None, partition=None, atomic=True):
308
344
  """上传数据集至mc中创建新表
309
345
 
@@ -328,9 +364,11 @@ class ODPSRunner(object):
328
364
  legacy behaviour, e.g. for tables whose downstream readers expect
329
365
  a specific object identity.
330
366
  """
367
+ upload_data = df.copy()
331
368
  if table_schema is None:
332
- df.loc[:, "py_inserttime"] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
333
- table_schema = self.cre_table_schema(df=df, partition_name=None)
369
+ upload_data.loc[:, "py_inserttime"] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
370
+ table_schema = self.cre_table_schema(df=upload_data, partition_name=None)
371
+ records = self._to_odps_records(upload_data)
334
372
 
335
373
  if not atomic:
336
374
  # Legacy pre-0.4.2 non-atomic path — retained under an explicit
@@ -339,10 +377,10 @@ class ODPSRunner(object):
339
377
  t = self.o.create_table(table_name, table_schema)
340
378
  if bool(partition):
341
379
  with t.open_writer(partition=partition, create_partition=True) as writer:
342
- writer.write(df.values.tolist())
380
+ writer.write(records)
343
381
  else:
344
382
  with t.open_writer() as writer:
345
- writer.write(df.values.tolist())
383
+ writer.write(records)
346
384
  logger.info(f'<<<< 完成数据入表{table_name}: shape={df.shape} >>>>')
347
385
  return
348
386
 
@@ -360,10 +398,10 @@ class ODPSRunner(object):
360
398
  try:
361
399
  if bool(partition):
362
400
  with tmp_table.open_writer(partition=partition, create_partition=True) as writer:
363
- writer.write(df.values.tolist())
401
+ writer.write(records)
364
402
  else:
365
403
  with tmp_table.open_writer() as writer:
366
- writer.write(df.values.tolist())
404
+ writer.write(records)
367
405
  except Exception:
368
406
  try:
369
407
  self.o.delete_table(tmp_name, if_exists=True)
@@ -379,7 +417,7 @@ class ODPSRunner(object):
379
417
  target_exists = self.o.exist_table(table_name)
380
418
  if target_exists:
381
419
  try:
382
- self.o.run_sql(f"ALTER TABLE {table_name} RENAME TO {old_rename_name};")
420
+ self._execute_sql_blocking(f"ALTER TABLE {table_name} RENAME TO {old_rename_name};")
383
421
  except Exception:
384
422
  # Rename of the live target failed — do not touch it. Drop the
385
423
  # tmp table so we don't leak it, then re-raise.
@@ -392,13 +430,13 @@ class ODPSRunner(object):
392
430
  raise
393
431
 
394
432
  try:
395
- self.o.run_sql(f"ALTER TABLE {tmp_name} RENAME TO {target_rename_name};")
433
+ self._execute_sql_blocking(f"ALTER TABLE {tmp_name} RENAME TO {target_rename_name};")
396
434
  except Exception:
397
435
  # tmp -> target rename failed. If we already moved the original,
398
436
  # try to restore it so the caller is not left with a missing table.
399
437
  if target_exists:
400
438
  try:
401
- self.o.run_sql(f"ALTER TABLE {old_name} RENAME TO {target_rename_name};")
439
+ self._execute_sql_blocking(f"ALTER TABLE {old_name} RENAME TO {target_rename_name};")
402
440
  logger.warning(
403
441
  f"upload_df: swap of {tmp_name} -> {table_name} failed; restored original from {old_name}"
404
442
  )
@@ -439,7 +477,9 @@ class ODPSRunner(object):
439
477
  def _rename_partition(self, table_name, source_partition, target_partition):
440
478
  src = _format_odps_partition_spec(_parse_odps_partition_spec(source_partition), quoted=True)
441
479
  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});")
480
+ self._execute_sql_blocking(
481
+ f"ALTER TABLE {table_name} PARTITION ({src}) RENAME TO PARTITION ({dst});"
482
+ )
443
483
 
444
484
  def insert_df(self, df, table_name, overwrite=True, partition=None, atomic=True):
445
485
  """将数据集插入至mc已存在的表中.
@@ -457,7 +497,9 @@ class ODPSRunner(object):
457
497
  """
458
498
  t = self.o.get_table(table_name)
459
499
  if "py_inserttime" in t.schema and "py_inserttime" not in df.columns:
500
+ df = df.copy()
460
501
  df.loc[:, "py_inserttime"] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
502
+ records = self._to_odps_records(df)
461
503
 
462
504
  if bool(partition):
463
505
  if overwrite and atomic:
@@ -476,7 +518,7 @@ class ODPSRunner(object):
476
518
 
477
519
  try:
478
520
  with t.open_writer(partition=staging_partition, create_partition=True) as writer:
479
- writer.write(df.values.tolist())
521
+ writer.write(records)
480
522
  if target_exists:
481
523
  self._rename_partition(table_name, partition, backup_partition)
482
524
  self._rename_partition(table_name, staging_partition, partition)
@@ -510,10 +552,10 @@ class ODPSRunner(object):
510
552
  if overwrite:
511
553
  t.delete_partition(partition, if_exists=True)
512
554
  with t.open_writer(partition=partition, create_partition=True) as writer:
513
- writer.write(df.values.tolist())
555
+ writer.write(records)
514
556
  else:
515
557
  if overwrite:
516
558
  t.truncate()
517
559
  with t.open_writer() as writer:
518
- writer.write(df.values.tolist())
560
+ writer.write(records)
519
561
  logger.info('<<<< 完成数据入表: shape={0} >>>>'.format(df.shape))
@@ -38,6 +38,7 @@ class ProcCompareConfig:
38
38
  numeric_tol: float = 1e-8
39
39
  numeric_rtol: float = 0.0
40
40
  datetime_tol_seconds: float = 0.0
41
+ datetime_cols: list[str] = field(default_factory=list)
41
42
  per_column_tolerance: dict[str, Any] = field(default_factory=dict)
42
43
 
43
44
  both_null_equal: bool = True
@@ -185,15 +186,27 @@ class ProcCompareEngine:
185
186
  cols = sorted((set(left_df.columns) | set(right_df.columns)) - {_ROW_ID_COL})
186
187
  rows = []
187
188
  ignore = set(self.config.ignore_cols)
188
- compare_set = set(self._resolve_compare_cols(left_df, right_df, key_cols, allow_missing=True))
189
+ requested_compare_set = set(
190
+ self._resolve_compare_cols(left_df, right_df, key_cols, allow_missing=True)
191
+ )
189
192
  for col in cols:
190
193
  in_left = col in left_df.columns
191
194
  in_right = col in right_df.columns
195
+ requested_for_compare = col in requested_compare_set
196
+ eligible_for_compare = bool(
197
+ requested_for_compare
198
+ and in_left
199
+ and in_right
200
+ and col not in key_cols
201
+ and col not in ignore
202
+ )
192
203
  if col in key_cols:
193
204
  role = "key"
205
+ elif not (in_left and in_right):
206
+ role = "schema_only"
194
207
  elif col in ignore:
195
208
  role = "ignored"
196
- elif col in compare_set:
209
+ elif eligible_for_compare:
197
210
  role = "compare"
198
211
  else:
199
212
  role = "not_compared"
@@ -207,6 +220,8 @@ class ProcCompareEngine:
207
220
  {
208
221
  "column": col,
209
222
  "role": role,
223
+ "requested_for_compare": bool(requested_for_compare),
224
+ "eligible_for_compare": bool(eligible_for_compare),
210
225
  "in_left": bool(in_left),
211
226
  "in_right": bool(in_right),
212
227
  "dtype_left": str(left_df[col].dtype) if in_left else "",
@@ -239,6 +254,7 @@ class ProcCompareEngine:
239
254
  right_df: pd.DataFrame,
240
255
  key_cols: list[str],
241
256
  schema_summary: pd.DataFrame | None = None,
257
+ keep_internal_stats: bool = False,
242
258
  ) -> ProcCompareResult:
243
259
  left_df, left_dups = self._handle_duplicates(left_df, key_cols, self.config.left_name)
244
260
  right_df, right_dups = self._handle_duplicates(right_df, key_cols, self.config.right_name)
@@ -349,7 +365,11 @@ class ProcCompareEngine:
349
365
  return ProcCompareResult(
350
366
  coverage_summary=coverage_summary,
351
367
  schema_summary=schema_summary,
352
- column_summary=self._strip_internal_columns(column_summary),
368
+ column_summary=(
369
+ column_summary
370
+ if keep_internal_stats
371
+ else self._strip_internal_columns(column_summary)
372
+ ),
353
373
  row_summary=row_summary,
354
374
  cell_mismatches=cell_mismatches,
355
375
  duplicate_key_summary=duplicate_key_summary,
@@ -370,7 +390,7 @@ class ProcCompareEngine:
370
390
  diff = pd.Series(np.nan, index=left.index, dtype="float64")
371
391
  abs_diff = pd.Series(np.nan, index=left.index, dtype="float64")
372
392
 
373
- if self._should_compare_datetime(left, right):
393
+ if self._should_compare_datetime(col, left, right):
374
394
  left_dt = pd.to_datetime(left, errors="coerce")
375
395
  right_dt = pd.to_datetime(right, errors="coerce")
376
396
  diff = (left_dt - right_dt).dt.total_seconds()
@@ -421,9 +441,28 @@ class ProcCompareEngine:
421
441
  return False
422
442
  return bool(left_num.notna().all() and right_num.notna().all())
423
443
 
424
- @staticmethod
425
- def _should_compare_datetime(left: pd.Series, right: pd.Series) -> bool:
426
- return bool(pd.api.types.is_datetime64_any_dtype(left) or pd.api.types.is_datetime64_any_dtype(right))
444
+ def _should_compare_datetime(self, col: str, left: pd.Series, right: pd.Series) -> bool:
445
+ if pd.api.types.is_datetime64_any_dtype(left) or pd.api.types.is_datetime64_any_dtype(right):
446
+ return True
447
+ if col in self.config.datetime_cols:
448
+ return True
449
+
450
+ override = self.config.per_column_tolerance.get(col)
451
+ if isinstance(override, dict) and "datetime_tol_seconds" in override:
452
+ return True
453
+ if self.config.datetime_tol_seconds <= 0:
454
+ return False
455
+ if self._should_compare_numeric(left, right):
456
+ return False
457
+
458
+ values = pd.concat([left.dropna(), right.dropna()], ignore_index=True)
459
+ if values.empty:
460
+ return False
461
+ text = values.astype("string").str.strip()
462
+ if not text.str.match(r"^\d{4}[-/]\d{1,2}[-/]\d{1,2}(?:[ T].*)?$", na=False).all():
463
+ return False
464
+ parsed = pd.to_datetime(values, errors="coerce")
465
+ return bool(parsed.notna().all())
427
466
 
428
467
  def _column_numeric_tol(self, col: str) -> tuple[float, float]:
429
468
  override = self.config.per_column_tolerance.get(col)
@@ -547,7 +586,13 @@ class ProcCompareEngine:
547
586
  right_part_df = pd.read_csv(right_part) if right_part.exists() else pd.DataFrame(columns=right_columns)
548
587
  left_part_df = self._normalize_missing(left_part_df)
549
588
  right_part_df = self._normalize_missing(right_part_df)
550
- return self._compare_frames(left_part_df, right_part_df, key_cols, schema_summary=schema_summary)
589
+ return self._compare_frames(
590
+ left_part_df,
591
+ right_part_df,
592
+ key_cols,
593
+ schema_summary=schema_summary,
594
+ keep_internal_stats=True,
595
+ )
551
596
 
552
597
  def _partition_input(self, data: pd.DataFrame | str | Path, out_dir: Path, key_cols: list[str]) -> None:
553
598
  if isinstance(data, pd.DataFrame):
@@ -589,21 +634,19 @@ class ProcCompareEngine:
589
634
  ]
590
635
  )
591
636
 
592
- cols = pd.concat([p.column_summary.assign(_partial=idx) for idx, p in enumerate(partials)], ignore_index=True)
637
+ cols = pd.concat(
638
+ [p.column_summary.assign(_partial=idx) for idx, p in enumerate(partials)],
639
+ ignore_index=True,
640
+ )
593
641
  if len(cols):
594
- cols["_diff_sum"] = pd.concat(
595
- [
596
- p.column_summary.assign(_partial=idx).get("_diff_sum", pd.Series(dtype=float))
597
- for idx, p in enumerate(partials)
598
- ],
599
- ignore_index=True,
600
- ) if "_diff_sum" in cols.columns else 0.0
601
642
  grouped = cols.groupby("column", dropna=False).agg(
602
643
  n_compared=("n_compared", "sum"),
603
644
  n_equal=("n_equal", "sum"),
604
645
  n_mismatch=("n_mismatch", "sum"),
605
646
  n_one_side_null=("n_one_side_null", "sum"),
606
647
  n_both_null=("n_both_null", "sum"),
648
+ _diff_sum=("_diff_sum", "sum"),
649
+ _diff_count=("_diff_count", "sum"),
607
650
  max_abs_diff=("max_abs_diff", "max"),
608
651
  ).reset_index()
609
652
  grouped["pct_mismatch"] = np.where(
@@ -611,7 +654,11 @@ class ProcCompareEngine:
611
654
  grouped["n_mismatch"] / grouped["n_compared"] * 100,
612
655
  0.0,
613
656
  )
614
- grouped["mean_diff"] = np.nan
657
+ grouped["mean_diff"] = np.where(
658
+ grouped["_diff_count"] > 0,
659
+ grouped["_diff_sum"] / grouped["_diff_count"],
660
+ np.nan,
661
+ )
615
662
  column_summary = grouped[
616
663
  [
617
664
  "column",
@@ -402,12 +402,14 @@ def _missing_rate_for_series(
402
402
  return 1.0
403
403
  valid = int(values.notna().sum())
404
404
  return 1.0 - float(valid) / float(total)
405
- arr = values.to_numpy(dtype=float, copy=False)
406
- finite = np.isfinite(arr) & values.notna().to_numpy()
405
+ present = values.notna().to_numpy()
406
+ if pd.api.types.is_numeric_dtype(values):
407
+ arr = values.to_numpy(dtype=float, copy=False)
408
+ present &= np.isfinite(arr)
407
409
  total_w = float(np.sum(w))
408
410
  if total_w <= 0:
409
411
  return 1.0
410
- return 1.0 - float(np.sum(w[finite])) / total_w
412
+ return 1.0 - float(np.sum(w[present])) / total_w
411
413
 
412
414
 
413
415
  def _apply_missing_rate_stage(
@@ -55,6 +55,63 @@ def as_list(value: Any | Iterable[Any] | None, default: list[Any] | None = None)
55
55
  return list(value)
56
56
 
57
57
 
58
+ def normalize_group_specs(
59
+ value: Any,
60
+ *,
61
+ default_min_size: int | None = None,
62
+ ) -> list[dict[str, Any]]:
63
+ """Normalize supported Pipeline group-spec forms into named dictionaries."""
64
+ if value is None:
65
+ return []
66
+
67
+ if isinstance(value, Mapping):
68
+ raw_specs = []
69
+ for name, spec in value.items():
70
+ if isinstance(spec, Mapping):
71
+ item = dict(spec)
72
+ item.setdefault("name", str(name))
73
+ else:
74
+ item = {"name": str(name), "columns": as_list(spec)}
75
+ raw_specs.append(item)
76
+ else:
77
+ raw_specs = as_list(value)
78
+
79
+ normalized: list[dict[str, Any]] = []
80
+ seen_names: set[str] = set()
81
+ for idx, spec in enumerate(raw_specs):
82
+ if isinstance(spec, Mapping):
83
+ item = dict(spec)
84
+ columns = item.get("columns", item.get("cols"))
85
+ if columns is None:
86
+ raise ValueError(
87
+ f"group_specs entry {idx} must contain 'columns'; got keys {sorted(item)}"
88
+ )
89
+ columns = as_list(columns)
90
+ name = str(item.get("name") or "_x_".join(str(col) for col in columns))
91
+ min_size = item.get("min_size", default_min_size)
92
+ else:
93
+ columns = as_list(spec)
94
+ name = "_x_".join(str(col) for col in columns) or f"group_{idx}"
95
+ min_size = default_min_size
96
+
97
+ if not columns or any(not isinstance(col, str) or not col.strip() for col in columns):
98
+ raise ValueError(
99
+ f"group_specs entry {idx} must define a non-empty list of column names; "
100
+ f"got {columns!r}"
101
+ )
102
+ if name in seen_names:
103
+ raise ValueError(f"group_specs contains duplicate name {name!r}")
104
+ seen_names.add(name)
105
+ normalized.append(
106
+ {
107
+ "name": name,
108
+ "columns": [str(col) for col in columns],
109
+ "min_size": default_min_size if min_size is None else int(min_size),
110
+ }
111
+ )
112
+ return normalized
113
+
114
+
58
115
  def merge_dict(base: Mapping[str, Any] | None, override: Mapping[str, Any] | None) -> dict[str, Any]:
59
116
  merged = dict(base or {})
60
117
  merged.update(dict(override or {}))
@@ -929,6 +929,21 @@ class CreditModelPipeline:
929
929
  base_params = dict(cfg.model_params.get("lr", {}))
930
930
  standardize = bool(base_params.pop("standardize", False))
931
931
  lr = LRMaster(params=base_params or None, standardize=standardize)
932
+ allowed_search_params = {
933
+ "objective",
934
+ "primary_set",
935
+ "gap_ref_sets",
936
+ "metric",
937
+ "refit",
938
+ "verbose",
939
+ }
940
+ unknown_search_params = sorted(set(cfg.lr_search_params) - allowed_search_params)
941
+ if unknown_search_params:
942
+ raise ValueError(
943
+ f"Unsupported lr_search_params keys: {unknown_search_params}. "
944
+ f"Allowed keys are {sorted(allowed_search_params)}. "
945
+ "LRMaster.grid_search_params uses holdout eval_sets and does not accept cv."
946
+ )
932
947
  params = merge_dict(
933
948
  {
934
949
  "objective": "oot_gap_penalized",
@@ -16,6 +16,7 @@ from ._common import (
16
16
  apply_woe_fit_query,
17
17
  as_list,
18
18
  make_dirs,
19
+ normalize_group_specs,
19
20
  safe_to_csv,
20
21
  split_oot_by_flag,
21
22
  validate_woe_fit_query_columns,
@@ -992,13 +993,10 @@ class FeatureValidationPipeline:
992
993
  def _group_specs(self, include_global: bool = True) -> dict[str, list[str]]:
993
994
  cfg = self.config
994
995
  if cfg.group_specs is not None:
995
- if isinstance(cfg.group_specs, dict):
996
- return {str(k): list(v) for k, v in cfg.group_specs.items()}
997
- specs = {}
998
- for idx, spec in enumerate(cfg.group_specs):
999
- cols = [spec] if isinstance(spec, str) else list(spec)
1000
- specs["x".join(cols) or f"group_{idx}"] = cols
1001
- return specs
996
+ return {
997
+ str(spec["name"]): list(spec["columns"])
998
+ for spec in normalize_group_specs(cfg.group_specs)
999
+ }
1002
1000
  specs: dict[str, list[str]] = {}
1003
1001
  if include_global:
1004
1002
  specs["global"] = []
@@ -567,9 +567,10 @@ def _build_pipeline_registry() -> dict[str, PipelineRegistryEntry]:
567
567
  result_attrs=[
568
568
  "label_coverage_summary",
569
569
  "segment_bad_rate_summary",
570
+ "profile_summary",
570
571
  "split_candidate_summary",
571
572
  "split_recommendation",
572
- "report_path",
573
+ "output_paths",
573
574
  ],
574
575
  ),
575
576
  "mock_sample": PipelineRegistryEntry(
@@ -1029,6 +1030,17 @@ def validate_pipeline_config(pipeline_key: str, values: dict[str, Any] | Any) ->
1029
1030
  errors.append("optuna_n_trials 必须 >= 1。")
1030
1031
  if int(vals.get("optuna_n_trials", 5) or 0) < 5:
1031
1032
  warnings.append("optuna_n_trials 建议至少为 5,过小的搜索轮数不稳定。")
1033
+ allowed_lr_search_params = {
1034
+ "objective", "primary_set", "gap_ref_sets", "metric", "refit", "verbose"
1035
+ }
1036
+ unknown_lr_search_params = sorted(
1037
+ set(vals.get("lr_search_params") or {}) - allowed_lr_search_params
1038
+ )
1039
+ if unknown_lr_search_params:
1040
+ errors.append(
1041
+ f"Unsupported lr_search_params keys: {unknown_lr_search_params}; "
1042
+ f"allowed keys are {sorted(allowed_lr_search_params)}."
1043
+ )
1032
1044
  elif entry.key == "feature_validation":
1033
1045
  has_batch_config = bool(vals.get("feature_batches")) or vals.get("feature_batch_size") is not None
1034
1046
  if vals.get("feature_batch_size") is not None and int(vals["feature_batch_size"]) <= 0:
@@ -1057,6 +1069,21 @@ def validate_pipeline_config(pipeline_key: str, values: dict[str, Any] | Any) ->
1057
1069
  errors.append("target_col 不能为空。")
1058
1070
  if missing("score_cols") and missing("base_score"):
1059
1071
  warnings.append("未配置 score_cols/base_score 时将依赖 Pipeline 自动探测分数字段。")
1072
+ if vals.get("group_specs") is not None:
1073
+ try:
1074
+ from ._common import normalize_group_specs
1075
+
1076
+ normalize_group_specs(vals["group_specs"])
1077
+ except (TypeError, ValueError) as exc:
1078
+ errors.append(f"Invalid group_specs: {exc}")
1079
+ for name, spec in (vals.get("cross_metrics") or {}).items():
1080
+ if not isinstance(spec, (list, tuple)) or len(spec) != 2:
1081
+ errors.append(
1082
+ f"cross_metrics[{name!r}] must be a two-item [column, aggregation] pair."
1083
+ )
1084
+ pairwise_agg = vals.get("pairwise_cross_agg_dict")
1085
+ if pairwise_agg is not None and not isinstance(pairwise_agg, dict):
1086
+ errors.append("pairwise_cross_agg_dict must be a {column: aggregation(s)} mapping.")
1060
1087
  elif entry.key == "score_consistency_uat":
1061
1088
  if missing("main_model_score_col"):
1062
1089
  errors.append("main_model_score_col 不能为空。")