"""Analyzer for ID uniqueness and overlap detection.""" from collections import Counter from typing import List, Dict, Any import logging from .base_analyzer import BaseAnalyzer from ..utils.stats_utils import merge_id_sets, calculate_overlap logger = logging.getLogger(__name__) class IDAnalyzer(BaseAnalyzer): """Analyze ID uniqueness, duplicates, and overlaps between datasets.""" def __init__(self, output_dir): super().__init__("id", output_dir) def process_chunk(self, records: List[Dict[str, Any]]) -> Dict[str, Any]: """Process a chunk of records for ID analysis. Args: records: List of JSON records Returns: Intermediate results for this chunk """ id_counter = Counter() missing_id_count = 0 id_lengths = [] for record in records: if "id" in record: id_val = record["id"] id_counter[id_val] += 1 id_lengths.append(len(id_val)) else: missing_id_count += 1 return { "id_counter": dict(id_counter), "missing_id_count": missing_id_count, "id_lengths": id_lengths, "total_records": len(records), } def aggregate(self, chunk_results: List[Dict[str, Any]]) -> Dict[str, Any]: """Aggregate results from all chunks. Args: chunk_results: List of results from each chunk Returns: Final aggregated analysis """ from ..utils.stats_utils import aggregate_counters logger.info("ID Analyzer: Starting aggregation...") total_missing = 0 total_records = 0 all_id_lengths = [] # Collect all ID counters and lengths id_counters = [] for chunk in chunk_results: id_counters.append(chunk.get("id_counter", {})) total_missing += chunk.get("missing_id_count", 0) total_records += chunk.get("total_records", 0) all_id_lengths.extend(chunk.get("id_lengths", [])) logger.info(f"ID Analyzer: Merging {len(id_counters)} chunk counters...") # Merge all ID counters global_id_counter = aggregate_counters(id_counters) logger.info("ID Analyzer: Calculating statistics...") # Count unique IDs and duplicates unique_count = len(global_id_counter) duplicate_count = sum(1 for count in global_id_counter.values() if count > 1) total_duplicate_records = sum(count - 1 for count in global_id_counter.values() if count > 1) # Extract duplicate IDs list duplicate_ids_list = [ (id_val, count) for id_val, count in global_id_counter.items() if count > 1 ] duplicate_ids_list.sort(key=lambda x: x[1], reverse=True) # Sort by count descending # ID length statistics id_length_dist = self.calculate_distribution(all_id_lengths) if all_id_lengths else {} logger.info("ID Analyzer: Aggregation complete") return { "summary": { "total_records": total_records, "records_with_id": total_records - total_missing, "records_without_id": total_missing, "unique_ids": unique_count, "duplicate_ids": duplicate_count, "total_duplicates": total_duplicate_records, }, "id_length_distribution": id_length_dist, "id_set": set(global_id_counter.keys()), # For overlap analysis "duplicate_ids_list": duplicate_ids_list, # List of (id, count) tuples } def analyze_overlap(self, train_results: Dict, val_results: Dict) -> Dict[str, Any]: """Analyze overlap between training and validation sets. Args: train_results: Results from training set analysis val_results: Results from validation set analysis Returns: Overlap statistics """ train_ids = train_results.get("id_set", set()) val_ids = val_results.get("id_set", set()) overlap_stats = calculate_overlap(train_ids, val_ids) # Get sample of overlapping IDs overlapping_ids = train_ids & val_ids overlap_sample = sorted(overlapping_ids)[:100] return {"overlap_statistics": overlap_stats, "overlap_sample": overlap_sample}