Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
- name: Set up pixi
uses: prefix-dev/setup-pixi@a09b6247153796b190642a2b53fac4241043cf6f # v0.10.0
with:
environments: default lint polars-minimal
environments: default default-py310
- name: Install Rust
run: rustup show
- name: Cache Rust dependencies
Expand Down
2 changes: 1 addition & 1 deletion .lefthook.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ pre-commit:
run: pixi {run} ruff format --force-exclude
- name: mypy
glob: "*.py"
run: pixi {run} mypy {staged_files}
run: pixi {run} -e default-py310 mypy {staged_files}
- name: prettier
glob: "*.{md,yml,yaml}"
run: pixi {run} prettier --write --no-error-on-unmatched-pattern --list-different --ignore-unknown {staged_files}
Expand Down
16 changes: 9 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ pip install dataframely
import dataframely as dy
import polars as pl


class HouseSchema(dy.Schema):
zip_code = dy.String(nullable=False, min_length=3)
num_bedrooms = dy.UInt8(nullable=False)
Expand All @@ -64,15 +65,16 @@ class HouseSchema(dy.Schema):
### Validating data against schema

```python

import polars as pl

df = pl.DataFrame({
"zip_code": ["01234", "01234", "1", "213", "123", "213"],
"num_bedrooms": [2, 2, 1, None, None, 2],
"num_bathrooms": [1, 2, 1, 1, 0, 8],
"price": [100_000, 110_000, 50_000, 80_000, 60_000, 160_000]
})
df = pl.DataFrame(
{
"zip_code": ["01234", "01234", "1", "213", "123", "213"],
"num_bedrooms": [2, 2, 1, None, None, 2],
"num_bathrooms": [1, 2, 1, 1, 0, 8],
"price": [100_000, 110_000, 50_000, 80_000, 60_000, 160_000],
}
)

# Validate the data and cast columns to expected types
validated_df: dy.DataFrame[HouseSchema] = HouseSchema.validate(df, cast=True)
Expand Down
8 changes: 4 additions & 4 deletions docs/guides/coding-agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,15 @@ right time.
For example:

```python
def preprocess(raw: dy.LazyFrame[MyRawSchema]) -> dy.DataFrame[MyPreprocessedSchema]:
...
def preprocess(
raw: dy.LazyFrame[MyRawSchema],
) -> dy.DataFrame[MyPreprocessedSchema]: ...
```

gives a coding agent much more information than the schema-less alternative:

```python
def load_data(raw: pl.LazyFrame) -> pl.DataFrame:
...
def load_data(raw: pl.LazyFrame) -> pl.DataFrame: ...
```

This convention also makes your code more readable and maintainable for human developers.
Expand Down
10 changes: 6 additions & 4 deletions docs/guides/features/column-metadata.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ For instance, one may use the `metadata` parameter to mark a column as pseudonym
class UserSchema(dy.Schema):
id = dy.String(primary_key=True)
# Mark last name column as pseudonymized and (non-docstring) comment on it.
last_name = dy.String(metadata={
"pseudonymized": True,
"comment": "Pseudonymized using cryptographic hash function"
})
last_name = dy.String(
metadata={
"pseudonymized": True,
"comment": "Pseudonymized using cryptographic hash function",
}
)
# Add information about database column type.
address = dy.String(metadata={"database-type": "VARCHAR(MAX)"})
```
Expand Down
46 changes: 32 additions & 14 deletions docs/guides/features/data-generation.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class InvoiceSchema(dy.Schema):
discharge_date = dy.Date(nullable=False)
amount = dy.Decimal(nullable=False)


# Get data frame with correct type hint.
df: dy.DataFrame[InvoiceSchema] = InvoiceSchema.create_empty()
```
Expand All @@ -34,6 +35,7 @@ class InvoiceSchema(dy.Schema):
discharge_date = dy.Date(nullable=False)
amount = dy.Decimal(nullable=False)


df: dy.DataFrame[InvoiceSchema] = InvoiceSchema.sample(num_rows=100)
```

Expand All @@ -54,6 +56,7 @@ class InvoiceSchema(dy.Schema):
def discharge_after_admission(cls) -> pl.Expr:
return InvoiceSchema.discharge_date.col >= InvoiceSchema.admission_date.col


# `@dy.rule`s will be respected as well for data generation.
df: dy.DataFrame[InvoiceSchema] = InvoiceSchema.sample(num_rows=100)
```
Expand All @@ -79,25 +82,30 @@ The column-wise specification specifies an iterable of values for each specified
from datetime import date

# Override values for specific columns.
df: dy.DataFrame[InvoiceSchema] = InvoiceSchema.sample(overrides={
# Use either <schema>.<column>.name or just the column name as a string.
InvoiceSchema.invoice_id.name: ["1234567890", "2345678901", "3456789012"],
# Dataframely will automatically infer the number of rows based on the longest given
# sequence of values and broadcast all other columns to that shape.
"admission_date": date(2025, 1, 1),
})
df: dy.DataFrame[InvoiceSchema] = InvoiceSchema.sample(
overrides={
# Use either <schema>.<column>.name or just the column name as a string.
InvoiceSchema.invoice_id.name: ["1234567890", "2345678901", "3456789012"],
# Dataframely will automatically infer the number of rows based on the longest given
# sequence of values and broadcast all other columns to that shape.
"admission_date": date(2025, 1, 1),
}
)
```

The row-wise specification implements an iterable of mappings for the rows that should be sampled. It is particularly helpful if you want to make it easy to understand how values will be combined in specific rows (e.g., when each row represents one object).

```python
from datetime import date

# Override values for specific columns.
df: dy.DataFrame[InvoiceSchema] = InvoiceSchema.sample(overrides=[
{"invoice_id": "1234567890", "admission_date": date(2025, 1, 1)},
{"invoice_id": "2345678901", "admission_date": date(2025, 1, 1)},
{"invoice_id": "3456789012", "admission_date": date(2025, 1, 1)},
])
df: dy.DataFrame[InvoiceSchema] = InvoiceSchema.sample(
overrides=[
{"invoice_id": "1234567890", "admission_date": date(2025, 1, 1)},
{"invoice_id": "2345678901", "admission_date": date(2025, 1, 1)},
{"invoice_id": "3456789012", "admission_date": date(2025, 1, 1)},
]
)
```

### Providing custom column overrides
Expand All @@ -107,6 +115,8 @@ Complex validation rules (such as dependencies between columns or ordering crite
```python
import polars as pl
import dataframely as dy


class OrderedSchema(dy.Schema):
"""A schema that requires `iter` to be ordered with respect to `a` and `b`."""

Expand All @@ -116,14 +126,17 @@ class OrderedSchema(dy.Schema):

@dy.rule()
def iter_order_correct(cls) -> pl.Expr:
return pl.col("iter").rank(method="ordinal") == pl.struct(pl.col("a"), pl.col("b")).rank(method="ordinal")
return pl.col("iter").rank(method="ordinal") == pl.struct(
pl.col("a"), pl.col("b")
).rank(method="ordinal")

@classmethod
def _sampling_overrides(cls) -> dict[str, pl.Expr]:
return {
"iter": pl.struct(pl.col("a"), pl.col("b")).rank(method="ordinal"),
}


result = OrderedSchema.sample(100)
```

Expand All @@ -137,10 +150,12 @@ class DiagnosisSchema(dy.Schema):
invoice_id = dy.String(primary_key=True)
code = dy.String(nullable=False, regex=r"[A-Z][0-9]{2,4}")


class HospitalInvoiceData(dy.Collection):
invoice: dy.LazyFrame[InvoiceSchema]
diagnosis: dy.LazyFrame[DiagnosisSchema]


invoice_data: HospitalInvoiceData = HospitalInvoiceData.sample(num_rows=10)
```

Expand All @@ -153,6 +168,7 @@ class DiagnosisSchema(dy.Schema):
invoice_id = dy.String(primary_key=True)
code = dy.String(primary_key=True, regex=r"[A-Z][0-9]{2,4}")


class HospitalInvoiceData(dy.Collection):
invoice: dy.LazyFrame[InvoiceSchema]
diagnosis: dy.LazyFrame[DiagnosisSchema]
Expand Down Expand Up @@ -190,7 +206,9 @@ class HospitalInvoiceData(dy.Collection):

@classmethod
@override
def _preprocess_sample(cls, sample: dict[str, Any], index: int, generator: Generator):
def _preprocess_sample(
cls, sample: dict[str, Any], index: int, generator: Generator
):
# Set common primary key.
if "invoice_id" not in sample:
sample["invoice_id"] = str(index)
Expand Down
6 changes: 1 addition & 5 deletions docs/guides/features/sql-generation.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,7 @@ Uploading data can then be handled by {meth}`polars.DataFrame.write_database`:
```python
df: dy.DataFrame[MySchema]

df.write_database(
connection=engine,
table_name=my_table.name,
if_table_exists="append"
)
df.write_database(connection=engine, table_name=my_table.name, if_table_exists="append")
```

```{note}
Expand Down
6 changes: 2 additions & 4 deletions docs/guides/migration/v1-v2.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,7 @@ class MySchema(dy.Schema):
...

@dy.rule()
def my_rule() -> pl.Expr:
...
def my_rule() -> pl.Expr: ...
```

turns into
Expand All @@ -55,8 +54,7 @@ class MySchema(dy.Schema):
...

@dy.rule()
def my_rule(cls) -> pl.Expr:
...
def my_rule(cls) -> pl.Expr: ...
```

Within the schema rule, `cls` can now be used to access columns or other information from the schema. Specifically,
Expand Down
3 changes: 3 additions & 0 deletions docs/guides/migration/v2-v3.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ If `categories` is not specified, the behavior is unchanged.
import polars as pl
import dataframely as dy


class MySchema(dy.Schema):
# Column-scoped categories with a specific physical backing type
a = dy.Categorical(pl.UInt16)
Expand Down Expand Up @@ -82,6 +83,7 @@ df = MySchema.read_parquet("data.parquet")

# After (v3)
import polars as pl

df = MySchema.validate(pl.read_parquet("data.parquet"), cast=True)
```

Expand Down Expand Up @@ -168,6 +170,7 @@ class HouseSchema(dy.Schema):
def minimum_zip_code_count(cls) -> pl.Expr:
return pl.len() >= 2


# After (v3)
class HouseSchema(dy.Schema):
zip_code = dy.String(nullable=False, min_length=3)
Expand Down
34 changes: 18 additions & 16 deletions docs/guides/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@
For the purpose of this guide, let's assume that we're working with data that we use to predict housing prices.
To this end, we want to ensure that all the data we're using meets several expectations.
As a running example, consider the following data set:
| `zip_code` | `num_bedrooms` | `num_bathrooms` | `price`|
|-------|--------------|--------------|----------|
| "01234" | 2 | 1 | 100,000 |
| "01234" | 2 | 2 | 110,000 |
| "1" | 1 | 1 | 50,000 |
| "213" | NULL | 1 | 80,000 |
| "123" | NULL | 0 | 60,000 |
| "213" | 2 | 8 | 160,000

| `zip_code` | `num_bedrooms` | `num_bathrooms` | `price` |
| ---------- | -------------- | --------------- | ------- |
| "01234" | 2 | 1 | 100,000 |
| "01234" | 2 | 2 | 110,000 |
| "1" | 1 | 1 | 50,000 |
| "213" | NULL | 1 | 80,000 |
| "123" | NULL | 0 | 60,000 |
| "213" | 2 | 8 | 160,000 |

## Creating a {class}`~dataframely.Schema` class

Expand Down Expand Up @@ -85,12 +86,14 @@ the data set above as follows:
```python
import polars as pl

df = pl.DataFrame({
"zip_code": ["01234", "01234", "1", "213", "123", "213"],
"num_bedrooms": [2, 2, 1, None, None, 2],
"num_bathrooms": [1, 2, 1, 1, 0, 8],
"price": [100_000, 110_000, 50_000, 80_000, 60_000, 160_000]
})
df = pl.DataFrame(
{
"zip_code": ["01234", "01234", "1", "213", "123", "213"],
"num_bedrooms": [2, 2, 1, None, None, 2],
"num_bathrooms": [1, 2, 1, 1, 0, 8],
"price": [100_000, 110_000, 50_000, 80_000, 60_000, 160_000],
}
)

# Validate the data and cast columns to expected types
validated_df = HouseSchema.validate(df, cast=True)
Expand All @@ -113,8 +116,7 @@ The generic data frame types allow for more readable function signatures to expr
expectations on the schema of the data frame, e.g.:

```python
def train_model(df: dy.DataFrame[HouseSchema]) -> None:
...
def train_model(df: dy.DataFrame[HouseSchema]) -> None: ...
```

The type checker (typically `mypy`) then ensures that it is actually a
Expand Down
Loading