Типирование DataFrame для статического анализа и проверки времени выполнения

DataFrame типирование для статического анализа и проверки времени выполнения

Как StaticFrame обеспечивает полные подсказки по типам DataFrame

Фото автора

С момента появления подсказок типов в Python 3.5 статическая типизация DataFrame, как правило, ограничивалась только указанием типа:

def process(f: DataFrame) -> Series: ...

Это недостаточно, так как оно игнорирует типы, содержащиеся в контейнере. В DataFrame могут присутствовать столбцы строковых меток и три столбца целочисленных, строковых и числовых значений; эти характеристики определяют тип. Функция, которая принимает такие подсказки типов, предоставляет разработчикам, статическим анализаторам и проверяющим выполнение средствам всю необходимую информацию для понимания ожиданий от интерфейса. StaticFrame 2 (открытый проект, в котором я являюсь главным разработчиком) теперь позволяет это:

from typing import Anyfrom static_frame import Frame, Index, TSeriesAnydef process(f: Frame[   # тип контейнера        Any,            # тип меток индекса        Index[np.str_], # тип меток столбцов        np.int_,        # тип первого столбца        np.str_,        # тип второго столбца        np.float64,     # тип третьего столбца        ]) -> TSeriesAny: ...

Все основные контейнеры StaticFrame теперь поддерживают обобщенные спецификации. При статической проверке новый декоратор @CallGuard.check позволяет выполнять проверку подсказок типов на интерфейсах функций во время выполнения. Кроме того, с использованием аннотированных обобщенных типов новый класс Require определяет семейство мощных проверяющих во время выполнения, позволяющих выполнять проверку данных для каждого столбца или строки. Наконец, каждый контейнер предоставляет новый интерфейс via_type_clinic для производства и проверки подсказок типов. Вместе эти инструменты предлагают сплоченный подход к подсказыванию типов и проверке DataFrames.

Требования для обобщенного DataFrame

Встроенные обобщенные типы Python (например, tuple или dict) требуют указания типов компонентов (например, tuple[int, str, bool] или dict[str, int]). Указание типов компонентов позволяет проводить более точный статический анализ. Хотя это также справедливо для DataFrames, было сделано немного попыток определить полные подсказки типов для DataFrames.

Pandas, даже с пакетом pandas-stubs, не позволяет указывать типы компонентов DataFrame. DataFrame в Pandas, позволяющий обширное изменение на месте, может быть несостоятельным для статической типизации. К счастью, в StaticFrame доступны неизменяемые DataFrames.

Кроме того, до недавнего времени инструменты Python для определения обобщенных типов не были хорошо подходящими для DataFrames. То, что у DataFrame переменное количество гетерогенных колонок, представляет сложность для обобщенной спецификации. Задача типизации такой структуры стала проще с использованием нового TypeVarTuple, введенного в Python 3.11 (а также обратно принятого в пакете typing_extensions).

TypeVarTuple позволяет определить обобщенные типы, принимающие переменное количество типов. (См. PEP 646 для получения подробной информации.) С помощью этой новой переменной типа StaticFrame может определить обобщенный Frame с TypeVar для индекса, TypeVar для столбцов и TypeVarTuple для нуля или более типов колонок.

Обобщенный Series определяется с использованием TypeVar для индекса и TypeVar для значений. StaticFrame Index и IndexHierarchy также являются обобщенными, последний снова использует TypeVarTuple, чтобы определить переменное число компонентов Index для каждого уровня глубины.

StaticFrame использует типы NumPy для определения типов колонок Frame или значений Series или Index. Это позволяет узко указывать размерные числовые типы, такие как np.uint8 или np.complex128, или широко указывать категории типов, такие как np.integer или np.inexact. Поскольку StaticFrame поддерживает все типы NumPy, соответствие является прямым.

Интерфейсы, определенные с помощью обобщенных DataFrames

Расширяя приведенный выше пример, приведенный ниже интерфейс функции показывает Frame с тремя столбцами, преобразованными в словарь Series. С таким большим количеством информации, предоставленной подсказками о типах компонентов, назначение функции становится почти очевидным.

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[                 # тип контейнера                        IndexYearMonth, # тип меток индекса                        np.float64,     # тип значений                        ],                ]: ...

Эта функция обрабатывает таблицу сигналов из набора данных Open Source Asset Pricing (OSAP) (Характеристики на уровне фирмы / Индивидуальные / Предикторы). Каждая таблица имеет три столбца: идентификатор безопасности (помеченный “permno”), год и месяц (помеченные “yyyymm”) и сигнал (с именем, специфичным для сигнала).

Функция игнорирует индекс предоставленного Frame (обозначенного как Any) и создает группы, определенные первым столбцом “permno” с значениями np.int_. Возвращается словарь с ключами по “permno”, где каждое значение является серией np.float64 для этого “permno”; индексом является IndexYearMonth, созданный из столбца np.str_ “yyyymm” (StaticFrame использует значения типа NumPy datetime64 для определения индексов с типами единицы: IndexYearMonth хранит метки datetime64[M]).

Вместо возврата dict, функция ниже возвращает Series с иерархическим индексом. Обобщение IndexHierarchy указывает индексный компонент для каждого уровня глубины; здесь внешняя глубина – это Index[np.int_] (полученный из столбца “permno”), внутренняя глубина – IndexYearMonth (полученный из столбца “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[                    # тип контейнера                IndexHierarchy[          # тип меток индекса                        Index[np.int_],  # тип глубины индекса 0                        IndexYearMonth], # тип глубины индекса 1                np.float64,              # тип значений                ]: ...

Богатые подсказки о типах обеспечивают самодокументирующийся интерфейс, который делает функциональность явной. Что еще лучше, эти подсказки о типах могут использоваться для статического анализа с помощью Pyright (теперь) и Mypy (при поддержке полной поддержки TypeVarTuple). Например, вызов этой функции с Frame из двух столбцов np.float64 не пройдет проверку типа статического анализа или выдаст предупреждение в редакторе.

Проверка типа во время выполнения

Статическая проверка типов может быть недостаточной: оценка времени выполнения позволяет установить еще более сильные ограничения, особенно для динамических или неполностью (или некорректно) заданных типизированных значений.

В основе нового проверяющего типы времени выполнения с именем TypeClinic StaticFrame 2 вводит @CallGuard.check, декоратор для проверки типизированных интерфейсов времени выполнения. Все обобщения StaticFrame и NumPy поддерживаются, и поддерживается большинство встроенных типов Python, даже те, которые глубоко вложены. Функция ниже добавляет декоратор @CallGuard.check.

from typing import Anyfrom static_frame import Frame, Series, Index, IndexYearMonth, IndexHierarchy, CallGuard@CallGuard.checkdef process(f: Frame[        Any,        Index[np.str_],        np.int_,        np.str_,        np.float64,        ]) -> Series[                IndexHierarchy[Index[np.int_], IndexYearMonth],                np.float64,                ]: ...

Теперь украшенный с помощью @CallGuard.check, если функция выше вызывается с незаголовленной Frame из двух столбцов типа np.float64, будет вызвано исключение ClinicError, иллюстрирующее то, что ожидалось три столбца, а предоставлены два, а также что ожидались строковые метки столбцов, а предоставлены целочисленные метки. (Чтобы выводить предупреждения вместо вызова исключений, используйте декоратор @CallGuard.warn.)

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]    └── Ожидаемый фрейм имеет 3 dtype, предоставленный фрейм имеет 2 dtypeIn 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_]        └── Ожидался str_, предоставлен invalid int64

Проверка данных во время выполнения

Другие характеристики могут быть проверены во время выполнения. Например, атрибуты shape или name, или последовательность меток на индексе или столбцах. Класс Require в StaticFrame предоставляет набор настраиваемых валидаторов.

  • Require.Name: Проверка атрибута “name“ контейнера.
  • Require.Len: Проверка длины контейнера.
  • Require.Shape: Проверка атрибута “shape“ контейнера.
  • Require.LabelsOrder: Проверка упорядочения меток.
  • Require.LabelsMatch: Проверка включения меток независимо от порядка.
  • Require.Apply: Применение функции, возвращающей логическое значение, к контейнеру.

Следуя растущему тренду, эти объекты предоставляются в виде дополнительных аргументов одного или нескольких типов Annotated. (См. PEP 593 для получения подробностей.) Тип, на который ссылается первый аргумент Annotated, является целью для последующих валидаторов. Например, если тип Index[np.str_] заменить на тип Annotated[Index[np.str_], Require.Len(20)], то будет применена проверка длины во время выполнения к индексу, связанному с первым аргументом.

Расширяя пример обработки таблицы сигналов OSAP, мы можем проверить наше ожидание меток столбцов. Валидатор Require.LabelsOrder может определить последовательность меток, используя для непосредственных регионов нулевых или неопределенных меток. Чтобы указать, что первые два столбца таблицы помечены как «permno» и «yyyymm», а третья метка является переменной (в зависимости от сигнала), следующий Require.LabelsOrder может быть определен внутри обобщенного типа Annotated:

from typing import Any, Annotatedfrom static_frame import Frame, Series, Index, IndexYearMonth, IndexHierarchy, CallGuard, Require@CallGuard.checkdef 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,                ]: ...

Если интерфейс ожидает небольшую коллекцию таблиц сигналов OSAP, мы можем проверить второй столбец с помощью валидатора Require.LabelsMatch. Этот валидатор может указывать требуемые метки, наборы меток (из которых должна совпадать хотя бы одна) и регулярные выражения. Если ожидается наличие таблиц только из трех файлов (т.е. «Mom12m.csv», «Mom6m.csv» и «LRreversal.csv»), мы можем проверить метки третьего столбца, определив Require.LabelsMatch с помощью набора меток:

@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,                ]: ...

Как Require.LabelsOrder, так и Require.LabelsMatch позволяют ассоциировать функции с спецификаторами меток для проверки значений данных. Если валидатор применяется к меткам столбцов, функции будут переданы Series значений столбцов. Если валидатор применяется к меткам индексов, функции будут переданы Series значений строк.

Аналогично использованию Annotated, спецификатор метки заменяется списком, где первый элемент – это спецификатор метки, а остальные элементы – функции обработки строк или столбцов, возвращающие значение типа Boolean.

Для расширения приведенного выше примера мы можем проверить, что все значения “permno” больше нуля, а все значения сигнала (“Mom12m”, “Mom6m”, “LRreversal”) больше или равны -1.

from typing import Any, Annotatedfrom static_frame import Frame, Series, Index, IndexYearMonth, IndexHierarchy, CallGuard, Require@CallGuard.checkdef 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,                ]: ...

Если проверка не пройдена, @CallGuard.check вызовет исключение. Например, если функция, указанная выше, вызывается с Frame, содержащим неожидаемую метку третьего столбца, будет вызвано следующее исключение:

ClinicError:В аргументах функции (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>])            └── Ожидалось соответствие метки множеству {'Mom12m', 'LRreversal', 'Mom6m'}, но соответствие не указано

Выразительная мощь TypeVarTuple

Как показано выше, TypeVarTuple позволяет указывать для Frame ноль или более гетерогенных колоночных типов. Например, мы можем указать подсказки типа для Frame с двумя вещественными числами или шестью смешанными типами:

>>> 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]

Хотя это позволяет использовать разнообразные DataFrame, подсказка типов для широких DataFrame, таких как те, у которых сотни столбцов, может быть громоздкой. В Python 3.11 вводится новый синтаксис для указания переменного количества типов в обобщениях TypeVarTuple: звездочное выражение для обобщенного псевдонима tuple. Например, для указания типа для Frame с индексом типа дата, метками столбцов типа строка и любой конфигурацией колоночных типов мы можем распаковать звездочкой tuple с нулевым или более All:

>>> от печати импорта любые>>> от статичного_кадра импорт Кадр, Индекс>>> f: sf.Frame[Index[np.datetime64], Index[np.str_], *tuple[Все, ...]]

Выражение с звездочкой tuple может находиться в любом месте списка типов, но может быть только один. Например, ниже приведено подсказка типа, которая определяет Frame, который должен начинаться с логических и строковых столбцов, но имеет подвижную спецификацию для любого количества последующих столбцов np.float64.

>>> от печати импорта любые>>> от статичного_кадра импорт Кадр>>> f: sf.Frame[Any, Any, np.bool_, np.str_, *tuple[np.float64, ...]]

Утилиты для Подсказок типов

Работа с такими детальными подсказками типов может быть сложной. Чтобы помочь пользователям, StaticFrame предоставляет удобные утилиты для подсказки типов во время выполнения и проверки. Все контейнеры StaticFrame 2 теперь имеют интерфейс via_type_clinic, который позволяет получить доступ к функциональности TypeClinic.

Во-первых, предоставляются утилиты для преобразования контейнера, такого как полный Frame, в подсказку типа. Строковое представление интерфейса via_type_clinic предоставляет строковое представление подсказки типа контейнера; либо метод to_hint() возвращает полный объект обобщающего псевдонима.

>>> импортировать static_frame как 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]

Во-вторых, предоставляются утилиты для тестирования подсказок типов во время выполнения. Функция via_type_clinic.check() позволяет проводить проверку контейнера на соответствие указанной подсказке типа.

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

Чтобы поддержать постепенную типизацию, StaticFrame определяет несколько обобщающих псевдонимов, настроенных с использованием любых для каждого типа компонента. Например, TFrameAny может использоваться для любого Frame, а TSeriesAny для любого Series. Как и ожидалось, TFrameAny будет проверять созданный выше Frame.

>>> f.via_type_clinic.check(sf.TFrameAny)

Заключение

Лучшая подсказка типов для DataFrames давно ожидалась. С современными инструментами типизации Python и DataFrame, построенным на неизменной модели данных, StaticFrame 2 отвечает этим требованиям, предоставляя мощные инструменты для инженеров, приоритетом которых является поддержка поддерживаемости и проверяемости.