{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/Hawksight-AI/semantica/blob/main/cookbook/introduction/18_Deduplication.ipynb)\n",
    "\n",
    "# Deduplication in Semantica\n",
    "\n",
    "Welcome to the **Deduplication** walkthrough! In any Knowledge Graph, data often comes from multiple sources, leading to duplicate entities (e.g., \"Apple Inc.\" vs. \"Apple Inc\"). \n",
    "\n",
    "Semantica provides a robust **Deduplication Module** to help you:\n",
    "1. **Calculate Similarity**: Compare entities using strings, properties, and embeddings.\n",
    "2. **Detect Duplicates**: Find pairs or groups of entities that represent the same real-world object.\n",
    "3. **Cluster Entities**: Group similar entities together.\n",
    "4. **Merge Entities**: Combine duplicates into a single, canonical entity while resolving conflicts.\n",
    "\n",
    "This notebook will guide you through each step with clear examples."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Install Semantica\n",
    "!pip install -q semantica"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 1. Preparing Sample Data\n",
    "\n",
    "Let's create a dataset with some intentional duplicates. We'll simulate data coming from different sources (e.g., a CRM and a public database).\n",
    "\n",
    "**Our Entities:**\n",
    "- **Apple**: Variations like \"Apple Inc.\", \"Apple Inc\", and just \"Apple\".\n",
    "- **Microsoft**: Variations like \"Microsoft Corp\" and \"Microsoft\".\n",
    "- **Google**: A unique entity for control."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "entities = [\n",
    "    # Apple Variations\n",
    "    {\n",
    "        \"id\": \"e1\",\n",
    "        \"name\": \"Apple Inc.\",\n",
    "        \"type\": \"Company\",\n",
    "        \"properties\": {\"industry\": \"Technology\", \"hq\": \"Cupertino\", \"founded\": 1976},\n",
    "        \"relationships\": [{\"predicate\": \"founded_by\", \"object\": \"Steve Jobs\"}]\n",
    "    },\n",
    "    {\n",
    "        \"id\": \"e2\",\n",
    "        \"name\": \"Apple Inc\",\n",
    "        \"type\": \"Company\",\n",
    "        \"properties\": {\"industry\": \"Tech\", \"hq\": \"Cupertino, CA\"}, # Slightly different properties\n",
    "        \"relationships\": []\n",
    "    },\n",
    "    {\n",
    "        \"id\": \"e3\",\n",
    "        \"name\": \"Apple\",\n",
    "        \"type\": \"Company\",\n",
    "        \"properties\": {\"industry\": \"Consumer Electronics\"}, \n",
    "        \"relationships\": [{\"predicate\": \"ceo\", \"object\": \"Tim Cook\"}]\n",
    "    },\n",
    "    \n",
    "    # Microsoft Variations\n",
    "    {\n",
    "        \"id\": \"e4\",\n",
    "        \"name\": \"Microsoft Corp\",\n",
    "        \"type\": \"Company\",\n",
    "        \"properties\": {\"industry\": \"Software\", \"hq\": \"Redmond\"}\n",
    "    },\n",
    "    {\n",
    "        \"id\": \"e5\",\n",
    "        \"name\": \"Microsoft\",\n",
    "        \"type\": \"Company\",\n",
    "        \"properties\": {\"industry\": \"Tech\", \"hq\": \"Redmond, WA\"}\n",
    "    },\n",
    "    \n",
    "    # Unique Entity\n",
    "    {\n",
    "        \"id\": \"e6\",\n",
    "        \"name\": \"Google LLC\",\n",
    "        \"type\": \"Company\",\n",
    "        \"properties\": {\"industry\": \"Internet\"}\n",
    "    }\n",
    "]\n",
    "\n",
    "print(f\"Created {len(entities)} sample entities.\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 2. Similarity Calculation\n",
    "\n",
    "The `SimilarityCalculator` is the core engine. It compares two entities and returns a score between 0 and 1. It looks at:\n",
    "- **String Similarity**: Names and text fields.\n",
    "- **Property Similarity**: Overlap in key-value pairs.\n",
    "- **Relationship Similarity**: Connections to other entities.\n",
    "- **Embeddings**: Semantic vector similarity (if available).\n",
    "\n",
    "You can customize the weights for each factor."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from semantica.deduplication import SimilarityCalculator, SimilarityResult\n",
    "# Initialize calculator with custom weights\n",
    "calculator = SimilarityCalculator(\n",
    "    string_weight=0.5,      # High importance on name\n",
    "    property_weight=0.3,    # Medium importance on properties\n",
    "    relationship_weight=0.2 # Lower importance on relationships\n",
    ")\n",
    "\n",
    "# Compare \"Apple Inc.\" (e1) vs \"Apple Inc\" (e2)\n",
    "score_e1_e2 = calculator.calculate_similarity(entities[0], entities[1])\n",
    "\n",
    "print(f\"Similarity between '{entities[0]['name']}' and '{entities[1]['name']}':\")\n",
    "print(f\"  Total Score: {score_e1_e2.score:.4f}\")\n",
    "print(f\"  Breakdown:   {score_e1_e2.components}\")\n",
    "\n",
    "# Compare \"Apple Inc.\" (e1) vs \"Microsoft\" (e5)\n",
    "score_e1_e5 = calculator.calculate_similarity(entities[0], entities[4])\n",
    "\n",
    "print(f\"\\nSimilarity between '{entities[0]['name']}' and '{entities[4]['name']}':\")\n",
    "print(f\"  Total Score: {score_e1_e5.score:.4f}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 3. Duplicate Detection\n",
    "\n",
    "The `DuplicateDetector` uses the similarity calculator to scan your dataset for duplicates. It can find:\n",
    "- **Pairs**: Simple A matches B.\n",
    "- **Groups**: A matches B, and B matches C.\n",
    "\n",
    "It uses a `similarity_threshold` to decide what counts as a match."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Import specific classes for Duplicate Detection\n",
    "from semantica.deduplication import DuplicateDetector, DuplicateCandidate, DuplicateGroup\n",
    "from semantica.deduplication import DeduplicationConfig\n",
    "\n",
    "detector = DuplicateDetector(\n",
    "    similarity_threshold=0.7, \n",
    "    confidence_threshold=0.6  \n",
    ")\n",
    "\n",
    "# Detect pairs\n",
    "candidates = detector.detect_duplicates(entities)\n",
    "\n",
    "print(f\"Found {len(candidates)} duplicate pairs:\")\n",
    "for c in candidates:\n",
    "    print(f\"  - {c.entity1['name']} <==> {c.entity2['name']} (Score: {c.similarity_score:.2f})\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Incremental Detection\n",
    "If you have an existing database and ingest new data, you don't want to re-compare everything. Use `incremental_detect`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "existing_db = entities[:3] # The Apple entities\n",
    "new_data = [entities[4]]   # Microsoft\n",
    "# Check if new data matches anything in existing DB\n",
    "inc_candidates = detector.incremental_detect(new_data, existing_db)\n",
    "\n",
    "print(f\"New matches found: {len(inc_candidates)}\")\n",
    "# Expected: 0, because Microsoft is not Apple."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 4. Clustering\n",
    "\n",
    "Sometimes pairs aren't enough. `ClusterBuilder` groups related entities into clusters. This is useful for understanding the full scope of a duplicated entity."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Import specific classes for Clustering\n",
    "from semantica.deduplication import ClusterBuilder, Cluster, ClusterResult\n",
    "cluster_builder = ClusterBuilder(threshold=0.7)\n",
    "result = cluster_builder.build_clusters(entities)\n",
    "\n",
    "print(f\"Found {len(result.clusters)} clusters:\")\n",
    "for i, cluster in enumerate(result.clusters):\n",
    "    names = [e['name'] for e in cluster.entities]\n",
    "    print(f\"  Cluster {i+1}: {names}\")\n",
    "\n",
    "cluster_builder = ClusterBuilder(threshold=0.7)\n",
    "result = cluster_builder.build_clusters(entities)\n",
    "\n",
    "print(f\"Found {len(result.clusters)} clusters:\")\n",
    "for i, cluster in enumerate(result.clusters):\n",
    "    names = [e['name'] for e in cluster.entities]\n",
    "    print(f\"  Cluster {i+1}: {names}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 5. Entity Merging\n",
    "\n",
    "Once duplicates are found, `EntityMerger` combines them. You need to choose a **Merge Strategy**:\n",
    "\n",
    "- `KEEP_FIRST` / `KEEP_LAST`: Based on order.\n",
    "- `KEEP_MOST_COMPLETE`: Keeps the entity with the most data (properties + relationships).\n",
    "- `KEEP_HIGHEST_CONFIDENCE`: Uses internal confidence scores.\n",
    "- `MERGE_ALL`: Combines everything (arrays are concatenated, conflicts resolved by voting)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Import specific classes for Entity Merging\n",
    "from semantica.deduplication import EntityMerger, MergeStrategy, MergeStrategyManager, MergeOperation, MergeResult"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "merger = EntityMerger()\n",
    "\n",
    "# We will use the 'KEEP_MOST_COMPLETE' strategy\n",
    "# This ensures we don't lose valuable information from richer entities\n",
    "merge_ops = merger.merge_duplicates(\n",
    "    entities, \n",
    "    strategy=MergeStrategy.KEEP_MOST_COMPLETE\n",
    ")\n",
    "\n",
    "print(f\"Performed {len(merge_ops)} merge operations.\")\n",
    "\n",
    "print(\"\\n--- Merged Results ---\")\n",
    "for op in merge_ops:\n",
    "    final_ent = op.merged_entity\n",
    "    original_count = len(op.source_entities)\n",
    "    print(f\"Merged {original_count} entities into: '{final_ent['name']}'\")\n",
    "    print(f\"  - Final Properties: {final_ent['properties']}\")\n",
    "    print(f\"  - Final Relationships: {len(final_ent.get('relationships', []))}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 6. Complete Workflow\n",
    "\n",
    "Let's wrap this up into a clean function that takes dirty data and returns clean data."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def deduplicate_dataset(raw_entities):\n",
    "    print(\"1. Detecting duplicates...\")\n",
    "    # Step 1: Detect\n",
    "    detector = DuplicateDetector(similarity_threshold=0.75)\n",
    "    # We can skip explicit detection calls if we just want to merge, \n",
    "    # as EntityMerger calls detection internally, but doing it manually allows inspection.\n",
    "    \n",
    "    print(\"2. Merging entities...\")\n",
    "    # Step 2: Merge\n",
    "    merger = EntityMerger()\n",
    "    ops = merger.merge_duplicates(raw_entities, strategy=MergeStrategy.KEEP_MOST_COMPLETE)\n",
    "    \n",
    "    # Let's collect all final IDs to see what remains\n",
    "    merged_entities = [op.merged_entity for op in ops]\n",
    "    \n",
    "    # Find entities that were NOT part of any merge (singletons)\n",
    "    merged_ids = set()\n",
    "    for op in ops:\n",
    "        for source in op.source_entities:\n",
    "            merged_ids.add(source['id'])\n",
    "            \n",
    "    singletons = [e for e in raw_entities if e['id'] not in merged_ids]\n",
    "    \n",
    "    final_dataset = merged_entities + singletons\n",
    "    return final_dataset\n",
    "\n",
    "# Run the workflow\n",
    "clean_data = deduplicate_dataset(entities)\n",
    "\n",
    "print(f\"\\nOriginal Size: {len(entities)}\")\n",
    "print(f\"Cleaned Size:  {len(clean_data)}\")\n",
    "print(\"\\nFinal Entity Names:\")\n",
    "for e in clean_data:\n",
    "    print(f\"  - {e['name']}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Summary\n",
    "\n",
    "You've learned how to:\n",
    "1. **Import** the necessary Deduplication classes.\n",
    "2. **Calculate Similarity** between entities.\n",
    "3. **Detect Duplicates** using configurable thresholds.\n",
    "4. **Cluster** similar entities.\n",
    "5. **Merge** duplicates into a clean, canonical dataset.\n",
    "\n",
    "This module is essential for maintaining high-quality Knowledge Graphs, especially when ingesting data from multiple, potentially messy sources."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.9"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
