PrimeKG merged nodes: review table for #438 item 2
Refs #438 item 2. Richard decided on 2026-10-05 to “fix all the wrong merges”, starting from this table. Related: roadmap D13 (#611), the Jaccard safety report (#59). The fix lands before Run 3’s export.
Decided 2026-10-05: D-Q4-22 A (
#819). Rules J0–J9 were approved as proposed, and the six unclear groups split along the CSV’spartitioncolumn, as the review leans.kg_split_primekg_merges.pycarries it out (#832).With that partition the split restores 109,412 rows, not the 109,440 quoted below. §4’s ”+ the 6 unclear” column splits D-glucose from dextrose, and J5 keeps those two together. The difference is 28 enzyme and transporter rows. Fed the approved partition, the replay in §8.5 gives the same 109,412.
Nothing here changes data. Whether two nodes are one entity is a mapping decision, so under the CLAUDE.md hard rule this table goes to Richard before any entity is split. Each verdict is a proposal. Where a verdict rests on a rule rather than on evidence, the rule is named (J0–J9, §3), and those rules are what he approves. Counts were read from prod, read-only and PrimeKG rows only, at 15:06 PDT on 2026-10-05, and replayed from PrimeKG’s own
kg.csv.
Files. This page, and kg_primekg_merge_review.csv beside it: one row per merged node (401 rows), with every measured column and the proposal. §8 has the two scripts that produce it.
Summary
- 401 PrimeKG nodes sit on 183 canonical entities. The audit’s “218” is the surplus: 401 − 183 = 218 nodes that lost an entity of their own. By node type:
- drug: 237 nodes in 101 groups, a surplus of 136 (the audit’s “136”);
- gene: 122 nodes in 61 groups;
- pathway: 36 nodes in 18 groups;
- anatomy: 4 nodes in 2 groups;
- disease: 2 nodes in 1 group.
- The audit’s edge counts reproduce exactly. Replaying
kg.csvunder these merges gives 7,988,012 relations. That is what prod stores, on every one of the 33 relation keys and on every one of the 183 entities. The merges collapsed 112,486 rows: drug_drug 108,174, contraindication 1,566, indication 428, and nine smaller relations (§1). - Three mechanisms made the groups (§2).
- 63 groups (61 gene, 2 anatomy): PrimeKG ships two nodes for one id. Either two symbols of one gene (C16orf58 and RUSF1, both NCBI Gene 64755) or two labels of one UBERON term. These were one entity from the first backfill. The merges are right and cost no rows.
- 101 groups (all drugs): Node Normalizer gave their DrugBank ids one preferred CURIE.
apply_canonical_entity_merges.pypass 1 then merged them in May 2026. Node Normalizer folds salts, esters, enantiomers and radiotracers into one chemical, and twice it folded together unrelated drugs. - 19 groups (18 pathway, 1 disease): the names matched, and the same script’s pass 2 merged them. Ten of them join a SARS-CoV-1 pathway to its SARS-CoV-2 namesake.
- Proposed verdicts: 110 distinct, 67 same, 6 unclear.
- Same (67): the 63 one-id groups, two hydrate or “unspecified form” pairs (J5), and two pairs that are one product under two DrugBank entries (J8): CT-011 with pidilizumab, and PEG-uricase with pegloticase.
- Distinct (110): 91 drug groups, all 18 pathway groups and the disease group. 29 are distinct on evidence alone: unrelated drugs, different molecules, different viruses. The other 81 rest on one of J1–J4, J6, J7 or J9.
- Unclear (6): drug groups where the evidence does not settle it (§3).
- Both known examples are distinct. D- and L-tartaric acid are enantiomers (J1). Choline C-11 and choline salicylate sit in a group of four different drugs.
- What a split restores. Splitting the 110 distinct groups restores 106,818 rows. That includes every lost contraindication (1,566), indication (428), drug_effect (888) and off-label use (46) row. Splitting the 6 unclear groups as well restores 109,440. The remaining 3,046 rows stay merged on purpose. They sit in the same groups and in the two kept pairs of the sodium phosphate group, where each is one assertion made twice (§4).
- What Richard approves: the ten rules in §3. Each comes with the number of groups it decides. A rejected rule flips its own groups and no others, and the CSV names the rule behind every node.
1. Reconciliation with the audit
| Quantity | Audit (#438) | Measured | Why they differ |
|---|---|---|---|
| PrimeKG nodes on a shared entity | 218 | 401 | The audit counted the surplus: 401 − 183 = 218. |
| Entities they form | 183 | 183 | — |
| Drug nodes | 136 | 237, in 101 groups | Also the surplus: 237 − 101 = 136. |
| drug_drug rows lost | 108,174 | 108,174 | — |
| contraindication rows lost | 1,566 | 1,566 | — |
| indication rows lost | 428 | 428 | — |
| drug–protein rows lost | 1,272 (2026-10-03 comment) | 1,250 | 1,272 counted (drug, protein) pairs, as stored before #696. 1,250 counts the role rows stored since: target 364, enzyme 438, transporter 346, carrier 102. |
| All rows lost | 112,508 (same comment) | 112,486 | The 22 drug–protein rows above. |
How “lost” is counted. A row is lost when two kg.csv rows map to one stored key, (subject, object, predicate, source, source_predicate), after their endpoints are merged. A row whose two endpoints land on one entity becomes a self-loop and is deleted too. The unit is the directed kg.csv row, which is the unit mods_kg_relation stores; PrimeKG writes every edge in both directions.
What prod holds. PrimeKG’s 129,375 nodes are all stored, as 129,375 prime_kg external ids on 129,157 entities. No external id sits on two entities. kg.csv has no duplicate rows and no native self-loops.
| Relation | kg.csv rows | Stored on prod | Lost to merges | Of which self-loops |
|---|---|---|---|---|
| drug_drug | 2,672,628 | 2,564,454 | 108,174 | 164 |
| drug_effect | 129,568 | 128,680 | 888 | 0 |
| pathway_protein | 85,292 | 85,184 | 108 | 0 |
| disease_disease | 64,388 | 64,386 | 2 | 2 |
| contraindication | 61,350 | 59,784 | 1,566 | 0 |
| target | 32,760 | 32,396 | 364 | 0 |
| indication | 18,776 | 18,348 | 428 | 0 |
| enzyme | 10,634 | 10,196 | 438 | 0 |
| transporter | 6,184 | 5,838 | 346 | 0 |
| off-label use | 5,136 | 5,090 | 46 | 0 |
| pathway_pathway | 5,070 | 5,046 | 24 | 0 |
| carrier | 1,728 | 1,626 | 102 | 0 |
| all 33 relation keys | 8,100,498 | 7,988,012 | 112,486 | 166 |
The other 21 relation keys lost nothing. Nothing self-loops on prod today: the 166 self-loop rows were deleted when the merges were made.
2. How the groups were merged
| Mechanism | Groups | Nodes | Domains | The data that joined them | Proposed |
|---|---|---|---|---|---|
| Backfill: one CURIE hint. PrimeKG has two nodes with the same source id, so both resolve to one CURIE. | 63 | 126 | 61 gene, 2 anatomy | Both nodes carry the same x_source:x_id in kg.csv, e.g. NCBI:64755 for “RUSF1” and “C16orf58”. They were already one entity in the pre-merge snapshot. | 63 same |
apply_canonical_entity_merges.py pass 1: Node Normalizer. Entities whose CURIEs have one preferred_curie in mods_kg_curie_cache are merged; the winner takes that CURIE. | 101 | 237 | drug | DrugBank:DB09459 → CHEBI:15671 “L-tartaric acid” and DrugBank:DB01694 → CHEBI:15671. Each node sat on its own entity in the pre-merge snapshot. | 91 distinct, 4 same, 6 unclear |
| The same script’s pass 2: name match. An entity Node Normalizer does not know is merged into one of the same name and domain whose CURIE has a canonical prefix. | 19 | 38 | 18 pathway, 1 disease | Identical names, e.g. two “Maturation of spike protein” pathways, REACTOME:R-HSA-9683686 and R-HSA-9694548; neither is in the cache. | 19 distinct |
How the second and third rows were dated and attributed:
- The script refuses to run without the snapshot tables
mods_kg_entity_premerge,mods_kg_entity_source_premergeandmods_kg_relation_premerge, and prod has all three. - In that snapshot the 120 pass-1 and pass-2 groups sit on 237 + 38 separate entities. The 63 one-id groups are already one entity each.
- Pass 1’s grouping key,
COALESCE(preferred_curie, curie)within a domain, reproduces every drug group. Every member of a drug group has the same cachedpreferred_curie. - Commit 9874abb (2026-05-13) records “actually running the merge against ~20K mergers and ~9M relations”.
This is an inference from the snapshot and the code. No run log names these groups.
The script’s first commit, 7f9180b, put an “edge-overlap safety threshold” out of scope, pending “review of the actual merges”. The jaccard_with_anchor column in the CSV is that review, per node.
3. The judgement calls: rules for Richard
Each rule decides a family of groups. Rejecting a rule flips the groups it decides, and only those. A group can rest on more than one rule: Vitamin E (J1, J2, J7), the sodium phosphates (J3, J4, J5) and glucose (J1, J5). Such a group is counted under each rule.
| Rule | Proposal | Groups it decides | If rejected¹ | Examples |
|---|---|---|---|---|
| J0 | Same id, same entity. Two PrimeKG nodes with one source id (one NCBI Gene id, one UBERON id) are one entity. → same | 63 (all same) | 63 duplicate nodes return; no row comes back | RUSF1 and C16orf58 (NCBI Gene 64755); ovary and female gonad (UBERON 992) |
| J1 | Stereoisomers are distinct: enantiomers, a racemate and its enantiomer, D and L forms. → distinct | 9 (7 distinct, 2 unclear) | 3,664 fewer rows restored | L- and D-tartaric acid; ketoconazole and levoketoconazole; norgestrel and levonorgestrel; atropine and hyoscyamine; serine and D-serine; levomenthol and racementhol |
| J2 | An ester or other covalent prodrug is distinct from its parent, and two esters of one parent are distinct from each other. → distinct | 34 (all distinct) | 67,392 fewer rows restored | hydrocortisone and six esters; testosterone and four; estradiol and four; candesartan cilexetil; benazepril and benazeprilat; gabapentin enacarbil; paclitaxel poliglumex |
| J3 | A salt is distinct from its free acid or base, and from another salt of it, when DrugBank gives each its own entry. Applies where the counter-ion is inert; a second active ingredient is evidence, not J3. → distinct | 17 (all distinct) | 10,714 fewer rows restored | methylene blue and methylthioninium; sodium oxybate and GHB; atracurium and its besylate; ascorbic acid and sodium ascorbate; tafamidis and Fx-1006A (its meglumine salt); monobasic and dibasic sodium phosphate |
| J4 | A radiolabelled form is distinct from the unlabelled compound, and from the same carrier with another radionuclide. → distinct | 6 (all distinct) | 2,670 fewer rows restored | choline C-11; ammonia N-13; Lu-177 and Ga-68 dotatate; iobenguane sulfate I-123; sodium phosphate P-32; selenomethionine Se-75 |
| J5 | A hydrate, or DrugBank’s “unspecified form” entry, is the same substance as the named compound. → same | 5 (2 same, 1 distinct, 2 unclear) | 2,308 more rows restored | zinc sulfate; magnesium acetate and its tetrahydrate; both sodium phosphates’ “unspecified form” entries; dextrose and D-glucose (unclear group) |
| J6 | Biologics from different sources are distinct: recombinant, plasma-derived or urine-derived; bovine and other heparin. → distinct | 5 (4 distinct, 1 unclear) | 648 fewer rows restored | recombinant and plasma-derived factor VIII; follitropin and urofollitropin; thyrotropin alfa and thyrotropin; heparin and bovine heparin |
| J7 | A class, mixture, polymer family or element is distinct from one member, component, grade or compound of it. → distinct | 11 (10 distinct, 1 unclear) | 7,862 fewer rows restored | opium and morphine; vitamin D with ergocalciferol and cholecalciferol; PEG with PEG 300 and PEG 400; iron and iron dextran; neomycin and framycetin; bentonite and montmorillonite |
| J8 | Two DrugBank entries for one product are the same: a development code and its INN, or an investigational entry and the approved one. → same | 2 (all same) | 738 more rows restored | CT-011 and pidilizumab; PEG-uricase and pegloticase |
| J9 | Two source ids joined only because their names matched are split back, even when PrimeKG gives them identical edges. → distinct | 6 (all distinct) | 24 fewer rows restored | six Reactome SLC disease-pathway pairs: each pair shares one failed reaction under two different normal pathways, and Reactome has since renamed them apart |
| evidence | No rule: the members are different molecules, products, viruses or terms. | 29 (all distinct) | 15,574 rows ride on these groups | see below |
¹ Rows restored by the proposal (106,818) minus rows restored with that rule rejected, from primekg_merge_review.py’s per_rule. Rejecting a split rule keeps its groups merged. Rejecting a merge rule (J0, J5, J8) splits its groups into single nodes. Unclear groups are left out. Vitamin E (J1, J2, J7) and the sodium phosphates (J3, J4, J5) cite several rules and flip whole, so for J1, J2, J3, J4 and J7 the figure is an upper bound.
No rule: evidence alone (29 groups, all distinct). These are:
- 2 groups of unrelated drugs that Node Normalizer conflated: guanabenz with icodextrin, and ethanol with ipecac;
- 12 groups of different molecules or materials:
- six insulins; three low-molecular-weight heparins; three Tc-99m agents;
- human and porcine secretin; human and ovine corticotropin-releasing hormone; factor VIII of human and of porcine sequence;
- isosorbide and its mononitrate; pentaerythritol and its tetranitrate; adenosine and AMP;
- two ferric pyrophosphate complexes; inositol and inositol nicotinate; almasilate and magnesium aluminium silicate;
- 2 salts whose counter-ion is a second active drug: procaine benzylpenicillin and ambroxol acefyllinate. Choline salicylate is a third, in a group whose split also needs J4;
- 12 Reactome pairs with different events: ten SARS-CoV-1/SARS-CoV-2 pairs and two disease pathways;
- the disease pair, which PrimeKG itself links parent to child.
The six unclear groups. The CSV’s partition column gives the split I lean to for each.
- Calcium phosphate and its dihydrate. DrugBank’s “Calcium Phosphate” does not say which calcium phosphate it is. The dihydrate (MW 172.09) is dicalcium phosphate.
- D-glucose, dextrose, and beta-D-glucose. J5 merges the first two, which carry the same DrugBank text. Beta-D-glucose is an anomer, and anomers interconvert in water.
- Dihydroergocryptine and dihydro-alpha-ergocryptine. DrugBank’s own text calls the alpha form “usually … the mixture”.
- Camphor and synthetic camphor. Neither entry states its stereochemistry.
- Two influenza vaccine antigen groups (A/California/7/2009 X-181, two entries; B/Brisbane/60/2008, four entries). Each group’s entries differ in antigen type or inactivation method and have identical PrimeKG edges.
A caution that cuts both ways. In 38 drug groups, PrimeKG lists drug–drug interactions between two members of the same group: 82 pairs, such as estradiol with each of its esters. The merges deleted those rows as self-loops. For a distinct group they are real assertions, and a split restores them. Three same groups have them too: CT-011 with pidilizumab, magnesium acetate with its tetrahydrate, and PEG-uricase with pegloticase. DrugBank generates interactions by drug class, so there they are a product interacting with itself, and they stay deleted. An interaction between two entries proves two DrugBank entries, not two drugs.
4. What a split restores
Rows restored, by relation. A group marked same stays merged. The sodium phosphate group splits three ways, keeping each salt with its “unspecified form” entry.
| Relation | Lost now | Split the 110 distinct | + the 6 unclear | Split all 183 |
|---|---|---|---|---|
| drug_drug | 108,174 | 102,670 | 105,138 | 108,174 |
| drug_effect | 888 | 888 | 888 | 888 |
| pathway_protein | 108 | 108 | 108 | 108 |
| disease_disease | 2 | 2 | 2 | 2 |
| contraindication | 1,566 | 1,566 | 1,566 | 1,566 |
| target | 364 | 300 | 364 | 364 |
| indication | 428 | 428 | 428 | 428 |
| enzyme | 438 | 432 | 438 | 438 |
| transporter | 346 | 258 | 336 | 346 |
| off-label use | 46 | 46 | 46 | 46 |
| pathway_pathway | 24 | 24 | 24 | 24 |
| carrier | 102 | 96 | 102 | 102 |
| all | 112,486 | 106,818 | 109,440 | 112,486 |
Per group (§7 and the CSV), the figure is marginal: what splitting that group alone restores, with every other merge in place. Some rows join two merged groups, for example an interaction between a hydrocortisone ester and a testosterone ester. Those depend on both merges, so the per-group figures add up to 111,096, not 112,486. The CSV’s group_rows_lost column is the other bound: every lost row touching the group, so a row between two merged groups counts in both.
5. Surprises
- Node Normalizer conflates unrelated drugs. Guanabenz, an alpha-2 agonist, shares an entity with icodextrin, a glucose polymer for peritoneal dialysis. Ethanol shares one with ipecac, a plant emetic. Its cliques also join different molecules: human insulin with four analogues and beef insulin, three Tc-99m agents that share only the radionuclide, and Lu-177 dotatate (therapy) with Ga-68 dotatate (imaging).
- SARS-CoV-1 and SARS-CoV-2 pathways are merged. Reactome builds both virus hierarchies with the same pathway names. Pass 2 merged ten such pairs by name, along with 8 disease-pathway pairs that share a name.
- One entity’s CURIE names a third antigen. Entity 88504 carries
DRUGBANK:DB14432, an A/Singapore/gp1908/2015 IVR-180 antigen. Its members are two A/California/7/2009 X-181 antigens, and PrimeKG has no node for DB14432. The merge script gave each surviving entity Node Normalizer’s preferred CURIE. - Name and CURIE disagree on 47 of the 101 drug entities. The name comes from the member with the most edges; the CURIE comes from Node Normalizer. Sometimes they name different substances:
- “Choline salicylate” carries
CHEBI:15354, choline; - “Procaine benzylpenicillin” carries
CHEBI:17334, penicillin; - “Technetium Tc-99m exametazime” carries
CHEBI:33353, the technetium atom; - “Levonorgestrel” carries
CHEBI:231594, norgestrel.
- “Choline salicylate” carries
- Rows lost understate the harm. 16 drug groups lost no row to their own merge. They include both tartaric acids, guanabenz with icodextrin, and ketoconazole with levoketoconazole. Each still serves two drugs’ edges as one node, and a split is what fixes that.
- A re-run would merge them again. For 228 of the 237 drug members,
mods_kg_curie_cachemaps the standardDRUGBANK:spelling of the id to the merged entity’s CURIE. Pass 1 groups on exactly that. A split therefore needs a guard, or the next run of the script, or a fresh backfill with Node Normalizer on, will merge them again. Pass 2 would re-merge the pathway pairs by name. - A PrimeKG mapping question sits behind two correct gene merges. The node PrimeKG names “COX2” (NCBI Gene 4513, MT-CO2) has disease edges to colorectal cancer, colorectal carcinoma and colorectal neoplasm, beside MELAS and cytochrome-c oxidase deficiency. The colorectal ones look like PTGS2 (COX-2) associations filed under MT-CO2 by symbol, the COX2 homonym of #752. “COX1” (MT-CO1) shows the same pattern. The merges are right, since each pair shares one id, and a split would not touch these edges. They are worth their own check, because colorectal cancer is the index disease.
6. Before any split: open points for the follow-up
These are notes for whoever implements the approved splits. None is decided here.
- Other sources’ rows on these entities were not measured. This review read PrimeKG rows only. Node Normalizer answers for an id, not for a source, so any source that met one of these DrugBank ids sits on the merged entity too. The split must route those rows as well.
- The patient overlay. Maureen’s drug objects that resolved to one of these entities need re-checking, and
materialize_patient_kg()must run after the split. The overlay was out of scope here: nomaple_patientrow was read.- One check needed no patient data. Paclitaxel, one of the RGCC taxanes the mission spec names, is merged with paclitaxel poliglumex. Every poliglumex row duplicated a paclitaxel row or joined the two, so the paclitaxel node carries no foreign edge today.
- The re-merge guard (surprise 6): pass 1 and pass 2 of
apply_canonical_entity_merges.py, and the cache rows behind them. - Which member keeps the entity row. A natural choice is the member the entity’s CURIE names, or the parent compound; each other member gets its own entity under its standard CURIE. First check whether an entity with that CURIE already exists. Entity 88504’s CURIE has to change whichever member keeps it.
- Expected counts. For any approved set of partitions,
primekg_merge_review.pyreplays the expected rows restored by relation (§8). The proposal as written gives 106,818. - Backup, dry run, expected count, per the plan in #438’s 2026-10-05 comment.
7. The review table
Columns: Lost is the rows that splitting this group alone restores, self-loops included; the CSV breaks it down by relation. Rows after a member is its kg.csv row count. J is the Jaccard overlap of the member’s (relation, neighbour) set with that of the group’s anchor, the member whose name the entity carries.
7.1 Drugs (101 groups)
Merge key for every row: Node Normalizer’s preferred CURIE for each member’s own DrugBank: CURIE (§2, pass 1). The members’ own ids are listed.
| # | Entity | Members: x_index DrugBank name (rows; J) | Node Normalizer | Lost | Verdict | Rule | Reason |
|---|---|---|---|---|---|---|---|
| 4 | 79509CHEBI:5134 Fluticasone propionate | 14020 DB00588 Fluticasone propionate (3,722)14038 DB08906 Fluticasone furoate (2,452; J 0.626)14040 DB13867 Fluticasone (2,438; J 0.586) | CHEBI:5134 ‘Fluticasone’ | 4,572 | distinct | J2 | Fluticasone and two different esters of it (propionate, furoate), each its own approved DrugBank drug |
| 5 | 79517CHEBI:17650 Hydrocortisone | 14028 DB00741 Hydrocortisone (3,016)14041 DB14538 Hydrocortisone aceponate (302; J 0.098)14042 DB14539 Hydrocortisone acetate (2,604; J 0.695)14043 DB14540 Hydrocortisone butyrate (2,302; J 0.606)14044 DB14541 Hydrocortisone cypionate (1,652; J 0.543)14046 DB14543 Hydrocortisone probutate (306; J 0.099)14047 DB14544 Hydrocortisone valerate (306; J 0.099) | CHEBI:17650 ‘Hydrocortisone’ | 6,932 | distinct | J2 | Hydrocortisone and six esters of it (aceponate, acetate, butyrate, cypionate, probutate, valerate) |
| 6 | 79519CHEBI:8378 Prednisolone | 14030 DB00860 Prednisolone (2,778)14048 DB14631 Prednisolone phosphate (2,214; J 0.461)14050 DB15566 Prednisolone acetate (1,510; J 0.544) | CHEBI:8378 ‘Prednisolone’ | 2,972 | distinct | J2 | Prednisolone and two esters of it (phosphate, acetate) |
| 7 | 79521CHEBI:205919 Clobetasol propionate | 14032 DB01013 Clobetasol propionate (2,178)15047 DB11750 Clobetasol (2,144; J 0.938) | CHEBI:205919 ‘Clobetasol’ | 2,026 | distinct | J2 | Clobetasol propionate (the marketed ester) and the parent clobetasol |
| 8 | 79543CHEBI:18145 Vitamin E | 14054 DB00163 Vitamin E (740)14110 DB14003 alpha-Tocopherol acetate (24; J 0.008)14524 DB14001 alpha-Tocopherol succinate (52; J 0.045)14525 DB14002 D-alpha-Tocopherol acetate (32; J 0.043)21946 DB14476 DL-alpha-Tocopherol (2; J 0.003) | CHEBI:18145 ‘Vitamin E’ | 74 | distinct | J1, J2, J7 | Vitamin E (DrugBank: a term for 8 tocopherols and tocotrienols), two tocopherol esters, a stereo-defined ester and the racemate |
| 9 | 79639CHEBI:17303 Morphine | 14150 DB00295 Morphine (3,262)14437 DB11130 Opium (3,230; J 0.517) | CHEBI:17303 ‘Morphine’ | 2,176 | distinct | J7 | Opium is a mixture of alkaloids; morphine is one of them |
| 10 | 79697CHEBI:17347 Testosterone | 14208 DB00624 Testosterone (3,430)14342 DB01420 Testosterone propionate (2,114; J 0.582)14476 DB13943 Testosterone cypionate (3,160; J 0.863)14477 DB13944 Testosterone enanthate (3,368; J 0.821)14478 DB13946 Testosterone undecanoate (3,362; J 0.734) | CHEBI:17347 ‘Testosterone’ | 10,984 | distinct | J2 | Testosterone and four esters of it (propionate, cypionate, enanthate, undecanoate) |
| 11 | 79710CHEBI:7627 Norethisterone | 14221 DB00717 Norethisterone (2,132)15744 DB14678 Norethindrone enanthate (852; J 0.400) | CHEBI:7627 ‘Norethisterone’ | 808 | distinct | J2 | Norethisterone and its enanthate ester |
| 12 | 79724CHEBI:16469 Estradiol | 14235 DB00783 Estradiol (4,458)14479 DB13952 Estradiol acetate (3,720; J 0.762)14480 DB13953 Estradiol benzoate (3,010; J 0.609)14481 DB13954 Estradiol cypionate (4,158; J 0.857)14483 DB13956 Estradiol valerate (4,154; J 0.856)14501 DB15690 Fluoroestradiol F-18 (32; J 0.003) | CHEBI:16469 ‘Estradiol’ | 14,552 | distinct | J2 | Estradiol, four esters of it, and fluoroestradiol F-18, a different molecule (a fluorinated PET tracer) |
| 13 | 79732CHEBI:45599 Sodium lauryl sulfate | 14243 DB00815 Sodium lauryl sulfate (2)19752 DB03967 Dodecyl sulfate (2; J 0.000) | CHEBI:45599 ‘Dodecyl sulfate’ | 0 | distinct | J3 | Sodium lauryl sulfate (a salt) and the dodecyl sulfate anion |
| 14 | 79758CHEBI:6888 Methylprednisolone | 14269 DB00959 Methylprednisolone (2,734)21385 DB14643 Methylprednisolone aceponate (240; J 0.088) | CHEBI:6888 ‘Methylprednisolone’ | 230 | distinct | J2 | Methylprednisolone and its aceponate ester |
| 15 | 79770CHEBI:47519 Ketoconazole | 14281 DB01026 Ketoconazole (2,802)17623 DB05667 Levoketoconazole (4; J 0.000) | CHEBI:47519 ‘Ketoconazole’ | 0 | distinct | J1 | Levoketoconazole is one enantiomer of racemic ketoconazole, developed for a different use (cortisol synthesis inhibition) |
| 16 | 79773CHEBI:5001 Fenofibrate | 14284 DB01039 Fenofibrate (2,988)16268 DB13873 Fenofibric acid (1,804; J 0.590) | CHEBI:5001 ‘Fenofibrate’ | 1,754 | distinct | J2 | Fenofibrate is the ester prodrug of fenofibric acid; both are approved products |
| 17 | 79809CHEBI:41879 Dexamethasone | 14320 DB01234 Dexamethasone (3,586)14493 DB14649 Dexamethasone acetate (56; J 0.016)15041 DB11487 Dexamethasone isonicotinate (2,128; J 0.585) | CHEBI:41879 ‘Dexamethasone’ | 2,100 | distinct | J2 | Dexamethasone and two esters of it (acetate, isonicotinate) |
| 18 | 79913CHEBI:43830 Methylene blue | 14424 DB09241 Methylene blue (4,506)17203 DB08167 Methylthioninium (486; J 0.107) | CHEBI:43830 ‘Methylthioninium’ | 482 | distinct | J3 | Methylene blue is methylthioninium chloride; the other entry is the cation |
| 19 | 79930CHEBI:9679 Calcium Phosphate | 14441 DB11348 Calcium Phosphate (740)14486 DB14481 Calcium phosphate dihydrate (250; J 0.320) | CHEBI:9679 ‘CID 516943’ | 240 | unclear (lean: split) | J5 | DrugBank’s ‘Calcium Phosphate’ does not say which calcium phosphate it is; the dihydrate (MW 172.09) is dicalcium phosphate dihydrate. Same salt: J5 keeps them merged. Tricalcium phosphate: different compounds, split |
| 20 | 79976CHEBI:25107 Magnesium | 14487 DB14513 Magnesium (2,060)20399 DB09407 Magnesium chloride (1,890; J 0.257) | CHEBI:25107 ‘magnesium atom’ | 800 | distinct | J7 | Magnesium (DrugBank: the element) and one specific compound of it |
| 21 | 79980CHEBI:35176 Zinc sulfate, unspecified form | 14491 DB14548 Zinc sulfate, unspecified form (374)14751 DB09322 Zinc sulfate (202; J 0.263) | CHEBI:35176 ‘Zinc sulfate’ | 120 | same | J5 | Zinc sulfate and DrugBank’s ‘unspecified form’ entry for it (hydration state not stated): one substance |
| 22 | 79999CHEBI:28304 Heparin | 14510 DB01109 Heparin (1,942)20334 DB15404 Heparin, bovine (76; J 0.039) | CHEBI:28304 ‘Heparin’ | 76 | distinct | J6 | Unfractionated heparin and bovine-derived heparin: different source tissue, a separate DrugBank entry |
| 23 | 80075CHEBI:16134 Ammonia | 14585 DB11118 Ammonia (36)14927 DB09326 Ammonia N-13 (4; J 0.000) | CHEBI:16134 ‘Ammonia’ | 0 | distinct | J4 | Ammonia and ammonia N-13, a PET perfusion tracer |
| 24 | 80077CHEBI:16236 Ethanol | 14587 DB00898 Ethanol (3,226)15256 DB13293 Ipecac (2,144; J 0.322) | CHEBI:16236 ‘Ethanol’ | 1,296 | distinct | evidence | Unrelated substances: ethanol and ipecac (a plant emetic). Node Normalizer conflated them |
| 25 | 80078CHEBI:6062 Isosorbide mononitrate | 14588 DB01020 Isosorbide mononitrate (2,382)16719 DB09401 Isosorbide (2,066; J 0.399) | CHEBI:6062 ‘Isosorbide mononitrate’ | 1,246 | distinct | evidence | Different molecules: isosorbide mononitrate (MW 191.14) and isosorbide (MW 146.14) |
| 26 | 80079CHEBI:30742 Polyethylene glycol 400 | 14589 DB11077 Polyethylene glycol 400 (1,632)20254 DB09287 Polyethylene glycol (984; J 0.175)20313 DB11161 Polyethylene glycol 300 (86; J 0.053) | CHEBI:30742 ‘macrogol’ | 480 | distinct | J7 | Polyethylene glycol (the polymer family) and two specific grades of it (PEG 300, PEG 400) |
| 27 | 80108CHEBI:3347 Candesartan | 14605 DB00796 Candesartan cilexetil (2,116; J 0.894)14618 DB13919 Candesartan (2,210) | CHEBI:3347 ‘Candesartan’ | 2,022 | distinct | J2 | Candesartan cilexetil is the ester prodrug; candesartan is its active form |
| 28 | 80109UMLS:C0700346 Antihemophilic factor, human recombinant | 14619 DB00025 Antihemophilic factor, human recombinant (220)14692 DB13192 Antihemophilic factor human (196; J 0.891) | UMLS:C0700346 ‘Koate-DVI’ | 192 | distinct | J6 | Recombinant factor VIII and plasma-derived factor VIII: different products |
| 29 | 80127CHEBI:25879 Pentaerythritol tetranitrate | 14637 DB06154 Pentaerythritol tetranitrate (1,628)20350 DB13526 Pentaerithrityl (888; J 0.164) | CHEBI:25879 ‘Pentaerythritol tetranitrate’ | 358 | distinct | evidence | Different molecules: pentaerythritol tetranitrate (MW 316.14), a vasodilator, and pentaerythritol (MW 136.15) |
| 30 | 80134CHEBI:30830 Sodium oxybate | 14644 DB09072 Sodium oxybate (1,688)19279 DB01440 gamma-Hydroxybutyric acid (1,478; J 0.865) | CHEBI:30830 ‘sodium oxybate’ | 1,452 | distinct | J3 | Sodium oxybate is the sodium salt of gamma-hydroxybutyric acid |
| 31 | 80144CHEBI:15354 Choline salicylate | 14653 DB00122 Choline (254; J 0.112)14654 DB14006 Choline salicylate (2,178)18900 DB04660 Choline alfoscerate (2; J 0.000)20813 DB09277 Choline C 11 (1,494; J 0.666) | CHEBI:15354 ‘Choline’ | 1,684 | distinct | J4 | Four different drugs: choline; choline salicylate (salicylate is an active NSAID); choline alfoscerate (glycerophosphocholine); choline C-11 (a PET tracer) |
| 32 | 80195CHEBI:27300 Vitamin D | 14560 DB00153 Ergocalciferol (520; J 0.194)14561 DB00169 Cholecalciferol (942; J 0.238)14705 DB11094 Vitamin D (1,180) | CHEBI:27300 ‘Ergocalciferol’ | 790 | distinct | J7 | Vitamin D (the class) and two different molecules in it, ergocalciferol (D2) and cholecalciferol (D3) |
| 33 | 80208CHEBI:27568 Selenium | 14716 DB11135 Selenium (1,464)14742 DB11127 Selenious acid (1,462; J 0.985) | CHEBI:27568 ‘Selenium’ | 1,430 | distinct | J7 | Selenium (the trace element) and selenious acid, one compound of it |
| 34 | 80259CHEBI:17234 Dextrose, unspecified form | 14353 DB01914 D-glucose (34; J 0.233)14766 DB09341 Dextrose, unspecified form (114)15755 DB02379 Beta-D-Glucose (46; J 0.000) | CHEBI:17234 ‘D-glucose’ | 28 | unclear → {14353, 14766} · {15755} | J1, J5 | D-glucose and dextrose (same DrugBank text) are one substance under J5; beta-D-glucose is one anomer, which interconverts with the other in water, so J1 may not apply |
| 35 | 80261UNII:PUE0TO3XDR Dalteparin | 14768 DB06779 Dalteparin (1,938)15890 DB06822 Tinzaparin (1,862; J 0.953)16669 DB09258 Bemiparin (1,856; J 0.952) | UNII:PUE0TO3XDR ‘Dalteparin’ | 3,634 | distinct | evidence | Three different low-molecular-weight heparins, each separately made and approved |
| 36 | 80285CHEBI:45863 Paclitaxel | 14792 DB01229 Paclitaxel (3,484)21238 DB13809 Paclitaxel poliglumex (118; J 0.033) | CHEBI:45863 ‘Paclitaxel’ | 118 | distinct | J2 | Paclitaxel and paclitaxel poliglumex, a polymer-drug conjugate prodrug |
| 37 | 80292CHEBI:5931 Insulin human | 14435 DB09564 Insulin degludec (1,466; J 0.891)14799 DB00030 Insulin human (1,636)15283 DB00047 Insulin glargine (1,548; J 0.925)15331 DB01306 Insulin aspart (1,292; J 0.755)15332 DB01309 Insulin glulisine (1,468; J 0.895)15378 DB09456 Insulin beef (1,612; J 0.983) | CHEBI:5931 ‘insulin human’ | 7,064 | distinct | evidence | Six different insulins: human insulin, four analogues with different sequences and kinetics, and beef insulin |
| 38 | 80297CHEBI:15671 L-tartaric acid | 14804 DB09459 L-tartaric acid (80)18840 DB01694 D-tartaric acid (10; J 0.000) | CHEBI:15671 ‘L-tartaric acid’ | 0 | distinct | J1 | L- and D-tartaric acid are enantiomers (known example) |
| 39 | 80309CHEBI:4735 Edetic acid | 14756 DB14600 Edetate disodium anhydrous (12; J 0.006)14816 DB00974 Edetic acid (1,340) | CHEBI:4735 ‘Edetic acid’ | 8 | distinct | J3 | Edetate disodium (a salt, used for hypercalcaemia) and edetic acid |
| 40 | 80328CHEBI:3011 Benazepril | 14835 DB00542 Benazepril (1,968)21149 DB14125 Benazeprilat (1,612; J 0.814) | CHEBI:3011 ‘Benazepril’ | 1,600 | distinct | J2 | Benazepril is the ester prodrug; benazeprilat is its active metabolite |
| 41 | 80388CHEBI:82717 Tedizolid phosphate | 14895 DB09042 Tedizolid phosphate (780)14896 DB14569 Tedizolid (42; J 0.054) | CHEBI:82717 ‘Tedizolid’ | 42 | distinct | J2 | Tedizolid phosphate is the prodrug of tedizolid |
| 42 | 80411CHEBI:16335 Adenosine | 14632 DB00131 Adenosine phosphate (42; J 0.000)14918 DB00640 Adenosine (1,520) | CHEBI:16335 ‘Adenosine’ | 0 | distinct | evidence | Different molecules: adenosine monophosphate (a nucleotide) and adenosine (the nucleoside) |
| 43 | 80436CHEBI:231594 Levonorgestrel | 14826 DB09389 Norgestrel (924; J 0.515)14943 DB00367 Levonorgestrel (1,788) | CHEBI:231594 ‘Norgestrel’ | 878 | distinct | J1 | Norgestrel is the racemate; levonorgestrel is its active enantiomer |
| 44 | 80442CHEBI:17698 Chloramphenicol | 14392 DB07565 Chloramphenicol succinate (38; J 0.019)14949 DB00446 Chloramphenicol (1,764) | CHEBI:17698 ‘Chloramphenicol’ | 34 | distinct | J2 | Chloramphenicol succinate is the ester prodrug of chloramphenicol |
| 45 | 80496CHEBI:16962 Cortisone acetate | 15003 DB01380 Cortisone acetate (2,864)15071 DB14681 Cortisone (1,926; J 0.637) | CHEBI:16962 ‘Cortisone’ | 1,800 | distinct | J2 | Cortisone acetate and the parent cortisone |
| 46 | 80519CHEBI:17252 Hydroxyprogesterone caproate | 15026 DB06789 Hydroxyprogesterone caproate (1,426)15740 DB14570 Hydroxyprogesterone (266; J 0.154) | CHEBI:17252 ‘Hydroxyprogesterone’ | 222 | distinct | J2 | Hydroxyprogesterone caproate (the drug) and 17-hydroxyprogesterone (the endogenous steroid) |
| 47 | 80557CHEBI:6970 Mometasone furoate | 14970 DB00764 Mometasone (2,250; J 0.873)15064 DB14512 Mometasone furoate (2,350) | CHEBI:6970 ‘Mometasone’ | 2,078 | distinct | J2 | Mometasone and its furoate ester (the marketed form) |
| 48 | 80620CHEBI:8028 Perphenazine | 15127 DB00850 Perphenazine (2,770)21386 DB14651 Perphenazine enanthate (1,782; J 0.641) | CHEBI:8028 ‘Perphenazine’ | 1,762 | distinct | J2 | Perphenazine and its enanthate depot ester |
| 49 | 80720CHEBI:85155 Netupitant | 15227 DB09048 Netupitant (1,860)15731 DB14019 Fosnetupitant (414; J 0.221) | CHEBI:85155 ‘Netupitant’ | 414 | distinct | J2 | Fosnetupitant is the intravenous prodrug of netupitant |
| 50 | 80753CHEBI:4464 Dexchlorpheniramine | 14575 DB09555 Dexchlorpheniramine maleate (1,484; J 0.777)15260 DB13679 Dexchlorpheniramine (1,682) | CHEBI:4464 ‘Dexchlorpheniramine’ | 1,382 | distinct | J3 | Dexchlorpheniramine maleate (the salt) and dexchlorpheniramine |
| 51 | 80797CHEBI:92998 Guanabenz | 15304 DB00629 Guanabenz (2,040)15754 DB00702 Icodextrin (2; J 0.000) | CHEBI:92998 ‘Guanabenz’ | 0 | distinct | evidence | Unrelated drugs: guanabenz (an alpha-2 agonist) and icodextrin (a glucose polymer for peritoneal dialysis). Node Normalizer conflated them |
| 52 | 80960CHEBI:50785 Ethynodiol diacetate | 15467 DB00823 Ethynodiol diacetate (1,172)15724 DB13866 Etynodiol (830; J 0.708) | CHEBI:50785 ‘Etynodiol’ | 786 | distinct | J2 | Ethynodiol diacetate (the marketed ester) and etynodiol (never marketed, per DrugBank) |
| 53 | 80961CHEBI:25187 Levomenthol | 15468 DB00825 Levomenthol (1,324)20335 DB14123 Racementhol (122; J 0.000) | CHEBI:25187 ‘Menthol’ | 0 | distinct | J1 | Levomenthol is one enantiomer; racementhol is the racemate |
| 54 | 80980CHEBI:16973 Desoxycorticosterone pivalate | 15487 DB01134 Desoxycorticosterone pivalate (18)15581 DB06780 Desoxycorticosterone acetate (18; J 1.000) | CHEBI:16973 ‘desoxycortone’ | 18 | distinct | J2 | Two different esters of desoxycorticosterone (pivalate, acetate) |
| 55 | 81072CHEBI:135590 Ambroxol | 15579 DB06742 Ambroxol (18)15897 DB13141 Ambroxol acefyllinate (8; J 0.083) | CHEBI:135590 ‘Ambroxol’ | 2 | distinct | evidence | Ambroxol acefyllinate adds a second active ingredient (acefylline, a xanthine) |
| 56 | 81144CHEBI:34831 Melengestrol | 15070 DB14659 Melengestrol acetate (1,704; J 0.574)15651 DB11529 Melengestrol (1,860) | CHEBI:34831 ‘melengestrol’ | 1,240 | distinct | J2 | Melengestrol and its acetate ester |
| 57 | 81201CHEBI:59919 Dihydroergocryptine | 15641 DB11274 Dihydro-alpha-ergocryptine (24; J 0.008)15708 DB13385 Dihydroergocryptine (2,104) | CHEBI:59919 ‘Dihydro-alpha-ergocryptine’ | 16 | unclear (lean: split) | J7 | DrugBank’s own text says alpha-dihydroergocryptine ‘is usually referred to the mixture’ of alpha and beta; mixture vs component (J7) or one drug under two names |
| 58 | 81222CHEBI:135564 Nomegestrol acetate | 15658 DB11636 Nomegestrol (830; J 0.906)15729 DB13981 Nomegestrol acetate (916) | CHEBI:135564 ‘Nomegestrol acetate’ | 786 | distinct | J2 | Nomegestrol and its acetate ester |
| 59 | 81234CHEBI:174358 Eslicarbazepine | 15620 DB09119 Eslicarbazepine acetate (2,100; J 0.971)15741 DB14575 Eslicarbazepine (2,146) | CHEBI:174358 ‘Eslicarbazepine’ | 2,058 | distinct | J2 | Eslicarbazepine acetate is the prodrug of eslicarbazepine |
| 60 | 81264CHEBI:102484 Sulfisoxazole | 15771 DB00263 Sulfisoxazole (2,030)15803 DB14033 Acetyl sulfisoxazole (624; J 0.306) | CHEBI:102484 ‘Sulfisoxazole’ | 598 | distinct | J2 | Acetyl sulfisoxazole is an ester of sulfisoxazole |
| 61 | 81310CHEBI:2469 Adefovir dipivoxil | 15817 DB00718 Adefovir dipivoxil (1,618)20851 DB13868 Adefovir (1,410; J 0.869) | CHEBI:2469 ‘Adefovir’ | 1,388 | distinct | J2 | Adefovir dipivoxil is the diester prodrug of adefovir |
| 62 | 81534CHEBI:28939 Acetylcysteine | 16036 DB06151 Acetylcysteine (222)20971 DB14479 Acetylcysteine zinc (120; J 0.000) | CHEBI:28939 ‘Acetylcysteine’ | 0 | distinct | J3 | Acetylcysteine and its zinc salt |
| 63 | 81559CHEBI:92712 Atropine | 16057 DB00424 Hyoscyamine (2,910; J 0.773)16061 DB00572 Atropine (3,216) | CHEBI:92712 ‘ENDO-ATROPINE’ | 2,646 | distinct | J1 | Hyoscyamine is the L-enantiomer; atropine is the racemate |
| 64 | 81677CHEMBL.COMPOUND:CHEMBL2108455 Turoctocog alfa | 16179 DB09109 Turoctocog alfa (200)18412 DB11606 Susoctocog alfa (194; J 0.931) | CHEMBL.COMPOUND:CHEMBL2108455 ‘octocog alfa’ | 186 | distinct | evidence | Different factor VIII molecules: human sequence (turoctocog) and porcine sequence (susoctocog) |
| 65 | 81715CHEBI:17115 Serine | 16217 DB00133 Serine (98)17034 DB03929 D-Serine (10; J 0.019) | CHEBI:17115 ‘Serine’ | 2 | distinct | J1 | L-serine and D-serine are enantiomers |
| 66 | 81792CHEBI:132767 Ferric pyrophosphate | 16294 DB09147 Ferric pyrophosphate (318)16296 DB13995 Ferric pyrophosphate citrate (148; J 0.447) | CHEBI:132767 ‘Ferric pyrophosphate’ | 144 | distinct | evidence | Different iron complexes: ferric pyrophosphate (insoluble) and ferric pyrophosphate citrate (soluble, per DrugBank) |
| 67 | 81840CHEBI:18248 Iron | 14114 DB00893 Iron Dextran (284; J 0.810)16341 DB01592 Iron (324) | CHEBI:18248 ‘Iron’ | 266 | distinct | J7 | Iron (the element) and iron dextran (one specific complex) |
| 68 | 81935CHEBI:34584 Boldenone | 16436 DB01541 Boldenone (294)16463 DB14639 Boldenone undecylenate (2; J 0.007) | CHEBI:34584 ‘Boldenone’ | 2 | distinct | J2 | Boldenone and its undecylenate ester |
| 69 | 82393CHEBI:232390 Secretin human | 16892 DB09532 Secretin human (390)21400 DB09527 Secretin porcine (326; J 0.836) | CHEBI:232390 ‘CID 102090385’ | 318 | distinct | evidence | Different peptides: human and porcine secretin (MW 3039.46 vs 3055.46) |
| 70 | 82524CHEBI:78538 Tafamidis | 14081 DB05352 Fx-1006A (2; J 0.000)17023 DB11644 Tafamidis (350) | CHEBI:78538 ‘Fx-1006A’ | 0 | distinct | J3 | Fx-1006A is tafamidis meglumine (DrugBank’s tafamidis text says so), the salt; tafamidis is the free acid |
| 71 | 82605CHEBI:81569 Follitropin | 17104 DB00066 Follitropin (376)17105 DB00094 Urofollitropin (180; J 0.004) | CHEBI:81569 ‘Follitropin’ | 2 | distinct | J6 | Recombinant follitropin and urine-derived urofollitropin |
| 72 | 82785UNII:MRL3739G66 Lutetium Lu 177 dotatate | 17282 DB13985 Lutetium Lu 177 dotatate (24)19675 DB13925 Dotatate gallium Ga-68 (12; J 0.500) | UNII:MRL3739G66 ‘Dotatate’ | 12 | distinct | J4 | One peptide with two radionuclides: Lu-177 dotatate (therapy) and Ga-68 dotatate (imaging) |
| 73 | 82807CHEBI:15553 Dinoprost tromethamine | 17302 DB01160 Dinoprost tromethamine (228)19699 DB12789 Dinoprost (98; J 0.006) | CHEBI:15553 ‘Dinoprost’ | 2 | distinct | J3 | Dinoprost tromethamine (the salt) and dinoprost |
| 74 | 82837CHEBI:81567 Thyrotropin alfa | 17332 DB00024 Thyrotropin alfa (970)20956 DB15263 Thyrotropin (374; J 0.386) | CHEBI:81567 ‘Thyrotropin’ | 372 | distinct | J6 | Recombinant thyrotropin alfa and endogenous thyrotropin |
| 75 | 82852CHEBI:7507 Neomycin | 17347 DB00994 Neomycin (1,930)18101 DB00452 Framycetin (1,668; J 0.858) | CHEBI:7507 ‘Framycetin’ | 1,626 | distinct | J7 | Neomycin is a complex of components; framycetin is one of them (neomycin B) |
| 76 | 82901CHEBI:6900 Metocurine | 17396 DB01336 Metocurine (2,378)17645 DB00416 Metocurine iodide (2,178; J 0.914) | CHEBI:6900 ‘Metocurine’ | 2,122 | distinct | J3 | Metocurine and its iodide salt |
| 77 | 83066CHEBI:42797 Gabapentin enacarbil | 14709 DB00996 Gabapentin (2,220; J 0.456)17559 DB08872 Gabapentin enacarbil (2,740) | CHEBI:42797 ‘Gabapentin’ | 1,536 | distinct | J2 | Gabapentin enacarbil is a prodrug of gabapentin, approved for different indications |
| 78 | 84213CHEBI:75275 Tetradecyl hydrogen sulfate (ester) | 17060 DB00464 Sodium tetradecyl sulfate (32; J 0.000)18702 DB11328 Tetradecyl hydrogen sulfate (ester) (1,474) | CHEBI:75275 ‘Tetradecyl hydrogen sulfate (ester)‘ | 0 | distinct | J3 | Sodium tetradecyl sulfate (the salt) and tetradecyl hydrogen sulfate (the acid) |
| 79 | 85123UMLS:C2346821 CT-011 | 19602 DB05916 CT-011 (668)21194 DB15383 Pidilizumab (666; J 0.991) | UMLS:C2346821 ‘pidilizumab’ | 658 | same | J8 | CT-011 is the development code for pidilizumab, one anti-PD-1 antibody |
| 80 | 85689CHEBI:62964 Magnesium acetate tetrahydrate | 17633 DB13996 Magnesium acetate (554; J 0.965)20145 DB09409 Magnesium acetate tetrahydrate (566) | CHEBI:62964 ‘CID 517043’ | 544 | same | J5 | Magnesium acetate and its tetrahydrate: one salt |
| 81 | 85779CHEBI:30769 Sodium citrate | 16350 DB04272 Citric acid (246; J 0.071)20218 DB09154 Sodium citrate (1,746) | CHEBI:30769 ‘Citric acid’ | 132 | distinct | J3 | Citric acid and sodium citrate, its sodium salt |
| 82 | 85782CHEBI:92769 Iobenguane sulfate I-123 | 20220 DB06704 Iobenguane (276; J 0.038)20221 DB09546 Iobenguane sulfate I-123 (1,474) | CHEBI:92769 ‘Iobenguane’ | 64 | distinct | J4 | Iobenguane and the I-123 iobenguane sulfate radiopharmaceutical |
| 83 | 85856CHEBI:34683 Sodium phosphate, monobasic | 20290 DB09449 Sodium phosphate, monobasic (1,738)20291 DB14502 Sodium phosphate, dibasic (870; J 0.495)20292 DB14503 Sodium phosphate, monobasic, unspecified form (832; J 0.470)20542 DB09370 Sodium phosphate P 32 (6; J 0.000)21026 DB14504 Sodium phosphate, dibasic, unspecified form (820; J 0.465) | CHEBI:34683 ‘CID 58592228’ | 2,482 | distinct → {20290, 20292} · {20291, 21026} · {20542} | J3, J4, J5 | Two different salts (monobasic, dibasic), each with its ‘unspecified form’ entry, and the P-32 radiotherapeutic |
| 84 | 85860CHEBI:17268 Inositol | 15885 DB08949 Inositol nicotinate (70; J 0.039)20293 DB13178 Inositol (1,520) | CHEBI:17268 ‘Inositol’ | 60 | distinct | evidence | Inositol nicotinate is a niacin ester that delivers nicotinic acid; inositol is a different drug |
| 85 | 85891CHEBI:135361 Methscopolamine | 16058 DB00462 Methscopolamine bromide (1,922; J 0.899)20321 DB11315 Methscopolamine (2,120) | CHEBI:135361 ‘Methscopolamine’ | 1,892 | distinct | J3 | Methscopolamine and its bromide salt |
| 86 | 85926CHEBI:36773 Synthetic camphor | 18372 DB01744 Camphor (8; J 0.000)20338 DB14156 Synthetic camphor (68) | CHEBI:36773 ‘camphor’ | 0 | unclear (lean: split) | J1 | Synthetic camphor is the racemate and natural camphor the (+)-enantiomer, but neither DrugBank entry states its stereochemistry |
| 87 | 85941CHEBI:53248 Povidone | 20352 DB11061 Povidone (22)20353 DB14478 Povidone K30 (22; J 1.000) | CHEBI:53248 ‘Povidone’ | 22 | distinct | J7 | Povidone (the polymer) and povidone K30, one grade of it |
| 88 | 86030UNII:6M3P64V0NC Almasilate | 20403 DB13595 Almasilate (1,964)21273 DB11230 Magnesium Aluminum Silicate (182; J 0.050) | UNII:6M3P64V0NC ‘Almasilate’ | 100 | distinct | evidence | Different materials: almasilate, a synthetic crystalline antacid, and magnesium aluminium silicate, a natural clay excipient |
| 89 | 86036CHEBI:17334 Procaine benzylpenicillin | 14288 DB01053 Benzylpenicillin (1,232; J 0.271)20409 DB09320 Procaine benzylpenicillin (1,806) | CHEBI:17334 ‘penicillin’ | 620 | distinct | evidence | Procaine benzylpenicillin adds procaine, a second active ingredient (a depot formulation) |
| 90 | 86069CHEBI:7466 Nandrolone | 14825 DB08804 Nandrolone decanoate (402; J 0.762)16433 DB00984 Nandrolone phenpropionate (374; J 0.809)20432 DB13169 Nandrolone (458) | CHEBI:7466 ‘Nandrolone’ | 678 | distinct | J2 | Nandrolone and two esters of it (decanoate, phenpropionate) |
| 91 | 86079CHEBI:78320 Ammonium lactate | 20217 DB04398 Lactic acid (28; J 0.000)20437 DB06768 Ammonium lactate (80) | CHEBI:78320 ‘Lactic acid’ | 0 | distinct | J3 | Lactic acid and ammonium lactate, its salt |
| 92 | 86199CHEBI:2914 Atracurium | 17648 DB00732 Atracurium besylate (2,178; J 0.971)20460 DB13295 Atracurium (2,236) | CHEBI:2914 ‘Atracurium’ | 2,120 | distinct | J3 | Atracurium and its besylate salt |
| 93 | 86316UMLS:C2350656 Pegloticase | 20488 DB09208 Pegloticase (114)21528 DB05321 PEG-uricase (72; J 0.603) | UMLS:C2350656 ‘pegloticase’ | 70 | same | J8 | PEG-uricase (a PEG conjugate of recombinant porcine uricase, in phase III in 2006) is pegloticase before approval |
| 94 | 87902CHEBI:34838 Drostanolone propionate | 14526 DB00858 Drostanolone (6; J 0.000)20630 DB14655 Drostanolone propionate (192) | CHEBI:34838 ‘Drostanolone’ | 0 | distinct | J2 | Drostanolone and its propionate ester |
| 95 | 88287CHEBI:140407 Ceftobiprole | 20665 DB04918 Ceftobiprole (1,548)21610 DB14733 Ceftobiprole medocaril (34; J 0.022) | CHEBI:140407 ‘Ceftobiprole’ | 34 | distinct | J2 | Ceftobiprole medocaril is the prodrug of ceftobiprole |
| 96 | 88429CHEBI:33353 Technetium Tc-99m exametazime | 16329 DB00113 Technetium Tc-99m arcitumomab (660; J 0.011)20181 DB09266 Technetium Tc-99m tilmanocept (58; J 0.004)20807 DB09163 Technetium Tc-99m exametazime (1,472) | CHEBI:33353 ‘technetium atom’ | 24 | distinct | evidence | Three different radiopharmaceuticals that share only the Tc-99m label: an anti-CEA antibody fragment, tilmanocept, exametazime |
| 97 | 88504DRUGBANK:DB14432 Influenza A virus A/California/7/2009 X-181 (H1N1) antigen (propiolactone inactivated) | 20882 DB10600 Influenza A virus A/California/7/2009 X-181 (H1N1) antigen (propiolactone inactivated) (584)20893 DB11041 Influenza A virus A/California/7/2009 X-181 (H1N1) hemagglutinin antigen (propiolactone inactivated) (584; J 1.000) | DRUGBANK:DB14432 ‘Influenza A virus A/Singapore/gp1908/2015 IVR-180 (H1N1) hemagglutinin antigen (formaldehyde inactivated)‘ | 560 | unclear (lean: split) | none | Two DrugBank vaccine components for one strain (A/California/7/2009 X-181), ‘antigen’ and ‘hemagglutinin antigen’, both propiolactone-inactivated; identical PrimeKG edges |
| 98 | 88506UMLS:C4033637 Influenza B virus B/Brisbane/60/2008 antigen (formaldehyde inactivated) | 20884 DB10794 Influenza B virus B/Brisbane/60/2008 antigen (formaldehyde inactivated) (584)20892 DB11040 Influenza B virus B/Brisbane/60/2008 hemagglutinin antigen (propiolactone inactivated) (584; J 1.000)20894 DB11044 Influenza B virus B/Brisbane/60/2008 antigen (propiolactone inactivated) (584; J 1.000)20944 DB14385 Influenza B virus B/Brisbane/60/2008 hemagglutinin antigen (formaldehyde inactivated) (584; J 1.000) | UMLS:C4033637 ‘influenza B virus B/Brisbane/60/2008 antigen’ | 1,680 | unclear (lean: split) | J6 | Four DrugBank vaccine components for one strain (B/Brisbane/60/2008): antigen or hemagglutinin antigen, formaldehyde- or propiolactone-inactivated; identical PrimeKG edges |
| 99 | 88589CHEBI:133354 Bentonite | 20967 DB11306 Bentonite (54)20969 DB13654 Montmorillonite (54; J 1.000) | CHEBI:133354 ‘Bentonite’ | 54 | distinct | J7 | Bentonite is a clay; montmorillonite is its main mineral |
| 100 | 88647CHEBI:22652 Sodium ascorbate | 14128 DB00126 Ascorbic acid (320; J 0.108)21025 DB14482 Sodium ascorbate (934) | CHEBI:22652 ‘Ascorbic acid’ | 114 | distinct | J3 | Ascorbic acid and sodium ascorbate, its salt |
| 101 | 89004CHEBI:28842 Magnesium stearate | 14355 DB03193 Stearic acid (8; J 0.000)21382 DB14077 Magnesium stearate (182) | CHEBI:28842 ‘Stearic acid’ | 0 | distinct | J3 | Stearic acid and magnesium stearate, its salt |
| 102 | 89006CHEBI:50848 Loteprednol | 14754 DB14596 Loteprednol etabonate (46; J 0.000)21384 DB00873 Loteprednol (374) | CHEBI:50848 ‘Loteprednol’ | 0 | distinct | J2 | Loteprednol etabonate (the marketed ester) and loteprednol |
| 103 | 89236CHEBI:136037 Corticorelin ovine triflutate | 14642 DB05394 Corticorelin (20; J 0.000)21614 DB09067 Corticorelin ovine triflutate (136) | CHEBI:136037 ‘Corticorelin’ | 0 | distinct | evidence | Different peptides: corticorelin (human CRF) and corticorelin ovine triflutate (the ovine sequence) |
| 104 | 89486CHEBI:30021 Selenomethionine Se-75 | 21864 DB09400 Selenomethionine Se-75 (2)21866 DB11142 Selenomethionine (2; J 1.000) | CHEBI:30021 ‘Selenomethionine’ | 2 | distinct | J4 | Selenomethionine and its Se-75 radiolabelled form |
7.2 Pathways (18 groups)
Merge key for every row: identical names, joined by pass 2. Reactome records were read on 2026-10-05.
| # | Entity | Members: x_index Reactome id name (rows; J) | Lost | Verdict | Rule | Reason |
|---|---|---|---|---|---|---|
| 166 | 169000REACTOME:R-HSA-3656244 Defective B4GALT1 causes B4GALT1-CDG (CDG-2d) | 128266 R-HSA-3656244 Defective B4GALT1 causes B4GALT1-CDG (CDG-2d) (18)128330 R-HSA-4793953 Defective B4GALT1 causes B4GALT1-CDG (CDG-2d) (4; J 0.100) | 2 | distinct | evidence | Different Reactome pathways that shared a name: R-HSA-3656244: disease congenital disorder of glycosylation type II; normal pathway Keratan sulfate/keratin metabolism; 3 events; vs R-HSA-4793953: disease congenital disorder of glycosylation type II; normal pathway Asparagine N-linked glycosylation; event R-HSA-4793956; PrimeKG proteins 8 vs 1 |
| 167 | 169493REACTOME:R-HSA-5683678 Defective ABCA3 causes SMDP3 | 128636 R-HSA-5683678 Defective ABCA3 causes SMDP3 (4)128776 R-HSA-5688399 Defective ABCA3 causes SMDP3 (4; J 0.333) | 2 | distinct | evidence | Different Reactome pathways that shared a name: R-HSA-5683678: disease newborn respiratory distress syndrome; normal pathway ABC-family protein mediated transport; event R-HSA-5683672; vs R-HSA-5688399: disease interstitial lung disease, newborn respiratory distress syndrome; normal pathway Surfactant metabolism; event R-HSA-5688397; different PrimeKG parents |
| 168 | 169498REACTOME:R-HSA-5619037 Defective SLC35A1 causes congenital disorder of glycosylation 2F (CDG2F) | 128641 R-HSA-5619037 Defective SLC35A1 causes congenital disorder of glycosylation 2F (CDG2F) (4)128707 R-HSA-5663020 Defective SLC35A1 causes congenital disorder of glycosylation 2F (CDG2F) (4; J 1.000) | 4 | distinct | J9 | Joined only by name; in PrimeKG both have the same parent and protein. R-HSA-5619037: disease congenital disorder of glycosylation type II; normal pathway Transport of vitamins, nucleosides, and related molecules; event R-HSA-5651942; vs R-HSA-5663020: disease congenital disorder of glycosylation type II; normal pathway Sialic acid metabolism; event R-HSA-5651942; Reactome now names them apart |
| 169 | 169504REACTOME:R-HSA-5619044 Defective SLC6A19 causes Hartnup disorder (HND) | 128647 R-HSA-5619044 Defective SLC6A19 causes Hartnup disorder (HND) (4)128702 R-HSA-5659735 Defective SLC6A19 causes Hartnup disorder (HND) (4; J 1.000) | 4 | distinct | J9 | Joined only by name; in PrimeKG both have the same parent and protein. R-HSA-5619044: disease Hartnup disease; normal pathway SLC-mediated transport of neurotransmitters; event R-HSA-5659734; vs R-HSA-5659735: disease Hartnup disease; normal pathway SLC-mediated transport of amino acids; event R-HSA-5659734; Reactome now names them apart |
| 170 | 169531REACTOME:R-HSA-5619079 Defective SLC6A18 may confer susceptibility to iminoglycinuria and/or hyperglycinuria | 128674 R-HSA-5619079 Defective SLC6A18 may confer susceptibility to iminoglycinuria and/or hyperglycinuria (4)128701 R-HSA-5659729 Defective SLC6A18 may confer susceptibility to iminoglycinuria and/or hyperglycinuria (4; J 1.000) | 4 | distinct | J9 | Joined only by name; in PrimeKG both have the same parent and protein. R-HSA-5619079: not in Reactome now (HTTP Error 404: Not Found); vs R-HSA-5659729: not in Reactome now (HTTP Error 404: Not Found) |
| 171 | 169532REACTOME:R-HSA-5619081 Defective SLC6A3 causes Parkinsonism-dystonia infantile (PKDYS) | 128675 R-HSA-5619081 Defective SLC6A3 causes Parkinsonism-dystonia infantile (PKDYS) (4)128704 R-HSA-5660724 Defective SLC6A3 causes Parkinsonism-dystonia infantile (PKDYS) (4; J 1.000) | 4 | distinct | J9 | Joined only by name; in PrimeKG both have the same parent and protein. R-HSA-5619081: disease neurodegenerative disease; normal pathway Neurotransmitter clearance; event R-HSA-5660706; vs R-HSA-5660724: disease neurodegenerative disease; normal pathway SLC-mediated transport of neurotransmitters; event R-HSA-5660706; Reactome now names them apart |
| 172 | 169545REACTOME:R-HSA-5619101 Variant SLC6A20 contributes towards hyperglycinuria (HG) and iminoglycinuria (IG) | 128688 R-HSA-5619101 Variant SLC6A20 contributes towards hyperglycinuria (HG) and iminoglycinuria (IG) (4)128703 R-HSA-5660686 Variant SLC6A20 contributes towards hyperglycinuria (HG) and iminoglycinuria (IG) (4; J 1.000) | 4 | distinct | J9 | Joined only by name; in PrimeKG both have the same parent and protein. R-HSA-5619101: disease amino acid metabolic disorder; normal pathway SLC-mediated transport of neurotransmitters; event R-HSA-5660694; vs R-HSA-5660686: disease amino acid metabolic disorder; normal pathway SLC-mediated transport of amino acids; event R-HSA-5660694; Reactome now names them apart |
| 173 | 169553REACTOME:R-HSA-5619114 Defective SLC5A7 causes distal hereditary motor neuronopathy 7A (HMN7A) | 128696 R-HSA-5619114 Defective SLC5A7 causes distal hereditary motor neuronopathy 7A (HMN7A) (4)128700 R-HSA-5658471 Defective SLC5A7 causes distal hereditary motor neuronopathy 7A (HMN7A) (4; J 1.000) | 4 | distinct | J9 | Joined only by name; in PrimeKG both have the same parent and protein. R-HSA-5619114: disease motor peripheral neuropathy; normal pathway Neurotransmitter release cycle; event R-HSA-5658483; vs R-HSA-5658471: disease motor peripheral neuropathy; normal pathway SLC-mediated transport of organic anions; event R-HSA-5658483; Reactome now names them apart |
| 174 | 170344REACTOME:R-HSA-9679504 Translation of Replicase and Assembly of the Replication Transcription Complex | 63050 R-HSA-9679504 Translation of Replicase and Assembly of the Replication Transcription Complex (34)63058 R-HSA-9694676 Translation of Replicase and Assembly of the Replication Transcription Complex (34; J 0.700) | 30 | distinct | evidence | The same name in two virus hierarchies, SARS-CoV-1 and SARS-CoV-2 (PrimeKG parents); R-HSA-9679504: disease severe acute respiratory syndrome; 8 events; vs R-HSA-9694676: disease COVID-19; 7 events; 0 event(s) shared |
| 175 | 170345REACTOME:R-HSA-9679509 Virion Assembly and Release | 129307 R-HSA-9679509 Virion Assembly and Release (2)129322 R-HSA-9694322 Virion Assembly and Release (2; J 0.000) | 0 | distinct | evidence | The same name in two virus hierarchies, SARS-CoV-1 and SARS-CoV-2 (PrimeKG parents); R-HSA-9679509: disease severe acute respiratory syndrome; 8 events; vs R-HSA-9694322: disease COVID-19; 8 events; 0 event(s) shared |
| 176 | 170347REACTOME:R-HSA-9683701 Translation of structural proteins | 63054 R-HSA-9683701 Translation of structural proteins (12)63057 R-HSA-9694635 Translation of structural proteins (12; J 0.000) | 10 | distinct | evidence | The same name in two virus hierarchies, SARS-CoV-1 and SARS-CoV-2 (PrimeKG parents); R-HSA-9683701: disease severe acute respiratory syndrome; 10 events; vs R-HSA-9694635: disease COVID-19; 10 events; 0 event(s) shared |
| 177 | 170349REACTOME:R-HSA-9684325 Maturation of replicase proteins | 129309 R-HSA-9684325 Maturation of replicase proteins (2)129330 R-HSA-9694301 Maturation of replicase proteins (2; J 0.000) | 2 | distinct | evidence | The same name in two virus hierarchies, SARS-CoV-1 and SARS-CoV-2 (PrimeKG parents); R-HSA-9684325: disease severe acute respiratory syndrome; 14 events; vs R-HSA-9694301: disease COVID-19; 18 events; 0 event(s) shared |
| 178 | 170359REACTOME:R-HSA-9683610 Maturation of nucleoprotein | 129316 R-HSA-9683610 Maturation of nucleoprotein (22)129327 R-HSA-9694631 Maturation of nucleoprotein (22; J 0.833) | 22 | distinct | evidence | The same name in two virus hierarchies, SARS-CoV-1 and SARS-CoV-2 (PrimeKG parents); R-HSA-9683610: disease severe acute respiratory syndrome; 9 events; vs R-HSA-9694631: disease COVID-19; 14 events; 1 event(s) shared |
| 179 | 170360REACTOME:R-HSA-9683612 Maturation of protein M | 129317 R-HSA-9683612 Maturation of protein M (2)129326 R-HSA-9694594 Maturation of protein M (2; J 0.000) | 2 | distinct | evidence | The same name in two virus hierarchies, SARS-CoV-1 and SARS-CoV-2 (PrimeKG parents); R-HSA-9683612: disease severe acute respiratory syndrome; 3 events; vs R-HSA-9694594: disease COVID-19; 3 events; 0 event(s) shared |
| 180 | 170361REACTOME:R-HSA-9683673 Maturation of protein 3a | 129318 R-HSA-9683673 Maturation of protein 3a (20)129328 R-HSA-9694719 Maturation of protein 3a (20; J 0.818) | 20 | distinct | evidence | The same name in two virus hierarchies, SARS-CoV-1 and SARS-CoV-2 (PrimeKG parents); R-HSA-9683673: disease severe acute respiratory syndrome; 7 events; vs R-HSA-9694719: disease COVID-19; 7 events; 0 event(s) shared |
| 181 | 170362REACTOME:R-HSA-9683683 Maturation of protein E | 129319 R-HSA-9683683 Maturation of protein E (10)129324 R-HSA-9694493 Maturation of protein E (10; J 0.667) | 10 | distinct | evidence | The same name in two virus hierarchies, SARS-CoV-1 and SARS-CoV-2 (PrimeKG parents); R-HSA-9683683: disease severe acute respiratory syndrome; 5 events; vs R-HSA-9694493: disease COVID-19; 5 events; 0 event(s) shared |
| 182 | 170366REACTOME:R-HSA-9694614 Attachment and Entry | 129306 R-HSA-9678110 Attachment and Entry (10)129323 R-HSA-9694614 Attachment and Entry (12; J 0.571) | 8 | distinct | evidence | The same name in two virus hierarchies, SARS-CoV-1 and SARS-CoV-2 (PrimeKG parents); R-HSA-9678110: disease severe acute respiratory syndrome; 11 events; vs R-HSA-9694614: disease COVID-19; 11 events; 2 event(s) shared |
| 183 | 170371REACTOME:R-HSA-9694548 Maturation of spike protein | 129320 R-HSA-9683686 Maturation of spike protein (12)129325 R-HSA-9694548 Maturation of spike protein (60; J 0.161) | 12 | distinct | evidence | The same name in two virus hierarchies, SARS-CoV-1 and SARS-CoV-2 (PrimeKG parents); R-HSA-9683686: disease severe acute respiratory syndrome; 8 events; vs R-HSA-9694548: disease COVID-19; 12 events; 1 event(s) shared |
7.3 Disease (1 group)
| # | Entity | Members: x_index PrimeKG id name (rows; J) | Lost | Verdict | Rule | Reason |
|---|---|---|---|---|---|---|
| 3 | 117498MONDO:31178 monosomy 7 myelodysplasia and leukemia syndrome | 29018 MONDO_grouped:9646_30801 Monosomy 7 myelodysplasia and leukemia syndrome (24)38696 MONDO:31178 monosomy 7 myelodysplasia and leukemia syndrome (4; J 0.000) | 2 | distinct | evidence | MONDO:0031178 is the parent syndrome; the other node is PrimeKG’s grouping of its subtypes 1 and 2 (MONDO:0009646, MONDO:0030801). PrimeKG links the two with a disease_disease edge, which the merge deleted as a self-loop |
7.4 Genes (61 groups) and anatomy (2 groups)
Merge key for every row: both nodes carry one PrimeKG source id, so both resolved to one CURIE at backfill. Verdict: same (J0) for all 63. No row was lost to these merges. In each gene pair, one node carries only disease and phenotype edges and the other carries none of those, so the merge joins two halves of one gene’s record. Both anatomy pairs are one UBERON term under two labels, and there the second label carries most of the tissue-expression edges.
| # | Entity | PrimeKG id | Name the entity carries (rows) | Other name (rows) |
|---|---|---|---|---|
| 1 | 170810 UBERON:468 | UBERON:468 | 63373 multicellular organism (28) | 129373 multi-cellular organism (34,710) |
| 2 | 170926 UBERON:992 | UBERON:992 | 63461 ovary (4) | 129374 female gonad (31,684) |
| 105 | 61477 NCBIGene:64755 | NCBI:64755 | 1419 RUSF1 (168) | 34126 C16orf58 (12) |
| 106 | 61592 NCBIGene:55112 | NCBI:55112 | 7728 DYNC2I1 (102) | 34948 WDR60 (16) |
| 107 | 61769 NCBIGene:57535 | NCBI:57535 | 58296 ELAPOR1 (34) | 34122 KIAA1324 (12) |
| 108 | 62840 NCBIGene:55262 | NCBI:55262 | 8127 TRAPPC14 (62) | 35122 MAP11 (2) |
| 109 | 62987 NCBIGene:6901 | NCBI:6901 | 3236 TAFAZZIN (198) | 34675 TAZ (8) |
| 110 | 63175 NCBIGene:63932 | NCBI:63932 | 1027 STEEP1 (38) | 34863 CXorf56 (4) |
| 111 | 63856 NCBI:4540 | NCBI:4540 | 1368 MT-ND5 (340) | 35060 ND5 (10) |
| 112 | 64134 NCBI:4509 | NCBI:4509 | 5082 MT-ATP8 (146) | 35227 ATP8 (4) |
| 113 | 66178 NCBIGene:115948 | NCBI:115948 | 9854 ODAD3 (88) | 35195 CCDC151 (2) |
| 114 | 66208 NCBIGene:23247 | NCBI:23247 | 11990 KATNIP (24) | 35011 KIAA0556 (2) |
| 115 | 66391 NCBIGene:83538 | NCBI:83538 | 4703 ODAD4 (68) | 35191 TTC25 (2) |
| 116 | 66924 NCBI:4538 | NCBI:4538 | 3101 MT-ND4 (328) | 33773 ND4 (12) |
| 117 | 66984 NCBI:4536 | NCBI:4536 | 3142 MT-ND2 (154) | 35239 ND2 (10) |
| 118 | 67641 NCBIGene:139212 | NCBI:139212 | 10009 DNAAF6 (54) | 35196 PIH1D3 (4) |
| 119 | 67673 NCBIGene:255758 | NCBI:255758 | 13237 DYNLT2B (70) | 34952 TCTEX1D2 (16) |
| 120 | 67795 NCBIGene:4512 | NCBI:4512 | 3606 MT-CO1 (202) | 34395 COX1 (22) |
| 121 | 68126 NCBIGene:4514 | NCBI:4514 | 7358 MT-CO3 (136) | 35065 COX3 (12) |
| 122 | 68589 NCBIGene:55259 | NCBI:55259 | 11159 DNAI7 (26) | 33947 CASC1 (4) |
| 123 | 68934 NCBIGene:113201 | NCBI:113201 | 9792 GOLM2 (48) | 34042 CASC4 (2) |
| 124 | 69034 NCBIGene:2538 | NCBI:2538 | 4377 G6PC1 (80) | 34095 G6PC (22) |
| 125 | 69343 NCBIGene:144577 | NCBI:144577 | 13763 KICS2 (36) | 34708 C12orf66 (2) |
| 126 | 71116 NCBI:4541 | NCBI:4541 | 5789 MT-ND6 (134) | 35061 ND6 (12) |
| 127 | 71142 NCBIGene:4513 | NCBI:4513 | 5811 MT-CO2 (196) | 33906 COX2 (20) |
| 128 | 71591 NCBIGene:23639 | NCBI:23639 | 10538 DNAAF11 (78) | 35187 LRRC6 (2) |
| 129 | 72286 NCBIGene:4535 | NCBI:4535 | 6637 MT-ND1 (254) | 34591 ND1 (16) |
| 130 | 72497 NCBI:4539 | NCBI:4539 | 11604 MT-ND4L (118) | 34397 ND4L (8) |
| 131 | 72856 NCBIGene:54928 | NCBI:54928 | 7103 BPNT2 (90) | 35319 IMPAD1 (2) |
| 132 | 73246 NCBIGene:89891 | NCBI:89891 | 8366 DYNC2I2 (116) | 35089 WDR34 (12) |
| 133 | 73250 NCBIGene:4508 | NCBI:4508 | 7434 MT-ATP6 (140) | 34700 ATP6 (18) |
| 134 | 73334 NCBI:4537 | NCBI:4537 | 7504 MT-ND3 (134) | 35059 ND3 (12) |
| 135 | 73604 NCBIGene:93233 | NCBI:93233 | 8276 ODAD1 (34) | 35194 CCDC114 (2) |
| 136 | 73620 NCBIGene:30834 | NCBI:30834 | 13436 POLR1H (78) | 34671 ZNRD1 (10) |
| 137 | 74479 NCBIGene:57700 | NCBI:57700 | 11243 FHIP2A (14) | 22104 FAM160B1 (4) |
| 138 | 74852 NCBIGene:84529 | NCBI:84529 | 56820 CDIN1 (18) | 35291 C15orf41 (6) |
| 139 | 75550 NCBIGene:4519 | NCBI:4519 | 9427 MT-CYB (174) | 34396 CYTB (12) |
| 140 | 75581 NCBIGene:728215 | NCBI:728215 | 59893 NALF1 (18) | 35413 FAM155A (2) |
| 141 | 76168 NCBIGene:137392 | NCBI:137392 | 13548 CIBAR1 (38) | 34545 FAM92A (12) |
| 142 | 77341 NCBIGene:91574 | NCBI:91574 | 56871 MTRFR (22) | 35256 C12orf65 (6) |
| 143 | 77429 NCBIGene:157657 | NCBI:157657 | 57109 CFAP418 (26) | 34503 C8orf37 (6) |
| 144 | 78308 NCBI:132332 | NCBI:132332 | 83713 SMIM43 (2) | 34750 TMEM155 (4) |
| 145 | 78431 NCBIGene:55130 | NCBI:55130 | 56658 ODAD2 (22) | 35189 ARMC4 (2) |
| 146 | 78965 NCBI:65998 | NCBI:65998 | 61463 ZFTA (4) | 35205 C11orf95 (8) |
| 147 | 89652 NCBIGene:4549 | NCBI:4549 | 22053 RNR1 (10) | 78273 MT-RNR1 (86) |
| 148 | 89851 NCBIGene:4567 | NCBI:4567 | 22109 TRNL1 (20) | 78249 MT-TL1 (78) |
| 149 | 111295 NCBI:339970 | NCBI:339970 | 34784 GCOM2 (30) | 60796 POLR2MP1 (10) |
| 150 | 111835 NCBIGene:4570 | NCBI:4570 | 35150 TRNN (8) | 78258 MT-TN (78) |
| 151 | 111836 NCBIGene:4574 | NCBI:4574 | 35151 TRNS1 (16) | 78261 MT-TS1 (78) |
| 152 | 111840 NCBI:339145 | NCBI:339145 | 35156 FAM92B (10) | 57339 CIBAR2 (10) |
| 153 | 112051 NCBIGene:4575 | NCBI:4575 | 35220 TRNS2 (6) | 78268 MT-TS2 (70) |
| 154 | 112118 NCBIGene:4578 | NCBI:4578 | 35228 TRNW (6) | 78256 MT-TW (56) |
| 155 | 112119 NCBIGene:4511 | NCBI:4511 | 35230 TRNC (2) | 78259 MT-TC (78) |
| 156 | 112120 NCBIGene:4558 | NCBI:4558 | 35231 TRNF (4) | 78250 MT-TF (80) |
| 157 | 112121 NCBI:4564 | NCBI:4564 | 35232 TRNH (4) | 78266 MT-TH (78) |
| 158 | 112122 NCBIGene:4566 | NCBI:4566 | 35233 TRNK (8) | 78263 MT-TK (78) |
| 159 | 112123 NCBIGene:4572 | NCBI:4572 | 35234 TRNQ (4) | 78254 MT-TQ (76) |
| 160 | 112124 NCBIGene:4577 | NCBI:4577 | 35235 TRNV (4) | 78251 MT-TV (78) |
| 161 | 112125 NCBIGene:4565 | NCBI:4565 | 35236 TRNI (2) | 78253 MT-TI (70) |
| 162 | 112126 NCBIGene:4571 | NCBI:4571 | 35237 TRNP (2) | 78272 MT-TP (78) |
| 163 | 112127 NCBI:4568 | NCBI:4568 | 35238 TRNL2 (6) | 78269 MT-TL2 (56) |
| 164 | 112206 NCBI:4556 | NCBI:4556 | 35255 TRNE (6) | 78270 MT-TE (78) |
| 165 | 112209 NCBI:4576 | NCBI:4576 | 35257 TRNT (4) | 78271 MT-TT (78) |
8. Method
8.1 Data
- Prod, read-only. The snapshot ran in the backend container, against the production database. Every session opened with
SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY, and the script assertstransaction_read_only = on.- Tables read:
mods_kg_entity,mods_kg_entity_source,mods_kg_relation,mods_kg_curie_cache,mods_kg_entity_premerge,mods_kg_entity_source_premerge. - Every relation and source query filters to
source_id = 'prime_kg'. Every query is an aggregate, or reads the 183 merged entities and their 401 nodes. - Nothing patient-side was read: no
maple_patientrow, nomods_patient, no document or EHR table. - The snapshot was taken on 2026-10-05 at 15:06 PDT and came to 243 KB.
- Tables read:
- PrimeKG
kg.csv.- Path:
C:\Maple\worker\data\kg\prime_kg\kg.csv, the file #696’s re-ingest used. 981,751,236 bytes, file date 2026-04-07, 8,100,498 rows. - MD5
aac8191d4fbc5bf09cdf8c3c78b4e75f, which matches Harvard Dataverse datafile 6180620 (kg.csv, dataset doi:10.7910/DVN/IXA7BM, version 2.1, released 2022-05-02). - SHA-256
0a5a010fcd88dcec572a573d98c26c1c244a11567092b6161d3ded611cfb5c69. - The bundle copy at
C:\Maple\docs\References\PrimeKG\Data\kg.csv(downloaded 2026-04-09) is byte-identical.
- Path:
- Evidence for the verdicts, cited in the
reasonandbasiscolumns:- DrugBank descriptions, groups and molecular weights from PrimeKG’s
drug_features.csv; - MONDO names and BERT groups from
disease_features.csv; - pathway parents and proteins from
kg.csv; - Reactome Content Service records (
GET https://reactome.org/ContentService/data/query/{stId}, read 2026-10-05). - The two feature files are in the same bundle directory:
drug_features.csvSHA-25621ca9cc6…5de73,disease_features.csvSHA-256163bbcf9…50d50.
- DrugBank descriptions, groups and molecular weights from PrimeKG’s
8.2 Reproduce
Save §8.4 and §8.5 as files, then, from Git Bash on the host:
# 1. Prod snapshot, read-only (about 30 s, about 243 KB of egress)
docker exec -i BACKEND python - < primekg_merge_snapshot.py > primekg_merge_snapshot.json
# 2. Replay and table (local only, about 40 s). Feeding the committed CSV back in
# carries its proposal columns over and recomputes every measured column.
python primekg_merge_review.py --snapshot primekg_merge_snapshot.json \
--kg-csv C:/Maple/worker/data/kg/prime_kg/kg.csv \
--proposal docs/kg_primekg_merge_review.csv \
--out-csv kg_primekg_merge_review.csv --out-json review_totals.jsonOn 2026-10-05, step 2 run against the 15:06 PDT snapshot reproduced the committed CSV byte for byte. The script writes LF line endings, and a Windows checkout with core.autocrlf on holds CRLF, so compare with diff --strip-trailing-cr. Its console line reports two checks, and both held:
- the stored relations match the replay on all 33 relation keys;
- they match on all 183 entities, relation by relation.
A later snapshot gives the same table until the store changes.
The proposal columns are written by hand: verdict, partition, rules, reason and basis. The script only carries them over, by group key. The key is the group’s PrimeKG x_index values joined by +, which do not change between environments. A partition lists the proposed nodes after a split: blocks separated by |, members of a block joined by +. To test another proposal, edit those columns and re-run step 2. The JSON’s scenarios then gives the rows each split restores, by relation.
8.3 Column dictionary (CSV)
| Column | Meaning |
|---|---|
group, group_key | Group number on this page; the members’ x_index values joined by +. |
entity_id, entity_curie, entity_name, domain | The canonical entity today. |
merge_step, merge_key | Which mechanism joined the group (§2), and the data that shows it. |
x_index, primekg_id, primekg_type, primekg_name | The PrimeKG node, as in kg.csv. |
premerge_curie, nn_preferred | The node’s entity CURIE in the pre-merge snapshot, and Node Normalizer’s cached answer for it. |
anchor | yes for the member whose name the entity carries. |
rows, rows_by_relation | kg.csv rows touching the node. |
shared_with_anchor, jaccard_with_anchor | Overlap of the node’s (relation, neighbour) set with the anchor’s. |
group_rows_lost, group_lost_by_relation | Rows touching the group that the merges collapsed. A row between two merged groups counts in both. |
group_rows_restored_if_only_this_split | Rows that splitting this group alone restores. |
group_self_loop_rows | Rows between two members, deleted as self-loops. |
verdict, partition, rules, reason, basis | The proposal. basis is per node. |
8.4 primekg_merge_snapshot.py
"""#438 item 2: read-only snapshot of the PrimeKG entity merges on the canonical KG.
Reads only KG tables, only PrimeKG rows (source_id = 'prime_kg'), and only
aggregates or the merged groups themselves. Every transaction is read-only.
Writes one JSON document to stdout.
docker exec -i BACKEND python - < primekg_merge_snapshot.py > primekg_merge_snapshot.json
"""
import datetime
import json
import os
import sys
from sqlalchemy import create_engine, text
# PrimeKG's x_source label -> standard CURIE prefix, and the zero-padded
# prefixes: the rules of connectors/prime_kg_connector.primekg_curie (#53, #704),
# inlined so the snapshot does not depend on the running container's code.
_PREFIX = {"NCBI": "NCBIGene", "DrugBank": "DRUGBANK", "HPO": "HP"}
_PAD = {"HP": 7, "MONDO": 7, "UBERON": 7, "GO": 7}
def hint_spellings(src, oid):
"""Every spelling of a node's CURIE the store has used: PrimeKG's own label
(before #53), the standard prefix (#53), padded and unpadded (#704)."""
src, oid = (src or "").strip(), (oid or "").strip()
if not src or not oid:
return set()
out = {f"{src}:{oid}"}
p = _PREFIX.get(src, src)
out.add(f"{p}:{oid}")
if _PAD.get(p) and oid.isdigit():
out.add(f"{p}:{oid.zfill(_PAD[p])}")
out.add(f"{p}:{int(oid)}")
return out
GROUPS = """
SELECT entity_id FROM mods_kg_entity_source WHERE source_id = 'prime_kg'
GROUP BY entity_id HAVING COUNT(DISTINCT external_id) > 1
"""
engine = create_engine(os.environ["DATABASE_URL"])
out = {"taken_at": datetime.datetime.now(datetime.timezone.utc).isoformat()}
with engine.connect() as conn:
conn.execute(text("SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY"))
conn.commit()
conn.execute(text("SET statement_timeout = '600s'"))
assert conn.execute(text("SHOW transaction_read_only")).scalar() == "on"
r = conn.execute(text("""
SELECT COUNT(*), COUNT(DISTINCT external_id), COUNT(DISTINCT entity_id)
FROM mods_kg_entity_source WHERE source_id = 'prime_kg'
""")).one()
out["prime_kg_source_rows"] = {"rows": r[0], "external_ids": r[1], "entities": r[2]}
out["external_ids_on_two_entities"] = conn.execute(text("""
SELECT COUNT(*) FROM (SELECT external_id FROM mods_kg_entity_source
WHERE source_id = 'prime_kg' GROUP BY external_id
HAVING COUNT(DISTINCT entity_id) > 1) t
""")).scalar()
ents = conn.execute(text(f"""
WITH g AS ({GROUPS})
SELECT e.id, e.curie, e.name, e.domain, e.is_synthetic_curie
FROM mods_kg_entity e JOIN g ON g.entity_id = e.id ORDER BY e.id
""")).fetchall()
ids = [e.id for e in ents]
out["entities"] = [{"entity_id": e.id, "curie": e.curie, "name": e.name,
"domain": e.domain, "is_synthetic_curie": e.is_synthetic_curie}
for e in ents]
mem = conn.execute(text("""
SELECT entity_id, external_id, source_name, source_entity_type, source_metadata
FROM mods_kg_entity_source
WHERE source_id = 'prime_kg' AND entity_id = ANY(:ids)
ORDER BY entity_id, external_id::bigint
"""), {"ids": ids}).fetchall()
out["members"] = [{"entity_id": m.entity_id, "external_id": m.external_id,
"source_name": m.source_name, "source_entity_type": m.source_entity_type,
"original_source": (m.source_metadata or {}).get("original_source"),
"original_id": (m.source_metadata or {}).get("original_id")}
for m in mem]
# Where each member sat in the pre-merge snapshot that
# scripts/apply_canonical_entity_merges.py requires before it runs.
pre = conn.execute(text("""
SELECT es.external_id, es.entity_id, e.curie
FROM mods_kg_entity_source_premerge es
LEFT JOIN mods_kg_entity_premerge e ON e.id = es.entity_id
WHERE es.source_id = 'prime_kg' AND es.external_id = ANY(:x)
"""), {"x": [m.external_id for m in mem]}).fetchall()
out["premerge"] = [{"external_id": p.external_id, "entity_id": p.entity_id,
"curie": p.curie} for p in pre]
# What Node Normalizer answered for each spelling (mods_kg_curie_cache).
queries = set(e.curie for e in ents) | set(p.curie for p in pre if p.curie)
for m in out["members"]:
queries |= hint_spellings(m["original_source"], m["original_id"])
cache = conn.execute(text("""
SELECT query_curie, preferred_curie, preferred_name
FROM mods_kg_curie_cache WHERE query_curie = ANY(:q)
"""), {"q": sorted(queries)}).fetchall()
out["cache"] = [{"query_curie": c.query_curie, "preferred_curie": c.preferred_curie,
"preferred_name": c.preferred_name} for c in cache]
# Stored PrimeKG relations, for the reconciliation against kg.csv.
out["stored_by_source_predicate"] = {sp: n for sp, n in conn.execute(text("""
SELECT source_predicate, COUNT(*) FROM mods_kg_relation
WHERE source_id = 'prime_kg' GROUP BY 1
""")).fetchall()}
out["stored_self_loops"] = conn.execute(text("""
SELECT COUNT(*) FROM mods_kg_relation
WHERE source_id = 'prime_kg' AND subject_id = object_id
""")).scalar()
# Stored PrimeKG relations touching each merged entity, for a per-group check.
out["stored_touching_entity"] = [[e, sp, n] for e, sp, n in conn.execute(text(f"""
WITH g AS ({GROUPS})
SELECT g.entity_id, r.source_predicate, COUNT(*)
FROM g JOIN mods_kg_relation r
ON r.subject_id = g.entity_id OR r.object_id = g.entity_id
WHERE r.source_id = 'prime_kg'
GROUP BY 1, 2 ORDER BY 1, 2
""")).fetchall()]
conn.rollback()
json.dump(out, sys.stdout, indent=1, default=str)8.5 primekg_merge_review.py
"""#438 item 2: the review table for PrimeKG nodes merged onto one canonical entity.
Replays PrimeKG's kg.csv under the merges recorded on prod, and measures what
each merge costs. Local only: reads two or three files, writes two, touches no
database.
Inputs
--snapshot JSON from primekg_merge_snapshot.py (read-only on prod)
--kg-csv PrimeKG kg.csv (Harvard Dataverse file 6180620)
--proposal optional: the review CSV (or a JSON of the same fields). Its
hand-written columns (verdict, partition, rules, reason, basis)
are carried over by group key; every other column is recomputed.
Outputs
--out-csv one row per merged PrimeKG node
--out-json the totals the review doc quotes
python primekg_merge_review.py --snapshot primekg_merge_snapshot.json \
--kg-csv C:/Maple/worker/data/kg/prime_kg/kg.csv \
--proposal docs/kg_primekg_merge_review.csv \
--out-csv docs/kg_primekg_merge_review.csv --out-json review_totals.json
Units: rows are directed kg.csv rows, the unit mods_kg_relation stores
(PrimeKG writes every edge in both directions). A drug_protein row's relation
key is its role (display_relation), as stored since #696.
"""
import argparse
import csv
import hashlib
import json
from collections import Counter, defaultdict
import numpy as np
SHIFT = 1 << 21 # node ids < 2**21 (PrimeKG x_index < 129,375; entity block ids offset)
ENT_OFFSET = 1_000_000 # merged entity -> 1e6 + entity_id (< 2**21)
BLOCK_OFFSET = 1_500_000 # a kept-together block of a split group -> 1.5e6 + its smallest x_index
PROPOSAL_COLS = ["verdict", "partition", "rules", "reason"]
def sha256(path):
h = hashlib.sha256()
with open(path, "rb") as f:
for chunk in iter(lambda: f.read(1 << 20), b""):
h.update(chunk)
return h.hexdigest()
def read_kg(path, members):
"""kg.csv as int arrays (x, y, relation key code), plus node info for members."""
codes, xs, ys, rs, info = {}, [], [], [], {}
with open(path, encoding="utf-8", newline="") as f:
rd = csv.reader(f)
ix = {h: i for i, h in enumerate(next(rd))}
for row in rd:
rel = row[ix["relation"]]
key = row[ix["display_relation"]] if rel == "drug_protein" else rel
c = codes.setdefault(key, len(codes))
x, y = int(row[ix["x_index"]]), int(row[ix["y_index"]])
xs.append(x); ys.append(y); rs.append(c)
if x in members and x not in info:
info[x] = {k: row[ix["x_" + k]] for k in ("id", "type", "name", "source")}
names = [None] * len(codes)
for k, v in codes.items():
names[v] = k
return np.array(xs, np.int64), np.array(ys, np.int64), np.array(rs, np.int64), names, info
def keys(gx, gy, r):
return (gx * SHIFT + gy) * 64 + r
def kept_by_rel(gx, gy, r, nrel):
"""Distinct stored keys per relation code, self-loops dropped."""
m = gx != gy
u = np.unique(keys(gx[m], gy[m], r[m]))
return np.bincount(u % 64, minlength=nrel)
def fmt(counter):
return "; ".join(f"{k} {v:,}" for k, v in sorted(counter.items(), key=lambda kv: (-kv[1], kv[0])) if v)
def load_proposal(path):
if not path:
return {}, {}
if path.endswith(".json"):
p = json.load(open(path, encoding="utf-8"))
return ({g: {c: v.get(c, "") for c in PROPOSAL_COLS} for g, v in p.items()},
{int(x): b for v in p.values() for x, b in v.get("basis", {}).items()})
groups, basis = {}, {}
with open(path, encoding="utf-8", newline="") as f:
for row in csv.DictReader(f):
groups[row["group_key"]] = {c: row.get(c, "") for c in PROPOSAL_COLS}
basis[int(row["x_index"])] = row.get("basis", "")
return groups, basis
def parse_partition(text_, xs):
"""'a+b | c' -> [[a, b], [c]]; empty -> one block (the merge as it stands)."""
if not text_.strip():
return [sorted(xs)]
return [[int(v) for v in blk.split("+")] for blk in text_.split("|")]
def main():
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--snapshot", required=True)
ap.add_argument("--kg-csv", required=True)
ap.add_argument("--proposal")
ap.add_argument("--out-csv", required=True)
ap.add_argument("--out-json", required=True)
a = ap.parse_args()
snap = json.load(open(a.snapshot, encoding="utf-8"))
ents = {e["entity_id"]: e for e in snap["entities"]}
mem = {int(m["external_id"]): m for m in snap["members"]}
pre = {int(p["external_id"]): p for p in snap["premerge"]}
cache = {c["query_curie"]: c for c in snap["cache"]}
groups = defaultdict(list)
for x, m in mem.items():
groups[m["entity_id"]].append(x)
for g in groups.values():
g.sort()
proposal, basis = load_proposal(a.proposal)
x, y, r, rel_names, info = read_kg(a.kg_csv, mem)
nrel = len(rel_names)
nmax = int(max(x.max(), y.max())) + 1
ident = np.arange(nmax, dtype=np.int64)
cur = ident.copy()
for xi, m in mem.items():
cur[xi] = ENT_OFFSET + m["entity_id"]
# ── Reconciliation: kg.csv replayed under the merges vs what prod stores ──
raw = np.bincount(r, minlength=nrel)
assert len(np.unique(keys(x, y, r))) == len(r), "kg.csv has duplicate (x, y, relation) rows"
assert not (x == y).any(), "kg.csv has native self-loops"
replay = kept_by_rel(cur[x], cur[y], r, nrel)
loops = np.bincount(r[(cur[x] == cur[y])], minlength=nrel)
stored = snap["stored_by_source_predicate"]
recon = [{"relation": rel_names[c], "kg_csv_rows": int(raw[c]), "replayed": int(replay[c]),
"stored_on_prod": int(stored.get(rel_names[c], 0)),
"lost_to_merges": int(raw[c] - replay[c]), "of_which_self_loops": int(loops[c])}
for c in np.argsort(-raw)]
mismatch = [row for row in recon if row["replayed"] != row["stored_on_prod"]]
# ── Per-node neighbours and row counts (rows touching a member) ──────────
touch = np.isin(x, list(mem)) | np.isin(y, list(mem))
ti = np.nonzero(touch)[0]
tx, ty, tr = x[ti], y[ti], r[ti]
rows_by = defaultdict(Counter)
nbrs = defaultdict(set)
for a_, b_, c_ in zip(tx.tolist(), ty.tolist(), tr.tolist()):
if a_ in mem:
rows_by[a_][rel_names[c_]] += 1
nbrs[a_].add((c_, b_))
if b_ in mem:
rows_by[b_][rel_names[c_]] += 1
def restored(mapping, sel):
"""Rows restored, per relation, when `mapping` replaces the current one on rows `sel`."""
before = kept_by_rel(cur[tx[sel]], cur[ty[sel]], tr[sel], nrel)
after = kept_by_rel(mapping[tx[sel]], mapping[ty[sel]], tr[sel], nrel)
return after - before
def split_map(mapping, xs, partition):
for blk in partition:
for v in blk:
mapping[v] = v if len(blk) == 1 else BLOCK_OFFSET + min(blk)
return mapping
stored_touch = defaultdict(dict)
for eid, sp, n in snap["stored_touching_entity"]:
stored_touch[eid][sp] = n
touch_mismatch = []
out_rows, gsum = [], []
order = sorted(groups, key=lambda e: (ents[e]["domain"], ents[e]["entity_id"]))
for gi, eid in enumerate(order, 1):
e, xs = ents[eid], groups[eid]
gkey = "+".join(str(v) for v in xs)
pre_ents = {pre[v]["entity_id"] for v in xs}
prefs = [cache.get(pre[v]["curie"] or "", {}) for v in xs]
pref_set = {p.get("preferred_curie") for p in prefs}
if len(pre_ents) == 1:
step = "backfill: one CURIE hint for all members"
src_ids = sorted({f"{info[v]['source']}:{info[v]['id']}" for v in xs})
key_txt = f"every member carries the same PrimeKG id {', '.join(src_ids)}"
elif None not in pref_set and len(pref_set) == 1:
step = "apply_canonical_entity_merges pass 1: Node Normalizer"
p0 = prefs[0]
key_txt = (f"Node Normalizer gives {', '.join(pre[v]['curie'] for v in xs)} "
f"one preferred CURIE, {p0['preferred_curie']} ('{p0.get('preferred_name')}')")
else:
step = "apply_canonical_entity_merges pass 2: name match"
key_txt = (f"same name, same domain; Node Normalizer knows none of "
f"{', '.join(pre[v]['curie'] for v in xs)}")
anchor = next((v for v in xs if info[v]["name"].lower() == e["name"].lower()), xs[0])
sel = np.isin(tx, xs) | np.isin(ty, xs)
# Rows touching the group that the merges collapsed (a row between two
# merged groups counts in both), and rows that splitting this group
# alone, with every other merge left in place, would restore.
kept = kept_by_rel(cur[tx[sel]], cur[ty[sel]], tr[sel], nrel)
g_lost = np.bincount(tr[sel], minlength=nrel) - kept
g_restore = restored(split_map(cur.copy(), xs, [[v] for v in xs]), sel)
g_loops = int(((cur[tx[sel]] == cur[ty[sel]])).sum())
prod_touch = stored_touch.get(eid, {})
if {rel_names[c]: int(n) for c, n in enumerate(kept) if n} != prod_touch:
touch_mismatch.append(eid)
prop = proposal.get(gkey, {c: "" for c in PROPOSAL_COLS})
gsum.append({"group": gi, "group_key": gkey, "entity_id": eid, "domain": e["domain"],
"step": step, "lost_touching": {rel_names[c]: int(n) for c, n in enumerate(g_lost) if n},
"restored_if_only_this_split": {rel_names[c]: int(n) for c, n in enumerate(g_restore) if n},
"self_loop_rows": g_loops, **prop})
for v in xs:
inter = len(nbrs[v] & nbrs[anchor]); union = len(nbrs[v] | nbrs[anchor])
out_rows.append({
"group": gi, "group_key": gkey, "entity_id": eid, "entity_curie": e["curie"],
"entity_name": e["name"], "domain": e["domain"], "merge_step": step,
"merge_key": key_txt, "x_index": v,
"primekg_id": f"{info[v]['source']}:{info[v]['id']}", "primekg_type": info[v]["type"],
"primekg_name": info[v]["name"], "premerge_curie": pre[v]["curie"],
"nn_preferred": cache.get(pre[v]["curie"] or "", {}).get("preferred_curie") or "",
"anchor": "yes" if v == anchor else "",
"rows": sum(rows_by[v].values()), "rows_by_relation": fmt(rows_by[v]),
"shared_with_anchor": "" if v == anchor else inter,
"jaccard_with_anchor": "" if v == anchor else (f"{inter / union:.3f}" if union else ""),
"group_rows_lost": int(g_lost.sum()),
"group_lost_by_relation": fmt({rel_names[c]: int(n) for c, n in enumerate(g_lost)}),
"group_rows_restored_if_only_this_split": int(g_restore.sum()),
"group_self_loop_rows": g_loops,
**prop, "basis": basis.get(v, ""),
})
# ── Scenarios: what a split restores, by relation ─────────────────────────
allm = np.ones(len(tx), bool)
all_split = restored(ident, allm)
scen = {"split_all_183": {rel_names[c]: int(n) for c, n in enumerate(all_split) if n}}
def cites(p, rule):
return rule in [t.strip() for t in p.get("rules", "").split(",")]
# A rule that some `same` group cites keeps nodes together (a merge rule);
# any other rule separates them.
merge_rules = {t.strip() for p in proposal.values() if p.get("verdict") == "same"
for t in p.get("rules", "").split(",") if t.strip()}
def proposal_restores(split_verdicts, flip_rule=None):
"""Rows restored, per relation, by splitting the groups whose verdict is in
`split_verdicts` (a distinct group along its partition). With `flip_rule`,
the groups citing that rule are treated as if it were rejected: for a merge
rule they split into single nodes, for any other rule they stay merged.
A group citing several rules flips whole, so for those three groups the
figure is an upper bound. Unclear groups are never flipped."""
mp = cur.copy()
for eid in order:
xs = groups[eid]
p = proposal.get("+".join(map(str, xs)), {})
v = p.get("verdict")
if flip_rule and cites(p, flip_rule) and v != "unclear":
v = "split-whole" if flip_rule in merge_rules else "same"
if v == "split-whole":
split_map(mp, xs, [[x_] for x_ in xs])
elif v == "distinct" and v in split_verdicts:
split_map(mp, xs, parse_partition(p.get("partition", ""), xs))
elif v in split_verdicts:
split_map(mp, xs, [[x_] for x_ in xs])
return restored(mp, allm)
per_rule = {}
if proposal:
for name, verdicts in (("split_distinct", {"distinct"}), ("split_distinct_and_unclear", {"distinct", "unclear"})):
res = proposal_restores(verdicts)
scen[name] = {rel_names[c]: int(n) for c, n in enumerate(res) if n}
base = sum(scen["split_distinct"].values())
rules_seen = sorted({t.strip() for p in proposal.values() for t in p.get("rules", "").split(",") if t.strip()})
for rule in rules_seen:
flipped = int(proposal_restores({"distinct"}, flip_rule=rule).sum())
per_rule[rule] = {"groups": sum(1 for p in proposal.values() if cites(p, rule)),
"kind": "merge" if rule in merge_rules else "split",
"rows_restored_if_rejected": flipped, "change_vs_proposal": flipped - base}
cols = ["group", "group_key", "entity_id", "entity_curie", "entity_name", "domain", "merge_step",
"merge_key", "x_index", "primekg_id", "primekg_type", "primekg_name", "premerge_curie",
"nn_preferred", "anchor", "rows", "rows_by_relation", "shared_with_anchor",
"jaccard_with_anchor", "group_rows_lost", "group_lost_by_relation",
"group_rows_restored_if_only_this_split", "group_self_loop_rows", *PROPOSAL_COLS, "basis"]
with open(a.out_csv, "w", encoding="utf-8", newline="") as f:
w = csv.DictWriter(f, fieldnames=cols, lineterminator="\n")
w.writeheader()
w.writerows(out_rows)
totals = {
"kg_csv": {"path": a.kg_csv, "sha256": sha256(a.kg_csv), "rows": int(len(r))},
"snapshot_taken_at": snap["taken_at"],
"prime_kg_source_rows": snap["prime_kg_source_rows"],
"groups": len(groups), "member_nodes": len(mem), "surplus_nodes": len(mem) - len(groups),
"by_domain": {d: {"groups": sum(1 for e in groups if ents[e]["domain"] == d),
"nodes": sum(len(g) for e, g in groups.items() if ents[e]["domain"] == d)}
for d in sorted({e["domain"] for e in ents.values()})},
"by_step": dict(Counter(g["step"] for g in gsum)),
"reconciliation": recon, "reconciliation_mismatches": mismatch,
"replayed_total": int(replay.sum()), "stored_total": sum(stored.values()),
"stored_self_loops": snap["stored_self_loops"],
"groups_whose_stored_rows_differ_from_replay": touch_mismatch,
"scenarios": scen, "per_rule": per_rule, "groups_detail": gsum,
}
json.dump(totals, open(a.out_json, "w", encoding="utf-8"), indent=1)
print(f"{len(groups)} groups, {len(mem)} nodes; replayed {int(replay.sum()):,} rows vs "
f"{sum(stored.values()):,} stored; mismatched relations: {len(mismatch)}; "
f"groups whose stored rows differ from the replay: {len(touch_mismatch)}")
for k, v in scen.items():
print(f"{k}: {sum(v.values()):,} rows {fmt(Counter(v))}")
if __name__ == "__main__":
main()