Narrative Comparison

Compare narratives to find similarities and differences.

Basic Comparison

use spatial_narrative::analysis::{compare_narratives, ComparisonConfig};
use spatial_narrative::core::Narrative;

let similarity = compare_narratives(&narrative1, &narrative2, &ComparisonConfig::default());

println!("Similarity scores:");
println!("  Spatial: {:.2}", similarity.spatial);
println!("  Temporal: {:.2}", similarity.temporal);
println!("  Thematic: {:.2}", similarity.thematic);
println!("  Overall: {:.2}", similarity.overall);

NarrativeSimilarity

The comparison returns a NarrativeSimilarity struct:

FieldTypeDescription
spatialf64Geographic overlap (0.0 to 1.0)
temporalf64Time overlap (0.0 to 1.0)
thematicf64Tag/topic similarity (0.0 to 1.0)
overallf64Weighted average score

Scores range from 0.0 (no similarity) to 1.0 (identical).

ComparisonConfig

Customize how similarity is calculated:

use spatial_narrative::analysis::ComparisonConfig;

let config = ComparisonConfig {
    spatial_weight: 0.4,    // 40% weight on geography
    temporal_weight: 0.3,   // 30% weight on time
    thematic_weight: 0.3,   // 30% weight on themes
    spatial_threshold_km: 10.0,  // Events within 10km considered "same place"
    temporal_threshold_hours: 24.0,  // Events within 24h considered "same time"
};

let similarity = compare_narratives(&n1, &n2, &config);

Spatial Comparison Functions

Spatial Similarity

Calculate geographic overlap:

use spatial_narrative::analysis::spatial_similarity;

let score = spatial_similarity(&narrative1, &narrative2);
println!("Spatial similarity: {:.2}", score);  // 0.0 to 1.0

Spatial Intersection

Find events that occur in similar locations:

use spatial_narrative::analysis::spatial_intersection;

// Events within 5km of each other
let matching_pairs = spatial_intersection(&narrative1, &narrative2, 5000.0);

for (e1, e2) in matching_pairs {
    println!("Match: '{}' and '{}'", e1.text, e2.text);
    println!("  Distance: {:.0}m apart", 
        e1.location.distance_to(&e2.location));
}

Spatial Union

Get combined geographic bounds:

use spatial_narrative::analysis::spatial_union;

let combined_bounds = spatial_union(&narrative1, &narrative2);
println!("Combined area: {:.2}° x {:.2}°",
    combined_bounds.lat_span(),
    combined_bounds.lon_span());

Temporal Comparison

Temporal Similarity

use spatial_narrative::analysis::temporal_similarity;

let score = temporal_similarity(&narrative1, &narrative2);
println!("Temporal similarity: {:.2}", score);

Thematic Comparison

Thematic Similarity

Compare based on shared tags:

use spatial_narrative::analysis::thematic_similarity;

let score = thematic_similarity(&narrative1, &narrative2);
println!("Thematic similarity: {:.2}", score);

Common Locations

Find locations that appear in both narratives:

use spatial_narrative::analysis::common_locations;

// Locations within 1km considered the same
let shared = common_locations(&narrative1, &narrative2, 1000.0);

println!("Common locations ({}):", shared.len());
for loc in shared {
    println!("  ({:.4}, {:.4})", loc.lat, loc.lon);
}

Use Cases

let threshold = 0.5;  // 50% similarity threshold
let mut related = Vec::new();

for candidate in &all_narratives {
    let sim = compare_narratives(&target, candidate, &ComparisonConfig::default());
    if sim.overall > threshold {
        related.push((candidate, sim.overall));
    }
}

// Sort by similarity
related.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());

println!("Related narratives:");
for (narrative, score) in related.iter().take(5) {
    println!("  {}: {:.0}% similar", 
        narrative.title.as_deref().unwrap_or("Untitled"),
        score * 100.0);
}

Detecting Duplicate Reports

let config = ComparisonConfig {
    spatial_threshold_km: 0.1,      // 100m
    temporal_threshold_hours: 1.0,   // 1 hour
    ..Default::default()
};

for i in 0..narratives.len() {
    for j in (i + 1)..narratives.len() {
        let sim = compare_narratives(&narratives[i], &narratives[j], &config);
        
        if sim.overall > 0.9 {
            println!("Potential duplicate:");
            println!("  '{}' and '{}'",
                narratives[i].title.as_deref().unwrap_or("?"),
                narratives[j].title.as_deref().unwrap_or("?"));
            println!("  Similarity: {:.0}%", sim.overall * 100.0);
        }
    }
}

Clustering Narratives by Topic

// Group narratives by thematic similarity
let mut groups: Vec<Vec<&Narrative>> = Vec::new();

for narrative in &narratives {
    let mut found_group = false;
    
    for group in &mut groups {
        // Check if similar to group representative
        let sim = thematic_similarity(narrative, group[0]);
        if sim > 0.6 {
            group.push(narrative);
            found_group = true;
            break;
        }
    }
    
    if !found_group {
        groups.push(vec![narrative]);
    }
}

println!("Found {} thematic groups", groups.len());
for (i, group) in groups.iter().enumerate() {
    println!("  Group {}: {} narratives", i + 1, group.len());
}