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
Link mentions to stable identifiers
A notice says forklift certification, while a reference list says Forklift Operator Licence. The coordinator needs to know whether those names refer to the same credential before counting credentials.
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.
- state the limit of alias matching.
Load entities and aliases
Three CSV files provide the reference list, aliases, and sentence examples. The chunk compares the alias-file fingerprint, a value used to detect text changes, with its expected value. dplyr and tibble create joins and tables.
An entity is an item in the reference list, such as an organisation, place, or credential. The canonical name is the reference listβs main name for that entity. An alias is an alternative name 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 deliberately contains two rows for Riverton to create an ambiguous case. It also contains the exact strings forklift certification and Forklift Operator Licence. A singleton candidate means that only one record matches. The singleton results below reflect this small teaching table, not general linking accuracy.
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 constructed checks cover credential matches and missing matches.
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
Names in real datasets can use different capitalization. The code converts mentions and aliases to lowercase before the join. The join generates candidates but does not prove a link. Each mention can have zero, one, or several candidates.
A separate resolution policy decides whether to accept a candidate or return NIL, meaning no accepted reference entity.
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 teaching 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: State the accent and capitalization rules before the join. NFC/NFD normalization specifies consistent Unicode representations for characters such as accented letters. Apply the same rules to mentions and aliases. Do not report linking rates from an unnormalized exact-string join.
- Candidate generation and ranking: An ambiguous alias, such as
Riverton, can match several entities. Production systems rank those candidates with the surrounding sentence context. - NIL handling: A matching alias can refer to a different entity outside the knowledge base, the reference collection of entities. The system needs a NIL result rather than an incorrect link.
- KB versioning: An entityβs canonical name or status may change. Linking results must record the specific knowledge-base (KB) version.
- Alias and candidate-recall checks: Evaluation must measure coverage of common misspellings in the alias table, called alias recall. It must also measure whether candidate generation retains the correct entity, called candidate recall.
What to remember
- Entity linking connects a mention to a stable identifier.
- An alias table can link different spellings to one record.
- Count ambiguous cases caused by duplicate aliases.
- Alias matching finds only names that the table contains.
The two forklift spellings count as one credential in these examples. The table does not identify the intended Riverton. The badge name remains unlinked because the alias file does not contain that name.