tabpackage 0.0.1__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.
@@ -0,0 +1,307 @@
1
+ import dataclasses
2
+ from typing import TYPE_CHECKING, TypeAlias
3
+
4
+ import numpy as np
5
+ import torch
6
+ from sklearn.preprocessing import OrdinalEncoder
7
+ from torch import Tensor
8
+
9
+ if TYPE_CHECKING:
10
+ import pandas as pd
11
+ import polars as pl
12
+ else:
13
+ try:
14
+ import pandas as pd
15
+ except ModuleNotFoundError as error:
16
+ if error.name != 'pandas':
17
+ raise
18
+ pd = None
19
+
20
+ try:
21
+ import polars as pl
22
+ except ModuleNotFoundError as error:
23
+ if error.name != 'polars':
24
+ raise
25
+ pl = None
26
+
27
+ XLike: TypeAlias = 'np.ndarray | pd.DataFrame | pl.DataFrame'
28
+ YLike: TypeAlias = 'np.ndarray | pd.Series | pl.Series'
29
+ YPredLike: TypeAlias = np.ndarray
30
+ DType: TypeAlias = 'np.dtype | pd.api.extensions.ExtensionDtype | pl.DataType'
31
+
32
+
33
+ def _get_dataframe_values(
34
+ X: 'pd.DataFrame | pl.DataFrame', columns: list[str] | tuple[str, ...]
35
+ ) -> np.ndarray:
36
+ return (
37
+ X[list(columns)].to_numpy(writable=True)
38
+ if pl is not None and isinstance(X, pl.DataFrame)
39
+ else X[list(columns)].to_numpy()
40
+ )
41
+
42
+
43
+ @dataclasses.dataclass(frozen=True, kw_only=True)
44
+ class FeatureSchema:
45
+ container: type
46
+ dtypes: tuple[DType, ...]
47
+ columns: tuple[str, ...] | None
48
+
49
+ @property
50
+ def n_features(self) -> int:
51
+ return len(self.dtypes)
52
+
53
+ @property
54
+ def categorical_columns(self) -> tuple[str, ...]:
55
+ return tuple(
56
+ name
57
+ for name, dtype in zip(self.columns or (), self.dtypes)
58
+ if not (
59
+ (isinstance(dtype, np.dtype) and dtype == np.float32)
60
+ or (pl is not None and dtype == pl.Float32)
61
+ )
62
+ )
63
+
64
+ @property
65
+ def n_numerical_features(self) -> int:
66
+ return self.n_features - len(self.categorical_columns)
67
+
68
+ @classmethod
69
+ def from_data(cls, X: XLike) -> 'FeatureSchema':
70
+ if np.ma.isMaskedArray(X):
71
+ raise TypeError('Masked arrays are not supported')
72
+
73
+ if len(X.shape) != 2:
74
+ raise ValueError('`X` must be two-dimensional')
75
+
76
+ if X.shape[0] == 0 or X.shape[1] == 0:
77
+ raise ValueError('`X` must have at least one row and one column')
78
+
79
+ if pd is not None and isinstance(X, pd.DataFrame):
80
+ if not X.columns.is_unique:
81
+ raise ValueError('Feature column names must be unique')
82
+
83
+ if not all(isinstance(name, str) for name in X.columns):
84
+ raise TypeError('Feature column names must be strings')
85
+
86
+ columns = tuple(X.columns)
87
+ dtypes = tuple(X.dtypes)
88
+ for name, dtype in zip(columns, dtypes, strict=True):
89
+ column = X[name]
90
+ if isinstance(dtype, np.dtype) and dtype == np.float32:
91
+ if not np.isfinite(column.to_numpy()).all():
92
+ raise ValueError(
93
+ 'Numerical features must contain only finite values'
94
+ )
95
+ else:
96
+ if isinstance(dtype, pd.CategoricalDtype):
97
+ categories = dtype.categories
98
+ if not (
99
+ _is_string_dtype(categories.dtype)
100
+ or (
101
+ categories.dtype == object
102
+ and pd.api.types.infer_dtype(categories, skipna=False)
103
+ == 'string'
104
+ )
105
+ ):
106
+ raise TypeError('Feature categories must be strings')
107
+ elif not _is_string_dtype(dtype):
108
+ raise TypeError(
109
+ 'Numerical features require `numpy.float32`; categorical'
110
+ ' features require a string data type or string categories'
111
+ )
112
+
113
+ if column.isna().any():
114
+ raise ValueError(
115
+ 'Categorical features must not contain missing values'
116
+ )
117
+ elif pl is not None and isinstance(X, pl.DataFrame):
118
+ columns = tuple(X.columns)
119
+ dtypes = tuple(X.dtypes)
120
+ for name, dtype in zip(columns, dtypes, strict=True):
121
+ column = X[name]
122
+ if column.null_count() != 0:
123
+ raise ValueError('Features must not contain missing values')
124
+
125
+ if dtype == pl.Float32:
126
+ if not column.is_finite().all():
127
+ raise ValueError(
128
+ 'Numerical features must contain only finite values'
129
+ )
130
+ elif not (
131
+ dtype == pl.String or isinstance(dtype, (pl.Categorical, pl.Enum))
132
+ ):
133
+ raise TypeError(
134
+ 'Polars feature columns require `polars.Float32`,'
135
+ ' `polars.String`, `polars.Categorical`, or `polars.Enum`'
136
+ )
137
+ else:
138
+ columns = None
139
+ dtypes = (X.dtype,) * X.shape[1]
140
+ if X.dtype != np.float32:
141
+ raise TypeError('`X` must have the data type `numpy.float32`')
142
+
143
+ if not np.isfinite(X).all():
144
+ raise ValueError('`X` must contain only finite values')
145
+
146
+ return cls(container=type(X), dtypes=dtypes, columns=columns)
147
+
148
+ def make_encoder(self, X: XLike) -> OrdinalEncoder | None:
149
+ return (
150
+ None
151
+ if isinstance(X, np.ndarray) or not self.categorical_columns
152
+ else OrdinalEncoder(
153
+ dtype=np.int64, handle_unknown='use_encoded_value', unknown_value=-1
154
+ ).fit(_get_dataframe_values(X, self.categorical_columns))
155
+ )
156
+
157
+ def encode(self, X: XLike, encoder: OrdinalEncoder | None) -> dict[str, np.ndarray]:
158
+ if isinstance(X, np.ndarray):
159
+ return {'x_num': X}
160
+
161
+ categorical_columns = self.categorical_columns
162
+ numerical_columns = [
163
+ name for name in X.columns if name not in categorical_columns
164
+ ]
165
+
166
+ arrays = {}
167
+ if numerical_columns:
168
+ arrays['x_num'] = _get_dataframe_values(X, numerical_columns)
169
+ if encoder is not None:
170
+ codes = encoder.transform(_get_dataframe_values(X, categorical_columns))
171
+ cardinalities = np.array([len(values) for values in encoder.categories_])
172
+ arrays['x_cat'] = np.where(codes == -1, cardinalities, codes)
173
+
174
+ return arrays
175
+
176
+
177
+ def _is_numpy_string_dtype(dtype: np.dtype) -> bool:
178
+ string_dtype = getattr(getattr(np, 'dtypes', None), 'StringDType', None)
179
+ return dtype.kind == 'U' or (
180
+ string_dtype is not None
181
+ and isinstance(dtype, string_dtype)
182
+ and not hasattr(dtype, 'na_object')
183
+ )
184
+
185
+
186
+ def _is_string_dtype(dtype: DType) -> bool:
187
+ return (pd is not None and isinstance(dtype, pd.StringDtype)) or (
188
+ isinstance(dtype, np.dtype) and _is_numpy_string_dtype(dtype)
189
+ )
190
+
191
+
192
+ def _get_label_values(y: YLike) -> np.ndarray:
193
+ return (
194
+ y.to_numpy(writable=True)
195
+ if pl is not None and isinstance(y, pl.Series)
196
+ else (
197
+ y.to_numpy()
198
+ if pd is not None and isinstance(y, pd.Series)
199
+ else (y.astype(object) if y.dtype.kind == 'T' else y)
200
+ )
201
+ )
202
+
203
+
204
+ @dataclasses.dataclass(frozen=True, kw_only=True)
205
+ class LabelSchema:
206
+ container: type
207
+ dtype: DType
208
+ classes: np.ndarray | None
209
+
210
+ @classmethod
211
+ def from_data(cls, y: YLike, *, classification: bool) -> 'LabelSchema':
212
+ if np.ma.isMaskedArray(y):
213
+ raise TypeError('Masked arrays are not supported')
214
+
215
+ if len(y.shape) != 1:
216
+ raise ValueError('`y` must be one-dimensional')
217
+
218
+ if pl is not None and isinstance(y, pl.Series):
219
+ if y.dtype not in (
220
+ (pl.Int64, pl.String) if classification else (pl.Float32,)
221
+ ):
222
+ raise TypeError(
223
+ 'Classification targets require `polars.Int64` or `polars.String`'
224
+ if classification
225
+ else 'Regression targets require `polars.Float32`'
226
+ )
227
+
228
+ if y.null_count() != 0:
229
+ raise ValueError('`y` must not contain missing values')
230
+ else:
231
+ numeric_dtype = np.int64 if classification else np.float32
232
+ if not (
233
+ (isinstance(y.dtype, np.dtype) and y.dtype == numeric_dtype)
234
+ or (classification and _is_string_dtype(y.dtype))
235
+ ):
236
+ raise TypeError(
237
+ 'Classification targets require `numpy.int64` or a string data type'
238
+ if classification
239
+ else 'Regression targets require `numpy.float32`'
240
+ )
241
+
242
+ if pd is not None and isinstance(y, pd.Series) and y.isna().any():
243
+ raise ValueError('`y` must not contain missing values')
244
+
245
+ values = _get_label_values(y)
246
+ if classification:
247
+ classes = np.unique(values)
248
+ if len(classes) < 2:
249
+ raise ValueError(
250
+ 'Classification targets must contain at least two classes'
251
+ )
252
+ else:
253
+ if not np.isfinite(values).all():
254
+ raise ValueError('Regression targets must contain only finite values')
255
+
256
+ classes = None
257
+
258
+ return cls(container=type(y), dtype=y.dtype, classes=classes)
259
+
260
+ def validate(self, y: YLike) -> None:
261
+ if np.ma.isMaskedArray(y):
262
+ raise TypeError('Masked arrays are not supported')
263
+
264
+ if type(y) is not self.container:
265
+ raise TypeError(
266
+ 'Labels must use the same container type as the training labels'
267
+ )
268
+
269
+ if len(y.shape) != 1:
270
+ raise ValueError('`y` must be one-dimensional')
271
+
272
+ if y.dtype != self.dtype:
273
+ raise TypeError(
274
+ 'Labels must have the same data type as the training labels'
275
+ )
276
+
277
+ if pl is not None and isinstance(y, pl.Series):
278
+ if y.null_count() != 0:
279
+ raise ValueError('`y` must not contain missing values')
280
+ elif pd is not None and isinstance(y, pd.Series) and y.isna().any():
281
+ raise ValueError('`y` must not contain missing values')
282
+
283
+ values = _get_label_values(y)
284
+ if self.classes is None:
285
+ if not np.isfinite(values).all():
286
+ raise ValueError('Regression targets must contain only finite values')
287
+ elif not np.isin(values, self.classes).all():
288
+ raise ValueError(
289
+ 'Labels contain classes that were not present in the training data'
290
+ )
291
+
292
+ def encode(self, y: YLike) -> np.ndarray:
293
+ values = _get_label_values(y)
294
+ return (
295
+ values
296
+ if self.classes is None
297
+ else np.searchsorted(self.classes, values).astype(np.int64)
298
+ )
299
+
300
+
301
+ def make_dataset(
302
+ arrays: dict[str, np.ndarray], *, device: torch.device
303
+ ) -> dict[str, Tensor]:
304
+ return {
305
+ name: torch.as_tensor(np.ascontiguousarray(array), device=device)
306
+ for name, array in arrays.items()
307
+ }