From 2ac3a8b6ea8aaec3883bb723762734be5473cf81 Mon Sep 17 00:00:00 2001 From: Florian Charlier <477844+trevismd@users.noreply.github.com> Date: Sat, 22 Mar 2025 12:17:43 +0100 Subject: [PATCH] add p separators for annotations Signed-off-by: Florian Charlier <477844+trevismd@users.noreply.github.com> --- CHANGELOG.md | 8 +++ coverage.svg | 4 +- statannotations/PValueFormat.py | 49 +++++++++++++---- statannotations/_version.py | 2 +- statannotations/format_annotations.py | 24 ++++++--- tests/test_pvalue_format.py | 76 ++++++++++++++++++++------- 6 files changed, 124 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 834040a..35f8db3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,12 @@ ## v0.7 +### v0.7.2 +### Features +- Add option to capitalize "p" in annotations + (PR [#172](https://github.com/trevismd/statannotations/pull/172) by + [Pentabyteman](https://github.com/Pentabyteman)) +- Add option to change spacing in annotations + (PR [#173](https://github.com/trevismd/statannotations/pull/173)) + ### v0.7.1 #### Fixes - Fix minimum Python requirements in setup.py diff --git a/coverage.svg b/coverage.svg index 607d3de..a8c7e72 100644 --- a/coverage.svg +++ b/coverage.svg @@ -15,7 +15,7 @@ coverage coverage - 91% - 91% + 92% + 92% diff --git a/statannotations/PValueFormat.py b/statannotations/PValueFormat.py index 8403af8..f453e7b 100644 --- a/statannotations/PValueFormat.py +++ b/statannotations/PValueFormat.py @@ -1,19 +1,21 @@ -from statannotations.stats.StatResult import StatResult +from typing import Tuple, Union from statannotations.format_annotations import pval_annotation_text, \ simple_text +from statannotations.stats.StatResult import StatResult from statannotations.utils import DEFAULT, check_valid_text_format, \ InvalidParametersError CONFIGURABLE_PARAMETERS = [ 'correction_format', 'fontsize', - 'pvalue_format_string', 'simple_format_string', 'text_format', + 'pvalue_format_string', 'pvalue_thresholds', + 'p_capitalized', + 'p_separators', 'show_test_name', - 'p_capitalized' ] @@ -40,6 +42,7 @@ def __init__(self): self._correction_format = "{star} ({suffix})" self.show_test_name = True self.p_capitalized = False + self._p_separators = (" ", " ") def config(self, **parameters): @@ -69,6 +72,30 @@ def text_format(self, text_format): check_valid_text_format(text_format) self._text_format = text_format + @property + def p_separators(self): + return { + (" ", " "): 'both', + ("", " "): 'after', + ("", ""): 'none', + }.get(self._p_separators, self._p_separators) + + @p_separators.setter + def p_separators(self, p_separators: Union[bool, str, Tuple[str]] = True): + """ + :param p_separators: + 'both' (or True)(default), 'none' (or False), 'after', + or tuple[bool] for before and after + """ + if isinstance(p_separators, tuple): + self._p_separators = p_separators + elif p_separators in {'none', False}: + self._p_separators = ("", "") + elif p_separators == 'after': + self._p_separators = ("", " ") + else: + self._p_separators = (" ", " ") + def _get_pvalue_thresholds(self, pvalue_thresholds): if self._default_pvalue_thresholds: if self.text_format == "star": @@ -179,10 +206,10 @@ def format_data(self, result): else "") p_letter = "P" if self.p_capitalized else "p" - - return ("{}{} = {}{}" - .format('{}', p_letter, self.pvalue_format_string, '{}') - .format(text, result.pvalue, result.significance_suffix)) + equals = f"{self._p_separators[0]}={self._p_separators[1]}" + formatted_pvalue = self.pvalue_format_string.format(result.pvalue) + full_pvalue = f"{formatted_pvalue}{result.significance_suffix}" + return f"{text}{p_letter}{equals}{full_pvalue}" elif self.text_format == 'star': was_list = False @@ -200,10 +227,12 @@ def format_data(self, result): return annotations[0] - # elif self.text_format == 'simple': else: - return simple_text(result, self.simple_format_string, - self.pvalue_thresholds, self.show_test_name, self.p_capitalized) + return simple_text( + result, self.simple_format_string, self.pvalue_thresholds, + short_test_name=self.show_test_name, + p_capitalized=self.p_capitalized, separators=self._p_separators + ) def get_configuration(self): return {key: getattr(self, key) for key in CONFIGURABLE_PARAMETERS} diff --git a/statannotations/_version.py b/statannotations/_version.py index a5f830a..bc8c296 100644 --- a/statannotations/_version.py +++ b/statannotations/_version.py @@ -1 +1 @@ -__version__ = "0.7.1" +__version__ = "0.7.2" diff --git a/statannotations/format_annotations.py b/statannotations/format_annotations.py index 883a9a1..2e602fa 100644 --- a/statannotations/format_annotations.py +++ b/statannotations/format_annotations.py @@ -1,8 +1,10 @@ -from statannotations.stats.StatResult import StatResult -from typing import List +from operator import itemgetter +from typing import List, Tuple + import numpy as np import pandas as pd -from operator import itemgetter + +from statannotations.stats.StatResult import StatResult def pval_annotation_text(result: List[StatResult], @@ -32,8 +34,10 @@ def pval_annotation_text(result: List[StatResult], return [(star, res) for star, res in zip(x_annot, result)] -def simple_text(result: StatResult, pvalue_format, pvalue_thresholds, - short_test_name=True, p_capitalized=False) -> str: +def simple_text(result: StatResult, pvalue_format: str, + pvalue_thresholds: List[list], + short_test_name: bool = True, p_capitalized: bool = False, + separators: Tuple[str] = (" ", " ")) -> str: """ Generates simple text for test name and pvalue. @@ -41,10 +45,11 @@ def simple_text(result: StatResult, pvalue_format, pvalue_thresholds, :param pvalue_format: format string for pvalue :param pvalue_thresholds: String to display per pvalue range :param short_test_name: whether to display the test (short) name + :param p_capitalized: set True to show "P" instead of "p" in annotations + :param separators: separators for before and after '=' or '≤' :returns: simple annotation """ - # Sort thresholds thresholds = sorted(pvalue_thresholds, key=lambda x: x[0]) text = (f"{result.test_short_name} " @@ -52,12 +57,15 @@ def simple_text(result: StatResult, pvalue_format, pvalue_thresholds, else "") p_letter = "P" if p_capitalized else "p" + p_equals = f"{p_letter}{separators[0]}={separators[1]}" + p_lte = f"{p_letter}{separators[0]}≤{separators[1]}" for threshold in thresholds: if result.pvalue < threshold[0]: - pval_text = "{} ≤ {}".format(p_letter, threshold[1]) + pval_text = f"{p_lte}{threshold[1]}" break else: - pval_text = "{} = {}".format(p_letter, pvalue_format).format(result.pvalue) + formatted_pvalue = pvalue_format.format(result.pvalue) + pval_text = f"{p_equals}{formatted_pvalue}" return result.adjust(text + pval_text) diff --git a/tests/test_pvalue_format.py b/tests/test_pvalue_format.py index 844a729..bbd2099 100644 --- a/tests/test_pvalue_format.py +++ b/tests/test_pvalue_format.py @@ -111,22 +111,26 @@ def test_print_pvalue_other(self): def test_get_configuration(self): pvalue_format = PValueFormat() - self.assertDictEqual(pvalue_format.get_configuration(), - {'correction_format': '{star} ({suffix})', - 'fontsize': 'medium', - 'p_capitalized': False, - 'pvalue_format_string': '{:.3e}', - 'show_test_name': True, - 'simple_format_string': '{:.2f}', - 'text_format': 'star', - 'pvalue_thresholds': [ - [1e-4, "****"], - [1e-3, "***"], - [1e-2, "**"], - [0.05, "*"], - [1, "ns"]] - } - ) + self.assertDictEqual( + pvalue_format.get_configuration(), + { + 'correction_format': '{star} ({suffix})', + 'fontsize': 'medium', + 'p_capitalized': False, + 'p_separators': 'both', + 'pvalue_format_string': '{:.3e}', + 'pvalue_thresholds': [ + [1e-4, "****"], + [1e-3, "***"], + [1e-2, "**"], + [0.05, "*"], + [1, "ns"] + ], + 'show_test_name': True, + 'simple_format_string': '{:.2f}', + 'text_format': 'star', + } + ) def test_config_pvalue_thresholds(self): pvalue_format = PValueFormat() @@ -138,7 +142,7 @@ def test_config_pvalue_thresholds(self): " ns: 5.00e-02 < p <= 1.00e+00\n" " <= 0.05: 1.00e-03 < p <= 5.00e-02\n" "<= 0.001: p <= 1.00e-03\n\n") - + def test_pvalue_simple_capitalized(self): self.annotator.configure(pvalue_format={"text_format": "simple", "p_capitalized": True}) @@ -153,4 +157,40 @@ def test_pvalue_full_capitalized(self): "pvalue_format_string": "{:.2f}"}) annotations = self.annotator._get_results("auto", pvalues=self.pvalues) self.assertEqual(["P = 0.04", "P = 0.03", "P = 0.90"], - [annotation.text for annotation in annotations]) \ No newline at end of file + [annotation.text for annotation in annotations]) + + def test_pvalue_simple_separators(self): + self.annotator.configure(pvalue_format={"text_format": "simple"}) + for separator, expected_results in [ + (None, ["p ≤ 0.05", "p ≤ 0.05", "p = 0.90"]), + (True, ["p ≤ 0.05", "p ≤ 0.05", "p = 0.90"]), + ('both', ["p ≤ 0.05", "p ≤ 0.05", "p = 0.90"]), + ('none', ["p≤0.05", "p≤0.05", "p=0.90"]), + ('after', ["p≤ 0.05", "p≤ 0.05", "p= 0.90"]), + ]: + with self.subTest(separator=separator): + config = {"pvalue_format": {"p_separators": separator}} + self.annotator.configure(**config) + annotations = self.annotator._get_results("auto", pvalues=self.pvalues) + self.assertEqual(expected_results, [a.text for a in annotations]) + + def test_pvalue_full_separators(self): + self.annotator.configure( + pvalue_format={ + "text_format": "full", + "show_test_name": False, + "pvalue_format_string": "{:.2f}" + }, + ) + for separator, expected_results in [ + (None, ["p = 0.04", "p = 0.03", "p = 0.90"]), + (True, ["p = 0.04", "p = 0.03", "p = 0.90"]), + ('both', ["p = 0.04", "p = 0.03", "p = 0.90"]), + ('none', ["p=0.04", "p=0.03", "p=0.90"]), + ('after', ["p= 0.04", "p= 0.03", "p= 0.90"]), + ]: + with self.subTest(separator=separator): + config = {"pvalue_format": {"p_separators": separator}} + self.annotator.configure(**config) + annotations = self.annotator._get_results("auto", pvalues=self.pvalues) + self.assertEqual(expected_results, [a.text for a in annotations])