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

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.

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 un Frame que debe comenzar con columnas booleanas y de cadena pero tiene una especificación flexible para cualquier número de columnas np.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 de TypeClinic.

Primero, se proporcionan utilidades para traducir un contenedor, como un Frame completo, en una pista de tipo. La representación de cadena de la interfaz via_type_clinic proporciona una representación de cadena de la pista de tipo del contenedor; alternativamente, el método to_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 cualquier Frame, y TSeriesAny para cualquier Series. Como era de esperar, TFrameAny validará el Frame 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!

Share:

Was this article helpful?

93 out of 132 found this helpful

Discover more

Inteligencia Artificial

Cómo introducir computadoras cuánticas sin frenar el crecimiento económico

Para allanar el camino de la revolución cuántica, los investigadores y los gobiernos deben predecir y prepararse para...

Inteligencia Artificial

Mejorando los Modelos de Lenguaje con Indicaciones Analógicas para Mejorar el Razonamiento

En los últimos años, los modelos de lenguaje han demostrado una notable habilidad para entender y generar texto simil...

Inteligencia Artificial

Aterrizaje de Chandrayaan 3 Cómo la IA y los sensores ayudaron en la épica empresa lunar de la ISRO.

En la fascinante expansión de la exploración espacial, cada misión es una apuesta cósmica, cada una un lanzamiento de...

Inteligencia Artificial

Google DeepMind utilizó un gran modelo de lenguaje para resolver un problema matemático insoluble

Tuvieron que desechar la mayor parte de lo que producían, pero había oro entre la basura.

Inteligencia Artificial

Ayudando a la Visión por Computadora y a los Modelos de Lenguaje a Comprender lo que Ven

El Instituto de Tecnología de Massachusetts y otros investigadores desarrollaron una técnica que utiliza datos genera...

Inteligencia Artificial

Rice e IIT Kanpur anuncian los ganadores del Premio de Investigación Colaborativa

La Universidad Rice y el Instituto Indio de Tecnología Kanpur han anunciado los primeros beneficiarios del programa d...