Source code for cc_tk.relationship.schema

"""Defines the schema for the relationship module."""

import inspect
import sys
from enum import Enum, unique
from functools import wraps
from typing import Callable, Tuple, get_args

import numpy as np
import pandas as pd
from pandera import Check, DataFrameSchema
from pydantic import validate_call

from cc_tk.util.types import ArrayLike1D


[docs] def all_columns_numeric(df: pd.DataFrame) -> bool: """Check if all columns in a DataFrame are numeric. Parameters ---------- df : pd.DataFrame The DataFrame to check. Returns ------- bool True if all columns are numeric, False otherwise. """ return df.select_dtypes(include=[np.number]).shape[1] == df.shape[1]
[docs] def all_columns_categorical(df: pd.DataFrame) -> bool: """Check if all columns in a DataFrame are categorical. Parameters ---------- df : pd.DataFrame The DataFrame to check. Returns ------- bool True if all columns are categorical, False otherwise. """ return df.select_dtypes(exclude=[np.number]).shape[1] == df.shape[1]
OnlyNumericSchema = DataFrameSchema(checks=Check(all_columns_numeric)) OnlyCategoricalSchema = DataFrameSchema(checks=Check(all_columns_categorical))
[docs] @unique class SeriesType(str, Enum): """Defines the type of a series.""" NUMERIC = "numeric" CATEGORICAL = "categorical"
[docs] def check_series_in_signature( func: Callable, *arg_names: str ) -> inspect.Signature: """Check that the specified arguments are pd.Series. Parameters ---------- func : Callable The function to check. *arg_names : str The names of the arguments to check. Returns ------- Signature The signature. Raises ------ ValueError If an argument does not exist. TypeError If an argument is not a pd.Series. """ signature = inspect.signature(func) for arg_name in arg_names: if arg_name not in signature.parameters: raise ValueError(f"Argument '{arg_name}' does not exist") elif ( sys.version_info >= (3, 10) and not issubclass( signature.parameters[arg_name].annotation, ArrayLike1D ) ) or signature.parameters[arg_name].annotation not in get_args( ArrayLike1D ): raise TypeError(f"Argument '{arg_name}' must be a 1D-array.") return signature
[docs] @validate_call def check_input_types(*type_specs: Tuple[str, SeriesType]) -> Callable: """Check the types of the arguments of the decorated function. Parameters ---------- *type_specs : Tuple[str, SeriesType] A tuple of tuples, each tuple contains the name of the argument and the expected type of the argument. Returns ------- Callable The decorator. """ def decorator(func: Callable) -> Callable: signature = check_series_in_signature( func, *[arg_name for arg_name, _ in type_specs] ) @wraps(func) def wrapper(*args, **kwargs): bound_arguments = signature.bind(*args, **kwargs) bound_arguments.apply_defaults() for arg_name, expected_type in type_specs: series = bound_arguments.arguments[arg_name] if ( expected_type == SeriesType.NUMERIC and not pd.api.types.is_numeric_dtype(series) ): raise TypeError(f"Argument '{arg_name}' must be numeric") elif ( expected_type == SeriesType.CATEGORICAL and pd.api.types.is_numeric_dtype(series) ): raise TypeError( f"Argument '{arg_name}' must be categorical" ) return func(*args, **kwargs) return wrapper return decorator
[docs] @validate_call def check_input_index(*arg_names: str) -> Callable: """Check that the specified arguments have the same index. Parameters ---------- *arg_names : str The names of the arguments to check. Returns ------- Callable The decorator. """ def decorator(func): signature = check_series_in_signature(func, *arg_names) @wraps(func) def wrapper(*args, **kwargs): bound_arguments = signature.bind(*args, **kwargs) bound_arguments.apply_defaults() series_list = [ bound_arguments.arguments[arg_name] for arg_name in arg_names ] first_series_index = series_list[0].index for series in series_list[1:]: if not series.index.equals(first_series_index): raise ValueError( "All specified Series must have the same index." ) return func(*args, **kwargs) return wrapper return decorator