"""Core comparison logic for analyzing differences between data versions.""" import json from pathlib import Path from typing import Any, Dict, List, Set, Tuple from .load_profiles import get_version_summary def compare_id_sets(old_ids: Set[str], new_ids: Set[str]) -> Tuple[Set[str], Set[str], Set[str]]: """Compare two ID sets and return added, removed, and common IDs. Args: old_ids: Set of IDs from the older version new_ids: Set of IDs from the newer version Returns: Tuple of (added_ids, removed_ids, common_ids) """ added = new_ids - old_ids removed = old_ids - new_ids common = old_ids & new_ids return added, removed, common def compare_tag_distributions( old_analysis: Dict[str, Any], new_analysis: Dict[str, Any], top_n: int = 50 ) -> Dict[str, Any]: """Compare tag distributions between two versions. Args: old_analysis: Analysis results from older version new_analysis: Analysis results from newer version top_n: Number of top tags to analyze Returns: Dictionary with tag comparison results """ old_tags = old_analysis.get("tags", {}) new_tags = new_analysis.get("tags", {}) old_top = dict(old_tags.get("top_tags", [])[:top_n]) new_top = dict(new_tags.get("top_tags", [])[:top_n]) # Find tags that appeared or disappeared from top N old_tag_set = set(old_top.keys()) new_tag_set = set(new_top.keys()) new_tags_in_top = new_tag_set - old_tag_set removed_tags_from_top = old_tag_set - new_tag_set # Calculate count changes for common tags common_tags = old_tag_set & new_tag_set tag_count_changes = {} for tag in common_tags: old_count = old_top[tag] new_count = new_top[tag] change = new_count - old_count pct_change = (change / old_count * 100) if old_count > 0 else 0 tag_count_changes[tag] = { "old_count": old_count, "new_count": new_count, "change": change, "pct_change": round(pct_change, 2), } # Sort by absolute change top_movers = sorted(tag_count_changes.items(), key=lambda x: abs(x[1]["change"]), reverse=True)[:20] return { "old_unique_tags": old_tags.get("summary", {}).get("unique_tags", 0), "new_unique_tags": new_tags.get("summary", {}).get("unique_tags", 0), "new_tags_in_top_n": list(new_tags_in_top), "removed_tags_from_top_n": list(removed_tags_from_top), "top_movers": dict(top_movers), "new_tag_examples": {tag: new_top[tag] for tag in list(new_tags_in_top)[:10]}, } def compare_language_distributions( old_analysis: Dict[str, Any], new_analysis: Dict[str, Any] ) -> Dict[str, Any]: """Compare language distributions between two versions. Args: old_analysis: Analysis results from older version new_analysis: Analysis results from newer version Returns: Dictionary with language comparison results """ old_lang = old_analysis.get("language", {}) new_lang = new_analysis.get("language", {}) old_dist = dict(old_lang.get("language_distribution", [])) new_dist = dict(new_lang.get("language_distribution", [])) # Find new and removed languages old_lang_set = set(old_dist.keys()) new_lang_set = set(new_dist.keys()) new_languages = new_lang_set - old_lang_set removed_languages = old_lang_set - new_lang_set # Calculate changes for common languages common_langs = old_lang_set & new_lang_set lang_changes = {} for lang in common_langs: old_count = old_dist[lang] new_count = new_dist[lang] change = new_count - old_count pct_change = (change / old_count * 100) if old_count > 0 else 0 lang_changes[lang] = { "old_count": old_count, "new_count": new_count, "change": change, "pct_change": round(pct_change, 2), } # Top movers top_movers = sorted(lang_changes.items(), key=lambda x: abs(x[1]["change"]), reverse=True)[:10] return { "old_unique_languages": old_lang.get("summary", {}).get("unique_languages", 0), "new_unique_languages": new_lang.get("summary", {}).get("unique_languages", 0), "old_mismatch_rate": old_lang.get("summary", {}).get("mismatch_rate", 0.0), "new_mismatch_rate": new_lang.get("summary", {}).get("mismatch_rate", 0.0), "new_languages": list(new_languages), "removed_languages": list(removed_languages), "top_movers": dict(top_movers), } def compare_source_distributions( old_analysis: Dict[str, Any], new_analysis: Dict[str, Any] ) -> Dict[str, Any]: """Compare data source distributions between two versions. Args: old_analysis: Analysis results from older version new_analysis: Analysis results from newer version Returns: Dictionary with source comparison results """ old_paths = old_analysis.get("paths", {}) new_paths = new_analysis.get("paths", {}) old_local = dict(old_paths.get("local_directories", [])) new_local = dict(new_paths.get("local_directories", [])) # Find new and removed source directories old_dirs = set(old_local.keys()) new_dirs = set(new_local.keys()) new_directories = new_dirs - old_dirs removed_directories = old_dirs - new_dirs # Calculate changes for common directories common_dirs = old_dirs & new_dirs dir_changes = {} for dir_path in common_dirs: old_count = old_local[dir_path] new_count = new_local[dir_path] change = new_count - old_count pct_change = (change / old_count * 100) if old_count > 0 else 0 dir_changes[dir_path] = { "old_count": old_count, "new_count": new_count, "change": change, "pct_change": round(pct_change, 2), } # Top movers top_movers = sorted(dir_changes.items(), key=lambda x: abs(x[1]["change"]), reverse=True)[:10] return { "old_unique_dirs": old_paths.get("summary", {}).get("unique_local_dirs", 0), "new_unique_dirs": new_paths.get("summary", {}).get("unique_local_dirs", 0), "old_anomalies": old_paths.get("summary", {}).get("total_anomalies", 0), "new_anomalies": new_paths.get("summary", {}).get("total_anomalies", 0), "new_directories": list(new_directories), "removed_directories": list(removed_directories), "top_movers": dict(top_movers), "new_directory_examples": {d: new_local[d] for d in list(new_directories)[:5]}, } def compare_stem_distributions( old_analysis: Dict[str, Any], new_analysis: Dict[str, Any] ) -> Dict[str, Any]: """Compare stem distributions between two versions. Args: old_analysis: Analysis results from older version new_analysis: Analysis results from newer version Returns: Dictionary with stem comparison results """ old_stems = old_analysis.get("stems", {}) new_stems = new_analysis.get("stems", {}) old_freq = dict(old_stems.get("stem_names_frequency", [])) new_freq = dict(new_stems.get("stem_names_frequency", [])) # Find new and removed stem types old_stem_set = set(old_freq.keys()) new_stem_set = set(new_freq.keys()) new_stem_types = new_stem_set - old_stem_set removed_stem_types = old_stem_set - new_stem_set # Calculate changes for common stems common_stems = old_stem_set & new_stem_set stem_changes = {} for stem in common_stems: old_count = old_freq[stem] new_count = new_freq[stem] change = new_count - old_count pct_change = (change / old_count * 100) if old_count > 0 else 0 stem_changes[stem] = { "old_count": old_count, "new_count": new_count, "change": change, "pct_change": round(pct_change, 2), } # Top movers top_movers = sorted(stem_changes.items(), key=lambda x: abs(x[1]["change"]), reverse=True)[:10] # Check for stems_captions changes (important for v4→v5, v8→v9) old_captions = old_stems.get("summary", {}).get("records_with_stems_captions", 0) new_captions = new_stems.get("summary", {}).get("records_with_stems_captions", 0) return { "old_records_with_stems": old_stems.get("summary", {}).get("records_with_stems", 0), "new_records_with_stems": new_stems.get("summary", {}).get("records_with_stems", 0), "old_stems_captions": old_captions, "new_stems_captions": new_captions, "stems_captions_change": new_captions - old_captions, "old_unique_stem_names": old_stems.get("summary", {}).get("unique_stem_names", 0), "new_unique_stem_names": new_stems.get("summary", {}).get("unique_stem_names", 0), "new_stem_types": list(new_stem_types), "removed_stem_types": list(removed_stem_types), "top_movers": dict(top_movers), } def compare_quality_metrics( old_analysis: Dict[str, Any], new_analysis: Dict[str, Any] ) -> Dict[str, Any]: """Compare quality metrics between two versions. Args: old_analysis: Analysis results from older version new_analysis: Analysis results from newer version Returns: Dictionary with quality metric comparison """ old_id = old_analysis.get("id", {}).get("summary", {}) new_id = new_analysis.get("id", {}).get("summary", {}) old_lang = old_analysis.get("language", {}).get("summary", {}) new_lang = new_analysis.get("language", {}).get("summary", {}) old_paths = old_analysis.get("paths", {}).get("summary", {}) new_paths = new_analysis.get("paths", {}).get("summary", {}) old_text = old_analysis.get("text", {}).get("summary", {}) new_text = new_analysis.get("text", {}).get("summary", {}) return { "duplicates": { "old": old_id.get("duplicate_ids", 0), "new": new_id.get("duplicate_ids", 0), "change": new_id.get("duplicate_ids", 0) - old_id.get("duplicate_ids", 0), }, "language_mismatch_rate": { "old": old_lang.get("mismatch_rate", 0.0), "new": new_lang.get("mismatch_rate", 0.0), "change": new_lang.get("mismatch_rate", 0.0) - old_lang.get("mismatch_rate", 0.0), }, "path_anomalies": { "old": old_paths.get("total_anomalies", 0), "new": new_paths.get("total_anomalies", 0), "change": new_paths.get("total_anomalies", 0) - old_paths.get("total_anomalies", 0), }, "empty_texts": { "old": old_text.get("empty_texts", 0), "new": new_text.get("empty_texts", 0), "change": new_text.get("empty_texts", 0) - old_text.get("empty_texts", 0), }, "missing_tags": { "old": old_analysis.get("tags", {}).get("summary", {}).get("records_without_tags", 0), "new": new_analysis.get("tags", {}).get("summary", {}).get("records_without_tags", 0), "change": new_analysis.get("tags", {}).get("summary", {}).get("records_without_tags", 0) - old_analysis.get("tags", {}).get("summary", {}).get("records_without_tags", 0), }, } def compare_versions( old_analysis: Dict[str, Any], new_analysis: Dict[str, Any], old_ids: Set[str], new_ids: Set[str], old_version: str, new_version: str, ) -> Dict[str, Any]: """Perform comprehensive comparison between two versions. Args: old_analysis: Analysis results from older version new_analysis: Analysis results from newer version old_ids: Set of IDs from older version new_ids: Set of IDs from newer version old_version: Version string (e.g., "v0") new_version: Version string (e.g., "v1") Returns: Dictionary with comprehensive comparison results """ # Get summaries old_summary = get_version_summary(old_analysis) new_summary = get_version_summary(new_analysis) # Compare ID sets added_ids, removed_ids, common_ids = compare_id_sets(old_ids, new_ids) # Calculate summary statistics summary = { "old_version": old_version, "new_version": new_version, "old_total_records": old_summary["total_records"], "new_total_records": new_summary["total_records"], "record_change": new_summary["total_records"] - old_summary["total_records"], "added_ids_count": len(added_ids), "removed_ids_count": len(removed_ids), "common_ids_count": len(common_ids), "net_change": len(added_ids) - len(removed_ids), } # Perform detailed comparisons return { "summary": summary, "old_summary": old_summary, "new_summary": new_summary, "id_changes": { "added_count": len(added_ids), "removed_count": len(removed_ids), "common_count": len(common_ids), "added_ids": list(added_ids), "removed_ids": list(removed_ids), }, "tag_changes": compare_tag_distributions(old_analysis, new_analysis), "language_changes": compare_language_distributions(old_analysis, new_analysis), "source_changes": compare_source_distributions(old_analysis, new_analysis), "stem_changes": compare_stem_distributions(old_analysis, new_analysis), "quality_metrics": compare_quality_metrics(old_analysis, new_analysis), } def save_comparison_results(results: Dict[str, Any], output_dir: Path) -> None: """Save comparison results to JSON files. Args: results: Dictionary with comparison results output_dir: Directory to save results """ output_dir.mkdir(parents=True, exist_ok=True) # Save summary with open(output_dir / "summary.json", "w") as f: json.dump( { "summary": results["summary"], "old_summary": results["old_summary"], "new_summary": results["new_summary"], }, f, indent=2, ) # Save ID changes (without full ID lists, which can be huge) id_changes_summary = { "added_count": results["id_changes"]["added_count"], "removed_count": results["id_changes"]["removed_count"], "common_count": results["id_changes"]["common_count"], } with open(output_dir / "id_changes.json", "w") as f: json.dump(id_changes_summary, f, indent=2) # Save full ID lists to separate text files with open(output_dir / "added_ids.txt", "w") as f: for id_str in sorted(results["id_changes"]["added_ids"]): f.write(f"{id_str}\n") with open(output_dir / "removed_ids.txt", "w") as f: for id_str in sorted(results["id_changes"]["removed_ids"]): f.write(f"{id_str}\n") # Save other comparisons with open(output_dir / "tag_changes.json", "w") as f: json.dump(results["tag_changes"], f, indent=2) with open(output_dir / "language_changes.json", "w") as f: json.dump(results["language_changes"], f, indent=2) with open(output_dir / "source_changes.json", "w") as f: json.dump(results["source_changes"], f, indent=2) with open(output_dir / "stem_changes.json", "w") as f: json.dump(results["stem_changes"], f, indent=2) with open(output_dir / "quality_metrics.json", "w") as f: json.dump(results["quality_metrics"], f, indent=2) print(f"Comparison results saved to {output_dir}")