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.
Files changed (43) hide show
  1. {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/PKG-INFO +1 -1
  2. {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/algobench/__init__.py +1 -1
  3. algobench_sdk-2.0.4/algobench/optimization/__init__.py +16 -0
  4. algobench_sdk-2.0.4/algobench/optimization/decision_tree.py +365 -0
  5. algobench_sdk-2.0.4/algobench/optimization/feature_importance.py +373 -0
  6. algobench_sdk-2.0.4/algobench/optimization/knee_point.py +113 -0
  7. algobench_sdk-2.0.4/algobench/optimization/response_surface.py +255 -0
  8. {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/algobench_sdk.egg-info/PKG-INFO +1 -1
  9. {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/algobench_sdk.egg-info/SOURCES.txt +5 -0
  10. {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/pyproject.toml +1 -1
  11. {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/README.md +0 -0
  12. {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/algobench/__main__.py +0 -0
  13. {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/algobench/api.py +0 -0
  14. {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/algobench/cli.py +0 -0
  15. {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/algobench/config/__init__.py +0 -0
  16. {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/algobench/config/analysis_criteria.py +0 -0
  17. {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/algobench/config/business.py +0 -0
  18. {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/algobench/config/metric_keywords.py +0 -0
  19. {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/algobench/config/metrics.py +0 -0
  20. {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/algobench/config/thresholds.py +0 -0
  21. {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/algobench/exceptions.py +0 -0
  22. {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/algobench/models.py +0 -0
  23. {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/algobench/parsers/__init__.py +0 -0
  24. {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/algobench/parsers/csv_parser.py +0 -0
  25. {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/algobench/stats/__init__.py +0 -0
  26. {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/algobench/stats/core.py +0 -0
  27. {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/algobench/stats/diagnosis.py +0 -0
  28. {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/algobench/stats/qvalue.py +0 -0
  29. {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/algobench/stats/sample_processing.py +0 -0
  30. {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/algobench/stats/tests/__init__.py +0 -0
  31. {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/algobench/stats/tests/data_type.py +0 -0
  32. {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/algobench/stats/tests/effect_size.py +0 -0
  33. {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/algobench/stats/tests/nonparametric.py +0 -0
  34. {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/algobench/stats/tests/smart.py +0 -0
  35. {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/algobench/stats/tests/t_tests.py +0 -0
  36. {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/algobench/utils/__init__.py +0 -0
  37. {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/algobench/utils/improvement.py +0 -0
  38. {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/algobench/utils/math_utils.py +0 -0
  39. {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/algobench/utils/numbers.py +0 -0
  40. {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/algobench_sdk.egg-info/dependency_links.txt +0 -0
  41. {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/algobench_sdk.egg-info/requires.txt +0 -0
  42. {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/algobench_sdk.egg-info/top_level.txt +0 -0
  43. {algobench_sdk-2.0.2 → algobench_sdk-2.0.4}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: algobench-sdk
3
- Version: 2.0.2
3
+ Version: 2.0.4
4
4
  Summary: AlgoBench - 算法基准测试分析工具的 Python SDK 与 CLI
5
5
  Author: DuanxiangLiu
6
6
  License-Expression: AGPL-3.0-only
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- __version__ = "2.0.2"
5
+ __version__ = "2.0.4"
6
6
 
7
7
  from algobench.api import analyze, compute_statistics_only as compute_statistics
8
8
  from algobench.models import (
@@ -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"]}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: algobench-sdk
3
- Version: 2.0.2
3
+ Version: 2.0.4
4
4
  Summary: AlgoBench - 算法基准测试分析工具的 Python SDK 与 CLI
5
5
  Author: DuanxiangLiu
6
6
  License-Expression: AGPL-3.0-only
@@ -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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "algobench-sdk"
7
- version = "2.0.2"
7
+ version = "2.0.4"
8
8
  description = "AlgoBench - 算法基准测试分析工具的 Python SDK 与 CLI"
9
9
  readme = "README.md"
10
10
  license = "AGPL-3.0-only"
File without changes
File without changes