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.
- tabpackage/__init__.py +18 -0
- tabpackage/_training.py +3380 -0
- tabpackage/ensemble.py +263 -0
- tabpackage/metrics.py +253 -0
- tabpackage/nn.py +2110 -0
- tabpackage/optim.py +943 -0
- tabpackage/sklearn/__init__.py +18 -0
- tabpackage/sklearn/_config.py +561 -0
- tabpackage/sklearn/_data.py +307 -0
- tabpackage/sklearn/_estimators.py +1210 -0
- tabpackage/utils/__init__.py +1 -0
- tabpackage/utils/torch.py +166 -0
- tabpackage-0.0.1.dist-info/METADATA +398 -0
- tabpackage-0.0.1.dist-info/RECORD +15 -0
- tabpackage-0.0.1.dist-info/WHEEL +4 -0
|
@@ -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
|
+
}
|