datamatters24 commited on
Commit
d491771
·
verified ·
1 Parent(s): 9e72c3a

Upload notebooks/02_entity_network/21_entity_resolution.ipynb with huggingface_hub

Browse files
notebooks/02_entity_network/21_entity_resolution.ipynb ADDED
@@ -0,0 +1,302 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "metadata": {},
6
+ "source": [
7
+ "# 21 - Entity Resolution\n",
8
+ "\n",
9
+ "Pipeline notebook for fuzzy entity matching and alias resolution.\n",
10
+ "\n",
11
+ "Groups similar entity names into clusters using normalized forms and fuzzy string matching.\n",
12
+ "For each cluster, picks the most frequent form as the canonical name.\n",
13
+ "Results are stored in the `entity_aliases` table."
14
+ ]
15
+ },
16
+ {
17
+ "cell_type": "code",
18
+ "execution_count": null,
19
+ "metadata": {
20
+ "tags": [
21
+ "parameters"
22
+ ]
23
+ },
24
+ "outputs": [],
25
+ "source": [
26
+ "# Parameters\n",
27
+ "source_section = None\n",
28
+ "similarity_threshold = 0.85"
29
+ ]
30
+ },
31
+ {
32
+ "cell_type": "code",
33
+ "execution_count": null,
34
+ "metadata": {},
35
+ "outputs": [],
36
+ "source": [
37
+ "import sys\n",
38
+ "sys.path.insert(0, '/opt/epstein_env/research')\n",
39
+ "\n",
40
+ "import difflib\n",
41
+ "from collections import defaultdict, Counter\n",
42
+ "from tqdm.auto import tqdm\n",
43
+ "\n",
44
+ "from research_lib.db import fetch_df, fetch_all, bulk_insert\n",
45
+ "from research_lib.nlp import normalize_entity\n",
46
+ "from research_lib.incremental import start_run, finish_run"
47
+ ]
48
+ },
49
+ {
50
+ "cell_type": "code",
51
+ "execution_count": null,
52
+ "metadata": {},
53
+ "outputs": [],
54
+ "source": [
55
+ "# Start run\n",
56
+ "run_id = start_run(\n",
57
+ " 'entity_resolution',\n",
58
+ " source_section=source_section,\n",
59
+ " parameters={'similarity_threshold': similarity_threshold},\n",
60
+ ")\n",
61
+ "print(f'Started run {run_id}')"
62
+ ]
63
+ },
64
+ {
65
+ "cell_type": "code",
66
+ "execution_count": null,
67
+ "metadata": {},
68
+ "outputs": [],
69
+ "source": [
70
+ "# Load all unique entity texts with their frequencies, grouped by type\n",
71
+ "where_clause = ''\n",
72
+ "params = []\n",
73
+ "if source_section:\n",
74
+ " where_clause = 'WHERE d.source_section = %s'\n",
75
+ " params = [source_section]\n",
76
+ "\n",
77
+ "sql = f\"\"\"\n",
78
+ " SELECT e.entity_text, e.entity_type, COUNT(*) as freq\n",
79
+ " FROM entities e\n",
80
+ " JOIN documents d ON d.id = e.document_id\n",
81
+ " {where_clause}\n",
82
+ " GROUP BY e.entity_text, e.entity_type\n",
83
+ " ORDER BY freq DESC\n",
84
+ "\"\"\"\n",
85
+ "entity_df = fetch_df(sql, params or None)\n",
86
+ "print(f'Total unique entity-type combinations: {len(entity_df)}')\n",
87
+ "print(f'Entity types: {entity_df[\"entity_type\"].value_counts().to_dict()}')"
88
+ ]
89
+ },
90
+ {
91
+ "cell_type": "code",
92
+ "execution_count": null,
93
+ "metadata": {},
94
+ "outputs": [],
95
+ "source": [
96
+ "# Normalize entity texts\n",
97
+ "entity_df['normalized'] = entity_df.apply(\n",
98
+ " lambda row: normalize_entity(row['entity_text'], row['entity_type']),\n",
99
+ " axis=1,\n",
100
+ ")\n",
101
+ "\n",
102
+ "# Group entities by type for within-type matching\n",
103
+ "entities_by_type = {}\n",
104
+ "for etype, group in entity_df.groupby('entity_type'):\n",
105
+ " entities_by_type[etype] = group.reset_index(drop=True)\n",
106
+ " print(f' {etype}: {len(group)} unique entities')"
107
+ ]
108
+ },
109
+ {
110
+ "cell_type": "code",
111
+ "execution_count": null,
112
+ "metadata": {},
113
+ "outputs": [],
114
+ "source": [
115
+ "def find_clusters(entities_group, threshold):\n",
116
+ " \"\"\"Cluster entities using fuzzy matching with Union-Find.\"\"\"\n",
117
+ " texts = entities_group['normalized'].tolist()\n",
118
+ " freqs = entities_group['freq'].tolist()\n",
119
+ " originals = entities_group['entity_text'].tolist()\n",
120
+ " n = len(texts)\n",
121
+ "\n",
122
+ " # Union-Find\n",
123
+ " parent = list(range(n))\n",
124
+ "\n",
125
+ " def find(x):\n",
126
+ " while parent[x] != x:\n",
127
+ " parent[x] = parent[parent[x]]\n",
128
+ " x = parent[x]\n",
129
+ " return x\n",
130
+ "\n",
131
+ " def union(a, b):\n",
132
+ " ra, rb = find(a), find(b)\n",
133
+ " if ra != rb:\n",
134
+ " # Attach less frequent to more frequent\n",
135
+ " if freqs[ra] >= freqs[rb]:\n",
136
+ " parent[rb] = ra\n",
137
+ " else:\n",
138
+ " parent[ra] = rb\n",
139
+ "\n",
140
+ " # Compare all pairs using SequenceMatcher\n",
141
+ " # For efficiency, first group by exact normalized form\n",
142
+ " norm_groups = defaultdict(list)\n",
143
+ " for i, norm in enumerate(texts):\n",
144
+ " norm_groups[norm.lower()].append(i)\n",
145
+ "\n",
146
+ " # Union exact normalized matches\n",
147
+ " for indices in norm_groups.values():\n",
148
+ " for j in range(1, len(indices)):\n",
149
+ " union(indices[0], indices[j])\n",
150
+ "\n",
151
+ " # Fuzzy match across distinct normalized forms\n",
152
+ " unique_norms = list(norm_groups.keys())\n",
153
+ " for i in tqdm(range(len(unique_norms)), desc='Fuzzy matching', leave=False):\n",
154
+ " for j in range(i + 1, len(unique_norms)):\n",
155
+ " ratio = difflib.SequenceMatcher(\n",
156
+ " None, unique_norms[i], unique_norms[j]\n",
157
+ " ).ratio()\n",
158
+ " if ratio >= threshold:\n",
159
+ " # Union representatives from each group\n",
160
+ " union(norm_groups[unique_norms[i]][0], norm_groups[unique_norms[j]][0])\n",
161
+ "\n",
162
+ " # Build clusters\n",
163
+ " clusters = defaultdict(list)\n",
164
+ " for i in range(n):\n",
165
+ " root = find(i)\n",
166
+ " clusters[root].append(i)\n",
167
+ "\n",
168
+ " # Pick canonical name (most frequent original form)\n",
169
+ " result = []\n",
170
+ " for root, members in clusters.items():\n",
171
+ " if len(members) <= 1:\n",
172
+ " continue # Skip singletons\n",
173
+ " best_idx = max(members, key=lambda i: freqs[i])\n",
174
+ " canonical = originals[best_idx]\n",
175
+ " for idx in members:\n",
176
+ " if idx != best_idx:\n",
177
+ " result.append({\n",
178
+ " 'alias_text': originals[idx],\n",
179
+ " 'canonical_text': canonical,\n",
180
+ " 'similarity': difflib.SequenceMatcher(\n",
181
+ " None,\n",
182
+ " texts[idx].lower(),\n",
183
+ " texts[best_idx].lower(),\n",
184
+ " ).ratio(),\n",
185
+ " })\n",
186
+ "\n",
187
+ " return result, len(clusters)\n",
188
+ "\n",
189
+ "print('Cluster function defined.')"
190
+ ]
191
+ },
192
+ {
193
+ "cell_type": "code",
194
+ "execution_count": null,
195
+ "metadata": {},
196
+ "outputs": [],
197
+ "source": [
198
+ "# Run clustering for each entity type\n",
199
+ "all_aliases = []\n",
200
+ "total_clusters = 0\n",
201
+ "total_entities = 0\n",
202
+ "\n",
203
+ "for etype, group in entities_by_type.items():\n",
204
+ " print(f'\\nProcessing {etype} ({len(group)} entities)...')\n",
205
+ " total_entities += len(group)\n",
206
+ "\n",
207
+ " aliases, n_clusters = find_clusters(group, similarity_threshold)\n",
208
+ " total_clusters += n_clusters\n",
209
+ "\n",
210
+ " for alias in aliases:\n",
211
+ " alias['entity_type'] = etype\n",
212
+ "\n",
213
+ " all_aliases.extend(aliases)\n",
214
+ " print(f' Found {n_clusters} clusters, {len(aliases)} alias mappings')\n",
215
+ "\n",
216
+ "print(f'\\nTotal aliases found: {len(all_aliases)}')"
217
+ ]
218
+ },
219
+ {
220
+ "cell_type": "code",
221
+ "execution_count": null,
222
+ "metadata": {},
223
+ "outputs": [],
224
+ "source": [
225
+ "# Insert into entity_aliases table\n",
226
+ "rows = [\n",
227
+ " (\n",
228
+ " a['alias_text'],\n",
229
+ " a['canonical_text'],\n",
230
+ " a['entity_type'],\n",
231
+ " a['similarity'],\n",
232
+ " source_section,\n",
233
+ " )\n",
234
+ " for a in all_aliases\n",
235
+ "]\n",
236
+ "\n",
237
+ "if rows:\n",
238
+ " inserted = bulk_insert(\n",
239
+ " 'entity_aliases',\n",
240
+ " ['alias_text', 'canonical_text', 'entity_type', 'similarity_score', 'source_section'],\n",
241
+ " rows,\n",
242
+ " on_conflict='DO NOTHING',\n",
243
+ " )\n",
244
+ " print(f'Inserted {inserted} alias rows')\n",
245
+ "else:\n",
246
+ " print('No aliases to insert.')"
247
+ ]
248
+ },
249
+ {
250
+ "cell_type": "code",
251
+ "execution_count": null,
252
+ "metadata": {},
253
+ "outputs": [],
254
+ "source": [
255
+ "# Finish run\n",
256
+ "finish_run(run_id, documents_processed=total_entities)\n",
257
+ "print(f'Run {run_id} completed.')"
258
+ ]
259
+ },
260
+ {
261
+ "cell_type": "code",
262
+ "execution_count": null,
263
+ "metadata": {},
264
+ "outputs": [],
265
+ "source": [
266
+ "# Summary stats\n",
267
+ "print('=== Entity Resolution Summary ===')\n",
268
+ "print(f'Total unique entities: {total_entities}')\n",
269
+ "print(f'Total clusters (multi-member): {total_clusters}')\n",
270
+ "print(f'Total alias mappings: {len(all_aliases)}')\n",
271
+ "if total_entities > 0:\n",
272
+ " reduction = len(all_aliases) / total_entities * 100\n",
273
+ " print(f'Reduction ratio: {reduction:.1f}% of entities are aliases')\n",
274
+ "\n",
275
+ "# Show some example clusters\n",
276
+ "if all_aliases:\n",
277
+ " from collections import defaultdict\n",
278
+ " clusters_display = defaultdict(list)\n",
279
+ " for a in all_aliases[:100]:\n",
280
+ " clusters_display[a['canonical_text']].append(\n",
281
+ " f\"{a['alias_text']} ({a['similarity']:.2f})\"\n",
282
+ " )\n",
283
+ " print('\\nExample clusters (first 10):')\n",
284
+ " for i, (canonical, aliases) in enumerate(list(clusters_display.items())[:10]):\n",
285
+ " print(f' {canonical}: {aliases}')"
286
+ ]
287
+ }
288
+ ],
289
+ "metadata": {
290
+ "kernelspec": {
291
+ "display_name": "Python 3",
292
+ "language": "python",
293
+ "name": "python3"
294
+ },
295
+ "language_info": {
296
+ "name": "python",
297
+ "version": "3.10.0"
298
+ }
299
+ },
300
+ "nbformat": 4,
301
+ "nbformat_minor": 5
302
+ }