library(readr)
library(dplyr)
library(tibble)
library(digest)
sentences <- read_csv(
"data/workforce/workforce_sentences.csv",
na = character(),
col_types = cols(
sentence_id = col_character(),
document_id = col_character(),
source_line = col_character(),
text = col_character(),
reference_label = col_character(),
uncertainty = col_character(),
annotator_id = col_character(),
rationale = col_character(),
codebook_version = col_character(),
codebook_hash = col_character(),
derived = col_character(),
transformation = col_character()
)
)
entities <- read_csv(
"data/riverton/riverton-entities.csv",
na = character(),
col_types = cols(
entity_id = col_character(),
canonical_name = col_character(),
entity_type = col_character(),
description = col_character()
)
)
aliases <- read_csv(
"data/riverton/riverton-aliases.csv",
na = character(),
col_types = cols(
entity_id = col_character(),
alias = col_character()
)
)
metadata <- read_csv(
"data/riverton/riverton-reference-metadata.csv",
na = character(),
col_types = cols(
artifact = col_character(),
description = col_character(),
source = col_character(),
license = col_character(),
created_on = col_character(),
fingerprint = col_character()
)
)
alias_hash <- digest(
paste(read_lines("data/riverton/riverton-aliases.csv"), collapse = "\n"),
algo = "sha256",
serialize = FALSE
)
expected_alias_hash <- metadata |>
filter(artifact == "riverton-aliases.csv") |>
pull(fingerprint)Linking named entities
Turn mentions into stable identifiers
Credential counts get messy when one notice says forklift certification and a reference list says Forklift Operator Licence. The coordinator still wants to know whether the notices point to the same credential.
Named entity linking connects a mention in text to a stable identifier in a reference list. The identifier matters because spellings can change while the record stays the same.
The Riverton organisations, places, and credentials here are invented for the lesson.
By the end of this lesson, you will be able to:
- explain why an entity identifier matters;
- join mention strings to an alias table;
- count mentions with zero, one, or several candidates;
- show how one credential can have multiple aliases; and
- state the limit of alias matching.
Load entities and aliases
Three CSV files provide the reference list, aliases, and sentence examples. The chunk checks the alias-file fingerprint, then uses dplyr and tibble for the joins and tables. An entity is a thing the reference list knows about, such as an organisation, place, or credential. The canonical name is the reference list’s main name for that entity. An alias is a surface form that may appear in text.
| Entity ID | Canonical name | Type | Description |
|---|---|---|---|
| ORG-0001 | Riverton Workforce Lab | organisation | Fictional research group that labels workforce text |
| ORG-0002 | Riverton Skills Centre | organisation | Fictional training provider in Riverton |
| ORG-0003 | Marrow County Transit | organisation | Fictional public transport operator |
| LOC-0001 | Riverton | place | Fictional city in Marrow County, Calder |
| LOC-0002 | Riverton | place | Fictional town in Tidewater, unrelated to LOC-0001 |
| LOC-0003 | Bellhaven | place | Fictional city in Marrow County |
| CRD-0001 | Forklift Operator Licence | credential | Fictional certificate named in the job board |
| CRD-0002 | Data Support Certificate | credential | Fictional certificate named in the training flyer |
The alias table contains two rows for Riverton. That deliberate duplicate makes Riverton an ambiguity case for this lesson. It also contains the exact strings used below, including forklift certification and Forklift Operator Licence, so the singleton candidates are a property of this small teaching table.
Riverton Skills Centre is an organisation in this entity list. In the geocoding lesson, the same name is a building. The difference is a normal entity-linking problem: one name can point to an institution, a site, or both.
Prepare mention candidates
Two mention candidates come from the Riverton sentences. Three are constructed checks that exercise the credential and miss cases.
real_mentions <- bind_rows(
sentences |>
filter(sentence_id == "s023") |>
transmute(mention_id = sentence_id, source = "Riverton text", text, mention = "Riverton"),
sentences |>
filter(sentence_id == "s024") |>
transmute(mention_id = sentence_id, source = "Riverton text", text, mention = "DATA SUPPORT CERTIFICATE")
)
mention_candidates <- bind_rows(
real_mentions,
tibble(
mention_id = c("link-03", "link-04", "link-05"),
source = "constructed check",
text = c(
"Applicants ask about forklift certification.",
"The posting names a Forklift Operator Licence.",
"Customer service badge"
),
mention = c(
"forklift certification",
"Forklift Operator Licence",
"Customer service badge"
)
)
)| Mention ID | Source | Text | Mention |
|---|---|---|---|
| s023 | Riverton text | RIVERTON SKILLS OPEN HOUSE | Riverton |
| s024 | Riverton text | DATA SUPPORT CERTIFICATE | DATA SUPPORT CERTIFICATE |
| link-03 | constructed check | Applicants ask about forklift certification. | forklift certification |
| link-04 | constructed check | The posting names a Forklift Operator Licence. | Forklift Operator Licence |
| link-05 | constructed check | Customer service badge | Customer service badge |
A mention candidate is a span of text the team wants to link. This lesson starts with hand-selected mentions so the linking step remains visible.
Generate candidates from normalized aliases
In real datasets, exact casing rarely matches perfectly. Before joining, both the mentions and aliases are lowercased so differently capitalized names can match. This join generates candidates; it does not prove a link. A mention can have zero, one, or several candidates, and a separate resolution policy must decide whether to accept one or return NIL.
library(stringr)
normalized_mentions <- mention_candidates |>
mutate(normalized_mention = str_to_lower(mention))
normalized_aliases <- aliases |>
mutate(normalized_alias = str_to_lower(alias)) |>
distinct(entity_id, normalized_alias)
linked_rows <- normalized_mentions |>
left_join(
normalized_aliases,
by = join_by(normalized_mention == normalized_alias),
relationship = "many-to-many"
) |>
left_join(entities, by = join_by(entity_id))
link_status <- linked_rows |>
summarise(
candidate_ids = paste(unique(na.omit(entity_id)), collapse = ", "),
candidates = n_distinct(na.omit(entity_id)),
.by = c(mention_id, source, text, mention)
) |>
mutate(
candidate_ids = if_else(candidate_ids == "", "none", candidate_ids),
outcome = case_when(
candidates == 0L ~ "no candidate",
candidates == 1L ~ "one candidate",
TRUE ~ "multiple candidates"
)
)
resolved_links <- link_status |>
mutate(
resolved_entity_id = if_else(
candidates == 1L,
candidate_ids,
NA_character_
),
resolution = case_when(
candidates == 0L ~ "NIL",
candidates == 1L ~ "accepted singleton candidate",
TRUE ~ "needs disambiguation"
)
)
outcome_counts <- tibble(
outcome = c("one candidate", "multiple candidates", "no candidate")
) |>
left_join(
link_status |>
count(outcome, name = "mentions"),
by = join_by(outcome)
) |>
mutate(mentions = coalesce(mentions, 0L))| Mention ID | Source | Text | Mention | Candidate IDs | Candidates | Candidate outcome |
|---|---|---|---|---|---|---|
| s023 | Riverton text | RIVERTON SKILLS OPEN HOUSE | Riverton | LOC-0001, LOC-0002 | 2 | multiple candidates |
| s024 | Riverton text | DATA SUPPORT CERTIFICATE | DATA SUPPORT CERTIFICATE | CRD-0002 | 1 | one candidate |
| link-03 | constructed check | Applicants ask about forklift certification. | forklift certification | CRD-0001 | 1 | one candidate |
| link-04 | constructed check | The posting names a Forklift Operator Licence. | Forklift Operator Licence | CRD-0001 | 1 | one candidate |
| link-05 | constructed check | Customer service badge | Customer service badge | none | 0 | no candidate |
| Outcome | Mentions |
|---|---|
| one candidate | 3 |
| multiple candidates | 1 |
| no candidate | 1 |
The relationship argument tells dplyr that one mention may match several alias rows. DATA SUPPORT CERTIFICATE produces one candidate. This toy policy accepts singleton candidates, but a production linker would still apply a score or NIL threshold. Riverton maps to two place records, so the alias alone cannot choose one. Customer service badge has no candidate. The 3/1/1 counts describe only candidate generation for these five mentions.
Count by identifier
Two different aliases can point to the same credential identifier. That is the reason the identifier matters.
credential_mentions <- resolved_links |>
filter(resolution == "accepted singleton candidate") |>
rename(entity_id = resolved_entity_id) |>
left_join(entities, by = join_by(entity_id)) |>
filter(entity_type == "credential") |>
count(entity_id, canonical_name, name = "mentions") |>
arrange(entity_id)| Entity ID | Canonical name | Mentions |
|---|---|---|
| CRD-0001 | Forklift Operator Licence | 2 |
| CRD-0002 | Data Support Certificate | 1 |
The two forklift aliases count under CRD-0001. Without that identifier, the team would have to decide later whether the two spellings named the same credential.
Real-world linking pipelines
The normalized join here shows candidate generation from an alias table. Production entity linking systems add a separate resolution step:
- Unicode and case normalization policy: A pipeline must define how it handles accents (e.g., NFC/NFD normalization) and casing before joining, ensuring mentions and aliases are normalized consistently. Never report linking rates over an unnormalized exact string join.
- Candidate generation and ranking: When an alias is ambiguous (like
Riverton), a modern pipeline generates candidate entities and ranks them using the surrounding sentence context. - NIL handling: A robust system detects when a mention matches an alias string but refers to a different entity not in the knowledge base (a NIL entity), rather than blindly linking it.
- KB versioning: An entity’s canonical name or status may change. Linking results must record the specific version of the knowledge base (KB) used.
- Alias and candidate-recall checks: Pipelines need ongoing evaluation to ensure the alias table contains common misspellings (alias recall) and the ranking system surfaces the correct entity (candidate recall).
What to remember
- Entity linking connects a mention to a stable identifier.
- An alias table can turn different spellings into one record.
- Duplicate aliases create ambiguity that should be counted.
- Alias matching only finds names someone has already written down.
The two forklift spellings count as one credential in these examples. The table does not resolve which Riverton is meant, and a badge name absent from the alias file stays unlinked.