ialdev-dataman 0.1.0__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.
- iad/dataman/__init__.py +22 -0
- iad/dataman/datacast/__init__.py +8 -0
- iad/dataman/datacast/caster.py +523 -0
- iad/dataman/datacast/collect.py +1466 -0
- iad/dataman/datacast/labeled.py +80 -0
- iad/dataman/datacast/scan.py +178 -0
- iad/dataman/datacast/transforms.py +40 -0
- iad/dataman/datacast/transtools.py +608 -0
- iad/dataman/env.py +10 -0
- iad/dataman/factories.py +143 -0
- iad/dataman/models.py +425 -0
- iad/dataman/resman.py +1229 -0
- ialdev_dataman-0.1.0.dist-info/METADATA +33 -0
- ialdev_dataman-0.1.0.dist-info/RECORD +15 -0
- ialdev_dataman-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Union, Callable, Iterable, Literal, Any, Collection, get_args
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import pandas as pd
|
|
9
|
+
|
|
10
|
+
from iad.core import as_list, pdtools as pdt
|
|
11
|
+
from iad.core.fnctools import O, Namespace
|
|
12
|
+
from iad.core.label import Labels
|
|
13
|
+
from iad.core.wrap import name_tuple
|
|
14
|
+
|
|
15
|
+
EMPTY_CELL = (None, pd.NA, np.nan)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def transforms() -> dict[str, Callable]:
|
|
19
|
+
"""Return namespace of transforms defined in transform module."""
|
|
20
|
+
|
|
21
|
+
from . import transforms
|
|
22
|
+
_ns = vars(transforms)
|
|
23
|
+
return {name: _ns[name] for name in transforms.__all__}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def applicable(fnc, *pos_names, **kws_map):
|
|
27
|
+
"""
|
|
28
|
+
Given a function creates its wrap usable in pd.DataFrame.apply
|
|
29
|
+
Some of its arguments to be associated to the pd.DataFrame columns,
|
|
30
|
+
the rest must be either default or passed as `**kwargs` in `apply`.
|
|
31
|
+
|
|
32
|
+
Example:
|
|
33
|
+
>>> def f(x, y=0, *, z, w=None): pass
|
|
34
|
+
>>> af = applicable(f, 'data', z='weight') # data as first arg
|
|
35
|
+
>>> df = pd.DataFrame(dict(data=[1,2,3], weight=[0,1,2]))
|
|
36
|
+
>>> df['new'] = df.apply(af, axis=1, y=2, w=100)
|
|
37
|
+
|
|
38
|
+
:param fnc: function to be wrapped into the new interface
|
|
39
|
+
:param pos_names: column names of the positinal parameters
|
|
40
|
+
:param kws_map: mappings between the kw arguments and columns names
|
|
41
|
+
:return: function used for apply
|
|
42
|
+
"""
|
|
43
|
+
assert pos_names or kws_map, "At least one argument is required"
|
|
44
|
+
|
|
45
|
+
def apply_func(dct, **kwargs):
|
|
46
|
+
if hasattr(dct, '__getitem__'):
|
|
47
|
+
args = (dct[p] for p in pos_names)
|
|
48
|
+
kws = {par: dct[field] for par, field in kws_map.items()}
|
|
49
|
+
return fnc(*args, **kws, **kwargs)
|
|
50
|
+
else:
|
|
51
|
+
return fnc(dct, **kwargs)
|
|
52
|
+
|
|
53
|
+
return apply_func
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def apply_to(df: pd.DataFrame, func: Callable, out, *args, **kwargs):
|
|
57
|
+
"""Create new dataframe with Apply func to the data-frame into `out` column.
|
|
58
|
+
|
|
59
|
+
:param df: the data-frame
|
|
60
|
+
:param func: function to be passed to `df.apply`
|
|
61
|
+
:param out: column where the result is placed
|
|
62
|
+
:param args: arguments passed to `func`
|
|
63
|
+
:param kwargs: keyword arguments passed to `func`
|
|
64
|
+
:return The modified input `df`
|
|
65
|
+
"""
|
|
66
|
+
import warnings
|
|
67
|
+
with warnings.catch_warnings():
|
|
68
|
+
warnings.filterwarnings('error')
|
|
69
|
+
ndf = df.copy()
|
|
70
|
+
ndf[out] = df.apply(func, *args, axis=1, **kwargs)
|
|
71
|
+
return df
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass
|
|
75
|
+
class Col:
|
|
76
|
+
"""Standard columns names"""
|
|
77
|
+
path: str = 'path'
|
|
78
|
+
data: str = 'data'
|
|
79
|
+
ext: str = 'ext'
|
|
80
|
+
desc: str = 'desc'
|
|
81
|
+
description: str = 'description'
|
|
82
|
+
transforms: str = 'transforms'
|
|
83
|
+
transformed: str = 'transformed'
|
|
84
|
+
align_trans: str = 'align_trans'
|
|
85
|
+
read_trans: str = 'read_trans'
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def apply_seq(df: pd.DataFrame, *trans, inp=Col.data, out=Col.data):
|
|
89
|
+
"""Apply sequence of transformations to the `df` columns
|
|
90
|
+
:param df:
|
|
91
|
+
:param trans: sequence of transformation stage instructions:
|
|
92
|
+
`(fnc, inp, out), (fnc, inp), (fnc, )` or `fnc`
|
|
93
|
+
:param inp: default inp column for the first stage. Others if their
|
|
94
|
+
`inp` is not specified use `out` from the previous stage.
|
|
95
|
+
:param out: default output column of any stage with unspecified `out`
|
|
96
|
+
|
|
97
|
+
Example:
|
|
98
|
+
>>> apply(df, [f1, f2, f3], inp=x, out=y)
|
|
99
|
+
means: `df[x] -> f1 -> df[y] -> f2 -> df[y] ->f3 -> df[y]`
|
|
100
|
+
>>> apply(df, [(f1, y1, x), (f2, y2), f3], out=y)
|
|
101
|
+
means: `df[x] -> f1 -> df[y1] -> f2 -> df[y2] -> f3 -> df[y]`,
|
|
102
|
+
So that in this case all the intermediate results are kept in y1, y2
|
|
103
|
+
"""
|
|
104
|
+
for t in trans:
|
|
105
|
+
fnc, _out, _inp = (*t, *(out, inp)[:3 - len(t)]) \
|
|
106
|
+
if isinstance(t, (list, tuple)) else (t, out, inp)
|
|
107
|
+
inp = _out # default inp for next stage is previous out
|
|
108
|
+
if not fnc:
|
|
109
|
+
continue
|
|
110
|
+
df[_out] = df[_inp].apply(applicable(fnc, _inp), axis=1)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def apply_transforms(df: pd.DataFrame, *, inp=Col.path, out=Col.data,
|
|
114
|
+
trans=Col.transforms, ns=None,
|
|
115
|
+
keep: Union[str, Iterable] = ()) -> pd.DataFrame:
|
|
116
|
+
"""
|
|
117
|
+
Apply sequence of data initialization transforms on the df.
|
|
118
|
+
Full sequence is:
|
|
119
|
+
[inp] -> read -> [out] -> {transforms} -> [out]
|
|
120
|
+
|
|
121
|
+
Stage `read` is applied only if defined (default is `imread`)
|
|
122
|
+
|
|
123
|
+
:param df: columns will be processed in this data frame
|
|
124
|
+
:param inp: name of first stage input column (if read then path)
|
|
125
|
+
:param out: name of output column
|
|
126
|
+
:param keep: `trans`, `inp` are dropped unless any is listed here
|
|
127
|
+
:param trans: name of column with transforms
|
|
128
|
+
:param ns: module or dict-like container with transform functions
|
|
129
|
+
:return:
|
|
130
|
+
"""
|
|
131
|
+
if ns is None:
|
|
132
|
+
ns = transforms()
|
|
133
|
+
|
|
134
|
+
trans = as_list(trans)
|
|
135
|
+
|
|
136
|
+
def row_transform(row: pd.Series):
|
|
137
|
+
return O.comp(*map(row.get, trans), ns=ns, skip=EMPTY_CELL)(row.get(inp))
|
|
138
|
+
|
|
139
|
+
drop = {*trans, inp}.difference({out}.union(as_list(keep)))
|
|
140
|
+
df = df.copy() # if df is a view, then df[out] = assigns to cached copy
|
|
141
|
+
|
|
142
|
+
if set(trans).issubset(df.columns):
|
|
143
|
+
df[out] = df.apply(row_transform, axis=1)
|
|
144
|
+
df.drop(drop, axis=1, inplace=True, errors='ignore')
|
|
145
|
+
return df
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def apply_column_transform(df: pd.Series | pd.DataFrame, *,
|
|
149
|
+
inp: str | Collection[str] = Col.path,
|
|
150
|
+
out: str = Col.data,
|
|
151
|
+
trans: str = Col.read_trans,
|
|
152
|
+
recover: bool = False,
|
|
153
|
+
args: Literal["series", "pos", "kws"] = 'pos',
|
|
154
|
+
fallback: Any = pd.NA,
|
|
155
|
+
drop: str | Collection[str] | bool = True,
|
|
156
|
+
inplace=False,
|
|
157
|
+
**kwargs) -> pd.DataFrame:
|
|
158
|
+
"""
|
|
159
|
+
Apply (by row) transforms stored in ``trans`` column on the ``inp`` column.
|
|
160
|
+
|
|
161
|
+
Default configuration is set for `read_trans` transformation to load data.
|
|
162
|
+
|
|
163
|
+
**See also**:
|
|
164
|
+
``compile_read_transforms`` to create ``read_trans`` with functions.
|
|
165
|
+
|
|
166
|
+
``apply_transforms`` as more general version of this function
|
|
167
|
+
|
|
168
|
+
By default, drop (True), all `used` columns (``trans`` and ``inp``).
|
|
169
|
+
Otherwise, specific columns to drop may be also specified, or None to keep.
|
|
170
|
+
|
|
171
|
+
Transform may receive arguments in three different forms:
|
|
172
|
+
- `pos` - as positional arguments ordered as in `inp` collection. `trans(*pos)`.
|
|
173
|
+
- `series` - row series as produced by the pandas `apply`. `trans(series)`
|
|
174
|
+
- `kws` - as keyword arguments with columns as keys: `trans(**kws)`
|
|
175
|
+
|
|
176
|
+
Transform failed on specific rows can be recovered by re-applying on the item
|
|
177
|
+
provided "fallback" function, or just producing given "error" value.
|
|
178
|
+
|
|
179
|
+
:param df: group (Series or Frame) usually produced
|
|
180
|
+
:param inp: name of the input column,
|
|
181
|
+
:param trans: column with transform functions objects
|
|
182
|
+
:param out: output column name
|
|
183
|
+
:param args: from in which arguments are passed into the `trans`
|
|
184
|
+
:param recover: if True - recover per row from failed transform using `fallback`
|
|
185
|
+
:param fallback: alternative callable or fallback value used to recover
|
|
186
|
+
:param drop: True to drop used columns, or specific columns to drop
|
|
187
|
+
:param kwargs: passed to `trans` function as additional kw args
|
|
188
|
+
:param inplace: update given ``DataFrame``, and return it
|
|
189
|
+
:return: transformed table
|
|
190
|
+
"""
|
|
191
|
+
# first make sure we work with frame
|
|
192
|
+
if isinstance(df, pd.Series):
|
|
193
|
+
if inplace:
|
|
194
|
+
raise TypeError("Can't inplace transform Series!")
|
|
195
|
+
df_out = df.to_frame()
|
|
196
|
+
elif not inplace:
|
|
197
|
+
df_out = df.copy()
|
|
198
|
+
else:
|
|
199
|
+
df_out = df
|
|
200
|
+
|
|
201
|
+
inputs = as_list(inp) or df_out.columns
|
|
202
|
+
|
|
203
|
+
if args == 'pos':
|
|
204
|
+
func = lambda g: g[trans](*(g[attr] for attr in inputs), **kwargs)
|
|
205
|
+
elif args == 'kws':
|
|
206
|
+
func = lambda g: g[trans](**{attr: g[attr] for attr in inputs}, **kwargs)
|
|
207
|
+
elif args == 'series':
|
|
208
|
+
func = lambda g: g[trans](g if inp is None else g[inputs], **kwargs)
|
|
209
|
+
else:
|
|
210
|
+
raise ValueError(f'Unknown {args=}')
|
|
211
|
+
|
|
212
|
+
if recover: # wrap func into (try - except) to ensure row-based resilience
|
|
213
|
+
if not isinstance(fallback, Callable):
|
|
214
|
+
error_value = fallback
|
|
215
|
+
fallback = lambda _: error_value
|
|
216
|
+
_func = func
|
|
217
|
+
|
|
218
|
+
def func(g):
|
|
219
|
+
try:
|
|
220
|
+
return _func(g)
|
|
221
|
+
except:
|
|
222
|
+
return fallback(g)
|
|
223
|
+
|
|
224
|
+
df_out[out] = df_out.apply(func, axis=1)
|
|
225
|
+
|
|
226
|
+
if drop is True:
|
|
227
|
+
drop = set(inputs) - {out} | {trans}
|
|
228
|
+
if drop:
|
|
229
|
+
if not inplace:
|
|
230
|
+
df.drop(drop, axis=1, inplace=True) # drop used columns in the original df, if returning new df
|
|
231
|
+
return df_out.drop(drop, axis=1)
|
|
232
|
+
df_out.drop(drop, axis=1, inplace=True) # drop used columns in the inplace df
|
|
233
|
+
return df_out
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def row_func_prepend_transform(fnc: Callable, trans=Col.read_trans, inp=Col.path, out=Col.data):
|
|
237
|
+
"""Prepend transform to the given row function
|
|
238
|
+
|
|
239
|
+
:param fnc: function to wrap with pre-transform
|
|
240
|
+
:param trans: name of the column with transform
|
|
241
|
+
:param inp: name of the columns used as input to the transform
|
|
242
|
+
:param out: name of the column to write transform results
|
|
243
|
+
"""
|
|
244
|
+
if not trans:
|
|
245
|
+
return fnc
|
|
246
|
+
|
|
247
|
+
def trans_fnc(row: pd.Series, *args, **kws):
|
|
248
|
+
row[out] = row[trans](row[inp])
|
|
249
|
+
return fnc(row, *args, **kws)
|
|
250
|
+
|
|
251
|
+
return trans_fnc
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _encode_transforms(series: pd.Series, invert=False) -> tuple[list[tuple[str]], dict[tuple[str], dict]]:
|
|
255
|
+
"""
|
|
256
|
+
Encode series with transforms (dict of dict or nan) in the cells into a two components data structure:
|
|
257
|
+
- series-size array of tuples with names of transforms, and
|
|
258
|
+
- map from such a tuple into the original transforms of the corresponding cell, or their inverse
|
|
259
|
+
|
|
260
|
+
More specifically:
|
|
261
|
+
- every cell of the series may contain dict of transforms ``C = {tr_name: tr_dict}``
|
|
262
|
+
- aggregated transform merges of all the cells dicts into one ``A``
|
|
263
|
+
- inverse cell transform ``IC`` is all the transforms in ``A`` except of those in ``C``: ``IC = A - C``
|
|
264
|
+
|
|
265
|
+
**ASSUMPTION**
|
|
266
|
+
|
|
267
|
+
Inversion implementation assumes that transforms are uniquely identified by their ``tr_name``
|
|
268
|
+
|
|
269
|
+
Return array of such `code names` and a map from name to the actual transforms, which
|
|
270
|
+
contains reference to cells with no transforms in the entry: ``"EMPTY": {}``.
|
|
271
|
+
|
|
272
|
+
:param series: a Series with transforms as dict of dict (or NONE)
|
|
273
|
+
:param invert: if TRue - request to construct map with inverse transforms
|
|
274
|
+
:return: array of code names per original cell, and map from the names to associated transforms
|
|
275
|
+
"""
|
|
276
|
+
series = series.reset_index(drop=True) # same index is used by the all_names np.array!
|
|
277
|
+
defined = series[~series.isna()] # work with cells actually containing transforms
|
|
278
|
+
|
|
279
|
+
if not all(filter(lambda x: isinstance(x, dict), defined)):
|
|
280
|
+
raise TypeError("Transforms must be dictionaries")
|
|
281
|
+
empty = 'EMPTY'
|
|
282
|
+
all_names = np.full(series.size, empty, dtype=object) # results initialized by empty names
|
|
283
|
+
cells_map = {empty: {}} # cell hash:tuple(names) -> transforms:dict
|
|
284
|
+
trans_map = {} # trans hash:str -> item:(func, {par}) - needed for INVERT case
|
|
285
|
+
|
|
286
|
+
for idx, cell_trans in defined.items():
|
|
287
|
+
items = [*cell_trans.items()] # list transform items in this cell
|
|
288
|
+
cell_names = tuple(map(str, items)) # tuple of items str (hashable!) in this cell
|
|
289
|
+
if cell_names not in cells_map: # new type of cell is found
|
|
290
|
+
cells_map[cell_names] = cell_trans # update mappings of cells and
|
|
291
|
+
invert and trans_map.update(zip(cell_names, items))
|
|
292
|
+
all_names[idx] = cell_names
|
|
293
|
+
|
|
294
|
+
if invert: # notice, cells names remain unconverted but still unique!
|
|
295
|
+
all_trans_names = set(trans_map) # crashes all transforms names together, ignores arguments!
|
|
296
|
+
cells_map = {cell: dict(map(trans_map.get, all_trans_names.difference(cell)))
|
|
297
|
+
for cell in cells_map}
|
|
298
|
+
return name_tuple(names=all_names, map=cells_map)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def compile_read_transforms(df: pd.DataFrame, *, col=Col, ns=None):
|
|
302
|
+
"""
|
|
303
|
+
From collection (usually a transformed column pd.Series) with dicts
|
|
304
|
+
describing transforms which have been applied on the data (in this row)
|
|
305
|
+
build a collection of transforms required to align the corresponding data
|
|
306
|
+
to same total transformation for entire collection.
|
|
307
|
+
|
|
308
|
+
Namely, in set terms:
|
|
309
|
+
``t_i -> T - t_i``, (where ``T = sum[ t_i ]``)
|
|
310
|
+
|
|
311
|
+
Assumes `commutability` of all the transformations!
|
|
312
|
+
|
|
313
|
+
If namespace with functions is provided, use it to substitute
|
|
314
|
+
dicts describing transforms to Callables from this namespace.
|
|
315
|
+
Otherwise, return ``np.ndarray`` with dicts or nans
|
|
316
|
+
|
|
317
|
+
:param df:
|
|
318
|
+
:param col: has attributes: transforms, transformed with names of those columns
|
|
319
|
+
:param ns: namespace with function to substitute from
|
|
320
|
+
:return: align_trans
|
|
321
|
+
"""
|
|
322
|
+
|
|
323
|
+
ns = ns and Namespace(ns) or {}
|
|
324
|
+
reader = (read_by_format, 'read')
|
|
325
|
+
|
|
326
|
+
# Prepare only transforms available in the columns
|
|
327
|
+
has_tm, has_td = map(df.columns.__contains__, [col.transforms, col.transformed])
|
|
328
|
+
if not (has_tm or has_td): # NO transforms columns - just use reader for all the cells
|
|
329
|
+
return np.full(len(df), O.comp(reader, ns=ns))
|
|
330
|
+
|
|
331
|
+
coded = dict( # hash only existing columns
|
|
332
|
+
td=has_td and _encode_transforms(df[col.transformed], invert=True),
|
|
333
|
+
tm=has_tm and _encode_transforms(df[col.transforms])
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
if has_td and has_tm: # BOTH transform types are defined
|
|
337
|
+
iterator = zip(coded['td'].names, coded['tm'].names) # iter over codes: (td-name, tm-name)
|
|
338
|
+
td_map, tm_map = coded['td'].map, coded['tm'].map
|
|
339
|
+
trans = lambda k: td_map[k[0]] | tm_map[k[1]] # combine from transforms and inversed transformed
|
|
340
|
+
else: # ONLY ONE of transform columns is defined
|
|
341
|
+
iterator, trans_map = coded[has_td and 'td' or 'tm'] # iter over codes: t[d|m]-name
|
|
342
|
+
trans = lambda k: trans_map[k] # function return transform given code
|
|
343
|
+
|
|
344
|
+
def trans_gen():
|
|
345
|
+
""" Generator of transforms for all the rows."""
|
|
346
|
+
cache = {} # Cache previously seen transforms to avoid expensive creations
|
|
347
|
+
for key in iterator:
|
|
348
|
+
if (t := cache.get(key, None)) is None:
|
|
349
|
+
t = O.comp(trans(key), reader, ns=ns)
|
|
350
|
+
cache[key] = t
|
|
351
|
+
yield t
|
|
352
|
+
|
|
353
|
+
return np.array([*trans_gen()])
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def is_data(item):
|
|
357
|
+
"""Check if item could be any data object:
|
|
358
|
+
|
|
359
|
+
`isinstance(item, Iterable) or not pd.isna(item)`
|
|
360
|
+
"""
|
|
361
|
+
return isinstance(item, Iterable) or not pd.isna(item)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
# noinspection PyTypeChecker
|
|
365
|
+
def cond_read_trans(s: pd.Series, check_data: bool | Callable = is_data, check_file=True):
|
|
366
|
+
"""
|
|
367
|
+
Conditional variant of ``read_trans`` function `applicable` to DataFrame with
|
|
368
|
+
columns: `Col.data`, `Col.path`, `Col.read_trans`.
|
|
369
|
+
|
|
370
|
+
Calls the original `s[Col.read_trans]` if two conditions are satisfied:
|
|
371
|
+
- `check_data(s[Col.data]) is False` AND
|
|
372
|
+
- `check_file` is False OR `s[Col.path].is_file`
|
|
373
|
+
|
|
374
|
+
:param s: pd.Series
|
|
375
|
+
:param check_data:
|
|
376
|
+
:param check_file: return ``pd.NA`` instead raising on not existing files
|
|
377
|
+
:return:
|
|
378
|
+
"""
|
|
379
|
+
if check_data and 'data' in s:
|
|
380
|
+
if (is_data if check_data is True else check_data)(data := s.data):
|
|
381
|
+
return data # return existing data
|
|
382
|
+
|
|
383
|
+
if path := s[Col.path]:
|
|
384
|
+
if check_file and not Path(path).is_file():
|
|
385
|
+
return pd.NA # file not found
|
|
386
|
+
read_trans: Callable = s[Col.read_trans]
|
|
387
|
+
return read_trans(path) # read file
|
|
388
|
+
|
|
389
|
+
return pd.NA # path is not defined
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def read_by_format(path):
|
|
393
|
+
"""
|
|
394
|
+
Read data from file by determining its format from its name or labels.
|
|
395
|
+
:param path:
|
|
396
|
+
:return: data from the file if format is found or None
|
|
397
|
+
"""
|
|
398
|
+
from iad.io.imread import imread, SUPPORT_READ
|
|
399
|
+
from iad.io.inu.utils.imread import imread_stereo
|
|
400
|
+
path = Path(path)
|
|
401
|
+
ext = path.suffix.lower()
|
|
402
|
+
if "StereoImage__Channel_" in path.name and ext == '.tif': # combined L and R from NU40
|
|
403
|
+
return imread_stereo(path, cam_info=True) # raise exception if there is no cam_info
|
|
404
|
+
elif ext in SUPPORT_READ:
|
|
405
|
+
return imread(path)
|
|
406
|
+
|
|
407
|
+
if path.name.lower() == 'calib.txt':
|
|
408
|
+
from iad.io.special import middlebury_calib
|
|
409
|
+
return middlebury_calib(path)
|
|
410
|
+
raise NotImplementedError(f"File's {str(path)} {ext=} is not among supported ({SUPPORT_READ=})")
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
# TODO: remove all the references, including docs
|
|
414
|
+
# def align_transformed(df, *, inp=Col.data, out=None,
|
|
415
|
+
# align_trans=Col.align_trans, transformed=Col.transformed,
|
|
416
|
+
# keep=(), lib=None) -> pd.DataFrame:
|
|
417
|
+
# """
|
|
418
|
+
# Align all the data items in the column to ensure that same
|
|
419
|
+
# transforms are applied to all of them.
|
|
420
|
+
#
|
|
421
|
+
# Note: transforms must commute (applied in arbitrary order)
|
|
422
|
+
#
|
|
423
|
+
# :param df: a pd.DataFrame containing the data column to align
|
|
424
|
+
# :param inp: name of the column to align
|
|
425
|
+
# :param out: name of the columns with aligned data (default=`inp`)
|
|
426
|
+
# :param keep: temporal columns to keep, otherwise `transformed`, 'align_trans', `inp`
|
|
427
|
+
# :param align_trans: column name with alignment transforms
|
|
428
|
+
# :param transformed: column describing transforms been applied
|
|
429
|
+
# to the `inp` by now
|
|
430
|
+
# :param lib: dictionary of transforms functions, by default from transforms()
|
|
431
|
+
# :return: pd.DataFrame with aligned column
|
|
432
|
+
# """
|
|
433
|
+
# from functools import reduce
|
|
434
|
+
# from box import Box # to make dicts hashable for sets
|
|
435
|
+
# out = out or inp
|
|
436
|
+
# keep = as_list(keep)
|
|
437
|
+
# if transformed in df:
|
|
438
|
+
# tran_set = lambda t: {*Box(t, frozen_box=True).items()} \
|
|
439
|
+
# if isinstance(t, dict) else set() # as set of tuples {(func, kwargs), }
|
|
440
|
+
# combined = reduce(set.union, map(tran_set, df[transformed])) # all transforms
|
|
441
|
+
#
|
|
442
|
+
# dif = lambda t: dict(combined.difference(tran_set(t))) # create column with
|
|
443
|
+
# df[align_trans] = df[transformed].apply(dif) # missing transforms
|
|
444
|
+
#
|
|
445
|
+
# df = apply_transforms(df, trans=align_trans, read=None, keep=keep,
|
|
446
|
+
# inp=inp, out=out, ns=lib or transforms()) # apply missing transforms
|
|
447
|
+
# if transformed not in keep:
|
|
448
|
+
# df.drop(transformed, axis=1, inplace=True) # drop temporary column
|
|
449
|
+
# else:
|
|
450
|
+
# if out != inp: # trivial inp -> out transform
|
|
451
|
+
# df[out] = df[inp]
|
|
452
|
+
# if inp not in keep:
|
|
453
|
+
# df.drop(inp, axis=1, inplace=True)
|
|
454
|
+
# return df
|
|
455
|
+
#
|
|
456
|
+
#
|
|
457
|
+
# def grp_code_regions(g: pd.DataFrame, codes='rgn_codes', data='data', out='series') -> pd.Series:
|
|
458
|
+
# """Transform group by splitting `data` column coded image into Regions
|
|
459
|
+
# object according to mapping in `code` column (usually 'rgn_code').
|
|
460
|
+
# If such column is not found - return the input group
|
|
461
|
+
#
|
|
462
|
+
# :param g: group with 'data' and codes columns
|
|
463
|
+
# :param codes: name of the codes mapping column (dict content)
|
|
464
|
+
# :param data: name of the data column (usually 'data')
|
|
465
|
+
# :param out: 'series' | 'frame' | None
|
|
466
|
+
# :return: pd.DataFrame with Regions in decoded rows of data column
|
|
467
|
+
# and with `codes` column dropped
|
|
468
|
+
# """
|
|
469
|
+
# if 'rgn_codes' in g.columns:
|
|
470
|
+
# from iad.core.image import Regions
|
|
471
|
+
# data_cols = ['data', codes]
|
|
472
|
+
# is_rgn = g.rgn_codes.notna()
|
|
473
|
+
# g.loc[is_rgn, data] = g.loc[is_rgn, data_cols].apply(lambda p: Regions(*p), axis=1)
|
|
474
|
+
# g = g.drop(codes, 1)
|
|
475
|
+
# return g[data] if out == 'series' else g
|
|
476
|
+
|
|
477
|
+
_FETCH_RES = Literal['data', 'all', 'essen']
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
class Fetchable:
|
|
481
|
+
# ToDo: @Ilya - Create Task: g.qix().fetch(), g.select().fetch() keep data in g!
|
|
482
|
+
|
|
483
|
+
def fetch(self: pdt.DTable, *, check_file=False,
|
|
484
|
+
out: _FETCH_RES = 'data',
|
|
485
|
+
reuse: bool | Callable = True,
|
|
486
|
+
inplace=True) -> CollectTable | CollectSeries:
|
|
487
|
+
"""
|
|
488
|
+
Return table with data fetched according to the read transform of every item.
|
|
489
|
+
|
|
490
|
+
Resulted table may be a data-only `series` or `frame` with columns depending
|
|
491
|
+
on the ``out`` argument:
|
|
492
|
+
- 'data' - `series` with only data,
|
|
493
|
+
- 'all' - `frame` with all the original + data,
|
|
494
|
+
- 'essen' - `frame` with essential columns: `all` except those used by transform.
|
|
495
|
+
(`frame` will be produced also in special case when source is series with only `data`)
|
|
496
|
+
|
|
497
|
+
If source already has valid data, data is not reloaded if ``reuse is True``.
|
|
498
|
+
|
|
499
|
+
The default condition of *validity* (``datacast.transtools.is_data``)
|
|
500
|
+
can be replaced by providing alternative function: ``reuse=new_data_cond``.
|
|
501
|
+
|
|
502
|
+
If not ``inplace``, fetch returns a new table without changing the source,
|
|
503
|
+
so that subsequent fetching from same source requires loading data again.
|
|
504
|
+
|
|
505
|
+
Using ``inplace=True`` allows to avoid reloading in certain scenarios.
|
|
506
|
+
|
|
507
|
+
In this case data column is added to the source table.
|
|
508
|
+
Then, only if requested format of the output table is different, a new table is produced.
|
|
509
|
+
|
|
510
|
+
It modifies self by loading into 'data' column, AND ALSO returns the data in the requested form.
|
|
511
|
+
|
|
512
|
+
Therefore, in this example only first ``fetch()`` will require loading:
|
|
513
|
+
|
|
514
|
+
>>> g2 = g1.fetch() # loading
|
|
515
|
+
>>> g3 = g2.qix(**labels).fetch() # no loading
|
|
516
|
+
>>> g1.loc[index].fetch() # no loading
|
|
517
|
+
```
|
|
518
|
+
However, that does not work if used AFTER querying:
|
|
519
|
+
|
|
520
|
+
>>> g2 = g1.qix(**labels).fetch() # loading
|
|
521
|
+
>>> g2.fetch() # no loading
|
|
522
|
+
>>> g1.loc[index].fetch() # loading again
|
|
523
|
+
|
|
524
|
+
:param self: table
|
|
525
|
+
:param out: columns to return,
|
|
526
|
+
:param check_file: if file in `Col.path` not exists don't raise just return ``pd.NA``
|
|
527
|
+
:param reuse: don't fetch data if already available in `Col.data`
|
|
528
|
+
:param inplace: update self ``DataFrame`` instead of fetching into a copy.
|
|
529
|
+
:return: with selected rows and loaded data as values or a data object
|
|
530
|
+
"""
|
|
531
|
+
assert out in get_args(_FETCH_RES)
|
|
532
|
+
trans_labels = [Col.path, Col.read_trans]
|
|
533
|
+
|
|
534
|
+
if self.empty:
|
|
535
|
+
self['data'] = None
|
|
536
|
+
return self
|
|
537
|
+
|
|
538
|
+
if not isinstance(self, pd.DataFrame): # Series, must be already with data
|
|
539
|
+
if reuse and self.name == 'data':
|
|
540
|
+
return self if out == 'data' else pdt.as_table(self, series=False)
|
|
541
|
+
raise IndexError(f"Fetch requires {trans_labels} or 'data' columns!")
|
|
542
|
+
|
|
543
|
+
if not set(trans_labels).issubset(self.columns):
|
|
544
|
+
if reuse and 'data' in self.columns:
|
|
545
|
+
return self.data if out == 'data' else self
|
|
546
|
+
raise IndexError(f"Fetch requires {trans_labels} or 'data' columns")
|
|
547
|
+
|
|
548
|
+
# Consider: use apply_column_transform for cases above to verify data?
|
|
549
|
+
|
|
550
|
+
if reuse or check_file: # change to conditional read if required
|
|
551
|
+
kws = {'check_file': check_file} | (reuse and {'check_data': reuse} or {})
|
|
552
|
+
trans = '__cond_trans__'
|
|
553
|
+
self[trans] = O(cond_read_trans, **kws)
|
|
554
|
+
trans_args = dict(drop=trans, args='series', inp=None) # series with all columns
|
|
555
|
+
else:
|
|
556
|
+
trans = Col.read_trans
|
|
557
|
+
trans_args = dict(drop=False)
|
|
558
|
+
|
|
559
|
+
res = apply_column_transform(self, out=Col.data, trans=trans, inplace=inplace, **trans_args)
|
|
560
|
+
match out:
|
|
561
|
+
case 'data': return res.data
|
|
562
|
+
case 'essen': return res.drop(trans_labels)
|
|
563
|
+
case 'all': return res
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
class CollectSeries(pdt.DataSeries, Fetchable):
|
|
567
|
+
_metadata = pdt.DataSeries._metadata + ["collection"]
|
|
568
|
+
|
|
569
|
+
@property
|
|
570
|
+
def _constructor(self):
|
|
571
|
+
return CollectSeries
|
|
572
|
+
|
|
573
|
+
@property
|
|
574
|
+
def _constructor_expanddim(self):
|
|
575
|
+
return CollectTable
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
class CollectTable(pdt.DataTable, Fetchable):
|
|
579
|
+
_metadata = pdt.DataTable._metadata + ['collection']
|
|
580
|
+
|
|
581
|
+
@property
|
|
582
|
+
def _constructor(self):
|
|
583
|
+
return CollectTable
|
|
584
|
+
|
|
585
|
+
@property
|
|
586
|
+
def _constructor_sliced(self):
|
|
587
|
+
return CollectSeries
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
class DataItem(Labels):
|
|
591
|
+
|
|
592
|
+
def fetch(self, data='data', trans='read_trans', inp='path',
|
|
593
|
+
reuse=True, check_file=True,
|
|
594
|
+
drop: str | Collection[str] | bool = True):
|
|
595
|
+
if missing := {trans, inp}.difference(self.keys()):
|
|
596
|
+
raise KeyError(f"Fetch requires keys which are {missing=}")
|
|
597
|
+
|
|
598
|
+
if reuse or check_file:
|
|
599
|
+
from .transtools import cond_read_trans
|
|
600
|
+
trans = cond_read_trans()
|
|
601
|
+
|
|
602
|
+
res = self.copy()
|
|
603
|
+
if not (reuse or data in res):
|
|
604
|
+
res[data] = res[trans](res[inp])
|
|
605
|
+
if drop is True:
|
|
606
|
+
drop = [trans, inp]
|
|
607
|
+
if drop:
|
|
608
|
+
res.drop(drop)
|
iad/dataman/env.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from iad.core.env import EnvLoc as CoreEnvLoc, Locator, env_log
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class EnvLoc(CoreEnvLoc):
|
|
5
|
+
"""Environment locators for dataman package."""
|
|
6
|
+
DATA = Locator(envar='ALG_DATA', alarm=env_log.error)
|
|
7
|
+
DATASETS = Locator(envar='ALG_DATASETS', alarm=env_log.error)
|
|
8
|
+
RESOURCES = Locator(envar='ALG_RESOURCES', alarm=env_log.error)
|
|
9
|
+
|
|
10
|
+
EnvLoc.reset(validate=False)
|