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.
- ExcelMaster/ExcelFormatTool.py +487 -0
- ExcelMaster/ExcelMaster.py +917 -0
- ExcelMaster/Template.py +525 -0
- ExcelMaster/Utility.py +233 -0
- ExcelMaster/__init__.py +3 -0
- Modeling_Tool/Core/Binning_Tool.py +1608 -0
- Modeling_Tool/Core/Binning_Tool.pyi +48 -0
- Modeling_Tool/Core/Check_DuckDB_Compatibility.py +835 -0
- Modeling_Tool/Core/Json_Data_Converter.py +621 -0
- Modeling_Tool/Core/Model_Registry_Tool.py +199 -0
- Modeling_Tool/Core/ODPS_Tool.py +284 -0
- Modeling_Tool/Core/Slope_Tool.py +356 -0
- Modeling_Tool/Core/Slope_Tool.pyi +31 -0
- Modeling_Tool/Core/XOR_Encryptor.py +207 -0
- Modeling_Tool/Core/XOR_Encryptor.pyi +26 -0
- Modeling_Tool/Core/__init__.py +99 -0
- Modeling_Tool/Core/kDataFrame.py +228 -0
- Modeling_Tool/Core/kDataFrame.pyi +43 -0
- Modeling_Tool/Core/sample_weight_utils.py +77 -0
- Modeling_Tool/Core/utils.py +2672 -0
- Modeling_Tool/Eval/Evaluation_Tool.py +1452 -0
- Modeling_Tool/Eval/Evaluation_Tool.pyi +47 -0
- Modeling_Tool/Eval/Model_Eval_Tool.py +2548 -0
- Modeling_Tool/Eval/Model_Eval_Tool.pyi +37 -0
- Modeling_Tool/Eval/__init__.py +54 -0
- Modeling_Tool/Eval/evaluate_model.py +2008 -0
- Modeling_Tool/Eval/evaluate_model.pyi +50 -0
- Modeling_Tool/Eval/weighted_eval_utils.py +326 -0
- Modeling_Tool/Explainability/Coalition_Structure.py +305 -0
- Modeling_Tool/Explainability/Model_Explainer.py +743 -0
- Modeling_Tool/Explainability/__init__.py +24 -0
- Modeling_Tool/Feature/Distribution_Tool.py +509 -0
- Modeling_Tool/Feature/Distribution_Tool.pyi +42 -0
- Modeling_Tool/Feature/Feature_Insights.py +762 -0
- Modeling_Tool/Feature/Feature_Insights.pyi +33 -0
- Modeling_Tool/Feature/PSI_Tool.py +1195 -0
- Modeling_Tool/Feature/PSI_Tool.pyi +29 -0
- Modeling_Tool/Feature/WOE_Engine_Feature_Patch.py +355 -0
- Modeling_Tool/Feature/__init__.py +40 -0
- Modeling_Tool/Model/Backward_Tool.py +778 -0
- Modeling_Tool/Model/Backward_Tool.pyi +45 -0
- Modeling_Tool/Model/GBM_Search_Tool.py +251 -0
- Modeling_Tool/Model/GBM_Tool.py +1610 -0
- Modeling_Tool/Model/GBM_Tool.pyi +90 -0
- Modeling_Tool/Model/LRM_Tool.py +1198 -0
- Modeling_Tool/Model/LRM_Tool.pyi +47 -0
- Modeling_Tool/Model/__init__.py +61 -0
- Modeling_Tool/Sample/Distribution_Adaptation.py +131 -0
- Modeling_Tool/Sample/Distribution_Adaptation.pyi +30 -0
- Modeling_Tool/Sample/Reject_Infer.py +413 -0
- Modeling_Tool/Sample/Reject_Infer.pyi +43 -0
- Modeling_Tool/Sample/Sample_Split.py +520 -0
- Modeling_Tool/Sample/Sample_Split.pyi +43 -0
- Modeling_Tool/Sample/__init__.py +31 -0
- Modeling_Tool/UAT/UAT_Consistency_Checker.py +1180 -0
- Modeling_Tool/UAT/__init__.py +19 -0
- Modeling_Tool/WOE/WOE_Adapter.py +204 -0
- Modeling_Tool/WOE/WOE_Adapter.pyi +21 -0
- Modeling_Tool/WOE/WOE_Master.py +491 -0
- Modeling_Tool/WOE/WOE_Master.pyi +40 -0
- Modeling_Tool/WOE/WOE_Monotone_Binner.py +3324 -0
- Modeling_Tool/WOE/WOE_Monotone_Binner.pyi +71 -0
- Modeling_Tool/WOE/WOE_Plot_Tool.py +919 -0
- Modeling_Tool/WOE/WOE_Plot_Tool.pyi +46 -0
- Modeling_Tool/WOE/WOE_Report_Builder.py +214 -0
- Modeling_Tool/WOE/WOE_Report_Builder.pyi +24 -0
- Modeling_Tool/WOE/WOE_Tool.py +1094 -0
- Modeling_Tool/WOE/WOE_Tool.pyi +41 -0
- Modeling_Tool/WOE/__init__.py +86 -0
- Modeling_Tool/WOE/plot_woe_tool.py +290 -0
- Modeling_Tool/WOE/plot_woe_tool.pyi +25 -0
- Modeling_Tool/__init__.py +176 -0
- Report/Report_Tool.py +380 -0
- Report/__init__.py +3 -0
- supermodelingfactory-0.2.0.dist-info/METADATA +268 -0
- supermodelingfactory-0.2.0.dist-info/RECORD +79 -0
- supermodelingfactory-0.2.0.dist-info/WHEEL +5 -0
- supermodelingfactory-0.2.0.dist-info/licenses/LICENSE +102 -0
- 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)
|