Allow customising p value formatting

This commit is contained in:
RunasSudo 2022-10-15 23:11:22 +11:00
parent 49b9230484
commit bf5cc434b5
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
4 changed files with 159 additions and 14 deletions

88
tests/test_fmt_pvalues.py Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
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'

View File

@ -15,6 +15,7 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
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

32
yli/config.py Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
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()

View File

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