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"