algobench-sdk 2.0.2__tar.gz → 2.0.4__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.
- {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/PKG-INFO +1 -1
- {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/algobench/__init__.py +1 -1
- algobench_sdk-2.0.4/algobench/optimization/__init__.py +16 -0
- algobench_sdk-2.0.4/algobench/optimization/decision_tree.py +365 -0
- algobench_sdk-2.0.4/algobench/optimization/feature_importance.py +373 -0
- algobench_sdk-2.0.4/algobench/optimization/knee_point.py +113 -0
- algobench_sdk-2.0.4/algobench/optimization/response_surface.py +255 -0
- {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/algobench_sdk.egg-info/PKG-INFO +1 -1
- {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/algobench_sdk.egg-info/SOURCES.txt +5 -0
- {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/pyproject.toml +1 -1
- {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/README.md +0 -0
- {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/algobench/__main__.py +0 -0
- {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/algobench/api.py +0 -0
- {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/algobench/cli.py +0 -0
- {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/algobench/config/__init__.py +0 -0
- {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/algobench/config/analysis_criteria.py +0 -0
- {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/algobench/config/business.py +0 -0
- {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/algobench/config/metric_keywords.py +0 -0
- {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/algobench/config/metrics.py +0 -0
- {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/algobench/config/thresholds.py +0 -0
- {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/algobench/exceptions.py +0 -0
- {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/algobench/models.py +0 -0
- {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/algobench/parsers/__init__.py +0 -0
- {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/algobench/parsers/csv_parser.py +0 -0
- {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/algobench/stats/__init__.py +0 -0
- {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/algobench/stats/core.py +0 -0
- {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/algobench/stats/diagnosis.py +0 -0
- {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/algobench/stats/qvalue.py +0 -0
- {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/algobench/stats/sample_processing.py +0 -0
- {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/algobench/stats/tests/__init__.py +0 -0
- {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/algobench/stats/tests/data_type.py +0 -0
- {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/algobench/stats/tests/effect_size.py +0 -0
- {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/algobench/stats/tests/nonparametric.py +0 -0
- {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/algobench/stats/tests/smart.py +0 -0
- {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/algobench/stats/tests/t_tests.py +0 -0
- {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/algobench/utils/__init__.py +0 -0
- {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/algobench/utils/improvement.py +0 -0
- {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/algobench/utils/math_utils.py +0 -0
- {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/algobench/utils/numbers.py +0 -0
- {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/algobench_sdk.egg-info/dependency_links.txt +0 -0
- {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/algobench_sdk.egg-info/requires.txt +0 -0
- {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/algobench_sdk.egg-info/top_level.txt +0 -0
- {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/setup.cfg +0 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""AlgoBench optimization module - parameter optimization algorithms."""
|
|
2
|
+
|
|
3
|
+
from .knee_point import detect_knee_point
|
|
4
|
+
from .feature_importance import compute_feature_importance, get_overall_param_ranking
|
|
5
|
+
from .decision_tree import extract_decision_rules, format_rule_conditions
|
|
6
|
+
from .response_surface import fit_response_surface, select_top_params
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
'detect_knee_point',
|
|
10
|
+
'compute_feature_importance',
|
|
11
|
+
'get_overall_param_ranking',
|
|
12
|
+
'extract_decision_rules',
|
|
13
|
+
'format_rule_conditions',
|
|
14
|
+
'fit_response_surface',
|
|
15
|
+
'select_top_params',
|
|
16
|
+
]
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
"""决策树规则提取(Decision Tree Rule Extraction)。
|
|
2
|
+
|
|
3
|
+
使用简单 CART(Classification And Regression Tree)算法,
|
|
4
|
+
从帕雷托前沿成员中提取参数规则。
|
|
5
|
+
|
|
6
|
+
方法:
|
|
7
|
+
1. 将每个数据点标记为 is_frontier (true/false)
|
|
8
|
+
2. 用 Gini 不纯度作为分裂准则训练决策树
|
|
9
|
+
3. 从根到每个叶节点提取规则路径
|
|
10
|
+
4. 每条规则标注覆盖样本数、前沿概率
|
|
11
|
+
|
|
12
|
+
限制:
|
|
13
|
+
- 最大深度:3(避免过拟合)
|
|
14
|
+
- 叶节点最少样本数:5
|
|
15
|
+
- 仅支持数值型参数
|
|
16
|
+
|
|
17
|
+
对应 JS: src/services/optimization/decisionTree.js
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import math
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def gini_impurity(samples: list[dict[str, Any]]) -> float:
|
|
27
|
+
"""计算 Gini 不纯度。
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
samples: 样本数组,每个样本含 is_frontier
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Gini 不纯度(0=纯,0.5=最不纯)
|
|
34
|
+
"""
|
|
35
|
+
if len(samples) == 0:
|
|
36
|
+
return 0
|
|
37
|
+
front_count = sum(1 for s in samples if s["is_frontier"])
|
|
38
|
+
back_count = len(samples) - front_count
|
|
39
|
+
p_front = front_count / len(samples)
|
|
40
|
+
p_back = back_count / len(samples)
|
|
41
|
+
return 1 - p_front * p_front - p_back * p_back
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _is_valid_number(v: Any) -> bool:
|
|
45
|
+
"""判断值是否为有效数字(非 None、非 NaN)。"""
|
|
46
|
+
if v is None:
|
|
47
|
+
return False
|
|
48
|
+
try:
|
|
49
|
+
n = float(v)
|
|
50
|
+
except (TypeError, ValueError):
|
|
51
|
+
return False
|
|
52
|
+
return not math.isnan(n)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def find_best_split_for_param(
|
|
56
|
+
samples: list[dict[str, Any]], param: str
|
|
57
|
+
) -> dict[str, Any] | None:
|
|
58
|
+
"""找到最佳分裂点(单个参数)。
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
samples: 样本数组
|
|
62
|
+
param: 参数名
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
{ threshold, gini, left_samples, right_samples } 或 None
|
|
66
|
+
"""
|
|
67
|
+
if len(samples) < 2:
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
# 提取该参数的所有值并排序
|
|
71
|
+
values = sorted(
|
|
72
|
+
float(s["params"][param])
|
|
73
|
+
for s in samples
|
|
74
|
+
if _is_valid_number(s["params"].get(param))
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
if len(values) < 2:
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
# 候选分裂点:相邻值的中点
|
|
81
|
+
candidates: list[float] = []
|
|
82
|
+
for i in range(len(values) - 1):
|
|
83
|
+
if values[i] != values[i + 1]:
|
|
84
|
+
candidates.append((values[i] + values[i + 1]) / 2)
|
|
85
|
+
|
|
86
|
+
if len(candidates) == 0:
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
best_split: dict[str, Any] | None = None
|
|
90
|
+
best_gini = float("inf")
|
|
91
|
+
|
|
92
|
+
for threshold in candidates:
|
|
93
|
+
left = [s for s in samples if float(s["params"][param]) <= threshold]
|
|
94
|
+
right = [s for s in samples if float(s["params"][param]) > threshold]
|
|
95
|
+
|
|
96
|
+
if len(left) == 0 or len(right) == 0:
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
# 加权 Gini
|
|
100
|
+
gini = (
|
|
101
|
+
len(left) * gini_impurity(left) + len(right) * gini_impurity(right)
|
|
102
|
+
) / len(samples)
|
|
103
|
+
|
|
104
|
+
if gini < best_gini:
|
|
105
|
+
best_gini = gini
|
|
106
|
+
best_split = {
|
|
107
|
+
"threshold": threshold,
|
|
108
|
+
"gini": gini,
|
|
109
|
+
"left_samples": left,
|
|
110
|
+
"right_samples": right,
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return best_split
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def find_best_split(
|
|
117
|
+
samples: list[dict[str, Any]], param_columns: list[str]
|
|
118
|
+
) -> dict[str, Any] | None:
|
|
119
|
+
"""找到所有参数中的最佳分裂。
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
samples: 样本数组
|
|
123
|
+
param_columns: 参数列名
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
{ param, threshold, gini, left_samples, right_samples } 或 None
|
|
127
|
+
"""
|
|
128
|
+
best: dict[str, Any] | None = None
|
|
129
|
+
|
|
130
|
+
for param in param_columns:
|
|
131
|
+
split = find_best_split_for_param(samples, param)
|
|
132
|
+
if split is not None and (best is None or split["gini"] < best["gini"]):
|
|
133
|
+
best = {**split, "param": param}
|
|
134
|
+
|
|
135
|
+
return best
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def build_tree(
|
|
139
|
+
samples: list[dict[str, Any]],
|
|
140
|
+
param_columns: list[str],
|
|
141
|
+
depth: int,
|
|
142
|
+
max_depth: int,
|
|
143
|
+
min_samples_leaf: int,
|
|
144
|
+
conditions: list[dict[str, Any]],
|
|
145
|
+
) -> list[dict[str, Any]]:
|
|
146
|
+
"""递归构建决策树。
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
samples: 当前节点的样本
|
|
150
|
+
param_columns: 参数列名
|
|
151
|
+
depth: 当前深度
|
|
152
|
+
max_depth: 最大深度
|
|
153
|
+
min_samples_leaf: 叶节点最少样本数
|
|
154
|
+
conditions: 从根到当前节点的条件路径
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
叶节点列表(每个叶节点是一个规则)
|
|
158
|
+
"""
|
|
159
|
+
# 终止条件 1:样本太少
|
|
160
|
+
if len(samples) < min_samples_leaf * 2:
|
|
161
|
+
return [{"conditions": list(conditions), "samples": samples}]
|
|
162
|
+
|
|
163
|
+
# 终止条件 2:达到最大深度
|
|
164
|
+
if depth >= max_depth:
|
|
165
|
+
return [{"conditions": list(conditions), "samples": samples}]
|
|
166
|
+
|
|
167
|
+
# 终止条件 3:节点已纯(全是前沿或全非前沿)
|
|
168
|
+
front_count = sum(1 for s in samples if s["is_frontier"])
|
|
169
|
+
if front_count == 0 or front_count == len(samples):
|
|
170
|
+
return [{"conditions": list(conditions), "samples": samples}]
|
|
171
|
+
|
|
172
|
+
# 寻找最佳分裂
|
|
173
|
+
split = find_best_split(samples, param_columns)
|
|
174
|
+
if (
|
|
175
|
+
split is None
|
|
176
|
+
or len(split["left_samples"]) < min_samples_leaf
|
|
177
|
+
or len(split["right_samples"]) < min_samples_leaf
|
|
178
|
+
):
|
|
179
|
+
return [{"conditions": list(conditions), "samples": samples}]
|
|
180
|
+
|
|
181
|
+
# 递归分裂
|
|
182
|
+
left_conditions = list(conditions) + [
|
|
183
|
+
{"param": split["param"], "op": "<=", "value": split["threshold"]}
|
|
184
|
+
]
|
|
185
|
+
right_conditions = list(conditions) + [
|
|
186
|
+
{"param": split["param"], "op": ">", "value": split["threshold"]}
|
|
187
|
+
]
|
|
188
|
+
|
|
189
|
+
left_leaves = build_tree(
|
|
190
|
+
split["left_samples"],
|
|
191
|
+
param_columns,
|
|
192
|
+
depth + 1,
|
|
193
|
+
max_depth,
|
|
194
|
+
min_samples_leaf,
|
|
195
|
+
left_conditions,
|
|
196
|
+
)
|
|
197
|
+
right_leaves = build_tree(
|
|
198
|
+
split["right_samples"],
|
|
199
|
+
param_columns,
|
|
200
|
+
depth + 1,
|
|
201
|
+
max_depth,
|
|
202
|
+
min_samples_leaf,
|
|
203
|
+
right_conditions,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
return left_leaves + right_leaves
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _to_number(v: Any) -> float | None:
|
|
210
|
+
"""将值转为数字,无法转换或为 NaN 时返回 None。"""
|
|
211
|
+
if v is None:
|
|
212
|
+
return None
|
|
213
|
+
try:
|
|
214
|
+
n = float(v)
|
|
215
|
+
except (TypeError, ValueError):
|
|
216
|
+
return None
|
|
217
|
+
if math.isnan(n):
|
|
218
|
+
return None
|
|
219
|
+
return n
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def extract_decision_rules(
|
|
223
|
+
parsed_data: list[dict[str, Any]] | None,
|
|
224
|
+
param_columns: list[str] | None,
|
|
225
|
+
frontier_case_names: set[str] | None,
|
|
226
|
+
options: dict[str, Any] | None = None,
|
|
227
|
+
) -> list[dict[str, Any]]:
|
|
228
|
+
"""提取决策规则。
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
parsed_data: 解析后的数据(含 raw、meta、Case)
|
|
232
|
+
param_columns: 参数列名
|
|
233
|
+
frontier_case_names: 前沿点的 case 名集合
|
|
234
|
+
options: 选项
|
|
235
|
+
- max_depth: 最大树深度,默认 3
|
|
236
|
+
- min_samples_leaf: 叶节点最少样本数,默认 5
|
|
237
|
+
- min_frontier_prob: 最小前沿概率(低于此值视为负向规则),默认 0.1
|
|
238
|
+
- selected_cases: 选中的 case(完整参数化名),空表示全部
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
规则列表 [{ conditions, samples, frontier_prob, is_positive,
|
|
242
|
+
front_count, total_count }]
|
|
243
|
+
"""
|
|
244
|
+
if (
|
|
245
|
+
not parsed_data
|
|
246
|
+
or not param_columns
|
|
247
|
+
or not frontier_case_names
|
|
248
|
+
or len(param_columns) == 0
|
|
249
|
+
):
|
|
250
|
+
return []
|
|
251
|
+
|
|
252
|
+
opts = options or {}
|
|
253
|
+
max_depth = opts.get("max_depth", 3)
|
|
254
|
+
min_samples_leaf = opts.get("min_samples_leaf", 5)
|
|
255
|
+
selected_cases = opts.get("selected_cases")
|
|
256
|
+
|
|
257
|
+
# 过滤数据
|
|
258
|
+
filtered = parsed_data
|
|
259
|
+
if selected_cases and len(selected_cases) > 0:
|
|
260
|
+
filtered = [d for d in parsed_data if d.get("Case") in selected_cases]
|
|
261
|
+
|
|
262
|
+
# 构建样本数组
|
|
263
|
+
samples: list[dict[str, Any]] = []
|
|
264
|
+
for d in filtered:
|
|
265
|
+
meta = d.get("meta") or {}
|
|
266
|
+
params: dict[str, float] = {}
|
|
267
|
+
has_all_params = True
|
|
268
|
+
for col in param_columns:
|
|
269
|
+
v = meta.get(col)
|
|
270
|
+
n = _to_number(v)
|
|
271
|
+
if n is None:
|
|
272
|
+
has_all_params = False
|
|
273
|
+
else:
|
|
274
|
+
params[col] = n
|
|
275
|
+
|
|
276
|
+
if not has_all_params:
|
|
277
|
+
continue
|
|
278
|
+
|
|
279
|
+
case_name = d.get("Case")
|
|
280
|
+
samples.append(
|
|
281
|
+
{
|
|
282
|
+
"case_name": case_name,
|
|
283
|
+
"params": params,
|
|
284
|
+
"is_frontier": case_name in frontier_case_names,
|
|
285
|
+
}
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
if len(samples) < min_samples_leaf * 2:
|
|
289
|
+
return []
|
|
290
|
+
|
|
291
|
+
# 构建决策树
|
|
292
|
+
leaves = build_tree(
|
|
293
|
+
samples, param_columns, 0, max_depth, min_samples_leaf, []
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
# 转换为规则
|
|
297
|
+
rules: list[dict[str, Any]] = []
|
|
298
|
+
for leaf in leaves:
|
|
299
|
+
total_count = len(leaf["samples"])
|
|
300
|
+
front_count = sum(1 for s in leaf["samples"] if s["is_frontier"])
|
|
301
|
+
frontier_prob = front_count / total_count if total_count > 0 else 0
|
|
302
|
+
|
|
303
|
+
rules.append(
|
|
304
|
+
{
|
|
305
|
+
"conditions": leaf["conditions"],
|
|
306
|
+
"samples": leaf["samples"],
|
|
307
|
+
"total_count": total_count,
|
|
308
|
+
"front_count": front_count,
|
|
309
|
+
"frontier_prob": frontier_prob,
|
|
310
|
+
"is_positive": frontier_prob >= 0.5,
|
|
311
|
+
}
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
rules = [rule for rule in rules if rule["total_count"] > 0]
|
|
315
|
+
|
|
316
|
+
# 按前沿概率降序排序(正向规则优先)
|
|
317
|
+
rules.sort(key=lambda r: r["frontier_prob"], reverse=True)
|
|
318
|
+
|
|
319
|
+
return rules
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _format_number(v: float) -> str:
|
|
323
|
+
"""简单的数字格式化(避免依赖外部 utils)。"""
|
|
324
|
+
if v is None or (isinstance(v, float) and math.isnan(v)):
|
|
325
|
+
return "-"
|
|
326
|
+
abs_v = abs(v)
|
|
327
|
+
if abs_v >= 1000:
|
|
328
|
+
return f"{v:.0f}"
|
|
329
|
+
if abs_v >= 1:
|
|
330
|
+
return f"{v:.3f}"
|
|
331
|
+
if abs_v >= 0.01:
|
|
332
|
+
return f"{v:.4f}"
|
|
333
|
+
return f"{v:.2e}"
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def format_rule_conditions(conditions: list[dict[str, Any]] | None) -> str:
|
|
337
|
+
"""格式化规则条件为可读字符串。
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
conditions: 条件数组 [{ param, op, value }]
|
|
341
|
+
|
|
342
|
+
Returns:
|
|
343
|
+
格式化的条件字符串,如 "lr ≤ 0.15 AND momentum > 0.85"
|
|
344
|
+
"""
|
|
345
|
+
if not conditions or len(conditions) == 0:
|
|
346
|
+
return "无条件(全部样本)"
|
|
347
|
+
|
|
348
|
+
parts: list[str] = []
|
|
349
|
+
for c in conditions:
|
|
350
|
+
param_label = c["param"]
|
|
351
|
+
if param_label.startswith("p_"):
|
|
352
|
+
param_label = param_label[2:]
|
|
353
|
+
value = c["value"]
|
|
354
|
+
# 对应 JS Number.isInteger:int 或值为整数的 float 都视为整数
|
|
355
|
+
is_integer = isinstance(value, int) or (
|
|
356
|
+
isinstance(value, float) and value.is_integer()
|
|
357
|
+
)
|
|
358
|
+
if is_integer:
|
|
359
|
+
value_str = str(int(value))
|
|
360
|
+
else:
|
|
361
|
+
value_str = _format_number(value)
|
|
362
|
+
op = "≤" if c["op"] == "<=" else ">"
|
|
363
|
+
parts.append(f"{param_label} {op} {value_str}")
|
|
364
|
+
|
|
365
|
+
return " AND ".join(parts)
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
"""参数重要性分析(Feature Importance)。
|
|
2
|
+
|
|
3
|
+
使用多元线性回归的标准化系数来衡量各参数对指标值的影响程度。
|
|
4
|
+
|
|
5
|
+
方法:
|
|
6
|
+
1. 对参数列做 z-score 标准化(减均值除标准差)
|
|
7
|
+
2. 构建回归模型:metric = β0 + β1·p1 + β2·p2 + ...
|
|
8
|
+
3. 用最小二乘法求解:β = (XᵀX)⁻¹Xᵀy
|
|
9
|
+
4. 标准化系数绝对值 = 参数重要性
|
|
10
|
+
|
|
11
|
+
解读:
|
|
12
|
+
- 系数为正:增大该参数 → 指标值增大
|
|
13
|
+
- 系数为负:增大该参数 → 指标值减小
|
|
14
|
+
- |系数|越大:该参数对指标影响越强
|
|
15
|
+
- R² 越接近 1:模型拟合越好
|
|
16
|
+
|
|
17
|
+
对应 JS: src/services/optimization/featureImportance.js
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import math
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def transpose(m: list[list[float]]) -> list[list[float]]:
|
|
27
|
+
"""矩阵转置。
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
m: 矩阵
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
转置矩阵
|
|
34
|
+
"""
|
|
35
|
+
if not m or len(m) == 0:
|
|
36
|
+
return []
|
|
37
|
+
return [[row[j] for row in m] for j in range(len(m[0]))]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def multiply(a: list[list[float]], b: list[list[float]]) -> list[list[float]]:
|
|
41
|
+
"""矩阵乘法。
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
a: 矩阵 a (n×m)
|
|
45
|
+
b: 矩阵 b (m×p)
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
结果矩阵 (n×p)
|
|
49
|
+
"""
|
|
50
|
+
if not a or not b or len(a) == 0 or len(b) == 0:
|
|
51
|
+
return []
|
|
52
|
+
n = len(a)
|
|
53
|
+
m = len(b)
|
|
54
|
+
p = len(b[0])
|
|
55
|
+
result = [[0.0 for _ in range(p)] for _ in range(n)]
|
|
56
|
+
for i in range(n):
|
|
57
|
+
for k in range(m):
|
|
58
|
+
aik = a[i][k]
|
|
59
|
+
if aik == 0:
|
|
60
|
+
continue
|
|
61
|
+
for j in range(p):
|
|
62
|
+
result[i][j] += aik * b[k][j]
|
|
63
|
+
return result
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def inverse(m: list[list[float]]) -> list[list[float]] | None:
|
|
67
|
+
"""矩阵求逆(高斯-约旦消元法)。
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
m: 方阵
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
逆矩阵,奇异矩阵返回 None
|
|
74
|
+
"""
|
|
75
|
+
n = len(m)
|
|
76
|
+
if n == 0 or len(m[0]) != n:
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
# 构造增广矩阵 [m | I]
|
|
80
|
+
aug: list[list[float]] = []
|
|
81
|
+
for i, row in enumerate(m):
|
|
82
|
+
identity = [0.0] * n
|
|
83
|
+
identity[i] = 1.0
|
|
84
|
+
aug.append(list(row) + identity)
|
|
85
|
+
|
|
86
|
+
# 前向消元 + 后向消元(部分主元选取)
|
|
87
|
+
for col in range(n):
|
|
88
|
+
# 找到当前列绝对值最大的行
|
|
89
|
+
max_row = col
|
|
90
|
+
max_val = abs(aug[col][col])
|
|
91
|
+
for row in range(col + 1, n):
|
|
92
|
+
if abs(aug[row][col]) > max_val:
|
|
93
|
+
max_val = abs(aug[row][col])
|
|
94
|
+
max_row = row
|
|
95
|
+
|
|
96
|
+
if max_val < 1e-12:
|
|
97
|
+
return None # 奇异矩阵
|
|
98
|
+
|
|
99
|
+
# 交换行
|
|
100
|
+
if max_row != col:
|
|
101
|
+
aug[col], aug[max_row] = aug[max_row], aug[col]
|
|
102
|
+
|
|
103
|
+
# 归一化主元行
|
|
104
|
+
pivot = aug[col][col]
|
|
105
|
+
for j in range(2 * n):
|
|
106
|
+
aug[col][j] /= pivot
|
|
107
|
+
|
|
108
|
+
# 消去其他行
|
|
109
|
+
for row in range(n):
|
|
110
|
+
if row == col:
|
|
111
|
+
continue
|
|
112
|
+
factor = aug[row][col]
|
|
113
|
+
if factor == 0:
|
|
114
|
+
continue
|
|
115
|
+
for j in range(2 * n):
|
|
116
|
+
aug[row][j] -= factor * aug[col][j]
|
|
117
|
+
|
|
118
|
+
# 提取逆矩阵
|
|
119
|
+
return [row[n:] for row in aug]
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def mean(arr: list[float]) -> float:
|
|
123
|
+
"""计算均值。"""
|
|
124
|
+
if len(arr) == 0:
|
|
125
|
+
return 0
|
|
126
|
+
return sum(arr) / len(arr)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def std(arr: list[float]) -> float:
|
|
130
|
+
"""计算标准差(总体标准差,ddof=0)。"""
|
|
131
|
+
if len(arr) == 0:
|
|
132
|
+
return 0
|
|
133
|
+
m = mean(arr)
|
|
134
|
+
variance = sum((v - m) ** 2 for v in arr) / len(arr)
|
|
135
|
+
return math.sqrt(variance)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def standardize(values: list[float]) -> list[float]:
|
|
139
|
+
"""Z-score 标准化。
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
values: 数值列表
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
标准化后的值(均值为 0,标准差为 1)
|
|
146
|
+
"""
|
|
147
|
+
m = mean(values)
|
|
148
|
+
s = std(values)
|
|
149
|
+
if s < 1e-12:
|
|
150
|
+
return [0.0 for _ in values] # 常量列
|
|
151
|
+
return [(v - m) / s for v in values]
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def linear_regression(
|
|
155
|
+
X: list[list[float]], y: list[float]
|
|
156
|
+
) -> dict[str, Any] | None:
|
|
157
|
+
"""计算单个指标的多元线性回归(最小二乘法)。
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
X: 设计矩阵 (n×(k+1)),第一列为 1(截距项),后续为标准化参数
|
|
161
|
+
y: 响应变量
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
{ coefficients, r_squared } 或 None
|
|
165
|
+
"""
|
|
166
|
+
n = len(X)
|
|
167
|
+
if n == 0:
|
|
168
|
+
return None
|
|
169
|
+
|
|
170
|
+
Xt = transpose(X)
|
|
171
|
+
XtX = multiply(Xt, X)
|
|
172
|
+
XtX_inv = inverse(XtX)
|
|
173
|
+
if XtX_inv is None:
|
|
174
|
+
return None # 奇异矩阵
|
|
175
|
+
|
|
176
|
+
# y 转为列向量
|
|
177
|
+
y_col = [[v] for v in y]
|
|
178
|
+
Xty = multiply(Xt, y_col)
|
|
179
|
+
beta = multiply(XtX_inv, Xty)
|
|
180
|
+
|
|
181
|
+
coefficients = [row[0] for row in beta]
|
|
182
|
+
|
|
183
|
+
# 计算 R²
|
|
184
|
+
y_mean = mean(y)
|
|
185
|
+
ss_total = 0.0
|
|
186
|
+
ss_residual = 0.0
|
|
187
|
+
for i in range(n):
|
|
188
|
+
y_pred = 0.0
|
|
189
|
+
for j in range(len(coefficients)):
|
|
190
|
+
y_pred += coefficients[j] * X[i][j]
|
|
191
|
+
ss_total += (y[i] - y_mean) ** 2
|
|
192
|
+
ss_residual += (y[i] - y_pred) ** 2
|
|
193
|
+
r_squared = 0 if ss_total < 1e-12 else 1 - ss_residual / ss_total
|
|
194
|
+
|
|
195
|
+
return {"coefficients": coefficients, "r_squared": r_squared}
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _to_number(v: Any) -> float | None:
|
|
199
|
+
"""将值转为数字,无法转换或为 NaN 时返回 None。"""
|
|
200
|
+
if v is None:
|
|
201
|
+
return None
|
|
202
|
+
try:
|
|
203
|
+
n = float(v)
|
|
204
|
+
except (TypeError, ValueError):
|
|
205
|
+
return None
|
|
206
|
+
if math.isnan(n):
|
|
207
|
+
return None
|
|
208
|
+
return n
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def compute_feature_importance(
|
|
212
|
+
parsed_data: list[dict[str, Any]] | None,
|
|
213
|
+
param_columns: list[str] | None,
|
|
214
|
+
metrics: list[str] | None,
|
|
215
|
+
algo: str | None,
|
|
216
|
+
selected_cases: set[str] | None = None,
|
|
217
|
+
) -> dict[str, Any]:
|
|
218
|
+
"""计算参数重要性。
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
parsed_data: 解析后的数据(含 raw、meta)
|
|
222
|
+
param_columns: 参数列名
|
|
223
|
+
metrics: 要分析的指标列表
|
|
224
|
+
algo: 算法名
|
|
225
|
+
selected_cases: 可选,选中的 case 名(完整参数化名),空表示全部
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
{ [metric]: { params, r_squared, sample_count } }
|
|
229
|
+
"""
|
|
230
|
+
if not parsed_data or not param_columns or not metrics or not algo:
|
|
231
|
+
return {}
|
|
232
|
+
|
|
233
|
+
# 过滤数据
|
|
234
|
+
filtered = parsed_data
|
|
235
|
+
if selected_cases and len(selected_cases) > 0:
|
|
236
|
+
filtered = [d for d in parsed_data if d.get("Case") in selected_cases]
|
|
237
|
+
|
|
238
|
+
if len(filtered) < 2 or len(param_columns) == 0:
|
|
239
|
+
return {}
|
|
240
|
+
|
|
241
|
+
result: dict[str, Any] = {}
|
|
242
|
+
|
|
243
|
+
for metric in metrics:
|
|
244
|
+
# 收集样本:(参数值数组, 指标值)
|
|
245
|
+
samples: list[dict[str, Any]] = []
|
|
246
|
+
for d in filtered:
|
|
247
|
+
raw = d.get("raw") or {}
|
|
248
|
+
metric_raw = raw.get(metric) or {}
|
|
249
|
+
y = metric_raw.get(algo)
|
|
250
|
+
y_num = _to_number(y)
|
|
251
|
+
if y_num is None:
|
|
252
|
+
continue
|
|
253
|
+
|
|
254
|
+
meta = d.get("meta") or {}
|
|
255
|
+
params: list[float | None] = []
|
|
256
|
+
for col in param_columns:
|
|
257
|
+
v = meta.get(col)
|
|
258
|
+
n = _to_number(v)
|
|
259
|
+
params.append(n)
|
|
260
|
+
|
|
261
|
+
# 任意参数缺失则跳过
|
|
262
|
+
if any(p is None for p in params):
|
|
263
|
+
continue
|
|
264
|
+
|
|
265
|
+
samples.append({"params": params, "y": y_num})
|
|
266
|
+
|
|
267
|
+
if len(samples) < len(param_columns) + 1:
|
|
268
|
+
# 样本不足
|
|
269
|
+
result[metric] = {
|
|
270
|
+
"params": [
|
|
271
|
+
{"name": name, "coef": 0, "importance": 0, "direction": "none"}
|
|
272
|
+
for name in param_columns
|
|
273
|
+
],
|
|
274
|
+
"r_squared": 0,
|
|
275
|
+
"sample_count": len(samples),
|
|
276
|
+
"error": "样本不足",
|
|
277
|
+
}
|
|
278
|
+
continue
|
|
279
|
+
|
|
280
|
+
# 标准化每个参数列
|
|
281
|
+
param_arrays = [
|
|
282
|
+
[s["params"][j] for s in samples] for j in range(len(param_columns))
|
|
283
|
+
]
|
|
284
|
+
standardized_params = [standardize(col) for col in param_arrays]
|
|
285
|
+
|
|
286
|
+
# 构建设计矩阵 X:第一列 1(截距),后续为标准化参数
|
|
287
|
+
X: list[list[float]] = []
|
|
288
|
+
for i in range(len(samples)):
|
|
289
|
+
row = [1.0]
|
|
290
|
+
for j in range(len(param_columns)):
|
|
291
|
+
row.append(standardized_params[j][i])
|
|
292
|
+
X.append(row)
|
|
293
|
+
|
|
294
|
+
y = [s["y"] for s in samples]
|
|
295
|
+
|
|
296
|
+
regression = linear_regression(X, y)
|
|
297
|
+
if regression is None:
|
|
298
|
+
result[metric] = {
|
|
299
|
+
"params": [
|
|
300
|
+
{"name": name, "coef": 0, "importance": 0, "direction": "none"}
|
|
301
|
+
for name in param_columns
|
|
302
|
+
],
|
|
303
|
+
"r_squared": 0,
|
|
304
|
+
"sample_count": len(samples),
|
|
305
|
+
"error": "矩阵奇异",
|
|
306
|
+
}
|
|
307
|
+
continue
|
|
308
|
+
|
|
309
|
+
# coefficients[0] 是截距,后续是各参数的标准化系数
|
|
310
|
+
coefficients = regression["coefficients"]
|
|
311
|
+
r_squared = regression["r_squared"]
|
|
312
|
+
params = []
|
|
313
|
+
for j, name in enumerate(param_columns):
|
|
314
|
+
coef = coefficients[j + 1]
|
|
315
|
+
params.append(
|
|
316
|
+
{
|
|
317
|
+
"name": name,
|
|
318
|
+
"coef": coef,
|
|
319
|
+
"importance": abs(coef),
|
|
320
|
+
"direction": (
|
|
321
|
+
"positive" if coef > 0 else ("negative" if coef < 0 else "none")
|
|
322
|
+
),
|
|
323
|
+
}
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
# 按重要性降序排序
|
|
327
|
+
params.sort(key=lambda p: p["importance"], reverse=True)
|
|
328
|
+
|
|
329
|
+
result[metric] = {
|
|
330
|
+
"params": params,
|
|
331
|
+
"r_squared": max(0, min(1, r_squared)),
|
|
332
|
+
"sample_count": len(samples),
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return result
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def get_overall_param_ranking(
|
|
339
|
+
importance_result: dict[str, Any] | None,
|
|
340
|
+
) -> list[dict[str, Any]]:
|
|
341
|
+
"""获取所有指标中最重要的参数排名(综合排名)。
|
|
342
|
+
|
|
343
|
+
Args:
|
|
344
|
+
importance_result: compute_feature_importance 的返回值
|
|
345
|
+
|
|
346
|
+
Returns:
|
|
347
|
+
[{ name, avg_importance, rank }] 按平均重要性降序
|
|
348
|
+
"""
|
|
349
|
+
if not importance_result:
|
|
350
|
+
return []
|
|
351
|
+
|
|
352
|
+
param_stats: dict[str, dict[str, float]] = {}
|
|
353
|
+
|
|
354
|
+
for metric_data in importance_result.values():
|
|
355
|
+
if not metric_data or not metric_data.get("params"):
|
|
356
|
+
continue
|
|
357
|
+
for p in metric_data["params"]:
|
|
358
|
+
name = p["name"]
|
|
359
|
+
if name not in param_stats:
|
|
360
|
+
param_stats[name] = {"sum": 0.0, "count": 0}
|
|
361
|
+
param_stats[name]["sum"] += p["importance"]
|
|
362
|
+
param_stats[name]["count"] += 1
|
|
363
|
+
|
|
364
|
+
ranking = [
|
|
365
|
+
{"name": name, "avg_importance": stats["sum"] / stats["count"]}
|
|
366
|
+
for name, stats in param_stats.items()
|
|
367
|
+
]
|
|
368
|
+
|
|
369
|
+
ranking.sort(key=lambda p: p["avg_importance"], reverse=True)
|
|
370
|
+
for i, p in enumerate(ranking):
|
|
371
|
+
p["rank"] = i + 1
|
|
372
|
+
|
|
373
|
+
return ranking
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""拐点检测(Knee Point Detection)。
|
|
2
|
+
|
|
3
|
+
在帕雷托前沿上找到"拐点"——即在此点附近,某目标的轻微改进
|
|
4
|
+
需要另一目标大幅退化。拐点通常是决策者偏好的自然折中解。
|
|
5
|
+
|
|
6
|
+
方法:最大距离法
|
|
7
|
+
1. 将前沿点按 X 排序
|
|
8
|
+
2. 连接两个极端点(X 最小和 X 最大)成一条线
|
|
9
|
+
3. 找到离这条线最远的前沿点 = knee point
|
|
10
|
+
|
|
11
|
+
对应 JS: src/services/optimization/kneePoint.js
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import math
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def point_to_line_distance(
|
|
21
|
+
px: float,
|
|
22
|
+
py: float,
|
|
23
|
+
x1: float,
|
|
24
|
+
y1: float,
|
|
25
|
+
x2: float,
|
|
26
|
+
y2: float,
|
|
27
|
+
) -> float:
|
|
28
|
+
"""计算点到线段的垂直距离。
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
px: 点 X
|
|
32
|
+
py: 点 Y
|
|
33
|
+
x1: 线段起点 X
|
|
34
|
+
y1: 线段起点 Y
|
|
35
|
+
x2: 线段终点 X
|
|
36
|
+
y2: 线段终点 Y
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
垂直距离
|
|
40
|
+
"""
|
|
41
|
+
dx = x2 - x1
|
|
42
|
+
dy = y2 - y1
|
|
43
|
+
length_sq = dx * dx + dy * dy
|
|
44
|
+
if length_sq == 0:
|
|
45
|
+
# 线段退化为点
|
|
46
|
+
ddx = px - x1
|
|
47
|
+
ddy = py - y1
|
|
48
|
+
return math.sqrt(ddx * ddx + ddy * ddy)
|
|
49
|
+
# 垂直距离 = |(y2-y1)*x0 - (x2-x1)*y0 + x2*y1 - y2*x1| / sqrt((y2-y1)^2 + (x2-x1)^2)
|
|
50
|
+
numerator = abs(dy * px - dx * py + x2 * y1 - y2 * x1)
|
|
51
|
+
return numerator / math.sqrt(length_sq)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def detect_knee_point(
|
|
55
|
+
frontier_points: list[dict[str, Any]] | None,
|
|
56
|
+
options: dict[str, Any] | None = None,
|
|
57
|
+
) -> dict[str, Any] | None:
|
|
58
|
+
"""检测帕雷托前沿的拐点。
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
frontier_points: 前沿点数组,每个点包含 { x, y, caseName, raw, algo }
|
|
62
|
+
options: 选项
|
|
63
|
+
- x_reversed: X 轴是否反转(higher is better)
|
|
64
|
+
- y_reversed: Y 轴是否反转(higher is better)
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
拐点信息 { point, index, distance, extremes, sorted_frontier } 或 None
|
|
68
|
+
"""
|
|
69
|
+
if not frontier_points or len(frontier_points) < 3:
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
opts = options or {}
|
|
73
|
+
x_reversed = opts.get("x_reversed", False)
|
|
74
|
+
# y_reversed 在 JS 中存在但未影响本算法逻辑,保持参数一致性
|
|
75
|
+
# y_reversed = opts.get("y_reversed", False)
|
|
76
|
+
|
|
77
|
+
# 按 X 排序(考虑方向:如果反转,则降序)
|
|
78
|
+
sorted_points = sorted(
|
|
79
|
+
frontier_points,
|
|
80
|
+
key=lambda p: p["x"],
|
|
81
|
+
reverse=x_reversed,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# 两个极端点
|
|
85
|
+
first = sorted_points[0]
|
|
86
|
+
last = sorted_points[-1]
|
|
87
|
+
|
|
88
|
+
# 计算每个点到极端点连线的距离
|
|
89
|
+
max_distance = -1.0
|
|
90
|
+
knee_index = -1
|
|
91
|
+
knee_point = None
|
|
92
|
+
|
|
93
|
+
for i, p in enumerate(sorted_points):
|
|
94
|
+
dist = point_to_line_distance(
|
|
95
|
+
p["x"], p["y"], first["x"], first["y"], last["x"], last["y"]
|
|
96
|
+
)
|
|
97
|
+
if dist > max_distance:
|
|
98
|
+
max_distance = dist
|
|
99
|
+
knee_index = i
|
|
100
|
+
knee_point = p
|
|
101
|
+
|
|
102
|
+
if not knee_point or max_distance <= 0:
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
"point": knee_point,
|
|
107
|
+
"index": knee_index,
|
|
108
|
+
"distance": max_distance,
|
|
109
|
+
# 前沿的两端点信息
|
|
110
|
+
"extremes": {"first": first, "last": last},
|
|
111
|
+
# 排序后的前沿(用于参考)
|
|
112
|
+
"sorted_frontier": sorted_points,
|
|
113
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"""响应面方法(Response Surface Methodology, RSM)。
|
|
2
|
+
|
|
3
|
+
通过二次多项式回归拟合参数与指标的关系曲面,
|
|
4
|
+
并在参数空间中寻找最优参数组合。
|
|
5
|
+
|
|
6
|
+
模型:y = β0 + β1·x1 + β2·x2 + β11·x1² + β22·x2² + β12·x1·x2
|
|
7
|
+
|
|
8
|
+
步骤:
|
|
9
|
+
1. 收集样本 (x1, x2, y)
|
|
10
|
+
2. 构建设计矩阵并求解最小二乘
|
|
11
|
+
3. 在参数空间生成网格,预测响应值
|
|
12
|
+
4. 根据指标方向(lower/higher)寻找极值点
|
|
13
|
+
|
|
14
|
+
对应 JS: src/services/optimization/responseSurface.js
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import math
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
# 复用 feature_importance 中的矩阵运算和重要性分析(避免重复代码)
|
|
23
|
+
from algobench.optimization.feature_importance import (
|
|
24
|
+
compute_feature_importance,
|
|
25
|
+
get_overall_param_ranking,
|
|
26
|
+
inverse,
|
|
27
|
+
multiply,
|
|
28
|
+
transpose,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def build_design_row(x1: float, x2: float) -> list[float]:
|
|
33
|
+
"""构建二次响应面设计矩阵的一行。
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
[1, x1, x2, x1², x2², x1·x2]
|
|
37
|
+
"""
|
|
38
|
+
return [1.0, x1, x2, x1 * x1, x2 * x2, x1 * x2]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def predict_response(
|
|
42
|
+
coefficients: list[float] | None, x1: float, x2: float
|
|
43
|
+
) -> float:
|
|
44
|
+
"""预测给定参数下的响应值。
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
coefficients: 回归系数 [β0, β1, β2, β11, β22, β12]
|
|
48
|
+
x1: 参数 A 值
|
|
49
|
+
x2: 参数 B 值
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
预测值
|
|
53
|
+
"""
|
|
54
|
+
if not coefficients or len(coefficients) < 6:
|
|
55
|
+
return 0
|
|
56
|
+
row = build_design_row(x1, x2)
|
|
57
|
+
y = 0.0
|
|
58
|
+
for i in range(len(coefficients)):
|
|
59
|
+
y += coefficients[i] * row[i]
|
|
60
|
+
return y
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _to_number(v: Any) -> float | None:
|
|
64
|
+
"""将值转为数字,无法转换或为 NaN 时返回 None。"""
|
|
65
|
+
if v is None:
|
|
66
|
+
return None
|
|
67
|
+
try:
|
|
68
|
+
n = float(v)
|
|
69
|
+
except (TypeError, ValueError):
|
|
70
|
+
return None
|
|
71
|
+
if math.isnan(n):
|
|
72
|
+
return None
|
|
73
|
+
return n
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def fit_response_surface(
|
|
77
|
+
parsed_data: list[dict[str, Any]] | None,
|
|
78
|
+
param_a: str | None,
|
|
79
|
+
param_b: str | None,
|
|
80
|
+
metric: str | None,
|
|
81
|
+
algo: str | None,
|
|
82
|
+
direction: str,
|
|
83
|
+
selected_cases: set[str] | None = None,
|
|
84
|
+
) -> dict[str, Any] | None:
|
|
85
|
+
"""拟合响应面。
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
parsed_data: 解析后的数据
|
|
89
|
+
param_a: 参数 A 列名
|
|
90
|
+
param_b: 参数 B 列名
|
|
91
|
+
metric: 指标名
|
|
92
|
+
algo: 算法名
|
|
93
|
+
direction: 指标方向 'lower' | 'higher'
|
|
94
|
+
selected_cases: 选中的 case
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
{ coefficients, r_squared, optimal_point, grid_data, param_ranges,
|
|
98
|
+
sample_count }
|
|
99
|
+
"""
|
|
100
|
+
if not parsed_data or not param_a or not param_b or not metric or not algo:
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
# 过滤数据
|
|
104
|
+
filtered = parsed_data
|
|
105
|
+
if selected_cases and len(selected_cases) > 0:
|
|
106
|
+
filtered = [d for d in parsed_data if d.get("Case") in selected_cases]
|
|
107
|
+
|
|
108
|
+
# 收集样本
|
|
109
|
+
samples: list[dict[str, float]] = []
|
|
110
|
+
for d in filtered:
|
|
111
|
+
raw = d.get("raw") or {}
|
|
112
|
+
metric_raw = raw.get(metric) or {}
|
|
113
|
+
y = _to_number(metric_raw.get(algo))
|
|
114
|
+
if y is None:
|
|
115
|
+
continue
|
|
116
|
+
|
|
117
|
+
meta = d.get("meta") or {}
|
|
118
|
+
x1 = _to_number(meta.get(param_a))
|
|
119
|
+
x2 = _to_number(meta.get(param_b))
|
|
120
|
+
if x1 is None or x2 is None:
|
|
121
|
+
continue
|
|
122
|
+
|
|
123
|
+
samples.append({"x1": x1, "x2": x2, "y": y})
|
|
124
|
+
|
|
125
|
+
# 二次模型有 6 个未知数,需 ≥ 7 个样本
|
|
126
|
+
if len(samples) < 7:
|
|
127
|
+
return {
|
|
128
|
+
"coefficients": None,
|
|
129
|
+
"r_squared": 0,
|
|
130
|
+
"optimal_point": None,
|
|
131
|
+
"grid_data": [],
|
|
132
|
+
"param_ranges": None,
|
|
133
|
+
"sample_count": len(samples),
|
|
134
|
+
"error": "样本不足(需 ≥ 7 个)",
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
# 构建设计矩阵 X 和响应向量 y
|
|
138
|
+
X = [build_design_row(s["x1"], s["x2"]) for s in samples]
|
|
139
|
+
y = [s["y"] for s in samples]
|
|
140
|
+
|
|
141
|
+
# 最小二乘:β = (XᵀX)⁻¹Xᵀy
|
|
142
|
+
Xt = transpose(X)
|
|
143
|
+
XtX = multiply(Xt, X)
|
|
144
|
+
XtX_inv = inverse(XtX)
|
|
145
|
+
|
|
146
|
+
if XtX_inv is None:
|
|
147
|
+
return {
|
|
148
|
+
"coefficients": None,
|
|
149
|
+
"r_squared": 0,
|
|
150
|
+
"optimal_point": None,
|
|
151
|
+
"grid_data": [],
|
|
152
|
+
"param_ranges": None,
|
|
153
|
+
"sample_count": len(samples),
|
|
154
|
+
"error": "矩阵奇异(参数可能共线性)",
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
y_col = [[v] for v in y]
|
|
158
|
+
Xty = multiply(Xt, y_col)
|
|
159
|
+
beta = multiply(XtX_inv, Xty)
|
|
160
|
+
coefficients = [row[0] for row in beta]
|
|
161
|
+
|
|
162
|
+
# 计算 R²
|
|
163
|
+
y_mean = sum(y) / len(y)
|
|
164
|
+
ss_total = 0.0
|
|
165
|
+
ss_residual = 0.0
|
|
166
|
+
for i in range(len(samples)):
|
|
167
|
+
y_pred = predict_response(coefficients, samples[i]["x1"], samples[i]["x2"])
|
|
168
|
+
ss_total += (y[i] - y_mean) ** 2
|
|
169
|
+
ss_residual += (y[i] - y_pred) ** 2
|
|
170
|
+
r_squared = 0 if ss_total < 1e-12 else 1 - ss_residual / ss_total
|
|
171
|
+
|
|
172
|
+
# 参数范围
|
|
173
|
+
x1_vals = [s["x1"] for s in samples]
|
|
174
|
+
x2_vals = [s["x2"] for s in samples]
|
|
175
|
+
x1_min = min(x1_vals)
|
|
176
|
+
x1_max = max(x1_vals)
|
|
177
|
+
x2_min = min(x2_vals)
|
|
178
|
+
x2_max = max(x2_vals)
|
|
179
|
+
param_ranges = {
|
|
180
|
+
"param_a": {"name": param_a, "min": x1_min, "max": x1_max},
|
|
181
|
+
"param_b": {"name": param_b, "min": x2_min, "max": x2_max},
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
# 生成网格数据(20×20)
|
|
185
|
+
grid_size = 20
|
|
186
|
+
grid_data: list[dict[str, float]] = []
|
|
187
|
+
x1_step = (x1_max - x1_min) / (grid_size - 1)
|
|
188
|
+
x2_step = (x2_max - x2_min) / (grid_size - 1)
|
|
189
|
+
|
|
190
|
+
optimal_point: dict[str, float] | None = None
|
|
191
|
+
optimal_value = float("-inf") if direction == "higher" else float("inf")
|
|
192
|
+
|
|
193
|
+
for i in range(grid_size):
|
|
194
|
+
for j in range(grid_size):
|
|
195
|
+
x1 = x1_min + i * x1_step
|
|
196
|
+
x2 = x2_min + j * x2_step
|
|
197
|
+
z = predict_response(coefficients, x1, x2)
|
|
198
|
+
grid_data.append({"x": x1, "y": x2, "z": z})
|
|
199
|
+
|
|
200
|
+
# 寻找极值
|
|
201
|
+
if direction == "higher":
|
|
202
|
+
if z > optimal_value:
|
|
203
|
+
optimal_value = z
|
|
204
|
+
optimal_point = {"x": x1, "y": x2, "value": z}
|
|
205
|
+
else:
|
|
206
|
+
if z < optimal_value:
|
|
207
|
+
optimal_value = z
|
|
208
|
+
optimal_point = {"x": x1, "y": x2, "value": z}
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
"coefficients": coefficients,
|
|
212
|
+
"r_squared": max(0, min(1, r_squared)),
|
|
213
|
+
"optimal_point": optimal_point,
|
|
214
|
+
"grid_data": grid_data,
|
|
215
|
+
"param_ranges": param_ranges,
|
|
216
|
+
"sample_count": len(samples),
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def select_top_params(
|
|
221
|
+
parsed_data: list[dict[str, Any]] | None,
|
|
222
|
+
param_columns: list[str] | None,
|
|
223
|
+
metrics: list[str] | None,
|
|
224
|
+
algo: str | None,
|
|
225
|
+
selected_cases: set[str] | None = None,
|
|
226
|
+
) -> dict[str, str] | None:
|
|
227
|
+
"""自动选择最重要的 2 个参数。
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
parsed_data: 解析后的数据
|
|
231
|
+
param_columns: 参数列名
|
|
232
|
+
metrics: 指标列表
|
|
233
|
+
algo: 算法名
|
|
234
|
+
selected_cases: 选中的 case
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
{ param_a, param_b } 或 None
|
|
238
|
+
"""
|
|
239
|
+
if not param_columns or len(param_columns) < 2:
|
|
240
|
+
return None
|
|
241
|
+
|
|
242
|
+
# 如果只有 2 个参数,直接使用
|
|
243
|
+
if len(param_columns) == 2:
|
|
244
|
+
return {"param_a": param_columns[0], "param_b": param_columns[1]}
|
|
245
|
+
|
|
246
|
+
# 使用 feature_importance 计算重要性排名
|
|
247
|
+
importance = compute_feature_importance(
|
|
248
|
+
parsed_data, param_columns, metrics, algo, selected_cases
|
|
249
|
+
)
|
|
250
|
+
ranking = get_overall_param_ranking(importance)
|
|
251
|
+
|
|
252
|
+
if len(ranking) < 2:
|
|
253
|
+
return None
|
|
254
|
+
|
|
255
|
+
return {"param_a": ranking[0]["name"], "param_b": ranking[1]["name"]}
|
|
@@ -12,6 +12,11 @@ algobench/config/business.py
|
|
|
12
12
|
algobench/config/metric_keywords.py
|
|
13
13
|
algobench/config/metrics.py
|
|
14
14
|
algobench/config/thresholds.py
|
|
15
|
+
algobench/optimization/__init__.py
|
|
16
|
+
algobench/optimization/decision_tree.py
|
|
17
|
+
algobench/optimization/feature_importance.py
|
|
18
|
+
algobench/optimization/knee_point.py
|
|
19
|
+
algobench/optimization/response_surface.py
|
|
15
20
|
algobench/parsers/__init__.py
|
|
16
21
|
algobench/parsers/csv_parser.py
|
|
17
22
|
algobench/stats/__init__.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|