"""Feature Signature Analysis Tool
This build-time analysis tool examines the annotated syllable corpus to identify
which feature combinations actually exist in the data and how frequently each
combination appears.
A "feature signature" is the set of all active (True) features for a syllable.
For example, a syllable with only "starts_with_vowel" and "ends_with_vowel" active
would have the signature: ('ends_with_vowel', 'starts_with_vowel').
This analysis helps answer questions like:
- What feature patterns are most common in natural language?
- Are certain feature combinations rare or impossible?
- How diverse is the feature space in the corpus?
Output is saved to _working/analysis/feature_signatures/ for review.
"""
import argparse
from collections import Counter
from datetime import datetime
from pathlib import Path
from typing import Counter as CounterType
from typing import Dict, List, Optional, Tuple
from build_tools.syllable_analysis.common import (
default_paths,
ensure_output_dir,
generate_timestamped_path,
load_annotated_syllables,
)
[docs]
def analyze_feature_signatures(records: List[Dict]) -> Counter:
"""Analyze feature signatures across all syllable records.
Counts how many syllables share each unique feature signature.
Args:
records: List of syllable records from syllables_annotated.json
Each record should have "syllable", "frequency", and "features" keys
Returns:
Counter mapping feature signatures to occurrence counts
Example:
>>> records = [
... {"syllable": "ka", "features": {"starts_with_vowel": False}},
... {"syllable": "a", "features": {"starts_with_vowel": True}}
... ]
>>> counter = analyze_feature_signatures(records)
>>> counter[('starts_with_vowel',)]
1
"""
signature_counter: CounterType[Tuple[str, ...]] = Counter()
for record in records:
sig = extract_signature(record["features"])
signature_counter[sig] += 1
return signature_counter
[docs]
def save_report(report: str, output_dir: Path) -> Path:
"""Save the formatted report to the output directory.
Args:
report: Formatted report string
output_dir: Directory to save the report in
Returns:
Path to the saved report file
"""
# Ensure output directory exists
ensure_output_dir(output_dir)
# Generate timestamped output path
output_path = generate_timestamped_path(output_dir, "feature_signatures", "txt")
# Write report
output_path.write_text(report, encoding="utf-8")
return output_path
[docs]
def run_analysis(input_path: Path, output_dir: Path, limit: Optional[int] = None) -> Dict:
"""Run the complete feature signature analysis pipeline.
Args:
input_path: Path to syllables_annotated.json
output_dir: Directory to save analysis results
limit: Maximum number of signatures to include in report (None = all)
Returns:
Dictionary with analysis results including:
- total_syllables: Total number of syllables analyzed
- unique_signatures: Number of unique feature signatures
- output_path: Path to the saved report
"""
# Load annotated syllables
records = load_annotated_syllables(input_path)
# Analyze signatures
signature_counter = analyze_feature_signatures(records)
# Generate report
report = format_signature_report(signature_counter, len(records), limit)
# Save report
output_path = save_report(report, output_dir)
return {
"total_syllables": len(records),
"unique_signatures": len(signature_counter),
"output_path": output_path,
"signature_counter": signature_counter,
}
[docs]
def create_argument_parser() -> argparse.ArgumentParser:
"""
Create and return the argument parser for feature signature analysis.
This function creates the ArgumentParser with all CLI options but does not
parse arguments. This separation allows Sphinx documentation tools to
introspect the parser and auto-generate CLI documentation.
Returns
-------
argparse.ArgumentParser
Configured ArgumentParser ready to parse command-line arguments
"""
parser = argparse.ArgumentParser(
description="Analyze feature signatures in annotated syllable corpus",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples
========
.. code-block:: bash
# Analyze with default paths
python -m build_tools.syllable_analysis.feature_signatures
# Show only top 50 signatures
python -m build_tools.syllable_analysis.feature_signatures --limit 50
# Custom input/output paths
python -m build_tools.syllable_analysis.feature_signatures \\
--input data/annotated/syllables_annotated.json \\
--output _working/my_analysis/
""",
)
parser.add_argument(
"--input",
type=Path,
default=default_paths.annotated_syllables,
help=f"Path to syllables_annotated.json (default: {default_paths.annotated_syllables})",
)
parser.add_argument(
"--output",
type=Path,
default=default_paths.analysis_output_dir("feature_signatures"),
help=f"Output directory for analysis results (default: {default_paths.analysis_output_dir('feature_signatures')})",
)
parser.add_argument(
"--limit",
type=int,
default=None,
help="Limit number of signatures in report (default: show all)",
)
return parser
[docs]
def parse_args() -> argparse.Namespace:
"""Parse command-line arguments."""
parser = create_argument_parser()
return parser.parse_args()
[docs]
def main() -> None:
"""Main entry point for the feature signature analysis tool."""
args = parse_args()
# Validate input file exists
if not args.input.exists():
print(f"Error: Input file not found: {args.input}")
print("Have you run the syllable feature annotator yet?")
return
print(f"Analyzing feature signatures from: {args.input}")
print(f"Output directory: {args.output}")
if args.limit:
print(f"Showing top {args.limit} signatures")
print()
# Run analysis
result = run_analysis(args.input, args.output, args.limit)
# Display summary
print(f"✓ Analyzed {result['total_syllables']:,} syllables")
print(f"✓ Found {result['unique_signatures']:,} unique feature signatures")
print(f"✓ Report saved to: {result['output_path']}")
if __name__ == "__main__":
main()