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,642 @@
1
+ import json
2
+ import logging
3
+ from pathlib import Path
4
+
5
+ import joblib
6
+ import numpy
7
+ import pandas
8
+ import torch
9
+ from torch.utils.data import Dataset
10
+
11
+ from kalfa.core import aliases
12
+ from kalfa.core.errors import BuildError, error, warning
13
+ from kalfa.std.data import flow
14
+ from kalfa.std.data.base import Loaders, config_directories, seeded_split
15
+ from kalfa.std.data.columns import (
16
+ Block,
17
+ Target,
18
+ apply_step,
19
+ as_matrix,
20
+ chain_of,
21
+ floats_of,
22
+ inverts,
23
+ is_pattern,
24
+ label_step,
25
+ mapping_of,
26
+ matches,
27
+ matches_any,
28
+ name_problems,
29
+ names_after,
30
+ names_of,
31
+ owners_of,
32
+ specificity,
33
+ step_names,
34
+ step_of,
35
+ target_of,
36
+ tensor_of,
37
+ )
38
+ from kalfa.std.data.options import (
39
+ BATCH_DEFAULTS,
40
+ FILTER_KEYS,
41
+ SET_NAMES,
42
+ STREAM_SPLIT,
43
+ batch_of,
44
+ batch_spec,
45
+ filters_of,
46
+ locate,
47
+ query,
48
+ read_table,
49
+ split_check,
50
+ split_of,
51
+ split_target,
52
+ stream_check,
53
+ stream_split_check,
54
+ )
55
+ from kalfa.std.data.weights import scale_of, weights_of
56
+
57
+
58
+ __all__ = ["BATCH_DEFAULTS", "Block", "FILTER_KEYS", "REGISTRATIONS", "SET_NAMES", "STREAM_SPLIT", "Table",
59
+ "TableDataset", "Target", "VALIDATORS", "apply_step", "as_matrix", "batch_of", "batch_spec", "chain_of",
60
+ "filters_of", "floats_of", "inverts", "is_pattern", "label_step", "locate", "mapping_of", "matches",
61
+ "matches_any", "names_after", "names_of", "owners_of", "query", "read_table", "register_aliases",
62
+ "specificity", "split_check", "split_of", "split_target", "step_names", "step_of", "stream_check",
63
+ "stream_split_check", "target_of", "tensor_of"]
64
+
65
+ logger = logging.getLogger("kalfa.data")
66
+
67
+ REGISTRATIONS = (
68
+ ("table", "kalfa.std.data.table.Table", "data",
69
+ "a parquet or csv table: split into sets, feature chains fitted on the train rows; the batch carries x "
70
+ "(float32 features) and every target by name"),
71
+ ("random", "kalfa.std.data.splits.RandomSplit", "value",
72
+ "shuffle the rows with a seed and cut them by ratios into train, valid and test"),
73
+ ("sequential", "kalfa.std.data.splits.SequentialSplit", "value",
74
+ "cut the rows in their order by ratios, within each group when group names a column"),
75
+ ("kfold", "kalfa.std.data.splits.KFoldSplit", "value",
76
+ "fold number fold of folds shuffled folds is the test set, the rest train; valid_ratio carves a valid share"),
77
+ ("given", "kalfa.std.data.splits.GivenSplit", "value",
78
+ "a column's values name the set of every row: train, valid and test"),
79
+ ("standard_scaler", "sklearn.preprocessing.StandardScaler", "value", "zero mean and unit variance per column"),
80
+ ("minmax_scaler", "sklearn.preprocessing.MinMaxScaler", "value",
81
+ "every column scaled into [0, 1], or into feature_range"),
82
+ ("robust_scaler", "sklearn.preprocessing.RobustScaler", "value",
83
+ "centered on the median and scaled by the interquartile range per column"),
84
+ ("one_hot", "kalfa.std.data.pre.one_hot", "value",
85
+ "one column per category, named <column>_<category>; unknown categories give zeros"),
86
+ ("label", "kalfa.std.data.pre.Label", "value",
87
+ "integer class codes of a label column; on a target it makes a classification task"),
88
+ ("abs", "kalfa.std.data.pre.Abs", "value", "absolute value"),
89
+ ("log", "kalfa.std.data.pre.Log", "value", "log(1 + x / norm) / log(base), inverted exactly"),
90
+ ("cast", "kalfa.std.data.pre.Cast", "value", "cast to a numpy dtype"),
91
+ )
92
+
93
+
94
+ def register_aliases():
95
+ for name, target, slot, doc in REGISTRATIONS:
96
+ aliases.register(name, target, slot=slot, doc=doc)
97
+
98
+
99
+ class TableDataset(Dataset):
100
+ def __init__(self, x, targets, rows):
101
+ self.x = x
102
+ self.targets = targets
103
+ self.rows = rows
104
+
105
+ def __len__(self):
106
+ return int(self.x.shape[0])
107
+
108
+ def __getitem__(self, position):
109
+ item = {"x": self.x[position]}
110
+ for name, values in self.targets.items():
111
+ item[name] = values[position]
112
+ return item
113
+
114
+
115
+ VALIDATORS = (
116
+ ("features", lambda value: mapping_of(value, "features")),
117
+ ("targets", lambda value: mapping_of(value, "targets")),
118
+ ("stream", stream_check),
119
+ ("filter", filters_of),
120
+ ("drop", lambda value: names_of(value, "drop")),
121
+ ("spectators", lambda value: names_of(value, "spectators")),
122
+ ("batch", batch_spec),
123
+ ("split", split_check),
124
+ )
125
+
126
+
127
+ class Table(Loaders):
128
+ inputs = ("x",)
129
+ exposes = ("n_features", "feature_names", "target_names", "n_classes", "class_weights", "classes")
130
+
131
+ @classmethod
132
+ def check(cls, params, problems, directories=()):
133
+ if params.get("split") is None:
134
+ problems.append(error("split_required", "data.split is required: a list of ratios such as "
135
+ "[0.7, 0.15, 0.15] or a call such as {use: kfold, folds: 5, fold: 0}", "data.split"))
136
+ file = params.get("file")
137
+ if isinstance(file, str) and file:
138
+ try:
139
+ locate(file, directories)
140
+ except BuildError as failure:
141
+ problems.append(warning("data_file_missing", str(failure), "data.file",
142
+ hint="the run looks in the working directory, then next to the config"))
143
+ for name, validator in VALIDATORS:
144
+ if name not in params:
145
+ continue
146
+ try:
147
+ validator(params[name])
148
+ except BuildError as failure:
149
+ problems.append(error("data_value", str(failure), f"data.{name}"))
150
+ for section in ("features", "targets"):
151
+ if isinstance(params.get(section), dict):
152
+ for key, chain in params[section].items():
153
+ name_problems(chain, f"data.{section}.{key}", problems)
154
+ name_problems(params.get("split"), "data.split", problems)
155
+ try:
156
+ stream_split_check(params.get("stream"), params.get("split"))
157
+ except BuildError as failure:
158
+ problems.append(error("data_value", str(failure), "data.split"))
159
+ try:
160
+ weights_of(params.get("weights"), params.get("targets"), params.get("drop"))
161
+ except BuildError as failure:
162
+ problems.append(error("data_value", str(failure), "data.weights"))
163
+
164
+ def __init__(self, file, split=None, features=None, targets=None, filter=None, drop=None, spectators=None,
165
+ feed=None, batch=128, stream=False, weights=None):
166
+ self.file = file
167
+ self.feed = feed
168
+ self.engine = None
169
+ if stream:
170
+ from kalfa.std.data.stream import StreamEngine
171
+
172
+ self.engine = StreamEngine(self, stream)
173
+ self.split = split_of(split)
174
+ self.features = mapping_of(features, "features")
175
+ self.targets = mapping_of(targets, "targets")
176
+ if "x" in self.targets:
177
+ raise BuildError("a target cannot be named x; x is the feature tensor")
178
+ self.filter = filter
179
+ self.all_filter, self.set_filters = filters_of(filter)
180
+ self.drop = names_of(drop, "drop")
181
+ self.weights = weights_of(weights, self.targets, self.drop)
182
+ self.weight_scales = {}
183
+ self.spectator_patterns = names_of(spectators, "spectators")
184
+ self.batch = batch_spec(batch)
185
+ self.blocks = []
186
+ self.fitted_targets = []
187
+ self.feature_names = []
188
+ self.n_features = 0
189
+ self.target_names = list(self.targets)
190
+ self.n_classes = None
191
+ self.classes = None
192
+ self.class_weights = None
193
+ self.datasets = {}
194
+ self.spectator_frames = {}
195
+ self.counts = {}
196
+ self.read_rows = None
197
+ self.read_columns = None
198
+ self.kept_rows = None
199
+ self.rng = None
200
+ self.loaders_by_set = None
201
+ self.fitted = False
202
+
203
+ def sets_of(self, ctx):
204
+ if self.engine is not None:
205
+ sets = self.engine.sets_of(ctx)
206
+ self.read_rows = self.engine.reader.count()
207
+ self.read_columns = len(self.engine.reader.header())
208
+ return sets
209
+ frame = read_table(locate(self.file, config_directories(ctx)))
210
+ self.read_rows = len(frame)
211
+ self.read_columns = len(frame.columns)
212
+ frame = self.filtered(frame)
213
+ self.kept_rows = len(frame)
214
+ sets = {name: part for name, part in seeded_split(self.split, ctx).split(frame).items() if len(part)}
215
+ for name, text in self.set_filters.items():
216
+ if name in sets:
217
+ sets[name] = query(sets[name], text, f"filter.{name}")
218
+ if not len(sets[name]):
219
+ raise BuildError(f"filter.{name}: the query {text!r} leaves the {name} set empty")
220
+ if "train" not in sets:
221
+ raise BuildError("the split leaves no train rows")
222
+ return {name: self.dropped(self.weighed(part)) for name, part in sets.items()}
223
+
224
+ def materialize(self, sets, ctx):
225
+ self.rng = getattr(ctx, "rng", None)
226
+ self.datasets = {}
227
+ self.spectator_frames = {}
228
+ self.counts = {}
229
+ previous = None
230
+ for name, part in sets.items():
231
+ if isinstance(part, pandas.DataFrame):
232
+ self.datasets[name] = self.dataset_of(part, name, context=previous)
233
+ self.spectator_frames[name] = self.spectators_of(part)
234
+ self.counts[name] = len(part)
235
+ previous = self.context_of(part)
236
+ else:
237
+ self.datasets[name] = self.engine.dataset(part, name)
238
+ self.spectator_frames[name] = None
239
+ self.counts[name] = self.engine.count(part)
240
+ previous = None
241
+ self.loaders_by_set = None
242
+ rows = ", ".join(f"{name} {count}" for name, count in self.counts.items())
243
+ logger.info(f"data: {rows} rows; {self.n_features} features; targets {self.target_names}")
244
+ windows = self.windows()
245
+ if windows is not None:
246
+ counted = ", ".join(f"{name} {count}" for name, count in windows.items())
247
+ logger.info(f"data: {counted} windows from the {type(self.feed).__name__} feed")
248
+
249
+ def windows(self):
250
+ if self.feed is None or not self.datasets:
251
+ return None
252
+ return {name: len(dataset) for name, dataset in self.datasets.items()}
253
+
254
+ def fit(self, ctx):
255
+ sets = self.sets_of(ctx)
256
+ train = sets["train"] if self.engine is None else self.engine.fit_sample(sets)
257
+ self.blocks = self.blocks_of(list(train.columns))
258
+ self.fit_blocks(train)
259
+ self.fitted_targets = self.fit_targets(train)
260
+ self.weight_scales = self.fit_weights(train)
261
+ self.expose()
262
+ self.fitted = True
263
+ self.materialize(sets, ctx)
264
+
265
+ def rebuild(self, ctx):
266
+ if not self.fitted:
267
+ raise BuildError("the data is not fitted; load(directory) first")
268
+ self.materialize(self.sets_of(ctx), ctx)
269
+
270
+ def filtered(self, frame):
271
+ return query(frame, self.all_filter, "filter") if self.all_filter else frame
272
+
273
+ def dropped(self, frame):
274
+ dropping = [column for column in self.drop if column in frame.columns]
275
+ return frame.drop(columns=dropping) if dropping else frame
276
+
277
+ def weighed(self, frame):
278
+ if not self.weights:
279
+ return frame
280
+ added = {}
281
+ for weight in self.weights:
282
+ if weight.name in frame.columns and weight.name != weight.column:
283
+ raise BuildError(f"weights.{weight.name}: the table already has a column {weight.name!r}; give the "
284
+ "weight field another name")
285
+ added[weight.name] = weight.values(frame)
286
+ return frame.assign(**added)
287
+
288
+ def weight_columns(self):
289
+ names = [weight.name for weight in self.weights]
290
+ return set(names) | {weight.column for weight in self.weights if weight.column is not None}
291
+
292
+ def fit_weights(self, train):
293
+ scales = {}
294
+ for weight in self.weights:
295
+ scales[weight.name] = scale_of(weight, train[weight.name].to_numpy(dtype="float64"))
296
+ logger.info(f"data: weight {weight.name} is scaled by {scales[weight.name]:.6g} so its train mean is one")
297
+ return scales
298
+
299
+ def weight_tensors(self, frame):
300
+ found = {}
301
+ for weight in self.weights:
302
+ if weight.name not in frame.columns:
303
+ continue
304
+ if weight.name not in self.weight_scales:
305
+ raise BuildError(f"weights.{weight.name}: the weight was not fitted; the fitted files hold no scale "
306
+ "for it")
307
+ values = frame[weight.name].to_numpy(dtype="float64") * self.weight_scales[weight.name]
308
+ found[weight.name] = tensor_of(values, "float32")
309
+ return found
310
+
311
+ def blocks_of(self, columns):
312
+ missing = [name for name in self.targets if name not in columns]
313
+ if missing:
314
+ raise BuildError(f"target columns {missing} are not in the table; the columns are {columns}")
315
+ spectators = [column for column in columns if matches_any(column, self.spectator_patterns)]
316
+ group = getattr(self.feed, "group", None)
317
+ weights = self.weight_columns()
318
+ candidates = [column for column in columns if column not in self.targets and column not in spectators
319
+ and column != group and column not in weights]
320
+ owners = owners_of(candidates, list(self.features))
321
+ blocks = []
322
+ for pattern, spec in self.features.items():
323
+ matched = [column for column in candidates if owners.get(column) == pattern]
324
+ if not matched:
325
+ if any(matches(column, pattern) for column in candidates):
326
+ logger.info(f"features pattern {pattern!r}: more specific patterns take every column it matches")
327
+ continue
328
+ raise BuildError(f"features pattern {pattern!r} matches no column; the columns are {candidates}")
329
+ blocks.append(Block(pattern, matched, chain_of(spec, f"features.{pattern}")))
330
+ unmatched = [column for column in candidates if column not in owners]
331
+ if unmatched:
332
+ logger.warning(f"columns {unmatched} match no features pattern and are not targets or spectators; "
333
+ f"they are dropped")
334
+ return blocks
335
+
336
+ def fit_blocks(self, train):
337
+ for block in self.blocks:
338
+ where = f"features.{block.pattern}"
339
+ names = list(block.columns)
340
+ values = as_matrix(train[block.columns].to_numpy())
341
+ for step in block.chain:
342
+ values = apply_step(step, values, where, fit=True)
343
+ names = names_after(step, names, values.shape[1])
344
+ floats_of(values, where)
345
+ block.names = names
346
+ self.feature_names = [name for block in self.blocks for name in block.names]
347
+ self.n_features = len(self.feature_names)
348
+
349
+ def fit_targets(self, train):
350
+ out = []
351
+ for name, spec in self.targets.items():
352
+ where = f"targets.{name}"
353
+ chain = chain_of(spec, where)
354
+ values = as_matrix(train[[name]].to_numpy())
355
+ for step in chain:
356
+ values = apply_step(step, values, where, fit=True)
357
+ target = Target(name, chain, label_step(chain), int(values.shape[1]))
358
+ if target.label is not None:
359
+ codes = numpy.asarray(values, dtype="int64").reshape(-1)
360
+ target.classes = target.label.classes_.tolist()
361
+ counts = numpy.bincount(codes, minlength=len(target.classes)).astype("float64")
362
+ weights = 1.0 / counts
363
+ target.class_weights = torch.as_tensor(weights / weights.mean(), dtype=torch.float32)
364
+ else:
365
+ floats_of(values, where)
366
+ out.append(target)
367
+ return out
368
+
369
+ def expose(self):
370
+ self.target_names = [target.name for target in self.fitted_targets]
371
+ first = next((target for target in self.fitted_targets if target.label is not None), None)
372
+ self.n_classes = None if first is None else len(first.classes)
373
+ self.classes = None if first is None else list(first.classes)
374
+ self.class_weights = None if first is None else first.class_weights
375
+
376
+ def features_of(self, frame):
377
+ missing = [column for block in self.blocks for column in block.columns if column not in frame.columns]
378
+ if missing:
379
+ raise BuildError(f"the table lacks the feature columns {missing}")
380
+ pieces = []
381
+ for block in self.blocks:
382
+ where = f"features.{block.pattern}"
383
+ values = as_matrix(frame[block.columns].to_numpy())
384
+ for step in block.chain:
385
+ values = apply_step(step, values, where, fit=False)
386
+ pieces.append(floats_of(values, where))
387
+ if not pieces:
388
+ return torch.zeros((len(frame), 0), dtype=torch.float32)
389
+ return tensor_of(numpy.concatenate(pieces, axis=1) if len(pieces) > 1 else pieces[0], "float32")
390
+
391
+ def target_tensor(self, target, frame):
392
+ where = f"targets.{target.name}"
393
+ values = as_matrix(frame[[target.name]].to_numpy())
394
+ for step in target.chain:
395
+ values = apply_step(step, values, where, fit=False)
396
+ if target.label is not None:
397
+ return tensor_of(numpy.asarray(values).reshape(-1), "int64")
398
+ return tensor_of(floats_of(values, where), "float32")
399
+
400
+ def tensors_of(self, frame, set_name, strict=True):
401
+ targets = {}
402
+ for target in self.fitted_targets:
403
+ if target.name not in frame.columns:
404
+ if strict:
405
+ raise BuildError(f"the {set_name} set lacks the target column {target.name!r}")
406
+ continue
407
+ targets[target.name] = self.target_tensor(target, frame)
408
+ targets.update(self.weight_tensors(frame))
409
+ return self.features_of(frame), targets, numpy.asarray(frame.index)
410
+
411
+ def groups_of(self, frame):
412
+ group = getattr(self.feed, "group", None)
413
+ if group is None:
414
+ return None
415
+ if group not in frame.columns:
416
+ raise BuildError(f"feed.group names the column {group!r}, which the table lacks")
417
+ return frame[group].to_numpy()
418
+
419
+ def context_of(self, frame):
420
+ if self.feed is None or not getattr(self.feed, "context", False):
421
+ return None
422
+ x, targets, rows = self.tensors_of(frame, "context", strict=False)
423
+ return x, targets, rows, self.groups_of(frame)
424
+
425
+ def dataset_of(self, frame, set_name, strict=True, context=None):
426
+ if not len(frame):
427
+ raise BuildError(f"the {set_name} set has no rows")
428
+ x, targets, rows = self.tensors_of(frame, set_name, strict)
429
+ if self.feed is not None:
430
+ return self.feed.dataset(x, targets, rows, self.groups_of(frame), context)
431
+ return TableDataset(x, targets, rows)
432
+
433
+ def spectators_of(self, frame):
434
+ return frame[[column for column in frame.columns if matches_any(column, self.spectator_patterns)]]
435
+
436
+ def spectators(self, set_name):
437
+ return self.spectator_frames.get(set_name)
438
+
439
+ def loader_for(self, frame, set_name="test"):
440
+ if not self.fitted:
441
+ raise BuildError("the data is not fitted; call fit(ctx) or load(directory) first")
442
+ if not isinstance(frame, pandas.DataFrame):
443
+ raise BuildError(f"loader_for takes a DataFrame, got {type(frame).__name__}")
444
+ prepared = self.dropped(self.filtered(frame))
445
+ return self.make_loader(self.dataset_of(prepared, set_name, strict=False), train=False)
446
+
447
+ def fitted_target(self, name):
448
+ for target in self.fitted_targets:
449
+ if target.name == name:
450
+ return target
451
+ raise BuildError(f"unknown target {name!r}; the targets are {self.target_names}")
452
+
453
+ def inverse(self, name, values):
454
+ target = self.fitted_target(name)
455
+ is_tensor = isinstance(values, torch.Tensor)
456
+ array = values.detach().cpu().numpy() if is_tensor else numpy.asarray(values)
457
+ steps = [step for step in target.chain if inverts(step)]
458
+ if steps:
459
+ if array.size % target.width:
460
+ raise BuildError(f"inverse({name!r}): the shape {array.shape} does not split into columns of width "
461
+ f"{target.width}")
462
+ flat = numpy.asarray(array, dtype="float64").reshape(-1, target.width)
463
+ for step in reversed(steps):
464
+ flat = as_matrix(step.inverse_transform(flat))
465
+ out = numpy.asarray(flat, dtype="float32").reshape(array.shape)
466
+ else:
467
+ out = numpy.asarray(array, dtype="float32")
468
+ if is_tensor:
469
+ return torch.from_numpy(numpy.array(out, dtype="float32", copy=True)).to(values.device)
470
+ return out
471
+
472
+ def decode(self, name, values):
473
+ target = self.fitted_target(name)
474
+ if target.label is None:
475
+ raise BuildError(f"target {name!r} has no label preprocessor; decode needs one")
476
+ array = values.detach().cpu().numpy() if isinstance(values, torch.Tensor) else numpy.asarray(values)
477
+ if array.ndim >= 2 and array.shape[-1] != 1:
478
+ if array.shape[-1] != len(target.classes):
479
+ raise BuildError(f"decode({name!r}): takes class indices or scores of width {len(target.classes)}, "
480
+ f"got the shape {array.shape}")
481
+ array = array.argmax(axis=-1)
482
+ codes = numpy.asarray(array).astype("int64").reshape(-1)
483
+ return numpy.asarray(target.label.inverse_transform(codes))
484
+
485
+ def rescales(self, name):
486
+ for target in self.fitted_targets:
487
+ if target.name == name:
488
+ return any(inverts(step) for step in target.chain)
489
+ return False
490
+
491
+ def is_label(self, name):
492
+ return any(target.name == name and target.label is not None for target in self.fitted_targets)
493
+
494
+ def save(self, directory):
495
+ if not self.fitted:
496
+ raise BuildError("the data is not fitted; there is nothing to save")
497
+ root = Path(directory) / "fitted"
498
+ root.mkdir(parents=True, exist_ok=True)
499
+ joblib.dump({"features": [block.chain for block in self.blocks],
500
+ "targets": [target.chain for target in self.fitted_targets]}, root / "preprocessors.joblib")
501
+ fields = {
502
+ "features": [{"pattern": block.pattern, "columns": list(block.columns), "names": list(block.names),
503
+ "chain": step_names(block.chain)} for block in self.blocks],
504
+ "targets": list(self.target_names),
505
+ "target_fields": [{"name": target.name, "width": target.width, "chain": step_names(target.chain),
506
+ "classes": target.classes,
507
+ "class_weights": None if target.class_weights is None
508
+ else target.class_weights.tolist()}
509
+ for target in self.fitted_targets],
510
+ "feature_names": list(self.feature_names),
511
+ "classes": self.classes,
512
+ "drops": list(self.drop),
513
+ "spectators": list(self.spectator_patterns),
514
+ "filter": self.all_filter,
515
+ "weights": [{"name": name, "scale": scale} for name, scale in self.weight_scales.items()],
516
+ }
517
+ (root / "fields.json").write_text(json.dumps(fields, indent=2))
518
+
519
+ def load(self, directory):
520
+ root = Path(directory) / "fitted"
521
+ try:
522
+ fields = json.loads((root / "fields.json").read_text())
523
+ chains = joblib.load(root / "preprocessors.joblib")
524
+ except FileNotFoundError as error:
525
+ raise BuildError(f"{root} lacks {Path(error.filename).name}; save(directory) writes the fitted "
526
+ f"files") from error
527
+ self.blocks = [Block(item["pattern"], list(item["columns"]), chain, list(item["names"]))
528
+ for item, chain in zip(fields["features"], chains["features"])]
529
+ self.fitted_targets = []
530
+ for item, chain in zip(fields["target_fields"], chains["targets"]):
531
+ target = Target(item["name"], chain, label_step(chain), int(item["width"]), item.get("classes"))
532
+ if item.get("class_weights") is not None:
533
+ target.class_weights = torch.tensor(item["class_weights"], dtype=torch.float32)
534
+ self.fitted_targets.append(target)
535
+ self.feature_names = list(fields["feature_names"])
536
+ self.n_features = len(self.feature_names)
537
+ self.drop = list(fields.get("drops") or [])
538
+ self.spectator_patterns = list(fields.get("spectators") or [])
539
+ self.all_filter = fields.get("filter")
540
+ self.weight_scales = {item["name"]: float(item["scale"]) for item in fields.get("weights") or []}
541
+ self.expose()
542
+ self.datasets = {}
543
+ self.spectator_frames = {}
544
+ self.counts = {}
545
+ self.read_rows = None
546
+ self.read_columns = None
547
+ self.kept_rows = None
548
+ self.loaders_by_set = None
549
+ self.fitted = True
550
+ return self
551
+
552
+ def report(self):
553
+ columns = {}
554
+ for block in self.blocks:
555
+ for name in block.names:
556
+ columns[name] = step_names(block.chain)
557
+ found = {"rows": dict(self.counts), "features": self.n_features, "targets": list(self.target_names),
558
+ "classes": None if self.classes is None else list(self.classes), "columns": columns,
559
+ "windows": self.windows()}
560
+ if self.weights:
561
+ found["weights"] = {weight.name: {"from": weight.source(), "scale": self.weight_scales.get(weight.name)}
562
+ for weight in self.weights}
563
+ return found
564
+
565
+ def feature_blocks(self):
566
+ if self.fitted:
567
+ return [(block.pattern, list(block.columns), self.features.get(block.pattern), block.chain)
568
+ for block in self.blocks]
569
+ return [(pattern, None, spec, ()) for pattern, spec in self.features.items()]
570
+
571
+ def target_specs(self):
572
+ if self.fitted:
573
+ return [(target.name, self.targets.get(target.name), target.chain, target.classes, target.width)
574
+ for target in self.fitted_targets]
575
+ return [(name, spec, (), None, None) for name, spec in self.targets.items()]
576
+
577
+ def set_details(self):
578
+ notes = {name: [f"where {text}"] for name, text in self.set_filters.items()}
579
+ if self.engine is not None:
580
+ notes.setdefault("train", []).insert(0, "streamed")
581
+ for name, count in (self.windows() or {}).items():
582
+ notes.setdefault(name, []).append(f"{count} windows")
583
+ return {name: ", ".join(parts) for name, parts in notes.items()}
584
+
585
+ def row_lane(self, graph, source):
586
+ rows = source
587
+ if self.engine is not None:
588
+ chunk, fit = self.engine.options["chunk"], self.engine.options["fit_rows"]
589
+ rows = graph.step(rows, "stream", "stream", "stream",
590
+ f"chunks of {chunk} rows, chains fit on at most {fit} train rows")
591
+ if self.all_filter and self.engine is None:
592
+ rows = graph.step(rows, "filter", "filter", "filter", self.all_filter, self.kept_rows)
593
+ rows = flow.split_node(graph, rows, self.split)
594
+ if self.all_filter and self.engine is not None:
595
+ rows = graph.step(rows, "filter", "filter", "filter", self.all_filter)
596
+ made = flow.set_nodes(graph, rows, self.counts, self.set_details(), flow.shares_of(self.split))
597
+ return list(made.values()) or [rows]
598
+
599
+ def column_lane(self, graph, source):
600
+ columns = source
601
+ if self.drop:
602
+ columns = graph.step(source, "drop", "drop", "drop", flow.listed(self.drop), len(self.drop))
603
+ ends = []
604
+ for position, (pattern, names, spec, chain) in enumerate(self.feature_blocks()):
605
+ key = graph.step(columns, f"features:{position}", "features", pattern, flow.listed(names),
606
+ None if names is None else len(names))
607
+ ends += graph.chain([key], f"features:{position}", flow.steps_of(spec, chain), "chain", pattern,
608
+ "feature chain, fitted on the train rows")
609
+ x = graph.node("tensor:x", "tensor", "x", f"{self.n_features} features" if self.fitted else "features")
610
+ graph.join(ends or [columns], x)
611
+ tensors = [x]
612
+ for name, spec, chain, classes, width in self.target_specs():
613
+ key = graph.step(columns, f"targets:{name}", "targets", name,
614
+ flow.listed(classes) if classes else "target column", len(classes) if classes else None)
615
+ ends = graph.chain([key], f"targets:{name}", flow.steps_of(spec, chain), "chain", name,
616
+ "target chain, fitted on the train rows")
617
+ if classes:
618
+ about = f"{len(classes)} classes"
619
+ else:
620
+ about = "" if width is None else f"{width} value" if width == 1 else f"{width} values"
621
+ tensor = graph.node(f"tensor:{name}", "tensor", name, about)
622
+ graph.join(ends, tensor)
623
+ tensors.append(tensor)
624
+ for weight in self.weights:
625
+ key = graph.step(source, f"weights:{weight.name}", "weights", weight.name, weight.detail())
626
+ tensor = graph.node(f"tensor:{weight.name}", "tensor", weight.name, "row weights, mean one on train")
627
+ graph.join([key], tensor)
628
+ tensors.append(tensor)
629
+ return tensors
630
+
631
+ def pipeline(self):
632
+ graph = flow.Flow()
633
+ columns = None if self.read_columns is None else f"{self.read_columns} columns"
634
+ source = flow.source_node(graph, self.file, self.read_rows, [columns])
635
+ ends = self.row_lane(graph, source) + self.column_lane(graph, source)
636
+ if self.feed is not None:
637
+ ends = [flow.feed_node(graph, ends, self.feed, self.windows())]
638
+ flow.batch_node(graph, ends, self.batch)
639
+ return graph.result()
640
+
641
+
642
+ register_aliases()