diff --git a/.bumpversion.toml b/.bumpversion.toml index 2cb59e7..cd4d8b5 100644 --- a/.bumpversion.toml +++ b/.bumpversion.toml @@ -1,5 +1,5 @@ [tool.bumpversion] -current_version = "0.1.5" +current_version = "0.2.0" parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)" serialize = ["{major}.{minor}.{patch}"] search = "{current_version}" diff --git a/CHANGELOG.md b/CHANGELOG.md index 16345c7..31ab6b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# 0.2.0 + + - Adjustable format for the accuracy and other metrics. + - Using the hatchling build system to avoid complains about the `project.license` format. + + # 0.1.0 - Command-line utility `binary-classification-ratios` for quick try. diff --git a/README.md b/README.md index c9fc13d..c55a445 100644 --- a/README.md +++ b/README.md @@ -32,8 +32,8 @@ F1-score and prints them to terminal. ```shell binary-classification-ratios -tp 10 -tn 20 -fp 30 -fn 40 -Confusion matrix: TP 10 TN 20 FP 30 FN 40 - accuracy 0.300 +Confusion matrix TP 10 TN 20 FP 30 FN 40 + accuracy 0.30000 precision 0.250 recall 0.200 f1-score 0.222 diff --git a/pyproject.toml b/pyproject.toml index 16685ea..52f2c34 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "binary-classification-ratios" -version = "0.1.5" +version = "0.2.0" description = "Binary classification ratios gathered in one package." readme = "README.md" requires-python = ">=3.8,<4.0" @@ -34,3 +34,8 @@ binary-classification-ratios = "binary_classification_ratios.cli.main:main" [tool.uv] package = true + +[build-system] +requires = ["hatchling >= 1.26"] +build-backend = "hatchling.build" + diff --git a/src/binary_classification_ratios/__init__.py b/src/binary_classification_ratios/__init__.py index 108f00c..779a500 100644 --- a/src/binary_classification_ratios/__init__.py +++ b/src/binary_classification_ratios/__init__.py @@ -1,6 +1,6 @@ """.""" -__version__ = '0.1.5' +__version__ = '0.2.0' from .ratios import BinaryClassificationRatios diff --git a/src/binary_classification_ratios/cli/cmd_line.py b/src/binary_classification_ratios/cli/cmd_line.py index 760e4fc..0957b5f 100644 --- a/src/binary_classification_ratios/cli/cmd_line.py +++ b/src/binary_classification_ratios/cli/cmd_line.py @@ -5,6 +5,7 @@ PROG = 'binary-classification-ratios' +HLP_FMT = 'Format for the recall, precision and F1-score.' class CmdLine: @@ -15,6 +16,8 @@ def __init__(self) -> None: self.tn: int = 0 self.fp: int = 0 self.fn: int = 0 + self.fmt: str = '.3f' + self.accuracy_fmt: str = '.5f' def get_cmd_line(args: Union[Sequence[str], None] = None) -> CmdLine: @@ -24,6 +27,8 @@ def get_cmd_line(args: Union[Sequence[str], None] = None) -> CmdLine: parser.add_argument('-tn', type=int, default=0, help='Number of true negatives.') parser.add_argument('-fp', type=int, default=0, help='Number of false positives.') parser.add_argument('-fn', type=int, default=0, help='Number of false negatives.') + parser.add_argument('--fmt', help=HLP_FMT) + parser.add_argument('--accuracy-fmt', help='Format for the accuracy.') namespace = CmdLine() parser.parse_args(args, namespace=namespace) return namespace diff --git a/src/binary_classification_ratios/cli/main.py b/src/binary_classification_ratios/cli/main.py index 7f2c490..5c142ba 100644 --- a/src/binary_classification_ratios/cli/main.py +++ b/src/binary_classification_ratios/cli/main.py @@ -11,6 +11,8 @@ def run(args: Union[Sequence[str], None] = None) -> float: """.""" cli = get_cmd_line(args) bcr = BinaryClassificationRatios(tp=cli.tp, tn=cli.tn, fp=cli.fp, fn=cli.fn) + bcr.summary.fmt = cli.fmt + bcr.summary.accuracy_fmt = cli.accuracy_fmt print(bcr.get_summary()) return bcr.get_f1_score() diff --git a/src/binary_classification_ratios/ratios.py b/src/binary_classification_ratios/ratios.py index 03eb336..e5561fc 100644 --- a/src/binary_classification_ratios/ratios.py +++ b/src/binary_classification_ratios/ratios.py @@ -3,6 +3,10 @@ and summarize classification metrics such as accuracy, precision, recall, and F1-score. """ +from typing import Dict, Union + +from binary_classification_ratios.summary import BinaryClassificationSummary + class BinaryClassificationRatios(object): """ @@ -21,6 +25,7 @@ def __init__(self, *, tp: int = 0, tn: int = 0, fp: int = 0, fn: int = 0) -> Non self.tn = tn self.fp = fp self.fn = fn + self.summary = BinaryClassificationSummary() def get_summary(self) -> str: """Return a summary of the classification metrics, including accuracy, @@ -29,14 +34,21 @@ def get_summary(self) -> str: Returns: str: A formatted string summarizing the classification metrics. """ - cc = self - return ( - f'Confusion matrix: TP {cc.tp} TN {cc.tn} FP {cc.fp} FN {cc.fn}\n' - f' accuracy {cc.get_accuracy():.3f}\n' - f' precision {cc.get_precision():.3f}\n' - f' recall {cc.get_recall():.3f}\n' - f' f1-score {cc.get_f1_score():.3f}\n' - ) + dct = self.get_summary_dct() + return self.summary.get_summary(dct) + + def get_summary_dct(self) -> Dict[str, Union[int, float]]: + """.""" + return { + 'tp': self.tp, + 'tn': self.tn, + 'fp': self.fp, + 'fn': self.fn, + 'accuracy': self.get_accuracy(), + 'precision': self.get_precision(), + 'recall': self.get_recall(), + 'f1_score': self.get_f1_score(), + } def get_precision(self) -> float: """Calculate the Precision. diff --git a/src/binary_classification_ratios/summary.py b/src/binary_classification_ratios/summary.py new file mode 100644 index 0000000..ce1ae2f --- /dev/null +++ b/src/binary_classification_ratios/summary.py @@ -0,0 +1,41 @@ +""".""" + +from typing import Dict, Union + + +class BinaryClassificationSummary: + """.""" + + def __init__(self) -> None: + """.""" + self.accuracy_fmt = '.5f' + self.fmt = '.3f' + self.confusion_matrix_prefix = 'Confusion matrix' + + def get_summary(self, dct: Dict[str, Union[int, float]]) -> str: + """Return a human-readable summary of the quality metrics. + + Returns: + str: A formatted string summarizing the classification metrics. + """ + tp = dct.get('tp', '?') + tn = dct.get('tn', '?') + fp = dct.get('fp', '?') + fn = dct.get('fn', '?') + lines = [f'{self.confusion_matrix_prefix} TP {tp} TN {tn} FP {fp} FN {fn}'] + accuracy = dct.get('accuracy', None) + recall = dct.get('recall', None) + precision = dct.get('precision', None) + f1_score = dct.get('f1_score', None) + + if accuracy is not None: + lines.append(f' accuracy {accuracy:{self.accuracy_fmt}}') + if precision is not None: + lines.append(f' precision {precision:{self.fmt}}') + if recall is not None: + lines.append(f' recall {recall:{self.fmt}}') + if f1_score is not None: + lines.append(f' f1-score {f1_score:{self.fmt}}') + + summary = '\n'.join(lines) + return summary diff --git a/test/cli/test_cmd_line.py b/test/cli/test_cmd_line.py index d9b8991..64dfce9 100644 --- a/test/cli/test_cmd_line.py +++ b/test/cli/test_cmd_line.py @@ -5,12 +5,16 @@ def test_cmd_line_short_args() -> None: """.""" - cli = get_cmd_line(['-tp', '1', '-tn', '2', '-fp', '3', '-fn', '4']) + cli = get_cmd_line( + ['-tp', '1', '-tn', '2', '-fp', '3', '-fn', '4', '--fmt', '.4f', '--accuracy-fmt', '.6f'] + ) assert isinstance(cli, CmdLine) assert cli.tp == 1 assert cli.tn == 2 assert cli.fp == 3 assert cli.fn == 4 + assert cli.fmt == '.4f' + assert cli.accuracy_fmt == '.6f' def test_cmd_line_no_args() -> None: @@ -20,3 +24,5 @@ def test_cmd_line_no_args() -> None: assert cli.tn == 0 assert cli.fn == 0 assert cli.fp == 0 + assert cli.accuracy_fmt == '.5f' + assert cli.fmt == '.3f' diff --git a/test/cli/test_main.py b/test/cli/test_main.py index 4acf828..2ecffc4 100644 --- a/test/cli/test_main.py +++ b/test/cli/test_main.py @@ -5,7 +5,13 @@ from binary_classification_ratios.cli.main import run -def test_run() -> None: +def test_run(capsys: pytest.CaptureFixture) -> None: """.""" - f1 = run(['-tp', '1', '-tn', '2', '-fp', '3', '-fn', '4']) + f1 = run( + ['-tp', '1', '-tn', '2', '-fp', '3', '-fn', '4', '--fmt', '.4f', '--accuracy-fmt', '.6f'] + ) assert f1 == pytest.approx(0.222222222222222222) + stdout = capsys.readouterr().out + assert 'Confusion matrix TP 1 TN 2 FP 3 FN 4' in stdout + assert 'accuracy 0.300000' in stdout + assert 'f1-score 0.2222' in stdout diff --git a/test/test_binary_classification_ratios.py b/test/test_ratios.py similarity index 71% rename from test/test_binary_classification_ratios.py rename to test/test_ratios.py index 15cfbb8..9d412b6 100644 --- a/test/test_binary_classification_ratios.py +++ b/test/test_ratios.py @@ -32,15 +32,27 @@ def test_get_summary(bcr: BinaryClassificationRatios) -> None: """.""" assert ( bcr.get_summary() - == """Confusion matrix: TP 10 TN 9 FP 8 FN 7 - accuracy 0.559 + == """Confusion matrix TP 10 TN 9 FP 8 FN 7 + accuracy 0.55882 precision 0.556 recall 0.588 - f1-score 0.571 -""" + f1-score 0.571""" ) +def test_get_summary_dct(bcr: BinaryClassificationRatios) -> None: + """.""" + dct = bcr.get_summary_dct() + assert dct['tp'] == 10 + assert dct['tn'] == 9 + assert dct['fp'] == 8 + assert dct['fn'] == 7 + assert dct['accuracy'] == pytest.approx(0.5588235294117647) + assert dct['precision'] == pytest.approx(0.5555555555555556) + assert dct['recall'] == pytest.approx(0.5882352941176471) + assert dct['f1_score'] == pytest.approx(0.5714285714285715) + + def test_assert_min(bcr: BinaryClassificationRatios) -> None: """.""" bcr.assert_min(0.558, 0.555, 0.587) diff --git a/test/test_summary.py b/test/test_summary.py new file mode 100644 index 0000000..1ebc1de --- /dev/null +++ b/test/test_summary.py @@ -0,0 +1,34 @@ +""".""" + +import pytest + +from binary_classification_ratios.summary import BinaryClassificationSummary + + +@pytest.fixture +def bcs() -> BinaryClassificationSummary: + """.""" + summary = BinaryClassificationSummary() + return summary + + +def test_get_summary(bcs: BinaryClassificationSummary) -> None: + """.""" + dct = { + 'tp': 10, + 'tn': 9, + 'fp': 8, + 'fn': 7, + 'accuracy': 0.5588256789012345, + 'precision': 0.5561234, + 'recall': 0.5881234, + 'f1_score': 0.57101234, + } + ref = """Confusion matrix TP 10 TN 9 FP 8 FN 7 + accuracy 0.5588 + precision 0.56 + recall 0.59 + f1-score 0.57""" + bcs.accuracy_fmt = '.4f' + bcs.fmt = '.2f' + assert bcs.get_summary(dct) == ref diff --git a/uv.lock b/uv.lock index a852e57..91964f7 100644 --- a/uv.lock +++ b/uv.lock @@ -33,7 +33,7 @@ wheels = [ [[package]] name = "binary-classification-ratios" -version = "0.1.5" +version = "0.2.0" source = { editable = "." } [package.dev-dependencies]