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.
- kalfa/__init__.py +19 -0
- kalfa/api.py +284 -0
- kalfa/board/__init__.py +1 -0
- kalfa/board/answers.py +419 -0
- kalfa/board/server.py +536 -0
- kalfa/board/static/api.js +176 -0
- kalfa/board/static/app.js +122 -0
- kalfa/board/static/board.css +228 -0
- kalfa/board/static/charts.css +46 -0
- kalfa/board/static/components/badge.js +14 -0
- kalfa/board/static/components/chart.js +204 -0
- kalfa/board/static/components/diagram.js +857 -0
- kalfa/board/static/components/error.js +17 -0
- kalfa/board/static/components/loading.js +7 -0
- kalfa/board/static/components/progress.js +25 -0
- kalfa/board/static/components/settings.js +418 -0
- kalfa/board/static/components/spark.js +43 -0
- kalfa/board/static/components/table.js +156 -0
- kalfa/board/static/diagram.css +76 -0
- kalfa/board/static/format.js +138 -0
- kalfa/board/static/index.html +34 -0
- kalfa/board/static/plot.js +594 -0
- kalfa/board/static/predictions.css +20 -0
- kalfa/board/static/store.js +215 -0
- kalfa/board/static/views/compare.js +185 -0
- kalfa/board/static/views/home.js +90 -0
- kalfa/board/static/views/record/config.js +81 -0
- kalfa/board/static/views/record/curves.js +137 -0
- kalfa/board/static/views/record/data.js +378 -0
- kalfa/board/static/views/record/files.js +128 -0
- kalfa/board/static/views/record/log.js +164 -0
- kalfa/board/static/views/record/model.js +286 -0
- kalfa/board/static/views/record/overview.js +216 -0
- kalfa/board/static/views/record/plots.js +103 -0
- kalfa/board/static/views/record/predictions.js +834 -0
- kalfa/board/static/views/record.js +179 -0
- kalfa/board/static/views/runs.js +124 -0
- kalfa/board/static/views/sweep.js +282 -0
- kalfa/board/watch.py +87 -0
- kalfa/cli.py +371 -0
- kalfa/commands/__init__.py +0 -0
- kalfa/commands/common.py +8 -0
- kalfa/commands/export.py +117 -0
- kalfa/commands/generate.py +31 -0
- kalfa/commands/guide/__init__.py +101 -0
- kalfa/commands/guide/about.py +93 -0
- kalfa/commands/guide/calls.py +102 -0
- kalfa/commands/guide/sections.py +186 -0
- kalfa/commands/guide/seen.py +307 -0
- kalfa/commands/guide/show.py +106 -0
- kalfa/commands/guide/training.py +333 -0
- kalfa/commands/guide/words.py +97 -0
- kalfa/commands/legos.py +508 -0
- kalfa/commands/plots.py +27 -0
- kalfa/commands/report/__init__.py +64 -0
- kalfa/commands/report/page.py +112 -0
- kalfa/commands/report/predictions.py +216 -0
- kalfa/commands/report/sections.py +356 -0
- kalfa/commands/stop.py +60 -0
- kalfa/commands/sweep/__init__.py +202 -0
- kalfa/commands/sweep/checks.py +98 -0
- kalfa/commands/sweep/plan.py +183 -0
- kalfa/commands/sweep/points.py +123 -0
- kalfa/commands/sweep/runner.py +100 -0
- kalfa/commands/sweep/space.py +67 -0
- kalfa/commands/sweep/strategies.py +228 -0
- kalfa/core/__init__.py +0 -0
- kalfa/core/aliases.py +229 -0
- kalfa/core/build.py +727 -0
- kalfa/core/check.py +550 -0
- kalfa/core/coerce.py +36 -0
- kalfa/core/context.py +149 -0
- kalfa/core/device.py +63 -0
- kalfa/core/errors.py +45 -0
- kalfa/core/graph.py +304 -0
- kalfa/core/history.py +70 -0
- kalfa/core/loop.py +710 -0
- kalfa/core/make.py +121 -0
- kalfa/core/plan.py +156 -0
- kalfa/core/predict.py +290 -0
- kalfa/core/protocols.py +122 -0
- kalfa/core/record.py +347 -0
- kalfa/core/refs.py +88 -0
- kalfa/core/rng.py +100 -0
- kalfa/core/rules.py +513 -0
- kalfa/core/run.py +568 -0
- kalfa/core/spec.py +413 -0
- kalfa/core/state.py +100 -0
- kalfa/core/wrap.py +671 -0
- kalfa/records/__init__.py +24 -0
- kalfa/records/compare.py +86 -0
- kalfa/records/reader.py +561 -0
- kalfa/records/stats/__init__.py +36 -0
- kalfa/records/stats/choices.py +282 -0
- kalfa/records/stats/classes.py +236 -0
- kalfa/records/stats/measures.py +144 -0
- kalfa/records/stats/pairs.py +225 -0
- kalfa/records/stats/tables.py +153 -0
- kalfa/records/sweep.py +244 -0
- kalfa/std/__init__.py +17 -0
- kalfa/std/actions.py +35 -0
- kalfa/std/common.py +56 -0
- kalfa/std/data/__init__.py +1 -0
- kalfa/std/data/base.py +45 -0
- kalfa/std/data/columns.py +190 -0
- kalfa/std/data/flow.py +317 -0
- kalfa/std/data/images.py +443 -0
- kalfa/std/data/options.py +147 -0
- kalfa/std/data/pre.py +84 -0
- kalfa/std/data/splits.py +140 -0
- kalfa/std/data/stream.py +294 -0
- kalfa/std/data/table.py +642 -0
- kalfa/std/data/text.py +436 -0
- kalfa/std/data/transforms.py +327 -0
- kalfa/std/data/weights.py +113 -0
- kalfa/std/data/window.py +135 -0
- kalfa/std/figure.py +37 -0
- kalfa/std/init.py +72 -0
- kalfa/std/loggers.py +105 -0
- kalfa/std/losses.py +161 -0
- kalfa/std/metrics.py +463 -0
- kalfa/std/models.py +122 -0
- kalfa/std/notes.py +1553 -0
- kalfa/std/objectives.py +360 -0
- kalfa/std/plots/__init__.py +75 -0
- kalfa/std/plots/classification.py +207 -0
- kalfa/std/plots/common.py +44 -0
- kalfa/std/plots/curves.py +56 -0
- kalfa/std/plots/images.py +188 -0
- kalfa/std/plots/pairs.py +66 -0
- kalfa/std/plots/predictions.py +154 -0
- kalfa/std/samplers.py +261 -0
- kalfa/std/samples.py +72 -0
- kalfa/std/schedules.py +74 -0
- kalfa/std/steps.py +133 -0
- kalfa/std/triggers.py +135 -0
- kalfa/summary.py +296 -0
- kalfa/tui/__init__.py +4 -0
- kalfa/tui/app.py +107 -0
- kalfa/tui/config.py +61 -0
- kalfa/tui/guide.py +100 -0
- kalfa/tui/jobs.py +97 -0
- kalfa/tui/legos.py +62 -0
- kalfa/tui/records.py +439 -0
- kalfa-3.0.0.dev0.dist-info/METADATA +261 -0
- kalfa-3.0.0.dev0.dist-info/RECORD +148 -0
- kalfa-3.0.0.dev0.dist-info/WHEEL +4 -0
- 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}
|