Type-Hinting DataFrames per l’analisi statica e la validazione in fase di esecuzione
Type-Hinting DataFrames per un'analisi statica e una validazione in fase di esecuzione efficaci
Come StaticFrame consente Hint di Tipi Completi per DataFrame
Dal momento dell’introduzione degli hint di tipo in Python 3.5, la definizione statica di un DataFrame era generalmente limitata alla specifica del solo tipo:
def process(f: DataFrame) -> Series: ...
Ciò è inadeguato, poiché ignora i tipi contenuti all’interno del contenitore. Un DataFrame potrebbe avere etichette di colonna di tipo stringa e tre colonne di valori intero, stringa e punto decimale; queste caratteristiche definiscono il tipo. Un argomento di funzione con tali hint di tipo fornisce agli sviluppatori, agli analizzatori statici e ai verificatori in esecuzione tutte le informazioni necessarie per comprendere le aspettative dell’interfaccia. StaticFrame 2 (un progetto open-source di cui sono il principale sviluppatore) ora permette questo:
from typing import Anyfrom static_frame import Frame, Index, TSeriesAnydef process(f: Frame[ # tipo del contenitore Any, # tipo delle etichette di indice Index[np.str_], # tipo delle etichette di colonna np.int_, # tipo della prima colonna np.str_, # tipo della seconda colonna np.float64, # tipo della terza colonna ]) -> TSeriesAny: ...
Ora tutti i contenitori core di StaticFrame supportano specifiche generiche. Pur essendo staticamente verificabili, un nuovo decorators, @CallGuard.check
, consente la validazione in fase di esecuzione di questi hint di tipo sulle interfacce delle funzioni. Inoltre, utilizzando i generics Annotated
, il nuovo Require
class definisce una famiglia di validatori di runtime potenti, consentendo controlli dati per colonna o riga. Infine, ogni contenitore espone una nuova interfaccia via_type_clinic
per derivare e validare gli hint di tipo. Insieme, questi strumenti offrono un approccio coeso per l’individuazione dei tipi e la validazione dei DataFrames.
Requisiti di un DataFrame Generico
I tipi generici integrati di Python (ad esempio, tuple
o dict
) richiedono la specifica dei tipi dei componenti (ad esempio, tuple[int, str, bool]
o dict[str, int]
). La definizione dei tipi dei componenti consente un’analisi statica più accurata. Anche se lo stesso vale per i DataFrames, sono stati fatti pochi tentativi per definire hint di tipo completi per i DataFrames.
- L’altro lato dei contratti dati risvegliare la responsabilità del consumatore
- Tre modi incredibili per innovare la tua azienda
- La semantica delle diverse tecniche SCD2
Pandas, anche con il pacchetto pandas-stubs
, non permette di specificare i tipi dei componenti di un DataFrame. Il DataFrame di Pandas, che consente una vasta mutabilità sul posto, potrebbe non essere sensibile alla tipizzazione statica. Fortunatamente, i DataFrames immutabili sono disponibili in StaticFrame.
Inoltre, gli strumenti forniti da Python per la definizione di generics, fino a poco tempo fa, non erano adatti ai DataFrames. Il fatto che un DataFrame abbia un numero variabile di tipi di colonne eterogenee rappresenta una sfida per la specifica generica. La specifica di una struttura del genere è diventata più semplice con il nuovo tipo di variabile TypeVarTuple
, introdotto in Python 3.11 (e riportato nella libreria typing_extensions
).
Un TypeVarTuple
consente di definire generics che accettano un numero variabile di tipi. (Vedi PEP 646 per i dettagli.) Con questa nuova variabile di tipo, StaticFrame può definire un generico Frame
con un TypeVar
per l’indice, un TypeVar
per le colonne e un TypeVarTuple
per zero o più tipi di colonne.
Un generico Series
è definito con un TypeVar
per l’indice e un TypeVar
per i valori. Anche Index
e IndexHierarchy
di StaticFrame sono generici, quest’ultimo sfrutta nuovamente TypeVarTuple
per definire un numero variabile di componenti Index
per ciascun livello di profondità.
StaticFrame utilizza tipi di NumPy per definire i tipi di colonne di un Frame
, o i valori di un Series
o Index
. Ciò consente di specificare in modo preciso tipi numerici dimensionati, come np.uint8
o np.complex128
; o di specificare ampiamente categorie di tipi, come np.integer
o np.inexact
. Poiché StaticFrame supporta tutti i tipi di NumPy, la corrispondenza è diretta.
Interfacce definite con DataFrame generici
Estendendo l’esempio sopra, l’interfaccia di funzione sottostante mostra un Frame
con tre colonne trasformate in un dizionario di Serie
. Con molte più informazioni fornite dai suggerimenti sul tipo di componente, lo scopo della funzione è quasi ovvio.
from typing import Anyfrom static_frame import Frame, Series, Index, IndexYearMonthdef process(f: Frame[ Any, Index[np.str_], np.int_, np.str_, np.float64, ]) -> dict[ int, Series[ # tipo del contenitore IndexYearMonth, # tipo delle etichette dell'indice np.float64, # tipo dei valori ], ]: ...
Questa funzione elabora una tabella di segnali da un dataset di Asset Pricing Open Source (OSAP) (Caratteristiche a livello di azienda / Individuale / Previsioni). Ogni tabella ha tre colonne: identificativo di sicurezza (etichettato come “permno”), anno e mese (etichettato come “yyyymm”), e il segnale (con un nome specifico per il segnale).
La funzione ignora l’indice del Frame
fornito (di tipo Any
) e crea gruppi definiti dalla prima colonna “permno” con valori di tipo np.int_
. Viene restituito un dizionario indicizzato per “permno”, dove ogni valore è una Serie
di valori di tipo np.float64
per quel “permno”; l’indice è un IndexYearMonth
creato dalla colonna np.str_
“yyyymm”. (StaticFrame utilizza valori di tipo datetime64
di NumPy per definire indici di tipo unità: IndexYearMonth
memorizza etichette di tipo datetime64[M]
.)
Invece di restituire un dict
, la funzione sottostante restituisce una Serie
con un indice gerarchico. Il generico IndexHierarchy
specifica un Index
di componente per ogni livello di profondità; in questo caso, la profondità esterna è un Index[np.int_]
(derivato dalla colonna “permno”), la profondità interna è un IndexYearMonth
(derivato dalla colonna “yyyymm”).
from typing import Anyfrom static_frame import Frame, Series, Index, IndexYearMonth, IndexHierarchydef process(f: Frame[ Any, Index[np.str_], np.int_, np.str_, np.float64, ]) -> Series[ # tipo del contenitore IndexHierarchy[ # tipo delle etichette dell'indice Index[np.int_], # tipo dell'indice per profondità 0 IndexYearMonth], # tipo dell'indice per profondità 1 np.float64, # tipo dei valori ]: ...
I suggerimenti sul tipo forniscono un’interfaccia auto-documentante che rende esplicita la funzionalità. Ancora meglio, questi suggerimenti sul tipo possono essere utilizzati per l’analisi statica con Pyright (ora) e Mypy (in attesa del pieno supporto di TypeVarTuple
). Ad esempio, chiamare questa funzione con un Frame
di due colonne di tipo np.float64
genererà un errore di tipo nel controllo dell’analisi statica o fornirà un avviso in un editor.
Convalida del tipo in fase di esecuzione
La verifica statica del tipo potrebbe non essere sufficiente: la valutazione in fase di runtime fornisce vincoli ancora più forti, specialmente per valori dinamici o per valori con suggerimenti di tipo incompleti (o errati).
Basandosi su un nuovo checker del tipo in fase di runtime chiamato TypeClinic
, StaticFrame 2 introduce @CallGuard.check
, un decoratore per la convalida in fase di runtime delle interfacce suggerite dal tipo. Sono supportati tutti i generici di StaticFrame e NumPy, e la maggior parte dei tipi incorporati di Python, anche quando sono annidati in modo profondo. La funzione sottostante aggiunge il decoratore @CallGuard.check
.
from typing import Anyfrom static_frame import Frame, Series, Index, IndexYearMonth, IndexHierarchy, [email protected] process(f: Frame[ Any, Index[np.str_], np.int_, np.str_, np.float64, ]) -> Series[ IndexHierarchy[Index[np.int_], IndexYearMonth], np.float64, ]: ...
Ora decorato con @CallGuard.check
, se la funzione sopra viene chiamata con un Frame
non etichettato di due colonne di np.float64
, verrà sollevata un’eccezione ClinicError
, illustrando che, dove ci si aspettava tre colonne, ne sono state fornite due, e dove si aspettavano etichette di colonne di tipo stringa, sono state fornite etichette di tipo intero. (Per emettere avvisi invece di sollevare eccezioni, utilizzare il decoratore @CallGuard.warn
.)
ClinicError: negli argomenti di (f: Frame[Any, Index[str_], int64, str_, float64]) -> Series[IndexHierarchy[Index[int64], IndexYearMonth], float64]└── Frame[Any, Index[str_], int64, str_, float64] └── Il Frame atteso ha 3 dtypes, il Frame fornito ha 2 dtypes negli argomenti di (f: Frame[Any, Index[str_], int64, str_, float64]) -> Series[IndexHierarchy[Index[int64], IndexYearMonth], float64]└── Frame[Any, Index[str_], int64, str_, float64] └── Index[str_] └── Previsto str_, fornito int64 non valido
Validazione dei dati in fase di esecuzione
Altre caratteristiche possono essere convalidate in fase di esecuzione. Ad esempio, gli attributi shape
o name
, o la sequenza di etichette sull’indice o sulle colonne. La classe Require
di StaticFrame fornisce una famiglia di convalidatori configurabili.
Require.Name
: Convalida l’attributo “name” del contenitore.Require.Len
: Convalida la lunghezza del contenitore.Require.Shape
: Convalida l’attributo “shape” del contenitore.Require.LabelsOrder
: Convalida l’ordine delle etichette.Require.LabelsMatch
: Convalida l’inclusione delle etichette indipendentemente dall’ordine.Require.Apply
: Applica una funzione che restituisce un booleano al contenitore.
In linea con una tendenza in crescita, questi oggetti vengono forniti all’interno degli aiuti di tipo come uno o più argomenti aggiuntivi ad un generico Annotated
. (Vedere PEP 593 per i dettagli.) Il tipo citato dal primo argomento Annotated
è il destinatario dei convalidatori che seguono come argomenti successivi. Ad esempio, se si sostituisce un suggerimento di tipo Index[np.str_]
con un suggerimento di tipo Annotated[Index[np.str_], Require.Len(20)]
, la convalida della lunghezza in fase di esecuzione viene applicata all’indice associato al primo argomento.
Estendendo l’esempio di elaborazione di una tabella di segnali OSAP, potremmo convalidare le nostre aspettative per le etichette di colonna. Il convalidatore Require.LabelsOrder
può definire una sequenza di etichette, utilizzando eventualmente ...
per regioni contigue di etichette non specificate o più specifiche. Per specificare che le prime due colonne della tabella sono etichettate “permno” e “yyyymm”, mentre la terza etichetta è variabile (a seconda del segnale), il seguente Require.LabelsOrder
può essere definito all’interno di un generico Annotated
:
from typing import Any, Annotatedfrom static_frame import Frame, Series, Index, IndexYearMonth, IndexHierarchy, CallGuard, [email protected] process(f: Frame[ Any, Annotated[ Index[np.str_], Require.LabelsOrder('permno', 'yyyymm', ...), ], np.int_, np.str_, np.float64, ]) -> Series[ IndexHierarchy[Index[np.int_], IndexYearMonth], np.float64, ]: ...
Se l’interfaccia si aspetta una piccola collezione di tabelle di segnali OSAP, possiamo convalidare la terza colonna con il convalidatore Require.LabelsMatch
. Questo convalidatore può specificare le etichette richieste, insiemi di etichette (da cui almeno una deve corrispondere) e pattern di espressioni regolari. Se ci si aspetta solo tabelle da tre file (ossia “Mom12m.csv”, “Mom6m.csv” e “LRreversal.csv”), possiamo convalidare le etichette della terza colonna definendo Require.LabelsMatch
con un insieme:
@CallGuard.checkdef process(f: Frame[ Any, Annotated[ Index[np.str_], Require.LabelsOrder('permno', 'yyyymm', ...), Require.LabelsMatch({'Mom12m', 'Mom6m', 'LRreversal'}), ], np.int_, np.str_, np.float64, ]) -> Series[ IndexHierarchy[Index[np.int_], IndexYearMonth], np.float64, ]: ...
Sia Require.LabelsOrder
che Require.LabelsMatch
possono associare funzioni alle specifiche dei label per convalidare i valori dei dati. Se il convalidatore viene applicato ai label delle colonne, una Series
dei valori delle colonne sarà fornita alla funzione; se il convalidatore viene applicato ai label degli indici, una Series
dei valori delle righe sarà fornita alla funzione.
Similmente all’uso di Annotated
, il label specifier viene sostituito con una lista in cui il primo elemento è il label specifier, e i rimanenti elementi sono le funzioni di elaborazione delle righe o delle colonne che restituiscono un valore booleano.
Per estendere l’esempio precedente, potremmo convalidare che tutti i valori “permno” siano maggiori di zero e che tutti i valori del segnale (“Mom12m”, “Mom6m”, “LRreversal”) siano maggiori o uguali a -1.
from typing import Any, Annotatedfrom static_frame import Frame, Series, Index, IndexYearMonth, IndexHierarchy, CallGuard, [email protected] process(f: Frame[ Any, Annotated[ Index[np.str_], Require.LabelsOrder( ['permno', lambda s: (s > 0).all()], 'yyyymm', ..., ), Require.LabelsMatch( [{'Mom12m', 'Mom6m', 'LRreversal'}, lambda s: (s >= -1).all()], ), ], np.int_, np.str_, np.float64, ]) -> Series[ IndexHierarchy[Index[np.int_], IndexYearMonth], np.float64, ]: ...
Se una convalida non riesce, @CallGuard.check
solleverà un’eccezione. Ad esempio, se la funzione precedente viene chiamata con un Frame
che ha un inaspettato label nella terza colonna, verrà sollevata la seguente eccezione:
ClinicError:In args of (f: Frame[Any, Annotated[Index[str_], LabelsOrder(['permno', <lambda>], 'yyyymm', ...), LabelsMatch([{'Mom12m', 'LRreversal', 'Mom6m'}, <lambda>])], int64, str_, float64]) -> Series[IndexHierarchy[Index[int64], IndexYearMonth], float64]└── Frame[Any, Annotated[Index[str_], LabelsOrder(['permno', <lambda>], 'yyyymm', ...), LabelsMatch([{'Mom12m', 'LRreversal', 'Mom6m'}, <lambda>])], int64, str_, float64] └── Annotated[Index[str_], LabelsOrder(['permno', <lambda>], 'yyyymm', ...), LabelsMatch([{'Mom12m', 'LRreversal', 'Mom6m'}, <lambda>])] └── LabelsMatch([{'Mom12m', 'LRreversal', 'Mom6m'}, <lambda>]) └── Expected label to match frozenset({'Mom12m', 'LRreversal', 'Mom6m'}), no provided match
Il Potere Espressivo di TypeVarTuple
Come mostrato precedentemente, TypeVarTuple
permette di specificare un Frame
con zero o più tipi di colonne eterogenee. Ad esempio, possiamo fornire suggerimenti di tipo per un Frame
con due tipi float o sei tipi misti:
>>> from typing import Any>>> from static_frame import Frame, Index>>> f1: sf.Frame[Any, Any, np.float64, np.float64]>>> f2: sf.Frame[Any, Any, np.bool_, np.float64, np.int8, np.int8, np.str_, np.datetime64]
Sebbene questo permetta l’inclusione di DataFrames diversi, dare suggerimenti di tipo a DataFrames larghi, come quelli con centinaia di colonne, sarebbe complicato. Python 3.11 introduce una nuova sintassi per fornire un insieme variabile di tipi nei generici di TypeVarTuple
: espressioni di asterischi per gli alias generici di tuple
. Ad esempio, per dare suggerimenti di tipo a un Frame
con un indice di data, label delle colonne di tipo stringa e qualsiasi configurazione di tipi colonnari, possiamo scompattare una tuple
di zero o più All
:
>>> da typing import Any>>> import static_frame as sf>>> from static_frame import Frame, Index>>> f: sf.Frame[Index[np.datetime64], Index[np.str_], *tuple[All, ...]]
L’espressione stella tuple
può essere inserita ovunque in un elenco di tipi, ma ne può essercene solo uno. Ad esempio, l’indicazione di tipo di seguito definisce un Frame
che deve iniziare con colonne Booleane e stringhe ma ha una specifica flessibile per qualsiasi numero di colonne np.float64
successive.
>>> from typing import Any>>> from static_frame import Frame>>> f: sf.Frame[Any, Any, np.bool_, np.str_, *tuple[np.float64, ...]]
Utility per l’indicazione dei tipi
Lavorare con indicazioni di tipo così dettagliate può essere difficile. Per aiutare gli utenti, StaticFrame fornisce utility comode per indicazioni di tipo ed il controllo durante l’esecuzione. Tutti i contenitori di StaticFrame 2 presentano un’interfaccia via_type_clinic
, che permette una vasta funzionalità di TypeClinic
.
Innanzitutto, vengono fornite utility per tradurre un contenitore, ad esempio un Frame
completo, in un’indicazione di tipo. La rappresentazione in stringa dell’interfaccia via_type_clinic
fornisce una rappresentazione in stringa dell’indicazione di tipo del contenitore; in alternativa, il metodo to_hint()
restituisce un oggetto alias generico completo.
>>> f = sf.Frame.from_records(([3, '192004', 0.3], [3, '192005', -0.4]), columns=('permno', 'yyyymm', 'Mom3m'))>>> f.via_type_clinicFrame[Index[int64], Index[str_], int64, str_, float64]>>> f.via_type_clinic.to_hint()static_frame.core.frame.Frame[static_frame.core.index.Index[numpy.int64], static_frame.core.index.Index[numpy.str_], numpy.int64, numpy.str_, numpy.float64]
In secondo luogo, vengono fornite utility per il test di indicazione di tipo durante l’esecuzione. La funzione via_type_clinic.check()
permette di convalidare il contenitore rispetto ad una specifica indicazione di tipo fornita.
>>> f.via_type_clinic.check(sf.Frame[sf.Index[np.str_], sf.TIndexAny, *tuple[tp.Any, ...]])ClinicError:In Frame[Index[str_], Index[Any], Unpack[Tuple[Any, ...]]]└── Index[str_] └── Expected str_, provided int64 invalid
Per supportare la scrittura graduale del codice, StaticFrame definisce diversi alias generici configurati con Any
per ogni tipo di componente. Ad esempio, TFrameAny
può essere utilizzato per qualsiasi Frame
, e TSeriesAny
per qualsiasi Series
. Come previsto, TFrameAny
convalida il Frame
creato in precedenza.
>>> f.via_type_clinic.check(sf.TFrameAny)
Conclusione
È giunto il momento di avere indicazioni di tipo migliori per i DataFrames. Con gli strumenti di indicazione dei tipi di Python moderno e un DataFrame basato su un modello dati immutabile, StaticFrame 2 soddisfa questa esigenza, fornendo risorse potenti per gli ingegneri che danno priorità alla manutenibilità e alla verificabilità.