FigCompose 0.1.1__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.
- data_visuals/__init__.py +18 -0
- data_visuals/api.py +159 -0
- data_visuals/charts/__init__.py +340 -0
- data_visuals/charts/bar.py +367 -0
- data_visuals/charts/box.py +180 -0
- data_visuals/charts/confusion.py +125 -0
- data_visuals/charts/heatmap.py +181 -0
- data_visuals/charts/histogram.py +120 -0
- data_visuals/charts/line.py +160 -0
- data_visuals/charts/pie.py +129 -0
- data_visuals/charts/radar.py +95 -0
- data_visuals/charts/scatter.py +153 -0
- data_visuals/compute/__init__.py +59 -0
- data_visuals/compute/aggregation.py +174 -0
- data_visuals/compute/preprocess.py +113 -0
- data_visuals/compute/statistics.py +396 -0
- data_visuals/core/__init__.py +37 -0
- data_visuals/core/canvas.py +67 -0
- data_visuals/core/config.py +227 -0
- data_visuals/core/facets.py +37 -0
- data_visuals/core/figure.py +1295 -0
- data_visuals/core/identity.py +197 -0
- data_visuals/core/legend.py +130 -0
- data_visuals/core/metadata.py +159 -0
- data_visuals/core/panel.py +197 -0
- data_visuals/core/quality.py +191 -0
- data_visuals/core/regression.py +370 -0
- data_visuals/core/spec.py +278 -0
- data_visuals/data/__init__.py +80 -0
- data_visuals/data/labels.py +126 -0
- data_visuals/data/loader.py +139 -0
- data_visuals/data/schema.py +305 -0
- data_visuals/data/transform.py +286 -0
- data_visuals/layout/__init__.py +106 -0
- data_visuals/layout/auto.py +295 -0
- data_visuals/layout/grid.py +562 -0
- data_visuals/layout/manager.py +694 -0
- data_visuals/layout/panels.py +36 -0
- data_visuals/layout/physical.py +344 -0
- data_visuals/layout/placement.py +107 -0
- data_visuals/layout/shared.py +156 -0
- data_visuals/layout/sizing.py +402 -0
- data_visuals/renderer/__init__.py +5 -0
- data_visuals/renderer/export.py +84 -0
- data_visuals/renderer/mpl/__init__.py +23 -0
- data_visuals/renderer/mpl/annotations.py +104 -0
- data_visuals/renderer/mpl/axes.py +176 -0
- data_visuals/renderer/mpl/canvas.py +1085 -0
- data_visuals/renderer/mpl/charts/__init__.py +7 -0
- data_visuals/renderer/mpl/charts/bar.py +105 -0
- data_visuals/renderer/mpl/charts/box.py +101 -0
- data_visuals/renderer/mpl/charts/confusion.py +30 -0
- data_visuals/renderer/mpl/charts/heatmap.py +26 -0
- data_visuals/renderer/mpl/charts/helpers.py +228 -0
- data_visuals/renderer/mpl/charts/histogram.py +58 -0
- data_visuals/renderer/mpl/charts/line.py +93 -0
- data_visuals/renderer/mpl/charts/pie.py +101 -0
- data_visuals/renderer/mpl/charts/radar.py +75 -0
- data_visuals/renderer/mpl/charts/registry.py +41 -0
- data_visuals/renderer/mpl/charts/scatter.py +291 -0
- data_visuals/renderer/mpl/config.py +281 -0
- data_visuals/renderer/mpl/custom.py +256 -0
- data_visuals/renderer/mpl/export.py +73 -0
- data_visuals/renderer/mpl/facets.py +141 -0
- data_visuals/renderer/mpl/legends.py +311 -0
- data_visuals/renderer/mpl/quality.py +552 -0
- data_visuals/renderer/mpl/styling.py +56 -0
- data_visuals/themes/__init__.py +131 -0
- data_visuals/themes/general.json +121 -0
- data_visuals/themes/modern.json +122 -0
- data_visuals/utils/__init__.py +34 -0
- data_visuals/utils/colors.py +402 -0
- figcompose-0.1.1.dist-info/METADATA +364 -0
- figcompose-0.1.1.dist-info/RECORD +77 -0
- figcompose-0.1.1.dist-info/WHEEL +5 -0
- figcompose-0.1.1.dist-info/licenses/LICENSE +202 -0
- figcompose-0.1.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
"""
|
|
2
|
+
charts/bar.py
|
|
3
|
+
=============
|
|
4
|
+
柱状图定义 — 声明柱状图所需的数据字段和计算需求。
|
|
5
|
+
|
|
6
|
+
设计原则:
|
|
7
|
+
- 只定义"柱状图需要 x/y/error/group/hline/mean_line 字段"
|
|
8
|
+
- 不负责渲染(属于 renderer/)
|
|
9
|
+
- 不负责颜色(属于 themes/)
|
|
10
|
+
- 不做实际计算(属于 compute/)
|
|
11
|
+
|
|
12
|
+
支持的变体:
|
|
13
|
+
- 单变量柱状图: x + y
|
|
14
|
+
- 带 error bar: x + y + error
|
|
15
|
+
- 分组柱状图: x + y + group
|
|
16
|
+
- 分组 + error: x + y + group + error
|
|
17
|
+
- 堆叠柱状图: x + y + group + stacked=True
|
|
18
|
+
- 百分比堆叠: x + y + group + stacked="percent"
|
|
19
|
+
- 多列宽表堆叠: x + y=["col1","col2","col3"] + stacked=True
|
|
20
|
+
- 多列百分比堆叠: x + y=["col1","col2","col3"] + stacked="percent"
|
|
21
|
+
- 固定基准线: x + y + hline=0.85
|
|
22
|
+
- 整体均值线: x + y + mean_line=True
|
|
23
|
+
- 水平柱状图: horizontal=True
|
|
24
|
+
- 双轴 (右轴 y2): x + y + y2="f1"
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
from dataclasses import dataclass, field
|
|
30
|
+
from copy import copy
|
|
31
|
+
from typing import Optional, Dict, List, Any, ClassVar, Union, Literal
|
|
32
|
+
|
|
33
|
+
from data_visuals.charts import ChartSpec, register_chart
|
|
34
|
+
|
|
35
|
+
# ── 类型别名 ─────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
StackedMode = Union[bool, Literal["percent"]]
|
|
38
|
+
YInput = Union[str, List[str]]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@register_chart
|
|
42
|
+
@dataclass
|
|
43
|
+
class Bar(ChartSpec):
|
|
44
|
+
"""
|
|
45
|
+
柱状图规格。
|
|
46
|
+
|
|
47
|
+
── 数据映射 ──
|
|
48
|
+
x — 分类轴(必需)
|
|
49
|
+
y — 数值轴(必需)→ 左轴。
|
|
50
|
+
单列 str 或 多列 List[str](宽表堆叠时)
|
|
51
|
+
y2 — 右轴列名(可选,启用双 y 轴)
|
|
52
|
+
error — 误差列名(可选,对应 std/sem 列)
|
|
53
|
+
group — 分组列名(可选)
|
|
54
|
+
有 group + stacked=False → 分组并排
|
|
55
|
+
有 group + stacked=True → 常规堆叠
|
|
56
|
+
|
|
57
|
+
── 选项 ──
|
|
58
|
+
horizontal — 是否水平柱状图,默认 False
|
|
59
|
+
stacked — False: 并排分组 | True: 数值堆叠 | "percent": 百分比堆叠
|
|
60
|
+
hline — 固定基准线 y 值(可选,如 SOTA 水平线)
|
|
61
|
+
hline_label — 基准线图例标签
|
|
62
|
+
mean_line — 是否显示整体均值线
|
|
63
|
+
mean_line_label — 均值线图例标签
|
|
64
|
+
|
|
65
|
+
Examples
|
|
66
|
+
--------
|
|
67
|
+
>>> # 基础
|
|
68
|
+
>>> Bar(x="method", y="accuracy")
|
|
69
|
+
>>>
|
|
70
|
+
>>> # 分组并排
|
|
71
|
+
>>> Bar(x="method", y="accuracy", group="dataset")
|
|
72
|
+
>>>
|
|
73
|
+
>>> # 堆叠(长表 group 方式)
|
|
74
|
+
>>> Bar(x="method", y="accuracy", group="dataset", stacked=True)
|
|
75
|
+
>>>
|
|
76
|
+
>>> # 百分比堆叠
|
|
77
|
+
>>> Bar(x="method", y="accuracy", group="dataset", stacked="percent")
|
|
78
|
+
>>>
|
|
79
|
+
>>> # 宽表多列堆叠
|
|
80
|
+
>>> Bar(x="method", y=["accuracy", "precision", "recall"], stacked=True)
|
|
81
|
+
>>>
|
|
82
|
+
>>> # 基准线 + 均值线
|
|
83
|
+
>>> Bar(x="method", y="accuracy", hline=0.85, hline_label="SOTA", mean_line=True)
|
|
84
|
+
>>>
|
|
85
|
+
>>> # 双轴 + 基准线
|
|
86
|
+
>>> Bar(x="method", y="accuracy", y2="f1", hline=0.90)
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
chart_type: ClassVar[str] = "bar"
|
|
90
|
+
|
|
91
|
+
required_fields: ClassVar[List[str]] = ["x", "y"]
|
|
92
|
+
optional_fields: ClassVar[Dict[str, Optional[str]]] = {
|
|
93
|
+
"error": None,
|
|
94
|
+
"group": None,
|
|
95
|
+
"y2": None,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
# ── 实例字段 ──────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
horizontal: bool = False
|
|
101
|
+
stacked: StackedMode = False
|
|
102
|
+
|
|
103
|
+
# 参考线
|
|
104
|
+
hline: Optional[float] = None
|
|
105
|
+
hline_label: str = ""
|
|
106
|
+
mean_line: bool = False
|
|
107
|
+
mean_line_label: str = "Mean"
|
|
108
|
+
show_value: bool = False
|
|
109
|
+
value_format: str = ".2f"
|
|
110
|
+
|
|
111
|
+
# ── 属性(从 mapping 提取) ─────────────────────────
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def x(self) -> Optional[str]:
|
|
115
|
+
return self.mapping.get("x")
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def y(self) -> Optional[YInput]:
|
|
119
|
+
return self.mapping.get("y")
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def y_list(self) -> List[str]:
|
|
123
|
+
"""将 y 统一为列表形式。"""
|
|
124
|
+
val = self.y
|
|
125
|
+
if val is None:
|
|
126
|
+
return []
|
|
127
|
+
if isinstance(val, str):
|
|
128
|
+
return [val]
|
|
129
|
+
return list(val)
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def is_multi_y(self) -> bool:
|
|
133
|
+
"""是否为多列 y(宽表堆叠模式)。"""
|
|
134
|
+
return len(self.y_list) > 1
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
def y2(self) -> Optional[str]:
|
|
138
|
+
return self.mapping.get("y2")
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def error(self) -> Optional[str]:
|
|
142
|
+
return self.mapping.get("error")
|
|
143
|
+
|
|
144
|
+
@property
|
|
145
|
+
def group(self) -> Optional[str]:
|
|
146
|
+
return self.mapping.get("group")
|
|
147
|
+
|
|
148
|
+
# ── 快捷判断 ──────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
@property
|
|
151
|
+
def is_stacked(self) -> bool:
|
|
152
|
+
"""是否堆叠(含常规堆叠和百分比堆叠)。"""
|
|
153
|
+
return self.stacked is not False
|
|
154
|
+
|
|
155
|
+
@property
|
|
156
|
+
def is_percent_stacked(self) -> bool:
|
|
157
|
+
"""是否为百分比堆叠。"""
|
|
158
|
+
return self.stacked == "percent"
|
|
159
|
+
|
|
160
|
+
@property
|
|
161
|
+
def is_grouped(self) -> bool:
|
|
162
|
+
"""是否为分组并排(有 group 且非堆叠)。"""
|
|
163
|
+
return bool(self.group) and not self.is_stacked
|
|
164
|
+
|
|
165
|
+
@property
|
|
166
|
+
def has_reference_lines(self) -> bool:
|
|
167
|
+
"""是否有参考线(hline 或 mean_line)。"""
|
|
168
|
+
return self.hline is not None or self.mean_line
|
|
169
|
+
|
|
170
|
+
# ── 工厂方法 ──────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
@classmethod
|
|
173
|
+
def from_mapping(
|
|
174
|
+
cls,
|
|
175
|
+
x: str,
|
|
176
|
+
y: YInput,
|
|
177
|
+
*,
|
|
178
|
+
y2: Optional[str] = None,
|
|
179
|
+
error: Optional[str] = None,
|
|
180
|
+
group: Optional[str] = None,
|
|
181
|
+
title: Optional[str] = None,
|
|
182
|
+
horizontal: bool = False,
|
|
183
|
+
stacked: StackedMode = False,
|
|
184
|
+
hline: Optional[float] = None,
|
|
185
|
+
hline_label: str = "",
|
|
186
|
+
mean_line: bool = False,
|
|
187
|
+
mean_line_label: str = "Mean",
|
|
188
|
+
show_value: bool = False,
|
|
189
|
+
value_format: str = ".2f",
|
|
190
|
+
) -> "Bar":
|
|
191
|
+
"""
|
|
192
|
+
快捷构造柱状图。
|
|
193
|
+
|
|
194
|
+
Parameters
|
|
195
|
+
----------
|
|
196
|
+
x : str
|
|
197
|
+
分类轴列名
|
|
198
|
+
y : str | list[str]
|
|
199
|
+
数值轴列名 → 左轴。单列或多列(宽表堆叠)
|
|
200
|
+
y2 : str | None
|
|
201
|
+
右轴列名(双 y 轴)
|
|
202
|
+
error : str | None
|
|
203
|
+
误差列名
|
|
204
|
+
group : str | None
|
|
205
|
+
分组列名
|
|
206
|
+
title : str | None
|
|
207
|
+
子图标题
|
|
208
|
+
horizontal : bool
|
|
209
|
+
水平柱状图
|
|
210
|
+
stacked : bool | "percent"
|
|
211
|
+
堆叠模式
|
|
212
|
+
hline : float | None
|
|
213
|
+
固定基准线 y 值
|
|
214
|
+
hline_label : str
|
|
215
|
+
基准线标签
|
|
216
|
+
mean_line : bool
|
|
217
|
+
显示整体均值线
|
|
218
|
+
mean_line_label : str
|
|
219
|
+
均值线标签
|
|
220
|
+
|
|
221
|
+
Returns
|
|
222
|
+
-------
|
|
223
|
+
Bar
|
|
224
|
+
"""
|
|
225
|
+
mapping: Dict[str, Any] = {"x": x, "y": y}
|
|
226
|
+
if y2:
|
|
227
|
+
mapping["y2"] = y2
|
|
228
|
+
if error:
|
|
229
|
+
mapping["error"] = error
|
|
230
|
+
if group:
|
|
231
|
+
mapping["group"] = group
|
|
232
|
+
|
|
233
|
+
return cls(
|
|
234
|
+
mapping=mapping,
|
|
235
|
+
title=title,
|
|
236
|
+
horizontal=horizontal,
|
|
237
|
+
stacked=stacked,
|
|
238
|
+
hline=hline,
|
|
239
|
+
hline_label=hline_label,
|
|
240
|
+
mean_line=mean_line,
|
|
241
|
+
mean_line_label=mean_line_label,
|
|
242
|
+
show_value=show_value,
|
|
243
|
+
value_format=value_format,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
# ── 校验 ──────────────────────────────────────────────
|
|
247
|
+
|
|
248
|
+
def validate(self, df: "pd.DataFrame") -> List[str]: # type: ignore[override]
|
|
249
|
+
"""
|
|
250
|
+
列存在性校验(委托基类) + 柱状图特有语义校验。
|
|
251
|
+
"""
|
|
252
|
+
validation_chart = self
|
|
253
|
+
if self.error == "std" and "std" not in df.columns:
|
|
254
|
+
# "std" may request uncertainty computed from repeated y samples.
|
|
255
|
+
validation_chart = copy(self)
|
|
256
|
+
validation_chart.mapping = dict(self.mapping)
|
|
257
|
+
validation_chart.mapping.pop("error")
|
|
258
|
+
errors = ChartSpec.validate(validation_chart, df)
|
|
259
|
+
|
|
260
|
+
if self.group and self.is_multi_y:
|
|
261
|
+
errors.append(
|
|
262
|
+
"[bar] 不能同时指定 group 和多列 y。"
|
|
263
|
+
"group 模式使用长表数据(单列 y),多列 y 模式使用宽表数据。"
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
if self.is_percent_stacked:
|
|
267
|
+
if not self.group and not self.is_multi_y:
|
|
268
|
+
errors.append(
|
|
269
|
+
"[bar] 百分比堆叠需要 group 或多列 y 来提供分段依据。"
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
return errors
|
|
273
|
+
|
|
274
|
+
# ── 计算需求 ──────────────────────────────────────────
|
|
275
|
+
|
|
276
|
+
def compute_requirements(self) -> Dict[str, Any]:
|
|
277
|
+
"""
|
|
278
|
+
柱状图的计算需求。
|
|
279
|
+
|
|
280
|
+
规则:
|
|
281
|
+
┌─────────────────────┬──────────────────────────────────┐
|
|
282
|
+
│ 条件 │ 需求 │
|
|
283
|
+
├─────────────────────┼──────────────────────────────────┤
|
|
284
|
+
│ 基础 │ groupby(x) → mean(y) │
|
|
285
|
+
│ + group │ groupby(x, group) → mean(y) │
|
|
286
|
+
│ + 多列 y │ groupby(x) → mean(y1,y2,y3) │
|
|
287
|
+
│ + error │ 额外 std(y) │
|
|
288
|
+
│ + y2 │ 额外 mean(y2) │
|
|
289
|
+
│ + mean_line │ statistics: overall_mean(y...) │
|
|
290
|
+
│ + stacked="percent" │ preprocess: normalize_percent │
|
|
291
|
+
│ + hline │ 不需要计算 │
|
|
292
|
+
└─────────────────────┴──────────────────────────────────┘
|
|
293
|
+
"""
|
|
294
|
+
group_by: List[str] = [self.x] if self.x else []
|
|
295
|
+
if self.group:
|
|
296
|
+
group_by = [self.x, self.group] # type: ignore[list-item]
|
|
297
|
+
|
|
298
|
+
columns: Dict[str, List[str]] = {}
|
|
299
|
+
|
|
300
|
+
# 所有 y 列(多列时每个都是一段)
|
|
301
|
+
for y_col in self.y_list:
|
|
302
|
+
columns[y_col] = ["mean"]
|
|
303
|
+
|
|
304
|
+
# 双轴 y2
|
|
305
|
+
if self.y2:
|
|
306
|
+
columns[self.y2] = ["mean"]
|
|
307
|
+
|
|
308
|
+
# error: 对 y 列追加 std
|
|
309
|
+
if self.error:
|
|
310
|
+
for y_col in self.y_list:
|
|
311
|
+
if y_col in columns and "std" not in columns[y_col]:
|
|
312
|
+
columns[y_col].append("std")
|
|
313
|
+
|
|
314
|
+
preprocess: List[str] = []
|
|
315
|
+
if self.is_percent_stacked:
|
|
316
|
+
preprocess.append("normalize_percent")
|
|
317
|
+
|
|
318
|
+
statistics: List[str] = []
|
|
319
|
+
if self.mean_line:
|
|
320
|
+
statistics.append("overall_mean")
|
|
321
|
+
|
|
322
|
+
# hline 不需要计算 — renderer 直接画线
|
|
323
|
+
|
|
324
|
+
return {
|
|
325
|
+
"group_by": group_by,
|
|
326
|
+
"columns": columns,
|
|
327
|
+
"preprocess": preprocess,
|
|
328
|
+
"statistics": statistics,
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
# ── 选项导出 ──────────────────────────────────────────
|
|
332
|
+
|
|
333
|
+
def _chart_options(self) -> Dict[str, Any]:
|
|
334
|
+
"""导出柱状图特有的渲染选项(仅非默认值)。"""
|
|
335
|
+
opts: Dict[str, Any] = {}
|
|
336
|
+
|
|
337
|
+
if self.horizontal:
|
|
338
|
+
opts["horizontal"] = True
|
|
339
|
+
|
|
340
|
+
if self.is_percent_stacked:
|
|
341
|
+
opts["stacked"] = "percent"
|
|
342
|
+
elif self.stacked is True:
|
|
343
|
+
opts["stacked"] = True
|
|
344
|
+
|
|
345
|
+
if self.hline is not None:
|
|
346
|
+
opts["hline"] = self.hline
|
|
347
|
+
if self.hline_label:
|
|
348
|
+
opts["hline_label"] = self.hline_label
|
|
349
|
+
|
|
350
|
+
if self.mean_line:
|
|
351
|
+
opts["mean_line"] = True
|
|
352
|
+
if self.mean_line_label and self.mean_line_label != "Mean":
|
|
353
|
+
opts["mean_line_label"] = self.mean_line_label
|
|
354
|
+
|
|
355
|
+
if self.show_value:
|
|
356
|
+
opts["show_value"] = True
|
|
357
|
+
if self.value_format and self.value_format != ".2f":
|
|
358
|
+
opts["value_format"] = self.value_format
|
|
359
|
+
|
|
360
|
+
return opts
|
|
361
|
+
|
|
362
|
+
def to_spec(self) -> Dict[str, Any]:
|
|
363
|
+
spec = super().to_spec()
|
|
364
|
+
# 确保多列 y 以 list 形式进入 spec(基类用 mapping.copy 浅复制,引用不变)
|
|
365
|
+
if self.is_multi_y:
|
|
366
|
+
spec["data"]["y"] = self.y_list
|
|
367
|
+
return spec
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""
|
|
2
|
+
charts/box.py
|
|
3
|
+
=============
|
|
4
|
+
箱线图定义 — 声明箱线图所需的数据字段和计算需求。
|
|
5
|
+
|
|
6
|
+
设计原则:
|
|
7
|
+
- 只定义"箱线图需要 x/y/group 字段"
|
|
8
|
+
- 不负责渲染(属于 renderer/)
|
|
9
|
+
- 不负责颜色(属于 themes/)
|
|
10
|
+
- 不负责实际统计计算(属于 compute/)
|
|
11
|
+
|
|
12
|
+
箱线图的统计需求:
|
|
13
|
+
groupby(x) → Q1, median, Q3, IQR, outlier detection
|
|
14
|
+
这是所有图表类型中计算需求最复杂的。
|
|
15
|
+
|
|
16
|
+
支持的变体:
|
|
17
|
+
- 单变量箱线图: x + y
|
|
18
|
+
- 分组箱线图: x + y + group
|
|
19
|
+
- 水平箱线图: horizontal=True
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from dataclasses import dataclass, field
|
|
25
|
+
from typing import Optional, Dict, List, Any, ClassVar
|
|
26
|
+
|
|
27
|
+
from data_visuals.charts import ChartSpec, register_chart
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@register_chart
|
|
31
|
+
@dataclass
|
|
32
|
+
class Box(ChartSpec):
|
|
33
|
+
"""
|
|
34
|
+
箱线图规格。
|
|
35
|
+
|
|
36
|
+
数据映射:
|
|
37
|
+
x — 分类轴(必需)
|
|
38
|
+
y — 数值轴(必需)
|
|
39
|
+
group — 分组列名(可选,用于分组箱线图)
|
|
40
|
+
|
|
41
|
+
选项:
|
|
42
|
+
horizontal — 是否水平箱线图,默认 False
|
|
43
|
+
show_outliers — 是否显示异常值点,默认 True
|
|
44
|
+
notch — 是否显示凹槽(median 置信区间),默认 False
|
|
45
|
+
|
|
46
|
+
Examples
|
|
47
|
+
--------
|
|
48
|
+
>>> box = Box(x="method", y="accuracy")
|
|
49
|
+
>>> box.compute_requirements()
|
|
50
|
+
{
|
|
51
|
+
'group_by': ['method'],
|
|
52
|
+
'columns': {},
|
|
53
|
+
'preprocess': [],
|
|
54
|
+
'statistics': ['quartiles', 'iqr', 'outliers']
|
|
55
|
+
}
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
chart_type: ClassVar[str] = "box"
|
|
59
|
+
|
|
60
|
+
required_fields: ClassVar[List[str]] = ["x", "y"]
|
|
61
|
+
optional_fields: ClassVar[Dict[str, Optional[str]]] = {
|
|
62
|
+
"group": None,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
# ── 箱线图特有选项 ──
|
|
66
|
+
|
|
67
|
+
horizontal: bool = False
|
|
68
|
+
show_outliers: bool = True
|
|
69
|
+
notch: bool = False # 中位数凹槽
|
|
70
|
+
mean_line: bool = False
|
|
71
|
+
mean_line_label: str = "Mean"
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def x(self) -> Optional[str]:
|
|
75
|
+
return self.mapping.get("x")
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def y(self) -> Optional[str]:
|
|
79
|
+
return self.mapping.get("y")
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def group(self) -> Optional[str]:
|
|
83
|
+
return self.mapping.get("group")
|
|
84
|
+
|
|
85
|
+
# ── 工厂方法 ──────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
@classmethod
|
|
88
|
+
def from_mapping(
|
|
89
|
+
cls,
|
|
90
|
+
x: str,
|
|
91
|
+
y: str,
|
|
92
|
+
*,
|
|
93
|
+
group: Optional[str] = None,
|
|
94
|
+
title: Optional[str] = None,
|
|
95
|
+
horizontal: bool = False,
|
|
96
|
+
show_outliers: bool = True,
|
|
97
|
+
notch: bool = False,
|
|
98
|
+
mean_line: bool = False,
|
|
99
|
+
mean_line_label: str = "Mean",
|
|
100
|
+
) -> "Box":
|
|
101
|
+
"""
|
|
102
|
+
快捷构造箱线图。
|
|
103
|
+
|
|
104
|
+
Parameters
|
|
105
|
+
----------
|
|
106
|
+
x : str
|
|
107
|
+
分类轴列名
|
|
108
|
+
y : str
|
|
109
|
+
数值轴列名
|
|
110
|
+
group : str | None
|
|
111
|
+
分组列名
|
|
112
|
+
title : str | None
|
|
113
|
+
子图标题
|
|
114
|
+
horizontal : bool
|
|
115
|
+
水平箱线图
|
|
116
|
+
show_outliers : bool
|
|
117
|
+
显示异常值
|
|
118
|
+
notch : bool
|
|
119
|
+
中位数凹槽
|
|
120
|
+
|
|
121
|
+
Returns
|
|
122
|
+
-------
|
|
123
|
+
Box
|
|
124
|
+
"""
|
|
125
|
+
mapping: Dict[str, Any] = {"x": x, "y": y}
|
|
126
|
+
if group:
|
|
127
|
+
mapping["group"] = group
|
|
128
|
+
|
|
129
|
+
return cls(
|
|
130
|
+
mapping=mapping,
|
|
131
|
+
title=title,
|
|
132
|
+
horizontal=horizontal,
|
|
133
|
+
show_outliers=show_outliers,
|
|
134
|
+
notch=notch,
|
|
135
|
+
mean_line=mean_line,
|
|
136
|
+
mean_line_label=mean_line_label,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# ── 计算需求 ──────────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
def compute_requirements(self) -> Dict[str, Any]:
|
|
142
|
+
"""
|
|
143
|
+
箱线图的计算需求。
|
|
144
|
+
|
|
145
|
+
箱线图需要分组统计量而非简单聚合:
|
|
146
|
+
- groupby(x) → Q1, median, Q3, IQR
|
|
147
|
+
- 异常值检测 (IQR × 1.5 法则)
|
|
148
|
+
- 有 group 时 → groupby(x, group) → 同上
|
|
149
|
+
|
|
150
|
+
这些统计量由 compute/statistics.py 提供,
|
|
151
|
+
compute/aggregation.py 做分组聚合。
|
|
152
|
+
"""
|
|
153
|
+
group_by: List[str] = [self.x] if self.x else []
|
|
154
|
+
if self.group:
|
|
155
|
+
group_by = [self.x, self.group] # type: ignore[list-item]
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
"group_by": group_by,
|
|
159
|
+
"columns": {}, # box 需要分组后逐组计算统计量,不是标准 agg
|
|
160
|
+
"preprocess": [],
|
|
161
|
+
# 告知下游需要哪些统计量
|
|
162
|
+
"statistics": ["quartiles", "iqr"],
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
# ── 选项导出 ──────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
def _chart_options(self) -> Dict[str, Any]:
|
|
168
|
+
"""导出箱线图特有的选项(仅非默认值)。"""
|
|
169
|
+
opts: Dict[str, Any] = {}
|
|
170
|
+
if self.horizontal:
|
|
171
|
+
opts["horizontal"] = True
|
|
172
|
+
if not self.show_outliers:
|
|
173
|
+
opts["show_outliers"] = False
|
|
174
|
+
if self.notch:
|
|
175
|
+
opts["notch"] = True
|
|
176
|
+
if self.mean_line:
|
|
177
|
+
opts["mean_line"] = True
|
|
178
|
+
if self.mean_line_label:
|
|
179
|
+
opts["mean_line_label"] = self.mean_line_label
|
|
180
|
+
return opts
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""
|
|
2
|
+
charts/confusion.py
|
|
3
|
+
===================
|
|
4
|
+
混淆矩阵定义 — 分类模型评估专用。
|
|
5
|
+
|
|
6
|
+
设计原则:
|
|
7
|
+
- 只定义"confusion matrix 需要 x/y/value 字段"
|
|
8
|
+
- 不负责渲染(属于 renderer/)
|
|
9
|
+
- 不负责颜色(属于 themes/)
|
|
10
|
+
|
|
11
|
+
数据格式:
|
|
12
|
+
长表: true_label, pred_label, count
|
|
13
|
+
系统自动 groupby(true_label, pred_label) → count
|
|
14
|
+
|
|
15
|
+
支持的变体:
|
|
16
|
+
- 计数混淆矩阵: x + y (+ value column)
|
|
17
|
+
- 归一化混淆矩阵: x + y + normalize=True
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from dataclasses import dataclass, field
|
|
23
|
+
from typing import Optional, Dict, List, Any, ClassVar
|
|
24
|
+
|
|
25
|
+
from data_visuals.charts import ChartSpec, register_chart
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@register_chart
|
|
29
|
+
@dataclass
|
|
30
|
+
class Confusion(ChartSpec):
|
|
31
|
+
"""
|
|
32
|
+
混淆矩阵规格。
|
|
33
|
+
|
|
34
|
+
数据映射:
|
|
35
|
+
x — true label 列名(必需)
|
|
36
|
+
y — pred label 列名(必需)
|
|
37
|
+
value — 计数值列名(可选,无则自动 groupby count)
|
|
38
|
+
|
|
39
|
+
选项:
|
|
40
|
+
normalize — True: 行归一化 (0~1), False: 原始计数
|
|
41
|
+
annotate — 是否在单元格显示数值
|
|
42
|
+
fmt — 数值格式
|
|
43
|
+
|
|
44
|
+
Examples
|
|
45
|
+
--------
|
|
46
|
+
>>> Confusion(x="true_label", y="pred_label")
|
|
47
|
+
>>> Confusion(x="true_label", y="pred_label", normalize=True)
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
chart_type: ClassVar[str] = "confusion"
|
|
51
|
+
|
|
52
|
+
required_fields: ClassVar[List[str]] = ["x", "y"]
|
|
53
|
+
optional_fields: ClassVar[Dict[str, Optional[str]]] = {
|
|
54
|
+
"value": None,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# ── 实例字段 ──
|
|
58
|
+
|
|
59
|
+
normalize: bool = False
|
|
60
|
+
annotate: bool = True
|
|
61
|
+
fmt: str = ".0f"
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def x(self) -> Optional[str]:
|
|
65
|
+
return self.mapping.get("x")
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def y(self) -> Optional[str]:
|
|
69
|
+
return self.mapping.get("y")
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def value(self) -> Optional[str]:
|
|
73
|
+
return self.mapping.get("value")
|
|
74
|
+
|
|
75
|
+
@classmethod
|
|
76
|
+
def from_mapping(
|
|
77
|
+
cls,
|
|
78
|
+
x: str,
|
|
79
|
+
y: str,
|
|
80
|
+
*,
|
|
81
|
+
value: Optional[str] = None,
|
|
82
|
+
title: Optional[str] = None,
|
|
83
|
+
normalize: bool = False,
|
|
84
|
+
annotate: bool = True,
|
|
85
|
+
fmt: str = ".0f",
|
|
86
|
+
) -> "Confusion":
|
|
87
|
+
mapping: Dict[str, Any] = {"x": x, "y": y}
|
|
88
|
+
if value:
|
|
89
|
+
mapping["value"] = value
|
|
90
|
+
return cls(
|
|
91
|
+
mapping=mapping,
|
|
92
|
+
title=title,
|
|
93
|
+
normalize=normalize,
|
|
94
|
+
annotate=annotate,
|
|
95
|
+
fmt=fmt,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
def compute_requirements(self) -> Dict[str, Any]:
|
|
99
|
+
group_by: List[str] = []
|
|
100
|
+
if self.x:
|
|
101
|
+
group_by.append(self.x)
|
|
102
|
+
if self.y:
|
|
103
|
+
group_by.append(self.y)
|
|
104
|
+
# Explicit counts are additive; without a value column count raw rows.
|
|
105
|
+
columns: Dict[str, List[str]] = {}
|
|
106
|
+
if self.value and group_by:
|
|
107
|
+
columns[self.value] = ["sum"]
|
|
108
|
+
elif self.x and group_by:
|
|
109
|
+
columns[self.x] = ["count"]
|
|
110
|
+
return {
|
|
111
|
+
"group_by": group_by,
|
|
112
|
+
"columns": columns,
|
|
113
|
+
"preprocess": [],
|
|
114
|
+
"statistics": [],
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
def _chart_options(self) -> Dict[str, Any]:
|
|
118
|
+
opts: Dict[str, Any] = {}
|
|
119
|
+
if self.normalize:
|
|
120
|
+
opts["normalize"] = True
|
|
121
|
+
if not self.annotate:
|
|
122
|
+
opts["annotate"] = False
|
|
123
|
+
if self.fmt != ".0f":
|
|
124
|
+
opts["fmt"] = self.fmt
|
|
125
|
+
return opts
|