Skip to content

Commit 3af9ab2

Browse files
Add train/val/test split and fix URL stripping in normalize_phrase
Hybrid split: common expressions (50+ rules) get stratified per rule, rare ones stay in one split for zero leakage References #5077
1 parent 235fd61 commit 3af9ab2

1 file changed

Lines changed: 53 additions & 14 deletions

File tree

etc/scripts/dataset_pipeline/build_dataset.py

Lines changed: 53 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
# extracts required phrases from .RULE files
22
# outputs a JSONL dataset for NER model training
3+
import hashlib
34
import json
45
import re
56
import unicodedata
7+
from collections import Counter
68
from pathlib import Path
79
import click
810

@@ -66,20 +68,41 @@ def tag_tokens(text):
6668
return tokens, labels
6769

6870

71+
def assign_splits(results, threshold=50):
72+
"""80/10/10 split. common expressions (>= threshold rules) get split per-rule,
73+
rare ones stay together in one split"""
74+
expr_counts = Counter(e['license_expression'] for e in results)
75+
heavy = {e for e, c in expr_counts.items() if c >= threshold}
76+
77+
# rare expressions: assign each to the split that needs more rules
78+
light_exprs = sorted((e for e in expr_counts if e not in heavy),
79+
key=lambda x: (-expr_counts[x], x))
80+
total = sum(expr_counts[e] for e in light_exprs)
81+
targets = {'train': 0.8 * total, 'val': 0.1 * total, 'test': 0.1 * total}
82+
filled = {'train': 0, 'val': 0, 'test': 0}
83+
assignment = {}
84+
for expr in light_exprs:
85+
best = min(targets, key=lambda s: filled[s] / max(targets[s], 1))
86+
assignment[expr] = best
87+
filled[best] += expr_counts[expr]
88+
89+
return heavy, assignment
90+
91+
6992
@click.command()
7093
@click.option('--rules-dir', type=click.Path(exists=True), default=None,
7194
help='Path to rules directory (defaults to repo rules dir)')
72-
@click.option('--output', default='dataset-output/required_phrases.jsonl',
73-
help='Output JSONL path')
74-
def main(rules_dir, output):
95+
@click.option('--output-dir', default='dataset-output',
96+
help='Output directory for train/val/test JSONL files')
97+
def main(rules_dir, output_dir):
7598
"""Extract required phrases from rule files for NER training"""
7699
if not rules_dir:
77100
repo_rules = Path(__file__).resolve().parents[3] / 'src' / 'licensedcode' / 'data' / 'rules'
78101
rules_dir = str(repo_rules) if repo_rules.is_dir() else default_rules_data_dir
79102

80103
rules_path = Path(rules_dir)
81-
out_path = Path(output)
82-
out_path.parent.mkdir(parents=True, exist_ok=True)
104+
out_dir = Path(output_dir)
105+
out_dir.mkdir(parents=True, exist_ok=True)
83106

84107
total_rules = 0
85108
annotated = 0
@@ -133,21 +156,37 @@ def main(rules_dir, output):
133156
'required_phrases': valid_phrases,
134157
})
135158

136-
# write jsonl
137-
with open(out_path, 'w', encoding='utf-8') as f:
138-
for entry in results:
139-
f.write(json.dumps(entry, ensure_ascii=False) + '\n')
159+
# split by license expression and write
160+
heavy, assignment = assign_splits(results)
161+
splits = {'train': [], 'val': [], 'test': []}
162+
for entry in results:
163+
expr = entry['license_expression']
164+
if expr in heavy:
165+
# common expressions: hash rule name for 80/10/10
166+
bucket = int(hashlib.md5(entry['identifier'].encode('utf-8')).hexdigest(), 16) % 100
167+
if bucket < 80:
168+
splits['train'].append(entry)
169+
elif bucket < 90:
170+
splits['val'].append(entry)
171+
else:
172+
splits['test'].append(entry)
173+
else:
174+
splits[assignment[expr]].append(entry)
175+
176+
for name, records in splits.items():
177+
path = out_dir / f'{name}.jsonl'
178+
with open(path, 'w', encoding='utf-8') as f:
179+
for entry in records:
180+
f.write(json.dumps(entry, ensure_ascii=False) + '\n')
140181

141182
click.echo('\ndone')
142183
click.echo(f' rules scanned: {total_rules}')
143184
click.echo(f' annotated: {annotated}')
144185
click.echo(f' phrases extracted: {total_phrases}')
145-
click.echo(f' output: {out_path}')
146-
186+
click.echo(f' train: {len(splits["train"])} val: {len(splits["val"])} test: {len(splits["test"])}')
187+
click.echo(f' output: {out_dir}')
147188

148189
# stuff to do(follow up commits):
149-
# train/val/test split by license expression
150-
151-
190+
# tests to be added in script
152191
if __name__ == '__main__':
153192
main()

0 commit comments

Comments
 (0)