Desde la llegada de las sugerencias de tipo en Python 3.5, escribir estáticamente un DataFrame generalmente se ha limitado a especificar solo el tipo:
def process(f: DataFrame) -> Series: ...
Esto es inadecuado, ya que ignora los tipos contenidos en el contenedor. Un DataFrame puede tener etiquetas de columnas de cadena y tres columnas de valores enteros, de cadena y de punto flotante; estas características definen el tipo. Un argumento de función con este tipo de sugerencias 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. Marco estático 2 (un proyecto de código abierto del cual soy desarrollador principal) ahora permite esto:
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: ...
Todos los contenedores StaticFrame principales ahora admiten especificaciones genéricas. Si bien se puede verificar estáticamente, un nuevo decorador, @CallGuard.check, permite la validación en tiempo de ejecución de estas sugerencias de tipo en las interfaces de funciones. Además, usando Annotated genéricos, lo nuevo Require La clase define una familia de potentes validadores de tiempo de ejecución, que permiten comprobaciones de datos por columna o por fila. Finalmente, cada contenedor expone una nueva via_type_clinic interfaz para derivar y validar sugerencias de tipo. Juntas, estas herramientas ofrecen un enfoque coherente para la sugerencia de tipos y la validación de DataFrames.
Requisitos de un marco de datos genérico
Los tipos genéricos integrados de Python (p. ej., tuple o dict) requieren la especificación de los tipos de componentes (p. ej., tuple[int, str, bool] o dict[str, int]). La definición de tipos de componentes permite un análisis estático más preciso. Si bien ocurre lo mismo con los DataFrames, ha habido pocos intentos de definir sugerencias de tipo integrales para DataFrames.
Los pandas, incluso con el pandas-stubs paquete, no permite especificar los tipos de componentes de un DataFrame. Es posible que no sea conveniente escribir de forma estática el Pandas DataFrame, que permite una amplia mutación in situ. Afortunadamente, los DataFrames inmutables están disponibles en StaticFrame.
Además, las herramientas de Python para definir genéricos, hasta hace poco, no eran adecuadas para DataFrames. Que un DataFrame tenga un número variable de tipos de columnas heterogéneos plantea un desafío para la especificación genérica. Escribir una estructura de este tipo se volvió más fácil con la nueva TypeVarTupleintroducido en Python 3.11 (y adaptado en el typing_extensions paquete).
A 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 con un TypeVar para el índice, un TypeVar para las columnas y un TypeVarTuple para cero o más tipos de columnas.
un genérico Series se define con una TypeVar para el índice y un TypeVar por los valores. El marco estático Index y IndexHierarchy también son genéricos, este último nuevamente aprovechando TypeVarTuple para definir un número variable de componentes Index para cada nivel de profundidad.
StaticFrame usa tipos NumPy para definir los tipos de columnas de un Frameo los valores de un Series o Index. Esto permite especificar tipos numéricos de tamaño estricto, como np.uint8 o np.complex128; o especificar ampliamente categorías de tipos, como np.integer o np.inexact. Como StaticFrame admite todos los tipos de NumPy, la correspondencia es directa.
Interfaces definidas con marcos de datos genéricos
Ampliando el ejemplo anterior, la siguiente interfaz de funciones muestra una Frame con tres columnas transformadas en un diccionario de Series. Con tanta más información proporcionada por las sugerencias de tipo de componente, el propósito de la función es casi obvio.
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
],
]: ...
Esta función procesa una tabla de señales de un Precios de activos de código abierto (OSAP) conjunto de datos (características a nivel de empresa/individuo/predictores). Cada tabla tiene tres columnas: identificador de seguridad (etiquetado “permno”), año y mes (etiquetados “aaaamm”) y la señal (con un nombre específico de la señal).
La función ignora el índice del proporcionado. Frame (escrito como Any) y crea grupos definidos por la primera columna “permno” np.int_ valores. Se devuelve un diccionario con la clave “permno”, donde cada valor es un Series de np.float64 valores para ese “permno”; el índice es un IndexYearMonth creado a partir de la np.str_ columna “aaaamm”. (StaticFrame usa NumPy datetime64 valores para definir índices unitarios: IndexYearMonth historias datetime64[M] etiquetas.)
En lugar de devolver un dictla siguiente función devuelve un Series con un índice jerárquico. El IndexHierarchy genérico especifica un componente Index para cada nivel de profundidad; aquí, la profundidad exterior es una Index[np.int_] (derivado de la columna “permno”), la profundidad interior y IndexYearMonth (derivado de la columna “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
]: ...
Las sugerencias de tipo enriquecido proporcionan una interfaz autodocumentada que hace que la funcionalidad sea explícita. Aún mejor, estas sugerencias de tipo se pueden usar para análisis estático con Pyright (ahora) y Mypy (pendiente de completar). TypeVarTuple apoyo). Por ejemplo, llamar a esta función con un Frame de dos columnas de np.float64 fallará una verificación de tipo de análisis estático o entregará una advertencia en un editor.
Validación del tipo de tiempo de ejecución
La verificación de tipos estática puede no ser suficiente: la evaluación en tiempo de ejecución proporciona restricciones aún más fuertes, particularmente para valores dinámicos o con sugerencias de tipo incompletas (o incorrectas).
Construyendo sobre un nuevo verificador de tipo de tiempo de ejecución llamado TypeClinicStaticFrame 2 presenta @CallGuard.check, un decorador para la validación en tiempo de ejecución de interfaces con sugerencias de tipo. Se admiten todos los genéricos StaticFrame y NumPy, y la mayoría de los tipos integrados de Python son compatibles, incluso cuando están profundamente anidados. La siguiente función agrega el @CallGuard.check decorador.
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,
]: ...
Ahora decorado con @CallGuard.checksi la función anterior se llama con un sin etiquetar Frame de dos columnas de np.float64a ClinicError Se generará una excepción, lo que ilustra que, donde se esperaban tres columnas, se proporcionaron dos y donde se esperaban etiquetas de columna de cadena, se proporcionaron etiquetas de números enteros. (Para emitir advertencias en lugar de generar excepciones, utilice el @CallGuard.warn decorador.)
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
Validación de datos en tiempo de ejecución
Otras características se pueden validar en tiempo de ejecución. Por ejemplo, el shape o name atributos, o la secuencia de etiquetas en el índice o columnas. El marco estático Require La clase proporciona una familia de validadores configurables.
Require.Name: Valida el atributo “nombre“ del contenedor.Require.Len: Validar la longitud del contenedor.Require.Shape: Valida el atributo “forma“ del contenedor.Require.LabelsOrder: Validar el orden de las etiquetas.Require.LabelsMatch: Validar inclusión de etiquetas independientemente del pedido.Require.Apply: aplique una función de retorno booleano al contenedor.
Alineándose con una tendencia creciente, estos objetos se proporcionan dentro de sugerencias de tipo como uno o más argumentos adicionales para un Annotated genérico. (Ver PEP 593 para más detalles.) El tipo al que hace referencia la primera Annotated El argumento es el objetivo de los validadores de argumentos posteriores. Por ejemplo, si un Index[np.str_] la sugerencia de tipo se reemplaza con una Annotated[Index[np.str_], Require.Len(20)] escriba sugerencia, la validación de la longitud del tiempo de ejecución se aplica al índice asociado con el primer argumento.
Ampliando el ejemplo del procesamiento de una tabla de señales OSAP, podríamos validar nuestra expectativa de etiquetas de columna. El Require.LabelsOrder El validador puede definir una secuencia de etiquetas, opcionalmente usando … 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 “aaaamm”, mientras que la tercera etiqueta es variable (dependiendo de la señal), se debe realizar lo siguiente Require.LabelsOrder puede definirse dentro de un Annotated genérico:
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,
]: ...
Si la interfaz espera una pequeña colección de tablas de señales OSAP, podemos validar la tercera columna con el Require.LabelsMatch validador. Este validador puede especificar etiquetas requeridas, conjuntos de etiquetas (de las cuales al menos una debe coincidir) y patrones de expresión regular. Si se esperan tablas de sólo tres archivos (es decir, “Mom12m.csv”, “Mom6m.csv” y “LRreversal.csv”), podemos validar las etiquetas de la tercera columna definiendo Require.LabelsMatch con un conjunto:
@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,
]: ...
Ambos Require.LabelsOrder y Require.LabelsMatch Puede asociar funciones con especificadores de etiquetas para validar valores de datos. Si el validador se aplica a las etiquetas de las columnas, se Series de los valores de las columnas se proporcionarán a la función; si el validador se aplica a etiquetas de índice, un Series de valores de fila se proporcionarán a la función.
Similar al uso de Annotatedel especificador de etiqueta se reemplaza con una lista, donde el primer elemento es el especificador de etiqueta y los elementos restantes 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, 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,
]: ...
Si una validación falla, @CallGuard.check generará una excepción. Por ejemplo, si la función anterior se llama con un Frame que tiene una etiqueta inesperada en la tercera columna, 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>])
└── Expected label to match frozenset({'Mom12m', 'LRreversal', 'Mom6m'}), no provided match
El poder expresivo de TypeVarTuple
Como se muestra arriba, TypeVarTuple permisos especificando Frame con cero o más tipos de columnas heterogéneas. Por ejemplo, podemos proporcionar sugerencias de tipo para un Frame de dos tipos flotantes o seis mixtos:
>>> 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]
Si bien esto se adapta a diversos DataFrames, los DataFrames amplios con sugerencias de tipo, como aquellos con cientos de columnas, serían difíciles de manejar. Python 3.11 introduce una nueva sintaxis para proporcionar una gama variable de tipos en TypeVarTuple genéricos: expresiones estelares de tuple alias genéricos. Por ejemplo, para escribir una sugerencia Frame con un índice de fecha, etiquetas de columnas de cadena y cualquier configuración de tipos de columnas, podemos desempaquetar en estrella un tuple de cero o más All:
>>> from typing import Any
>>> from static_frame import Frame, Index>>> f: sf.Frame[Index[np.datetime64], Index[np.str_], *tuple[All, ...]]
El tuple La expresión estrella puede ir a cualquier lugar de una lista de tipos, pero sólo puede haber uno. Por ejemplo, la sugerencia de tipo siguiente define un Frame que debe comenzar con columnas booleanas y de cadena, pero tiene una especificación flexible para cualquier número de columnas posteriores. np.float64 columnas.
>>> from typing import Any
>>> from static_frame import Frame>>> f: sf.Frame[Any, Any, np.bool_, np.str_, *tuple[np.float64, ...]]
Utilidades para sugerencias de tipo
Trabajar con sugerencias tipográficas tan detalladas puede resultar un desafío. Para ayudar a los usuarios, StaticFrame proporciona utilidades convenientes para sugerencias y verificación de tipos en tiempo de ejecución. Todos los contenedores StaticFrame 2 ahora cuentan con un via_type_clinic interfaz que permite el acceso a TypeClinic funcionalidad.
En primer lugar, se proporcionan utilidades para traducir un contenedor, como un completo Frame, en una sugerencia de tipo. La representación en cadena del via_type_clinic la interfaz proporciona una representación de cadena de la sugerencia de tipo del contenedor; alternativamente, el to_hint() El método 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_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]
En segundo lugar, se proporcionan utilidades para pruebas de sugerencias de tipo en tiempo de ejecución. El via_type_clinic.check() La función permite validar el contenedor con una sugerencia 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_]
└── Expected str_, provided int64 invalid
Para admitir la escritura gradual, StaticFrame define varios alias genéricos configurados con Any para cada tipo de componente. Por ejemplo, TFrameAny se puede utilizar para cualquier Framey TSeriesAny para cualquier Series. Como se esperaba, TFrameAny validará el Frame creado arriba.
>>> f.via_type_clinic.check(sf.TFrameAny)
Conclusión
Ya hace tiempo que se necesitan mejores sugerencias de tipo para DataFrames. Con modernas herramientas de escritura de Python y un DataFrame construido sobre un modelo de datos inmutable, StaticFrame 2 satisface esta necesidad, proporcionando recursos poderosos para los ingenieros que priorizan la mantenibilidad y la verificabilidad.