SuperModelingFactory 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. ExcelMaster/ExcelFormatTool.py +487 -0
  2. ExcelMaster/ExcelMaster.py +917 -0
  3. ExcelMaster/Template.py +525 -0
  4. ExcelMaster/Utility.py +233 -0
  5. ExcelMaster/__init__.py +3 -0
  6. Modeling_Tool/Core/Binning_Tool.py +1608 -0
  7. Modeling_Tool/Core/Binning_Tool.pyi +48 -0
  8. Modeling_Tool/Core/Check_DuckDB_Compatibility.py +835 -0
  9. Modeling_Tool/Core/Json_Data_Converter.py +621 -0
  10. Modeling_Tool/Core/Model_Registry_Tool.py +199 -0
  11. Modeling_Tool/Core/ODPS_Tool.py +284 -0
  12. Modeling_Tool/Core/Slope_Tool.py +356 -0
  13. Modeling_Tool/Core/Slope_Tool.pyi +31 -0
  14. Modeling_Tool/Core/XOR_Encryptor.py +207 -0
  15. Modeling_Tool/Core/XOR_Encryptor.pyi +26 -0
  16. Modeling_Tool/Core/__init__.py +99 -0
  17. Modeling_Tool/Core/kDataFrame.py +228 -0
  18. Modeling_Tool/Core/kDataFrame.pyi +43 -0
  19. Modeling_Tool/Core/sample_weight_utils.py +77 -0
  20. Modeling_Tool/Core/utils.py +2672 -0
  21. Modeling_Tool/Eval/Evaluation_Tool.py +1452 -0
  22. Modeling_Tool/Eval/Evaluation_Tool.pyi +47 -0
  23. Modeling_Tool/Eval/Model_Eval_Tool.py +2548 -0
  24. Modeling_Tool/Eval/Model_Eval_Tool.pyi +37 -0
  25. Modeling_Tool/Eval/__init__.py +54 -0
  26. Modeling_Tool/Eval/evaluate_model.py +2008 -0
  27. Modeling_Tool/Eval/evaluate_model.pyi +50 -0
  28. Modeling_Tool/Eval/weighted_eval_utils.py +326 -0
  29. Modeling_Tool/Explainability/Coalition_Structure.py +305 -0
  30. Modeling_Tool/Explainability/Model_Explainer.py +743 -0
  31. Modeling_Tool/Explainability/__init__.py +24 -0
  32. Modeling_Tool/Feature/Distribution_Tool.py +509 -0
  33. Modeling_Tool/Feature/Distribution_Tool.pyi +42 -0
  34. Modeling_Tool/Feature/Feature_Insights.py +762 -0
  35. Modeling_Tool/Feature/Feature_Insights.pyi +33 -0
  36. Modeling_Tool/Feature/PSI_Tool.py +1195 -0
  37. Modeling_Tool/Feature/PSI_Tool.pyi +29 -0
  38. Modeling_Tool/Feature/WOE_Engine_Feature_Patch.py +355 -0
  39. Modeling_Tool/Feature/__init__.py +40 -0
  40. Modeling_Tool/Model/Backward_Tool.py +778 -0
  41. Modeling_Tool/Model/Backward_Tool.pyi +45 -0
  42. Modeling_Tool/Model/GBM_Search_Tool.py +251 -0
  43. Modeling_Tool/Model/GBM_Tool.py +1610 -0
  44. Modeling_Tool/Model/GBM_Tool.pyi +90 -0
  45. Modeling_Tool/Model/LRM_Tool.py +1198 -0
  46. Modeling_Tool/Model/LRM_Tool.pyi +47 -0
  47. Modeling_Tool/Model/__init__.py +61 -0
  48. Modeling_Tool/Sample/Distribution_Adaptation.py +131 -0
  49. Modeling_Tool/Sample/Distribution_Adaptation.pyi +30 -0
  50. Modeling_Tool/Sample/Reject_Infer.py +413 -0
  51. Modeling_Tool/Sample/Reject_Infer.pyi +43 -0
  52. Modeling_Tool/Sample/Sample_Split.py +520 -0
  53. Modeling_Tool/Sample/Sample_Split.pyi +43 -0
  54. Modeling_Tool/Sample/__init__.py +31 -0
  55. Modeling_Tool/UAT/UAT_Consistency_Checker.py +1180 -0
  56. Modeling_Tool/UAT/__init__.py +19 -0
  57. Modeling_Tool/WOE/WOE_Adapter.py +204 -0
  58. Modeling_Tool/WOE/WOE_Adapter.pyi +21 -0
  59. Modeling_Tool/WOE/WOE_Master.py +491 -0
  60. Modeling_Tool/WOE/WOE_Master.pyi +40 -0
  61. Modeling_Tool/WOE/WOE_Monotone_Binner.py +3324 -0
  62. Modeling_Tool/WOE/WOE_Monotone_Binner.pyi +71 -0
  63. Modeling_Tool/WOE/WOE_Plot_Tool.py +919 -0
  64. Modeling_Tool/WOE/WOE_Plot_Tool.pyi +46 -0
  65. Modeling_Tool/WOE/WOE_Report_Builder.py +214 -0
  66. Modeling_Tool/WOE/WOE_Report_Builder.pyi +24 -0
  67. Modeling_Tool/WOE/WOE_Tool.py +1094 -0
  68. Modeling_Tool/WOE/WOE_Tool.pyi +41 -0
  69. Modeling_Tool/WOE/__init__.py +86 -0
  70. Modeling_Tool/WOE/plot_woe_tool.py +290 -0
  71. Modeling_Tool/WOE/plot_woe_tool.pyi +25 -0
  72. Modeling_Tool/__init__.py +176 -0
  73. Report/Report_Tool.py +380 -0
  74. Report/__init__.py +3 -0
  75. supermodelingfactory-0.2.0.dist-info/METADATA +268 -0
  76. supermodelingfactory-0.2.0.dist-info/RECORD +79 -0
  77. supermodelingfactory-0.2.0.dist-info/WHEEL +5 -0
  78. supermodelingfactory-0.2.0.dist-info/licenses/LICENSE +102 -0
  79. supermodelingfactory-0.2.0.dist-info/top_level.txt +3 -0
@@ -0,0 +1,621 @@
1
+ # ============================================================================
2
+ # cdc_data_converter.py — CDC 征信数据格式双向转换工具
3
+ # ============================================================================
4
+ # 功能:
5
+ # 1) df_to_json(drv_df, input_vars) — 将 drv_df DataFrame 转换为 expected JSON 格式
6
+ # 2) json_to_df(json_data, input_vars) — 将 JSON 格式还原为 drv_df DataFrame
7
+ #
8
+ # 使用场景:
9
+ # drv_df 是从 ODPS SQL 查询返回的 pandas DataFrame,每行代表一条征信账户记录,
10
+ # 所有行共享同一组元信息(requestid / listingid / pulllogid / inserttime),
11
+ # 但每行对应不同的 input_vars 变量值(account_open_days / pagoactual 等)。
12
+ #
13
+ # JSON 格式将元信息提升为标量字段,input_vars 变成长度相等的数组放在
14
+ # cdc_credit_inputs 下,便于下游 API 消费和序列化传输。
15
+ #
16
+ # 数据格式对照:
17
+ #
18
+ # drv_df (DataFrame):
19
+ # ┌──────────────┬───────────┬──────────┬──────────────┬───────────────────┬────────────┬───────────────┬─────────────────┬──────────────────┐
20
+ # │ requestid │ listingid │ pulllogid│ inserttime │ account_open_days │ pagoactual │ saldoactual_2 │ creditomaximo_2 │ api_call_success │
21
+ # ├──────────────┼───────────┼──────────┼──────────────┼───────────────────┼────────────┼───────────────┼─────────────────┼──────────────────┤
22
+ # │ req_001 │ -1 │ 3836171 │ 1762092089285│ 2381 │ V │ 0.0 │ 6000.0 │ 1 │
23
+ # │ req_001 │ -1 │ 3836171 │ 1762092089285│ 4493 │ V │ 0.0 │ 5002.0 │ 1 │
24
+ # │ req_001 │ -1 │ 3836171 │ 1762092089285│ 1107 │ V │ 0.0 │ 4100.0 │ 1 │
25
+ # └──────────────┴───────────┴──────────┴──────────────┴───────────────────┴────────────┴───────────────┴─────────────────┴──────────────────┘
26
+ #
27
+ # expected JSON:
28
+ # {
29
+ # "requestid": "req_001",
30
+ # "listingid": -1,
31
+ # "pulllogid": 3836171,
32
+ # "inserttime": 1762092089285,
33
+ # "api_call_success": 1,
34
+ # "cdc_credit_inputs": {
35
+ # "account_open_days": [2381, 4493, 1107],
36
+ # "pagoactual": ["V", "V", "V"],
37
+ # "saldoactual_2": [0.0, 0.0, 0.0],
38
+ # "creditomaximo_2": [6000.0, 5002.0, 4100.0]
39
+ # }
40
+ # }
41
+ # ============================================================================
42
+
43
+ import json
44
+ import math
45
+ import os
46
+ from typing import Any, Dict, List, Optional, Union
47
+
48
+ import numpy as np
49
+ import pandas as pd
50
+
51
+
52
+ # ═══════════════════════════════════════════════════════════════════════════
53
+ # 辅助函数
54
+ # ═══════════════════════════════════════════════════════════════════════════
55
+
56
+ def _safe_json_value(val: Any) -> Any:
57
+ """将 numpy / pandas 类型转换为 JSON 友好的 Python 原生类型。
58
+
59
+ 特别处理:
60
+ - numpy NaN / Inf → None (JSON null)
61
+ - Python native float NaN / Inf → None (JSON null)
62
+ - 其他 numpy/pandas 类型 → Python 原生类型
63
+ """
64
+ if val is None:
65
+ return None
66
+ if isinstance(val, (np.integer,)):
67
+ return int(val)
68
+ if isinstance(val, (np.floating,)):
69
+ if np.isnan(val) or np.isinf(val):
70
+ return None
71
+ return float(val)
72
+ if isinstance(val, float):
73
+ # 兜底: Python 原生 float 的 NaN / Inf
74
+ if math.isnan(val) or math.isinf(val):
75
+ return None
76
+ return val
77
+ if isinstance(val, np.bool_):
78
+ return bool(val)
79
+ if isinstance(val, (np.ndarray,)):
80
+ return val.tolist()
81
+ if isinstance(val, pd.Timestamp):
82
+ return str(val)
83
+ return val
84
+
85
+
86
+ def _safe_series_to_list(series: pd.Series) -> List[Any]:
87
+ """将 pandas Series 转换为 Python list,同时处理 numpy 类型转换和 NaN 替换。"""
88
+ # 先用 object 类型兜底,避免 numpy 类型的 JSON 序列化问题
89
+ return [_safe_json_value(v) for v in series.to_list()]
90
+
91
+
92
+ def _sanitize_for_json(obj: Any) -> Any:
93
+ """递归遍历数据结构,将所有 NaN / Inf 值替换为 None (JSON null)。
94
+
95
+ 这是一道兜底防线 —— 确保任何通过 json.dump / json.dumps 写出的数据
96
+ 都是严格合法的 JSON,不会出现 ``NaN`` / ``Infinity`` / ``-Infinity`` 等
97
+ 非标准 token。
98
+
99
+ 同时处理深藏在嵌套 dict / list 中的 numpy 标量类型。
100
+ """
101
+ # ── 标量: float NaN / Inf ──
102
+ if isinstance(obj, float):
103
+ if math.isnan(obj) or math.isinf(obj):
104
+ return None
105
+ return obj
106
+
107
+ # ── numpy 浮点标量 ──
108
+ if isinstance(obj, (np.floating,)):
109
+ val = float(obj)
110
+ if math.isnan(val) or math.isinf(val):
111
+ return None
112
+ return val
113
+
114
+ # ── 其他 numpy 标量 ──
115
+ if isinstance(obj, (np.integer,)):
116
+ return int(obj)
117
+ if isinstance(obj, np.bool_):
118
+ return bool(obj)
119
+
120
+ # ── numpy 数组 → 递归处理 ──
121
+ if isinstance(obj, np.ndarray):
122
+ return _sanitize_for_json(obj.tolist())
123
+
124
+ # ── 容器: 深度优先递归 ──
125
+ if isinstance(obj, dict):
126
+ return {k: _sanitize_for_json(v) for k, v in obj.items()}
127
+ if isinstance(obj, (list, tuple)):
128
+ return [_sanitize_for_json(v) for v in obj]
129
+
130
+ # ── 其他类型原样返回 ──
131
+ return obj
132
+
133
+
134
+ # ═══════════════════════════════════════════════════════════════════════════
135
+ # 核心转换函数
136
+ # ═══════════════════════════════════════════════════════════════════════════
137
+
138
+ def df_to_json(
139
+ drv_df: pd.DataFrame,
140
+ input_vars: Optional[List[str]] = None,
141
+ metadata_cols: Optional[List[str]] = None,
142
+ ) -> Dict[str, Any]:
143
+ """将 drv_df DataFrame 转换为 JSON 格式。
144
+
145
+ 两种模式:
146
+ - 分区模式 (input_vars 指定):
147
+ 元信息列(非 input_vars 列)提取为标量,input_vars 列收集为数组
148
+ 放入 cdc_credit_inputs 下。
149
+ 输出: {"<meta>": <scalar>, ..., "cdc_credit_inputs": {"<var>": [...], ...}}
150
+
151
+ - 平铺模式 (input_vars=None):
152
+ 所有列都作为数组放在一级 JSON 下,不再区分 metadata / input_vars。
153
+ 输出: {"<col_1>": [...], "<col_2>": [...], ...}
154
+
155
+ Parameters
156
+ ----------
157
+ drv_df : pd.DataFrame
158
+ 源 DataFrame,每行一条记录。
159
+ input_vars : Optional[List[str]]
160
+ 入模特征列名列表。为 None 时使用平铺模式,所有列均为数组。
161
+ metadata_cols : Optional[List[str]]
162
+ 分区模式下显式指定元信息列。平铺模式下忽略。
163
+
164
+ Returns
165
+ -------
166
+ Dict[str, Any]
167
+ JSON 格式的字典。
168
+
169
+ Raises
170
+ ------
171
+ ValueError
172
+ 分区模式下,如果 metadata 列值不一致。
173
+ KeyError
174
+ 如果 input_vars 中的列在 DataFrame 中不存在。
175
+ """
176
+ # ── 平铺模式: 所有列直接作为一级字段 ──
177
+ # - 单行 DataFrame → 值直接为标量
178
+ # - 多行 DataFrame → 值为数组
179
+ if input_vars is None:
180
+ result: Dict[str, Any] = {}
181
+ if len(drv_df) == 1:
182
+ first_row = drv_df.iloc[0]
183
+ for col in drv_df.columns:
184
+ result[col] = _safe_json_value(first_row[col])
185
+ else:
186
+ for col in drv_df.columns:
187
+ result[col] = _safe_series_to_list(drv_df[col])
188
+ return result
189
+
190
+ # ── 分区模式 (原有逻辑) ──
191
+ missing_cols = set(input_vars) - set(drv_df.columns)
192
+ if missing_cols:
193
+ raise KeyError(
194
+ f"input_vars 中的列在 DataFrame 中不存在: {missing_cols}"
195
+ )
196
+
197
+ if metadata_cols is None:
198
+ metadata_cols = [c for c in drv_df.columns if c not in input_vars]
199
+
200
+ missing_meta = set(metadata_cols) - set(drv_df.columns)
201
+ if missing_meta:
202
+ raise KeyError(
203
+ f"metadata_cols 中的列在 DataFrame 中不存在: {missing_meta}"
204
+ )
205
+
206
+ # 校验 metadata 列的值在整个 DataFrame 中是否一致
207
+ for col in metadata_cols:
208
+ unique_vals = drv_df[col].drop_duplicates()
209
+ if len(unique_vals) > 1:
210
+ raise ValueError(
211
+ f"元信息列 '{col}' 存在多个不同的值: {unique_vals.to_list()}。"
212
+ f"预期 metadata 列在所有行中保持一致,请检查数据或调整 metadata_cols 参数。"
213
+ )
214
+
215
+ result = {}
216
+
217
+ # 1) 元信息: 取自第一行
218
+ first_row = drv_df.iloc[0]
219
+ for col in metadata_cols:
220
+ result[col] = _safe_json_value(first_row[col])
221
+
222
+ # 2) cdc_credit_inputs: 每个 input_var 的值收集为数组
223
+ cdc_credit_inputs: Dict[str, List[Any]] = {}
224
+ for col in input_vars:
225
+ cdc_credit_inputs[col] = _safe_series_to_list(drv_df[col])
226
+
227
+ result["cdc_credit_inputs"] = cdc_credit_inputs
228
+
229
+ return result
230
+
231
+
232
+ def df_to_json_custom(
233
+ drv_df: pd.DataFrame,
234
+ input_vars: List[str],
235
+ inputs_key: str = "inputs",
236
+ metadata_cols: Optional[List[str]] = None,
237
+ unwrap_single: bool = True,
238
+ ) -> Dict[str, Any]:
239
+ """将 drv_df DataFrame 转换为分区 JSON 格式,支持自定义二级 key 和自动解包。
240
+
241
+ 与 df_to_json 分区模式类似,但:
242
+ - 二级 JSON 的 key 名称可自定义(通过 inputs_key)
243
+ - 二级 JSON 中长度为 1 的数组自动解包为标量(通过 unwrap_single)
244
+
245
+ Parameters
246
+ ----------
247
+ drv_df : pd.DataFrame
248
+ 源 DataFrame。
249
+ input_vars : List[str]
250
+ 放入二级 JSON 的列名列表。
251
+ inputs_key : str
252
+ 二级 JSON 的 key 名称,默认 "inputs"。
253
+ metadata_cols : Optional[List[str]]
254
+ 一级元信息列。为 None 时自动推导(非 input_vars 的列)。
255
+ unwrap_single : bool
256
+ 是否将二级 JSON 中长度为 1 的数组解包为标量。默认 True。
257
+
258
+ Returns
259
+ -------
260
+ Dict[str, Any]
261
+ {
262
+ "<meta>": <scalar>,
263
+ ...,
264
+ "<inputs_key>": {
265
+ "<var>": <scalar_or_array>,
266
+ ...
267
+ }
268
+ }
269
+
270
+ Examples
271
+ --------
272
+ >>> df = pd.DataFrame({'req': ['a','a'], 'x': [1,2], 'y': [3,4]})
273
+ >>> df_to_json_custom(df, input_vars=['x','y'], inputs_key='features')
274
+ {'req': 'a', 'features': {'x': [1,2], 'y': [3,4]}}
275
+
276
+ >>> df_single = pd.DataFrame({'req': ['a'], 'x': [1], 'y': [3]})
277
+ >>> df_to_json_custom(df_single, input_vars=['x','y'], inputs_key='features')
278
+ {'req': 'a', 'features': {'x': 1, 'y': 3}}
279
+ """
280
+ # ── 参数校验 ──
281
+ missing_cols = set(input_vars) - set(drv_df.columns)
282
+ if missing_cols:
283
+ raise KeyError(
284
+ f"input_vars 中的列在 DataFrame 中不存在: {missing_cols}"
285
+ )
286
+
287
+ if metadata_cols is None:
288
+ metadata_cols = [c for c in drv_df.columns if c not in input_vars]
289
+
290
+ missing_meta = set(metadata_cols) - set(drv_df.columns)
291
+ if missing_meta:
292
+ raise KeyError(
293
+ f"metadata_cols 中的列在 DataFrame 中不存在: {missing_meta}"
294
+ )
295
+
296
+ # 校验 metadata 列的值在整个 DataFrame 中是否一致
297
+ for col in metadata_cols:
298
+ unique_vals = drv_df[col].drop_duplicates()
299
+ if len(unique_vals) > 1:
300
+ raise ValueError(
301
+ f"元信息列 '{col}' 存在多个不同的值: {unique_vals.to_list()}。"
302
+ f"预期 metadata 列在所有行中保持一致,请检查数据或调整 metadata_cols 参数。"
303
+ )
304
+
305
+ # ── 构建输出 ──
306
+ result: Dict[str, Any] = {}
307
+
308
+ # 1) 元信息标量
309
+ first_row = drv_df.iloc[0]
310
+ for col in metadata_cols:
311
+ result[col] = _safe_json_value(first_row[col])
312
+
313
+ # 2) 二级 JSON: 自定义 key + 单元素自动解包
314
+ inputs: Dict[str, Any] = {}
315
+ for col in input_vars:
316
+ arr = _safe_series_to_list(drv_df[col])
317
+ if unwrap_single and len(arr) == 1:
318
+ inputs[col] = arr[0]
319
+ else:
320
+ inputs[col] = arr
321
+
322
+ result[inputs_key] = inputs
323
+
324
+ return result
325
+
326
+
327
+ def json_to_df(
328
+ json_data: Union[str, Dict[str, Any]],
329
+ input_vars: Optional[List[str]] = None,
330
+ metadata_cols: Optional[List[str]] = None,
331
+ ) -> pd.DataFrame:
332
+ """将 JSON 格式还原为 DataFrame(自动检测三种格式)。
333
+
334
+ 三种模式:
335
+ 1) Row-Oriented 模式 (JSON 有 cdc_query_credits key)——NEW
336
+ 一级标量为 metadata,cdc_query_credits 为对象数组,每个对象一行。
337
+ 输出: metadata 列广播到所有行 + 对象字段展开为列。
338
+
339
+ {
340
+ "requestId": "req@123", "pullLogId": 3836171, ...,
341
+ "cdc_query_credits": [
342
+ {"_id": "id1", "montoPagar": 922, ...},
343
+ {"_id": "id2", "montoPagar": 0, ...}
344
+ ]
345
+ }
346
+
347
+ 2) 分区模式 (JSON 有 cdc_credit_inputs key)
348
+ 从 cdc_credit_inputs 取数组列,其余一级 key 为 metadata scalars。
349
+
350
+ {
351
+ "requestid": "req_001", "pulllogid": 3836171, ...,
352
+ "cdc_credit_inputs": {
353
+ "account_open_days": [2381, 4493],
354
+ "pagoactual": ["V", "V"]
355
+ }
356
+ }
357
+
358
+ 3) 平铺模式 (JSON 无 cdc_credit_inputs 也无 cdc_query_credits)
359
+ 所有一级 key 均为列名,list 值展开为行,scalar 值广播。
360
+
361
+ Parameters
362
+ ----------
363
+ json_data : Union[str, Dict[str, Any]]
364
+ JSON 字符串或字典。
365
+ input_vars : Optional[List[str]]
366
+ 分区模式下的入模特征列名。为 None 时自动检测 JSON 格式。
367
+ 注: row-oriented 模式下忽略此参数(cdc_query_credits 中的全部字段均展开)。
368
+ metadata_cols : Optional[List[str]]
369
+ 元信息列名。为 None 时自动推导(非 cdc_* 的一级 key)。
370
+
371
+ Returns
372
+ -------
373
+ pd.DataFrame
374
+ 还原后的 DataFrame,metadata 列在前,数据列在后。
375
+
376
+ Raises
377
+ ------
378
+ ValueError
379
+ 如果数组长度不一致,或 cdc_query_credits 不是数组。
380
+ """
381
+ if isinstance(json_data, str):
382
+ data = json.loads(json_data)
383
+ else:
384
+ data = json_data
385
+
386
+ has_cdc_inputs = "cdc_credit_inputs" in data
387
+ has_cdc_credits = "cdc_query_credits" in data
388
+
389
+ # ═══════════════════════════════════════════════════════════════════════
390
+ # 模式 1: Row-Oriented — cdc_query_credits(NEW)
391
+ # ═══════════════════════════════════════════════════════════════════════
392
+ if has_cdc_credits:
393
+ credits = data["cdc_query_credits"]
394
+ if not isinstance(credits, list):
395
+ raise ValueError(
396
+ f"cdc_query_credits 必须是数组,实际类型为 {type(credits).__name__}"
397
+ )
398
+
399
+ # 从对象数组构建 DataFrame
400
+ if len(credits) == 0:
401
+ df = pd.DataFrame()
402
+ else:
403
+ df = pd.DataFrame(credits)
404
+
405
+ # 广播一级 metadata 标量到所有行
406
+ for key, val in data.items():
407
+ if key == "cdc_query_credits":
408
+ continue
409
+ if len(df) == 0:
410
+ df[key] = pd.Series(dtype=type(val) if val is not None else object)
411
+ else:
412
+ df[key] = val
413
+
414
+ # 列排序: metadata 在前 → 数据字段在后
415
+ meta_cols_result = [k for k in data if k != "cdc_query_credits"]
416
+ credit_cols_result = [c for c in df.columns if c not in meta_cols_result]
417
+ df = df[meta_cols_result + credit_cols_result]
418
+
419
+ return df
420
+
421
+ # ═══════════════════════════════════════════════════════════════════════
422
+ # 模式 2 & 3: cdc_credit_inputs 分区模式 / 平铺模式
423
+ # ═══════════════════════════════════════════════════════════════════════
424
+
425
+ # ── 自动检测: input_vars 未指定时的处理 ──
426
+ if input_vars is None:
427
+ if has_cdc_inputs:
428
+ # 分区模式: input_vars = cdc_credit_inputs 的全部 key
429
+ input_vars = list(data["cdc_credit_inputs"].keys())
430
+ else:
431
+ # 平铺模式: 所有 list 值都是列
432
+ input_vars = [] # 无特殊 input_vars 区分
433
+
434
+ # ── 模式 3: 平铺模式: 所有一级 key 直接作为列 ──
435
+ if not has_cdc_inputs:
436
+ # 找到所有 list 列和 scalar 列
437
+ list_cols = {}
438
+ scalar_cols = {}
439
+ n_rows = 0
440
+ for key, val in data.items():
441
+ if isinstance(val, list):
442
+ list_cols[key] = val
443
+ if n_rows == 0:
444
+ n_rows = len(val)
445
+ elif len(val) != n_rows:
446
+ raise ValueError(
447
+ f"数组长度不一致: 期望 {n_rows},'{key}' 长度为 {len(val)}"
448
+ )
449
+ else:
450
+ scalar_cols[key] = val
451
+
452
+ if n_rows == 0:
453
+ # 没有 list 列时: 若存在 scalar 列,构造单行 DataFrame;
454
+ # 否则(空 JSON)返回空 DataFrame。
455
+ if scalar_cols:
456
+ return pd.DataFrame([scalar_cols])
457
+ return pd.DataFrame()
458
+
459
+ df = pd.DataFrame(list_cols)
460
+ for key, val in scalar_cols.items():
461
+ df[key] = val
462
+ return df
463
+
464
+ # ── 模式 2: 分区模式 (原有逻辑) ──
465
+ cdc_inputs = data["cdc_credit_inputs"]
466
+
467
+ missing_inputs = set(input_vars) - set(cdc_inputs.keys())
468
+ if missing_inputs:
469
+ raise ValueError(
470
+ f"input_vars 中的字段在 cdc_credit_inputs 中不存在: {missing_inputs}"
471
+ )
472
+
473
+ # 校验所有数组长度一致
474
+ lengths: Dict[str, int] = {}
475
+ for key in cdc_inputs:
476
+ if isinstance(cdc_inputs[key], list):
477
+ lengths[key] = len(cdc_inputs[key])
478
+
479
+ if lengths:
480
+ ref_key = next((k for k in input_vars if k in lengths), list(lengths.keys())[0])
481
+ ref_len = lengths[ref_key]
482
+ for key, length in lengths.items():
483
+ if length != ref_len:
484
+ raise ValueError(
485
+ f"cdc_credit_inputs 中数组长度不一致: "
486
+ f"'{ref_key}' 长度为 {ref_len},但 '{key}' 长度为 {length}"
487
+ )
488
+ n_rows = ref_len
489
+ else:
490
+ n_rows = 0
491
+
492
+ # 构建 DataFrame
493
+ df = pd.DataFrame({col: cdc_inputs[col] for col in input_vars})
494
+
495
+ extra_input_cols = [k for k in cdc_inputs if k not in input_vars]
496
+ for col in extra_input_cols:
497
+ df[col] = cdc_inputs[col]
498
+
499
+ if metadata_cols is None:
500
+ metadata_cols = [k for k in data if k != "cdc_credit_inputs"]
501
+
502
+ for col in metadata_cols:
503
+ if col in data:
504
+ df[col] = data[col]
505
+
506
+ ordered_cols = (
507
+ [c for c in metadata_cols if c in df.columns]
508
+ + [c for c in input_vars if c in df.columns and c not in metadata_cols]
509
+ + [c for c in extra_input_cols if c in df.columns]
510
+ )
511
+ df = df[ordered_cols]
512
+
513
+ return df
514
+
515
+
516
+ # ═══════════════════════════════════════════════════════════════════════════
517
+ # 便捷函数: JSON 字符串序列化 / 反序列化
518
+ # ═══════════════════════════════════════════════════════════════════════════
519
+
520
+ def df_to_json_string(
521
+ drv_df: pd.DataFrame,
522
+ input_vars: Optional[List[str]] = None,
523
+ metadata_cols: Optional[List[str]] = None,
524
+ indent: Optional[int] = 2,
525
+ ensure_ascii: bool = False,
526
+ ) -> str:
527
+ """df_to_json 的便捷封装,直接返回 JSON 字符串。"""
528
+ result = df_to_json(drv_df, input_vars, metadata_cols)
529
+ result = _sanitize_for_json(result)
530
+ return json.dumps(result, indent=indent, ensure_ascii=ensure_ascii)
531
+
532
+
533
+ def json_string_to_df(
534
+ json_string: str,
535
+ input_vars: Optional[List[str]] = None,
536
+ metadata_cols: Optional[List[str]] = None,
537
+ ) -> pd.DataFrame:
538
+ """json_to_df 的便捷封装,接受 JSON 字符串输入。"""
539
+ return json_to_df(json_string, input_vars, metadata_cols)
540
+
541
+
542
+ def df_to_json_file(
543
+ drv_df: pd.DataFrame,
544
+ output_path: str,
545
+ input_vars: Optional[List[str]] = None,
546
+ metadata_cols: Optional[List[str]] = None,
547
+ indent: Optional[int] = 2,
548
+ ensure_ascii: bool = False,
549
+ ) -> str:
550
+ """将 drv_df DataFrame 转换为 expected JSON 格式并写入 .json 文件。
551
+
552
+ Parameters
553
+ ----------
554
+ drv_df : pd.DataFrame
555
+ 源 DataFrame,每行一条征信账户记录。
556
+ input_vars : List[str]
557
+ 入模特征列名列表。
558
+ output_path : str
559
+ 输出 .json 文件的路径。
560
+ metadata_cols : Optional[List[str]]
561
+ 显式指定元信息列名。为 None 时自动推导。
562
+ indent : Optional[int]
563
+ JSON 缩进空格数。None 表示紧凑输出(单行),默认 2。
564
+ ensure_ascii : bool
565
+ 是否将非 ASCII 字符转义为 \\uXXXX。默认 False,保留中文等原始字符。
566
+
567
+ Returns
568
+ -------
569
+ str
570
+ 写入文件的绝对路径。
571
+ """
572
+ result = df_to_json(drv_df, input_vars, metadata_cols)
573
+ result = _sanitize_for_json(result)
574
+ with open(output_path, 'w', encoding='utf-8') as f:
575
+ json.dump(result, f, indent=indent, ensure_ascii=ensure_ascii)
576
+ return os.path.abspath(output_path)
577
+
578
+ def load_json_file(file_path: str) -> Dict[str, Any]:
579
+ """从 .json 文件加载为字典。
580
+
581
+ Parameters
582
+ ----------
583
+ file_path : str
584
+ .json 文件的路径。
585
+
586
+ Returns
587
+ -------
588
+ Dict[str, Any]
589
+ 解析后的字典,结构符合 expected JSON 格式。
590
+ """
591
+ with open(file_path, 'r', encoding='utf-8') as f:
592
+ return json.load(f)
593
+
594
+
595
+ def json_to_file(
596
+ data: Dict[str, Any],
597
+ output_path: str,
598
+ indent: Optional[int] = 2,
599
+ ensure_ascii: bool = False,
600
+ ) -> str:
601
+ """将 Python dict 写入 .json 文件。
602
+
603
+ Parameters
604
+ ----------
605
+ data : Dict[str, Any]
606
+ 待写入的字典。
607
+ output_path : str
608
+ 输出 .json 文件路径。
609
+ indent : Optional[int]
610
+ JSON 缩进空格数。None 表示紧凑单行,默认 2。
611
+ ensure_ascii : bool
612
+ 是否转义非 ASCII 字符。默认 False。
613
+
614
+ Returns
615
+ -------
616
+ str
617
+ 写入文件的绝对路径。
618
+ """
619
+ with open(output_path, 'w', encoding='utf-8') as f:
620
+ json.dump(_sanitize_for_json(data), f, indent=indent, ensure_ascii=ensure_ascii)
621
+ return os.path.abspath(output_path)