Типирование 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.
- Другая сторона контрактов на данные пробуждение ответственности потребителей
- Семантика различных техник SCD2
- Граф RAG Раскрытие силы графов познания с помощью LLM
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 отвечает этим требованиям, предоставляя мощные инструменты для инженеров, приоритетом которых является поддержка поддерживаемости и проверяемости.