Skip to content

Commit 0e4e9fd

Browse files
authored
fix(names): track assumed duplicate records (#679)
* fix(names): track assumed duplicate records * killed-in-gaza: json update * fix comment --------- Co-authored-by: sterlingwes <sterlingwes@users.noreply.github.com>
1 parent ad73524 commit 0e4e9fd

5 files changed

Lines changed: 118 additions & 15 deletions

File tree

killed-in-gaza.json

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25080,7 +25080,10 @@
2508025080
"dob": "1948-03-20",
2508125081
"sex": "m",
2508225082
"id": "938867033",
25083-
"source": "u"
25083+
"source": "u",
25084+
"duplicate_ids": [
25085+
"700187263"
25086+
]
2508425087
},
2508525088
{
2508625089
"en_name": "Majid Mohammed Hassan Abu Ouda",
@@ -226052,15 +226055,6 @@
226052226055
"id": "700189525",
226053226056
"source": "u"
226054226057
},
226055-
{
226056-
"en_name": "Abdel Raouf Hassan Awad Al-Najjar",
226057-
"name": "عبد الرؤوف حسن عوض النجار",
226058-
"age": 77,
226059-
"dob": "1948-03-20",
226060-
"sex": "m",
226061-
"id": "700187263",
226062-
"source": "u"
226063-
},
226064226058
{
226065226059
"en_name": "Ismail Mousa Jibril Al-Hammadin",
226066226060
"name": "اسماعيل موسي جبريل الحمادين",

killed-in-gaza.min.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/**
2+
* Manually curated map of known duplicate records.
3+
*
4+
* Key: canonical ID (the record to keep)
5+
* Value: array of duplicate IDs to be removed and merged into the canonical record
6+
*
7+
* The canonical record will get a `duplicate_ids` field listing the merged IDs
8+
* so API consumers can trace consolidated records.
9+
*/
10+
export const knownDuplicates: Record<string, string[]> = {
11+
// Abdel Raouf Hassan Awad Al-Najjar - assumed to be same person with two IDs
12+
// See: https://github.com/TechForPalestine/palestine-datasets/issues/599
13+
"938867033": ["700187263"],
14+
};

scripts/data/v2/killed-in-gaza.ts

Lines changed: 98 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import toEnName from "arabic-name-to-en";
33
import { writeJson } from "../../utils/fs";
44
import { ApiResource } from "../../../types/api.types";
55
import { readCsv } from "../../utils/csv";
6+
import { knownDuplicates } from "../common/killed-in-gaza/duplicates";
67

78
const jsonFileName = "killed-in-gaza.json";
89

@@ -27,7 +28,7 @@ interface MappedRecord extends Record<string, string | number> {
2728

2829
const sexMapping = {
2930
M: "m",
30-
F: "f"
31+
F: "f",
3132
};
3233

3334
const namesFallbackTranslated = new Map<string, number>();
@@ -176,18 +177,111 @@ const validateJson = (json: Array<Record<string, number | string>>) => {
176177
}
177178
};
178179

180+
/**
181+
* Build a set of all IDs that are known duplicates (values in knownDuplicates map)
182+
* so we can skip them during consolidation and unknown duplicate detection.
183+
*/
184+
const allKnownDuplicateIds = new Set<string>();
185+
for (const dupIds of Object.values(knownDuplicates)) {
186+
for (const dupId of dupIds) {
187+
allKnownDuplicateIds.add(dupId);
188+
}
189+
}
190+
191+
/**
192+
* Detect records that share the same name+dob+sex but have different IDs.
193+
* These are potential duplicates that should be resolved in the duplicates config.
194+
* Known duplicates (from the config) are excluded from this check.
195+
*/
196+
const detectUnknownDuplicates = (
197+
json: MappedRecord[]
198+
): Map<string, MappedRecord[]> => {
199+
const keyMap = new Map<string, MappedRecord[]>();
200+
201+
for (const record of json) {
202+
// Skip records that are known duplicates (to be consolidated)
203+
if (allKnownDuplicateIds.has(record.id)) {
204+
continue;
205+
}
206+
const key = `${record.name}|${record.dob}|${record.sex}`;
207+
if (!keyMap.has(key)) {
208+
keyMap.set(key, []);
209+
}
210+
keyMap.get(key)!.push(record);
211+
}
212+
213+
const unknownDupes = new Map<string, MappedRecord[]>();
214+
for (const [key, records] of keyMap) {
215+
if (records.length > 1) {
216+
unknownDupes.set(key, records);
217+
}
218+
}
219+
return unknownDupes;
220+
};
221+
222+
/**
223+
* Consolidate known duplicates: remove duplicate records and add duplicate_ids
224+
* to the canonical record.
225+
*/
226+
const consolidateKnownDuplicates = (json: MappedRecord[]): MappedRecord[] => {
227+
const result: MappedRecord[] = [];
228+
229+
for (const record of json) {
230+
// Skip records that are known duplicate IDs (non-canonical)
231+
if (allKnownDuplicateIds.has(record.id)) {
232+
continue;
233+
}
234+
235+
// If this is a canonical record with known duplicates, add the field
236+
if (knownDuplicates[record.id]) {
237+
(record as Record<string, unknown>).duplicate_ids =
238+
knownDuplicates[record.id];
239+
}
240+
241+
result.push(record);
242+
}
243+
244+
return result;
245+
};
246+
179247
const generateJsonFromTranslatedCsv = async () => {
180248
const [headerKeys, ...rows] = readCsv(
181249
"scripts/data/common/killed-in-gaza/output/result.csv"
182250
);
183251
const jsonArray = formatToJson(headerKeys, rows);
184252
validateJson(jsonArray);
253+
254+
// Detect unknown duplicates (same name+dob+sex, different IDs) and error
255+
const unknownDupes = detectUnknownDuplicates(jsonArray);
256+
if (unknownDupes.size > 0) {
257+
const dupeDetails = Array.from(unknownDupes.entries())
258+
.map(([key, records]) => {
259+
const ids = records.map((r) => r.id).join(", ");
260+
return ` ${records[0].en_name} (${key}): [${ids}]`;
261+
})
262+
.join("\n");
263+
throw new Error(
264+
`Found ${unknownDupes.size} unknown duplicate group(s) with same name+dob+sex but different IDs.\n` +
265+
`Add them to scripts/data/common/killed-in-gaza/duplicates.ts and remove duplicates from raw.csv.\n` +
266+
`Duplicate groups:\n${dupeDetails}`
267+
);
268+
}
269+
270+
// Consolidate known duplicates
271+
const consolidated = consolidateKnownDuplicates(jsonArray);
272+
const removedCount = jsonArray.length - consolidated.length;
273+
if (removedCount > 0) {
274+
console.log(
275+
`Consolidated ${removedCount} known duplicate record(s) from ${jsonArray.length} total`
276+
);
277+
}
278+
185279
// sort by descending ID
186-
jsonArray.sort((a, b) => b.id.localeCompare(a.id));
187-
writeJson(ApiResource.KilledInGazaV2, jsonFileName, jsonArray);
280+
consolidated.sort((a, b) => b.id.localeCompare(a.id));
281+
writeJson(ApiResource.KilledInGazaV2, jsonFileName, consolidated);
188282

189283
console.log(
190-
`generated JSON file with ${jsonArray.length} records: ${jsonFileName}`
284+
`generated JSON file with ${consolidated.length} records: ${jsonFileName}`
191285
);
192286

193287
const logLines: string[] = [];

types/killed-in-gaza.types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export type KilledInGaza = {
66
sex: "m" | "f";
77
en_name: string;
88
source: "h" | "c";
9+
duplicate_ids?: string[];
910
};
1011

1112
export type MarqueePerson = Pick<KilledInGaza, "id" | "name" | "age" | "sex">;

0 commit comments

Comments
 (0)