Type-Hinting DataFrames para análisis estático y validación en tiempo de ejecución
DataFrames con indicación de tipo para análisis estático y validación en tiempo de ejecución
Cómo StaticFrame habilita sugerencias de tipo integrales para DataFrame
![Foto por Autor](https://ai.miximages.com/miro.medium.com/v2/resize:fit:640/format:webp/1*j3DzqTntN1BHssIoBNibFw.jpeg)
Desde la llegada de las sugerencias de tipo en Python 3.5, tipar estáticamente un DataFrame se ha limitado generalmente a especificar solo el tipo:
def procesar(f: DataFrame) -> Series: ...
Esto es insuficiente, ya que ignora los tipos contenidos dentro del contenedor. Un DataFrame podría tener etiquetas de columna de tipo cadena y tres columnas de valores enteros, cadenas y de punto flotante; estas características definen el tipo. Un argumento de función con este tipo de sugerencias de tipo proporciona a los desarrolladores, analizadores estáticos y verificadores de tiempo de ejecución toda la información necesaria para comprender las expectativas de la interfaz. StaticFrame 2 (un proyecto de código abierto del cual soy el desarrollador principal) ahora permite esto:
from typing import Anyfrom static_frame import Frame, Index, TSeriesAnydef process(f: Frame[ # tipo del contenedor Any, # tipo de las etiquetas de índice Index[np.str_], # tipo de las etiquetas de las columnas np.int_, # tipo de la primera columna np.str_, # tipo de la segunda columna np.float64, # tipo de la tercera columna ]) -> TSeriesAny: ...
Todos los contenedores principales de StaticFrame ahora admiten especificaciones genéricas. Mientras son estáticamente comprobables, un nuevo decorador, @CallGuard.check
, permite la validación en tiempo de ejecución de estas sugerencias de tipo en las interfaces de función. Además, utilizando genéricos Annotated
, la nueva clase Require
define una familia de validadores de tiempo de ejecución poderosos, que permiten verificar datos por columna o por fila. Por último, cada contenedor expone una nueva interfaz via_type_clinic
para derivar y validar sugerencias de tipo. En conjunto, estas herramientas ofrecen un enfoque cohesivo para la sugerencia de tipos y la validación de DataFrames.
Requisitos de un DataFrame genérico
Los tipos genéricos integrados de Python (por ejemplo, tuple
o dict
) requieren especificación de los tipos de componentes (por ejemplo, tuple[int, str, bool]
o dict[str, int]
). Definir los tipos de componentes permite un análisis estático más preciso. Si bien lo mismo es cierto para los DataFrames, ha habido pocos intentos de definir sugerencias de tipo integrales para los DataFrames.
- El Otro Lado de los Contratos de Datos Despertando la Responsabilidad del Consumidor
- Tres formas increíbles de innovar tu negocio
- Investigadores de UCLA presentan ‘Reformular y Responder’ (RaR) un nuevo método de inteligencia artificial que mejora la comprensión de las LLMs de las preguntas humanas
Pandas, incluso con el paquete pandas-stubs
, no permite especificar los tipos de los componentes de un DataFrame. El DataFrame de Pandas, que permite una mutación extensa en el lugar, puede no ser apto para ser tipado estáticamente. Afortunadamente, en StaticFrame existen DataFrames inmutables.
Además, las herramientas de Python para definir genéricos, hasta hace poco, no eran adecuadas para los DataFrames. El hecho de que un DataFrame tenga un número variable de tipos columnares heterogéneos plantea un desafío para la especificación genérica. La tipificación de una estructura así se hizo más fácil con el nuevo tipo variable TypeVarTuple
, introducido en Python 3.11 (y retroportado en el paquete typing_extensions
).
Un TypeVarTuple
permite definir genéricos que aceptan un número variable de tipos. (Ver PEP 646 para más detalles). Con esta nueva variable de tipo, StaticFrame puede definir un Frame
genérico con un TypeVar
para el índice, un TypeVar
para las columnas y un TypeVarTuple
para cero o más tipos columnares.
Un Series
genérico se define con un TypeVar
para el índice y un TypeVar
para los valores. El Index
y el IndexHierarchy
de StaticFrame también son genéricos, este último aprovechando nuevamente TypeVarTuple
para definir un número variable de componentes Index
para cada nivel de profundidad.
StaticFrame utiliza los tipos de NumPy para definir los tipos columnares de un Frame
, o los valores de un Series
o Index
. Esto permite especificar estrechamente tipos numéricos de tamaño, como np.uint8
o np.complex128
; o especificar ampliamente categorías de tipos, como np.integer
o np.inexact
. Dado que StaticFrame admite todos los tipos de NumPy, la correspondencia es directa.
Interfaces Definidas con DataFrames Genéricos
Extendiendo el ejemplo anterior, la interfaz de función a continuación muestra un Frame
con tres columnas transformadas en un diccionario de Series
. Con tanta información proporcionada por las sugerencias de tipos de componentes, el propósito de la función es casi obvio.
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 contenedor IndexYearMonth, # tipo de las etiquetas de índice np.float64, # tipo de los valores ], ]: ...
Esta función procesa una tabla de señales de un conjunto de datos accionarios de Nivel de Empresa / Individual / Predictores de Precios de Activos de código abierto. Cada tabla tiene tres columnas: identificador de seguridad (etiquetado como “permno”), año y mes (etiquetado como “yyyymm”), y la señal (con un nombre específico para la señal).
La función ignora el índice del Frame
proporcionado (de tipo Any
) y crea grupos definidos por la primera columna “permno” con valores np.int_
. Se devuelve un diccionario indexado por “permno”, donde cada valor es una Serie
de valores np.float64
para ese “permno”; el índice es un IndexYearMonth
creado a partir de la columna np.str_
“yyyymm”. (StaticFrame utiliza valores datetime64
de NumPy para definir índices de tipo unidad: IndexYearMonth
almacena etiquetas datetime64[M]
.)
En lugar de devolver un dict
, la función a continuación devuelve una Serie
con un índice jerárquico. El genérico IndexHierarchy
especifica un componente Index
para cada nivel de profundidad; aquí, la profundidad externa es un Index[np.int_]
(derivado de la columna “permno”), la profundidad interna es un IndexYearMonth
(derivado de la columna “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 contenedor IndexHierarchy[ # tipo de las etiquetas de índice Index[np.int_], # tipo de la profundidad de índice 0 IndexYearMonth], # tipo de la profundidad de índice 1 np.float64, # tipo de los valores ]: ...
Las sugerencias de tipos completas proporcionan una interfaz auto documentada que hace que la funcionalidad sea explícita. Aún mejor, estas sugerencias de tipos se pueden utilizar para análisis estático con Pyright (ahora) y Mypy (pendiente de un soporte completo de TypeVarTuple
). Por ejemplo, llamar a esta función con un Frame
de dos columnas de np.float64
generará un error en la verificación de tipos del análisis estático o mostrará una advertencia en un editor.
Validación de Tipos en Tiempo de Ejecución
La comprobación de tipos estáticos puede no ser suficiente: la evaluación en tiempo de ejecución proporciona restricciones aún más sólidas, especialmente para valores dinámicos o con sugerencias de tipos incompletas (o incorrectas).
Basado en un nuevo comprobador de tipos en tiempo de ejecución llamado TypeClinic
, StaticFrame 2 presenta @CallGuard.check
, un decorador para la validación en tiempo de ejecución de interfaces con sugerencias de tipos. Se admiten todos los genéricos de StaticFrame y NumPy, y la mayoría de los tipos incorporados de Python también son compatibles, incluso cuando están anidados en varios niveles. La función a continuación agrega el decorador @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, ]: ...
Ahora decorado con @CallGuard.check
, si la función anterior se llama con un Frame
sin etiquetas de dos columnas de np.float64
, se generará una excepción ClinicError
, ilustrando que se esperaban tres columnas y se proporcionaron dos, y se esperaban etiquetas de columna de tipo string, pero se proporcionaron etiquetas de tipo entero. (Para emitir advertencias en lugar de generar excepciones, use el decorador @CallGuard.warn
.)
ClinicError:En args of (f: Frame[Any, Index[str_], int64, str_, float64]) -> Series[IndexHierarchy[Index[int64], IndexYearMonth], float64]└── Frame[Any, Index[str_], int64, str_, float64] └── Se esperaban 3 dtypes para el Frame, pero el Frame proporcionado tiene 2 dtypesIn 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_] └── Se esperaba str_, pero se proporcionó int64 inválido
Validación de datos en tiempo de ejecución
Otras características pueden validarse en tiempo de ejecución. Por ejemplo, los atributos shape
o name
, o la secuencia de etiquetas en el índice o las columnas. La clase Require
de StaticFrame proporciona una familia de validadores configurables.
Require.Name
: Valida el atributo de “name” del contenedor.Require.Len
: Valida la longitud del contenedor.Require.Shape
: Valida el atributo de “shape” del contenedor.Require.LabelsOrder
: Valida el orden de las etiquetas.Require.LabelsMatch
: Valida la inclusión de etiquetas independientemente del orden.Require.Apply
: Aplica una función booleana al contenedor.
En consonancia con una tendencia creciente, estos objetos se proporcionan en sugerencias de tipos como uno o más argumentos adicionales a un genérico Annotated
. (Consulte PEP 593 para más detalles.) El tipo al que hace referencia el primer argumento Annotated
es el objetivo de los siguientes validadores de argumentos. Por ejemplo, si una sugerencia de tipo Index[np.str_]
se reemplaza por una sugerencia de tipo Annotated[Index[np.str_], Require.Len(20)]
, se aplicará una validación de longitud en tiempo de ejecución al índice asociado con el primer argumento.
Extendiendo el ejemplo de procesar una tabla de señales OSAP, podemos validar nuestras expectativas de las etiquetas de columna. El validador Require.LabelsOrder
puede definir una secuencia de etiquetas, utilizando opcionalmente ...
para regiones contiguas de cero o más etiquetas no especificadas. Para especificar que las dos primeras columnas de la tabla están etiquetadas como “permno” y “yyyymm”, mientras que la tercera etiqueta es variable (dependiendo de la señal), se puede definir el siguiente Require.LabelsOrder
dentro de un genérico 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, ]: ...
Si la interfaz espera una pequeña colección de tablas de señales OSAP, podemos validar la tercera columna con el validador Require.LabelsMatch
. Este validador puede especificar etiquetas requeridas, conjuntos de etiquetas (de los cuales al menos una debe coincidir) y patrones de expresión regular. Si solo se esperan tablas de tres archivos (es decir, “Mom12m.csv”, “Mom6m.csv” y “LRreversal.csv”), se puede validar las etiquetas de la tercera columna definiendo Require.LabelsMatch
con un conjunto:
@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, ]: ...
Ambos Require.LabelsOrder
y Require.LabelsMatch
pueden asociar funciones con especificadores de etiquetas para validar valores de datos. Si el validador se aplica a etiquetas de columna, se proporcionará una Serie
de valores de columna a la función; si el validador se aplica a etiquetas de índice, se proporcionará una Serie
de valores de fila a la función.
Similar al uso de Annotated
, el especificador de etiqueta se reemplaza por una lista, donde el primer elemento es el especificador de etiqueta y los demás elementos son funciones de procesamiento de filas o columnas que devuelven un valor booleano.
Para ampliar el ejemplo anterior, podríamos validar que todos los valores de “permno” sean mayores que cero y que todos los valores de señal (“Mom12m”, “Mom6m”, “LRreversal”) sean mayores o iguales 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, ]: ...
Si una validación falla, @CallGuard.check
generará una excepción. Por ejemplo, si se llama a la función anterior con un Frame
que tiene una etiqueta de tercera columna inesperada, se generará la siguiente excepción:
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>]) └── Se esperaba que la etiqueta coincidiera con el conjun
>>> from typing import Any >>> from static_frame import Frame, Index >>> f: sf.Frame[Index[np.datetime64], Index[np.str_], *tuple[All, ...]]
La expresión de estrella
tuple
puede ir en cualquier lugar de una lista de tipos, pero solo puede haber una. Por ejemplo, la pista de tipo a continuación define unFrame
que debe comenzar con columnas booleanas y de cadena pero tiene una especificación flexible para cualquier número de columnasnp.float64
posteriores.>>> from typing import Any >>> from static_frame import Frame >>> f: sf.Frame[Any, Any, np.bool_, np.str_, *tuple[np.float64, ...]]
Utilidades para la sugerencia de tipos
Trabajar con pistas de tipo tan detalladas puede ser desafiante. Para ayudar a los usuarios, StaticFrame proporciona utilidades convenientes para la sugerencia de tipos y la verificación en tiempo de ejecución. Todas las contenedores de StaticFrame 2 ahora cuentan con una interfaz
via_type_clinic
, lo que permite acceder a la funcionalidad deTypeClinic
.Primero, se proporcionan utilidades para traducir un contenedor, como un
Frame
completo, en una pista de tipo. La representación de cadena de la interfazvia_type_clinic
proporciona una representación de cadena de la pista de tipo del contenedor; alternativamente, el métodoto_hint()
devuelve un objeto de alias genérico 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_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]
En segundo lugar, se proporcionan utilidades para la prueba de sugerencia de tipos en tiempo de ejecución. La función
via_type_clinic.check()
permite validar el contenedor con una pista de tipo proporcionada.>>> 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_] └── Se esperaba str_, se proporcionó int64 inválido
Para admitir la escritura gradual, StaticFrame define varios alias genéricos configurados con
Any
para cada tipo de componente. Por ejemplo,TFrameAny
se puede usar para cualquierFrame
, yTSeriesAny
para cualquierSeries
. Como era de esperar,TFrameAny
validará elFrame
creado anteriormente.>>> f.via_type_clinic.check(sf.TFrameAny)
Conclusión
La sugerencia de tipos mejorada para DataFrames se ha demorado. Con las herramientas modernas de escritura de Python y un DataFrame construido sobre un modelo de datos inmutable, StaticFrame 2 satisface esta necesidad, proporcionando recursos potentes para los ingenieros que priorizan la mantenibilidad y verificabilidad.
We will continue to update Zepes; if you have any questions or suggestions, please contact us!
Was this article helpful?
93 out of 132 found this helpful
Related articles
- 6 Problemas de LLMs que LangChain está tratando de evaluar
- Prediciendo Touchdowns de Futbol Americano con Aprendizaje Automático
- Explorando Google Cloud Platform Una Visión General Completa de Servicios y Capacidades
- Detrás de Ghostbuster El nuevo método de la Universidad de Berkeley para detectar contenido generado por IA
- Optimizando el análisis de datos Integrando GitHub Copilot en Databricks
- El papel proactivo de la IA en el combate a la corrupción en el gobierno
- Una Guía Completa para la División de Entrenamiento-Prueba-Validación en 2023