diff --git a/tests/test_fmt_pvalues.py b/tests/test_fmt_pvalues.py new file mode 100644 index 0000000..52988f4 --- /dev/null +++ b/tests/test_fmt_pvalues.py @@ -0,0 +1,88 @@ +# scipy-yli: Helpful SciPy utilities and recipes +# Copyright © 2022 Lee Yingtong Li (RunasSudo) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import yli +from yli.config import config +from yli.utils import fmt_p + +def test_fmt_pvalues_ord(): + """Test formatting of p values requiring no special handling""" + + # Default config + config.pvalue_min_dps = 2 + config.pvalue_max_dps = 3 + config.pvalue_leading_zero = True + config.alpha = 0.05 + + assert fmt_p(0.0096, html=False) == '= 0.01*' + assert fmt_p(0.01, html=False) == '= 0.01*' + assert fmt_p(0.04, html=False) == '= 0.04*' + assert fmt_p(0.11, html=False) == '= 0.11' + assert fmt_p(0.55, html=False) == '= 0.55' + +def test_fmt_pvalues_ord_noleadingzero(): + """Test formatting of p values requiring no special handling, no leading zero""" + + # Default config + config.pvalue_min_dps = 2 + config.pvalue_max_dps = 3 + config.pvalue_leading_zero = False + config.alpha = 0.05 + + assert fmt_p(0.0096, html=False) == '= .01*' + assert fmt_p(0.01, html=False) == '= .01*' + assert fmt_p(0.04, html=False) == '= .04*' + assert fmt_p(0.11, html=False) == '= .11' + assert fmt_p(0.55, html=False) == '= .55' + +def test_fmt_pvalues_small(): + """Test formatting of small p values requiring extra decimal points to represent""" + + # Default config + config.pvalue_min_dps = 2 + config.pvalue_max_dps = 3 + config.pvalue_leading_zero = True + config.alpha = 0.05 + + assert fmt_p(0.009, html=False) == '= 0.009*' + +def test_fmt_pvalues_ambiguous(): + """Test formatting of p values requiring extra decimal points to avoid ambiguity""" + + # Default config + config.pvalue_min_dps = 2 + config.pvalue_max_dps = 3 + config.pvalue_leading_zero = True + config.alpha = 0.05 + + assert fmt_p(0.048, html=False) == '= 0.048*' + assert fmt_p(0.052, html=False) == '= 0.052' + + # Special rounding rules + assert fmt_p(0.04999, html=False) == '= 0.049*' + assert fmt_p(0.05001, html=False) == '= 0.051' + +def test_fmt_pvalues_extreme(): + """Test formatting of p values too small or large to be represented""" + + # Default config + config.pvalue_min_dps = 2 + config.pvalue_max_dps = 3 + config.pvalue_leading_zero = True + config.alpha = 0.05 + + assert fmt_p(0.0009, html=False) == '< 0.001*' + assert fmt_p(0.999, html=False) == '> 0.99' diff --git a/yli/__init__.py b/yli/__init__.py index 9489567..c49b56e 100644 --- a/yli/__init__.py +++ b/yli/__init__.py @@ -15,6 +15,7 @@ # along with this program. If not, see . from .bayes_factors import bayesfactor_afbf +from .config import config from .distributions import beta_oddsratio, beta_ratio, hdi, transformed_dist from .io import pickle_read_compressed, pickle_read_encrypted, pickle_write_compressed, pickle_write_encrypted from .regress import PenalisedLogit, regress, vif diff --git a/yli/config.py b/yli/config.py new file mode 100644 index 0000000..33e1ba1 --- /dev/null +++ b/yli/config.py @@ -0,0 +1,32 @@ +# scipy-yli: Helpful SciPy utilities and recipes +# Copyright © 2022 Lee Yingtong Li (RunasSudo) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +class Config: + """Stores global configuration for the library""" + + def __init__(self): + # Display at least this many decimal places for p values + self.pvalue_min_dps = 2 + # Display at most this many decimal places for p values + self.pvalue_max_dps = 3 + # Display a leading zero for p values + self.pvalue_leading_zero = True + + # Alpha level for significance tests, confidence intervals + self.alpha = 0.05 + +"""Global config singleton""" +config = Config() diff --git a/yli/utils.py b/yli/utils.py index 895e84d..b83c2d1 100644 --- a/yli/utils.py +++ b/yli/utils.py @@ -20,6 +20,8 @@ import patsy import warnings +from .config import config + # ---------------------------- # Data cleaning and validation @@ -63,25 +65,47 @@ def as_2groups(df, data, group): def do_fmt_p(p): """Return sign and formatted p value""" - if p < 0.001: - return '<', '0.001*' - elif p < 0.0095: - return None, '{:.3f}*'.format(p) - elif p < 0.045: - return None, '{:.2f}*'.format(p) - elif p < 0.05: - return None, '{:.3f}*'.format(p) # 3dps to show significance - elif p < 0.055: - return None, '{:.3f}'.format(p) # 3dps to show non-significance - elif p < 0.095: - return None, '{:.2f}'.format(p) - else: - return None, '{:.1f}'.format(p) + if p < 10**-config.pvalue_max_dps: + # Smaller than min value + return '<', '{:.1g}'.format(10**-config.pvalue_max_dps) + + if p > 1 - 10**-config.pvalue_min_dps: + # Larger than max value + return '>', '{0:.{dps}f}'.format(1 - 10**-config.pvalue_min_dps, dps=config.pvalue_min_dps) + + if round(p, config.pvalue_min_dps) == config.alpha: + # Rounding to pvalue_min_dps makes significance ambiguous + + if round(p, config.pvalue_max_dps) == config.alpha: + # Still ambiguous to pvalue_max_dps + + if p < config.alpha: + # Significant: round down + p = config.alpha - 10**-config.pvalue_max_dps + else: + # Nonsignificant: round up + p = config.alpha + 10**-config.pvalue_max_dps + + return None, '{0:.{dps}f}'.format(p, dps=config.pvalue_max_dps) + + if p < 10**-config.pvalue_min_dps: + # Insufficient resolution at pvalue_min_dps + # We know from earlier comparison that 1 s.f. fits within pvalue_max_dps + return None, '{:.1g}'.format(p) + + # OK to round to pvalue_min_dps + return None, '{0:.{dps}f}'.format(p, dps=config.pvalue_min_dps) def fmt_p(p, *, html, nospace=False): """Format p value""" sign, fmt = do_fmt_p(p) + + if not config.pvalue_leading_zero: + fmt = fmt.lstrip('0') + if p < config.alpha: + fmt += '*' + if sign is not None: if nospace: pfmt = sign + fmt # e.g. "<0.001"