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.
Files changed (77) hide show
  1. data_visuals/__init__.py +18 -0
  2. data_visuals/api.py +159 -0
  3. data_visuals/charts/__init__.py +340 -0
  4. data_visuals/charts/bar.py +367 -0
  5. data_visuals/charts/box.py +180 -0
  6. data_visuals/charts/confusion.py +125 -0
  7. data_visuals/charts/heatmap.py +181 -0
  8. data_visuals/charts/histogram.py +120 -0
  9. data_visuals/charts/line.py +160 -0
  10. data_visuals/charts/pie.py +129 -0
  11. data_visuals/charts/radar.py +95 -0
  12. data_visuals/charts/scatter.py +153 -0
  13. data_visuals/compute/__init__.py +59 -0
  14. data_visuals/compute/aggregation.py +174 -0
  15. data_visuals/compute/preprocess.py +113 -0
  16. data_visuals/compute/statistics.py +396 -0
  17. data_visuals/core/__init__.py +37 -0
  18. data_visuals/core/canvas.py +67 -0
  19. data_visuals/core/config.py +227 -0
  20. data_visuals/core/facets.py +37 -0
  21. data_visuals/core/figure.py +1295 -0
  22. data_visuals/core/identity.py +197 -0
  23. data_visuals/core/legend.py +130 -0
  24. data_visuals/core/metadata.py +159 -0
  25. data_visuals/core/panel.py +197 -0
  26. data_visuals/core/quality.py +191 -0
  27. data_visuals/core/regression.py +370 -0
  28. data_visuals/core/spec.py +278 -0
  29. data_visuals/data/__init__.py +80 -0
  30. data_visuals/data/labels.py +126 -0
  31. data_visuals/data/loader.py +139 -0
  32. data_visuals/data/schema.py +305 -0
  33. data_visuals/data/transform.py +286 -0
  34. data_visuals/layout/__init__.py +106 -0
  35. data_visuals/layout/auto.py +295 -0
  36. data_visuals/layout/grid.py +562 -0
  37. data_visuals/layout/manager.py +694 -0
  38. data_visuals/layout/panels.py +36 -0
  39. data_visuals/layout/physical.py +344 -0
  40. data_visuals/layout/placement.py +107 -0
  41. data_visuals/layout/shared.py +156 -0
  42. data_visuals/layout/sizing.py +402 -0
  43. data_visuals/renderer/__init__.py +5 -0
  44. data_visuals/renderer/export.py +84 -0
  45. data_visuals/renderer/mpl/__init__.py +23 -0
  46. data_visuals/renderer/mpl/annotations.py +104 -0
  47. data_visuals/renderer/mpl/axes.py +176 -0
  48. data_visuals/renderer/mpl/canvas.py +1085 -0
  49. data_visuals/renderer/mpl/charts/__init__.py +7 -0
  50. data_visuals/renderer/mpl/charts/bar.py +105 -0
  51. data_visuals/renderer/mpl/charts/box.py +101 -0
  52. data_visuals/renderer/mpl/charts/confusion.py +30 -0
  53. data_visuals/renderer/mpl/charts/heatmap.py +26 -0
  54. data_visuals/renderer/mpl/charts/helpers.py +228 -0
  55. data_visuals/renderer/mpl/charts/histogram.py +58 -0
  56. data_visuals/renderer/mpl/charts/line.py +93 -0
  57. data_visuals/renderer/mpl/charts/pie.py +101 -0
  58. data_visuals/renderer/mpl/charts/radar.py +75 -0
  59. data_visuals/renderer/mpl/charts/registry.py +41 -0
  60. data_visuals/renderer/mpl/charts/scatter.py +291 -0
  61. data_visuals/renderer/mpl/config.py +281 -0
  62. data_visuals/renderer/mpl/custom.py +256 -0
  63. data_visuals/renderer/mpl/export.py +73 -0
  64. data_visuals/renderer/mpl/facets.py +141 -0
  65. data_visuals/renderer/mpl/legends.py +311 -0
  66. data_visuals/renderer/mpl/quality.py +552 -0
  67. data_visuals/renderer/mpl/styling.py +56 -0
  68. data_visuals/themes/__init__.py +131 -0
  69. data_visuals/themes/general.json +121 -0
  70. data_visuals/themes/modern.json +122 -0
  71. data_visuals/utils/__init__.py +34 -0
  72. data_visuals/utils/colors.py +402 -0
  73. figcompose-0.1.1.dist-info/METADATA +364 -0
  74. figcompose-0.1.1.dist-info/RECORD +77 -0
  75. figcompose-0.1.1.dist-info/WHEEL +5 -0
  76. figcompose-0.1.1.dist-info/licenses/LICENSE +202 -0
  77. 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