Dall’avvento dei suggerimenti di tipo in Python 3.5, la digitazione statica di un DataFrame è stata generalmente limitata a specificare solo il tipo:
def process(f: DataFrame) -> Series: ...
Ciò è inadeguato poiché ignora i tipi contenuti nel contenitore. Un DataFrame potrebbe avere etichette di colonne di stringa e tre colonne di valori interi, stringhe e a virgola mobile; queste caratteristiche definiscono il tipo. Un argomento di funzione con tali suggerimenti di tipo fornisce agli sviluppatori, agli analizzatori statici e ai controllori di runtime tutte le informazioni necessarie per comprendere le aspettative dell’interfaccia. Telaio statico 2 (un progetto open source di cui sono sviluppatore capo) ora lo consente:
from typing import Any
from static_frame import Frame, Index, TSeriesAnydef process(f: Frame( # type of the container
Any, # type of the index labels
Index(np.str_), # type of the column labels
np.int_, # type of the first column
np.str_, # type of the second column
np.float64, # type of the third column
)) -> TSeriesAny: ...
Tutti i contenitori StaticFrame principali ora supportano specifiche generiche. Anche se controllabile staticamente, un nuovo decoratore, @CallGuard.check
consente la convalida in fase di esecuzione di questi suggerimenti di tipo sulle interfacce delle funzioni. Inoltre, utilizzando Annotated
generici, il nuovo Require
La classe definisce una famiglia di potenti validatori di runtime, consentendo controlli dei dati per colonna o per riga. Infine, ogni contenitore espone un nuovo file via_type_clinic
interfaccia per derivare e convalidare i suggerimenti sul tipo. Insieme, questi strumenti offrono un approccio coeso ai suggerimenti sul tipo e alla convalida dei DataFrames.
Requisiti di un DataFrame generico
I tipi generici incorporati di Python (ad esempio, tuple
O dict
) richiedono la specifica dei tipi di componenti (ad esempio, tuple(int, str, bool)
O dict(str, int)
). La definizione dei tipi di componente consente un’analisi statica più accurata. Anche se lo stesso vale per DataFrames, ci sono stati pochi tentativi di definire suggerimenti di tipo completi per DataFrames.
Panda, anche con il pandas-stubs
package, non consente di specificare i tipi dei componenti di un DataFrame. Il Pandas DataFrame, consentendo un’ampia mutazione sul posto, potrebbe non essere sensato da digitare staticamente. Fortunatamente, DataFrame immutabili sono disponibili in StaticFrame.
Inoltre, gli strumenti di Python per definire i generici, fino a poco tempo fa, non erano adatti ai DataFrames. Il fatto che un DataFrame abbia un numero variabile di tipi colonnari eterogenei rappresenta una sfida per le specifiche generiche. Digitare una struttura del genere è diventato più semplice con il nuovo TypeVarTuple
introdotto in Python 3.11 (e portato indietro in typing_extensions
pacchetto).
UN TypeVarTuple
consente di definire generici che accettano un numero variabile di tipi. (Vedere PEP 646 per i dettagli.) Con questa nuova variabile di tipo, StaticFrame può definire un generico Frame
con un TypeVar
per l’indice, a TypeVar
per le colonne e a TypeVarTuple
per zero o più tipi colonnari.
Un generico Series
è definito con a TypeVar
per l’indice e a TypeVar
per i valori. Il telaio statico Index
E IndexHierarchy
sono anche generici, di cui approfittano nuovamente questi ultimi TypeVarTuple
per definire un numero variabile di componenti Index
per ogni livello di profondità.
StaticFrame utilizza i tipi NumPy per definire i tipi colonnari di a Frame
o i valori di a Series
O Index
. Ciò consente di specificare in modo restrittivo tipi numerici di dimensioni, come ad esempio np.uint8
O np.complex128
; o specificando in modo ampio categorie di tipi, come ad esempio np.integer
O np.inexact
. Poiché StaticFrame supporta tutti i tipi NumPy, la corrispondenza è diretta.
Interfacce definite con dataframe generici
Estendendo l’esempio sopra, l’interfaccia delle funzioni di seguito mostra a Frame
con tre colonne trasformate in un dizionario di Series
. Con così tante informazioni in più fornite dai suggerimenti sul tipo di componente, lo scopo della funzione è quasi ovvio.
from typing import Any
from static_frame import Frame, Series, Index, IndexYearMonthdef process(f: Frame(
Any,
Index(np.str_),
np.int_,
np.str_,
np.float64,
)) -> dict(
int,
Series( # type of the container
IndexYearMonth, # type of the index labels
np.float64, # type of the values
),
): ...
Questa funzione elabora una tabella di segnali da un Prezzi degli asset open source (OSAP) dataset (Caratteristiche a livello aziendale/Individuo/Predittori). Ogni tabella ha tre colonne: identificatore di sicurezza (etichettato “permno”), anno e mese (etichettato “aaaamm”) e il segnale (con un nome specifico per il segnale).
La funzione ignora l’indice del fornito Frame
(digitato come Any
) e crea i gruppi definiti dalla prima colonna “permno” np.int_
valori. Viene restituito un dizionario con chiave “permno”, dove ogni valore è a Series
Di np.float64
valori per quel “permno”; l’indice è un IndexYearMonth
creato da np.str_
colonna “aaaamm”. (StaticFrame utilizza NumPy datetime64
valori per definire indici tipizzati in unità: IndexYearMonth
I negozi datetime64(M)
etichette.)
Piuttosto che restituire a dict
la funzione seguente restituisce a Series
con un indice gerarchico. IL IndexHierarchy
generico specifica un componente Index
per ogni livello di profondità; qui, la profondità esterna è an Index(np.int_)
(derivato dalla colonna “permno”), la profondità interna an IndexYearMonth
(derivato dalla colonna “aaaamm”).
from typing import Any
from static_frame import Frame, Series, Index, IndexYearMonth, IndexHierarchydef process(f: Frame(
Any,
Index(np.str_),
np.int_,
np.str_,
np.float64,
)) -> Series( # type of the container
IndexHierarchy( # type of the index labels
Index(np.int_), # type of index depth 0
IndexYearMonth), # type of index depth 1
np.float64, # type of the values
): ...
I suggerimenti di tipo avanzato forniscono un’interfaccia autodocumentante che rende esplicita la funzionalità. Ancora meglio, questi suggerimenti di tipo possono essere utilizzati per l’analisi statica con Pyright (ora) e Mypy (in attesa della versione completa TypeVarTuple
supporto). Ad esempio, chiamando questa funzione con a Frame
di due colonne di np.float64
fallirà un controllo del tipo di analisi statica o fornirà un avviso in un editor.
Convalida del tipo di runtime
Il controllo statico del tipo potrebbe non essere sufficiente: la valutazione runtime fornisce vincoli ancora più forti, in particolare per valori dinamici o con suggerimento di tipo incompleto (o errato).
Basandosi su un nuovo controllo del tipo di runtime denominato TypeClinic
StaticFrame 2 introduce @CallGuard.check
un decoratore per la convalida runtime delle interfacce con suggerimento di tipo. Sono supportati tutti i generici StaticFrame e NumPy e la maggior parte dei tipi Python incorporati sono supportati, anche se profondamente annidati. La funzione seguente aggiunge il file @CallGuard.check
decoratore.
from typing import Any
from static_frame import Frame, Series, Index, IndexYearMonth, IndexHierarchy, CallGuard@CallGuard.check
def 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 senza etichetta Frame
di due colonne di np.float64
UN ClinicError
verrà sollevata un’eccezione, illustrando che, dove erano previste tre colonne, ne sono state fornite due e dove erano previste etichette di colonna di tipo stringa, sono state fornite etichette intere. (Per emettere avvisi invece di sollevare eccezioni, utilizzare il file @CallGuard.warn
decoratore.)
ClinicError:
In args of (f: Frame(Any, Index(str_), int64, str_, float64)) -> Series(IndexHierarchy(Index(int64), IndexYearMonth), float64)
└── Frame(Any, Index(str_), int64, str_, float64)
└── Expected Frame has 3 dtype, provided Frame has 2 dtype
In args of (f: Frame(Any, Index(str_), int64, str_, float64)) -> Series(IndexHierarchy(Index(int64), IndexYearMonth), float64)
└── Frame(Any, Index(str_), int64, str_, float64)
└── Index(str_)
└── Expected str_, provided int64 invalid
Convalida dei dati di runtime
Altre caratteristiche possono essere convalidate in fase di esecuzione. Ad esempio, il shape
O name
attributi o la sequenza di etichette sull’indice o sulle colonne. Il telaio statico Require
fornisce una famiglia di validatori 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 di restituzione booleana al contenitore.
In linea con una tendenza in crescita, questi oggetti vengono forniti all’interno dei suggerimenti sul tipo come uno o più argomenti aggiuntivi a an Annotated
generico. (Vedere PEP 593 per i dettagli.) Il tipo a cui fa riferimento il primo Annotated
L’argomento è l’obiettivo dei validatori dell’argomento successivo. Ad esempio, se a Index(np.str_)
il suggerimento sul tipo viene sostituito con un Annotated(Index(np.str_), Require.Len(20))
tipo suggerimento, la convalida della lunghezza di runtime viene applicata all’indice associato al primo argomento.
Estendendo l’esempio dell’elaborazione di una tabella di segnali OSAP, potremmo convalidare la nostra aspettativa riguardo alle etichette delle colonne. IL Require.LabelsOrder
il validatore può definire una sequenza di etichette, facoltativamente utilizzando …
per regioni contigue di zero o più etichette non specificate. Per specificare che le prime due colonne della tabella portano la label “permno” e “aaaamm”, mentre la terza label è variabile (a seconda del segnale), quanto segue Require.LabelsOrder
può essere definito all’interno di un Annotated
generico:
from typing import Any, Annotated
from static_frame import Frame, Series, Index, IndexYearMonth, IndexHierarchy, CallGuard, Require@CallGuard.check
def 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 prevede una piccola raccolta di tabelle di segnali OSAP, possiamo convalidare la terza colonna con il file Require.LabelsMatch
validatore. Questo validatore può specificare le etichette richieste, i set di etichette (di cui almeno una deve corrispondere) e i modelli di espressione regolare. Se sono previste tabelle di soli tre file (ad esempio “Mom12m.csv”, “Mom6m.csv” e “LRreversal.csv”), possiamo convalidare le etichette della terza colonna definendo Require.LabelsMatch
con un insieme:
@CallGuard.check
def 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,
): ...
Entrambi Require.LabelsOrder
E Require.LabelsMatch
può associare funzioni con identificatori di etichetta per convalidare i valori dei dati. Se il validatore viene applicato alle etichette di colonna, a Series
dei valori delle colonne verranno forniti alla funzione; se il validatore viene applicato alle etichette indice, a Series
di valori di riga verranno forniti alla funzione.
Simile all’uso di Annotated
l’identificatore dell’etichetta viene sostituito con un elenco, dove il primo elemento è l’identificatore dell’etichetta e gli elementi rimanenti sono funzioni di elaborazione di righe o colonne che restituiscono un valore booleano.
Per estendere l’esempio precedente, potremmo convalidare che tutti i valori “permno” sono maggiori di zero e che tutti i valori del segnale (“Mom12m”, “Mom6m”, “LRreversal”) sono maggiori o uguali a -1.
from typing import Any, Annotated
from static_frame import Frame, Series, Index, IndexYearMonth, IndexHierarchy, CallGuard, Require@CallGuard.check
def 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 fallisce, @CallGuard.check
solleverà un’eccezione. Ad esempio, se la funzione precedente viene chiamata con a Frame
che ha un’etichetta di terza colonna inaspettata, 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
La forza espressiva di TypeVarTuple
Come mostrato sopra, TypeVarTuple
permette di specificare Frame
con zero o più tipi colonnari eterogenei. Ad esempio, possiamo fornire suggerimenti sul tipo per a Frame
di due float o sei tipologie miste:
>>> 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 ciò possa ospitare diversi DataFrames, DataFrames ampi con suggerimenti sul tipo, come quelli con centinaia di colonne, sarebbero ingombranti. Python 3.11 introduce una nuova sintassi per fornire una gamma variabile di tipi TypeVarTuple
generici: espressioni stellari di tuple
alias generici. Ad esempio, per digitare il suggerimento a Frame
con un indice di data, etichette di colonne di stringhe e qualsiasi configurazione di tipi di colonne, possiamo decomprimere a stella a tuple
pari a zero o più All
:
>>> from typing import Any
>>> from static_frame import Frame, Index>>> f: sf.Frame(Index(np.datetime64), Index(np.str_), *tuple(All, ...))
IL tuple
l’espressione star può essere posizionata ovunque in un elenco di tipi, ma può essercene solo uno. Ad esempio, il suggerimento sul tipo riportato di seguito definisce a Frame
che deve iniziare con colonne booleane e stringa ma ha una specifica flessibile per qualsiasi numero di colonne successive np.float64
colonne.
>>> from typing import Any
>>> from static_frame import Frame>>> f: sf.Frame(Any, Any, np.bool_, np.str_, *tuple(np.float64, ...))
Utilità per i suggerimenti sul tipo
Lavorare con suggerimenti di tipo così dettagliati può essere difficile. Per aiutare gli utenti, StaticFrame fornisce utili utilità per suggerimenti e controlli del tipo di runtime. Tutti i contenitori StaticFrame 2 ora dispongono di a via_type_clinic
interfaccia, consentendo l’accesso a TypeClinic
funzionalità.
Innanzitutto, vengono fornite le utilità per tradurre un contenitore, ad esempio un file complete Frame
in un suggerimento sul tipo. La rappresentazione di stringa di via_type_clinic
l’interfaccia fornisce una rappresentazione di stringa del suggerimento sul tipo del contenitore; in alternativa, il to_hint()
Il metodo restituisce un oggetto alias generico completo.
>>> import static_frame as sf
>>> f = sf.Frame.from_records(((3, '192004', 0.3), (3, '192005', -0.4)), columns=('permno', 'yyyymm', 'Mom3m'))>>> f.via_type_clinic
Frame(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 utilità per il test dei suggerimenti sul tipo di runtime. IL via_type_clinic.check()
la funzione consente di convalidare il contenitore rispetto a un suggerimento di tipo fornito.
>>> 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 digitazione graduale, StaticFrame definisce diversi alias generici configurati con Any
per ogni tipo di componente. Per esempio, TFrameAny
può essere utilizzato per qualsiasi Frame
E TSeriesAny
per ogni Series
. Come previsto, TFrameAny
convaliderà il Frame
creato sopra.
>>> f.via_type_clinic.check(sf.TFrameAny)
Conclusione
È in ritardo il suggerimento di tipo migliore per DataFrames. Con i moderni strumenti di digitazione Python e un DataFrame costruito su un modello di dati immutabili, StaticFrame 2 soddisfa questa esigenza, fornendo potenti risorse agli ingegneri che danno priorità alla manutenibilità e alla verificabilità.
Fonte: towardsdatascience.com