• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

PyThaiNLP / pythainlp / 21632228205

03 Feb 2026 01:32PM UTC coverage: 65.571% (-0.08%) from 65.653%
21632228205

Pull #1270

github

web-flow
Merge c6560764b into adbcbd40b
Pull Request #1270: Align docstrings with type hints across codebase

260 of 347 new or added lines in 7 files covered. (74.93%)

3 existing lines in 3 files now uncovered.

5723 of 8728 relevant lines covered (65.57%)

0.66 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.95
/pythainlp/cli/benchmark.py
1
#!/usr/bin/env python3
2
# SPDX-FileCopyrightText: 2016-2026 PyThaiNLP Project
3
# SPDX-FileType: SOURCE
4
# SPDX-License-Identifier: Apache-2.0
5

6
from __future__ import annotations
1✔
7

8
import argparse
1✔
9
import json
1✔
10
import os
1✔
11
from typing import TYPE_CHECKING
1✔
12

13
from pythainlp import cli
1✔
14
from pythainlp.tools import safe_print
1✔
15

16
if TYPE_CHECKING:
17
    from collections.abc import Sequence
18

19

20
def _read_file(path: str) -> list[str]:
1✔
21
    with open(path, encoding="utf-8") as f:
1✔
22
        lines = (r.strip() for r in f.readlines())
1✔
23
    return list(lines)
1✔
24

25

26
class App:
1✔
27
    def __init__(self, argv: Sequence[str]) -> None:
1✔
28
        parser = argparse.ArgumentParser(
1✔
29
            prog="benchmark",
30
            description=(
31
                "Benchmark for various tasks;\n"
32
                "currently, we have only for word tokenization."
33
            ),
34
            usage=(
35
                "thainlp benchmark [task] [task-options]\n\n"
36
                "tasks:\n\n"
37
                "word-tokenization      benchmark word tokenization\n\n"
38
                "--"
39
            ),
40
        )
41

42
        parser.add_argument("task", type=str, help="[word-tokenization]")
1✔
43

44
        args = parser.parse_args(argv[2:3])
1✔
45
        cli.exit_if_empty(args.task, parser)
1✔
46
        task = str.lower(args.task)
1✔
47

48
        task_argv = argv[3:]
1✔
49
        if task == "word-tokenization":
1✔
50
            WordTokenizationBenchmark(task, task_argv)
1✔
51

52

53
class WordTokenizationBenchmark:
1✔
54
    def __init__(self, name: str, argv: Sequence[str]) -> None:
1✔
55
        parser = argparse.ArgumentParser(**cli.make_usage("benchmark " + name))  # type: ignore[arg-type]
1✔
56

57
        parser.add_argument(
1✔
58
            "--input-file",
59
            action="store",
60
            help="Path to input file to compare against the test file",
61
        )
62

63
        parser.add_argument(
1✔
64
            "--test-file",
65
            action="store",
66
            help="Path to test file i.e. ground truth",
67
        )
68

69
        parser.add_argument(
1✔
70
            "--save-details",
71
            default=False,
72
            action="store_true",
73
            help=(
74
                "Save comparison details to files (eval-XXX.json"
75
                " and eval-details-XXX.json)"
76
            ),
77
        )
78

79
        args = parser.parse_args(argv)
1✔
80

81
        actual = _read_file(args.input_file)
1✔
82
        expected = _read_file(args.test_file)
1✔
83

84
        if len(actual) != len(expected):
1✔
NEW
85
            raise ValueError(
×
86
                "Input and test files do not have the same number of samples"
87
            )
88

89
        safe_print(
1✔
90
            "Benchmarking %s against %s with %d samples in total"
91
            % (args.input_file, args.test_file, len(actual))
92
        )
93

94
        try:
1✔
95
            import yaml
1✔
96

97
            from pythainlp.benchmarks import word_tokenization
1✔
98
        except ImportError:
×
99
            raise ImportError(
×
100
                "Please install the extra dependencies `benchmarks` to use this command by running `pip install pythainlp[benchmarks]`"
101
            )
102

103
        df_raw = word_tokenization.benchmark(expected, actual)
1✔
104

105
        columns = [
1✔
106
            "char_level:tp",
107
            "char_level:fp",
108
            "char_level:tn",
109
            "char_level:fn",
110
            "word_level:correctly_tokenised_words",
111
            "word_level:total_words_in_sample",
112
            "word_level:total_words_in_ref_sample",
113
        ]
114

115
        statistics = {}
1✔
116

117
        for c in columns:
1✔
118
            statistics[c] = float(df_raw[c].sum())
1✔
119

120
        statistics["char_level:precision"] = statistics["char_level:tp"] / (
1✔
121
            statistics["char_level:tp"] + statistics["char_level:fp"]
122
        )
123

124
        statistics["char_level:recall"] = statistics["char_level:tp"] / (
1✔
125
            statistics["char_level:tp"] + statistics["char_level:fn"]
126
        )
127

128
        statistics["word_level:precision"] = (
1✔
129
            statistics["word_level:correctly_tokenised_words"]
130
            / statistics["word_level:total_words_in_sample"]
131
        )
132

133
        statistics["word_level:recall"] = (
1✔
134
            statistics["word_level:correctly_tokenised_words"]
135
            / statistics["word_level:total_words_in_ref_sample"]
136
        )
137

138
        safe_print("============== Benchmark Result ==============")
1✔
139

140
        for c in ["tp", "fn", "tn", "fp", "precision", "recall"]:
1✔
141
            c = f"char_level:{c}"
1✔
142
            v = statistics[c]
1✔
143
            safe_print(f"{c:>40s} {v:.4f}")
1✔
144

145
        for c in [
1✔
146
            "total_words_in_sample",
147
            "total_words_in_ref_sample",
148
            "correctly_tokenised_words",
149
            "precision",
150
            "recall",
151
        ]:
152
            c = f"word_level:{c}"
1✔
153
            v = statistics[c]
1✔
154
            safe_print(f"{c:>40s} {v:.4f}")
1✔
155

156
        if args.save_details:
1✔
157
            dir_name = os.path.dirname(args.input_file)
1✔
158
            file_name = args.input_file.split("/")[-1].split(".")[0]
1✔
159

160
            res_path = "%s/eval-%s.yml" % (dir_name, file_name)
1✔
161
            safe_print("Evaluation result is saved to %s" % res_path)
1✔
162

163
            with open(res_path, "w", encoding="utf-8") as outfile:
1✔
164
                yaml.dump(statistics, outfile, default_flow_style=False)
1✔
165

166
            res_path = "%s/eval-details-%s.json" % (dir_name, file_name)
1✔
167
            safe_print("Details of comparisons is saved to %s" % res_path)
1✔
168

169
            with open(res_path, "w", encoding="utf-8") as f:
1✔
170
                samples = []
1✔
171
                for i, r in enumerate(df_raw.to_dict("records")):
1✔
172
                    expected, actual = r["expected"], r["actual"]
1✔
173
                    del r["expected"]
1✔
174
                    del r["actual"]
1✔
175

176
                    samples.append(
1✔
177
                        {
178
                            "metrics": r,
179
                            "expected": expected,
180
                            "actual": actual,
181
                            "id": i,
182
                        }
183
                    )
184

185
                details = {"metrics": statistics, "samples": samples}
1✔
186

187
                json.dump(details, f, ensure_ascii=False)
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc