feat: add diversity threshold — skip groups with unique_ratio > 0.9 or methods < min_methods
Adds --min-methods CLI flag (default 5). Groups that are too diverse or too small are skipped with skip_reason in meta. Prevents noisy/meaningless grammars from diverse packages.
This commit is contained in:
parent
dfb56a083a
commit
ab20b03256
1 changed files with 41 additions and 6 deletions
|
|
@ -245,7 +245,7 @@ def _preprocess_files(file_paths):
|
||||||
return sequences, seq_files
|
return sequences, seq_files
|
||||||
|
|
||||||
|
|
||||||
def analyze_clusters(file_paths, extension, project_root="", min_coverage=DEFAULT_COVERAGE, prefer=None, kmax=2, N=3, include_kore=False, method='langsize'):
|
def analyze_clusters(file_paths, extension, project_root="", min_coverage=DEFAULT_COVERAGE, prefer=None, kmax=2, N=3, include_kore=False, method='langsize', min_methods=5):
|
||||||
"""Run full pipeline: preprocess → frequency filter → ensemble infer.
|
"""Run full pipeline: preprocess → frequency filter → ensemble infer.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
|
@ -264,25 +264,50 @@ def analyze_clusters(file_paths, extension, project_root="", min_coverage=DEFAUL
|
||||||
packages = _top_packages(cluster_fps, project_root)
|
packages = _top_packages(cluster_fps, project_root)
|
||||||
|
|
||||||
symbol_seqs = [[text for _, text, _ in seq] for seq in sequences]
|
symbol_seqs = [[text for _, text, _ in seq] for seq in sequences]
|
||||||
result = infer_ensemble(symbol_seqs, kmax=kmax, N=N, prefer=prefer, min_coverage=min_coverage, include_kore=include_kore, method=method)
|
|
||||||
|
# Diversity threshold
|
||||||
|
n_methods = len(symbol_seqs)
|
||||||
meta = {"files": cluster_fps, "imports": imports, "arg_patterns": arg_patterns, "packages": packages}
|
meta = {"files": cluster_fps, "imports": imports, "arg_patterns": arg_patterns, "packages": packages}
|
||||||
|
if n_methods < min_methods:
|
||||||
|
meta["skip_reason"] = "too_few_methods"
|
||||||
|
return [("(all methods)", None, len(sequences), meta)]
|
||||||
|
unique_seqs = len(set(tuple(s) for s in symbol_seqs))
|
||||||
|
unique_ratio = unique_seqs / n_methods
|
||||||
|
if unique_ratio > 0.9:
|
||||||
|
meta["skip_reason"] = "too_diverse"
|
||||||
|
return [("(all methods)", None, len(sequences), meta)]
|
||||||
|
|
||||||
|
result = infer_ensemble(symbol_seqs, kmax=kmax, N=N, prefer=prefer, min_coverage=min_coverage, include_kore=include_kore, method=method)
|
||||||
|
|
||||||
return [("(all methods)", result, len(sequences), meta)]
|
return [("(all methods)", result, len(sequences), meta)]
|
||||||
|
|
||||||
|
|
||||||
def _infer_group(label, group_seqs, group_files, project_root, min_coverage, prefer, kmax, N, include_kore=False, method='langsize'):
|
def _infer_group(label, group_seqs, group_files, project_root, min_coverage, prefer, kmax, N, include_kore=False, method='langsize', min_methods=5):
|
||||||
"""Infer grammar for one package group. Module-level for ProcessPoolExecutor."""
|
"""Infer grammar for one package group. Module-level for ProcessPoolExecutor."""
|
||||||
filtered = frequency_filter(group_seqs, min_coverage=0.2)
|
filtered = frequency_filter(group_seqs, min_coverage=0.2)
|
||||||
imports = _extract_imports(group_files)
|
imports = _extract_imports(group_files)
|
||||||
arg_patterns = _build_arg_patterns(group_files)
|
arg_patterns = _build_arg_patterns(group_files)
|
||||||
packages = _top_packages(group_files, project_root)
|
packages = _top_packages(group_files, project_root)
|
||||||
symbol_seqs = [[text for _, text, _ in seq] for seq in filtered]
|
symbol_seqs = [[text for _, text, _ in seq] for seq in filtered]
|
||||||
|
|
||||||
|
# Diversity threshold: skip if too few methods or too diverse
|
||||||
|
n_methods = len(symbol_seqs)
|
||||||
|
if n_methods < min_methods:
|
||||||
|
meta = {"files": group_files, "imports": imports, "arg_patterns": arg_patterns, "packages": packages, "skip_reason": "too_few_methods"}
|
||||||
|
return (label, None, len(filtered), meta)
|
||||||
|
|
||||||
|
unique_seqs = len(set(tuple(s) for s in symbol_seqs))
|
||||||
|
unique_ratio = unique_seqs / n_methods
|
||||||
|
if unique_ratio > 0.9:
|
||||||
|
meta = {"files": group_files, "imports": imports, "arg_patterns": arg_patterns, "packages": packages, "skip_reason": "too_diverse"}
|
||||||
|
return (label, None, len(filtered), meta)
|
||||||
|
|
||||||
result = infer_ensemble(symbol_seqs, kmax=kmax, N=N, prefer=prefer, min_coverage=min_coverage, include_kore=include_kore)
|
result = infer_ensemble(symbol_seqs, kmax=kmax, N=N, prefer=prefer, min_coverage=min_coverage, include_kore=include_kore)
|
||||||
meta = {"files": group_files, "imports": imports, "arg_patterns": arg_patterns, "packages": packages}
|
meta = {"files": group_files, "imports": imports, "arg_patterns": arg_patterns, "packages": packages}
|
||||||
return (label, result, len(filtered), meta)
|
return (label, result, len(filtered), meta)
|
||||||
|
|
||||||
|
|
||||||
def analyze_by_package(file_paths, extension, project_root="", min_coverage=DEFAULT_COVERAGE, prefer=None, kmax=2, N=3, min_pkg_size=3, include_kore=False, method='langsize'):
|
def analyze_by_package(file_paths, extension, project_root="", min_coverage=DEFAULT_COVERAGE, prefer=None, kmax=2, N=3, min_pkg_size=3, include_kore=False, method='langsize', min_methods=5):
|
||||||
"""Preprocess and group by package directory, infer per group.
|
"""Preprocess and group by package directory, infer per group.
|
||||||
|
|
||||||
Groups methods by their file's relative directory path, merging
|
Groups methods by their file's relative directory path, merging
|
||||||
|
|
@ -317,7 +342,7 @@ def analyze_by_package(file_paths, extension, project_root="", min_coverage=DEFA
|
||||||
gs = [sequences[i] for i in indices]
|
gs = [sequences[i] for i in indices]
|
||||||
gf = set(seq_files[i] for i in indices)
|
gf = set(seq_files[i] for i in indices)
|
||||||
f = ex.submit(_infer_group, label, gs, gf, project_root,
|
f = ex.submit(_infer_group, label, gs, gf, project_root,
|
||||||
min_coverage, prefer, kmax, N, include_kore, method)
|
min_coverage, prefer, kmax, N, include_kore, method, min_methods)
|
||||||
futures[f] = label
|
futures[f] = label
|
||||||
|
|
||||||
for f in as_completed(futures):
|
for f in as_completed(futures):
|
||||||
|
|
@ -427,6 +452,7 @@ def analyze_directory(
|
||||||
main_only=False,
|
main_only=False,
|
||||||
include_kore=False,
|
include_kore=False,
|
||||||
method='langsize',
|
method='langsize',
|
||||||
|
min_methods=5,
|
||||||
):
|
):
|
||||||
"""Scan a directory and run analysis for each language found.
|
"""Scan a directory and run analysis for each language found.
|
||||||
|
|
||||||
|
|
@ -462,6 +488,7 @@ def analyze_directory(
|
||||||
kmax=kmax,
|
kmax=kmax,
|
||||||
include_kore=include_kore,
|
include_kore=include_kore,
|
||||||
method=method,
|
method=method,
|
||||||
|
min_methods=min_methods,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
results[ext] = analyze_clusters(
|
results[ext] = analyze_clusters(
|
||||||
|
|
@ -472,6 +499,7 @@ def analyze_directory(
|
||||||
kmax=kmax,
|
kmax=kmax,
|
||||||
include_kore=include_kore,
|
include_kore=include_kore,
|
||||||
method=method,
|
method=method,
|
||||||
|
min_methods=min_methods,
|
||||||
)
|
)
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
@ -618,6 +646,10 @@ def _parse_args(argv=None):
|
||||||
"--scoring-method", choices=["langsize", "mdl"], default="langsize",
|
"--scoring-method", choices=["langsize", "mdl"], default="langsize",
|
||||||
help="Scoring method: langsize (default, Bex et al.) or mdl (fallback)",
|
help="Scoring method: langsize (default, Bex et al.) or mdl (fallback)",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--min-methods", type=int, default=5,
|
||||||
|
help="Minimum methods per group to infer grammar (default: 5). Groups with fewer are skipped.",
|
||||||
|
)
|
||||||
return parser.parse_args(argv)
|
return parser.parse_args(argv)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -638,6 +670,7 @@ def main():
|
||||||
main_only=args.main_only,
|
main_only=args.main_only,
|
||||||
include_kore=args.kore,
|
include_kore=args.kore,
|
||||||
method=args.scoring_method,
|
method=args.scoring_method,
|
||||||
|
min_methods=args.min_methods,
|
||||||
)
|
)
|
||||||
|
|
||||||
if args.json_flag or args.format == "json":
|
if args.json_flag or args.format == "json":
|
||||||
|
|
@ -654,7 +687,9 @@ def main():
|
||||||
print(f" Grammar: {best['grammar']}")
|
print(f" Grammar: {best['grammar']}")
|
||||||
print(f" Score: {best['mdl_score']}")
|
print(f" Score: {best['mdl_score']}")
|
||||||
else:
|
else:
|
||||||
print(f" ╰─ {label} ({count} methods) — no grammar")
|
reason = meta.get("skip_reason", "")
|
||||||
|
suffix = f" — {reason}" if reason else ""
|
||||||
|
print(f" ╰─ {label} ({count} methods) — no grammar{suffix}")
|
||||||
imps = meta.get("imports", [])
|
imps = meta.get("imports", [])
|
||||||
if imps:
|
if imps:
|
||||||
joined = " | ".join(imps[:6])
|
joined = " | ".join(imps[:6])
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue