Saltar al contingut

Patito

Patito ofereix una manera senzilla de declarar models de dades pydantic que serveixen també com a esquema per als teus dataframes de polars.

patito és una biblioteca de validació de dataframes construïda sobre Polars.

La idea central de Patito és que has de definir un anomenat model per a cadascuna de les teves fonts de dades.

Un model és una classe declarativa de Python que descriu les propietats generals d’un conjunt de dades tabulars: els noms de totes les columnes, els seus tipus, límits de valors, etc.

Aquests models es poden utilitzar per validar les fonts de dades quan s’ingressen al pipeline de dades del teu projecte. Al seu torn, els teus models es converteixen en un catàleg centralitzat i fiable de tots els fets essencials sobre les teves dades, fets en els quals pots confiar durant el desenvolupament.

Crea un projecte nou i instal.la patito:

Terminal window
uv init patito-test
cd patito-test
uv add patito polars

Suposem que el teu projecte fa un seguiment de productes, i que aquests productes tenen quatre propietats principals:

  1. Un identificador numèric únic
  2. Un nom
  3. Una zona de temperatura ideal que pot ser "dry" (sec), "cold" (fred) o "frozen" (congelat)
  4. Una demanda del producte expressada com a percentatge de la previsió total de vendes per la següent setmana

En forma tabular, les dades podrien tenir aquest aspecte:

product_idnametemperature_zonedemand_percentage
1Appledry0.23%
2Milkcold0.61%
3Ice cubesfrozen0.01%

Ara comencem a modelar les restriccions que volem aplicar a les nostres dades.

A Patito, això es fa definint una classe que hereta de patito.Model, una classe que té una anotació de camp per a cada columna de les dades.

Aquests models s’haurien de definir preferiblement en un lloc centralitzat, convencionalment models.py, on els puguis trobar i consultar fàcilment.

Aquests models s’haurien de definir preferiblement en un lloc centralitzat, convencionalment models.py, on els puguis trobar i consultar fàcilme

models.py
from typing import Literal
import patito as pt
class Product(pt.Model):
product_id: int
name: str
temperature_zone: Literal["dry", "cold", "frozen"]
demand_percentage: float

Aquí hem utilitzat typing.Literal de la biblioteca estàndard per especificar que temperature_zone no és només un str, sinó específicament un dels valors literals "dry", "cold", o "frozen".

Ara pots utilitzar aquesta classe per representar una única instància específica d’un producte:

main.py
from models import Product
apple: Product = Product(product_id=1, name="Apple", temperature_zone="dry", demand_percentage=0.23)
print(apple)

La classe també ofereix automàticament validació de dades d’entrada, per exemple, si proporciones un valor invàlid per a temperature_zone:

from models import Product
pizza: Product = Product(product_id=64, name="Pizza", temperature_zone="oven", demand_percentage=0.12)
Terminal window
uv run main.py
Terminal window
pydantic_core._pydantic_core.ValidationError: 1 validation error for Product
temperature_zone
Input should be 'dry', 'cold' or 'frozen' [type=literal_error, input_value='oven', input_type=str]
For further information visit https://errors.pydantic.dev/2.9/v/literal_error

Potser has notat que això s’assembla sospitosament als models de dades de Pydantic, i això és perquè ho és!

La classe model de Patito està construïda sobre pydantic.BaseClass de pydantic i, per tant, ofereix tota la funcionalitat de pydantic.

Però la diferència és que Patito estén la validació de pydantic d’instàncies d’objectes singulars a col·leccions dels mateixos objectes representats com a dataframes.

Agafa aquestes dades i representa-les com un dataframe de polars:

from models import Product
import polars as pl
df = pl.DataFrame(
{
"product_id": [1, 2, 3],
"name": ["Apple", "Milk", "Ice cubes"],
"temperature_zone": ["dry", "cold", "frozen"],
"demand_percentage": [0.23, 0.61, 0.01],
}
)

Ara podem utilitzar Product.validate() per validar el contingut del nostre dataframe:

Product.validate(df)

Bé, això no ha estat gaire interessant…

El mètode validate simplement retorna el dataframe si no es troben errors.

Està pensat com una declaració de protecció per posar abans de qualsevol lògica que requereixi que les dades siguin vàlides. D’aquesta manera pots confiar que les dades són compatibles amb l’esquema del model donat, en cas contrari el mètode .validate() hauria llançat una excepció.

Provem-ho amb dades invàlides, establint la zona de temperatura d’un dels productes a "oven":

import polars as pl
from models import Product
from patito.exceptions import DataFrameValidationError
df = pl.DataFrame(
{
"product_id": [64, 64],
"name": ["Pizza", "Cereal"],
"temperature_zone": ["oven", "dry"],
"demand_percentage": [0.07, 0.16],
}
)
try:
Product.validate(df)
except DataFrameValidationError as e:
print(e)
Terminal window
uv run main.py
Terminal window
1 validation error for Product
temperature_zone
Rows with invalid values: {'oven'}. (type=value_error.rowvalue)

Ara sí que parlem!

Patito et permet definir una única classe que valida tant instàncies d’objectes singulars com col·leccions de dataframes sense duplicació de codi!

Same class definition

pydantic.BaseModel

------------------------------

Singular Instance Validation

patito.Model

------------------------------

Singular Instance Validation

+

DataFrame Validation

Patito intenta basar-se tant com sigui possible en els conceptes de modelatge existents de pydantic, estenent-los naturalment al domini dels dataframes quan és adequat. Els camps del model anotats amb str es mapejen a columnes de dataframe emmagatzemades com a pl.Utf8, int com a pl.Int8/pl.Int16/…/pl.Int64, i així successivament. Els tipus de camp envoltats en Optional permeten valors nuls, mentre que els tipus simples no ho fan.

Però certs conceptes de modelatge no són aplicables en el context d’instàncies d’objectes singulars i, per tant, necessàriament no formen part de l’API de pydantic.

Agafem product_id com a exemple, esperaríem que aquesta columna fos única entre tots els productes i, per tant, els duplicats s’haurien de considerar invàlids. A pydantic no tens manera d’expressar això, però Patito expandeix pydantic de diverses maneres per representar restriccions relacionades amb dataframes. Una d’aquestes extensions és el paràmetre unique acceptat per patito.Field, que et permet especificar que tots els valors d’una columna determinada han de ser únics.

import patito as pt
from typing import Literal
class Product(pt.Model):
product_id: int = pt.Field(unique=True)
name: str
temperature_zone: Literal["dry", "cold", "frozen"]
demand_percentage: float

La classe patito.Field accepta els mateixos paràmetres que pydantic.Field, però afegeix restriccions addicionals específiques per a dataframes documentades aquí. En aquells casos on les restriccions incorporades de Patito no són suficients, pots especificar restriccions arbitràries en forma d’expressions de polars que han d’avaluar-se com a True per a cada fila perquè el dataframe es consideri vàlid.

Suposem que volem assegurar-nos que demand_percentage suma 100% per a tot el dataframe, en cas contrari podríem estar perdent un o més productes. Podem fer-ho passant el paràmetre constraints a patito.Field:

class Product(pt.Model):
product_id: int = pt.Field(unique=True)
name: str
temperature_zone: Literal["dry", "cold", "frozen"]
demand_percentage: float = pt.Field(constraints=pt.field.sum() == 100.0)

Aquí patito.field és un àlies per a la columna del camp i es reemplaça automàticament amb polars.col("demand_percentage") abans de la validació.

Si ara utilitzem aquesta classe millorada per validar df, hauríem de detectar nous errors:

Terminal window
uv run main.py
Terminal window
3 validation errors for Product
product_id
2 rows with duplicated values. (type=value_error.rowvalue)
temperature_zone
Rows with invalid values: {'oven'}. (type=value_error.rowvalue)
demand_percentage
2 rows does not match custom constraints. (type=value_error.rowvalue)

Patito ha detectat ara que product_id conté duplicats i que demand_percentage no suma 100%!

Hi ha diverses propietats i mètodes més disponibles a patito.Model com s’indica aquí:

DataFrame

Model

Field

class Product(pt.Model):
# Do not allow duplicates
product_id: int = pt.Field(unique=True)
# Price must be stored as unsigned 16-bit integers
price: int = pt.Field(dtype=pl.UInt16)
# The product name should be from 3 to 128 characters long
name: str = pt.Field(min_length=3, max_length=128)
# Represent colors in the form of upper cased hex colors
#_color: str = pt.Field(regex=r"^\#[0-9A-F]{6}$")
Product.DataFrame(
{
"product_id": [1, 1],
"price": [400, 600],
"brand_color": ["#ab00ff", "AB00FF"],
}
).validate()

El contingut d'aquest lloc web té llicència CC BY-NC-ND 4.0.

©2022-2025 xtec.dev