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,199 @@
|
|
|
1
|
+
"""Model artifact persistence helpers with metadata support.
|
|
2
|
+
|
|
3
|
+
This module provides the public ``save_model`` / ``load_model`` helpers used by
|
|
4
|
+
``Modeling_Tool``. It stays backward compatible with legacy files that contain
|
|
5
|
+
only a raw joblib object while adding an SMF artifact envelope for model version
|
|
6
|
+
and deployment metadata.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import platform
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
from typing import Any, Dict, Optional
|
|
13
|
+
|
|
14
|
+
import joblib
|
|
15
|
+
|
|
16
|
+
_ARTIFACT_MARKER = "__smf_model_artifact__"
|
|
17
|
+
_ARTIFACT_VERSION = "1.0"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _get_smf_version():
|
|
21
|
+
"""Resolve the installed SMF version without creating hard import cycles."""
|
|
22
|
+
try:
|
|
23
|
+
import Modeling_Tool
|
|
24
|
+
return getattr(Modeling_Tool, "__version__", None)
|
|
25
|
+
except Exception:
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _model_class_name(model):
|
|
30
|
+
if model is None:
|
|
31
|
+
return None
|
|
32
|
+
return type(model).__name__
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _model_module_name(model):
|
|
36
|
+
if model is None:
|
|
37
|
+
return None
|
|
38
|
+
return type(model).__module__
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _as_list(value):
|
|
42
|
+
if value is None:
|
|
43
|
+
return None
|
|
44
|
+
if isinstance(value, str):
|
|
45
|
+
return [value]
|
|
46
|
+
try:
|
|
47
|
+
return list(value)
|
|
48
|
+
except TypeError:
|
|
49
|
+
return [value]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _build_model_metadata(
|
|
53
|
+
model,
|
|
54
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
55
|
+
feature_cols=None,
|
|
56
|
+
woe_mapping_path: Optional[str] = None,
|
|
57
|
+
train_window: Optional[Dict[str, Any]] = None,
|
|
58
|
+
metrics: Optional[Dict[str, Any]] = None,
|
|
59
|
+
model_name: Optional[str] = None,
|
|
60
|
+
model_version: Optional[str] = None,
|
|
61
|
+
):
|
|
62
|
+
"""Build the standard metadata block stored in an SMF model artifact."""
|
|
63
|
+
base = {
|
|
64
|
+
"smf_version": _get_smf_version(),
|
|
65
|
+
"artifact_version": _ARTIFACT_VERSION,
|
|
66
|
+
"model_name": model_name,
|
|
67
|
+
"model_version": model_version,
|
|
68
|
+
"model_class": _model_class_name(model),
|
|
69
|
+
"model_module": _model_module_name(model),
|
|
70
|
+
"feature_cols": _as_list(feature_cols),
|
|
71
|
+
"woe_mapping_path": woe_mapping_path,
|
|
72
|
+
"train_window": train_window,
|
|
73
|
+
"metrics": metrics,
|
|
74
|
+
"created_at": datetime.now(timezone.utc).isoformat(),
|
|
75
|
+
"python_version": platform.python_version(),
|
|
76
|
+
"platform": platform.platform(),
|
|
77
|
+
}
|
|
78
|
+
if metadata:
|
|
79
|
+
# User-provided values intentionally override the generated defaults.
|
|
80
|
+
base.update(dict(metadata))
|
|
81
|
+
return base
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _is_smf_model_artifact(obj):
|
|
85
|
+
"""Return True when *obj* follows the SMF artifact envelope schema."""
|
|
86
|
+
return isinstance(obj, dict) and obj.get(_ARTIFACT_MARKER) is True and "model" in obj
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def make_model_artifact(
|
|
90
|
+
model,
|
|
91
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
92
|
+
feature_cols=None,
|
|
93
|
+
woe_mapping_path: Optional[str] = None,
|
|
94
|
+
train_window: Optional[Dict[str, Any]] = None,
|
|
95
|
+
metrics: Optional[Dict[str, Any]] = None,
|
|
96
|
+
model_name: Optional[str] = None,
|
|
97
|
+
model_version: Optional[str] = None,
|
|
98
|
+
):
|
|
99
|
+
"""Create an in-memory SMF model artifact without writing it to disk."""
|
|
100
|
+
artifact_metadata = _build_model_metadata(
|
|
101
|
+
model=model,
|
|
102
|
+
metadata=metadata,
|
|
103
|
+
feature_cols=feature_cols,
|
|
104
|
+
woe_mapping_path=woe_mapping_path,
|
|
105
|
+
train_window=train_window,
|
|
106
|
+
metrics=metrics,
|
|
107
|
+
model_name=model_name,
|
|
108
|
+
model_version=model_version,
|
|
109
|
+
)
|
|
110
|
+
return {
|
|
111
|
+
_ARTIFACT_MARKER: True,
|
|
112
|
+
"artifact_version": _ARTIFACT_VERSION,
|
|
113
|
+
"model": model,
|
|
114
|
+
"metadata": artifact_metadata,
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def save_model(
|
|
119
|
+
model,
|
|
120
|
+
filename,
|
|
121
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
122
|
+
feature_cols=None,
|
|
123
|
+
woe_mapping_path: Optional[str] = None,
|
|
124
|
+
train_window: Optional[Dict[str, Any]] = None,
|
|
125
|
+
metrics: Optional[Dict[str, Any]] = None,
|
|
126
|
+
model_name: Optional[str] = None,
|
|
127
|
+
model_version: Optional[str] = None,
|
|
128
|
+
include_metadata: bool = True,
|
|
129
|
+
):
|
|
130
|
+
"""Save a model, optionally wrapped with standard SMF metadata.
|
|
131
|
+
|
|
132
|
+
Parameters
|
|
133
|
+
----------
|
|
134
|
+
model : object
|
|
135
|
+
Model object to persist.
|
|
136
|
+
filename : str or path-like
|
|
137
|
+
Destination path.
|
|
138
|
+
metadata : dict, optional
|
|
139
|
+
Additional or overriding metadata fields.
|
|
140
|
+
feature_cols : list-like, optional
|
|
141
|
+
Training feature list.
|
|
142
|
+
woe_mapping_path : str, optional
|
|
143
|
+
Path to the WOE mapping table used by the model.
|
|
144
|
+
train_window : dict, optional
|
|
145
|
+
Training / validation / OOT sample window metadata.
|
|
146
|
+
metrics : dict, optional
|
|
147
|
+
Evaluation metrics such as AUC / KS by dataset.
|
|
148
|
+
model_name : str, optional
|
|
149
|
+
Business model name.
|
|
150
|
+
model_version : str, optional
|
|
151
|
+
Business model version.
|
|
152
|
+
include_metadata : bool, default True
|
|
153
|
+
If True, save an SMF artifact envelope. If False, save the raw model
|
|
154
|
+
object exactly like the legacy helper.
|
|
155
|
+
|
|
156
|
+
Returns
|
|
157
|
+
-------
|
|
158
|
+
int
|
|
159
|
+
0 on success.
|
|
160
|
+
"""
|
|
161
|
+
payload = model
|
|
162
|
+
if include_metadata:
|
|
163
|
+
payload = make_model_artifact(
|
|
164
|
+
model=model,
|
|
165
|
+
metadata=metadata,
|
|
166
|
+
feature_cols=feature_cols,
|
|
167
|
+
woe_mapping_path=woe_mapping_path,
|
|
168
|
+
train_window=train_window,
|
|
169
|
+
metrics=metrics,
|
|
170
|
+
model_name=model_name,
|
|
171
|
+
model_version=model_version,
|
|
172
|
+
)
|
|
173
|
+
joblib.dump(payload, filename)
|
|
174
|
+
return 0
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def load_model(model_path, return_metadata: bool = False):
|
|
178
|
+
"""Load a legacy model or an SMF model artifact.
|
|
179
|
+
|
|
180
|
+
By default this keeps backward compatibility and returns only the model
|
|
181
|
+
object. Set ``return_metadata=True`` to receive ``(model, metadata)``.
|
|
182
|
+
"""
|
|
183
|
+
obj = joblib.load(model_path)
|
|
184
|
+
if _is_smf_model_artifact(obj):
|
|
185
|
+
model = obj["model"]
|
|
186
|
+
metadata = obj.get("metadata", {})
|
|
187
|
+
return (model, metadata) if return_metadata else model
|
|
188
|
+
return (obj, {}) if return_metadata else obj
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def load_model_metadata(model_path):
|
|
192
|
+
"""Load only metadata from an SMF model artifact.
|
|
193
|
+
|
|
194
|
+
Legacy raw-model files return an empty dict.
|
|
195
|
+
"""
|
|
196
|
+
obj = joblib.load(model_path)
|
|
197
|
+
if _is_smf_model_artifact(obj):
|
|
198
|
+
return obj.get("metadata", {})
|
|
199
|
+
return {}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
logger = logging.getLogger(__name__)
|
|
5
|
+
import pandas as pd
|
|
6
|
+
from odps import ODPS, options
|
|
7
|
+
from odps.models import Schema, Column, Partition
|
|
8
|
+
|
|
9
|
+
# Available only in newer pandas versions. Older Airflow images should skip it.
|
|
10
|
+
try:
|
|
11
|
+
pd.set_option('future.no_silent_downcasting', True)
|
|
12
|
+
except (KeyError, ValueError):
|
|
13
|
+
pass
|
|
14
|
+
pd.options.mode.chained_assignment = None # default='warn'
|
|
15
|
+
|
|
16
|
+
logging.basicConfig(level=logging.INFO, format="%(message)s")
|
|
17
|
+
|
|
18
|
+
class ODPSRunner(object):
|
|
19
|
+
"""ODPS执行类
|
|
20
|
+
"""
|
|
21
|
+
def __init__(self):
|
|
22
|
+
self.o = ODPS(
|
|
23
|
+
os.environ["ALIBABA_CLOUD_ACCESS_KEY_ID"],
|
|
24
|
+
os.environ["ALIBABA_CLOUD_ACCESS_KEY_SECRET"],
|
|
25
|
+
os.environ.get("ODPS_PROJECT", "mex_anls"),
|
|
26
|
+
endpoint=os.environ.get(
|
|
27
|
+
"ODPS_ENDPOINT",
|
|
28
|
+
"https://service.ap-southeast-1-vpc.maxcompute.aliyun-inc.com/api",
|
|
29
|
+
),
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
options.retry_times = 6 # 请求重试次数
|
|
33
|
+
options.pool_maxsize = 200 # 连接池最大容量
|
|
34
|
+
options.connect_timeout = 3600 # 连接超时
|
|
35
|
+
options.read_timeout = 3600 # 读取超时
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def run_sql(self, sql, to_df=True, n_process=1, csv_path=None):
|
|
39
|
+
"""运行SQL并下载结果。
|
|
40
|
+
|
|
41
|
+
Parameters
|
|
42
|
+
----------
|
|
43
|
+
sql : str
|
|
44
|
+
单个 SQL 代码。
|
|
45
|
+
to_df : bool, default True
|
|
46
|
+
是否把结果加载到内存中作为 ``pandas.DataFrame`` 返回。
|
|
47
|
+
若 ``False``, 函数返回**空 DataFrame**, 但仍会下载数据(当 ``csv_path`` 被指定时)。
|
|
48
|
+
n_process : int, default 1
|
|
49
|
+
``executor.open_reader().to_pandas`` 的并行进程数。
|
|
50
|
+
csv_path : str, default None
|
|
51
|
+
把结果另存为本地 CSV 的路径。**与 ``to_df`` 互相独立**:
|
|
52
|
+
* 只设 ``csv_path`` → 下载 + 写 CSV, 不返回数据 (返回空 DataFrame)
|
|
53
|
+
* 只设 ``to_df=True`` → 下载 + 返回 DataFrame, 不写 CSV
|
|
54
|
+
* 两个都设 → 下载 + 返回 + 写 CSV
|
|
55
|
+
* 都不设 → 只跑 SQL 不下载 (用于 DDL/INSERT 等)
|
|
56
|
+
|
|
57
|
+
Returns
|
|
58
|
+
-------
|
|
59
|
+
pandas.DataFrame
|
|
60
|
+
当 ``to_df=True`` 时返回完整数据;
|
|
61
|
+
当 ``to_df=False`` 时返回空 DataFrame (用于占位).
|
|
62
|
+
|
|
63
|
+
Notes
|
|
64
|
+
-----
|
|
65
|
+
- **执行**阶段(execute_sql)只跑一次, 无重试.
|
|
66
|
+
- **下载**阶段(to_pandas + to_csv)最多重试 6 次, 适用于网络抖动.
|
|
67
|
+
- 当 SQL 返回列数 > 200 时, ``_patch_wide_schema_download`` 自动 patch ODPS Tunnel,
|
|
68
|
+
防止 HTTP 414 (URI too long).
|
|
69
|
+
|
|
70
|
+
Examples
|
|
71
|
+
--------
|
|
72
|
+
>>> odps = ODPSRunner()
|
|
73
|
+
>>> df = odps.run_sql("SELECT * FROM dual LIMIT 10") # 仅 DataFrame
|
|
74
|
+
>>> df = odps.run_sql("SELECT * FROM dual LIMIT 10", csv_path="x.csv") # DataFrame + CSV
|
|
75
|
+
>>> _ = odps.run_sql("SELECT * FROM dual LIMIT 10", to_df=False, # 仅 CSV
|
|
76
|
+
... csv_path="x.csv")
|
|
77
|
+
>>> _ = odps.run_sql("CREATE TABLE t AS SELECT 1") # 仅执行, 不下载
|
|
78
|
+
"""
|
|
79
|
+
# 准备SQL
|
|
80
|
+
sqldesc = sql[:100]+"..." if len(sql)>100 else sql
|
|
81
|
+
logging.info(f"SQL: \n{sqldesc}")
|
|
82
|
+
|
|
83
|
+
# 运行SQL(只执行一次,不重试)
|
|
84
|
+
starttime = datetime.now()
|
|
85
|
+
logging.info(f' execute_sql: {starttime.strftime("%Y-%m-%d %H:%M:%S")}')
|
|
86
|
+
executor = self.o.execute_sql(sql)
|
|
87
|
+
|
|
88
|
+
# 决定是否需要下载: 至少满足 to_df=True 或 csv_path 不为空
|
|
89
|
+
should_download = bool(to_df) or bool(csv_path)
|
|
90
|
+
df = pd.DataFrame()
|
|
91
|
+
|
|
92
|
+
if should_download:
|
|
93
|
+
k = 6
|
|
94
|
+
for i in range(k):
|
|
95
|
+
orig_build = None
|
|
96
|
+
try:
|
|
97
|
+
logging.info(f' to_pandas: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}')
|
|
98
|
+
reader = executor.open_reader()
|
|
99
|
+
orig_build = self._patch_wide_schema_download(reader)
|
|
100
|
+
if n_process > 1:
|
|
101
|
+
df = reader.to_pandas(n_process=n_process)
|
|
102
|
+
else:
|
|
103
|
+
df = reader.to_pandas()
|
|
104
|
+
if bool(csv_path):
|
|
105
|
+
logging.info(f' to_csv: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}')
|
|
106
|
+
df.to_csv(csv_path)
|
|
107
|
+
break
|
|
108
|
+
except Exception as e:
|
|
109
|
+
logging.error(f' download failed [{i+1}/{k}]: {e}')
|
|
110
|
+
if i == k - 1:
|
|
111
|
+
endtime = datetime.now()
|
|
112
|
+
duration = round((endtime - starttime).total_seconds(), 1)
|
|
113
|
+
raise SystemError(
|
|
114
|
+
f' break: {endtime.strftime("%Y-%m-%d %H:%M:%S")} duration {duration}\n'
|
|
115
|
+
)
|
|
116
|
+
finally:
|
|
117
|
+
if orig_build is not None:
|
|
118
|
+
from odps.tunnel.instancetunnel import InstanceDownloadSession
|
|
119
|
+
InstanceDownloadSession._build_input_stream = orig_build
|
|
120
|
+
|
|
121
|
+
# 当用户显式要求不要 DataFrame 时, 主动释放引用, 节省内存
|
|
122
|
+
if not to_df:
|
|
123
|
+
df = pd.DataFrame()
|
|
124
|
+
|
|
125
|
+
endtime = datetime.now()
|
|
126
|
+
duration = round((endtime - starttime).total_seconds(), 1)
|
|
127
|
+
logging.info(f' done: {endtime.strftime("%Y-%m-%d %H:%M:%S")} duration {duration}\n')
|
|
128
|
+
return df
|
|
129
|
+
|
|
130
|
+
def download_table(self, table_name, partition=None, n_process=1, csv_path=None):
|
|
131
|
+
"""读取表中数据至DataFrame
|
|
132
|
+
|
|
133
|
+
Parameters
|
|
134
|
+
----------
|
|
135
|
+
table_name : str
|
|
136
|
+
表名
|
|
137
|
+
partition : dict
|
|
138
|
+
分区, 例如: 'dt=2022-01-01,taino=0'
|
|
139
|
+
n_process : int, default 1
|
|
140
|
+
将查询数据转为pandas.DataFrame的进程数
|
|
141
|
+
csv_path : str
|
|
142
|
+
查询数据保存至csv文件路径
|
|
143
|
+
|
|
144
|
+
Returns
|
|
145
|
+
-------
|
|
146
|
+
df: pandas.DataFrame
|
|
147
|
+
SQL查询数据结果
|
|
148
|
+
"""
|
|
149
|
+
logging.info(f"Table: \n{table_name} {partition}")
|
|
150
|
+
starttime = datetime.now()
|
|
151
|
+
logging.info(f' to_pandas: {starttime.strftime("%Y-%m-%d %H:%M:%S")}')
|
|
152
|
+
|
|
153
|
+
t = self.o.get_table(table_name)
|
|
154
|
+
if bool(partition):
|
|
155
|
+
reader = t.open_reader(partition=partition)
|
|
156
|
+
else:
|
|
157
|
+
reader = t.open_reader()
|
|
158
|
+
if n_process > 1:
|
|
159
|
+
df = reader.to_pandas(n_process=10)
|
|
160
|
+
else:
|
|
161
|
+
df = reader.to_pandas()
|
|
162
|
+
|
|
163
|
+
if bool(csv_path):
|
|
164
|
+
logging.info(f' to_csv: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}')
|
|
165
|
+
df.to_csv(csv_path, index=False)
|
|
166
|
+
|
|
167
|
+
endtime = datetime.now()
|
|
168
|
+
duration = round((endtime - starttime).total_seconds(), 3)
|
|
169
|
+
logging.info(f' done shape={df.shape}: {endtime.strftime("%Y-%m-%d %H:%M:%S")} duration {duration}\n')
|
|
170
|
+
|
|
171
|
+
return df
|
|
172
|
+
|
|
173
|
+
@staticmethod
|
|
174
|
+
def _patch_wide_schema_download(reader, col_threshold=200):
|
|
175
|
+
"""Prevent HTTP 414 when a query result has many columns.
|
|
176
|
+
|
|
177
|
+
ODPS Tunnel encodes column names as URL query params. With 200+ columns
|
|
178
|
+
the URI exceeds the server limit. We temporarily replace the class-level
|
|
179
|
+
_build_input_stream to omit the columns param, so the server returns all
|
|
180
|
+
columns without URL-based filtering.
|
|
181
|
+
|
|
182
|
+
Returns the original method so the caller can restore it in a finally block.
|
|
183
|
+
Returns None when no patch is needed (schema is narrow enough).
|
|
184
|
+
"""
|
|
185
|
+
ds = getattr(reader, '_download_session', None)
|
|
186
|
+
if ds is None:
|
|
187
|
+
return None
|
|
188
|
+
schema = getattr(ds, 'schema', None)
|
|
189
|
+
if schema is None or len(schema.simple_columns) <= col_threshold:
|
|
190
|
+
return None
|
|
191
|
+
|
|
192
|
+
from odps.tunnel.instancetunnel import InstanceDownloadSession
|
|
193
|
+
orig_build = InstanceDownloadSession._build_input_stream
|
|
194
|
+
|
|
195
|
+
def _build_no_column_filter(self, start, count, compress=False, columns=None, arrow=False, raw_size=None):
|
|
196
|
+
return orig_build(self, start, count, compress=compress, columns=None, arrow=arrow, raw_size=raw_size)
|
|
197
|
+
|
|
198
|
+
InstanceDownloadSession._build_input_stream = _build_no_column_filter
|
|
199
|
+
logging.info(f' wide schema ({len(schema.simple_columns)} cols): patched tunnel to omit column filter from URL')
|
|
200
|
+
return orig_build
|
|
201
|
+
|
|
202
|
+
@staticmethod
|
|
203
|
+
def cre_table_schema(df, partition_name=None):
|
|
204
|
+
|
|
205
|
+
dtypes = df.dtypes
|
|
206
|
+
isnum_dtypes = [pd.api.types.is_numeric_dtype(x) for x in dtypes]
|
|
207
|
+
isint_dtypes = [pd.api.types.is_integer_dtype(x) for x in dtypes]
|
|
208
|
+
|
|
209
|
+
table_columns = []
|
|
210
|
+
table_partitions = []
|
|
211
|
+
for i in range(len(df.columns)):
|
|
212
|
+
col = df.columns[i]
|
|
213
|
+
col_type = "string" if not isnum_dtypes[i] else "float"
|
|
214
|
+
col_type = "bigint" if isint_dtypes[i] else col_type
|
|
215
|
+
if col == partition_name:
|
|
216
|
+
table_partitions.append(Partition(name=col, type=col_type))
|
|
217
|
+
else:
|
|
218
|
+
table_columns.append(Column(name=col, type=col_type))
|
|
219
|
+
|
|
220
|
+
if bool(table_partitions):
|
|
221
|
+
table_schema = Schema(columns=table_columns, partitions=table_partitions)
|
|
222
|
+
else:
|
|
223
|
+
table_schema = Schema(columns=table_columns)
|
|
224
|
+
|
|
225
|
+
return table_schema
|
|
226
|
+
|
|
227
|
+
def upload_df(self, df, table_name, table_schema=None, partition=None):
|
|
228
|
+
"""上传数据集至mc中创建新表
|
|
229
|
+
|
|
230
|
+
Parameters
|
|
231
|
+
----------
|
|
232
|
+
table_name: str
|
|
233
|
+
表名
|
|
234
|
+
table_schema: odps.models.Schema
|
|
235
|
+
表Schema
|
|
236
|
+
df: pandas.DataFrame
|
|
237
|
+
数据集
|
|
238
|
+
partition: string
|
|
239
|
+
保存分区
|
|
240
|
+
"""
|
|
241
|
+
if table_schema is None:
|
|
242
|
+
df.loc[:, "py_inserttime"] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
|
243
|
+
table_schema = self.cre_table_schema(df=df, partition_name=None)
|
|
244
|
+
|
|
245
|
+
self.o.delete_table(table_name, if_exists=True)
|
|
246
|
+
t = self.o.create_table(table_name, table_schema)
|
|
247
|
+
|
|
248
|
+
if bool(partition):
|
|
249
|
+
with t.open_writer(partition=partition, create_partition=True) as writer:
|
|
250
|
+
writer.write(df.values.tolist())
|
|
251
|
+
else:
|
|
252
|
+
with t.open_writer() as writer:
|
|
253
|
+
writer.write(df.values.tolist())
|
|
254
|
+
logger.info(f'<<<< 完成数据入表{table_name}: shape={df.shape} >>>>')
|
|
255
|
+
|
|
256
|
+
def insert_df(self, df, table_name, overwrite=True, partition=None):
|
|
257
|
+
"""将数据集插入至mc已存在的表中.
|
|
258
|
+
|
|
259
|
+
Parameters
|
|
260
|
+
----------
|
|
261
|
+
df: pandas.DataFrame
|
|
262
|
+
数据集
|
|
263
|
+
table_name: str
|
|
264
|
+
表名
|
|
265
|
+
overwrite: Bool, default True
|
|
266
|
+
是否覆盖
|
|
267
|
+
partition: string, default None
|
|
268
|
+
写入分区, 默认为None即无分区
|
|
269
|
+
"""
|
|
270
|
+
t = self.o.get_table(table_name)
|
|
271
|
+
if "py_inserttime" in t.schema and "py_inserttime" not in df.columns:
|
|
272
|
+
df.loc[:, "py_inserttime"] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
|
273
|
+
|
|
274
|
+
if bool(partition):
|
|
275
|
+
if overwrite:
|
|
276
|
+
t.delete_partition(partition, if_exists=True)
|
|
277
|
+
with t.open_writer(partition=partition, create_partition=True) as writer:
|
|
278
|
+
writer.write(df.values.tolist())
|
|
279
|
+
else:
|
|
280
|
+
if overwrite:
|
|
281
|
+
t.truncate()
|
|
282
|
+
with t.open_writer() as writer:
|
|
283
|
+
writer.write(df.values.tolist())
|
|
284
|
+
logger.info('<<<< 完成数据入表: shape={0} >>>>'.format(df.shape))
|