pyplotutil 2.0.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.
- pyplotutil/__init__.py +70 -0
- pyplotutil/_typing.py +38 -0
- pyplotutil/datautil.py +1295 -0
- pyplotutil/loggingutil.py +743 -0
- pyplotutil/plotutil.py +846 -0
- pyplotutil/py.typed +0 -0
- pyplotutil-2.0.0.dist-info/METADATA +123 -0
- pyplotutil-2.0.0.dist-info/RECORD +10 -0
- pyplotutil-2.0.0.dist-info/WHEEL +4 -0
- pyplotutil-2.0.0.dist-info/licenses/LICENSE +21 -0
pyplotutil/datautil.py
ADDED
|
@@ -0,0 +1,1295 @@
|
|
|
1
|
+
"""Data Handling and Manipulation Module.
|
|
2
|
+
|
|
3
|
+
This module provides classes for managing and manipulating tabular data, with functionalities to
|
|
4
|
+
load data from various sources, group data structure by specified tags, and access columns or rows
|
|
5
|
+
with intuitive syntax. The primary classes, `Data`, `TaggedData`, and `Dataset`, facilitate working with
|
|
6
|
+
tabular data in polars DataFrame while allowing access to specific features like data grouping,
|
|
7
|
+
dynamic attribute setting, and easy retrieval of parameter values.
|
|
8
|
+
|
|
9
|
+
Classes
|
|
10
|
+
-------
|
|
11
|
+
BaseData : Abstract base class providing the core attributes and methods for data handling.
|
|
12
|
+
Defines basic properties for data path and DataFrame storage.
|
|
13
|
+
|
|
14
|
+
Data : Extends BaseData to represent a single tabular data.
|
|
15
|
+
Provides methods to access columns and retrieve specific parameters.
|
|
16
|
+
|
|
17
|
+
TaggedData : Extends BaseData to handle grouped data based on a specified tag column.
|
|
18
|
+
Allows grouping data by a tag and accessing each group as a separate `Data` object.
|
|
19
|
+
|
|
20
|
+
Dataset : Collection of multiple Data objects with analysis capabilities.
|
|
21
|
+
Manages multiple data files and provides methods for cross-data analysis and comparison.
|
|
22
|
+
|
|
23
|
+
Examples
|
|
24
|
+
--------
|
|
25
|
+
Basic usage:
|
|
26
|
+
>>> data = Data("data.csv")
|
|
27
|
+
>>> print(data.param("column_name"))
|
|
28
|
+
|
|
29
|
+
Tagged data usage:
|
|
30
|
+
>>> tagged_data = TaggedData("data.csv", tag="tag")
|
|
31
|
+
>>> data = tagged_data.get("specific_tag")
|
|
32
|
+
>>> print(group.param("column_name"))
|
|
33
|
+
|
|
34
|
+
Dataset usage:
|
|
35
|
+
>>> dataset = Dataset(["data1.csv", "data2.csv"])
|
|
36
|
+
>>> t, y = dataset.get_timeseries("value_column")
|
|
37
|
+
|
|
38
|
+
This module is designed to streamline operations with tabular data in data analysis, data plotting,
|
|
39
|
+
and other applications requiring structured data handling.
|
|
40
|
+
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
from __future__ import annotations
|
|
44
|
+
|
|
45
|
+
import warnings
|
|
46
|
+
from collections import OrderedDict
|
|
47
|
+
from collections.abc import Callable, Iterable, Mapping, Sequence
|
|
48
|
+
from functools import cached_property, partial
|
|
49
|
+
from io import StringIO
|
|
50
|
+
from pathlib import Path
|
|
51
|
+
from typing import TYPE_CHECKING, TextIO, overload
|
|
52
|
+
|
|
53
|
+
import numpy as np
|
|
54
|
+
import polars as pl
|
|
55
|
+
from polars.exceptions import ColumnNotFoundError
|
|
56
|
+
|
|
57
|
+
from pyplotutil._typing import (
|
|
58
|
+
FilePath,
|
|
59
|
+
MultiColSelector,
|
|
60
|
+
MultiIndexSelector,
|
|
61
|
+
NoDefault,
|
|
62
|
+
SingleColSelector,
|
|
63
|
+
SingleIndexSelector,
|
|
64
|
+
Unknown,
|
|
65
|
+
no_default,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
if TYPE_CHECKING:
|
|
69
|
+
from collections.abc import ItemsView, Iterator, KeysView
|
|
70
|
+
|
|
71
|
+
from pyplotutil._typing import DataSourceType
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
_ERR_MSG_NOT_LOADED_FROM_FILE = "Data object may not be loaded from a file."
|
|
75
|
+
_ERR_MSG_PATH_NOT_EXIST = "Source path does not exist"
|
|
76
|
+
_WARN_MSG_CLONE_NOT_LOADED_FROM_FILE = "clone: Source Data object may not be loaded from a file."
|
|
77
|
+
_WARN_MSG_NO_FILES_FOUND = "No files found with the following glob pattern"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class BaseData:
|
|
81
|
+
"""Base class for data handling and manipulation.
|
|
82
|
+
|
|
83
|
+
This class provides functionalities for setting and retrieving the path and the main DataFrame
|
|
84
|
+
associated with the data.
|
|
85
|
+
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
_datapath: Path
|
|
89
|
+
_dataframe: pl.DataFrame
|
|
90
|
+
|
|
91
|
+
def __init__(
|
|
92
|
+
self,
|
|
93
|
+
data_source: DataSourceType,
|
|
94
|
+
*,
|
|
95
|
+
separator: str,
|
|
96
|
+
has_header: bool,
|
|
97
|
+
columns: Sequence[int] | Sequence[str] | None,
|
|
98
|
+
names: Sequence[str] | None,
|
|
99
|
+
n_rows: int | None,
|
|
100
|
+
comment: str | None,
|
|
101
|
+
) -> None:
|
|
102
|
+
"""Initialize the BaseData object with the provided data source.
|
|
103
|
+
|
|
104
|
+
Parameters
|
|
105
|
+
----------
|
|
106
|
+
data_source : str | Path | StringIO | pl.DataFrame | pl.Series
|
|
107
|
+
The data source.
|
|
108
|
+
separator : str
|
|
109
|
+
Single byte character to use as separator in the source, by default ",".
|
|
110
|
+
has_header : bool, optional
|
|
111
|
+
Indicate if the first row of the dataset is a header or not, by default True.
|
|
112
|
+
columns : Sequence[int], Sequence[str] or range, optional
|
|
113
|
+
Columns to read from the data source.
|
|
114
|
+
names : Sequence[str], optional
|
|
115
|
+
Rename columns right after parsing the source.
|
|
116
|
+
n_rows : int, optional
|
|
117
|
+
Number of rows to read.
|
|
118
|
+
comment : str, optional
|
|
119
|
+
Character to indicate comments in the data source.
|
|
120
|
+
|
|
121
|
+
Raises
|
|
122
|
+
------
|
|
123
|
+
TypeError
|
|
124
|
+
If the data type is unsupported.
|
|
125
|
+
|
|
126
|
+
"""
|
|
127
|
+
if isinstance(data_source, pl.DataFrame):
|
|
128
|
+
self._set_dataframe(data_source)
|
|
129
|
+
elif isinstance(data_source, pl.Series):
|
|
130
|
+
self._set_dataframe(data_source.to_frame())
|
|
131
|
+
elif isinstance(data_source, StringIO | FilePath):
|
|
132
|
+
self._set_dataframe(
|
|
133
|
+
self.read_csv(
|
|
134
|
+
data_source,
|
|
135
|
+
separator=separator,
|
|
136
|
+
has_header=has_header,
|
|
137
|
+
columns=columns,
|
|
138
|
+
names=names,
|
|
139
|
+
n_rows=n_rows,
|
|
140
|
+
comment=comment,
|
|
141
|
+
),
|
|
142
|
+
)
|
|
143
|
+
if isinstance(data_source, FilePath):
|
|
144
|
+
self.set_datapath(data_source)
|
|
145
|
+
else:
|
|
146
|
+
msg = f"Unsupported data source type: {type(data_source)}"
|
|
147
|
+
raise TypeError(msg)
|
|
148
|
+
|
|
149
|
+
@staticmethod
|
|
150
|
+
def read_commented_column_names(
|
|
151
|
+
file_or_buffer: FilePath | StringIO,
|
|
152
|
+
*,
|
|
153
|
+
separator: str,
|
|
154
|
+
comment: str,
|
|
155
|
+
) -> list[str] | None:
|
|
156
|
+
"""Return a list of column names extracted from commented lines in the file.
|
|
157
|
+
|
|
158
|
+
Parameters
|
|
159
|
+
----------
|
|
160
|
+
file_or_buffer : str | Path | StringIO
|
|
161
|
+
The file or buffer containing the data.
|
|
162
|
+
separator : str
|
|
163
|
+
Single byte character to use as separator in the file.
|
|
164
|
+
comment : str
|
|
165
|
+
Character indicating commented lines.
|
|
166
|
+
|
|
167
|
+
Returns
|
|
168
|
+
-------
|
|
169
|
+
list of str or None
|
|
170
|
+
List of column names if found; otherwise, None.
|
|
171
|
+
|
|
172
|
+
"""
|
|
173
|
+
|
|
174
|
+
def last_commented_header(buffer: TextIO, comment: str) -> str:
|
|
175
|
+
header = ""
|
|
176
|
+
for line in buffer:
|
|
177
|
+
if line.startswith(comment):
|
|
178
|
+
header = line
|
|
179
|
+
else:
|
|
180
|
+
break
|
|
181
|
+
return header
|
|
182
|
+
|
|
183
|
+
if isinstance(file_or_buffer, FilePath):
|
|
184
|
+
with Path(file_or_buffer).open() as f:
|
|
185
|
+
header = last_commented_header(f, comment)
|
|
186
|
+
else:
|
|
187
|
+
header = last_commented_header(file_or_buffer, comment)
|
|
188
|
+
if len(header) > 0:
|
|
189
|
+
n = len(comment)
|
|
190
|
+
names = header[n:].strip().split(separator)
|
|
191
|
+
if len(names) == 1 and names[0] == "":
|
|
192
|
+
names = []
|
|
193
|
+
return names
|
|
194
|
+
return None
|
|
195
|
+
|
|
196
|
+
@staticmethod
|
|
197
|
+
def read_csv(
|
|
198
|
+
file_or_buffer: FilePath | StringIO,
|
|
199
|
+
*,
|
|
200
|
+
separator: str,
|
|
201
|
+
has_header: bool,
|
|
202
|
+
columns: Sequence[int] | Sequence[str] | None,
|
|
203
|
+
names: Sequence[str] | None,
|
|
204
|
+
n_rows: int | None,
|
|
205
|
+
comment: str | None,
|
|
206
|
+
) -> pl.DataFrame:
|
|
207
|
+
"""Return a polars DataFrame loaded from a file or string buffer.
|
|
208
|
+
|
|
209
|
+
Parameters
|
|
210
|
+
----------
|
|
211
|
+
file_or_buffer : str | Path | StringIO
|
|
212
|
+
The file or buffer to read from.
|
|
213
|
+
separator : str
|
|
214
|
+
Single byte character to use as separator in the source.
|
|
215
|
+
has_header : bool
|
|
216
|
+
Indicate if the first row of the dataset is a header or not.
|
|
217
|
+
columns : Sequence[int], Sequence[str] or range
|
|
218
|
+
Columns to read from the data source.
|
|
219
|
+
names : Sequence[str]
|
|
220
|
+
Rename columns right after parsing the source.
|
|
221
|
+
n_rows : int
|
|
222
|
+
Number of rows to read.
|
|
223
|
+
comment : str
|
|
224
|
+
Character to indicate comments in the data source.
|
|
225
|
+
|
|
226
|
+
Returns
|
|
227
|
+
-------
|
|
228
|
+
pl.DataFrame
|
|
229
|
+
The loaded DataFrame.
|
|
230
|
+
|
|
231
|
+
"""
|
|
232
|
+
if comment is not None and names is None:
|
|
233
|
+
names = BaseData.read_commented_column_names(file_or_buffer, separator=separator, comment=comment)
|
|
234
|
+
if names is not None:
|
|
235
|
+
has_header = False
|
|
236
|
+
if isinstance(file_or_buffer, StringIO):
|
|
237
|
+
file_or_buffer.seek(0)
|
|
238
|
+
return pl.read_csv(
|
|
239
|
+
file_or_buffer,
|
|
240
|
+
has_header=has_header,
|
|
241
|
+
columns=columns,
|
|
242
|
+
new_columns=names,
|
|
243
|
+
separator=separator,
|
|
244
|
+
comment_prefix=comment,
|
|
245
|
+
n_rows=n_rows,
|
|
246
|
+
rechunk=True,
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
def _set_dataframe(self, dataframe: pl.DataFrame) -> None:
|
|
250
|
+
"""Set the DataFrame associated with the data object.
|
|
251
|
+
|
|
252
|
+
Parameters
|
|
253
|
+
----------
|
|
254
|
+
dataframe : pl.DataFrame
|
|
255
|
+
The DataFrame to associate with the data.
|
|
256
|
+
|
|
257
|
+
"""
|
|
258
|
+
self._dataframe = dataframe
|
|
259
|
+
|
|
260
|
+
@property
|
|
261
|
+
def dataframe(self) -> pl.DataFrame:
|
|
262
|
+
"""Retrieve the raw DataFrame associated with the data.
|
|
263
|
+
|
|
264
|
+
Returns
|
|
265
|
+
-------
|
|
266
|
+
pl.DataFrame
|
|
267
|
+
The DataFrame associated with the data.
|
|
268
|
+
|
|
269
|
+
"""
|
|
270
|
+
return self._dataframe
|
|
271
|
+
|
|
272
|
+
@property
|
|
273
|
+
def df(self) -> pl.DataFrame:
|
|
274
|
+
"""Alias for `dataframe` attribute.
|
|
275
|
+
|
|
276
|
+
Returns
|
|
277
|
+
-------
|
|
278
|
+
pl.DataFrame
|
|
279
|
+
The DataFrame associated with the data.
|
|
280
|
+
|
|
281
|
+
"""
|
|
282
|
+
return self.dataframe
|
|
283
|
+
|
|
284
|
+
def is_loaded_from_file(self) -> bool:
|
|
285
|
+
"""Check if the Data object was loaded from a file.
|
|
286
|
+
|
|
287
|
+
Returns
|
|
288
|
+
-------
|
|
289
|
+
bool
|
|
290
|
+
True if loaded from a file; otherwise, False.
|
|
291
|
+
|
|
292
|
+
"""
|
|
293
|
+
try:
|
|
294
|
+
_ = self._datapath
|
|
295
|
+
except AttributeError:
|
|
296
|
+
return False
|
|
297
|
+
return True
|
|
298
|
+
|
|
299
|
+
def set_datapath(self, datapath: str | Path) -> None:
|
|
300
|
+
"""Set the path to the data file.
|
|
301
|
+
|
|
302
|
+
Parameters
|
|
303
|
+
----------
|
|
304
|
+
datapath : str or Path
|
|
305
|
+
Path to the data file.
|
|
306
|
+
|
|
307
|
+
"""
|
|
308
|
+
self._datapath = Path(datapath)
|
|
309
|
+
|
|
310
|
+
@property
|
|
311
|
+
def datapath(self) -> Path:
|
|
312
|
+
"""Retrieve the path to the data file.
|
|
313
|
+
|
|
314
|
+
Returns
|
|
315
|
+
-------
|
|
316
|
+
Path
|
|
317
|
+
Path to the data file.
|
|
318
|
+
|
|
319
|
+
"""
|
|
320
|
+
try:
|
|
321
|
+
return self._datapath
|
|
322
|
+
except AttributeError as e:
|
|
323
|
+
msg = _ERR_MSG_NOT_LOADED_FROM_FILE
|
|
324
|
+
raise AttributeError(msg) from e
|
|
325
|
+
|
|
326
|
+
@property
|
|
327
|
+
def datadir(self) -> Path:
|
|
328
|
+
"""Retrieve the directory of the data file.
|
|
329
|
+
|
|
330
|
+
Returns
|
|
331
|
+
-------
|
|
332
|
+
Path
|
|
333
|
+
Directory of the data file.
|
|
334
|
+
|
|
335
|
+
"""
|
|
336
|
+
return self.datapath.parent
|
|
337
|
+
|
|
338
|
+
def __str__(self) -> str:
|
|
339
|
+
"""Return a string of the associated DataFrame.
|
|
340
|
+
|
|
341
|
+
Returns
|
|
342
|
+
-------
|
|
343
|
+
str
|
|
344
|
+
String representation of the associated DataFrame object.
|
|
345
|
+
|
|
346
|
+
"""
|
|
347
|
+
return str(self.dataframe)
|
|
348
|
+
|
|
349
|
+
def __repr__(self) -> str:
|
|
350
|
+
"""Return a string representation of the object.
|
|
351
|
+
|
|
352
|
+
Returns
|
|
353
|
+
-------
|
|
354
|
+
str
|
|
355
|
+
String representation of the object.
|
|
356
|
+
|
|
357
|
+
"""
|
|
358
|
+
if self.is_loaded_from_file():
|
|
359
|
+
return f"{self.__class__.__name__}({self.datapath})"
|
|
360
|
+
return f"{self.__class__.__name__}({self.dataframe})"
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
class Data(BaseData):
|
|
364
|
+
"""A class representing tabular data loaded from a source.
|
|
365
|
+
|
|
366
|
+
This class provides methods for easy data access and specific data parameter extraction. When
|
|
367
|
+
the source data contains column names, this class provides attributes for access the data column
|
|
368
|
+
with its name.
|
|
369
|
+
|
|
370
|
+
Attributes
|
|
371
|
+
----------
|
|
372
|
+
dataframe : pl.DataFrame
|
|
373
|
+
The DataFrame containing the tabular data.
|
|
374
|
+
datapath : Path
|
|
375
|
+
The path to the data file.
|
|
376
|
+
datadir : Path
|
|
377
|
+
The directory of the data file
|
|
378
|
+
|
|
379
|
+
Examples
|
|
380
|
+
--------
|
|
381
|
+
Initialization of the data object and access for column data with its name.
|
|
382
|
+
|
|
383
|
+
>>> import polars as pl
|
|
384
|
+
>>> data = Data(pl.DataFrame({"a": [1, 2, 3], "b": [0.1, 0.2, 0.3], "c": [5, 5, 5]}))
|
|
385
|
+
>>> data.a
|
|
386
|
+
0 1
|
|
387
|
+
1 2
|
|
388
|
+
2 3
|
|
389
|
+
Name: a, dtype: int64
|
|
390
|
+
>>> data.param("c")
|
|
391
|
+
np.int64(5)
|
|
392
|
+
|
|
393
|
+
"""
|
|
394
|
+
|
|
395
|
+
def __init__(
|
|
396
|
+
self,
|
|
397
|
+
data_source: DataSourceType,
|
|
398
|
+
*,
|
|
399
|
+
separator: str = ",",
|
|
400
|
+
has_header: bool = True,
|
|
401
|
+
columns: Sequence[int] | Sequence[str] | None = None,
|
|
402
|
+
names: Sequence[str] | None = None,
|
|
403
|
+
n_rows: int | None = None,
|
|
404
|
+
comment: str | None = None,
|
|
405
|
+
) -> None:
|
|
406
|
+
"""Initialize the BaseData object with the provided data source.
|
|
407
|
+
|
|
408
|
+
Parameters
|
|
409
|
+
----------
|
|
410
|
+
data_source : str | Path | StringIO | pl.DataFrame | pl.Series
|
|
411
|
+
The data source.
|
|
412
|
+
separator : str
|
|
413
|
+
Single byte character to use as separator in the source, by default ",".
|
|
414
|
+
has_header : bool, optional
|
|
415
|
+
Indicate if the first row of the dataset is a header or not, by default True.
|
|
416
|
+
columns : Sequence[int], Sequence[str] or range, optional
|
|
417
|
+
Columns to read from the data source.
|
|
418
|
+
names : Sequence[str], optional
|
|
419
|
+
Rename columns right after parsing the source.
|
|
420
|
+
n_rows : int, optional
|
|
421
|
+
Number of rows to read.
|
|
422
|
+
comment : str, optional
|
|
423
|
+
Character to indicate comments in the data source.
|
|
424
|
+
|
|
425
|
+
Raises
|
|
426
|
+
------
|
|
427
|
+
TypeError
|
|
428
|
+
If the data type is unsupported.
|
|
429
|
+
|
|
430
|
+
"""
|
|
431
|
+
super().__init__(
|
|
432
|
+
data_source,
|
|
433
|
+
separator=separator,
|
|
434
|
+
has_header=has_header,
|
|
435
|
+
columns=columns,
|
|
436
|
+
names=names,
|
|
437
|
+
n_rows=n_rows,
|
|
438
|
+
comment=comment,
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
@overload
|
|
442
|
+
def __getitem__(self, key: tuple[SingleIndexSelector, SingleColSelector]) -> Unknown: ...
|
|
443
|
+
|
|
444
|
+
@overload
|
|
445
|
+
def __getitem__( # type: ignore[overload-overlap]
|
|
446
|
+
self,
|
|
447
|
+
key: str | tuple[MultiIndexSelector, SingleColSelector],
|
|
448
|
+
) -> pl.Series: ...
|
|
449
|
+
|
|
450
|
+
@overload
|
|
451
|
+
def __getitem__(
|
|
452
|
+
self,
|
|
453
|
+
key: (
|
|
454
|
+
SingleIndexSelector
|
|
455
|
+
| MultiIndexSelector
|
|
456
|
+
| MultiColSelector
|
|
457
|
+
| tuple[SingleIndexSelector, MultiColSelector]
|
|
458
|
+
| tuple[MultiIndexSelector, MultiColSelector]
|
|
459
|
+
),
|
|
460
|
+
) -> pl.DataFrame: ...
|
|
461
|
+
|
|
462
|
+
def __getitem__(
|
|
463
|
+
self,
|
|
464
|
+
key: (
|
|
465
|
+
SingleIndexSelector
|
|
466
|
+
| SingleColSelector
|
|
467
|
+
| MultiColSelector
|
|
468
|
+
| MultiIndexSelector
|
|
469
|
+
| tuple[SingleIndexSelector, SingleColSelector]
|
|
470
|
+
| tuple[SingleIndexSelector, MultiColSelector]
|
|
471
|
+
| tuple[MultiIndexSelector, SingleColSelector]
|
|
472
|
+
| tuple[MultiIndexSelector, MultiColSelector]
|
|
473
|
+
),
|
|
474
|
+
) -> pl.DataFrame | pl.Series | Unknown:
|
|
475
|
+
"""Access a specific column(s).
|
|
476
|
+
|
|
477
|
+
Parameters
|
|
478
|
+
----------
|
|
479
|
+
key : str or int or Sequence of str or int
|
|
480
|
+
Column name or column index.
|
|
481
|
+
|
|
482
|
+
Returns
|
|
483
|
+
-------
|
|
484
|
+
pl.Series or pl.DataFrame
|
|
485
|
+
Series or frame of the specified column(s).
|
|
486
|
+
|
|
487
|
+
"""
|
|
488
|
+
return self.dataframe.__getitem__(key)
|
|
489
|
+
|
|
490
|
+
def __len__(self) -> int:
|
|
491
|
+
"""Return the number of rows in the `Data` object.
|
|
492
|
+
|
|
493
|
+
Returns
|
|
494
|
+
-------
|
|
495
|
+
int
|
|
496
|
+
Number of rows in the `Data` object.
|
|
497
|
+
|
|
498
|
+
"""
|
|
499
|
+
return len(self.dataframe)
|
|
500
|
+
|
|
501
|
+
def __getattr__(self, name: str) -> Unknown:
|
|
502
|
+
"""Access DataFrame attributes not explicitly defined in Data.
|
|
503
|
+
|
|
504
|
+
Parameters
|
|
505
|
+
----------
|
|
506
|
+
name : str
|
|
507
|
+
Attribute name.
|
|
508
|
+
|
|
509
|
+
Returns
|
|
510
|
+
-------
|
|
511
|
+
Unknown
|
|
512
|
+
The attribute from the DataFrame.
|
|
513
|
+
|
|
514
|
+
"""
|
|
515
|
+
if name in ("datapath", "datadir"):
|
|
516
|
+
return self.__getattribute__(name)
|
|
517
|
+
if name in self.dataframe.columns:
|
|
518
|
+
return self.dataframe.get_column(name)
|
|
519
|
+
return getattr(self.dataframe, name)
|
|
520
|
+
|
|
521
|
+
def __iter__(self) -> Iterator[np.ndarray]:
|
|
522
|
+
"""Return an iterator over the Data objects.
|
|
523
|
+
|
|
524
|
+
Returns
|
|
525
|
+
-------
|
|
526
|
+
Iterator[np.ndarray]
|
|
527
|
+
An iterator over the Data objects.
|
|
528
|
+
|
|
529
|
+
"""
|
|
530
|
+
return iter(self.dataframe.to_numpy())
|
|
531
|
+
|
|
532
|
+
def split_by_row(self, row_index: int) -> tuple[Data, Data]:
|
|
533
|
+
"""Split the Data object into two parts at a specified row index.
|
|
534
|
+
|
|
535
|
+
Parameters
|
|
536
|
+
----------
|
|
537
|
+
row_index : int
|
|
538
|
+
The index at which to split the data object. Rows from the start up to
|
|
539
|
+
`row_index` will go to the first split, and rows from `row_index` to
|
|
540
|
+
the end will go to the second split.
|
|
541
|
+
|
|
542
|
+
Returns
|
|
543
|
+
-------
|
|
544
|
+
tuple[Data, Data]
|
|
545
|
+
A tuple containing two Data objects. The first contains rows from the
|
|
546
|
+
start to `row_index`, and the second contains rows from `row_index`
|
|
547
|
+
to the end.
|
|
548
|
+
|
|
549
|
+
"""
|
|
550
|
+
df1 = self.dataframe[:row_index]
|
|
551
|
+
df2 = self.dataframe[row_index:]
|
|
552
|
+
return Data(df1), Data(df2)
|
|
553
|
+
|
|
554
|
+
@overload
|
|
555
|
+
def param(self, key: int | str) -> float: ...
|
|
556
|
+
|
|
557
|
+
@overload
|
|
558
|
+
def param(self, key: Sequence) -> tuple[float, ...]: ...
|
|
559
|
+
|
|
560
|
+
def param(self, key):
|
|
561
|
+
"""Retrieve specific parameter(s) for column(s) as float.
|
|
562
|
+
|
|
563
|
+
Parameters
|
|
564
|
+
----------
|
|
565
|
+
key : int or str or Sequence of int or str
|
|
566
|
+
The column(s) for which to retrieve the parameter.
|
|
567
|
+
|
|
568
|
+
Returns
|
|
569
|
+
-------
|
|
570
|
+
float or tuple[float, ...]
|
|
571
|
+
Retrieved parameter value(s).
|
|
572
|
+
|
|
573
|
+
"""
|
|
574
|
+
subset = self.dataframe[:, key]
|
|
575
|
+
if isinstance(subset, pl.Series):
|
|
576
|
+
return subset.cast(pl.Float64)[0]
|
|
577
|
+
return subset.cast(pl.Float64).row(0)
|
|
578
|
+
|
|
579
|
+
def _copy_datapath_if_loaded_from_file(self, dest_data: Data) -> Data:
|
|
580
|
+
"""Copy datapath from source to destination Data object if source was loaded from file.
|
|
581
|
+
|
|
582
|
+
Parameters
|
|
583
|
+
----------
|
|
584
|
+
dest_data : Data
|
|
585
|
+
Destination Data object to copy datapath to
|
|
586
|
+
|
|
587
|
+
Returns
|
|
588
|
+
-------
|
|
589
|
+
Data
|
|
590
|
+
Data object with copied datapath if applicable
|
|
591
|
+
|
|
592
|
+
Warns
|
|
593
|
+
-----
|
|
594
|
+
UserWarning
|
|
595
|
+
If source data was not loaded from file
|
|
596
|
+
|
|
597
|
+
"""
|
|
598
|
+
try:
|
|
599
|
+
dest_data.set_datapath(self.datapath)
|
|
600
|
+
except AttributeError as e:
|
|
601
|
+
if _ERR_MSG_NOT_LOADED_FROM_FILE in str(e):
|
|
602
|
+
msg = _WARN_MSG_CLONE_NOT_LOADED_FROM_FILE
|
|
603
|
+
warnings.warn(msg, UserWarning, stacklevel=2)
|
|
604
|
+
else:
|
|
605
|
+
raise AttributeError(str(e)) from e
|
|
606
|
+
return dest_data
|
|
607
|
+
|
|
608
|
+
def clone(
|
|
609
|
+
self,
|
|
610
|
+
*,
|
|
611
|
+
rename_mapping: dict[str, str] | Sequence[str] | None = None,
|
|
612
|
+
keep_datapath: bool = False,
|
|
613
|
+
) -> Data:
|
|
614
|
+
"""Create a clone of the current Data object.
|
|
615
|
+
|
|
616
|
+
Parameters
|
|
617
|
+
----------
|
|
618
|
+
rename_mapping : dict[str, str] | Sequence[str] | None, optional
|
|
619
|
+
Mapping to rename columns in the cloned data. If sequence, maps existing column names to
|
|
620
|
+
new names in order.
|
|
621
|
+
keep_datapath : bool, optional
|
|
622
|
+
Whether to preserve the datapath in the cloned object, by default False.
|
|
623
|
+
|
|
624
|
+
Returns
|
|
625
|
+
-------
|
|
626
|
+
Data
|
|
627
|
+
A new Data object with cloned content.
|
|
628
|
+
|
|
629
|
+
"""
|
|
630
|
+
if isinstance(rename_mapping, Mapping):
|
|
631
|
+
cloned_df = self.df.rename(rename_mapping)
|
|
632
|
+
elif isinstance(rename_mapping, Sequence):
|
|
633
|
+
cloned_df = self.df.rename(dict(zip(self.df.columns, rename_mapping, strict=True)))
|
|
634
|
+
else:
|
|
635
|
+
cloned_df = self.df.clone()
|
|
636
|
+
cloned_data = Data(cloned_df)
|
|
637
|
+
if keep_datapath:
|
|
638
|
+
cloned_data = self._copy_datapath_if_loaded_from_file(cloned_data)
|
|
639
|
+
return cloned_data
|
|
640
|
+
|
|
641
|
+
def subset(
|
|
642
|
+
self,
|
|
643
|
+
key: Sequence[str],
|
|
644
|
+
*,
|
|
645
|
+
start: int | None = None,
|
|
646
|
+
end: int | None = None,
|
|
647
|
+
rename_mapping: dict[str, str] | Sequence[str] | None = None,
|
|
648
|
+
keep_datapath: bool = False,
|
|
649
|
+
) -> Data:
|
|
650
|
+
"""Create a subset of the current Data object.
|
|
651
|
+
|
|
652
|
+
Parameters
|
|
653
|
+
----------
|
|
654
|
+
key : Sequence[str]
|
|
655
|
+
Column names to include in subset
|
|
656
|
+
start : int | None, optional
|
|
657
|
+
Start index for row selection, by default None.
|
|
658
|
+
end : int | None, optional
|
|
659
|
+
End index for row selection, by default None.
|
|
660
|
+
rename_mapping : dict[str, str] | Sequence[str] | None, optional
|
|
661
|
+
Mapping to rename columns in subset. If sequence, maps existing column names to new
|
|
662
|
+
names in order.
|
|
663
|
+
keep_datapath : bool, optional
|
|
664
|
+
Whether to preserve the datapath in subset, by default False.
|
|
665
|
+
|
|
666
|
+
Returns
|
|
667
|
+
-------
|
|
668
|
+
Data
|
|
669
|
+
A new Data object containing the specified subset.
|
|
670
|
+
|
|
671
|
+
"""
|
|
672
|
+
subset_df = self[start:end, key]
|
|
673
|
+
if isinstance(rename_mapping, Mapping):
|
|
674
|
+
subset_df = subset_df.rename(rename_mapping)
|
|
675
|
+
elif isinstance(rename_mapping, Sequence):
|
|
676
|
+
subset_df = subset_df.rename(dict(zip(subset_df.columns, rename_mapping, strict=True)))
|
|
677
|
+
subset_data = Data(subset_df)
|
|
678
|
+
if keep_datapath:
|
|
679
|
+
subset_data = self._copy_datapath_if_loaded_from_file(subset_data)
|
|
680
|
+
return subset_data
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
class TaggedData(BaseData):
|
|
684
|
+
"""A class for handling data grouped by a specified tag.
|
|
685
|
+
|
|
686
|
+
This class provides methods to load and access data grouped by a tag, enabling
|
|
687
|
+
easy access to each group's data as individual `Data` objects.
|
|
688
|
+
|
|
689
|
+
Attributes
|
|
690
|
+
----------
|
|
691
|
+
dataframe : pl.DataFrame
|
|
692
|
+
The DataFrame containing the tabular data with tags.
|
|
693
|
+
datadict : dict of str to Data
|
|
694
|
+
A dictionary mapping each tag value to a corresponding `Data` object.
|
|
695
|
+
|
|
696
|
+
"""
|
|
697
|
+
|
|
698
|
+
_datadict: dict[str, Data]
|
|
699
|
+
_tag_column_name: str
|
|
700
|
+
|
|
701
|
+
def __init__(
|
|
702
|
+
self,
|
|
703
|
+
data_source: DataSourceType,
|
|
704
|
+
*,
|
|
705
|
+
separator: str = ",",
|
|
706
|
+
has_header: bool = True,
|
|
707
|
+
columns: Sequence[int] | Sequence[str] | None = None,
|
|
708
|
+
names: Sequence[str] | None = None,
|
|
709
|
+
n_rows: int | None = None,
|
|
710
|
+
comment: str | None = None,
|
|
711
|
+
tag_column: str = "tag",
|
|
712
|
+
) -> None:
|
|
713
|
+
"""Initialize the BaseData object with the provided data source.
|
|
714
|
+
|
|
715
|
+
Parameters
|
|
716
|
+
----------
|
|
717
|
+
data_source : str | Path | StringIO | pl.DataFrame | pl.Series
|
|
718
|
+
The data source.
|
|
719
|
+
separator : str
|
|
720
|
+
Single byte character to use as separator in the source, by default ",".
|
|
721
|
+
has_header : bool, optional
|
|
722
|
+
Indicate if the first row of the dataset is a header or not, by default True.
|
|
723
|
+
columns : Sequence[int], Sequence[str] or range, optional
|
|
724
|
+
Columns to read from the data source.
|
|
725
|
+
names : Sequence[str], optional
|
|
726
|
+
Rename columns right after parsing the source.
|
|
727
|
+
n_rows : int, optional
|
|
728
|
+
Number of rows to read.
|
|
729
|
+
comment : str, optional
|
|
730
|
+
Character to indicate comments in the data source.
|
|
731
|
+
tag_column : str, optional
|
|
732
|
+
Column name used to tag and group data.
|
|
733
|
+
|
|
734
|
+
Raises
|
|
735
|
+
------
|
|
736
|
+
TypeError
|
|
737
|
+
If the data type is unsupported.
|
|
738
|
+
|
|
739
|
+
"""
|
|
740
|
+
super().__init__(
|
|
741
|
+
data_source,
|
|
742
|
+
separator=separator,
|
|
743
|
+
has_header=has_header,
|
|
744
|
+
columns=columns,
|
|
745
|
+
names=names,
|
|
746
|
+
n_rows=n_rows,
|
|
747
|
+
comment=comment,
|
|
748
|
+
)
|
|
749
|
+
self._tag_column_name = tag_column
|
|
750
|
+
self._make_groups(self._tag_column_name)
|
|
751
|
+
|
|
752
|
+
def __iter__(self) -> Iterator[tuple[str, Data]]:
|
|
753
|
+
"""Return an iterator over the grouped Data objects.
|
|
754
|
+
|
|
755
|
+
Each group is represented by a tuple of (tag, Data).
|
|
756
|
+
|
|
757
|
+
Returns
|
|
758
|
+
-------
|
|
759
|
+
Iterator[tuple[str, Data]]
|
|
760
|
+
An iterator over the grouped Data objects.
|
|
761
|
+
|
|
762
|
+
"""
|
|
763
|
+
return iter(self.datadict.items())
|
|
764
|
+
|
|
765
|
+
def __len__(self) -> int:
|
|
766
|
+
"""Return the number of Data objects divided by the tag.
|
|
767
|
+
|
|
768
|
+
Returns
|
|
769
|
+
-------
|
|
770
|
+
int
|
|
771
|
+
Number of Data objects divided by the tag.
|
|
772
|
+
|
|
773
|
+
"""
|
|
774
|
+
return len(self._datadict)
|
|
775
|
+
|
|
776
|
+
def _make_groups(self, tag_column_name: str) -> None:
|
|
777
|
+
"""Group data by specified tag column and store in internal dictionary.
|
|
778
|
+
|
|
779
|
+
Parameters
|
|
780
|
+
----------
|
|
781
|
+
tag_column_name : str
|
|
782
|
+
Name of column to use for grouping
|
|
783
|
+
|
|
784
|
+
"""
|
|
785
|
+
try:
|
|
786
|
+
self._datadict = {
|
|
787
|
+
str(name[0]): Data(group.drop(tag_column_name))
|
|
788
|
+
for name, group in self.dataframe.group_by(tag_column_name)
|
|
789
|
+
}
|
|
790
|
+
except ColumnNotFoundError:
|
|
791
|
+
self._datadict = {"unknown": Data(self.dataframe)}
|
|
792
|
+
|
|
793
|
+
@property
|
|
794
|
+
def datadict(self) -> dict[str, Data]:
|
|
795
|
+
"""Retrieve the dictionary of grouped Data objects.
|
|
796
|
+
|
|
797
|
+
Returns
|
|
798
|
+
-------
|
|
799
|
+
dict of str, Data
|
|
800
|
+
Dictionary of grouped Data objects.
|
|
801
|
+
|
|
802
|
+
"""
|
|
803
|
+
return self._datadict
|
|
804
|
+
|
|
805
|
+
def tags(self) -> KeysView[str]:
|
|
806
|
+
"""Return the tags associated with the data groups.
|
|
807
|
+
|
|
808
|
+
Returns
|
|
809
|
+
-------
|
|
810
|
+
KeysView of str
|
|
811
|
+
Dictionary keys of tags.
|
|
812
|
+
|
|
813
|
+
"""
|
|
814
|
+
return self.datadict.keys()
|
|
815
|
+
|
|
816
|
+
def items(self) -> ItemsView[str, Data]:
|
|
817
|
+
"""Retrieve the items (tag and Data object) of the grouped data.
|
|
818
|
+
|
|
819
|
+
Returns
|
|
820
|
+
-------
|
|
821
|
+
ItemsView of tuple of (str, Data)
|
|
822
|
+
Dictionary items of tag-Data object pairs.
|
|
823
|
+
|
|
824
|
+
"""
|
|
825
|
+
return self.datadict.items()
|
|
826
|
+
|
|
827
|
+
@cached_property
|
|
828
|
+
def first_tag(self) -> str:
|
|
829
|
+
"""Return the first tag stored in the TaggedData object.
|
|
830
|
+
|
|
831
|
+
Returns
|
|
832
|
+
-------
|
|
833
|
+
str
|
|
834
|
+
The first tag after tags in the object are sorted alphabetically.
|
|
835
|
+
|
|
836
|
+
Raises
|
|
837
|
+
------
|
|
838
|
+
RuntimeError
|
|
839
|
+
If no tagged data is stored.
|
|
840
|
+
|
|
841
|
+
"""
|
|
842
|
+
try:
|
|
843
|
+
tag = sorted(self.tags())[0]
|
|
844
|
+
except IndexError as e:
|
|
845
|
+
msg = "No tagged data is stored."
|
|
846
|
+
raise RuntimeError(msg) from e
|
|
847
|
+
return tag
|
|
848
|
+
|
|
849
|
+
@overload
|
|
850
|
+
def get(self, tag: str) -> Data: ...
|
|
851
|
+
|
|
852
|
+
@overload
|
|
853
|
+
def get(self, tag: str, default: Data | NoDefault) -> Data: ...
|
|
854
|
+
|
|
855
|
+
@overload
|
|
856
|
+
def get(self, tag: str, default: None) -> Data | None: ...
|
|
857
|
+
|
|
858
|
+
def get(self, tag, default=no_default):
|
|
859
|
+
"""Retrieve the Data object associated with the specified tag.
|
|
860
|
+
|
|
861
|
+
Parameters
|
|
862
|
+
----------
|
|
863
|
+
tag : str
|
|
864
|
+
Tag of the data group to retrieve.
|
|
865
|
+
|
|
866
|
+
default : Data or None, optional
|
|
867
|
+
The default data object when the specified tag is not found.
|
|
868
|
+
|
|
869
|
+
Returns
|
|
870
|
+
-------
|
|
871
|
+
Data
|
|
872
|
+
Data object corresponding to the tag.
|
|
873
|
+
|
|
874
|
+
Raises
|
|
875
|
+
------
|
|
876
|
+
KeyError
|
|
877
|
+
If the specified tag value does not exist and no default value is given.
|
|
878
|
+
|
|
879
|
+
"""
|
|
880
|
+
if default is no_default:
|
|
881
|
+
return self.datadict[tag]
|
|
882
|
+
if default is None:
|
|
883
|
+
# note: this return statement seems nonsense, but without it type checker produces an
|
|
884
|
+
# error, somehow.
|
|
885
|
+
return self.datadict.get(tag, None)
|
|
886
|
+
return self.datadict.get(tag, default)
|
|
887
|
+
|
|
888
|
+
@overload
|
|
889
|
+
def param(self, key: int | str, tag: str | None = None) -> float: ...
|
|
890
|
+
|
|
891
|
+
@overload
|
|
892
|
+
def param(self, key: Sequence, tag: str | None = None) -> tuple[float, ...]: ...
|
|
893
|
+
|
|
894
|
+
def param(self, key, tag=None):
|
|
895
|
+
"""Retrieve specific parameter(s) for column(s) from a tagged Data object.
|
|
896
|
+
|
|
897
|
+
Parameters
|
|
898
|
+
----------
|
|
899
|
+
key : int or str or Sequence of int or str
|
|
900
|
+
The column(s) for which to retrieve the parameter.
|
|
901
|
+
tag : str or None, optional
|
|
902
|
+
Tag of the data group to retrieve, by default None. If None is given, the first tag is
|
|
903
|
+
used.
|
|
904
|
+
|
|
905
|
+
Returns
|
|
906
|
+
-------
|
|
907
|
+
float or tuple[float, ...]
|
|
908
|
+
Retrieved parameter value(s).
|
|
909
|
+
|
|
910
|
+
"""
|
|
911
|
+
if tag is None:
|
|
912
|
+
tag = self.first_tag
|
|
913
|
+
return self.get(tag).param(key)
|
|
914
|
+
|
|
915
|
+
def __str__(self) -> str:
|
|
916
|
+
"""Return a string of the grouped mapping of tag to Data.
|
|
917
|
+
|
|
918
|
+
Returns
|
|
919
|
+
-------
|
|
920
|
+
str
|
|
921
|
+
String representation of the grouped mapping of tag to Data.
|
|
922
|
+
|
|
923
|
+
"""
|
|
924
|
+
return str(self.datadict)
|
|
925
|
+
|
|
926
|
+
def __repr__(self) -> str:
|
|
927
|
+
"""Return a string representation of the object.
|
|
928
|
+
|
|
929
|
+
Returns
|
|
930
|
+
-------
|
|
931
|
+
str
|
|
932
|
+
String representation of the object.
|
|
933
|
+
|
|
934
|
+
"""
|
|
935
|
+
if self.is_loaded_from_file():
|
|
936
|
+
return f"{self.__class__.__name__}({self.datapath}, tag={self._tag_column_name})"
|
|
937
|
+
return f"{self.__class__.__name__}({self.dataframe}, tag={self._tag_column_name})"
|
|
938
|
+
|
|
939
|
+
|
|
940
|
+
class Dataset:
|
|
941
|
+
"""A class for managing multiple Data objects as a collection.
|
|
942
|
+
|
|
943
|
+
This class provides functionality to load and handle multiple Data objects from files
|
|
944
|
+
or directories, with methods to access and analyze data across the collection.
|
|
945
|
+
|
|
946
|
+
Attributes
|
|
947
|
+
----------
|
|
948
|
+
dataset : list[Data]
|
|
949
|
+
List of Data objects in the collection
|
|
950
|
+
datapaths : list[Path]
|
|
951
|
+
List of paths to data files
|
|
952
|
+
datadirs : list[Path]
|
|
953
|
+
List of unique directory paths containing data files
|
|
954
|
+
min_n_rows : int
|
|
955
|
+
Minimum number of rows across all Data objects
|
|
956
|
+
|
|
957
|
+
"""
|
|
958
|
+
|
|
959
|
+
_dataset: list[Data]
|
|
960
|
+
|
|
961
|
+
def __init__(
|
|
962
|
+
self,
|
|
963
|
+
source_paths: FilePath | Iterable[FilePath],
|
|
964
|
+
*,
|
|
965
|
+
separator: str = ",",
|
|
966
|
+
has_header: bool = True,
|
|
967
|
+
columns: Sequence[int] | Sequence[str] | None = None,
|
|
968
|
+
names: Sequence[str] | None = None,
|
|
969
|
+
n_rows: int | None = None,
|
|
970
|
+
comment: str | None = None,
|
|
971
|
+
glob_pattern: str = "**/*.csv",
|
|
972
|
+
n_pickup_per_directory: int | None = None,
|
|
973
|
+
) -> None:
|
|
974
|
+
"""Initialize Dataset with data from specified sources.
|
|
975
|
+
|
|
976
|
+
Parameters
|
|
977
|
+
----------
|
|
978
|
+
source_paths : FilePath | Iterable[FilePath]
|
|
979
|
+
Path(s) to data files or directories.
|
|
980
|
+
separator : str, optional
|
|
981
|
+
Single byte character to use as separator in the file, by default ",".
|
|
982
|
+
has_header : bool, optional
|
|
983
|
+
Whether data files have headers, by default True.
|
|
984
|
+
columns : Sequence[int] | Sequence[str] | None, optional
|
|
985
|
+
Columns to read from files.
|
|
986
|
+
names : Sequence[str] | None, optional
|
|
987
|
+
Names to assign to columns.
|
|
988
|
+
n_rows : int | None, optional
|
|
989
|
+
Number of rows to read.
|
|
990
|
+
comment : str | None, optional
|
|
991
|
+
Character to indicate comments in data files.
|
|
992
|
+
glob_pattern : str, optional
|
|
993
|
+
Pattern for finding files in directories, by default "**/*.csv".
|
|
994
|
+
n_pickup_per_directory : int | None, optional
|
|
995
|
+
Maximum number of files to load per directory.
|
|
996
|
+
|
|
997
|
+
"""
|
|
998
|
+
self._dataset = Dataset.load_dataset(
|
|
999
|
+
source_paths,
|
|
1000
|
+
separator=separator,
|
|
1001
|
+
has_header=has_header,
|
|
1002
|
+
columns=columns,
|
|
1003
|
+
names=names,
|
|
1004
|
+
n_rows=n_rows,
|
|
1005
|
+
comment=comment,
|
|
1006
|
+
glob_pattern=glob_pattern,
|
|
1007
|
+
n_pickup_per_directory=n_pickup_per_directory,
|
|
1008
|
+
)
|
|
1009
|
+
|
|
1010
|
+
def __len__(self) -> int:
|
|
1011
|
+
"""Return the number of Data objects in the dataset.
|
|
1012
|
+
|
|
1013
|
+
Returns
|
|
1014
|
+
-------
|
|
1015
|
+
int
|
|
1016
|
+
Number of Data objects.
|
|
1017
|
+
"""
|
|
1018
|
+
return len(self._dataset)
|
|
1019
|
+
|
|
1020
|
+
def __str__(self) -> str:
|
|
1021
|
+
"""Return a string of the grouped mapping of tag to Data.
|
|
1022
|
+
|
|
1023
|
+
Returns
|
|
1024
|
+
-------
|
|
1025
|
+
str
|
|
1026
|
+
String representation of the grouped mapping of tag to Data.
|
|
1027
|
+
|
|
1028
|
+
"""
|
|
1029
|
+
return str(self.dataset)
|
|
1030
|
+
|
|
1031
|
+
def __repr__(self) -> str:
|
|
1032
|
+
"""Return a string representation of the object.
|
|
1033
|
+
|
|
1034
|
+
Returns
|
|
1035
|
+
-------
|
|
1036
|
+
str
|
|
1037
|
+
String representation of the object.
|
|
1038
|
+
|
|
1039
|
+
"""
|
|
1040
|
+
return f"{self.__class__.__name__}({self.datadirs})"
|
|
1041
|
+
|
|
1042
|
+
def __iter__(self) -> Iterator[Data]:
|
|
1043
|
+
"""Return an iterator over the Data objects.
|
|
1044
|
+
|
|
1045
|
+
Returns
|
|
1046
|
+
-------
|
|
1047
|
+
Iterator[Data]
|
|
1048
|
+
Iterator yielding Data objects.
|
|
1049
|
+
"""
|
|
1050
|
+
return iter(self._dataset)
|
|
1051
|
+
|
|
1052
|
+
@overload
|
|
1053
|
+
def __getitem__(self, index: int) -> Data: ...
|
|
1054
|
+
|
|
1055
|
+
@overload
|
|
1056
|
+
def __getitem__(self, index: slice) -> list[Data]: ...
|
|
1057
|
+
|
|
1058
|
+
def __getitem__(self, index):
|
|
1059
|
+
"""Access Data object(s) by index.
|
|
1060
|
+
|
|
1061
|
+
Parameters
|
|
1062
|
+
----------
|
|
1063
|
+
index : int or slice
|
|
1064
|
+
Index or slice to access Data objects.
|
|
1065
|
+
|
|
1066
|
+
Returns
|
|
1067
|
+
-------
|
|
1068
|
+
Data or list[Data]
|
|
1069
|
+
Single Data object if index is int, list of Data objects if slice.
|
|
1070
|
+
"""
|
|
1071
|
+
return self._dataset[index]
|
|
1072
|
+
|
|
1073
|
+
@property
|
|
1074
|
+
def datapaths(self) -> list[Path]:
|
|
1075
|
+
"""Get paths of all data files in the dataset.
|
|
1076
|
+
|
|
1077
|
+
Returns
|
|
1078
|
+
-------
|
|
1079
|
+
list[Path]
|
|
1080
|
+
List of paths to data files.
|
|
1081
|
+
"""
|
|
1082
|
+
return [data.datapath for data in self]
|
|
1083
|
+
|
|
1084
|
+
@property
|
|
1085
|
+
def datadirs(self) -> list[Path]:
|
|
1086
|
+
"""Get unique directory paths containing data files.
|
|
1087
|
+
|
|
1088
|
+
Returns
|
|
1089
|
+
-------
|
|
1090
|
+
list[Path]
|
|
1091
|
+
List of unique directory paths.
|
|
1092
|
+
"""
|
|
1093
|
+
return list(OrderedDict.fromkeys(x.parent for x in self.datapaths))
|
|
1094
|
+
|
|
1095
|
+
@property
|
|
1096
|
+
def dataset(self) -> list[Data]:
|
|
1097
|
+
"""Get the list of Data objects.
|
|
1098
|
+
|
|
1099
|
+
Returns
|
|
1100
|
+
-------
|
|
1101
|
+
list[Data]
|
|
1102
|
+
List of Data objects in the dataset.
|
|
1103
|
+
"""
|
|
1104
|
+
return self._dataset
|
|
1105
|
+
|
|
1106
|
+
@staticmethod
|
|
1107
|
+
def load_dataset(
|
|
1108
|
+
source_paths: FilePath | Iterable[FilePath],
|
|
1109
|
+
*,
|
|
1110
|
+
separator: str = ",",
|
|
1111
|
+
has_header: bool = True,
|
|
1112
|
+
columns: Sequence[int] | Sequence[str] | None = None,
|
|
1113
|
+
names: Sequence[str] | None = None,
|
|
1114
|
+
n_rows: int | None = None,
|
|
1115
|
+
comment: str | None = None,
|
|
1116
|
+
glob_pattern: str = "**/*.csv",
|
|
1117
|
+
n_pickup_per_directory: int | None = None,
|
|
1118
|
+
) -> list[Data]:
|
|
1119
|
+
"""Load multiple Data objects from specified sources.
|
|
1120
|
+
|
|
1121
|
+
Parameters
|
|
1122
|
+
----------
|
|
1123
|
+
source_paths : FilePath | Iterable[FilePath]
|
|
1124
|
+
Path(s) to data files or directories.
|
|
1125
|
+
separator : str, optional
|
|
1126
|
+
Single byte character to use as separator in the file, by default ",".
|
|
1127
|
+
has_header : bool, optional
|
|
1128
|
+
Whether data files have headers, by default True.
|
|
1129
|
+
columns : Sequence[int] | Sequence[str] | None, optional
|
|
1130
|
+
Columns to read from files.
|
|
1131
|
+
names : Sequence[str] | None, optional
|
|
1132
|
+
Names to assign to columns.
|
|
1133
|
+
n_rows : int | None, optional
|
|
1134
|
+
Number of rows to read.
|
|
1135
|
+
comment : str | None, optional
|
|
1136
|
+
Character to indicate comments in data files.
|
|
1137
|
+
glob_pattern : str, optional
|
|
1138
|
+
Pattern for finding files in directories, by default "**/*.csv".
|
|
1139
|
+
n_pickup_per_directory : int | None, optional
|
|
1140
|
+
Maximum number of files to load per directory.
|
|
1141
|
+
|
|
1142
|
+
Returns
|
|
1143
|
+
-------
|
|
1144
|
+
list[Data]
|
|
1145
|
+
List of loaded Data objects.
|
|
1146
|
+
|
|
1147
|
+
Raises
|
|
1148
|
+
------
|
|
1149
|
+
ValueError
|
|
1150
|
+
If source path does not exist.
|
|
1151
|
+
|
|
1152
|
+
"""
|
|
1153
|
+
if isinstance(source_paths, FilePath):
|
|
1154
|
+
source_paths = [source_paths]
|
|
1155
|
+
|
|
1156
|
+
data_loader: Callable[[Path], Data] = partial(
|
|
1157
|
+
Data,
|
|
1158
|
+
separator=separator,
|
|
1159
|
+
has_header=has_header,
|
|
1160
|
+
columns=columns,
|
|
1161
|
+
names=names,
|
|
1162
|
+
n_rows=n_rows,
|
|
1163
|
+
comment=comment,
|
|
1164
|
+
)
|
|
1165
|
+
|
|
1166
|
+
dataset: list[Data] = []
|
|
1167
|
+
for source_path in source_paths:
|
|
1168
|
+
path = Path(source_path)
|
|
1169
|
+
if not path.exists():
|
|
1170
|
+
msg = f"{_ERR_MSG_PATH_NOT_EXIST}: {path!s}"
|
|
1171
|
+
raise ValueError(msg)
|
|
1172
|
+
if path.is_dir():
|
|
1173
|
+
found_files = sorted(path.glob(glob_pattern))
|
|
1174
|
+
if n_pickup_per_directory is not None:
|
|
1175
|
+
found_files = found_files[:n_pickup_per_directory]
|
|
1176
|
+
if len(found_files) == 0:
|
|
1177
|
+
msg = f"{_WARN_MSG_NO_FILES_FOUND}: {path!s}, {glob_pattern}"
|
|
1178
|
+
warnings.warn(msg, UserWarning, stacklevel=2)
|
|
1179
|
+
dataset.extend(data_loader(x) for x in found_files)
|
|
1180
|
+
else:
|
|
1181
|
+
dataset.append(data_loader(path))
|
|
1182
|
+
return dataset
|
|
1183
|
+
|
|
1184
|
+
@cached_property
|
|
1185
|
+
def min_n_rows(self) -> int:
|
|
1186
|
+
"""Get the minimum number of rows across all Data objects.
|
|
1187
|
+
|
|
1188
|
+
Returns
|
|
1189
|
+
-------
|
|
1190
|
+
int
|
|
1191
|
+
Minimum number of rows.
|
|
1192
|
+
"""
|
|
1193
|
+
return min(map(len, self._dataset))
|
|
1194
|
+
|
|
1195
|
+
def get_columns(self, name: str, *, aligned: bool = True) -> list[pl.Series]:
|
|
1196
|
+
"""Get columns with specified name from all Data objects.
|
|
1197
|
+
|
|
1198
|
+
Parameters
|
|
1199
|
+
----------
|
|
1200
|
+
name : str
|
|
1201
|
+
Column name to retrieve.
|
|
1202
|
+
aligned : bool, optional
|
|
1203
|
+
Whether to align columns to minimum length, by default True.
|
|
1204
|
+
|
|
1205
|
+
Returns
|
|
1206
|
+
-------
|
|
1207
|
+
list[pl.Series]
|
|
1208
|
+
List of columns from each Data object.
|
|
1209
|
+
|
|
1210
|
+
"""
|
|
1211
|
+
if not aligned:
|
|
1212
|
+
return [data.get_column(name) for data in self]
|
|
1213
|
+
n = self.min_n_rows
|
|
1214
|
+
return [data.get_column(name)[:n] for data in self]
|
|
1215
|
+
|
|
1216
|
+
def get_columns_as_array(self, name: str) -> np.ndarray:
|
|
1217
|
+
"""Get columns as a numpy array with aligned lengths.
|
|
1218
|
+
|
|
1219
|
+
Parameters
|
|
1220
|
+
----------
|
|
1221
|
+
name : str
|
|
1222
|
+
Column name to retrieve.
|
|
1223
|
+
|
|
1224
|
+
Returns
|
|
1225
|
+
-------
|
|
1226
|
+
np.ndarray
|
|
1227
|
+
Array containing aligned columns from all Data objects.
|
|
1228
|
+
|
|
1229
|
+
"""
|
|
1230
|
+
return np.asarray(self.get_columns(name, aligned=True))
|
|
1231
|
+
|
|
1232
|
+
def get_axis_data(
|
|
1233
|
+
self,
|
|
1234
|
+
x_axis_name: str,
|
|
1235
|
+
y_data_name: str,
|
|
1236
|
+
*,
|
|
1237
|
+
x_axis_index: int = 0,
|
|
1238
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
1239
|
+
"""Get x-axis and y-axis data for plotting.
|
|
1240
|
+
|
|
1241
|
+
Parameters
|
|
1242
|
+
----------
|
|
1243
|
+
x_axis_name : str
|
|
1244
|
+
Column name for x-axis.
|
|
1245
|
+
y_data_name : str
|
|
1246
|
+
Column name for y-axis.
|
|
1247
|
+
x_axis_index : int, optional
|
|
1248
|
+
Index of Data object to use for x-axis, by default 0.
|
|
1249
|
+
|
|
1250
|
+
Returns
|
|
1251
|
+
-------
|
|
1252
|
+
tuple[np.ndarray, np.ndarray]
|
|
1253
|
+
X-axis values and corresponding Y-axis values.
|
|
1254
|
+
|
|
1255
|
+
"""
|
|
1256
|
+
y = self.get_columns_as_array(y_data_name)
|
|
1257
|
+
n = self.min_n_rows
|
|
1258
|
+
x = self._dataset[x_axis_index].get_column(x_axis_name).to_numpy()[:n]
|
|
1259
|
+
return x, y
|
|
1260
|
+
|
|
1261
|
+
def get_timeseries(
|
|
1262
|
+
self,
|
|
1263
|
+
name: str,
|
|
1264
|
+
*,
|
|
1265
|
+
t_shift: float = 0.0,
|
|
1266
|
+
t_axis_name: str = "t",
|
|
1267
|
+
t_axis_index: int = 0,
|
|
1268
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
1269
|
+
"""Get time series data with optional time shift.
|
|
1270
|
+
|
|
1271
|
+
Parameters
|
|
1272
|
+
----------
|
|
1273
|
+
name : str
|
|
1274
|
+
Name of data column.
|
|
1275
|
+
t_shift : float, optional
|
|
1276
|
+
Time shift to apply, by default 0.0.
|
|
1277
|
+
t_axis_name : str, optional
|
|
1278
|
+
Name of time axis column, by default "t".
|
|
1279
|
+
t_axis_index : int, optional
|
|
1280
|
+
Index of Data object to use for time axis, by default 0.
|
|
1281
|
+
|
|
1282
|
+
Returns
|
|
1283
|
+
-------
|
|
1284
|
+
tuple[np.ndarray, np.ndarray]
|
|
1285
|
+
Time values and corresponding data values.
|
|
1286
|
+
|
|
1287
|
+
"""
|
|
1288
|
+
t, y = self.get_axis_data(t_axis_name, name, x_axis_index=t_axis_index)
|
|
1289
|
+
t = t - t_shift
|
|
1290
|
+
return t, y
|
|
1291
|
+
|
|
1292
|
+
|
|
1293
|
+
# Local Variables:
|
|
1294
|
+
# jinx-local-words: "Iterable Runtime StringIO csv datadict datadir datadirs dataframe datapath datapaths dataset dest dtype ndarray noqa np numpy param polars str timeseries" # noqa: E501
|
|
1295
|
+
# End:
|