DataFrames con suggerimento di tipo per analisi statica e convalida runtime |  di Christopher Ariza |  Novembre 2023

 | Intelligenza-Artificiale

In che modo StaticFrame abilita suggerimenti completi sul tipo DataFrame

Un mosaico di vetro multicolore
Foto dell’autore

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, TSeriesAny

def 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.checkconsente 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 TypeVarTupleintrodotto 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 Frameo 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, IndexYearMonth

def 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 dictla 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, IndexHierarchy

def 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 TypeClinicStaticFrame 2 introduce @CallGuard.checkun 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.checkse la funzione sopra viene chiamata con un senza etichetta Frame di due colonne di np.float64UN 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 Annotatedl’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 Framein 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 FrameE 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

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *