kalfa 3.0.0.dev0__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 (148) hide show
  1. kalfa/__init__.py +19 -0
  2. kalfa/api.py +284 -0
  3. kalfa/board/__init__.py +1 -0
  4. kalfa/board/answers.py +419 -0
  5. kalfa/board/server.py +536 -0
  6. kalfa/board/static/api.js +176 -0
  7. kalfa/board/static/app.js +122 -0
  8. kalfa/board/static/board.css +228 -0
  9. kalfa/board/static/charts.css +46 -0
  10. kalfa/board/static/components/badge.js +14 -0
  11. kalfa/board/static/components/chart.js +204 -0
  12. kalfa/board/static/components/diagram.js +857 -0
  13. kalfa/board/static/components/error.js +17 -0
  14. kalfa/board/static/components/loading.js +7 -0
  15. kalfa/board/static/components/progress.js +25 -0
  16. kalfa/board/static/components/settings.js +418 -0
  17. kalfa/board/static/components/spark.js +43 -0
  18. kalfa/board/static/components/table.js +156 -0
  19. kalfa/board/static/diagram.css +76 -0
  20. kalfa/board/static/format.js +138 -0
  21. kalfa/board/static/index.html +34 -0
  22. kalfa/board/static/plot.js +594 -0
  23. kalfa/board/static/predictions.css +20 -0
  24. kalfa/board/static/store.js +215 -0
  25. kalfa/board/static/views/compare.js +185 -0
  26. kalfa/board/static/views/home.js +90 -0
  27. kalfa/board/static/views/record/config.js +81 -0
  28. kalfa/board/static/views/record/curves.js +137 -0
  29. kalfa/board/static/views/record/data.js +378 -0
  30. kalfa/board/static/views/record/files.js +128 -0
  31. kalfa/board/static/views/record/log.js +164 -0
  32. kalfa/board/static/views/record/model.js +286 -0
  33. kalfa/board/static/views/record/overview.js +216 -0
  34. kalfa/board/static/views/record/plots.js +103 -0
  35. kalfa/board/static/views/record/predictions.js +834 -0
  36. kalfa/board/static/views/record.js +179 -0
  37. kalfa/board/static/views/runs.js +124 -0
  38. kalfa/board/static/views/sweep.js +282 -0
  39. kalfa/board/watch.py +87 -0
  40. kalfa/cli.py +371 -0
  41. kalfa/commands/__init__.py +0 -0
  42. kalfa/commands/common.py +8 -0
  43. kalfa/commands/export.py +117 -0
  44. kalfa/commands/generate.py +31 -0
  45. kalfa/commands/guide/__init__.py +101 -0
  46. kalfa/commands/guide/about.py +93 -0
  47. kalfa/commands/guide/calls.py +102 -0
  48. kalfa/commands/guide/sections.py +186 -0
  49. kalfa/commands/guide/seen.py +307 -0
  50. kalfa/commands/guide/show.py +106 -0
  51. kalfa/commands/guide/training.py +333 -0
  52. kalfa/commands/guide/words.py +97 -0
  53. kalfa/commands/legos.py +508 -0
  54. kalfa/commands/plots.py +27 -0
  55. kalfa/commands/report/__init__.py +64 -0
  56. kalfa/commands/report/page.py +112 -0
  57. kalfa/commands/report/predictions.py +216 -0
  58. kalfa/commands/report/sections.py +356 -0
  59. kalfa/commands/stop.py +60 -0
  60. kalfa/commands/sweep/__init__.py +202 -0
  61. kalfa/commands/sweep/checks.py +98 -0
  62. kalfa/commands/sweep/plan.py +183 -0
  63. kalfa/commands/sweep/points.py +123 -0
  64. kalfa/commands/sweep/runner.py +100 -0
  65. kalfa/commands/sweep/space.py +67 -0
  66. kalfa/commands/sweep/strategies.py +228 -0
  67. kalfa/core/__init__.py +0 -0
  68. kalfa/core/aliases.py +229 -0
  69. kalfa/core/build.py +727 -0
  70. kalfa/core/check.py +550 -0
  71. kalfa/core/coerce.py +36 -0
  72. kalfa/core/context.py +149 -0
  73. kalfa/core/device.py +63 -0
  74. kalfa/core/errors.py +45 -0
  75. kalfa/core/graph.py +304 -0
  76. kalfa/core/history.py +70 -0
  77. kalfa/core/loop.py +710 -0
  78. kalfa/core/make.py +121 -0
  79. kalfa/core/plan.py +156 -0
  80. kalfa/core/predict.py +290 -0
  81. kalfa/core/protocols.py +122 -0
  82. kalfa/core/record.py +347 -0
  83. kalfa/core/refs.py +88 -0
  84. kalfa/core/rng.py +100 -0
  85. kalfa/core/rules.py +513 -0
  86. kalfa/core/run.py +568 -0
  87. kalfa/core/spec.py +413 -0
  88. kalfa/core/state.py +100 -0
  89. kalfa/core/wrap.py +671 -0
  90. kalfa/records/__init__.py +24 -0
  91. kalfa/records/compare.py +86 -0
  92. kalfa/records/reader.py +561 -0
  93. kalfa/records/stats/__init__.py +36 -0
  94. kalfa/records/stats/choices.py +282 -0
  95. kalfa/records/stats/classes.py +236 -0
  96. kalfa/records/stats/measures.py +144 -0
  97. kalfa/records/stats/pairs.py +225 -0
  98. kalfa/records/stats/tables.py +153 -0
  99. kalfa/records/sweep.py +244 -0
  100. kalfa/std/__init__.py +17 -0
  101. kalfa/std/actions.py +35 -0
  102. kalfa/std/common.py +56 -0
  103. kalfa/std/data/__init__.py +1 -0
  104. kalfa/std/data/base.py +45 -0
  105. kalfa/std/data/columns.py +190 -0
  106. kalfa/std/data/flow.py +317 -0
  107. kalfa/std/data/images.py +443 -0
  108. kalfa/std/data/options.py +147 -0
  109. kalfa/std/data/pre.py +84 -0
  110. kalfa/std/data/splits.py +140 -0
  111. kalfa/std/data/stream.py +294 -0
  112. kalfa/std/data/table.py +642 -0
  113. kalfa/std/data/text.py +436 -0
  114. kalfa/std/data/transforms.py +327 -0
  115. kalfa/std/data/weights.py +113 -0
  116. kalfa/std/data/window.py +135 -0
  117. kalfa/std/figure.py +37 -0
  118. kalfa/std/init.py +72 -0
  119. kalfa/std/loggers.py +105 -0
  120. kalfa/std/losses.py +161 -0
  121. kalfa/std/metrics.py +463 -0
  122. kalfa/std/models.py +122 -0
  123. kalfa/std/notes.py +1553 -0
  124. kalfa/std/objectives.py +360 -0
  125. kalfa/std/plots/__init__.py +75 -0
  126. kalfa/std/plots/classification.py +207 -0
  127. kalfa/std/plots/common.py +44 -0
  128. kalfa/std/plots/curves.py +56 -0
  129. kalfa/std/plots/images.py +188 -0
  130. kalfa/std/plots/pairs.py +66 -0
  131. kalfa/std/plots/predictions.py +154 -0
  132. kalfa/std/samplers.py +261 -0
  133. kalfa/std/samples.py +72 -0
  134. kalfa/std/schedules.py +74 -0
  135. kalfa/std/steps.py +133 -0
  136. kalfa/std/triggers.py +135 -0
  137. kalfa/summary.py +296 -0
  138. kalfa/tui/__init__.py +4 -0
  139. kalfa/tui/app.py +107 -0
  140. kalfa/tui/config.py +61 -0
  141. kalfa/tui/guide.py +100 -0
  142. kalfa/tui/jobs.py +97 -0
  143. kalfa/tui/legos.py +62 -0
  144. kalfa/tui/records.py +439 -0
  145. kalfa-3.0.0.dev0.dist-info/METADATA +261 -0
  146. kalfa-3.0.0.dev0.dist-info/RECORD +148 -0
  147. kalfa-3.0.0.dev0.dist-info/WHEEL +4 -0
  148. kalfa-3.0.0.dev0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,112 @@
1
+ import base64
2
+ import html
3
+ import io
4
+ import math
5
+
6
+ from matplotlib.backends.backend_agg import FigureCanvasAgg
7
+ from matplotlib.figure import Figure
8
+
9
+ from kalfa import summary as summarizing
10
+ from kalfa.std.common import numeric
11
+ from kalfa.std.figure import PALETTE
12
+
13
+
14
+ DPI = 100
15
+ CSS = """
16
+ :root { color-scheme: light; --ink: #16181d; --muted: #6b7079; --line: #dfe2e7; --soft: #f4f6f9; --accent: #2a78d6; }
17
+ * { box-sizing: border-box; }
18
+ body { margin: 0; background: #fff; color: var(--ink);
19
+ font: 14px/1.5 system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; }
20
+ main { max-width: 1100px; margin: 0 auto; padding: 28px 16px 64px; }
21
+ h1 { font-size: 24px; margin: 0 0 4px; }
22
+ h2 { font-size: 19px; margin: 36px 0 10px; padding-top: 10px; border-top: 1px solid var(--line); }
23
+ h3 { font-size: 16px; margin: 30px 0 4px; }
24
+ h4 { font-size: 14px; margin: 16px 0 6px; }
25
+ h5 { font-size: 13px; margin: 14px 0 6px; }
26
+ p { margin: 6px 0; overflow-wrap: anywhere; }
27
+ nav a { color: var(--accent); text-decoration: none; margin-right: 14px; }
28
+ .muted { color: var(--muted); }
29
+ .best { font-weight: 600; }
30
+ .note { background: #fff8e6; border: 1px solid #f1d8a0; border-radius: 6px; padding: 6px 10px; }
31
+ .scroll { overflow-x: auto; margin: 6px 0 14px; }
32
+ table { border-collapse: collapse; font-size: 12.5px; }
33
+ th, td { padding: 3px 9px; border-bottom: 1px solid var(--line); text-align: left; white-space: nowrap; }
34
+ th { background: var(--soft); font-weight: 600; }
35
+ td.num { text-align: right; font-variant-numeric: tabular-nums; }
36
+ .pair { display: flex; flex-wrap: wrap; gap: 0 28px; }
37
+ .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 420px), 1fr)); gap: 0 18px; }
38
+ figure { margin: 10px 0 18px; }
39
+ figure img { display: block; max-width: 100%; height: auto; border: 1px solid var(--line); border-radius: 6px; }
40
+ figcaption { color: var(--muted); font-size: 12.5px; margin-top: 4px; }
41
+ section.record { margin-bottom: 18px; }
42
+ @media print { figure, table { break-inside: avoid; } nav { display: none; } }
43
+ """
44
+
45
+
46
+ def escape(value):
47
+ return html.escape(str(value), quote=True)
48
+
49
+
50
+ def finite(value):
51
+ return numeric(value) and math.isfinite(float(value))
52
+
53
+
54
+ def shown(value, column=None):
55
+ if isinstance(value, (list, tuple)):
56
+ return "[" + ", ".join(shown(item) for item in value) + "]"
57
+ if isinstance(value, dict):
58
+ return "{" + ", ".join(f"{key}: {shown(item)}" for key, item in value.items()) + "}"
59
+ return summarizing.cell(value, column)
60
+
61
+
62
+ def plural(count, word):
63
+ return f"{count} {word}" if count == 1 else f"{count} {word}s"
64
+
65
+
66
+ def paragraph(text, kind=None):
67
+ attribute = f' class="{kind}"' if kind else ""
68
+ return f"<p{attribute}>{escape(text)}</p>"
69
+
70
+
71
+ def table(headers, rows):
72
+ head = "".join(f"<th>{escape(header)}</th>" for header in headers)
73
+ lines = []
74
+ for row in rows:
75
+ cells = []
76
+ for value, header in zip(row, headers):
77
+ kind = ' class="num"' if numeric(value) else ""
78
+ cells.append(f"<td{kind}>{escape(shown(value, header))}</td>")
79
+ lines.append("<tr>" + "".join(cells) + "</tr>")
80
+ body = "".join(lines)
81
+ return f'<div class="scroll"><table><thead><tr>{head}</tr></thead><tbody>{body}</tbody></table></div>'
82
+
83
+
84
+ def new_figure(width=6.0, height=3.6, columns=1):
85
+ figure = Figure(figsize=(width, height), layout="constrained")
86
+ FigureCanvasAgg(figure)
87
+ return figure, figure.subplots(1, columns)
88
+
89
+
90
+ def embedded(figure, caption):
91
+ with io.BytesIO() as buffer:
92
+ figure.savefig(buffer, format="png", dpi=DPI, metadata={"Software": None})
93
+ data = buffer.getvalue()
94
+ figure.clear()
95
+ encoded = base64.b64encode(data).decode("ascii")
96
+ return (f'<figure><img alt="{escape(caption)}" src="data:image/png;base64,{encoded}">'
97
+ f"<figcaption>{escape(caption)}</figcaption></figure>")
98
+
99
+
100
+ def grid(figures):
101
+ return '<div class="grid">' + "".join(figures) + "</div>"
102
+
103
+
104
+ def color_of(position):
105
+ return PALETTE[position % len(PALETTE)]
106
+
107
+
108
+ def document(title, body):
109
+ return ('<!doctype html>\n<html lang="en">\n<head>\n<meta charset="utf-8">\n'
110
+ '<meta name="viewport" content="width=device-width, initial-scale=1">\n'
111
+ f"<title>{escape(title)}</title>\n<style>{CSS}</style>\n</head>\n<body>\n<main>\n{body}\n</main>\n"
112
+ "</body>\n</html>\n")
@@ -0,0 +1,216 @@
1
+ from pathlib import Path
2
+
3
+ from kalfa.commands.report.page import color_of, embedded, escape, finite, new_figure, paragraph, plural, shown, table
4
+ from kalfa.core.errors import KalfaError
5
+ from kalfa.records import reader
6
+ from kalfa.records.stats import PREDICTIONS, prediction_stats
7
+ from kalfa.std.plots import INK, LEGEND_LIMIT, MAX_CLASSES, MUTED, dress
8
+
9
+
10
+ ANNOTATED = 16
11
+ THRESHOLD_RULES = {"0.5": "the threshold 0.5 of a probability", "given": "a threshold given by hand",
12
+ "best_f1": "the threshold with the best f1 on this table"}
13
+
14
+
15
+ def regression_part(pair, found):
16
+ keys = ("count", "rmse", "mae", "r2", "bias")
17
+ parts = [table(keys, [[found.get(key) for key in keys]])]
18
+ scatter = found.get("scatter") or {}
19
+ points = [(truth, guess) for truth, guess in zip(scatter.get("truth") or [], scatter.get("pred") or [])
20
+ if finite(truth) and finite(guess)]
21
+ if not points:
22
+ parts.append(paragraph("no finite rows to draw", "muted"))
23
+ return "\n".join(parts)
24
+ figure, (left, right) = new_figure(9.6, 3.8, columns=2)
25
+ truths, guesses = zip(*points)
26
+ left.scatter(truths, guesses, s=10, alpha=0.6, color=color_of(0), linewidths=0)
27
+ low, high = min(min(truths), min(guesses)), max(max(truths), max(guesses))
28
+ left.plot([low, high], [low, high], color=MUTED, linestyle="--", linewidth=1)
29
+ left.set_title("pred against truth", loc="left")
30
+ left.set_xlabel(pair["target"])
31
+ left.set_ylabel(pair["pred"])
32
+ dress(left)
33
+ for position, (key, label) in enumerate((("truth_hist", pair["target"]), ("pred_hist", pair["pred"]))):
34
+ counted = found.get(key) or {}
35
+ if counted.get("counts") and len(counted.get("edges") or []) == len(counted["counts"]) + 1:
36
+ right.stairs(counted["counts"], counted["edges"], label=label, color=color_of(position), linewidth=1.6)
37
+ if right.get_legend_handles_labels()[0]:
38
+ right.legend(frameon=False, fontsize="small")
39
+ right.set_title("histograms", loc="left")
40
+ right.set_xlabel("value")
41
+ right.set_ylabel("rows")
42
+ dress(right)
43
+ parts.append(embedded(figure, f"{pair['pred']} against {pair['target']} over "
44
+ f"{plural(found.get('count') or len(points), 'row')}, and their histograms"))
45
+ return "\n".join(parts)
46
+
47
+
48
+ def confusion_axis(axis, pair, classes, confusion):
49
+ count = len(classes)
50
+ axis.imshow(confusion, cmap="Blues")
51
+ axis.set_xticks(range(count), classes, rotation=90 if count > 8 else 0)
52
+ axis.set_yticks(range(count), classes)
53
+ axis.set_xlabel(f"predicted ({pair['pred']})")
54
+ axis.set_ylabel(f"truth ({pair['target']})")
55
+ axis.set_title("confusion matrix", loc="left")
56
+ if count > ANNOTATED:
57
+ return
58
+ peak = max((max(row) for row in confusion if row), default=0) or 1
59
+ for row_position, row in enumerate(confusion):
60
+ for column_position, value in enumerate(row):
61
+ axis.text(column_position, row_position, str(value), ha="center", va="center", fontsize=8,
62
+ color="white" if value > peak / 2 else INK)
63
+
64
+
65
+ def roc_curves(found):
66
+ roc = found.get("roc")
67
+ if not isinstance(roc, dict) or not roc:
68
+ return []
69
+ if "fpr" in roc:
70
+ return [(None, roc)]
71
+ return [(str(label), curve) for label, curve in roc.items() if isinstance(curve, dict)]
72
+
73
+
74
+ def roc_axis(axis, curves):
75
+ for position, (label, roc) in enumerate(curves):
76
+ name = None if label is None else f"{label}, auc {shown(roc.get('auc'))}"
77
+ axis.plot(roc.get("fpr") or [], roc.get("tpr") or [], color=color_of(position), linewidth=1.6, label=name)
78
+ axis.plot([0, 1], [0, 1], color=MUTED, linestyle="--", linewidth=1)
79
+ if curves[0][0] is None:
80
+ axis.set_title(f"roc curve, auc {shown(curves[0][1].get('auc'))}", loc="left")
81
+ else:
82
+ axis.set_title("roc, one class against the rest", loc="left")
83
+ if len(curves) <= LEGEND_LIMIT:
84
+ axis.legend(frameon=False, fontsize="small")
85
+ axis.set_xlabel("false positive rate")
86
+ axis.set_ylabel("true positive rate")
87
+ dress(axis)
88
+
89
+
90
+ def classification_numbers(pair, found, curves):
91
+ if pair.get("task") != "binary":
92
+ keys = ("count", "accuracy", "macro_f1", "weighted_f1")
93
+ return [table([key.replace("_", " ") for key in keys], [[found.get(key) for key in keys]])]
94
+ keys = ("count", "accuracy", "precision", "recall", "f1", "balanced_accuracy")
95
+ auc = [curves[0][1].get("auc")] if curves else []
96
+ parts = [table([key.replace("_", " ") for key in keys] + ["roc auc"] * len(auc),
97
+ [[found.get(key) for key in keys] + auc])]
98
+ if found.get("score"):
99
+ rule = THRESHOLD_RULES.get(found.get("threshold_rule"), found.get("threshold_rule"))
100
+ text = f"{found['score']} >= {shown(found.get('threshold'))} reads as {found.get('positive')}, {rule}"
101
+ else:
102
+ text = f"no score column, so the predicted labels are counted; the positive class is {found.get('positive')}"
103
+ return parts + [paragraph(text, "muted")]
104
+
105
+
106
+ def classification_part(pair, found):
107
+ curves = roc_curves(found)
108
+ parts = classification_numbers(pair, found, curves)
109
+ per_class = found.get("per_class") or {}
110
+ if per_class:
111
+ keys = ("precision", "recall", "f1", "support")
112
+ aucs = {label: curve.get("auc") for label, curve in curves if label is not None}
113
+ parts.append(table(["class", *keys, *(["roc auc"] if aucs else [])],
114
+ [[label, *(scores.get(key) for key in keys), *([aucs.get(str(label))] if aucs else [])]
115
+ for label, scores in per_class.items()]))
116
+ classes = [str(label) for label in found.get("classes") or []]
117
+ confusion = found.get("confusion") or []
118
+ if not found.get("count") or not classes:
119
+ parts.append(paragraph("confusion matrix not drawn: no rows", "muted"))
120
+ return "\n".join(parts)
121
+ if len(classes) > MAX_CLASSES:
122
+ parts.append(paragraph(f"confusion matrix not drawn: {len(classes)} classes, more than {MAX_CLASSES}",
123
+ "muted"))
124
+ return "\n".join(parts)
125
+ figure, axes = new_figure(9.6 if curves else 5.2, 4.2, columns=2 if curves else 1)
126
+ confusion_axis(axes[0] if curves else axes, pair, classes, confusion)
127
+ if curves:
128
+ roc_axis(axes[1], curves)
129
+ caption = f"confusion matrix of {pair['pred']} against {pair['target']}"
130
+ ending = ", and the roc curve" if curves and curves[0][0] is None else ", and the roc curves" if curves else ""
131
+ parts.append(embedded(figure, caption + ending))
132
+ return "\n".join(parts)
133
+
134
+
135
+ def stairs_of(axis, found, title, label):
136
+ counts, edges = (found or {}).get("counts") or [], (found or {}).get("edges") or []
137
+ if counts and len(edges) == len(counts) + 1:
138
+ axis.stairs(counts, edges, color=color_of(0), linewidth=1.6)
139
+ axis.set_title(title, loc="left")
140
+ axis.set_xlabel(label)
141
+ axis.set_ylabel("rows")
142
+ dress(axis)
143
+
144
+
145
+ def embedding_part(pair, found):
146
+ pca = found.get("pca") or {}
147
+ explained = list(pca.get("explained") or [None, None])[:2]
148
+ parts = [table(["count", "dims", "first component", "second component"],
149
+ [[found.get("count"), found.get("dims"), *explained]])]
150
+ colors = pca.get("color") or [None] * len(pca.get("x") or [])
151
+ points = [(x, y, str(color)) for x, y, color in zip(pca.get("x") or [], pca.get("y") or [], colors)
152
+ if finite(x) and finite(y)]
153
+ if not points:
154
+ parts.append(paragraph("no finite rows to draw", "muted"))
155
+ return "\n".join(parts)
156
+ figure, (left, right) = new_figure(9.6, 3.8, columns=2)
157
+ names = sorted({color for _, _, color in points}) if pca.get("color") else []
158
+ grouped = 0 < len(names) <= LEGEND_LIMIT
159
+ for index, name in enumerate(names if grouped else [None]):
160
+ chosen = [(x, y) for x, y, color in points if name is None or color == name]
161
+ left.scatter(*zip(*chosen), s=10, alpha=0.7, color=color_of(index), linewidths=0, label=name)
162
+ if grouped:
163
+ left.legend(frameon=False, fontsize="small", title=pca.get("color_by"))
164
+ left.set_title("pca of the rows", loc="left")
165
+ left.set_xlabel("first component")
166
+ left.set_ylabel("second component")
167
+ dress(left)
168
+ stairs_of(right, found.get("norm_hist"), "vector length", "length")
169
+ colored = f", colored by {pca.get('color_by')}" if grouped else ""
170
+ parts.append(embedded(figure, f"pca map of {pair['pred']} over {plural(len(points), 'row')}{colored}, and the "
171
+ f"length of each vector"))
172
+ return "\n".join(parts)
173
+
174
+
175
+ def values_part(pair, found):
176
+ labels = found.get("labels")
177
+ if isinstance(labels, dict):
178
+ rows = list(zip(labels.get("labels") or [], labels.get("counts") or []))
179
+ return "\n".join([table(["count"], [[found.get("count")]]), table(["label", "rows"], rows)])
180
+ quantiles = found.get("quantiles") or {}
181
+ parts = [table(["count", "mean", "std", *quantiles],
182
+ [[found.get("count"), found.get("mean"), found.get("std"), *quantiles.values()]])]
183
+ if (found.get("hist") or {}).get("counts"):
184
+ figure, axis = new_figure(5.2, 3.4)
185
+ stairs_of(axis, found["hist"], "distribution", pair["pred"])
186
+ parts.append(embedded(figure, f"distribution of {pair['pred']} over {plural(found.get('count') or 0, 'row')}"))
187
+ return "\n".join(parts)
188
+
189
+
190
+ def pair_heading(pair):
191
+ task = pair.get("task") or pair["kind"]
192
+ named = pair["kind"] if task == pair["kind"] else f"{pair['kind']} ({task})"
193
+ if pair.get("target") is not None:
194
+ return f"{pair['target']} against {pair['pred']}, {named}"
195
+ columns = pair.get("columns") or [pair["pred"]]
196
+ return f"{pair['pred']}{f', {len(columns)} columns' if len(columns) > 1 else ''}, {named}"
197
+
198
+
199
+ def predictions_part(directory):
200
+ if not (Path(directory) / PREDICTIONS).is_file():
201
+ return ""
202
+ try:
203
+ found = prediction_stats(directory, reader.read_plan(directory))
204
+ except (KalfaError, OSError, ValueError) as exc:
205
+ return paragraph(f"{PREDICTIONS} could not be read: {exc}", "note")
206
+ if found is None:
207
+ return paragraph(f"{PREDICTIONS} could not be read", "note")
208
+ parts = ["<h4>predictions</h4>", paragraph(f"{plural(found.get('rows') or 0, 'row')} in {PREDICTIONS}", "muted")]
209
+ pairs = found.get("pairs") or []
210
+ if not pairs:
211
+ parts.append(paragraph("the table has no pred_ column", "note"))
212
+ drawers = {"classification": classification_part, "embedding": embedding_part, "values": values_part}
213
+ for pair in pairs:
214
+ parts.append(f"<h5>{escape(pair_heading(pair))}</h5>")
215
+ parts.append(drawers.get(pair["kind"], regression_part)(pair, pair.get("stats") or {}))
216
+ return "\n".join(parts)