silver-data 0.1.0__tar.gz
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.
- silver_data-0.1.0/CHANGELOG.md +34 -0
- silver_data-0.1.0/CONTRIBUTING.md +114 -0
- silver_data-0.1.0/LICENSE +12 -0
- silver_data-0.1.0/MANIFEST.in +6 -0
- silver_data-0.1.0/PKG-INFO +225 -0
- silver_data-0.1.0/README.md +191 -0
- silver_data-0.1.0/pyproject.toml +49 -0
- silver_data-0.1.0/setup.cfg +4 -0
- silver_data-0.1.0/src/silver_data/__init__.py +19 -0
- silver_data-0.1.0/src/silver_data/dataset.py +160 -0
- silver_data-0.1.0/src/silver_data/models.py +35 -0
- silver_data-0.1.0/src/silver_data.egg-info/PKG-INFO +225 -0
- silver_data-0.1.0/src/silver_data.egg-info/SOURCES.txt +16 -0
- silver_data-0.1.0/src/silver_data.egg-info/dependency_links.txt +1 -0
- silver_data-0.1.0/src/silver_data.egg-info/requires.txt +12 -0
- silver_data-0.1.0/src/silver_data.egg-info/top_level.txt +1 -0
- silver_data-0.1.0/tests/__init__.py +1 -0
- silver_data-0.1.0/tests/test_data.py +194 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.1.0] - 2024-08-04
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Initial release of silver-data
|
|
12
|
+
- Dataset loading from CSV, JSON, JSONL, and pandas DataFrames
|
|
13
|
+
- Dataset inspection with column statistics and metadata
|
|
14
|
+
- Dataset validation with error and warning reporting
|
|
15
|
+
- Deterministic dataset fingerprinting for caching and reproducibility
|
|
16
|
+
- Train/validation/test splitting with customizable ratios
|
|
17
|
+
- Immutable dataset design for safe data handling
|
|
18
|
+
- Full type hints for better IDE support
|
|
19
|
+
- Comprehensive test suite with >90% coverage
|
|
20
|
+
- Support for Python 3.8-3.12
|
|
21
|
+
|
|
22
|
+
### Features
|
|
23
|
+
- `Dataset.from_records()` - Create datasets from Python dictionaries
|
|
24
|
+
- `Dataset.from_csv()` - Load datasets from CSV files
|
|
25
|
+
- `Dataset.from_json()` - Parse JSON data into datasets
|
|
26
|
+
- `Dataset.from_jsonl()` - Handle JSONL format for streaming data
|
|
27
|
+
- `Dataset.from_pandas()` - Convert pandas DataFrames to Silver datasets
|
|
28
|
+
- `Dataset.inspect()` - Get detailed dataset statistics
|
|
29
|
+
- `Dataset.validate()` - Check for data quality issues
|
|
30
|
+
- `Dataset.fingerprint()` - Generate deterministic dataset identifiers
|
|
31
|
+
- `Dataset.split()` - Split datasets for ML workflows
|
|
32
|
+
- `Dataset.to_pandas()` - Convert back to pandas for compatibility
|
|
33
|
+
|
|
34
|
+
## [Unreleased]
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# Contributing to silver-data
|
|
2
|
+
|
|
3
|
+
Thank you for your interest in contributing to silver-data! This document provides guidelines and instructions for contributing to the project.
|
|
4
|
+
|
|
5
|
+
## Development Setup
|
|
6
|
+
|
|
7
|
+
### Prerequisites
|
|
8
|
+
- Python 3.8 or higher
|
|
9
|
+
- Git
|
|
10
|
+
- Virtual environment (recommended)
|
|
11
|
+
|
|
12
|
+
### Setting Up Development Environment
|
|
13
|
+
|
|
14
|
+
1. **Clone the repository**
|
|
15
|
+
```bash
|
|
16
|
+
git clone https://github.com/adfgdartec/silver-data.git
|
|
17
|
+
cd silver-data
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
2. **Create a virtual environment**
|
|
21
|
+
```bash
|
|
22
|
+
python -m venv venv
|
|
23
|
+
source venv/bin/activate # On Windows: venv\Scripts\activate
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
3. **Install development dependencies**
|
|
27
|
+
```bash
|
|
28
|
+
pip install -e ".[dev]"
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
4. **Run tests**
|
|
32
|
+
```bash
|
|
33
|
+
pytest
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
5. **Run tests with coverage**
|
|
37
|
+
```bash
|
|
38
|
+
pytest --cov=silver_data --cov-report=html
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Code Style
|
|
42
|
+
|
|
43
|
+
We use the following tools to maintain code quality:
|
|
44
|
+
|
|
45
|
+
- **flake8** for linting
|
|
46
|
+
- **mypy** for type checking
|
|
47
|
+
- **pytest** for testing
|
|
48
|
+
|
|
49
|
+
Run all quality checks:
|
|
50
|
+
```bash
|
|
51
|
+
flake8 src/ tests/
|
|
52
|
+
mypy src/
|
|
53
|
+
pytest
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Making Changes
|
|
57
|
+
|
|
58
|
+
1. **Create a branch**
|
|
59
|
+
```bash
|
|
60
|
+
git checkout -b feature/your-feature-name
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
2. **Make your changes**
|
|
64
|
+
- Write clear, descriptive commit messages
|
|
65
|
+
- Add tests for new functionality
|
|
66
|
+
- Update documentation as needed
|
|
67
|
+
|
|
68
|
+
3. **Run tests**
|
|
69
|
+
```bash
|
|
70
|
+
pytest
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
4. **Submit a pull request**
|
|
74
|
+
- Describe your changes clearly
|
|
75
|
+
- Reference any related issues
|
|
76
|
+
- Ensure all tests pass
|
|
77
|
+
|
|
78
|
+
## Testing
|
|
79
|
+
|
|
80
|
+
We aim for high test coverage. When adding new features:
|
|
81
|
+
|
|
82
|
+
- Write unit tests for new functions
|
|
83
|
+
- Test edge cases and error conditions
|
|
84
|
+
- Ensure existing tests still pass
|
|
85
|
+
|
|
86
|
+
### Test Structure
|
|
87
|
+
```
|
|
88
|
+
tests/
|
|
89
|
+
├── __init__.py
|
|
90
|
+
└── test_data.py
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Documentation
|
|
94
|
+
|
|
95
|
+
- Update docstrings for any modified functions
|
|
96
|
+
- Add examples for new features
|
|
97
|
+
- Update README.md if user-facing changes are made
|
|
98
|
+
|
|
99
|
+
## Release Process
|
|
100
|
+
|
|
101
|
+
Releases are managed by maintainers:
|
|
102
|
+
|
|
103
|
+
1. Update version in `pyproject.toml`
|
|
104
|
+
2. Update `CHANGELOG.md`
|
|
105
|
+
3. Create a GitHub release
|
|
106
|
+
4. Package will be automatically published to PyPI
|
|
107
|
+
|
|
108
|
+
## Questions?
|
|
109
|
+
|
|
110
|
+
Feel free to open an issue for questions or discussions about contributions.
|
|
111
|
+
|
|
112
|
+
## License
|
|
113
|
+
|
|
114
|
+
By contributing, you agree that your contributions will be licensed under the Apache-2.0 License.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
|
|
4
|
+
Copyright 2026 Silver Contributors
|
|
5
|
+
|
|
6
|
+
Licensed under the Apache License, Version 2.0. You may obtain a copy of the
|
|
7
|
+
License at https://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
|
|
9
|
+
Unless required by applicable law or agreed to in writing, software distributed
|
|
10
|
+
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
|
11
|
+
CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
|
12
|
+
specific language governing permissions and limitations under the License.
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: silver-data
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Inspectable, deterministic dataset contracts and loaders for Silver.
|
|
5
|
+
License-Expression: Apache-2.0
|
|
6
|
+
Project-URL: Homepage, https://github.com/adfgdartec/silver-data
|
|
7
|
+
Project-URL: Repository, https://github.com/adfgdartec/silver-data
|
|
8
|
+
Project-URL: Issues, https://github.com/adfgdartec/silver-data/issues
|
|
9
|
+
Keywords: machine-learning,datasets,csv,data-validation,python
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
20
|
+
Requires-Python: >=3.8
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Provides-Extra: pandas
|
|
24
|
+
Requires-Dist: pandas>=1.0.0; extra == "pandas"
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
27
|
+
Requires-Dist: pandas>=1.0; extra == "dev"
|
|
28
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
29
|
+
Requires-Dist: flake8>=6.0.0; extra == "dev"
|
|
30
|
+
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
31
|
+
Requires-Dist: build>=0.10.0; extra == "dev"
|
|
32
|
+
Requires-Dist: twine>=4.0.0; extra == "dev"
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
|
|
35
|
+
# silver-data
|
|
36
|
+
|
|
37
|
+
[](https://www.python.org/downloads/)
|
|
38
|
+
[](LICENSE)
|
|
39
|
+
[](tests/)
|
|
40
|
+
[](https://flake8.pycqa.org/)
|
|
41
|
+
|
|
42
|
+
Inspectable, deterministic dataset contracts and loaders for Silver. A Python package designed for ML researchers who need reliable dataset handling with built-in validation and reproducibility features.
|
|
43
|
+
|
|
44
|
+
The base install uses only the Python standard library for records, JSON, JSONL,
|
|
45
|
+
and CSV. Add pandas only when you need DataFrame conversion:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install 'silver-data[pandas]'
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Installation
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install silver-data
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Quick Start
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from silver_data import Dataset
|
|
61
|
+
|
|
62
|
+
# Load from CSV file
|
|
63
|
+
dataset = Dataset.from_csv("my_data", "path/to/data.csv")
|
|
64
|
+
|
|
65
|
+
# Optional: load from pandas
|
|
66
|
+
import pandas as pd
|
|
67
|
+
df = pd.read_csv("path/to/data.csv")
|
|
68
|
+
dataset = Dataset.from_pandas("my_data", df)
|
|
69
|
+
|
|
70
|
+
# Inspect dataset
|
|
71
|
+
report = dataset.inspect()
|
|
72
|
+
print(f"Rows: {report.rows}, Columns: {len(report.columns)}")
|
|
73
|
+
for col in report.columns:
|
|
74
|
+
print(f" {col.name}: {col.value_type} ({col.unique} unique, {col.missing} missing)")
|
|
75
|
+
|
|
76
|
+
# Validate dataset
|
|
77
|
+
validation = dataset.validate()
|
|
78
|
+
if not validation.valid:
|
|
79
|
+
print("Errors:", validation.errors)
|
|
80
|
+
if validation.warnings:
|
|
81
|
+
print("Warnings:", validation.warnings)
|
|
82
|
+
|
|
83
|
+
# Split dataset for ML workflows
|
|
84
|
+
train, val, test = dataset.split(train=0.8, validation=0.1, test=0.1)
|
|
85
|
+
print(f"Train: {len(train.records())}, Val: {len(val.records())}, Test: {len(test.records())}")
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Features
|
|
89
|
+
|
|
90
|
+
- **Multiple Data Sources**: Load from CSV, JSON, JSONL, and pandas DataFrames
|
|
91
|
+
- **Dataset Inspection**: Get detailed column statistics and metadata
|
|
92
|
+
- **Data Validation**: Automatic detection of missing values, inconsistent columns, and data quality issues
|
|
93
|
+
- **Deterministic Fingerprinting**: Generate unique identifiers for datasets to ensure reproducibility
|
|
94
|
+
- **Smart Splitting**: Train/validation/test splitting with customizable ratios
|
|
95
|
+
- **Immutable Design**: Safe data handling with copy-on-write semantics
|
|
96
|
+
- **Type Safety**: Full type hints for better IDE support and fewer bugs
|
|
97
|
+
|
|
98
|
+
## Use Cases
|
|
99
|
+
|
|
100
|
+
### ML Pipeline Integration
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from silver_data import Dataset
|
|
104
|
+
import pandas as pd
|
|
105
|
+
|
|
106
|
+
# Load and validate training data
|
|
107
|
+
df = pd.read_csv("train.csv")
|
|
108
|
+
dataset = Dataset.from_pandas("training", df)
|
|
109
|
+
|
|
110
|
+
# Ensure data quality before training
|
|
111
|
+
validation = dataset.validate()
|
|
112
|
+
if not validation.valid:
|
|
113
|
+
raise ValueError(f"Dataset validation failed: {validation.errors}")
|
|
114
|
+
|
|
115
|
+
# Split for cross-validation
|
|
116
|
+
train_split, val_split, test_split = dataset.split(train=0.7, validation=0.15, test=0.15)
|
|
117
|
+
|
|
118
|
+
# Use fingerprints for caching
|
|
119
|
+
cache_key = dataset.fingerprint()
|
|
120
|
+
print(f"Dataset fingerprint: {cache_key}")
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Data Quality Monitoring
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
from silver_data import Dataset
|
|
127
|
+
|
|
128
|
+
# Monitor data drift over time
|
|
129
|
+
dataset_v1 = Dataset.from_csv("data_v1", "data_2024_01.csv")
|
|
130
|
+
dataset_v2 = Dataset.from_csv("data_v2", "data_2024_02.csv")
|
|
131
|
+
|
|
132
|
+
if dataset_v1.fingerprint() != dataset_v2.fingerprint():
|
|
133
|
+
print("Dataset has changed - retrain models")
|
|
134
|
+
|
|
135
|
+
# Check for new data quality issues
|
|
136
|
+
report_v2 = dataset_v2.inspect()
|
|
137
|
+
for col in report_v2.columns:
|
|
138
|
+
if col.missing > len(dataset_v2.records()) * 0.1: # More than 10% missing
|
|
139
|
+
print(f"Warning: {col.name} has high missing rate: {col.missing}")
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Experiment Reproducibility
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
from silver_data import Dataset
|
|
146
|
+
|
|
147
|
+
# Ensure exact same data across experiments
|
|
148
|
+
dataset = Dataset.from_csv("experiment", "data.csv")
|
|
149
|
+
experiment_id = f"exp_{dataset.fingerprint()}"
|
|
150
|
+
|
|
151
|
+
# Log for reproducibility
|
|
152
|
+
print(f"Running experiment {experiment_id} with dataset fingerprint {dataset.fingerprint()}")
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Advanced Usage
|
|
156
|
+
|
|
157
|
+
### Custom Data Loading
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
from silver_data import Dataset
|
|
161
|
+
import json
|
|
162
|
+
|
|
163
|
+
# Load from custom JSON format
|
|
164
|
+
with open("custom_data.json") as f:
|
|
165
|
+
data = json.load(f)
|
|
166
|
+
dataset = Dataset.from_json("custom", data)
|
|
167
|
+
|
|
168
|
+
# Load from streaming JSONL
|
|
169
|
+
with open("streaming_data.jsonl") as f:
|
|
170
|
+
dataset = Dataset.from_jsonl("streaming", f.read())
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Data Type Analysis
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
from silver_data import Dataset
|
|
177
|
+
|
|
178
|
+
dataset = Dataset.from_csv("analysis", "mixed_data.csv")
|
|
179
|
+
report = dataset.inspect()
|
|
180
|
+
|
|
181
|
+
# Analyze column types
|
|
182
|
+
string_cols = [c.name for c in report.columns if c.value_type == "string"]
|
|
183
|
+
numeric_cols = [c.name for c in report.columns if c.value_type == "number"]
|
|
184
|
+
mixed_cols = [c.name for c in report.columns if c.value_type == "mixed"]
|
|
185
|
+
|
|
186
|
+
print(f"String columns: {string_cols}")
|
|
187
|
+
print(f"Numeric columns: {numeric_cols}")
|
|
188
|
+
print(f"Mixed type columns: {mixed_cols}")
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Requirements
|
|
192
|
+
|
|
193
|
+
- Python 3.8+
|
|
194
|
+
- pandas 1.0+
|
|
195
|
+
|
|
196
|
+
## Development
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
# Install development dependencies
|
|
200
|
+
pip install -e ".[dev]"
|
|
201
|
+
|
|
202
|
+
# Run tests
|
|
203
|
+
pytest
|
|
204
|
+
|
|
205
|
+
# Run tests with coverage
|
|
206
|
+
pytest --cov=silver_data --cov-report=html
|
|
207
|
+
|
|
208
|
+
# Run linting
|
|
209
|
+
flake8 src/ tests/
|
|
210
|
+
mypy src/
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Contributing
|
|
214
|
+
|
|
215
|
+
Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
|
216
|
+
|
|
217
|
+
## License
|
|
218
|
+
|
|
219
|
+
Apache-2.0 - see [LICENSE](LICENSE) file for details.
|
|
220
|
+
|
|
221
|
+
## Related Packages
|
|
222
|
+
|
|
223
|
+
- [silver-run](https://github.com/adfgdartec/silver-run) - Training lifecycle management
|
|
224
|
+
- [silver-diagnostics](https://github.com/adfgdartec/silver-diagnostics) - ML diagnostics
|
|
225
|
+
- [silver-adapters](https://github.com/adfgdartec/silver-adapters) - Framework adapters
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# silver-data
|
|
2
|
+
|
|
3
|
+
[](https://www.python.org/downloads/)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
[](tests/)
|
|
6
|
+
[](https://flake8.pycqa.org/)
|
|
7
|
+
|
|
8
|
+
Inspectable, deterministic dataset contracts and loaders for Silver. A Python package designed for ML researchers who need reliable dataset handling with built-in validation and reproducibility features.
|
|
9
|
+
|
|
10
|
+
The base install uses only the Python standard library for records, JSON, JSONL,
|
|
11
|
+
and CSV. Add pandas only when you need DataFrame conversion:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install 'silver-data[pandas]'
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install silver-data
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from silver_data import Dataset
|
|
27
|
+
|
|
28
|
+
# Load from CSV file
|
|
29
|
+
dataset = Dataset.from_csv("my_data", "path/to/data.csv")
|
|
30
|
+
|
|
31
|
+
# Optional: load from pandas
|
|
32
|
+
import pandas as pd
|
|
33
|
+
df = pd.read_csv("path/to/data.csv")
|
|
34
|
+
dataset = Dataset.from_pandas("my_data", df)
|
|
35
|
+
|
|
36
|
+
# Inspect dataset
|
|
37
|
+
report = dataset.inspect()
|
|
38
|
+
print(f"Rows: {report.rows}, Columns: {len(report.columns)}")
|
|
39
|
+
for col in report.columns:
|
|
40
|
+
print(f" {col.name}: {col.value_type} ({col.unique} unique, {col.missing} missing)")
|
|
41
|
+
|
|
42
|
+
# Validate dataset
|
|
43
|
+
validation = dataset.validate()
|
|
44
|
+
if not validation.valid:
|
|
45
|
+
print("Errors:", validation.errors)
|
|
46
|
+
if validation.warnings:
|
|
47
|
+
print("Warnings:", validation.warnings)
|
|
48
|
+
|
|
49
|
+
# Split dataset for ML workflows
|
|
50
|
+
train, val, test = dataset.split(train=0.8, validation=0.1, test=0.1)
|
|
51
|
+
print(f"Train: {len(train.records())}, Val: {len(val.records())}, Test: {len(test.records())}")
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Features
|
|
55
|
+
|
|
56
|
+
- **Multiple Data Sources**: Load from CSV, JSON, JSONL, and pandas DataFrames
|
|
57
|
+
- **Dataset Inspection**: Get detailed column statistics and metadata
|
|
58
|
+
- **Data Validation**: Automatic detection of missing values, inconsistent columns, and data quality issues
|
|
59
|
+
- **Deterministic Fingerprinting**: Generate unique identifiers for datasets to ensure reproducibility
|
|
60
|
+
- **Smart Splitting**: Train/validation/test splitting with customizable ratios
|
|
61
|
+
- **Immutable Design**: Safe data handling with copy-on-write semantics
|
|
62
|
+
- **Type Safety**: Full type hints for better IDE support and fewer bugs
|
|
63
|
+
|
|
64
|
+
## Use Cases
|
|
65
|
+
|
|
66
|
+
### ML Pipeline Integration
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from silver_data import Dataset
|
|
70
|
+
import pandas as pd
|
|
71
|
+
|
|
72
|
+
# Load and validate training data
|
|
73
|
+
df = pd.read_csv("train.csv")
|
|
74
|
+
dataset = Dataset.from_pandas("training", df)
|
|
75
|
+
|
|
76
|
+
# Ensure data quality before training
|
|
77
|
+
validation = dataset.validate()
|
|
78
|
+
if not validation.valid:
|
|
79
|
+
raise ValueError(f"Dataset validation failed: {validation.errors}")
|
|
80
|
+
|
|
81
|
+
# Split for cross-validation
|
|
82
|
+
train_split, val_split, test_split = dataset.split(train=0.7, validation=0.15, test=0.15)
|
|
83
|
+
|
|
84
|
+
# Use fingerprints for caching
|
|
85
|
+
cache_key = dataset.fingerprint()
|
|
86
|
+
print(f"Dataset fingerprint: {cache_key}")
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Data Quality Monitoring
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from silver_data import Dataset
|
|
93
|
+
|
|
94
|
+
# Monitor data drift over time
|
|
95
|
+
dataset_v1 = Dataset.from_csv("data_v1", "data_2024_01.csv")
|
|
96
|
+
dataset_v2 = Dataset.from_csv("data_v2", "data_2024_02.csv")
|
|
97
|
+
|
|
98
|
+
if dataset_v1.fingerprint() != dataset_v2.fingerprint():
|
|
99
|
+
print("Dataset has changed - retrain models")
|
|
100
|
+
|
|
101
|
+
# Check for new data quality issues
|
|
102
|
+
report_v2 = dataset_v2.inspect()
|
|
103
|
+
for col in report_v2.columns:
|
|
104
|
+
if col.missing > len(dataset_v2.records()) * 0.1: # More than 10% missing
|
|
105
|
+
print(f"Warning: {col.name} has high missing rate: {col.missing}")
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Experiment Reproducibility
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
from silver_data import Dataset
|
|
112
|
+
|
|
113
|
+
# Ensure exact same data across experiments
|
|
114
|
+
dataset = Dataset.from_csv("experiment", "data.csv")
|
|
115
|
+
experiment_id = f"exp_{dataset.fingerprint()}"
|
|
116
|
+
|
|
117
|
+
# Log for reproducibility
|
|
118
|
+
print(f"Running experiment {experiment_id} with dataset fingerprint {dataset.fingerprint()}")
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Advanced Usage
|
|
122
|
+
|
|
123
|
+
### Custom Data Loading
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
from silver_data import Dataset
|
|
127
|
+
import json
|
|
128
|
+
|
|
129
|
+
# Load from custom JSON format
|
|
130
|
+
with open("custom_data.json") as f:
|
|
131
|
+
data = json.load(f)
|
|
132
|
+
dataset = Dataset.from_json("custom", data)
|
|
133
|
+
|
|
134
|
+
# Load from streaming JSONL
|
|
135
|
+
with open("streaming_data.jsonl") as f:
|
|
136
|
+
dataset = Dataset.from_jsonl("streaming", f.read())
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Data Type Analysis
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
from silver_data import Dataset
|
|
143
|
+
|
|
144
|
+
dataset = Dataset.from_csv("analysis", "mixed_data.csv")
|
|
145
|
+
report = dataset.inspect()
|
|
146
|
+
|
|
147
|
+
# Analyze column types
|
|
148
|
+
string_cols = [c.name for c in report.columns if c.value_type == "string"]
|
|
149
|
+
numeric_cols = [c.name for c in report.columns if c.value_type == "number"]
|
|
150
|
+
mixed_cols = [c.name for c in report.columns if c.value_type == "mixed"]
|
|
151
|
+
|
|
152
|
+
print(f"String columns: {string_cols}")
|
|
153
|
+
print(f"Numeric columns: {numeric_cols}")
|
|
154
|
+
print(f"Mixed type columns: {mixed_cols}")
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Requirements
|
|
158
|
+
|
|
159
|
+
- Python 3.8+
|
|
160
|
+
- pandas 1.0+
|
|
161
|
+
|
|
162
|
+
## Development
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
# Install development dependencies
|
|
166
|
+
pip install -e ".[dev]"
|
|
167
|
+
|
|
168
|
+
# Run tests
|
|
169
|
+
pytest
|
|
170
|
+
|
|
171
|
+
# Run tests with coverage
|
|
172
|
+
pytest --cov=silver_data --cov-report=html
|
|
173
|
+
|
|
174
|
+
# Run linting
|
|
175
|
+
flake8 src/ tests/
|
|
176
|
+
mypy src/
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Contributing
|
|
180
|
+
|
|
181
|
+
Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
|
182
|
+
|
|
183
|
+
## License
|
|
184
|
+
|
|
185
|
+
Apache-2.0 - see [LICENSE](LICENSE) file for details.
|
|
186
|
+
|
|
187
|
+
## Related Packages
|
|
188
|
+
|
|
189
|
+
- [silver-run](https://github.com/adfgdartec/silver-run) - Training lifecycle management
|
|
190
|
+
- [silver-diagnostics](https://github.com/adfgdartec/silver-diagnostics) - ML diagnostics
|
|
191
|
+
- [silver-adapters](https://github.com/adfgdartec/silver-adapters) - Framework adapters
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "silver-data"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Inspectable, deterministic dataset contracts and loaders for Silver."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = "Apache-2.0"
|
|
12
|
+
authors = []
|
|
13
|
+
keywords = ["machine-learning", "datasets", "csv", "data-validation", "python"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Intended Audience :: Science/Research",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.8",
|
|
20
|
+
"Programming Language :: Python :: 3.9",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
25
|
+
]
|
|
26
|
+
dependencies = []
|
|
27
|
+
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
pandas = ["pandas>=1.0.0"]
|
|
30
|
+
dev = [
|
|
31
|
+
"pytest>=7.0.0",
|
|
32
|
+
"pandas>=1.0",
|
|
33
|
+
"pytest-cov>=4.0.0",
|
|
34
|
+
"flake8>=6.0.0",
|
|
35
|
+
"mypy>=1.0.0",
|
|
36
|
+
"build>=0.10.0",
|
|
37
|
+
"twine>=4.0.0",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
[project.urls]
|
|
41
|
+
Homepage = "https://github.com/adfgdartec/silver-data"
|
|
42
|
+
Repository = "https://github.com/adfgdartec/silver-data"
|
|
43
|
+
Issues = "https://github.com/adfgdartec/silver-data/issues"
|
|
44
|
+
|
|
45
|
+
[tool.setuptools.packages.find]
|
|
46
|
+
where = ["src"]
|
|
47
|
+
|
|
48
|
+
[tool.setuptools.package-dir]
|
|
49
|
+
"" = "src"
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from .dataset import Dataset
|
|
2
|
+
from .models import (
|
|
3
|
+
DataRecord,
|
|
4
|
+
DatasetColumn,
|
|
5
|
+
DatasetReport,
|
|
6
|
+
DatasetSplit,
|
|
7
|
+
DatasetValidation,
|
|
8
|
+
Primitive,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"Dataset",
|
|
13
|
+
"DatasetColumn",
|
|
14
|
+
"DatasetReport",
|
|
15
|
+
"DatasetValidation",
|
|
16
|
+
"DatasetSplit",
|
|
17
|
+
"Primitive",
|
|
18
|
+
"DataRecord",
|
|
19
|
+
]
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import csv
|
|
2
|
+
import hashlib
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any, List, Union
|
|
5
|
+
|
|
6
|
+
from .models import (
|
|
7
|
+
DataRecord,
|
|
8
|
+
DatasetColumn,
|
|
9
|
+
DatasetReport,
|
|
10
|
+
DatasetSplit,
|
|
11
|
+
DatasetValidation,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Dataset:
|
|
16
|
+
def __init__(self, name: str, rows: List[DataRecord]):
|
|
17
|
+
if not name.strip():
|
|
18
|
+
raise ValueError("Dataset name is required")
|
|
19
|
+
self.name = name
|
|
20
|
+
self._rows = [dict(row) for row in rows]
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
def from_records(cls, name: str, rows: List[DataRecord]) -> "Dataset":
|
|
24
|
+
if len(rows) == 0:
|
|
25
|
+
raise ValueError("Dataset must contain at least one row")
|
|
26
|
+
return cls(name, [dict(row) for row in rows])
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def from_json(
|
|
30
|
+
cls, name: str, value: Union[List[DataRecord], DataRecord]
|
|
31
|
+
) -> "Dataset":
|
|
32
|
+
if isinstance(value, dict):
|
|
33
|
+
return cls.from_records(name, [value])
|
|
34
|
+
return cls.from_records(name, value)
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def from_jsonl(cls, name: str, value: str) -> "Dataset":
|
|
38
|
+
lines = [line.strip() for line in value.strip().splitlines() if line.strip()]
|
|
39
|
+
records = []
|
|
40
|
+
for line in lines:
|
|
41
|
+
record = json.loads(line)
|
|
42
|
+
if not isinstance(record, dict) or isinstance(record, list):
|
|
43
|
+
raise ValueError("JSONL rows must be objects")
|
|
44
|
+
records.append(record)
|
|
45
|
+
return cls.from_records(name, records)
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def from_csv(cls, name: str, path: str) -> "Dataset":
|
|
49
|
+
try:
|
|
50
|
+
import pandas as pd
|
|
51
|
+
except ImportError:
|
|
52
|
+
pd = None
|
|
53
|
+
if pd is not None:
|
|
54
|
+
return cls.from_records(name, pd.read_csv(path).to_dict(orient="records"))
|
|
55
|
+
with open(path, newline="", encoding="utf-8") as stream:
|
|
56
|
+
return cls.from_records(name, list(csv.DictReader(stream)))
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def from_pandas(cls, name: str, df: Any) -> "Dataset":
|
|
60
|
+
if not hasattr(df, "to_dict"):
|
|
61
|
+
raise TypeError("from_pandas requires an object with to_dict")
|
|
62
|
+
return cls.from_records(name, df.to_dict(orient="records"))
|
|
63
|
+
|
|
64
|
+
def records(self) -> List[DataRecord]:
|
|
65
|
+
return [dict(row) for row in self._rows]
|
|
66
|
+
|
|
67
|
+
def columns(self) -> List[str]:
|
|
68
|
+
all_columns = set()
|
|
69
|
+
for row in self._rows:
|
|
70
|
+
all_columns.update(row.keys())
|
|
71
|
+
return sorted(all_columns)
|
|
72
|
+
|
|
73
|
+
def inspect(self) -> DatasetReport:
|
|
74
|
+
columns_report = []
|
|
75
|
+
for name in self.columns():
|
|
76
|
+
values = [row.get(name) for row in self._rows]
|
|
77
|
+
present = [value for value in values if value is not None]
|
|
78
|
+
if present:
|
|
79
|
+
types = {type(value).__name__ for value in present}
|
|
80
|
+
if len(types) == 1:
|
|
81
|
+
type_name = type(present[0]).__name__
|
|
82
|
+
value_type = {
|
|
83
|
+
"str": "string",
|
|
84
|
+
"int": "number",
|
|
85
|
+
"float": "number",
|
|
86
|
+
"bool": "boolean",
|
|
87
|
+
}.get(type_name, "mixed")
|
|
88
|
+
else:
|
|
89
|
+
value_type = "mixed"
|
|
90
|
+
else:
|
|
91
|
+
value_type = "null"
|
|
92
|
+
columns_report.append(
|
|
93
|
+
DatasetColumn(
|
|
94
|
+
name=name,
|
|
95
|
+
value_type=value_type,
|
|
96
|
+
missing=len(values) - len(present),
|
|
97
|
+
unique=len({json.dumps(value, sort_keys=True) for value in values}),
|
|
98
|
+
)
|
|
99
|
+
)
|
|
100
|
+
return DatasetReport(
|
|
101
|
+
name=self.name,
|
|
102
|
+
rows=len(self._rows),
|
|
103
|
+
columns=columns_report,
|
|
104
|
+
fingerprint=self.fingerprint(),
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
def validate(self) -> DatasetValidation:
|
|
108
|
+
errors = []
|
|
109
|
+
warnings = []
|
|
110
|
+
column_count = len(self.columns())
|
|
111
|
+
inconsistent_rows = [
|
|
112
|
+
index
|
|
113
|
+
for index, row in enumerate(self._rows)
|
|
114
|
+
if len(row.keys()) != column_count
|
|
115
|
+
]
|
|
116
|
+
if inconsistent_rows:
|
|
117
|
+
warnings.append(
|
|
118
|
+
f"Rows do not all contain the same columns: {inconsistent_rows[:5]}"
|
|
119
|
+
)
|
|
120
|
+
for column in self.inspect().columns:
|
|
121
|
+
if column.missing > 0:
|
|
122
|
+
warnings.append(f"{column.name} has {column.missing} missing values")
|
|
123
|
+
return DatasetValidation(
|
|
124
|
+
valid=len(errors) == 0, errors=errors, warnings=warnings
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
def fingerprint(self) -> str:
|
|
128
|
+
data = json.dumps({"name": self.name, "rows": self._rows}, sort_keys=True)
|
|
129
|
+
return hashlib.md5(data.encode()).hexdigest()[:8]
|
|
130
|
+
|
|
131
|
+
def split(
|
|
132
|
+
self, train: float = 0.8, validation: float = 0.1, test: float = 0.1
|
|
133
|
+
) -> DatasetSplit:
|
|
134
|
+
if train < 0 or validation < 0 or test < 0:
|
|
135
|
+
raise ValueError("Dataset split ratios must be non-negative")
|
|
136
|
+
if abs(train + validation + test - 1.0) > 1e-9:
|
|
137
|
+
raise ValueError("Dataset split ratios must sum to 1")
|
|
138
|
+
train_end = int(len(self._rows) * train)
|
|
139
|
+
validation_end = train_end + int(len(self._rows) * validation)
|
|
140
|
+
return DatasetSplit(
|
|
141
|
+
train=Dataset._from_rows(f"{self.name}/train", self._rows[:train_end]),
|
|
142
|
+
validation=Dataset._from_rows(
|
|
143
|
+
f"{self.name}/validation", self._rows[train_end:validation_end]
|
|
144
|
+
),
|
|
145
|
+
test=Dataset._from_rows(f"{self.name}/test", self._rows[validation_end:]),
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
@classmethod
|
|
149
|
+
def _from_rows(cls, name: str, rows: List[DataRecord]) -> "Dataset":
|
|
150
|
+
return cls(name, [dict(row) for row in rows])
|
|
151
|
+
|
|
152
|
+
def to_pandas(self) -> Any:
|
|
153
|
+
try:
|
|
154
|
+
import pandas as pd
|
|
155
|
+
except ImportError as error:
|
|
156
|
+
raise ImportError(
|
|
157
|
+
"Dataset.to_pandas() requires the optional 'pandas' dependency; "
|
|
158
|
+
"install silver-data[pandas]"
|
|
159
|
+
) from error
|
|
160
|
+
return pd.DataFrame(self._rows, columns=self.columns())
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Dict, List, Literal, Union
|
|
3
|
+
|
|
4
|
+
Primitive = Union[str, int, float, bool, None]
|
|
5
|
+
DataRecord = Dict[str, Primitive]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True)
|
|
9
|
+
class DatasetColumn:
|
|
10
|
+
name: str
|
|
11
|
+
value_type: Literal["string", "number", "boolean", "null", "mixed"]
|
|
12
|
+
missing: int
|
|
13
|
+
unique: int
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class DatasetReport:
|
|
18
|
+
name: str
|
|
19
|
+
rows: int
|
|
20
|
+
columns: List[DatasetColumn]
|
|
21
|
+
fingerprint: str
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class DatasetValidation:
|
|
26
|
+
valid: bool
|
|
27
|
+
errors: List[str]
|
|
28
|
+
warnings: List[str]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class DatasetSplit:
|
|
33
|
+
train: "Dataset"
|
|
34
|
+
validation: "Dataset"
|
|
35
|
+
test: "Dataset"
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: silver-data
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Inspectable, deterministic dataset contracts and loaders for Silver.
|
|
5
|
+
License-Expression: Apache-2.0
|
|
6
|
+
Project-URL: Homepage, https://github.com/adfgdartec/silver-data
|
|
7
|
+
Project-URL: Repository, https://github.com/adfgdartec/silver-data
|
|
8
|
+
Project-URL: Issues, https://github.com/adfgdartec/silver-data/issues
|
|
9
|
+
Keywords: machine-learning,datasets,csv,data-validation,python
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
20
|
+
Requires-Python: >=3.8
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Provides-Extra: pandas
|
|
24
|
+
Requires-Dist: pandas>=1.0.0; extra == "pandas"
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
27
|
+
Requires-Dist: pandas>=1.0; extra == "dev"
|
|
28
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
29
|
+
Requires-Dist: flake8>=6.0.0; extra == "dev"
|
|
30
|
+
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
31
|
+
Requires-Dist: build>=0.10.0; extra == "dev"
|
|
32
|
+
Requires-Dist: twine>=4.0.0; extra == "dev"
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
|
|
35
|
+
# silver-data
|
|
36
|
+
|
|
37
|
+
[](https://www.python.org/downloads/)
|
|
38
|
+
[](LICENSE)
|
|
39
|
+
[](tests/)
|
|
40
|
+
[](https://flake8.pycqa.org/)
|
|
41
|
+
|
|
42
|
+
Inspectable, deterministic dataset contracts and loaders for Silver. A Python package designed for ML researchers who need reliable dataset handling with built-in validation and reproducibility features.
|
|
43
|
+
|
|
44
|
+
The base install uses only the Python standard library for records, JSON, JSONL,
|
|
45
|
+
and CSV. Add pandas only when you need DataFrame conversion:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install 'silver-data[pandas]'
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Installation
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install silver-data
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Quick Start
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from silver_data import Dataset
|
|
61
|
+
|
|
62
|
+
# Load from CSV file
|
|
63
|
+
dataset = Dataset.from_csv("my_data", "path/to/data.csv")
|
|
64
|
+
|
|
65
|
+
# Optional: load from pandas
|
|
66
|
+
import pandas as pd
|
|
67
|
+
df = pd.read_csv("path/to/data.csv")
|
|
68
|
+
dataset = Dataset.from_pandas("my_data", df)
|
|
69
|
+
|
|
70
|
+
# Inspect dataset
|
|
71
|
+
report = dataset.inspect()
|
|
72
|
+
print(f"Rows: {report.rows}, Columns: {len(report.columns)}")
|
|
73
|
+
for col in report.columns:
|
|
74
|
+
print(f" {col.name}: {col.value_type} ({col.unique} unique, {col.missing} missing)")
|
|
75
|
+
|
|
76
|
+
# Validate dataset
|
|
77
|
+
validation = dataset.validate()
|
|
78
|
+
if not validation.valid:
|
|
79
|
+
print("Errors:", validation.errors)
|
|
80
|
+
if validation.warnings:
|
|
81
|
+
print("Warnings:", validation.warnings)
|
|
82
|
+
|
|
83
|
+
# Split dataset for ML workflows
|
|
84
|
+
train, val, test = dataset.split(train=0.8, validation=0.1, test=0.1)
|
|
85
|
+
print(f"Train: {len(train.records())}, Val: {len(val.records())}, Test: {len(test.records())}")
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Features
|
|
89
|
+
|
|
90
|
+
- **Multiple Data Sources**: Load from CSV, JSON, JSONL, and pandas DataFrames
|
|
91
|
+
- **Dataset Inspection**: Get detailed column statistics and metadata
|
|
92
|
+
- **Data Validation**: Automatic detection of missing values, inconsistent columns, and data quality issues
|
|
93
|
+
- **Deterministic Fingerprinting**: Generate unique identifiers for datasets to ensure reproducibility
|
|
94
|
+
- **Smart Splitting**: Train/validation/test splitting with customizable ratios
|
|
95
|
+
- **Immutable Design**: Safe data handling with copy-on-write semantics
|
|
96
|
+
- **Type Safety**: Full type hints for better IDE support and fewer bugs
|
|
97
|
+
|
|
98
|
+
## Use Cases
|
|
99
|
+
|
|
100
|
+
### ML Pipeline Integration
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from silver_data import Dataset
|
|
104
|
+
import pandas as pd
|
|
105
|
+
|
|
106
|
+
# Load and validate training data
|
|
107
|
+
df = pd.read_csv("train.csv")
|
|
108
|
+
dataset = Dataset.from_pandas("training", df)
|
|
109
|
+
|
|
110
|
+
# Ensure data quality before training
|
|
111
|
+
validation = dataset.validate()
|
|
112
|
+
if not validation.valid:
|
|
113
|
+
raise ValueError(f"Dataset validation failed: {validation.errors}")
|
|
114
|
+
|
|
115
|
+
# Split for cross-validation
|
|
116
|
+
train_split, val_split, test_split = dataset.split(train=0.7, validation=0.15, test=0.15)
|
|
117
|
+
|
|
118
|
+
# Use fingerprints for caching
|
|
119
|
+
cache_key = dataset.fingerprint()
|
|
120
|
+
print(f"Dataset fingerprint: {cache_key}")
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Data Quality Monitoring
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
from silver_data import Dataset
|
|
127
|
+
|
|
128
|
+
# Monitor data drift over time
|
|
129
|
+
dataset_v1 = Dataset.from_csv("data_v1", "data_2024_01.csv")
|
|
130
|
+
dataset_v2 = Dataset.from_csv("data_v2", "data_2024_02.csv")
|
|
131
|
+
|
|
132
|
+
if dataset_v1.fingerprint() != dataset_v2.fingerprint():
|
|
133
|
+
print("Dataset has changed - retrain models")
|
|
134
|
+
|
|
135
|
+
# Check for new data quality issues
|
|
136
|
+
report_v2 = dataset_v2.inspect()
|
|
137
|
+
for col in report_v2.columns:
|
|
138
|
+
if col.missing > len(dataset_v2.records()) * 0.1: # More than 10% missing
|
|
139
|
+
print(f"Warning: {col.name} has high missing rate: {col.missing}")
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Experiment Reproducibility
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
from silver_data import Dataset
|
|
146
|
+
|
|
147
|
+
# Ensure exact same data across experiments
|
|
148
|
+
dataset = Dataset.from_csv("experiment", "data.csv")
|
|
149
|
+
experiment_id = f"exp_{dataset.fingerprint()}"
|
|
150
|
+
|
|
151
|
+
# Log for reproducibility
|
|
152
|
+
print(f"Running experiment {experiment_id} with dataset fingerprint {dataset.fingerprint()}")
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Advanced Usage
|
|
156
|
+
|
|
157
|
+
### Custom Data Loading
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
from silver_data import Dataset
|
|
161
|
+
import json
|
|
162
|
+
|
|
163
|
+
# Load from custom JSON format
|
|
164
|
+
with open("custom_data.json") as f:
|
|
165
|
+
data = json.load(f)
|
|
166
|
+
dataset = Dataset.from_json("custom", data)
|
|
167
|
+
|
|
168
|
+
# Load from streaming JSONL
|
|
169
|
+
with open("streaming_data.jsonl") as f:
|
|
170
|
+
dataset = Dataset.from_jsonl("streaming", f.read())
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Data Type Analysis
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
from silver_data import Dataset
|
|
177
|
+
|
|
178
|
+
dataset = Dataset.from_csv("analysis", "mixed_data.csv")
|
|
179
|
+
report = dataset.inspect()
|
|
180
|
+
|
|
181
|
+
# Analyze column types
|
|
182
|
+
string_cols = [c.name for c in report.columns if c.value_type == "string"]
|
|
183
|
+
numeric_cols = [c.name for c in report.columns if c.value_type == "number"]
|
|
184
|
+
mixed_cols = [c.name for c in report.columns if c.value_type == "mixed"]
|
|
185
|
+
|
|
186
|
+
print(f"String columns: {string_cols}")
|
|
187
|
+
print(f"Numeric columns: {numeric_cols}")
|
|
188
|
+
print(f"Mixed type columns: {mixed_cols}")
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Requirements
|
|
192
|
+
|
|
193
|
+
- Python 3.8+
|
|
194
|
+
- pandas 1.0+
|
|
195
|
+
|
|
196
|
+
## Development
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
# Install development dependencies
|
|
200
|
+
pip install -e ".[dev]"
|
|
201
|
+
|
|
202
|
+
# Run tests
|
|
203
|
+
pytest
|
|
204
|
+
|
|
205
|
+
# Run tests with coverage
|
|
206
|
+
pytest --cov=silver_data --cov-report=html
|
|
207
|
+
|
|
208
|
+
# Run linting
|
|
209
|
+
flake8 src/ tests/
|
|
210
|
+
mypy src/
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Contributing
|
|
214
|
+
|
|
215
|
+
Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
|
216
|
+
|
|
217
|
+
## License
|
|
218
|
+
|
|
219
|
+
Apache-2.0 - see [LICENSE](LICENSE) file for details.
|
|
220
|
+
|
|
221
|
+
## Related Packages
|
|
222
|
+
|
|
223
|
+
- [silver-run](https://github.com/adfgdartec/silver-run) - Training lifecycle management
|
|
224
|
+
- [silver-diagnostics](https://github.com/adfgdartec/silver-diagnostics) - ML diagnostics
|
|
225
|
+
- [silver-adapters](https://github.com/adfgdartec/silver-adapters) - Framework adapters
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
CHANGELOG.md
|
|
2
|
+
CONTRIBUTING.md
|
|
3
|
+
LICENSE
|
|
4
|
+
MANIFEST.in
|
|
5
|
+
README.md
|
|
6
|
+
pyproject.toml
|
|
7
|
+
src/silver_data/__init__.py
|
|
8
|
+
src/silver_data/dataset.py
|
|
9
|
+
src/silver_data/models.py
|
|
10
|
+
src/silver_data.egg-info/PKG-INFO
|
|
11
|
+
src/silver_data.egg-info/SOURCES.txt
|
|
12
|
+
src/silver_data.egg-info/dependency_links.txt
|
|
13
|
+
src/silver_data.egg-info/requires.txt
|
|
14
|
+
src/silver_data.egg-info/top_level.txt
|
|
15
|
+
tests/__init__.py
|
|
16
|
+
tests/test_data.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
silver_data
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Tests for silver-data package
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import pandas as pd
|
|
3
|
+
from silver_data import (
|
|
4
|
+
Dataset,
|
|
5
|
+
DatasetColumn,
|
|
6
|
+
DatasetReport,
|
|
7
|
+
DatasetValidation,
|
|
8
|
+
DatasetSplit,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TestDatasetCreation:
|
|
13
|
+
def test_from_records(self):
|
|
14
|
+
records = [
|
|
15
|
+
{"name": "Alice", "age": 30, "city": "NYC"},
|
|
16
|
+
{"name": "Bob", "age": 25, "city": "LA"},
|
|
17
|
+
]
|
|
18
|
+
dataset = Dataset.from_records("test", records)
|
|
19
|
+
assert dataset.name == "test"
|
|
20
|
+
assert len(dataset.records()) == 2
|
|
21
|
+
|
|
22
|
+
def test_from_records_empty_raises_error(self):
|
|
23
|
+
with pytest.raises(ValueError, match="Dataset must contain at least one row"):
|
|
24
|
+
Dataset.from_records("test", [])
|
|
25
|
+
|
|
26
|
+
def test_from_records_empty_name_raises_error(self):
|
|
27
|
+
with pytest.raises(ValueError, match="Dataset name is required"):
|
|
28
|
+
Dataset.from_records("", [{"a": 1}])
|
|
29
|
+
|
|
30
|
+
def test_from_json_single_object(self):
|
|
31
|
+
dataset = Dataset.from_json("test", {"name": "Alice", "age": 30})
|
|
32
|
+
assert len(dataset.records()) == 1
|
|
33
|
+
assert dataset.records()[0] == {"name": "Alice", "age": 30}
|
|
34
|
+
|
|
35
|
+
def test_from_json_array(self):
|
|
36
|
+
dataset = Dataset.from_json("test", [{"name": "Alice"}, {"name": "Bob"}])
|
|
37
|
+
assert len(dataset.records()) == 2
|
|
38
|
+
|
|
39
|
+
def test_from_jsonl(self):
|
|
40
|
+
jsonl = '{"name": "Alice", "age": 30}\n{"name": "Bob", "age": 25}'
|
|
41
|
+
dataset = Dataset.from_jsonl("test", jsonl)
|
|
42
|
+
assert len(dataset.records()) == 2
|
|
43
|
+
|
|
44
|
+
def test_from_jsonl_invalid_raises_error(self):
|
|
45
|
+
with pytest.raises(ValueError, match="JSONL rows must be objects"):
|
|
46
|
+
Dataset.from_jsonl("test", '["not", "an", "object"]')
|
|
47
|
+
|
|
48
|
+
def test_from_pandas(self):
|
|
49
|
+
df = pd.DataFrame({"name": ["Alice", "Bob"], "age": [30, 25]})
|
|
50
|
+
dataset = Dataset.from_pandas("test", df)
|
|
51
|
+
assert len(dataset.records()) == 2
|
|
52
|
+
assert dataset.records()[0]["name"] == "Alice"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class TestDatasetInspection:
|
|
56
|
+
def test_columns(self):
|
|
57
|
+
records = [
|
|
58
|
+
{"name": "Alice", "age": 30},
|
|
59
|
+
{"name": "Bob", "age": 25, "city": "LA"},
|
|
60
|
+
]
|
|
61
|
+
dataset = Dataset.from_records("test", records)
|
|
62
|
+
columns = dataset.columns()
|
|
63
|
+
assert "age" in columns
|
|
64
|
+
assert "name" in columns
|
|
65
|
+
assert "city" in columns
|
|
66
|
+
|
|
67
|
+
def test_inspect(self):
|
|
68
|
+
records = [{"name": "Alice", "age": 30}, {"name": "Bob", "age": 25}]
|
|
69
|
+
dataset = Dataset.from_records("test", records)
|
|
70
|
+
report = dataset.inspect()
|
|
71
|
+
|
|
72
|
+
assert isinstance(report, DatasetReport)
|
|
73
|
+
assert report.name == "test"
|
|
74
|
+
assert report.rows == 2
|
|
75
|
+
assert len(report.columns) == 2
|
|
76
|
+
assert report.fingerprint == dataset.fingerprint()
|
|
77
|
+
|
|
78
|
+
def test_inspect_column_types(self):
|
|
79
|
+
records = [
|
|
80
|
+
{"name": "Alice", "age": 30, "active": True},
|
|
81
|
+
{"name": "Bob", "age": 25, "active": False},
|
|
82
|
+
]
|
|
83
|
+
dataset = Dataset.from_records("test", records)
|
|
84
|
+
report = dataset.inspect()
|
|
85
|
+
|
|
86
|
+
col_dict = {col.name: col for col in report.columns}
|
|
87
|
+
assert col_dict["name"].value_type == "string"
|
|
88
|
+
assert col_dict["age"].value_type == "number"
|
|
89
|
+
assert col_dict["active"].value_type == "boolean"
|
|
90
|
+
|
|
91
|
+
def test_inspect_missing_values(self):
|
|
92
|
+
records = [{"name": "Alice", "age": 30}, {"name": None, "age": 25}]
|
|
93
|
+
dataset = Dataset.from_records("test", records)
|
|
94
|
+
report = dataset.inspect()
|
|
95
|
+
|
|
96
|
+
col_dict = {col.name: col for col in report.columns}
|
|
97
|
+
assert col_dict["name"].missing == 1
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class TestDatasetValidation:
|
|
101
|
+
def test_validate_valid_dataset(self):
|
|
102
|
+
records = [{"name": "Alice", "age": 30}, {"name": "Bob", "age": 25}]
|
|
103
|
+
dataset = Dataset.from_records("test", records)
|
|
104
|
+
validation = dataset.validate()
|
|
105
|
+
|
|
106
|
+
assert validation.valid == True
|
|
107
|
+
assert len(validation.errors) == 0
|
|
108
|
+
|
|
109
|
+
def test_validate_inconsistent_columns(self):
|
|
110
|
+
records = [
|
|
111
|
+
{"name": "Alice", "age": 30},
|
|
112
|
+
{"name": "Bob", "age": 25, "city": "LA"},
|
|
113
|
+
]
|
|
114
|
+
dataset = Dataset.from_records("test", records)
|
|
115
|
+
validation = dataset.validate()
|
|
116
|
+
|
|
117
|
+
assert validation.valid == True # warnings don't make it invalid
|
|
118
|
+
assert len(validation.warnings) > 0
|
|
119
|
+
assert "do not all contain the same columns" in validation.warnings[0]
|
|
120
|
+
|
|
121
|
+
def test_validate_missing_values(self):
|
|
122
|
+
records = [{"name": "Alice", "age": 30}, {"name": None, "age": 25}]
|
|
123
|
+
dataset = Dataset.from_records("test", records)
|
|
124
|
+
validation = dataset.validate()
|
|
125
|
+
|
|
126
|
+
assert len(validation.warnings) > 0
|
|
127
|
+
assert "missing values" in validation.warnings[0]
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class TestDatasetFingerprint:
|
|
131
|
+
def test_fingerprint_deterministic(self):
|
|
132
|
+
records = [{"name": "Alice", "age": 30}]
|
|
133
|
+
dataset1 = Dataset.from_records("test", records)
|
|
134
|
+
dataset2 = Dataset.from_records("test", records)
|
|
135
|
+
|
|
136
|
+
assert dataset1.fingerprint() == dataset2.fingerprint()
|
|
137
|
+
|
|
138
|
+
def test_fingerprint_different_data(self):
|
|
139
|
+
dataset1 = Dataset.from_records("test", [{"name": "Alice"}])
|
|
140
|
+
dataset2 = Dataset.from_records("test", [{"name": "Bob"}])
|
|
141
|
+
|
|
142
|
+
assert dataset1.fingerprint() != dataset2.fingerprint()
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class TestDatasetSplit:
|
|
146
|
+
def test_split_default_ratios(self):
|
|
147
|
+
records = [{"value": i} for i in range(100)]
|
|
148
|
+
dataset = Dataset.from_records("test", records)
|
|
149
|
+
split = dataset.split()
|
|
150
|
+
|
|
151
|
+
assert isinstance(split, DatasetSplit)
|
|
152
|
+
assert len(split.train.records()) == 80
|
|
153
|
+
assert len(split.validation.records()) == 10
|
|
154
|
+
assert len(split.test.records()) == 10
|
|
155
|
+
|
|
156
|
+
def test_split_custom_ratios(self):
|
|
157
|
+
records = [{"value": i} for i in range(100)]
|
|
158
|
+
dataset = Dataset.from_records("test", records)
|
|
159
|
+
split = dataset.split(train=0.6, validation=0.2, test=0.2)
|
|
160
|
+
|
|
161
|
+
assert len(split.train.records()) == 60
|
|
162
|
+
assert len(split.validation.records()) == 20
|
|
163
|
+
assert len(split.test.records()) == 20
|
|
164
|
+
|
|
165
|
+
def test_split_invalid_ratios_raises_error(self):
|
|
166
|
+
dataset = Dataset.from_records("test", [{"value": 1}])
|
|
167
|
+
|
|
168
|
+
with pytest.raises(ValueError, match="must sum to 1"):
|
|
169
|
+
dataset.split(train=0.5, validation=0.3, test=0.1)
|
|
170
|
+
|
|
171
|
+
with pytest.raises(ValueError, match="must be non-negative"):
|
|
172
|
+
dataset.split(train=-0.1)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class TestDatasetToPandas:
|
|
176
|
+
def test_to_pandas(self):
|
|
177
|
+
records = [{"name": "Alice", "age": 30}, {"name": "Bob", "age": 25}]
|
|
178
|
+
dataset = Dataset.from_records("test", records)
|
|
179
|
+
df = dataset.to_pandas()
|
|
180
|
+
|
|
181
|
+
assert isinstance(df, pd.DataFrame)
|
|
182
|
+
assert len(df) == 2
|
|
183
|
+
assert list(df.columns) == ["age", "name"] # alphabetical order
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class TestDatasetImmutability:
|
|
187
|
+
def test_records_returns_copy(self):
|
|
188
|
+
records = [{"name": "Alice"}]
|
|
189
|
+
dataset = Dataset.from_records("test", records)
|
|
190
|
+
|
|
191
|
+
returned_records = dataset.records()
|
|
192
|
+
returned_records[0]["name"] = "Bob"
|
|
193
|
+
|
|
194
|
+
assert dataset.records()[0]["name"] == "Alice"
|