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,282 @@
1
+ import math
2
+ import numbers
3
+
4
+ import numpy
5
+
6
+ from ...core.errors import KalfaError
7
+ from ..reader import read_plan
8
+ from .classes import (
9
+ COMPLEMENT,
10
+ NEGATED,
11
+ best_threshold,
12
+ binary_rows,
13
+ binary_stats,
14
+ in_unit,
15
+ multiclass_rows,
16
+ multiclass_stats,
17
+ score_values,
18
+ )
19
+ from .measures import color_column, embedding_stats, regression_stats, values_stats
20
+ from .pairs import declared_targets, label_columns, score_labels, task_pairs
21
+ from .tables import (
22
+ PREDICTIONS,
23
+ filtered,
24
+ finite,
25
+ image_column,
26
+ label_text,
27
+ load_table,
28
+ numbers_of,
29
+ numeric,
30
+ plain_value,
31
+ present,
32
+ read_fitted_fields,
33
+ rows_in,
34
+ texts,
35
+ )
36
+
37
+
38
+ KINDS = {"binary": "classification", "multiclass": "classification", "regression": "regression",
39
+ "embedding": "embedding", "values": "values"}
40
+ GALLERY = 24
41
+ CHOICES = ("task", "score", "positive", "threshold", "bins")
42
+ HISTOGRAMS = {"binary": ("score_hist",), "multiclass": (), "regression": ("residual_hist", "truth_hist"),
43
+ "embedding": ("norm_hist",), "values": ("hist",)}
44
+ MAX_BINS = 1000
45
+
46
+
47
+ def entry(frame, position, truth=None, pred=None, score=None, error=None):
48
+ return {"index": int(frame.index[position]), "row": plain_value(rows_in(frame)[position]),
49
+ "truth": None if truth is None else plain_value(truth[position]),
50
+ "pred": None if pred is None else plain_value(pred[position]),
51
+ "score": None if score is None else plain_value(score[position]),
52
+ "error": None if error is None else plain_value(error[position])}
53
+
54
+
55
+ def class_gallery(frame, truth, guess, keep, score=None):
56
+ wrong = keep & (truth != guess).astype(bool)
57
+ picked = numpy.r_[numpy.flatnonzero(wrong), numpy.flatnonzero(keep & ~wrong)][:GALLERY]
58
+ return [entry(frame, position, truth, guess, score) for position in picked]
59
+
60
+
61
+ def class_scores(frame, columns, guess, picked):
62
+ found = numpy.full(len(guess), numpy.nan)
63
+ for position in picked:
64
+ column = columns.get(guess[position])
65
+ if column in frame.columns:
66
+ found[position] = numbers_of(frame[column].iloc[[position]])[0]
67
+ return found
68
+
69
+
70
+ def gallery_of(frame, pair, task, choice):
71
+ pred, target = pair["pred"], pair["target"]
72
+ if task == "binary":
73
+ truth, guess, values, keep = binary_rows(frame, pred, target, choice["labels"], choice["score"],
74
+ choice["threshold"])
75
+ return class_gallery(frame, truth, guess, keep, values)
76
+ if task == "multiclass":
77
+ truth, guess, keep = multiclass_rows(frame, pred, target)
78
+ wrong = keep & (truth != guess).astype(bool)
79
+ picked = numpy.r_[numpy.flatnonzero(wrong), numpy.flatnonzero(keep & ~wrong)][:GALLERY]
80
+ columns = label_columns(pair["scores"], pair["output"], pair["classes"])
81
+ return class_gallery(frame, truth, guess, keep, class_scores(frame, columns, guess, picked))
82
+ if task == "regression":
83
+ guess, truth = numbers_of(frame[pred]), numbers_of(frame[target])
84
+ error = guess - truth
85
+ keep = numpy.flatnonzero(numpy.isfinite(error))
86
+ picked = keep[numpy.argsort(-numpy.abs(error[keep]), kind="stable")][:GALLERY]
87
+ return [entry(frame, position, truth, guess, None, error) for position in picked]
88
+ color = choice.get("color")
89
+ truth = None if color is None else texts(frame[color])
90
+ guess = numbers_of(frame[pred]) if task == "values" else None
91
+ return [entry(frame, position, truth, guess) for position in range(min(len(frame), GALLERY))]
92
+
93
+
94
+ def number_value(value, label):
95
+ try:
96
+ found = float(value)
97
+ except (TypeError, ValueError):
98
+ raise KalfaError(f"{label} must be a number, not {value!r}") from None
99
+ if not math.isfinite(found):
100
+ raise KalfaError(f"{label} must be a finite number, not {value!r}")
101
+ return found
102
+
103
+
104
+ def bins_count(value, label):
105
+ if isinstance(value, bool) or not isinstance(value, (numbers.Integral, str)):
106
+ raise KalfaError(f"{label} must be a whole number, not {value!r}")
107
+ try:
108
+ found = int(value)
109
+ except ValueError:
110
+ raise KalfaError(f"{label} must be a whole number, not {value!r}") from None
111
+ if found < 1:
112
+ raise KalfaError(f"{label} must be at least 1, not {value!r}")
113
+ return min(found, MAX_BINS)
114
+
115
+
116
+ def checked_bins(pred, wanted):
117
+ if wanted in (None, ""):
118
+ return {}
119
+ if not isinstance(wanted, dict):
120
+ raise KalfaError(f"the bins of {pred} must map histogram names to counts, like {{residual_hist: 60}}")
121
+ return {str(name): bins_count(count, f"the bins of {name} for {pred}") for name, count in wanted.items()}
122
+
123
+
124
+ def checked_overrides(overrides, pairs):
125
+ if overrides in (None, {}):
126
+ return {}
127
+ if not isinstance(overrides, dict):
128
+ raise KalfaError("overrides must map prediction columns to choices")
129
+ known = [pair["pred"] for pair in pairs]
130
+ found = {}
131
+ for pred, choice in overrides.items():
132
+ if pred not in known:
133
+ raise KalfaError(f"no prediction pair {pred}; the table has {', '.join(known) or 'none'}")
134
+ if not isinstance(choice, dict):
135
+ raise KalfaError(f"the choice for {pred} must be a mapping of task, score, positive, threshold and bins")
136
+ unknown = sorted(set(choice) - set(CHOICES))
137
+ if unknown:
138
+ raise KalfaError(f"{pred} takes task, score, positive, threshold and bins, not {', '.join(unknown)}")
139
+ found[pred] = {key: value for key, value in choice.items() if key != "bins" and value not in (None, "")}
140
+ bins = checked_bins(pred, choice.get("bins"))
141
+ if bins:
142
+ found[pred]["bins"] = bins
143
+ return found
144
+
145
+
146
+ def chosen_task(pair, choice):
147
+ task = choice.get("task") or pair["auto"]
148
+ if task not in pair["tasks"]:
149
+ raise KalfaError(f"{pair['pred']} cannot be read as {task}; it can be {', '.join(pair['tasks'])}")
150
+ return task
151
+
152
+
153
+ def histogram_bins(pred, task, wanted, bins):
154
+ names = HISTOGRAMS[task]
155
+ unknown = sorted(set(wanted) - set(names))
156
+ if unknown:
157
+ kept = ", ".join(names) or "none"
158
+ raise KalfaError(f"{pred} read as {task} has no histogram {', '.join(unknown)}; its histograms are {kept}")
159
+ return {name: wanted.get(name, bins) for name in names}
160
+
161
+
162
+ def score_candidates(frame, pair, positive, labels):
163
+ owners = score_labels(pair["scores"], pair["output"], labels)
164
+ found = [column for column, label in owners.items() if label == positive]
165
+ found += [f"{COMPLEMENT}{column}" if label in labels else column
166
+ for column, label in owners.items() if label != positive]
167
+ grows = list(pair["classes"])[1]
168
+ for column in (pair["raw"], pair["pred"]):
169
+ if column and column not in found and numeric(frame[column]):
170
+ values = numbers_of(frame[column])
171
+ if len(numpy.unique(finite(values))) > 2:
172
+ found.append(column if positive == grows else turned(column, values))
173
+ return found
174
+
175
+
176
+ def turned(column, values):
177
+ return f"{COMPLEMENT}{column}" if in_unit(values) else f"{NEGATED}{column}"
178
+
179
+
180
+ def binary_choice(frame, pair, choice):
181
+ labels = list(pair["classes"])
182
+ positive = labels[1]
183
+ if choice.get("positive") is not None:
184
+ wanted = label_text(choice["positive"])
185
+ if wanted not in labels:
186
+ raise KalfaError(f"positive must be one of {', '.join(labels)} for {pair['pred']}, not {wanted!r}")
187
+ positive = wanted
188
+ labels = [label for label in labels if label != positive] + [positive]
189
+ candidates = score_candidates(frame, pair, positive, labels)
190
+ score = candidates[0] if candidates else None
191
+ if choice.get("score") is not None:
192
+ if choice["score"] not in candidates:
193
+ shown = ", ".join(candidates) or "none"
194
+ raise KalfaError(f"score must be one of {shown} for {pair['pred']}, not {choice['score']!r}")
195
+ score = choice["score"]
196
+ threshold, rule, unit = None, None, True
197
+ if score is not None:
198
+ values = score_values(frame, score)
199
+ unit = in_unit(values)
200
+ if choice.get("threshold") is not None:
201
+ threshold, rule = number_value(choice["threshold"], "threshold"), "given"
202
+ elif unit:
203
+ threshold, rule = 0.5, "0.5"
204
+ else:
205
+ truth = texts(frame[pair["target"]])
206
+ keep = present(truth) & numpy.isfinite(values)
207
+ threshold, rule = best_threshold((truth == positive)[keep].astype(bool), values[keep]), "best_f1"
208
+ return {"labels": labels, "positive": positive, "score": score, "threshold": threshold, "rule": rule,
209
+ "unit": unit, "candidates": candidates}
210
+
211
+
212
+ def headline(task, found):
213
+ if task == "regression":
214
+ return ("r2", found["r2"]) if found.get("r2") is not None else ("rmse", found.get("rmse"))
215
+ if task == "binary":
216
+ return ("auc", found["roc"]["auc"]) if found.get("roc") else ("f1", found.get("f1"))
217
+ if task == "multiclass":
218
+ return "accuracy", found.get("accuracy")
219
+ if task == "embedding":
220
+ explained = [value for value in (found.get("pca") or {}).get("explained") or [] if value is not None]
221
+ return "explained", float(sum(explained)) if explained else None
222
+ return "count", found.get("count")
223
+
224
+
225
+ def overview_of(pairs):
226
+ tasks = {}
227
+ rows = []
228
+ for pair in pairs:
229
+ tasks[pair["task"]] = tasks.get(pair["task"], 0) + 1
230
+ key, value = headline(pair["task"], pair["stats"])
231
+ rows.append({"output": pair["output"], "pred": pair["pred"], "target": pair["target"], "task": pair["task"],
232
+ "key": key, "value": value, "count": pair["stats"].get("count")})
233
+ return {"pairs": len(pairs), "tasks": tasks, "rows": rows}
234
+
235
+
236
+ def scored(full, frame, base, choice, images, context, bins, sample, worst):
237
+ task = chosen_task(base, choice)
238
+ counts = histogram_bins(base["pred"], task, choice.get("bins") or {}, bins)
239
+ pair = {"output": base["output"], "pred": base["pred"], "columns": list(base["columns"]),
240
+ "target": base["target"], "kind": KINDS[task], "task": task, "auto": base["auto"],
241
+ "tasks": list(base["tasks"]), "raw": base["raw"], "scores": list(base["scores"]), "score": None,
242
+ "positive": None, "images": images}
243
+ details = {}
244
+ if task == "binary":
245
+ details = binary_choice(full, base, choice)
246
+ pair["score"], pair["positive"] = details["score"], details["positive"]
247
+ stats = binary_stats(frame, base["pred"], base["target"], details["labels"], details["score"],
248
+ details["threshold"], details["rule"], details["unit"], counts["score_hist"],
249
+ details["candidates"])
250
+ elif task == "multiclass":
251
+ stats = multiclass_stats(frame, base["pred"], base["target"], base["classes"], base["scores"])
252
+ elif task == "regression":
253
+ stats = regression_stats(frame, base["pred"], base["target"], bins=counts["truth_hist"], sample=sample,
254
+ worst=worst, residual_bins=counts["residual_hist"])
255
+ elif task == "embedding":
256
+ details = {"color": color_column(full, context["targets"], images)}
257
+ stats = embedding_stats(frame, base["columns"], details["color"], bins=counts["norm_hist"], sample=sample)
258
+ else:
259
+ stats = values_stats(frame, base["pred"], bins=counts["hist"])
260
+ if images:
261
+ stats["gallery"] = gallery_of(frame, base, task, details)
262
+ pair["stats"] = stats
263
+ return pair
264
+
265
+
266
+ def prediction_stats(directory, plan=None, name=PREDICTIONS, where=None, bins=40, sample=2000, worst=20,
267
+ overrides=None):
268
+ full = load_table(directory, name)
269
+ if full is None:
270
+ return None
271
+ full = full.reset_index(drop=True)
272
+ frame = filtered(full, where, name)
273
+ if plan is None:
274
+ plan = read_plan(directory)
275
+ fitted = read_fitted_fields(directory)
276
+ bases = task_pairs(full, plan, fitted)
277
+ choices = checked_overrides(overrides, bases)
278
+ images = image_column(full)
279
+ context = {"targets": declared_targets(plan, fitted, full)}
280
+ pairs = [scored(full, frame, base, choices.get(base["pred"]) or {}, images, context, bins, sample, worst)
281
+ for base in bases]
282
+ return {"rows": int(len(frame)), "pairs": pairs, "overview": overview_of(pairs)}
@@ -0,0 +1,236 @@
1
+ import numpy
2
+ import pandas
3
+
4
+ from .measures import histogram
5
+ from .pairs import label_columns
6
+ from .tables import finite, labels, numbers_of, ordered_labels, plain, present, texts
7
+
8
+
9
+ RATES = ("accuracy", "precision", "recall", "f1", "specificity", "balanced_accuracy")
10
+ SWEEP = 101
11
+ CURVE = 2000
12
+ COMPLEMENT = "1 - "
13
+ NEGATED = "-"
14
+
15
+
16
+ def ratio(part, whole):
17
+ return float(part) / float(whole) if whole else 0.0
18
+
19
+
20
+ def positive_column(scores, output, classes):
21
+ named = label_columns(scores, output, classes).get(classes[1])
22
+ if named is not None:
23
+ return named
24
+ if len(scores) == 2:
25
+ return scores[1]
26
+ return scores[0] if len(scores) == 1 else None
27
+
28
+
29
+ def thin(*arrays, limit=CURVE):
30
+ size = len(arrays[0])
31
+ if size <= limit:
32
+ return arrays
33
+ chosen = numpy.unique(numpy.round(numpy.linspace(0, size - 1, limit)).astype(int))
34
+ return tuple(array[chosen] for array in arrays)
35
+
36
+
37
+ def binary_curves(truth, probabilities):
38
+ from sklearn.metrics import average_precision_score, precision_recall_curve, roc_auc_score, roc_curve
39
+
40
+ if len(numpy.unique(truth)) < 2:
41
+ return None, None
42
+ fpr, tpr, _ = roc_curve(truth, probabilities)
43
+ precision, recall, _ = precision_recall_curve(truth, probabilities)
44
+ fpr, tpr = thin(fpr, tpr)
45
+ precision, recall = thin(precision, recall)
46
+ roc = {"fpr": plain(fpr), "tpr": plain(tpr), "auc": float(roc_auc_score(truth, probabilities))}
47
+ pr = {"precision": plain(precision), "recall": plain(recall),
48
+ "ap": float(average_precision_score(truth, probabilities))}
49
+ return roc, pr
50
+
51
+
52
+ def per_class_of(classes, confusion):
53
+ found = {}
54
+ for index, label in enumerate(classes):
55
+ hits = int(confusion[index, index])
56
+ predicted, actual = int(confusion[:, index].sum()), int(confusion[index, :].sum())
57
+ precision, recall = ratio(hits, predicted), ratio(hits, actual)
58
+ found[label] = {"precision": precision, "recall": recall,
59
+ "f1": ratio(2 * precision * recall, precision + recall), "support": actual}
60
+ return found
61
+
62
+
63
+ def confusion_of(classes, truth, guess):
64
+ position = {label: index for index, label in enumerate(classes)}
65
+ confusion = numpy.zeros((len(classes), len(classes)), dtype=numpy.int64)
66
+ for actual, predicted in zip(truth, guess):
67
+ confusion[position[actual], position[predicted]] += 1
68
+ return confusion
69
+
70
+
71
+ def normalized(confusion):
72
+ totals = confusion.sum(axis=1, keepdims=True)
73
+ shares = numpy.divide(confusion, totals, out=numpy.zeros(confusion.shape, dtype=numpy.float64),
74
+ where=totals > 0)
75
+ return shares.tolist()
76
+
77
+
78
+ def classification_stats(frame, pred, target, raw=None, scores=None, classes=None):
79
+ guess, truth = texts(frame[pred]), texts(frame[target])
80
+ keep = present(guess) & present(truth)
81
+ guess, truth = guess[keep], truth[keep]
82
+ classes = labels(classes) or []
83
+ classes = classes + ordered_labels((set(truth.tolist()) | set(guess.tolist())) - set(classes))
84
+ confusion = confusion_of(classes, truth, guess)
85
+ count = int(len(truth))
86
+ roc = pr = None
87
+ output = pred[len("pred_"):] if pred.startswith("pred_") else pred
88
+ column = positive_column(list(scores), output, classes) if scores and len(classes) == 2 else None
89
+ if column is not None and column in frame.columns and count:
90
+ probabilities = pandas.to_numeric(frame[column], errors="coerce").to_numpy(dtype=numpy.float64)[keep]
91
+ finite_rows = numpy.isfinite(probabilities)
92
+ roc, pr = binary_curves((truth[finite_rows] == classes[1]).astype(int), probabilities[finite_rows])
93
+ return {"count": count, "accuracy": float(numpy.trace(confusion) / count) if count else None,
94
+ "classes": classes, "confusion": confusion.tolist(), "per_class": per_class_of(classes, confusion),
95
+ "roc": roc, "pr": pr}
96
+
97
+
98
+ def rates(tn, fp, fn, tp):
99
+ count = tn + fp + fn + tp
100
+ if not count:
101
+ return {key: None for key in RATES}
102
+ recall, specificity = ratio(tp, tp + fn), ratio(tn, tn + fp)
103
+ present_rates = [rate for rate, size in ((recall, tp + fn), (specificity, tn + fp)) if size]
104
+ return {"accuracy": ratio(tp + tn, count), "precision": ratio(tp, tp + fp), "recall": recall,
105
+ "f1": ratio(2 * tp, 2 * tp + fp + fn), "specificity": specificity,
106
+ "balanced_accuracy": float(sum(present_rates) / len(present_rates))}
107
+
108
+
109
+ def in_unit(values):
110
+ found = finite(values)
111
+ return not len(found) or (float(found.min()) >= 0.0 and float(found.max()) <= 1.0)
112
+
113
+
114
+ def best_threshold(actual, values):
115
+ if not len(values):
116
+ return None
117
+ order = numpy.argsort(-values, kind="stable")
118
+ ranked, hits = values[order], actual[order]
119
+ tp, fp = numpy.cumsum(hits), numpy.cumsum(~hits)
120
+ ends = numpy.r_[ranked[1:] != ranked[:-1], True]
121
+ tp, fp, ranked = tp[ends], fp[ends], ranked[ends]
122
+ f1 = 2.0 * tp / numpy.maximum(tp + fp + int(hits.sum()), 1)
123
+ return float(ranked[int(numpy.argmax(f1))])
124
+
125
+
126
+ def sweep_of(actual, values, unit):
127
+ if unit:
128
+ grid = numpy.linspace(0.0, 1.0, SWEEP)
129
+ else:
130
+ grid = numpy.unique(numpy.quantile(values, numpy.linspace(0.0, 1.0, SWEEP)))
131
+ positives, negatives = numpy.sort(values[actual]), numpy.sort(values[~actual])
132
+ tp = len(positives) - numpy.searchsorted(positives, grid, side="left")
133
+ fp = len(negatives) - numpy.searchsorted(negatives, grid, side="left")
134
+ fn = len(positives) - tp
135
+ return {"threshold": [float(value) for value in grid],
136
+ "precision": [ratio(hit, hit + miss) for hit, miss in zip(tp, fp)],
137
+ "recall": [ratio(hit, len(positives)) for hit in tp],
138
+ "f1": [ratio(2 * hit, 2 * hit + miss + lost) for hit, miss, lost in zip(tp, fp, fn)]}
139
+
140
+
141
+ def score_hist_of(actual, values, unit, bins):
142
+ if unit:
143
+ edges = numpy.histogram_bin_edges(values, bins=bins, range=(0.0, 1.0))
144
+ else:
145
+ edges = numpy.histogram_bin_edges(values, bins=bins)
146
+ return {"edges": [float(edge) for edge in edges],
147
+ "negative": histogram(values[~actual], edges=edges)["counts"],
148
+ "positive": histogram(values[actual], edges=edges)["counts"]}
149
+
150
+
151
+ def compared(values, threshold):
152
+ if threshold is None:
153
+ return numpy.zeros(len(values), dtype=bool)
154
+ with numpy.errstate(invalid="ignore"):
155
+ return values >= threshold
156
+
157
+
158
+ def score_values(frame, score):
159
+ if score in frame.columns:
160
+ return numbers_of(frame[score])
161
+ if score.startswith(COMPLEMENT):
162
+ return 1.0 - numbers_of(frame[score[len(COMPLEMENT):]])
163
+ if score.startswith(NEGATED) and score[len(NEGATED):] in frame.columns:
164
+ return -numbers_of(frame[score[len(NEGATED):]])
165
+ return numbers_of(frame[score])
166
+
167
+
168
+ def binary_rows(frame, pred, target, labels, score, threshold):
169
+ negative, positive = labels
170
+ truth = texts(frame[target])
171
+ keep = present(truth)
172
+ if score is None:
173
+ guess = texts(frame[pred])
174
+ return truth, guess, None, keep & present(guess)
175
+ values = score_values(frame, score)
176
+ guess = numpy.where(compared(values, threshold), positive, negative).astype(object)
177
+ return truth, guess, values, keep & numpy.isfinite(values)
178
+
179
+
180
+ def binary_stats(frame, pred, target, labels, score=None, threshold=None, rule=None, unit=True, bins=40,
181
+ candidates=None):
182
+ positive = labels[1]
183
+ truth, guess, values, keep = binary_rows(frame, pred, target, labels, score, threshold)
184
+ actual, predicted = (truth == positive)[keep].astype(bool), (guess == positive)[keep].astype(bool)
185
+ count = int(keep.sum())
186
+ tp, fp = int((actual & predicted).sum()), int((~actual & predicted).sum())
187
+ fn = int((actual & ~predicted).sum())
188
+ tn = count - tp - fp - fn
189
+ confusion = numpy.asarray([[tn, fp], [fn, tp]], dtype=numpy.int64)
190
+ stats = {"count": count, "labels": list(labels), "positive": positive, "score": score, "threshold": threshold,
191
+ "threshold_rule": rule, "confusion": confusion.tolist(), "confusion_normalized": normalized(confusion),
192
+ **rates(tn, fp, fn, tp), "roc": None, "pr": None, "score_hist": None, "sweep": None,
193
+ "candidates": list(candidates or []), "classes": list(labels),
194
+ "per_class": per_class_of(list(labels), confusion)}
195
+ if values is None or not count:
196
+ return stats
197
+ scored = values[keep]
198
+ stats["roc"], stats["pr"] = binary_curves(actual.astype(int), scored)
199
+ stats["score_hist"] = score_hist_of(actual, scored, unit, bins)
200
+ stats["sweep"] = sweep_of(actual, scored, unit)
201
+ return stats
202
+
203
+
204
+ def multiclass_rows(frame, pred, target):
205
+ truth, guess = texts(frame[target]), texts(frame[pred])
206
+ return truth, guess, present(truth) & present(guess)
207
+
208
+
209
+ def multiclass_stats(frame, pred, target, classes=None, scores=None):
210
+ truth, guess, keep = multiclass_rows(frame, pred, target)
211
+ truth, guess = truth[keep], guess[keep]
212
+ classes = list(classes or [])
213
+ classes = classes + ordered_labels((set(truth.tolist()) | set(guess.tolist())) - set(classes))
214
+ confusion = confusion_of(classes, truth, guess)
215
+ count = int(len(truth))
216
+ per_class = per_class_of(classes, confusion)
217
+ seen = [label for index, label in enumerate(classes) if confusion[index, :].sum() or confusion[:, index].sum()]
218
+ output = pred[len("pred_"):] if pred.startswith("pred_") else pred
219
+ columns = label_columns(scores or [], output, classes)
220
+ curves = {}
221
+ for label in classes:
222
+ column = columns.get(label)
223
+ if column is None or column not in frame.columns or not count:
224
+ continue
225
+ probabilities = numbers_of(frame[column])[keep]
226
+ rows = numpy.isfinite(probabilities)
227
+ roc, _ = binary_curves((truth[rows] == label).astype(int), probabilities[rows])
228
+ if roc is not None:
229
+ curves[label] = roc
230
+ return {"count": count, "classes": classes, "confusion": confusion.tolist(),
231
+ "confusion_normalized": normalized(confusion), "per_class": per_class,
232
+ "accuracy": ratio(numpy.trace(confusion), count) if count else None,
233
+ "macro_f1": float(numpy.mean([per_class[label]["f1"] for label in seen])) if seen else None,
234
+ "weighted_f1": float(sum(item["f1"] * item["support"] for item in per_class.values()) / count)
235
+ if count else None,
236
+ "roc": curves or None, "pr": None}
@@ -0,0 +1,144 @@
1
+ import numpy
2
+
3
+ from .tables import finite, label_key, numbers_of, numeric, plain, rows_in, texts
4
+
5
+
6
+ QUANTILES = {"p05": 0.05, "p25": 0.25, "p50": 0.5, "p75": 0.75, "p95": 0.95}
7
+ COLORS = 20
8
+ LABELS = 50
9
+
10
+
11
+ def histogram(values, bins=40, edges=None):
12
+ array = finite(values)
13
+ if edges is not None:
14
+ counts, found = numpy.histogram(array, bins=numpy.asarray(edges, dtype=numpy.float64))
15
+ elif len(array):
16
+ counts, found = numpy.histogram(array, bins=bins)
17
+ else:
18
+ counts, found = numpy.histogram(array, bins=bins, range=(0.0, 1.0))
19
+ return {"edges": [float(edge) for edge in found], "counts": [int(count) for count in counts]}
20
+
21
+
22
+ def quantiles_of(values):
23
+ if not len(values):
24
+ return {key: None for key in QUANTILES}
25
+ return {key: float(numpy.quantile(values, level)) for key, level in QUANTILES.items()}
26
+
27
+
28
+ def regression_stats(frame, pred, target, bins=40, sample=2000, worst=20, seed=0, residual_bins=None):
29
+ guess = numbers_of(frame[pred])
30
+ truth = numbers_of(frame[target])
31
+ keep = numpy.isfinite(guess) & numpy.isfinite(truth)
32
+ guess, truth, rows = guess[keep], truth[keep], rows_in(frame)[keep]
33
+ count = int(len(guess))
34
+ residual = guess - truth
35
+ stats = {"count": count, "rmse": None, "mae": None, "r2": None, "bias": None,
36
+ "residual_quantiles": {key: None for key in QUANTILES},
37
+ "pred_hist": histogram(guess, bins), "truth_hist": histogram(truth, bins),
38
+ "residual_hist": histogram(residual, bins if residual_bins is None else residual_bins),
39
+ "scatter": {"row": [], "truth": [], "pred": []}, "worst": []}
40
+ if count == 0:
41
+ return stats
42
+ edges = numpy.histogram_bin_edges(numpy.concatenate([guess, truth]), bins=bins)
43
+ total = float(((truth - truth.mean()) ** 2).sum())
44
+ stats["rmse"] = float(numpy.sqrt((residual ** 2).mean()))
45
+ stats["mae"] = float(numpy.abs(residual).mean())
46
+ stats["r2"] = None if total == 0 else float(1.0 - float((residual ** 2).sum()) / total)
47
+ stats["bias"] = float(residual.mean())
48
+ stats["residual_quantiles"] = quantiles_of(residual)
49
+ stats["pred_hist"] = histogram(guess, edges=edges)
50
+ stats["truth_hist"] = histogram(truth, edges=edges)
51
+ chosen = numpy.arange(count)
52
+ if sample is not None and count > int(sample):
53
+ chosen = numpy.random.default_rng(seed).choice(count, int(sample), replace=False)
54
+ chosen = chosen[numpy.argsort(rows[chosen], kind="stable")]
55
+ stats["scatter"] = {"row": plain(rows[chosen]), "truth": plain(truth[chosen]), "pred": plain(guess[chosen])}
56
+ order = numpy.argsort(-numpy.abs(residual), kind="stable")[:max(int(worst or 0), 0)]
57
+ stats["worst"] = [{"row": row, "truth": float(truth[index]), "pred": float(guess[index]),
58
+ "error": float(residual[index])} for index, row in zip(order, plain(rows[order]))]
59
+ return stats
60
+
61
+
62
+ def matrix_of(frame, columns):
63
+ matrix = numpy.column_stack([numbers_of(frame[column]) for column in columns])
64
+ return matrix, numpy.isfinite(matrix).all(axis=1)
65
+
66
+
67
+ def components(matrix):
68
+ centered = matrix - matrix.mean(axis=0)
69
+ _, spread, axes = numpy.linalg.svd(centered, full_matrices=False)
70
+ variance = spread ** 2
71
+ total = float(variance.sum())
72
+ axes = axes[:2]
73
+ signs = numpy.where(axes[numpy.arange(len(axes)), numpy.abs(axes).argmax(axis=1)] < 0, -1.0, 1.0)
74
+ coordinates = centered @ (axes * signs[:, None]).T
75
+ if coordinates.shape[1] < 2:
76
+ coordinates = numpy.column_stack([coordinates, numpy.zeros((len(coordinates), 2 - coordinates.shape[1]))])
77
+ explained = [float(variance[index] / total) if total > 0 and index < len(variance) else 0.0
78
+ for index in range(2)]
79
+ return coordinates, explained
80
+
81
+
82
+ def color_column(frame, targets, images=None):
83
+ columns = [str(column) for column in frame.columns]
84
+ if "label" in columns:
85
+ return "label"
86
+ for target in targets:
87
+ if target in columns:
88
+ return target
89
+ for column in columns:
90
+ if column in ("row", images) or column.startswith(("pred_", "raw_", "prob_")):
91
+ continue
92
+ series = frame[column]
93
+ if not numeric(series) and series.nunique(dropna=True) < COLORS:
94
+ return column
95
+ return None
96
+
97
+
98
+ def sampled(count, sample, seed=0):
99
+ chosen = numpy.arange(count)
100
+ if sample is not None and count > int(sample):
101
+ chosen = numpy.sort(numpy.random.default_rng(seed).choice(count, int(sample), replace=False))
102
+ return chosen
103
+
104
+
105
+ def embedding_stats(frame, columns, color=None, bins=40, sample=2000, seed=0):
106
+ matrix, keep = matrix_of(frame, columns)
107
+ matrix = matrix[keep]
108
+ count = int(len(matrix))
109
+ pca = {"x": [], "y": [], "explained": [None, None], "row": [], "index": [], "color": None, "color_by": color}
110
+ stats = {"count": count, "dims": len(columns), "pca": pca, "norm_hist": histogram([], bins)}
111
+ if not count:
112
+ return stats
113
+ coordinates, explained = components(matrix)
114
+ chosen = sampled(count, sample, seed)
115
+ positions = numpy.flatnonzero(keep)[chosen]
116
+ pca.update({"x": plain(coordinates[chosen, 0]), "y": plain(coordinates[chosen, 1]), "explained": explained,
117
+ "row": plain(rows_in(frame)[positions]), "index": [int(value) for value in frame.index[positions]]})
118
+ if color is not None:
119
+ pca["color"] = texts(frame[color].iloc[positions]).tolist()
120
+ stats["norm_hist"] = histogram(numpy.linalg.norm(matrix, axis=1), bins)
121
+ return stats
122
+
123
+
124
+ def label_counts(values):
125
+ counts = {}
126
+ for value in values:
127
+ counts[value] = counts.get(value, 0) + 1
128
+ found = sorted(counts.items(), key=lambda item: (-item[1], label_key(item[0])))[:LABELS]
129
+ return {"labels": [label for label, _ in found], "counts": [count for _, count in found]}
130
+
131
+
132
+ def values_stats(frame, pred, bins=40):
133
+ series = frame[pred]
134
+ if not numeric(series):
135
+ found = [value for value in texts(series).tolist() if value is not None]
136
+ return {"count": len(found), "mean": None, "std": None, "hist": None, "quantiles": None,
137
+ "labels": label_counts(found)}
138
+ values = finite(numbers_of(series))
139
+ count = int(len(values))
140
+ spread = {"min": float(values.min()) if count else None, **quantiles_of(values),
141
+ "max": float(values.max()) if count else None}
142
+ return {"count": count, "mean": float(values.mean()) if count else None,
143
+ "std": float(values.std()) if count else None, "hist": histogram(values, bins), "quantiles": spread,
144
+ "labels": None}