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,56 @@
|
|
|
1
|
+
from kalfa.records import reader
|
|
2
|
+
from kalfa.std.plots.common import LEGEND_LIMIT, color_of, directory_of, dress, quiet, spans_decades
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
DEFAULT_PREFIXES = ("train/", "val/")
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def turn_word(directory):
|
|
9
|
+
manifest = quiet(reader.read_manifest, directory) or {}
|
|
10
|
+
return "epoch" if manifest.get("unit") == "epochs" else "turn"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def history_series(history, names=None):
|
|
14
|
+
found = {}
|
|
15
|
+
for position, line in enumerate(history):
|
|
16
|
+
turn = reader.turn_of(line, position)
|
|
17
|
+
for key, value in line.items():
|
|
18
|
+
if not isinstance(key, str) or not reader.is_number(value):
|
|
19
|
+
continue
|
|
20
|
+
if names is None and not key.startswith(DEFAULT_PREFIXES):
|
|
21
|
+
continue
|
|
22
|
+
if names is not None and key not in names:
|
|
23
|
+
continue
|
|
24
|
+
turns, values = found.setdefault(key, ([], []))
|
|
25
|
+
turns.append(turn)
|
|
26
|
+
values.append(value)
|
|
27
|
+
if names is not None:
|
|
28
|
+
return {name: found[name] for name in names if name in found}
|
|
29
|
+
return found
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def loss_curve(record, figures, series=None):
|
|
33
|
+
from matplotlib.ticker import MaxNLocator
|
|
34
|
+
|
|
35
|
+
directory = directory_of(record)
|
|
36
|
+
history = reader.read_history(directory)
|
|
37
|
+
names = None if series is None else ([series] if isinstance(series, str) else [str(name) for name in series])
|
|
38
|
+
found = history_series(history, names)
|
|
39
|
+
if not found:
|
|
40
|
+
return None
|
|
41
|
+
figure = figures.new(8.0, 5.0)
|
|
42
|
+
axis = figure.subplots()
|
|
43
|
+
for position, (name, (turns, values)) in enumerate(found.items()):
|
|
44
|
+
axis.plot(turns, values, label=name, color=color_of(figures, position), linewidth=1.8)
|
|
45
|
+
if spans_decades([value for _, values in found.values() for value in values]):
|
|
46
|
+
axis.set_yscale("log")
|
|
47
|
+
if len(found) <= LEGEND_LIMIT:
|
|
48
|
+
axis.legend(loc="upper right", frameon=False, ncols=1 if len(found) < 6 else 2)
|
|
49
|
+
word = turn_word(directory)
|
|
50
|
+
axis.xaxis.set_major_locator(MaxNLocator(integer=True))
|
|
51
|
+
axis.set_title(f"Training history ({len(history)} {word}s, {len(found)} series)", loc="left")
|
|
52
|
+
axis.set_xlabel(word)
|
|
53
|
+
axis.set_ylabel("value")
|
|
54
|
+
dress(axis)
|
|
55
|
+
figures.save(figure, "loss_curve")
|
|
56
|
+
return None
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import math
|
|
2
|
+
|
|
3
|
+
import numpy
|
|
4
|
+
|
|
5
|
+
from kalfa.core.errors import KalfaError
|
|
6
|
+
from kalfa.std.plots.common import directory_of
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
TEXT_LINES = 20
|
|
10
|
+
TEXT_WIDTH = 120
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def first_output(outputs):
|
|
14
|
+
import torch
|
|
15
|
+
|
|
16
|
+
if isinstance(outputs, torch.Tensor):
|
|
17
|
+
return "output", outputs
|
|
18
|
+
items = outputs.items() if isinstance(outputs, dict) else enumerate(outputs or ())
|
|
19
|
+
for name, value in items:
|
|
20
|
+
if isinstance(value, torch.Tensor):
|
|
21
|
+
return str(name), value
|
|
22
|
+
return None, None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def image_field(ctx, batch):
|
|
26
|
+
import torch
|
|
27
|
+
|
|
28
|
+
for name in getattr(getattr(ctx, "data", None), "inputs", None) or ():
|
|
29
|
+
if isinstance(batch.get(name), torch.Tensor):
|
|
30
|
+
return str(name)
|
|
31
|
+
for name, value in batch.items():
|
|
32
|
+
if isinstance(value, torch.Tensor) and value.ndim in (3, 4):
|
|
33
|
+
return str(name)
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def shaped(output, images, name):
|
|
38
|
+
if output.shape[1:] == images.shape[1:]:
|
|
39
|
+
return output
|
|
40
|
+
if output.ndim >= 2 and len(output) == len(images) and output[0].numel() == images[0].numel():
|
|
41
|
+
return output.reshape(images.shape)
|
|
42
|
+
if output.ndim in (3, 4):
|
|
43
|
+
return output
|
|
44
|
+
raise KalfaError(f"image_pairs: the output {name!r} of shape {tuple(output.shape)} cannot be drawn like the "
|
|
45
|
+
f"inputs of shape {tuple(images.shape)}")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def pixels(images):
|
|
49
|
+
images = images.detach().float().cpu()
|
|
50
|
+
if images.ndim == 4:
|
|
51
|
+
if images.shape[1] not in (1, 3):
|
|
52
|
+
images = images[:, :1]
|
|
53
|
+
images = images.permute(0, 2, 3, 1)
|
|
54
|
+
if images.shape[-1] == 1:
|
|
55
|
+
images = images[..., 0]
|
|
56
|
+
return images.numpy()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def show(axis, image, low, high):
|
|
60
|
+
scaled = numpy.clip((image - low) / (high - low), 0.0, 1.0) if high > low else numpy.zeros_like(image)
|
|
61
|
+
if scaled.ndim == 2:
|
|
62
|
+
axis.imshow(scaled, cmap="gray", vmin=0.0, vmax=1.0, interpolation="nearest")
|
|
63
|
+
else:
|
|
64
|
+
axis.imshow(scaled, interpolation="nearest")
|
|
65
|
+
axis.set_xticks([])
|
|
66
|
+
axis.set_yticks([])
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def image_pairs(record, figures, n=8, set="test", ctx=None):
|
|
70
|
+
import torch
|
|
71
|
+
|
|
72
|
+
if ctx is None:
|
|
73
|
+
raise KalfaError("image_pairs needs the run; redraw it with the record open")
|
|
74
|
+
loaders = ctx.loaders()
|
|
75
|
+
loader = loaders.get(set)
|
|
76
|
+
if loader is None:
|
|
77
|
+
raise KalfaError(f"image_pairs: the data has no {set!r} set; its sets are {sorted(loaders)}")
|
|
78
|
+
modes = getattr(ctx, "set_modes", None)
|
|
79
|
+
if callable(modes):
|
|
80
|
+
modes(False)
|
|
81
|
+
device = getattr(ctx, "device", None)
|
|
82
|
+
count = max(int(n), 1)
|
|
83
|
+
field = name = None
|
|
84
|
+
shown, guessed = [], []
|
|
85
|
+
with torch.no_grad():
|
|
86
|
+
for batch in loader:
|
|
87
|
+
if device is not None:
|
|
88
|
+
batch = device.move(batch)
|
|
89
|
+
if not isinstance(batch, dict):
|
|
90
|
+
raise KalfaError("image_pairs needs batches that map field names to tensors")
|
|
91
|
+
field = field or image_field(ctx, batch)
|
|
92
|
+
if field is None:
|
|
93
|
+
raise KalfaError(f"image_pairs: none of the batch fields {sorted(batch)} holds images")
|
|
94
|
+
images = batch[field]
|
|
95
|
+
if images.ndim not in (3, 4):
|
|
96
|
+
raise KalfaError(f"image_pairs: the input {field!r} of shape {tuple(images.shape)} is not a batch of "
|
|
97
|
+
f"images")
|
|
98
|
+
name, output = first_output(ctx.step.predict(batch, ctx))
|
|
99
|
+
if output is None:
|
|
100
|
+
raise KalfaError("image_pairs: the run predicts no tensor; train.predicts names the model to draw")
|
|
101
|
+
shown.append(images.detach().float().cpu())
|
|
102
|
+
guessed.append(shaped(output, images, name).detach().float().cpu())
|
|
103
|
+
if sum(len(piece) for piece in shown) >= count:
|
|
104
|
+
break
|
|
105
|
+
if not shown:
|
|
106
|
+
return None
|
|
107
|
+
inputs = pixels(torch.cat(shown)[:count])
|
|
108
|
+
outputs = pixels(torch.cat(guessed)[:count])
|
|
109
|
+
total = min(len(inputs), len(outputs))
|
|
110
|
+
values = numpy.concatenate([inputs[:total].reshape(-1), outputs[:total].reshape(-1)])
|
|
111
|
+
values = values[numpy.isfinite(values)]
|
|
112
|
+
low, high = (float(values.min()), float(values.max())) if values.size else (0.0, 1.0)
|
|
113
|
+
figure = figures.new(max(1.3 * total + 0.8, 3.2), 3.2)
|
|
114
|
+
axes = figure.subplots(2, total, squeeze=False)
|
|
115
|
+
for row, (images, label) in enumerate(((inputs, field), (outputs, name))):
|
|
116
|
+
for column in range(total):
|
|
117
|
+
show(axes[row][column], images[column], low, high)
|
|
118
|
+
axes[row][0].set_ylabel(label)
|
|
119
|
+
figure.suptitle(f"{field} and {name} ({set}, n = {total})", x=0.02, ha="left")
|
|
120
|
+
figures.save(figure, "image_pairs")
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def sample_images(samples, path):
|
|
125
|
+
import torch
|
|
126
|
+
|
|
127
|
+
if isinstance(samples, (list, tuple)) and samples and all(isinstance(item, torch.Tensor) for item in samples):
|
|
128
|
+
try:
|
|
129
|
+
samples = torch.stack([item.detach().cpu() for item in samples])
|
|
130
|
+
except RuntimeError as error:
|
|
131
|
+
raise KalfaError(f"samples_grid: the tensors in {path.name} differ in shape: {error}") from None
|
|
132
|
+
if not isinstance(samples, torch.Tensor) or samples.ndim not in (3, 4) or not len(samples):
|
|
133
|
+
if isinstance(samples, torch.Tensor):
|
|
134
|
+
kind = f"a tensor of shape {tuple(samples.shape)}"
|
|
135
|
+
else:
|
|
136
|
+
kind = f"a {type(samples).__name__}"
|
|
137
|
+
raise KalfaError(f"samples_grid: {path.name} holds {kind}, not a batch of images (N, C, H, W)")
|
|
138
|
+
images = samples.detach().float().cpu()
|
|
139
|
+
if images.ndim == 4 and images.shape[1] not in (1, 3):
|
|
140
|
+
images = images[:, :1]
|
|
141
|
+
return images
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def text_sheet(path, figures, lines=None):
|
|
145
|
+
if lines is None:
|
|
146
|
+
lines = path.read_text(encoding="utf-8", errors="replace").splitlines()
|
|
147
|
+
shown = [line.expandtabs(4) for line in lines[:TEXT_LINES]]
|
|
148
|
+
shown = [line if len(line) <= TEXT_WIDTH else line[:TEXT_WIDTH - 3] + "..." for line in shown]
|
|
149
|
+
figure = figures.new(10.0, 0.8 + 0.22 * max(len(shown), 1))
|
|
150
|
+
axis = figure.subplots()
|
|
151
|
+
axis.axis("off")
|
|
152
|
+
axis.text(0.0, 1.0, "\n".join(shown) or "(empty)", family="monospace", fontsize=9, va="top", ha="left",
|
|
153
|
+
transform=axis.transAxes, parse_math=False)
|
|
154
|
+
axis.set_title(f"{path.name} (first {len(shown)} of {len(lines)} lines)", loc="left")
|
|
155
|
+
figures.save(figure, "samples_grid")
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def samples_grid(record, figures, n=8, file="samples"):
|
|
160
|
+
import torch
|
|
161
|
+
|
|
162
|
+
from kalfa.std.samples import image_grid, is_text
|
|
163
|
+
|
|
164
|
+
directory = directory_of(record) / "samples"
|
|
165
|
+
stem = str(file)
|
|
166
|
+
names = [stem] if stem.endswith((".pt", ".txt")) else [f"{stem}.pt", f"{stem}.txt"]
|
|
167
|
+
path = next((directory / name for name in names if (directory / name).is_file()), None)
|
|
168
|
+
if path is None:
|
|
169
|
+
wanted = " or ".join(f"samples/{name}" for name in names)
|
|
170
|
+
raise KalfaError(f"samples_grid: {wanted} is missing in {directory.parent}; a sampler under generate or a "
|
|
171
|
+
f"write_samples rule writes it")
|
|
172
|
+
if path.suffix == ".txt":
|
|
173
|
+
return text_sheet(path, figures)
|
|
174
|
+
loaded = torch.load(path, map_location="cpu", weights_only=False)
|
|
175
|
+
if is_text(loaded):
|
|
176
|
+
return text_sheet(path, figures, "\n\n".join(loaded).splitlines())
|
|
177
|
+
images = sample_images(loaded, path)
|
|
178
|
+
count = min(len(images), max(int(n), 1))
|
|
179
|
+
grid = image_grid(images[:count], columns=math.ceil(math.sqrt(count)))
|
|
180
|
+
height, width = grid.shape[:2]
|
|
181
|
+
scale = 8.0 / max(height, width)
|
|
182
|
+
figure = figures.new(max(width * scale + 0.4, 3.0), height * scale + 0.8)
|
|
183
|
+
axis = figure.subplots()
|
|
184
|
+
axis.imshow(grid, interpolation="nearest")
|
|
185
|
+
axis.axis("off")
|
|
186
|
+
axis.set_title(f"{path.name} ({count} of {len(images)} samples)", loc="left")
|
|
187
|
+
figures.save(figure, "samples_grid")
|
|
188
|
+
return None
|
kalfa/std/plots/pairs.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from kalfa.records import stats
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def read_predictions(directory):
|
|
5
|
+
frame = stats.read_predictions(directory)
|
|
6
|
+
return frame if frame is not None and len(frame) else None
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def pred_column(output):
|
|
10
|
+
return output if str(output).startswith("pred_") else f"pred_{output}"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def prediction_pairs(frame, plan, pairs=None):
|
|
14
|
+
columns = [str(column) for column in frame.columns]
|
|
15
|
+
if pairs:
|
|
16
|
+
chosen = [(pred_column(output), str(target)) for output, target in dict(pairs).items()]
|
|
17
|
+
return [(pred, target) for pred, target in chosen if pred in columns and target in columns]
|
|
18
|
+
found = [(pair["pred"], pair["target"]) for pair in stats.prediction_pairs(frame, plan)]
|
|
19
|
+
paired = {pred for pred, _ in found}
|
|
20
|
+
preds = [column for column in columns if column.startswith("pred_") and column not in paired
|
|
21
|
+
and stats.spread_member(column, columns) is None]
|
|
22
|
+
targets = [name for name in stats.plan_targets(plan) if name in columns]
|
|
23
|
+
if not targets:
|
|
24
|
+
targets = [column for column in columns
|
|
25
|
+
if not column.startswith(("pred_", "raw_", "prob_")) and column not in ("row", "set")]
|
|
26
|
+
if len(targets) == 1:
|
|
27
|
+
found.extend((pred, targets[0]) for pred in preds)
|
|
28
|
+
return found
|
|
29
|
+
for pred in preds:
|
|
30
|
+
output = pred[len("pred_"):]
|
|
31
|
+
matches = [target for target in targets if output == target or output.endswith(f"_{target}")]
|
|
32
|
+
if matches:
|
|
33
|
+
found.append((pred, max(matches, key=len)))
|
|
34
|
+
return found
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def pick_pair(frame, plan, pred=None, target=None):
|
|
38
|
+
column = None if pred is None else pred_column(pred)
|
|
39
|
+
if column is not None and target is not None:
|
|
40
|
+
columns = [str(name) for name in frame.columns]
|
|
41
|
+
return (column, str(target)) if column in columns and str(target) in columns else None
|
|
42
|
+
for found_pred, found_target in prediction_pairs(frame, plan):
|
|
43
|
+
if column is not None and found_pred != column:
|
|
44
|
+
continue
|
|
45
|
+
if target is not None and found_target != str(target):
|
|
46
|
+
continue
|
|
47
|
+
return found_pred, found_target
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def chosen_pair(frame, plan, fitted, pred=None, target=None, prefer=None):
|
|
52
|
+
columns = [str(column) for column in frame.columns]
|
|
53
|
+
column = None if pred is None else pred_column(pred)
|
|
54
|
+
if column is not None and target is not None:
|
|
55
|
+
target = str(target)
|
|
56
|
+
if column not in columns or target not in columns:
|
|
57
|
+
return None
|
|
58
|
+
output = column[len("pred_"):]
|
|
59
|
+
scores = [name for name in columns if name.startswith(f"prob_{output}_")]
|
|
60
|
+
raw = f"raw_{output}" if f"raw_{output}" in columns else None
|
|
61
|
+
kind = stats.kind_of(frame, target, stats.classes_for(plan, fitted, target), scores)
|
|
62
|
+
return {"output": output, "pred": column, "target": target, "kind": kind, "raw": raw, "scores": scores}
|
|
63
|
+
found = [pair for pair in stats.prediction_pairs(frame, plan, fitted)
|
|
64
|
+
if (column is None or pair["pred"] == column) and (target is None or pair["target"] == str(target))]
|
|
65
|
+
preferred = [pair for pair in found if pair["kind"] == prefer]
|
|
66
|
+
return (preferred or found or [None])[0]
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
import numpy
|
|
4
|
+
|
|
5
|
+
from kalfa.records import reader, stats
|
|
6
|
+
from kalfa.std.plots.common import color_of, directory_of, dress, label_text, quiet
|
|
7
|
+
from kalfa.std.plots.pairs import chosen_pair, pred_column, prediction_pairs, read_predictions
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
HORIZON = re.compile(r"(.+)_(\d+)")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def numeric(frame, column):
|
|
14
|
+
return frame[column].dtype.kind in "fiu"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def r_squared(truth, guess):
|
|
18
|
+
spread = float(numpy.sum((truth - truth.mean()) ** 2))
|
|
19
|
+
if spread <= 0:
|
|
20
|
+
return None
|
|
21
|
+
return 1.0 - float(numpy.sum((guess - truth) ** 2)) / spread
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def pred_vs_true(record, figures, pairs=None):
|
|
25
|
+
directory = directory_of(record)
|
|
26
|
+
frame = read_predictions(directory)
|
|
27
|
+
if frame is None:
|
|
28
|
+
return None
|
|
29
|
+
chosen = [(pred, target) for pred, target in prediction_pairs(frame, quiet(reader.read_plan, directory), pairs)
|
|
30
|
+
if numeric(frame, pred) and numeric(frame, target)]
|
|
31
|
+
if not chosen:
|
|
32
|
+
return None
|
|
33
|
+
columns = min(3, len(chosen))
|
|
34
|
+
rows = -(-len(chosen) // columns)
|
|
35
|
+
figure = figures.new(4.8 * columns, 4.4 * rows)
|
|
36
|
+
panels = [axis for row in figure.subplots(rows, columns, squeeze=False) for axis in row]
|
|
37
|
+
for axis, (pred, target) in zip(panels, chosen):
|
|
38
|
+
truth = frame[target].to_numpy(dtype="float64", na_value=numpy.nan)
|
|
39
|
+
guess = frame[pred].to_numpy(dtype="float64", na_value=numpy.nan)
|
|
40
|
+
keep = numpy.isfinite(truth) & numpy.isfinite(guess)
|
|
41
|
+
truth, guess = truth[keep], guess[keep]
|
|
42
|
+
if not len(truth):
|
|
43
|
+
axis.axis("off")
|
|
44
|
+
continue
|
|
45
|
+
axis.scatter(truth, guess, s=12, alpha=0.5, color=color_of(figures, 0), edgecolors="none", rasterized=True)
|
|
46
|
+
low = float(min(truth.min(), guess.min()))
|
|
47
|
+
high = float(max(truth.max(), guess.max()))
|
|
48
|
+
axis.plot([low, high], [low, high], color=color_of(figures, 1), linewidth=1.5, label="y = x")
|
|
49
|
+
axis.legend(loc="upper left", frameon=False)
|
|
50
|
+
score = r_squared(truth, guess)
|
|
51
|
+
note = f"R² {score:.3f}, " if score is not None else ""
|
|
52
|
+
axis.set_title(f"{target} ({note}n = {len(truth):,})", loc="left")
|
|
53
|
+
axis.set_xlabel(f"true {target}")
|
|
54
|
+
axis.set_ylabel(f"predicted ({pred[len('pred_'):]})")
|
|
55
|
+
dress(axis)
|
|
56
|
+
for axis in panels[len(chosen):]:
|
|
57
|
+
axis.axis("off")
|
|
58
|
+
figures.save(figure, "pred_vs_true")
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def indexed(columns, name):
|
|
63
|
+
found = {}
|
|
64
|
+
for column in columns:
|
|
65
|
+
match = HORIZON.fullmatch(column)
|
|
66
|
+
if match is not None and match.group(1) == name:
|
|
67
|
+
found[int(match.group(2))] = column
|
|
68
|
+
return found
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def horizon_pair(frame, plan, pred=None, target=None):
|
|
72
|
+
columns = [str(column) for column in frame.columns]
|
|
73
|
+
stems = []
|
|
74
|
+
for column in columns:
|
|
75
|
+
match = HORIZON.fullmatch(column)
|
|
76
|
+
if match is not None and match.group(1) not in stems:
|
|
77
|
+
stems.append(match.group(1))
|
|
78
|
+
if pred is not None:
|
|
79
|
+
outputs = [pred_column(pred)[len("pred_"):]]
|
|
80
|
+
else:
|
|
81
|
+
outputs = [stem[len("pred_"):] for stem in stems if stem.startswith("pred_")]
|
|
82
|
+
known = [stem for stem in stems if not stem.startswith(("pred_", "raw_", "prob_"))]
|
|
83
|
+
pairings = stats.plan_pairings(plan)
|
|
84
|
+
for output in outputs:
|
|
85
|
+
guesses = indexed(columns, f"pred_{output}")
|
|
86
|
+
names = [str(target)] if target is not None else [pairings.get(output), *stats.plan_targets(plan), *known]
|
|
87
|
+
for name in names:
|
|
88
|
+
truths = indexed(columns, name) if name else {}
|
|
89
|
+
steps = sorted(step for step in guesses if step in truths)
|
|
90
|
+
if steps:
|
|
91
|
+
return output, name, [truths[step] for step in steps], [guesses[step] for step in steps]
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def forecast_series(frame, plan, fitted, figures, pred, target):
|
|
96
|
+
pair = chosen_pair(frame, plan, fitted, pred, target, prefer="regression")
|
|
97
|
+
if pair is None or not numeric(frame, pair["pred"]) or not numeric(frame, pair["target"]):
|
|
98
|
+
return None
|
|
99
|
+
rows = frame["row"].to_numpy() if "row" in frame.columns else numpy.arange(len(frame))
|
|
100
|
+
truth = frame[pair["target"]].to_numpy(dtype="float64", na_value=numpy.nan)
|
|
101
|
+
guess = frame[pair["pred"]].to_numpy(dtype="float64", na_value=numpy.nan)
|
|
102
|
+
figure = figures.new(9.0, 4.2)
|
|
103
|
+
axis = figure.subplots()
|
|
104
|
+
axis.plot(rows, truth, color=color_of(figures, 0), linewidth=1.4, label="true")
|
|
105
|
+
axis.plot(rows, guess, color=color_of(figures, 1), linewidth=1.4, linestyle="--", label="predicted")
|
|
106
|
+
axis.legend(loc="upper right", frameon=False)
|
|
107
|
+
axis.set_title(f"{pair['target']} over rows (n = {len(frame):,})", loc="left")
|
|
108
|
+
axis.set_xlabel("row")
|
|
109
|
+
axis.set_ylabel(f"{pair['target']} and {pair['output']}")
|
|
110
|
+
dress(axis)
|
|
111
|
+
figures.save(figure, "forecast_samples")
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def forecast_samples(record, figures, n=4, pred=None, target=None):
|
|
116
|
+
from matplotlib.ticker import MaxNLocator
|
|
117
|
+
|
|
118
|
+
directory = directory_of(record)
|
|
119
|
+
frame = stats.read_predictions(directory)
|
|
120
|
+
if frame is None or not len(frame):
|
|
121
|
+
return None
|
|
122
|
+
plan = quiet(reader.read_plan, directory)
|
|
123
|
+
if "row" in frame.columns:
|
|
124
|
+
frame = frame.sort_values("row", kind="stable")
|
|
125
|
+
found = horizon_pair(frame, plan, pred, target)
|
|
126
|
+
if found is None:
|
|
127
|
+
return forecast_series(frame, plan, quiet(reader.fields_of, directory), figures, pred, target)
|
|
128
|
+
output, name, truths, guesses = found
|
|
129
|
+
picked = numpy.unique(numpy.linspace(0, len(frame) - 1, min(max(int(n), 1), len(frame))).round().astype(int))
|
|
130
|
+
columns = 1 if len(picked) == 1 else 2
|
|
131
|
+
rows = -(-len(picked) // columns)
|
|
132
|
+
figure = figures.new(5.2 * columns, 2.4 * rows + 0.8)
|
|
133
|
+
panels = [axis for line in figure.subplots(rows, columns, squeeze=False) for axis in line]
|
|
134
|
+
steps = numpy.arange(1, len(truths) + 1)
|
|
135
|
+
truth = frame[truths].to_numpy(dtype="float64", na_value=numpy.nan)
|
|
136
|
+
guess = frame[guesses].to_numpy(dtype="float64", na_value=numpy.nan)
|
|
137
|
+
labels = frame["row"].to_numpy() if "row" in frame.columns else frame.index.to_numpy()
|
|
138
|
+
for index, (axis, position) in enumerate(zip(panels, picked)):
|
|
139
|
+
axis.plot(steps, truth[position], color=color_of(figures, 0), linewidth=1.8, marker="o", markersize=3,
|
|
140
|
+
label="true")
|
|
141
|
+
axis.plot(steps, guess[position], color=color_of(figures, 1), linewidth=1.8, linestyle="--",
|
|
142
|
+
label="predicted")
|
|
143
|
+
axis.set_title(f"row {label_text(labels[position])}", loc="left")
|
|
144
|
+
axis.xaxis.set_major_locator(MaxNLocator(integer=True))
|
|
145
|
+
if index + columns >= len(picked):
|
|
146
|
+
axis.set_xlabel("steps ahead")
|
|
147
|
+
dress(axis)
|
|
148
|
+
for axis in panels[len(picked):]:
|
|
149
|
+
axis.axis("off")
|
|
150
|
+
panels[0].legend(loc="upper left", frameon=False)
|
|
151
|
+
figure.suptitle(f"Forecast windows ({name} against {output}, {len(steps)} steps, {len(picked)} of "
|
|
152
|
+
f"{len(frame):,} rows)", x=0.02, ha="left")
|
|
153
|
+
figures.save(figure, "forecast_samples")
|
|
154
|
+
return None
|