dasket 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.
- dasket/__init__.py +4 -0
- dasket/dataset.py +351 -0
- dasket/reports.py +15 -0
- dasket/validation.py +44 -0
- dasket-0.1.0.dist-info/METADATA +772 -0
- dasket-0.1.0.dist-info/RECORD +7 -0
- dasket-0.1.0.dist-info/WHEEL +4 -0
dasket/__init__.py
ADDED
dasket/dataset.py
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import math
|
|
2
|
+
import random
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import polars as pl
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
from dasket.reports import ValidationReport
|
|
11
|
+
from dasket.validation import validate_records
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Dataset:
|
|
15
|
+
def __init__(self, data: pl.DataFrame):
|
|
16
|
+
if not isinstance(data, pl.DataFrame):
|
|
17
|
+
raise TypeError("data must be a Polars DataFrame")
|
|
18
|
+
|
|
19
|
+
self._data = data
|
|
20
|
+
|
|
21
|
+
def validate(self, schema: type[BaseModel]) -> ValidationReport:
|
|
22
|
+
result = validate_records(self._data, schema)
|
|
23
|
+
|
|
24
|
+
return ValidationReport(**result)
|
|
25
|
+
|
|
26
|
+
def normalize(
|
|
27
|
+
self,
|
|
28
|
+
columns: list[str],
|
|
29
|
+
lowercase: list[str] | None = None,
|
|
30
|
+
) -> "Dataset":
|
|
31
|
+
lowercase = lowercase or []
|
|
32
|
+
|
|
33
|
+
if len(columns) != len(set(columns)):
|
|
34
|
+
raise ValueError("columns must not contain duplicates")
|
|
35
|
+
|
|
36
|
+
if len(lowercase) != len(set(lowercase)):
|
|
37
|
+
raise ValueError("lowercase must not contain duplicates")
|
|
38
|
+
|
|
39
|
+
if not columns:
|
|
40
|
+
return Dataset(self._data.clone())
|
|
41
|
+
|
|
42
|
+
missing_columns = set(columns) - set(self._data.columns)
|
|
43
|
+
|
|
44
|
+
if missing_columns:
|
|
45
|
+
raise ValueError(f"Columns not found: {sorted(missing_columns)}")
|
|
46
|
+
|
|
47
|
+
invalid_lowercase = set(lowercase) - set(columns)
|
|
48
|
+
|
|
49
|
+
if invalid_lowercase:
|
|
50
|
+
raise ValueError(
|
|
51
|
+
"Columns in lowercase must also be in columns: "
|
|
52
|
+
f"{sorted(invalid_lowercase)}"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
expressions = []
|
|
56
|
+
|
|
57
|
+
for column in columns:
|
|
58
|
+
if self._data.schema[column] != pl.String:
|
|
59
|
+
raise TypeError(f"Column '{column}' must have String dtype")
|
|
60
|
+
|
|
61
|
+
expression = pl.col(column).str.strip_chars()
|
|
62
|
+
|
|
63
|
+
if column in lowercase:
|
|
64
|
+
expression = expression.str.to_lowercase()
|
|
65
|
+
|
|
66
|
+
expressions.append(expression.alias(column))
|
|
67
|
+
|
|
68
|
+
return Dataset(self._data.with_columns(expressions))
|
|
69
|
+
|
|
70
|
+
def find_duplicates(
|
|
71
|
+
self,
|
|
72
|
+
columns: list[str] | None = None,
|
|
73
|
+
) -> pl.DataFrame:
|
|
74
|
+
if columns is None:
|
|
75
|
+
columns = self._data.columns
|
|
76
|
+
|
|
77
|
+
if not columns:
|
|
78
|
+
raise ValueError("At least one column is required for duplicate detection")
|
|
79
|
+
|
|
80
|
+
missing_columns = set(columns) - set(self._data.columns)
|
|
81
|
+
|
|
82
|
+
if missing_columns:
|
|
83
|
+
raise ValueError(f"Columns not found: {sorted(missing_columns)}")
|
|
84
|
+
|
|
85
|
+
if len(columns) != len(set(columns)):
|
|
86
|
+
raise ValueError("columns must not contain duplicates")
|
|
87
|
+
|
|
88
|
+
return self._data.filter(pl.struct(columns).is_duplicated()).unique(
|
|
89
|
+
subset=columns,
|
|
90
|
+
keep="first",
|
|
91
|
+
maintain_order=True,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
def missing_values(self) -> dict[str, int]:
|
|
95
|
+
return {
|
|
96
|
+
column: self._data[column].null_count() for column in self._data.columns
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
def drop_missing(
|
|
100
|
+
self,
|
|
101
|
+
columns: list[str] | None = None,
|
|
102
|
+
) -> "Dataset":
|
|
103
|
+
if columns is None:
|
|
104
|
+
columns = self._data.columns
|
|
105
|
+
|
|
106
|
+
if len(columns) != len(set(columns)):
|
|
107
|
+
raise ValueError("columns must not contain duplicates")
|
|
108
|
+
|
|
109
|
+
if not columns:
|
|
110
|
+
return Dataset(self._data.clone())
|
|
111
|
+
|
|
112
|
+
missing_columns = set(columns) - set(self._data.columns)
|
|
113
|
+
|
|
114
|
+
if missing_columns:
|
|
115
|
+
raise ValueError(f"Columns not found: {sorted(missing_columns)}")
|
|
116
|
+
|
|
117
|
+
cleaned_data = self._data.drop_nulls(subset=columns)
|
|
118
|
+
|
|
119
|
+
return Dataset(cleaned_data)
|
|
120
|
+
|
|
121
|
+
@classmethod
|
|
122
|
+
def from_file(cls, path: str | Path) -> "Dataset":
|
|
123
|
+
file_path = Path(path)
|
|
124
|
+
|
|
125
|
+
if not file_path.is_file():
|
|
126
|
+
raise FileNotFoundError(f"Dataset not found: {file_path}")
|
|
127
|
+
|
|
128
|
+
suffix = file_path.suffix.lower()
|
|
129
|
+
|
|
130
|
+
loaders = {
|
|
131
|
+
".csv": pl.read_csv,
|
|
132
|
+
".json": pl.read_json,
|
|
133
|
+
".jsonl": pl.read_ndjson,
|
|
134
|
+
".parquet": pl.read_parquet,
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
loader = loaders.get(suffix)
|
|
138
|
+
|
|
139
|
+
if loader is None:
|
|
140
|
+
raise ValueError(f"Unsupported file format: {suffix}")
|
|
141
|
+
|
|
142
|
+
return cls(loader(file_path))
|
|
143
|
+
|
|
144
|
+
def profile(self) -> dict[str, Any]:
|
|
145
|
+
return {
|
|
146
|
+
"row_count": self.row_count,
|
|
147
|
+
"column_count": len(self.columns),
|
|
148
|
+
"columns": {
|
|
149
|
+
column: {
|
|
150
|
+
"dtype": str(self._data.schema[column]),
|
|
151
|
+
"null_count": self._data[column].null_count(),
|
|
152
|
+
}
|
|
153
|
+
for column in self.columns
|
|
154
|
+
},
|
|
155
|
+
"duplicate_count": int(self._data.is_duplicated().sum()),
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
def to_file(self, path: str | Path) -> None:
|
|
159
|
+
file_path = Path(path)
|
|
160
|
+
suffix = file_path.suffix.lower()
|
|
161
|
+
|
|
162
|
+
exporters = {
|
|
163
|
+
".csv": self._data.write_csv,
|
|
164
|
+
".json": self._data.write_json,
|
|
165
|
+
".jsonl": self._data.write_ndjson,
|
|
166
|
+
".parquet": self._data.write_parquet,
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
exporter = exporters.get(suffix)
|
|
170
|
+
|
|
171
|
+
if exporter is None:
|
|
172
|
+
raise ValueError(f"Unsupported file format: {suffix}")
|
|
173
|
+
|
|
174
|
+
if file_path.exists() and file_path.is_dir():
|
|
175
|
+
raise IsADirectoryError(f"Expected a file path, got directory: {file_path}")
|
|
176
|
+
|
|
177
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
178
|
+
exporter(file_path)
|
|
179
|
+
|
|
180
|
+
def select_columns(self, columns: list[str]) -> "Dataset":
|
|
181
|
+
if not columns:
|
|
182
|
+
raise ValueError("At least one column must be selected")
|
|
183
|
+
|
|
184
|
+
if len(columns) != len(set(columns)):
|
|
185
|
+
raise ValueError("columns must not contain duplicates")
|
|
186
|
+
|
|
187
|
+
missing_columns = set(columns) - set(self._data.columns)
|
|
188
|
+
|
|
189
|
+
if missing_columns:
|
|
190
|
+
raise ValueError(f"Columns not found: {sorted(missing_columns)}")
|
|
191
|
+
|
|
192
|
+
return Dataset(self._data.select(columns))
|
|
193
|
+
|
|
194
|
+
def drop_columns(self, columns: list[str]) -> "Dataset":
|
|
195
|
+
if len(columns) != len(set(columns)):
|
|
196
|
+
raise ValueError("columns must not contain duplicates")
|
|
197
|
+
|
|
198
|
+
missing_columns = set(columns) - set(self._data.columns)
|
|
199
|
+
|
|
200
|
+
if missing_columns:
|
|
201
|
+
raise ValueError(f"Columns not found: {sorted(missing_columns)}")
|
|
202
|
+
|
|
203
|
+
if len(columns) >= len(self._data.columns):
|
|
204
|
+
raise ValueError("Cannot drop all columns from a dataset")
|
|
205
|
+
|
|
206
|
+
return Dataset(self._data.drop(columns))
|
|
207
|
+
|
|
208
|
+
def rename_columns(self, mapping: dict[str, str]) -> "Dataset":
|
|
209
|
+
missing_columns = set(mapping) - set(self._data.columns)
|
|
210
|
+
|
|
211
|
+
if missing_columns:
|
|
212
|
+
raise ValueError(f"Columns not found: {sorted(missing_columns)}")
|
|
213
|
+
|
|
214
|
+
if not mapping:
|
|
215
|
+
return Dataset(self._data.clone())
|
|
216
|
+
|
|
217
|
+
if any(not isinstance(name, str) or not name for name in mapping.values()):
|
|
218
|
+
raise ValueError("New column names must be non-empty strings")
|
|
219
|
+
|
|
220
|
+
resulting_names = [mapping.get(column, column) for column in self._data.columns]
|
|
221
|
+
|
|
222
|
+
if len(resulting_names) != len(set(resulting_names)):
|
|
223
|
+
raise ValueError("Renaming would create duplicate column names")
|
|
224
|
+
|
|
225
|
+
return Dataset(self._data.rename(mapping))
|
|
226
|
+
|
|
227
|
+
def filter(self, expression: pl.Expr) -> "Dataset":
|
|
228
|
+
if not isinstance(expression, pl.Expr):
|
|
229
|
+
raise TypeError("expression must be a Polars Expr")
|
|
230
|
+
|
|
231
|
+
filtered_data = self._data.filter(expression)
|
|
232
|
+
|
|
233
|
+
return Dataset(filtered_data)
|
|
234
|
+
|
|
235
|
+
def split(
|
|
236
|
+
self,
|
|
237
|
+
train_ratio: float = 0.8,
|
|
238
|
+
seed: int | None = None,
|
|
239
|
+
) -> tuple["Dataset", "Dataset"]:
|
|
240
|
+
if (
|
|
241
|
+
isinstance(train_ratio, bool)
|
|
242
|
+
or not isinstance(train_ratio, (int, float))
|
|
243
|
+
or not math.isfinite(train_ratio)
|
|
244
|
+
or not 0 < train_ratio < 1
|
|
245
|
+
):
|
|
246
|
+
raise ValueError("train_ratio must be a finite number between 0 and 1")
|
|
247
|
+
|
|
248
|
+
row_count = self.row_count
|
|
249
|
+
|
|
250
|
+
if row_count == 0:
|
|
251
|
+
return Dataset(self._data.clone()), Dataset(self._data.clone())
|
|
252
|
+
|
|
253
|
+
if row_count == 1:
|
|
254
|
+
raise ValueError("At least 2 rows are required to split a dataset")
|
|
255
|
+
|
|
256
|
+
train_count = math.floor(row_count * train_ratio)
|
|
257
|
+
train_count = max(1, min(train_count, row_count - 1))
|
|
258
|
+
|
|
259
|
+
indices = list(range(row_count))
|
|
260
|
+
random.Random(seed).shuffle(indices)
|
|
261
|
+
|
|
262
|
+
train_indices = indices[:train_count]
|
|
263
|
+
test_indices = indices[train_count:]
|
|
264
|
+
|
|
265
|
+
return (
|
|
266
|
+
Dataset(self._data[train_indices]),
|
|
267
|
+
Dataset(self._data[test_indices]),
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
def stratified_split(
|
|
271
|
+
self,
|
|
272
|
+
column: str,
|
|
273
|
+
train_ratio: float = 0.8,
|
|
274
|
+
seed: int | None = None,
|
|
275
|
+
) -> tuple["Dataset", "Dataset"]:
|
|
276
|
+
if (
|
|
277
|
+
isinstance(train_ratio, bool)
|
|
278
|
+
or not isinstance(train_ratio, (int, float))
|
|
279
|
+
or not math.isfinite(train_ratio)
|
|
280
|
+
or not 0 < train_ratio < 1
|
|
281
|
+
):
|
|
282
|
+
raise ValueError("train_ratio must be a finite number between 0 and 1")
|
|
283
|
+
|
|
284
|
+
if column not in self._data.columns:
|
|
285
|
+
raise ValueError(f"Column not found: {column}")
|
|
286
|
+
|
|
287
|
+
groups = defaultdict(list)
|
|
288
|
+
|
|
289
|
+
for index, value in enumerate(self._data[column].to_list()):
|
|
290
|
+
if value is None:
|
|
291
|
+
raise ValueError(f"Column '{column}' contains null values")
|
|
292
|
+
|
|
293
|
+
if isinstance(value, float) and math.isnan(value):
|
|
294
|
+
raise ValueError(f"Column '{column}' contains NaN values")
|
|
295
|
+
|
|
296
|
+
try:
|
|
297
|
+
hash(value)
|
|
298
|
+
except TypeError as error:
|
|
299
|
+
raise TypeError(
|
|
300
|
+
f"Column '{column}' must contain hashable scalar values "
|
|
301
|
+
"for stratified splitting"
|
|
302
|
+
) from error
|
|
303
|
+
|
|
304
|
+
groups[value].append(index)
|
|
305
|
+
|
|
306
|
+
rng = random.Random(seed)
|
|
307
|
+
train_indices = []
|
|
308
|
+
test_indices = []
|
|
309
|
+
|
|
310
|
+
for value, indices in groups.items():
|
|
311
|
+
if len(indices) < 2:
|
|
312
|
+
raise ValueError(
|
|
313
|
+
f"Every class must have at least 2 samples. "
|
|
314
|
+
f"Class '{value}' has {len(indices)}."
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
rng.shuffle(indices)
|
|
318
|
+
|
|
319
|
+
train_count = math.floor(len(indices) * train_ratio)
|
|
320
|
+
train_count = max(1, min(train_count, len(indices) - 1))
|
|
321
|
+
|
|
322
|
+
train_indices.extend(indices[:train_count])
|
|
323
|
+
test_indices.extend(indices[train_count:])
|
|
324
|
+
|
|
325
|
+
rng.shuffle(train_indices)
|
|
326
|
+
rng.shuffle(test_indices)
|
|
327
|
+
|
|
328
|
+
return (
|
|
329
|
+
Dataset(self._data[train_indices]),
|
|
330
|
+
Dataset(self._data[test_indices]),
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
@classmethod
|
|
334
|
+
def from_dataframe(cls, dataframe: pl.DataFrame) -> "Dataset":
|
|
335
|
+
return cls(dataframe)
|
|
336
|
+
|
|
337
|
+
@property
|
|
338
|
+
def data(self) -> pl.DataFrame:
|
|
339
|
+
return self._data
|
|
340
|
+
|
|
341
|
+
@property
|
|
342
|
+
def shape(self) -> tuple[int, int]:
|
|
343
|
+
return self._data.shape
|
|
344
|
+
|
|
345
|
+
@property
|
|
346
|
+
def columns(self) -> list[str]:
|
|
347
|
+
return self._data.columns
|
|
348
|
+
|
|
349
|
+
@property
|
|
350
|
+
def row_count(self) -> int:
|
|
351
|
+
return self._data.height
|
dasket/reports.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class ValidationReport:
|
|
7
|
+
total_count: int
|
|
8
|
+
valid_count: int
|
|
9
|
+
invalid_count: int
|
|
10
|
+
valid_records: list[dict[str, Any]]
|
|
11
|
+
invalid_records: list[dict[str, Any]]
|
|
12
|
+
|
|
13
|
+
@property
|
|
14
|
+
def is_valid(self) -> bool:
|
|
15
|
+
return self.invalid_count == 0
|
dasket/validation.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
import polars as pl
|
|
4
|
+
from pydantic import BaseModel, ValidationError
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def validate_records(
|
|
8
|
+
dataframe: pl.DataFrame,
|
|
9
|
+
schema: type[BaseModel],
|
|
10
|
+
) -> dict[str, Any]:
|
|
11
|
+
valid_records = []
|
|
12
|
+
invalid_records = []
|
|
13
|
+
|
|
14
|
+
for index, record in enumerate(dataframe.to_dicts()):
|
|
15
|
+
try:
|
|
16
|
+
validated = schema.model_validate(record)
|
|
17
|
+
valid_records.append(validated.model_dump())
|
|
18
|
+
|
|
19
|
+
except ValidationError as error:
|
|
20
|
+
errors = [
|
|
21
|
+
{
|
|
22
|
+
"field": ".".join(str(part) for part in item["loc"]),
|
|
23
|
+
"error_type": item["type"],
|
|
24
|
+
"message": item["msg"],
|
|
25
|
+
"input": item.get("input"),
|
|
26
|
+
}
|
|
27
|
+
for item in error.errors()
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
invalid_records.append(
|
|
31
|
+
{
|
|
32
|
+
"row": index,
|
|
33
|
+
"record": record,
|
|
34
|
+
"errors": errors,
|
|
35
|
+
}
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
"total_count": dataframe.height,
|
|
40
|
+
"valid_count": len(valid_records),
|
|
41
|
+
"invalid_count": len(invalid_records),
|
|
42
|
+
"valid_records": valid_records,
|
|
43
|
+
"invalid_records": invalid_records,
|
|
44
|
+
}
|
|
@@ -0,0 +1,772 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dasket
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A composable Python library for preparing and assessing datasets for AI workflows.
|
|
5
|
+
Author: amar
|
|
6
|
+
Author-email: amar <amargurung1013@gmail.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Requires-Dist: polars>=2.1.0
|
|
9
|
+
Requires-Dist: pyarrow>=26.0.1
|
|
10
|
+
Requires-Dist: pydantic>=2.14.2
|
|
11
|
+
Requires-Python: >=3.12
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
<div align="center">
|
|
15
|
+
|
|
16
|
+
<img src="docs/assets/dasket.png" alt="Dasket logo" width="520">
|
|
17
|
+
|
|
18
|
+
### Prepare and assess datasets for AI workflows.
|
|
19
|
+
|
|
20
|
+
A composable Python library for dataset ingestion, validation, transformation, profiling, and preparation.
|
|
21
|
+
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Overview
|
|
27
|
+
|
|
28
|
+
**Dasket** is a Python library designed to simplify the process of preparing and assessing datasets before they enter AI and machine learning workflows.
|
|
29
|
+
|
|
30
|
+
Working with raw data often involves repetitive operations: inspecting schemas, handling missing values, normalizing text, detecting duplicates, validating records, and preparing reproducible dataset partitions.
|
|
31
|
+
|
|
32
|
+
Dasket provides a consistent interface for these operations while leveraging established data-processing libraries rather than reinventing their underlying functionality.
|
|
33
|
+
|
|
34
|
+
Built on **Polars**, **Pydantic**, and **PyArrow**, Dasket focuses on composability, predictable behavior, and explicit data transformations.
|
|
35
|
+
|
|
36
|
+
### Design principles
|
|
37
|
+
|
|
38
|
+
- **Composability:** Dataset operations return Dataset instances, allowing transformations to be chained or combined.
|
|
39
|
+
- **Explicit validation:** Invalid inputs should produce meaningful errors rather than unexpected behavior.
|
|
40
|
+
- **Reproducibility:** Randomized dataset splitting supports deterministic seeds.
|
|
41
|
+
- **Separation of concerns:** Dasket provides a higher-level dataset interface while delegating dataframe operations to Polars.
|
|
42
|
+
- **Minimal abstraction:** Avoid unnecessary frameworks and hide as little of the underlying data-processing behavior as possible.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Features
|
|
47
|
+
|
|
48
|
+
| Category | Capabilities |
|
|
49
|
+
|---|---|
|
|
50
|
+
| Data ingestion | CSV, JSON, JSONL, Parquet |
|
|
51
|
+
| Data export | CSV, JSON, JSONL, Parquet |
|
|
52
|
+
| Schema validation | Pydantic model-based validation |
|
|
53
|
+
| Text processing | Whitespace normalization, lowercase conversion |
|
|
54
|
+
| Duplicate detection | Duplicate identification by selected columns |
|
|
55
|
+
| Missing values | Null-value analysis and row removal |
|
|
56
|
+
| Dataset profiling | Dimensions, column types, null counts, duplicate counts |
|
|
57
|
+
| Column operations | Selection, removal, renaming |
|
|
58
|
+
| Filtering | Polars expression-based filtering |
|
|
59
|
+
| Dataset splitting | Random train/test splitting |
|
|
60
|
+
| Stratification | Class-preserving train/test splitting |
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Architecture
|
|
65
|
+
|
|
66
|
+
Dasket uses a small wrapper abstraction around Polars DataFrames.
|
|
67
|
+
|
|
68
|
+
```text
|
|
69
|
+
Application
|
|
70
|
+
|
|
|
71
|
+
v
|
|
72
|
+
+---------------+
|
|
73
|
+
| Dasket |
|
|
74
|
+
| |
|
|
75
|
+
| Dataset |
|
|
76
|
+
+---------------+
|
|
77
|
+
|
|
|
78
|
+
+-----------+-----------+
|
|
79
|
+
| | |
|
|
80
|
+
v v v
|
|
81
|
+
Ingestion Transformations Profiling
|
|
82
|
+
| | |
|
|
83
|
+
+-----------+-----------+
|
|
84
|
+
|
|
|
85
|
+
v
|
|
86
|
+
Polars DataFrame
|
|
87
|
+
|
|
|
88
|
+
+----------+----------+
|
|
89
|
+
| |
|
|
90
|
+
v v
|
|
91
|
+
Validation Storage
|
|
92
|
+
| |
|
|
93
|
+
v v
|
|
94
|
+
Pydantic PyArrow
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Technology stack
|
|
98
|
+
|
|
99
|
+
| Technology | Responsibility |
|
|
100
|
+
|---|---|
|
|
101
|
+
| Python 3.12+ | Core implementation |
|
|
102
|
+
| Polars | Dataframe operations and transformations |
|
|
103
|
+
| Pydantic | Schema validation |
|
|
104
|
+
| PyArrow | Columnar data interoperability |
|
|
105
|
+
| pytest | Automated testing |
|
|
106
|
+
| Ruff | Formatting and linting |
|
|
107
|
+
| uv | Dependency and environment management |
|
|
108
|
+
|
|
109
|
+
Dasket deliberately delegates dataframe execution and storage-format handling to established libraries.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Installation
|
|
114
|
+
|
|
115
|
+
### Requirements
|
|
116
|
+
|
|
117
|
+
- Python >= 3.12
|
|
118
|
+
- uv (recommended)
|
|
119
|
+
|
|
120
|
+
### Development installation
|
|
121
|
+
|
|
122
|
+
Clone the repository:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
git clone <repository-url>
|
|
126
|
+
cd dasket
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Install dependencies:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
uv sync
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Activate the virtual environment:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
source .venv/bin/activate
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Verify the installation:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
uv run python -c "from dasket import Dataset; print(Dataset)"
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Quick Start
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
import polars as pl
|
|
153
|
+
|
|
154
|
+
from dasket import Dataset
|
|
155
|
+
|
|
156
|
+
# Create a dataset
|
|
157
|
+
df = pl.DataFrame(
|
|
158
|
+
{
|
|
159
|
+
"name": [" Amar ", "SAM", "Alex", "Amar"],
|
|
160
|
+
"age": [21, 24, None, 21],
|
|
161
|
+
"language": ["Python", "Go", "Rust", "Python"],
|
|
162
|
+
}
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
dataset = Dataset.from_dataframe(df)
|
|
166
|
+
|
|
167
|
+
# Inspect dataset dimensions
|
|
168
|
+
print(dataset.shape)
|
|
169
|
+
print(dataset.columns)
|
|
170
|
+
print(dataset.row_count)
|
|
171
|
+
|
|
172
|
+
# Normalize text
|
|
173
|
+
normalized = dataset.normalize(
|
|
174
|
+
columns=["name", "language"],
|
|
175
|
+
lowercase=["name", "language"],
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
# Inspect missing values
|
|
179
|
+
print(normalized.missing_values())
|
|
180
|
+
|
|
181
|
+
# Remove rows containing null values
|
|
182
|
+
cleaned = normalized.drop_missing()
|
|
183
|
+
|
|
184
|
+
# Generate a dataset profile
|
|
185
|
+
profile = cleaned.profile()
|
|
186
|
+
print(profile)
|
|
187
|
+
|
|
188
|
+
# Filter records
|
|
189
|
+
filtered = cleaned.filter(pl.col("age") > 20)
|
|
190
|
+
|
|
191
|
+
# Split into training and testing datasets
|
|
192
|
+
train, test = filtered.split(
|
|
193
|
+
train_ratio=0.8,
|
|
194
|
+
seed=42,
|
|
195
|
+
)
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Every transformation returns a new `Dataset` instance. The original dataset remains unchanged.
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## API Reference
|
|
203
|
+
|
|
204
|
+
### 1. Dataset Construction
|
|
205
|
+
|
|
206
|
+
Dasket supports construction from an existing Polars DataFrame or a supported file format.
|
|
207
|
+
|
|
208
|
+
#### `Dataset.from_dataframe()`
|
|
209
|
+
|
|
210
|
+
```python
|
|
211
|
+
import polars as pl
|
|
212
|
+
|
|
213
|
+
from dasket import Dataset
|
|
214
|
+
|
|
215
|
+
df = pl.DataFrame(
|
|
216
|
+
{
|
|
217
|
+
"id": [1, 2, 3],
|
|
218
|
+
"name": ["Amar", "Sam", "Alex"],
|
|
219
|
+
}
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
dataset = Dataset.from_dataframe(df)
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
#### `Dataset.from_file()`
|
|
226
|
+
|
|
227
|
+
```python
|
|
228
|
+
dataset = Dataset.from_file("data.csv")
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Supported extensions:
|
|
232
|
+
|
|
233
|
+
| Extension | Polars reader |
|
|
234
|
+
|---|---|
|
|
235
|
+
| `.csv` | `pl.read_csv` |
|
|
236
|
+
| `.json` | `pl.read_json` |
|
|
237
|
+
| `.jsonl` | `pl.read_ndjson` |
|
|
238
|
+
| `.parquet` | `pl.read_parquet` |
|
|
239
|
+
|
|
240
|
+
File extensions are matched case-insensitively.
|
|
241
|
+
|
|
242
|
+
Unsupported formats raise `ValueError`. Missing files and non-file paths raise `FileNotFoundError`.
|
|
243
|
+
|
|
244
|
+
#### Dataset properties
|
|
245
|
+
|
|
246
|
+
```python
|
|
247
|
+
dataset.data
|
|
248
|
+
dataset.shape
|
|
249
|
+
dataset.columns
|
|
250
|
+
dataset.row_count
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
| Property | Description |
|
|
254
|
+
|---|---|
|
|
255
|
+
| `data` | Underlying Polars DataFrame |
|
|
256
|
+
| `shape` | Tuple containing rows and columns |
|
|
257
|
+
| `columns` | List of column names |
|
|
258
|
+
| `row_count` | Number of rows |
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
### 2. Schema Validation
|
|
263
|
+
|
|
264
|
+
Dasket integrates with Pydantic to validate dataset records against Python models.
|
|
265
|
+
|
|
266
|
+
```python
|
|
267
|
+
from pydantic import BaseModel
|
|
268
|
+
|
|
269
|
+
from dasket import Dataset
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
class User(BaseModel):
|
|
273
|
+
name: str
|
|
274
|
+
age: int
|
|
275
|
+
email: str
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
dataset = Dataset.from_file("users.json")
|
|
279
|
+
|
|
280
|
+
report = dataset.validate(User)
|
|
281
|
+
|
|
282
|
+
print(report)
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
Validation is delegated to Dasket's validation module, which uses the supplied Pydantic model as the record schema.
|
|
286
|
+
|
|
287
|
+
This allows application developers to define their own validation requirements rather than relying on a fixed collection of predefined schemas.
|
|
288
|
+
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
### 3. Text Normalization
|
|
292
|
+
|
|
293
|
+
#### `normalize(columns, lowercase=None)`
|
|
294
|
+
|
|
295
|
+
Normalizes selected string columns by stripping surrounding whitespace and optionally converting values to lowercase.
|
|
296
|
+
|
|
297
|
+
```python
|
|
298
|
+
normalized = dataset.normalize(
|
|
299
|
+
columns=["name", "email"],
|
|
300
|
+
lowercase=["email"],
|
|
301
|
+
)
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Behavior:
|
|
305
|
+
|
|
306
|
+
- Removes surrounding whitespace.
|
|
307
|
+
- Supports optional lowercase conversion.
|
|
308
|
+
- Preserves null values.
|
|
309
|
+
- Rejects missing columns.
|
|
310
|
+
- Rejects non-string columns.
|
|
311
|
+
- Requires lowercase columns to be included in the selected columns.
|
|
312
|
+
- Does not modify the original dataset.
|
|
313
|
+
|
|
314
|
+
Example:
|
|
315
|
+
|
|
316
|
+
```text
|
|
317
|
+
Before:
|
|
318
|
+
|
|
319
|
+
" AMAR "
|
|
320
|
+
" Sam "
|
|
321
|
+
" ALEX "
|
|
322
|
+
|
|
323
|
+
After:
|
|
324
|
+
|
|
325
|
+
"amar"
|
|
326
|
+
"Sam"
|
|
327
|
+
"ALEX"
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
---
|
|
331
|
+
|
|
332
|
+
### 4. Duplicate Detection
|
|
333
|
+
|
|
334
|
+
#### `find_duplicates(columns=None)`
|
|
335
|
+
|
|
336
|
+
Identifies duplicate records based on selected columns.
|
|
337
|
+
|
|
338
|
+
```python
|
|
339
|
+
duplicates = dataset.find_duplicates()
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
Use specific columns:
|
|
343
|
+
|
|
344
|
+
```python
|
|
345
|
+
duplicates = dataset.find_duplicates(columns=["email"])
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
If no columns are specified, all columns are used.
|
|
349
|
+
|
|
350
|
+
**Return semantics:** The method returns one representative row for each duplicate group, retaining the first occurrence and preserving group order.
|
|
351
|
+
|
|
352
|
+
Example:
|
|
353
|
+
|
|
354
|
+
```text
|
|
355
|
+
Original:
|
|
356
|
+
|
|
357
|
+
id name
|
|
358
|
+
1 Amar
|
|
359
|
+
1 Amar
|
|
360
|
+
2 Sam
|
|
361
|
+
2 Sam
|
|
362
|
+
3 Alex
|
|
363
|
+
|
|
364
|
+
Result:
|
|
365
|
+
|
|
366
|
+
id name
|
|
367
|
+
1 Amar
|
|
368
|
+
2 Sam
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
This method does not return every occurrence belonging to a duplicate group.
|
|
372
|
+
|
|
373
|
+
---
|
|
374
|
+
|
|
375
|
+
### 5. Missing-Value Analysis
|
|
376
|
+
|
|
377
|
+
#### `missing_values()`
|
|
378
|
+
|
|
379
|
+
Returns the number of null values in each column.
|
|
380
|
+
|
|
381
|
+
```python
|
|
382
|
+
missing = dataset.missing_values()
|
|
383
|
+
|
|
384
|
+
print(missing)
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
Example:
|
|
388
|
+
|
|
389
|
+
```python
|
|
390
|
+
{
|
|
391
|
+
"name": 0,
|
|
392
|
+
"age": 3,
|
|
393
|
+
"email": 2,
|
|
394
|
+
}
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
The operation counts actual null values, not empty strings or whitespace-only strings.
|
|
398
|
+
|
|
399
|
+
#### `drop_missing(columns=None)`
|
|
400
|
+
|
|
401
|
+
Removes rows containing null values.
|
|
402
|
+
|
|
403
|
+
Remove rows with nulls in any column:
|
|
404
|
+
|
|
405
|
+
```python
|
|
406
|
+
cleaned = dataset.drop_missing()
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
Consider only selected columns:
|
|
410
|
+
|
|
411
|
+
```python
|
|
412
|
+
cleaned = dataset.drop_missing(columns=["email"])
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
An empty column selection is treated as a no-op.
|
|
416
|
+
|
|
417
|
+
---
|
|
418
|
+
|
|
419
|
+
### 6. Dataset Profiling
|
|
420
|
+
|
|
421
|
+
#### `profile()`
|
|
422
|
+
|
|
423
|
+
Generates a structural summary of the dataset.
|
|
424
|
+
|
|
425
|
+
```python
|
|
426
|
+
report = dataset.profile()
|
|
427
|
+
|
|
428
|
+
print(report)
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
Example structure:
|
|
432
|
+
|
|
433
|
+
```python
|
|
434
|
+
{
|
|
435
|
+
"row_count": 1000,
|
|
436
|
+
"column_count": 3,
|
|
437
|
+
"columns": {
|
|
438
|
+
"name": {
|
|
439
|
+
"dtype": "String",
|
|
440
|
+
"null_count": 5,
|
|
441
|
+
},
|
|
442
|
+
"age": {
|
|
443
|
+
"dtype": "Int64",
|
|
444
|
+
"null_count": 12,
|
|
445
|
+
},
|
|
446
|
+
"email": {
|
|
447
|
+
"dtype": "String",
|
|
448
|
+
"null_count": 3,
|
|
449
|
+
},
|
|
450
|
+
},
|
|
451
|
+
"duplicate_count": 8,
|
|
452
|
+
}
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
The profile includes:
|
|
456
|
+
|
|
457
|
+
- Total row count.
|
|
458
|
+
- Total column count.
|
|
459
|
+
- Column data types.
|
|
460
|
+
- Null count per column.
|
|
461
|
+
- Duplicate row count.
|
|
462
|
+
|
|
463
|
+
**Important:** `duplicate_count` counts all rows belonging to duplicate groups, not the number of duplicate groups or only the additional occurrences.
|
|
464
|
+
|
|
465
|
+
---
|
|
466
|
+
|
|
467
|
+
### 7. Column Operations
|
|
468
|
+
|
|
469
|
+
#### `select_columns(columns)`
|
|
470
|
+
|
|
471
|
+
Selects specific columns.
|
|
472
|
+
|
|
473
|
+
```python
|
|
474
|
+
subset = dataset.select_columns(["name", "email"])
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
Column order follows the supplied list.
|
|
478
|
+
|
|
479
|
+
At least one column must be selected.
|
|
480
|
+
|
|
481
|
+
#### `drop_columns(columns)`
|
|
482
|
+
|
|
483
|
+
Removes selected columns.
|
|
484
|
+
|
|
485
|
+
```python
|
|
486
|
+
reduced = dataset.drop_columns(["temporary_field", "internal_id"])
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
Dasket rejects attempts to drop all columns.
|
|
490
|
+
|
|
491
|
+
#### `rename_columns(mapping)`
|
|
492
|
+
|
|
493
|
+
Renames columns using a mapping.
|
|
494
|
+
|
|
495
|
+
```python
|
|
496
|
+
renamed = dataset.rename_columns(
|
|
497
|
+
{
|
|
498
|
+
"user_name": "name",
|
|
499
|
+
"user_email": "email",
|
|
500
|
+
}
|
|
501
|
+
)
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
The operation validates that:
|
|
505
|
+
|
|
506
|
+
- Original column names exist.
|
|
507
|
+
- New names are non-empty strings.
|
|
508
|
+
- Renaming does not create duplicate column names.
|
|
509
|
+
|
|
510
|
+
---
|
|
511
|
+
|
|
512
|
+
### 8. Expression-Based Filtering
|
|
513
|
+
|
|
514
|
+
#### `filter(expression)`
|
|
515
|
+
|
|
516
|
+
Dasket exposes Polars expression-based filtering through the Dataset abstraction.
|
|
517
|
+
|
|
518
|
+
```python
|
|
519
|
+
import polars as pl
|
|
520
|
+
|
|
521
|
+
adults = dataset.filter(pl.col("age") >= 18)
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
Multiple conditions:
|
|
525
|
+
|
|
526
|
+
```python
|
|
527
|
+
filtered = dataset.filter((pl.col("age") >= 18) & (pl.col("country") == "India"))
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
String filtering:
|
|
531
|
+
|
|
532
|
+
```python
|
|
533
|
+
filtered = dataset.filter(pl.col("email").str.ends_with("@gmail.com"))
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
Dasket accepts Polars expressions rather than implementing a separate filtering language.
|
|
537
|
+
|
|
538
|
+
Invalid expression types raise `TypeError`. Column resolution and expression execution are handled by Polars.
|
|
539
|
+
|
|
540
|
+
---
|
|
541
|
+
|
|
542
|
+
### 9. Dataset Splitting
|
|
543
|
+
|
|
544
|
+
#### `split(train_ratio=0.8, seed=None)`
|
|
545
|
+
|
|
546
|
+
Splits a dataset into training and testing partitions.
|
|
547
|
+
|
|
548
|
+
```python
|
|
549
|
+
train, test = dataset.split(
|
|
550
|
+
train_ratio=0.8,
|
|
551
|
+
seed=42,
|
|
552
|
+
)
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
The split:
|
|
556
|
+
|
|
557
|
+
- Randomly shuffles row indices.
|
|
558
|
+
- Uses the supplied ratio to determine the training partition size.
|
|
559
|
+
- Preserves all rows across both partitions.
|
|
560
|
+
- Supports reproducibility through a random seed.
|
|
561
|
+
- Does not modify the original dataset.
|
|
562
|
+
|
|
563
|
+
For datasets containing at least two rows, both partitions contain at least one row.
|
|
564
|
+
|
|
565
|
+
An empty dataset produces two empty partitions. A one-row dataset raises `ValueError`.
|
|
566
|
+
|
|
567
|
+
---
|
|
568
|
+
|
|
569
|
+
### 10. Stratified Dataset Splitting
|
|
570
|
+
|
|
571
|
+
#### `stratified_split(column, train_ratio=0.8, seed=None)`
|
|
572
|
+
|
|
573
|
+
Performs a train/test split while preserving class representation across partitions.
|
|
574
|
+
|
|
575
|
+
```python
|
|
576
|
+
train, test = dataset.stratified_split(
|
|
577
|
+
column="label",
|
|
578
|
+
train_ratio=0.8,
|
|
579
|
+
seed=42,
|
|
580
|
+
)
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
For example:
|
|
584
|
+
|
|
585
|
+
```text
|
|
586
|
+
Original distribution:
|
|
587
|
+
|
|
588
|
+
Class A: 100
|
|
589
|
+
Class B: 100
|
|
590
|
+
Class C: 100
|
|
591
|
+
|
|
592
|
+
Training distribution:
|
|
593
|
+
|
|
594
|
+
Class A: 80
|
|
595
|
+
Class B: 80
|
|
596
|
+
Class C: 80
|
|
597
|
+
|
|
598
|
+
Testing distribution:
|
|
599
|
+
|
|
600
|
+
Class A: 20
|
|
601
|
+
Class B: 20
|
|
602
|
+
Class C: 20
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
The operation:
|
|
606
|
+
|
|
607
|
+
1. Groups row indices by class label.
|
|
608
|
+
2. Shuffles indices within each class.
|
|
609
|
+
3. Calculates the training partition size independently for each class.
|
|
610
|
+
4. Ensures each class appears in both partitions.
|
|
611
|
+
5. Shuffles the resulting training and testing indices.
|
|
612
|
+
|
|
613
|
+
Constraints:
|
|
614
|
+
|
|
615
|
+
- The specified column must exist.
|
|
616
|
+
- Null class labels are rejected.
|
|
617
|
+
- NaN class labels are rejected.
|
|
618
|
+
- Class labels must be hashable.
|
|
619
|
+
- Every class must contain at least two samples.
|
|
620
|
+
- The training ratio must be between zero and one.
|
|
621
|
+
|
|
622
|
+
The random seed makes the operation reproducible.
|
|
623
|
+
|
|
624
|
+
---
|
|
625
|
+
|
|
626
|
+
### 11. Dataset Export
|
|
627
|
+
|
|
628
|
+
#### `to_file(path)`
|
|
629
|
+
|
|
630
|
+
Exports the underlying DataFrame to a supported file format.
|
|
631
|
+
|
|
632
|
+
```python
|
|
633
|
+
dataset.to_file("output.csv")
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
Supported formats:
|
|
637
|
+
|
|
638
|
+
```python
|
|
639
|
+
dataset.to_file("output.csv")
|
|
640
|
+
dataset.to_file("output.json")
|
|
641
|
+
dataset.to_file("output.jsonl")
|
|
642
|
+
dataset.to_file("output.parquet")
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
The destination directory is created automatically when necessary.
|
|
646
|
+
|
|
647
|
+
Unsupported extensions raise `ValueError`.
|
|
648
|
+
|
|
649
|
+
---
|
|
650
|
+
|
|
651
|
+
## Testing
|
|
652
|
+
|
|
653
|
+
Dasket uses pytest for automated testing and Ruff for formatting and linting.
|
|
654
|
+
|
|
655
|
+
Run the complete test suite:
|
|
656
|
+
|
|
657
|
+
```bash
|
|
658
|
+
uv run pytest -v
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
Run linting:
|
|
662
|
+
|
|
663
|
+
```bash
|
|
664
|
+
uv run ruff check .
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
Format the codebase:
|
|
668
|
+
|
|
669
|
+
```bash
|
|
670
|
+
uv run ruff format .
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
Run the complete project checks:
|
|
674
|
+
|
|
675
|
+
```bash
|
|
676
|
+
make check
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
The `make check` target executes formatting, linting, and automated tests.
|
|
680
|
+
|
|
681
|
+
---
|
|
682
|
+
|
|
683
|
+
## Project Structure
|
|
684
|
+
|
|
685
|
+
```text
|
|
686
|
+
dasket/
|
|
687
|
+
│
|
|
688
|
+
├── src/
|
|
689
|
+
│ └── dasket/
|
|
690
|
+
│ ├── __init__.py
|
|
691
|
+
│ ├── dataset.py
|
|
692
|
+
│ ├── reports.py
|
|
693
|
+
│ └── validation.py
|
|
694
|
+
│
|
|
695
|
+
├── tests/
|
|
696
|
+
│ ├── test_dataset.py
|
|
697
|
+
│ ├── test_validation.py
|
|
698
|
+
│ ├── test_normalization.py
|
|
699
|
+
│ ├── test_duplicates.py
|
|
700
|
+
│ ├── test_missing_values.py
|
|
701
|
+
│ ├── test_drop_missing.py
|
|
702
|
+
│ ├── test_columns.py
|
|
703
|
+
│ ├── test_filter.py
|
|
704
|
+
│ ├── test_split.py
|
|
705
|
+
│ └── test_stratified_split.py
|
|
706
|
+
│
|
|
707
|
+
├── docs/
|
|
708
|
+
│ └── assets/
|
|
709
|
+
│ └── dasket.png
|
|
710
|
+
│
|
|
711
|
+
├── Makefile
|
|
712
|
+
├── pyproject.toml
|
|
713
|
+
├── uv.lock
|
|
714
|
+
└── README.md
|
|
715
|
+
```
|
|
716
|
+
|
|
717
|
+
---
|
|
718
|
+
|
|
719
|
+
## Design Scope
|
|
720
|
+
|
|
721
|
+
Dasket is intended to provide practical dataset preparation and assessment primitives.
|
|
722
|
+
|
|
723
|
+
It is not intended to replace:
|
|
724
|
+
|
|
725
|
+
- Polars or Pandas as dataframe engines.
|
|
726
|
+
- Pydantic as a validation framework.
|
|
727
|
+
- PyArrow as a columnar data interchange layer.
|
|
728
|
+
- Dedicated machine learning frameworks.
|
|
729
|
+
- Distributed data-processing engines.
|
|
730
|
+
|
|
731
|
+
Instead, Dasket provides a consistent interface that combines common dataset preparation operations into one composable API.
|
|
732
|
+
|
|
733
|
+
---
|
|
734
|
+
|
|
735
|
+
## Roadmap
|
|
736
|
+
|
|
737
|
+
Potential future directions:
|
|
738
|
+
|
|
739
|
+
- [ ] Richer dataset profiling and statistical summaries.
|
|
740
|
+
- [ ] Configurable missing-value strategies.
|
|
741
|
+
- [ ] Dataset comparison and drift analysis.
|
|
742
|
+
- [ ] Schema inference.
|
|
743
|
+
- [ ] More advanced dataset sampling strategies.
|
|
744
|
+
- [ ] Improved validation diagnostics.
|
|
745
|
+
- [ ] Dataset lineage and transformation tracking.
|
|
746
|
+
|
|
747
|
+
These are potential directions, not currently implemented features.
|
|
748
|
+
|
|
749
|
+
---
|
|
750
|
+
|
|
751
|
+
## Contributing
|
|
752
|
+
|
|
753
|
+
Contributions, bug reports, and suggestions are welcome.
|
|
754
|
+
|
|
755
|
+
For development:
|
|
756
|
+
|
|
757
|
+
```bash
|
|
758
|
+
git clone <repository-url>
|
|
759
|
+
cd dasket
|
|
760
|
+
|
|
761
|
+
uv sync
|
|
762
|
+
|
|
763
|
+
make check
|
|
764
|
+
```
|
|
765
|
+
|
|
766
|
+
Please ensure that formatting, linting, and tests pass before submitting changes.
|
|
767
|
+
|
|
768
|
+
---
|
|
769
|
+
|
|
770
|
+
## License
|
|
771
|
+
|
|
772
|
+
See the repository's license file for licensing information.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
dasket/__init__.py,sha256=jJdPU_nOGkgx_2h-UN9xVFyFmVx4GpXoJSag6iDc250,122
|
|
2
|
+
dasket/dataset.py,sha256=vPsCPqqRrjnilsGOOA965sTki5-gZs_FOQ5-Q6aiwUw,10938
|
|
3
|
+
dasket/reports.py,sha256=sLCY-l6CcHRif8Zx0mBk1QdSLV1pqWeiEuDketNwHD4,327
|
|
4
|
+
dasket/validation.py,sha256=LVl0-S9nxR5GV60S2-hiGQdmH0x55lucKuAvwaj_GLQ,1230
|
|
5
|
+
dasket-0.1.0.dist-info/WHEEL,sha256=MWx7psoF7KNST9G72pGtiBYuaXRXQXBk9pw5R6FzYko,80
|
|
6
|
+
dasket-0.1.0.dist-info/METADATA,sha256=rv7KFmrqJZESgoc9qeCSoQzBpSqw4cl5AVYwPBG5jwU,16017
|
|
7
|
+
dasket-0.1.0.dist-info/RECORD,,
|