Source code for cc_tk.relationship.significance.statistical

"""Significance tests based on statistical methods."""

from typing import Optional, Tuple

import pandas as pd
from scipy import stats

from cc_tk.relationship.significance.base import (
    Constants,
    SignificanceCategoricalCategorical,
    SignificanceCategoricalNumeric,
    SignificanceNumericCategorical,
    SignificanceNumericNumeric,
    SignificanceOutput,
)
from cc_tk.relationship.utils import cut_influence, influence_from_correlation


[docs] class PearsonSignificance(SignificanceNumericNumeric): """Significance test based on Pearson correlation.""" def _evaluate( self, numeric_values_1: pd.Series, numeric_values_2: pd.Series ) -> SignificanceOutput: """Pearson correlation-based significance. The significance is based on the test of pearson correlation not being null. Parameters ---------- numeric_values_1 : pd.Series First numeric values numeric_values_2 : pd.Series Second numeric values Returns ------- SignificanceOutput Output of the significance function """ corr_results = stats.pearsonr(numeric_values_1, numeric_values_2) correlation = corr_results.statistic pvalue = corr_results.pvalue influence = influence_from_correlation(correlation) influence = pd.Series([influence]) output = SignificanceOutput( pvalue=pvalue, influence=influence, statistic=correlation, ) return output
[docs] class AnovaSignificance: """Base class for ANOVA significance tests."""
[docs] def compute_significance( self, numeric_values: pd.Series, categorical_values: pd.Series ) -> SignificanceOutput: """Anova or Kruskal-Wallis significance test. Parameters ---------- numeric_values : pd.Series Numeric values which we are interested in knowing if there is a difference in distribution categorical_values : pd.Series Categorical values to divide the numeric values in groups Returns ------- SignificanceOutput Output of the significance function """ group_info = self._compute_group_info( numeric_values, categorical_values ) ks_pvalue_series, bartlett_pvalue = self._perform_tests(group_info) # If any of the groups is not gaussian: ks_pvalue_series < 0.05 OR # If any of the groups does not have equal variance: # bartlett_pvalue < 0.05 # Then we use Kruskal-Wallis test if (ks_pvalue_series < Constants.STRONG_THRESHOLD).any() or ( bartlett_pvalue < Constants.STRONG_THRESHOLD ): test = stats.kruskal( *[info["values"] for info in group_info.values()] ) message = ( f"{numeric_values.name} grouped by {categorical_values.name} " f"are not gaussians with equal variances. " f"Computing Kruskal-Wallis p-value." ) else: # If all the groups are gaussian and have equal variances # we use ANOVA test test = stats.f_oneway( *[info["values"] for info in group_info.values()] ) message = ( f"{numeric_values.name} grouped by {categorical_values.name} " f"are gaussians and have equal variances. Computing " f"ANOVA p-value." ) statistic = test.statistic pvalue = test.pvalue mean_by_group = pd.Series( {key: info["mean"] for key, info in group_info.items()} ) influence = cut_influence(mean_by_group) output = SignificanceOutput( pvalue=pvalue, influence=influence, statistic=statistic, message=message, ) return output
@staticmethod def _compute_group_info( numeric_values: pd.Series, categorical_values: pd.Series ) -> dict: """Compute information for each categorical group. Informations computed are: mean, std, normalized values and Kolmogorov-Smirnov test. Parameters ---------- numeric_values : pd.Series Numeric values to divide in groups categorical_values : pd.Series Categorical values to divide the numeric values in groups Returns ------- dict Dictionary with the information of each group """ group_info = {} for categorical_value in categorical_values.unique(): group_values = numeric_values[ categorical_values == categorical_value ] group_mean = group_values.mean() group_std = group_values.std() group_normalized_values = (group_values - group_mean) / group_std group_test_ks = stats.kstest(group_normalized_values, "norm") group_info[categorical_value] = { "values": group_values, "normalized_values": group_normalized_values, "mean": group_mean, "std": group_std, "ks_test": group_test_ks, } return group_info @staticmethod def _perform_tests(group_info: dict) -> Tuple[pd.Series, float]: """Perform Kolmogorov-Smirnov and Bartlett tests for the groups. - Kolmogorov-Smirnov test is used to check if the groups are gaussian - Bartlett test is used to check if the groups have equal variances """ ks_pvalue_series = pd.Series( [group_info[key]["ks_test"].pvalue for key in group_info.keys()] ) bartlett_pvalue = stats.bartlett( *[info["values"] for info in group_info.values()] ).pvalue return ks_pvalue_series, bartlett_pvalue
[docs] class AnovaSignificanceNumericTarget( SignificanceCategoricalNumeric, AnovaSignificance ): """Anova significance test with numeric target.""" def _evaluate( self, categorical_values: pd.Series, numeric_values: pd.Series ) -> SignificanceOutput: return self.compute_significance(numeric_values, categorical_values)
[docs] class AnovaSignificanceCategoricalTarget( SignificanceNumericCategorical, AnovaSignificance ): """Anova significance test with categorical target.""" def _evaluate( self, numeric_values: pd.Series, categorical_values: pd.Series ) -> SignificanceOutput: return self.compute_significance(numeric_values, categorical_values)
[docs] class Chi2Significance(SignificanceCategoricalCategorical): """Significance based on Chi2 test.""" def __init__(self, hypotheses_threshold: Optional[int] = 5) -> None: """Initialize the Chi2 significance test. Parameters ---------- hypotheses_threshold : Optional[int], optional Threshold for hypotheses verification, by default 5 """ self.hypotheses_threshold = hypotheses_threshold def _evaluate( self, categorical_values_1: pd.Series, categorical_values_2: pd.Series ) -> SignificanceOutput: """Chi2 significance test for categorical/categorical variables. Parameters ---------- categorical_values_1 : pd.Series First categorical series categorical_values_2 : pd.Series Second categorical series Returns ------- SignificanceOutput Output of the significance function """ contingency_table = pd.crosstab( categorical_values_1, categorical_values_2, ) if (contingency_table < self.hypotheses_threshold).any().any(): hypotheses_verified = False else: hypotheses_verified = True chi2_results = stats.chi2_contingency(contingency_table) statistic = chi2_results.statistic pvalue = chi2_results.pvalue # Influence is computed based on the relative difference between actual # values and expected frequencies influence = cut_influence( (contingency_table - chi2_results.expected_freq) .divide(chi2_results.expected_freq) .unstack() ) if hypotheses_verified: message = "Hypotheses verified." else: message = "Hypotheses not verified." output = SignificanceOutput( pvalue=pvalue, influence=influence, statistic=statistic, message=message, ) return output