Separate the clustered keyword list from the keywords to merge

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-17 22:38:30 +08:00
co-authored by Claude Opus 5
parent 04d5095e44
commit 5cf2b8ee8c
5 changed files with 164 additions and 46 deletions
@@ -8,11 +8,14 @@ Builds the coding groups from the pooled keyword list, given as
the first positional command-line argument, by sentence-embedding
every keyword and clustering the embeddings: the group membership,
given as the second positional argument, is written as a CSV file
holding the clustering result alone. The coding keyword set,
given as the third positional argument, is written as a JSON file
holding the group name keywords plus the researcher's a-priori
topic term (see :data:`EXTRA_KEYWORD`). The step is fully
deterministic; no LLM call is made.
holding the clustering result alone. The group name keywords
alone, given as the third positional argument, are written as a
text file, one per line. The coding keyword set for
``export-llm-input --extras``, given as the fourth positional
argument, is written as a JSON file holding the group name
keywords plus the researcher's a-priori topic term (see
:data:`EXTRA_KEYWORD`). The step is fully deterministic; no LLM
call is made.
"""
import argparse
import csv
@@ -49,14 +52,17 @@ def parse_args(argv: list[str] | None) -> argparse.Namespace:
" sentence embeddings of the pooled"
" keywords.")
parser.add_argument(
"keywords_txt", type=Path,
"pool_txt", type=Path,
help="the pooled keyword list, one keyword per line")
parser.add_argument(
"groups_csv", type=Path,
help="the group membership CSV output file")
parser.add_argument(
"keywords_json", type=Path,
help="the group name keyword JSON output file")
"keywords_txt", type=Path,
help="the group name keyword text output file")
parser.add_argument(
"keywords_to_merge_json", type=Path,
help="the coding keyword set JSON output file")
parser.add_argument(
"--model", default=MODEL,
help=f"the sentence embedding model (default \"{MODEL}\")")
@@ -221,14 +227,35 @@ def write_groups(path: Path, groups: dict[str, list[str]]) -> None:
writer.writerow([group, keyword])
def write_keywords(path: Path,
groups: dict[str, list[str]]) -> None:
def write_keyword_names(path: Path,
groups: dict[str, list[str]]) -> None:
"""Write the group name keyword text file.
Writes a text file holding the lexicographically sorted
group names, one per line, UTF-8, LF line endings, with a
trailing newline. The file records the clustering result
alone; it holds no :data:`EXTRA_KEYWORD` line.
:param path: The path of the keyword text file to write.
:param groups: The keyword members of every group, keyed by
the group's medoid name.
:return: None.
:raises OSError: When the file cannot be written.
"""
names: list[str] = sorted(groups.keys())
path.write_text(
"".join(f"{x}\n" for x in names), encoding="utf-8")
def write_keywords_to_merge(path: Path,
groups: dict[str, list[str]]) -> None:
"""Write the coding keyword set JSON file.
Writes a JSON file holding a single object with one
``keywords`` key, whose value is the lexicographically
sorted list of the group names plus :data:`EXTRA_KEYWORD`,
UTF-8, with a trailing newline.
UTF-8, with a trailing newline. This is the file
``export-llm-input --extras`` consumes.
:param path: The path of the keyword JSON file to write.
:param groups: The keyword members of every group, keyed by
@@ -248,8 +275,10 @@ def main(argv: list[str] | None = None) -> int:
"""Cluster the pooled keywords into the coding groups.
Writes the group membership CSV file, holding the clustering
result alone, and the coding keyword set JSON file, holding
the group names plus :data:`EXTRA_KEYWORD`.
result alone; the group name keyword text file, holding the
same group names as a readable list; and the coding keyword
set JSON file, holding the group names plus
:data:`EXTRA_KEYWORD`.
:param argv: The command-line arguments, or None for
``sys.argv``.
@@ -258,7 +287,7 @@ def main(argv: list[str] | None = None) -> int:
started: float = time.monotonic()
args: argparse.Namespace = parse_args(argv)
try:
keywords: list[str] = load_keywords(args.keywords_txt)
keywords: list[str] = load_keywords(args.pool_txt)
except (OSError, ValueError) as error:
print(f"error: {error}", file=sys.stderr)
return 1
@@ -272,9 +301,12 @@ def main(argv: list[str] | None = None) -> int:
print(f"error: {error}", file=sys.stderr)
return 1
args.groups_csv.parent.mkdir(parents=True, exist_ok=True)
args.keywords_json.parent.mkdir(parents=True, exist_ok=True)
args.keywords_txt.parent.mkdir(parents=True, exist_ok=True)
args.keywords_to_merge_json.parent.mkdir(
parents=True, exist_ok=True)
write_groups(args.groups_csv, groups)
write_keywords(args.keywords_json, groups)
write_keyword_names(args.keywords_txt, groups)
write_keywords_to_merge(args.keywords_to_merge_json, groups)
elapsed: str = format_duration(time.monotonic() - started)
print(
f"done: {len(keywords)} keywords clustered into"