| Title: | Match and Deduplicate Messy Entity Names Against a Reference List |
|---|---|
| Description: | Aligns messy entity strings against a reference list using either string similarity or language-model embeddings. |
| Authors: | Saket Choudhary [aut, cre] (ORCID: <https://orcid.org/0000-0001-5202-7633>) |
| Maintainer: | Saket Choudhary <[email protected]> |
| License: | MIT + file LICENSE |
| Version: | 0.1.0 |
| Built: | 2026-10-16 12:43:08 UTC |
| Source: | https://github.com/saketlab/alethia |
The main entry point, mirroring the Python 'alethia()'. The pipeline is:
alethia( dirty_entries, reference_entries, model = "rapidfuzz", threshold = NULL, use_exact_matching = TRUE, exact_match_case_sensitive = FALSE, drop_duplicates = TRUE )alethia( dirty_entries, reference_entries, model = "rapidfuzz", threshold = NULL, use_exact_matching = TRUE, exact_match_case_sensitive = FALSE, drop_duplicates = TRUE )
dirty_entries |
Character vector of messy strings. |
reference_entries |
Character vector of canonical strings. |
model |
'"rapidfuzz"' for token-sort string similarity (the default, and the only backend needing no model), or anything [as_embedder()] accepts. |
threshold |
Minimum score to accept a match; below it the entry is reported as no match. 'NULL' means 'DEFAULT_THRESHOLD' for embeddings and no threshold for string matching. Scores are on '[0, 1]' either way, but string similarity and cosine measure different things, so a value tuned against one is not calibrated for the other. |
use_exact_matching |
Short-circuit entries already present in the references. |
exact_match_case_sensitive |
Whether that short-circuit respects case. |
drop_duplicates |
Collapse identical result rows, giving one row per distinct input. 'FALSE' keeps the input length and order, for joining back onto a table. |
1. drop 'NA' and null-like entries, remembering their positions; 2. short-circuit entries that already appear in the reference list; 3. send whatever remains to the chosen backend; 4. splice all three groups back into the caller's original row order.
A data frame carrying 'given_entity', 'alethia_prediction', 'alethia_score' and 'alethia_method', in input order.
alethia( c("Bombay", "Calcutta", "Mumbai"), c("Mumbai", "Kolkata", "Chennai") ) # Entries already present in the references short-circuit as exact matches, and # null-like values are preserved as no-match rows rather than dropped. alethia(c("Mumbai", NA, "N/A"), c("Mumbai"), drop_duplicates = FALSE)alethia( c("Bombay", "Calcutta", "Mumbai"), c("Mumbai", "Kolkata", "Chennai") ) # Entries already present in the references short-circuit as exact matches, and # null-like values are preserved as no-match rows rather than dropped. alethia(c("Mumbai", NA, "N/A"), c("Mumbai"), drop_duplicates = FALSE)
Mirrors the Python 'alethia_split()'. Each entry is split into pieces on a literal separator, every piece is matched independently against 'reference_entries' via [alethia()], and the highest-scoring piece becomes the entry's prediction. Ties break on first occurrence, same as the rest of the package. Every piece's own match rides along in 'alethia_parts'.
alethia_split(dirty_entries, reference_entries, split_on, ...)alethia_split(dirty_entries, reference_entries, split_on, ...)
dirty_entries |
Character vector of messy strings. |
reference_entries |
Character vector of canonical strings. |
split_on |
Literal separator each entry is split on (e.g. '","', '";"', '" and "'). Entries with no separator, or that split to nothing usable, are matched whole. |
... |
Forwarded to [alethia()] for the per-piece matching ('model', 'threshold', ...). |
A data frame of 'given_entity', 'alethia_prediction', 'alethia_score', 'alethia_matched_part' (the winning piece), and 'alethia_parts' (a list column: every piece tried, as a list of 'part'/'prediction'/'score', best first).
alethia_split( c("CNS Tuberculosis, Septic Shock"), c("Tuberculosis of nervous system", "Septic shock"), split_on = "," )alethia_split( c("CNS Tuberculosis, Septic Shock"), c("Tuberculosis of nervous system", "Septic shock"), split_on = "," )
Mean '||x - y||^alpha' over matched positive pairs, on L2-normalised embeddings. Lower means the model keeps a string and its dirtied variant close. After Wang & Isola (2020).
alignment_loss(src_emb, var_emb, alpha = 2)alignment_loss(src_emb, var_emb, alpha = 2)
src_emb, var_emb
|
Matched embedding matrices, row 'i' a positive pair. |
alpha |
Exponent on the distance. |
Scalar loss.
Other assess metrics:
centered_separability(),
hubness(),
intrinsic_dimensionality(),
mutual_nn_rate(),
positive_pair_rank(),
reference_separability(),
retrieval_margin(),
uniformity_loss()
bag <- function(x) { t(sapply( strsplit(tolower(x), ""), function(ch) tabulate(match(ch, letters), 26) )) } refs <- bag(c("aspirin", "ibuprofen", "paracetamol", "metformin")) queries <- bag(c("asprin", "ibuprofn", "paracetmol", "metfornin")) alignment_loss(refs, queries)bag <- function(x) { t(sapply( strsplit(tolower(x), ""), function(ch) tabulate(match(ch, letters), 26) )) } refs <- bag(c("aspirin", "ibuprofen", "paracetamol", "metformin")) queries <- bag(c("asprin", "ibuprofn", "paracetmol", "metfornin")) alignment_loss(refs, queries)
Sub-threshold rows are reported as no match. 'NULL' leaves the frame untouched.
apply_threshold(results, threshold)apply_threshold(results, threshold)
results |
Data frame with 'alethia_prediction' and 'alethia_score'. |
threshold |
Minimum acceptable score, or 'NULL'. |
The frame, with sub-threshold rows set to 'NA'.
res <- match_by_strings("Bombay", c("Mumbai", "Kolkata")) apply_threshold(res, 0.9)res <- match_by_strings("Bombay", c("Mumbai", "Kolkata")) apply_threshold(res, 0.9)
Mirrors 'alethia.embedder.as_embedder'. An embedder is any object with an 'encode(texts)' method returning an 'n x d' numeric matrix. This function accepts:
as_embedder(model, name = NULL)as_embedder(model, name = NULL)
model |
A function, an embedder, or a model name. |
name |
Optional display name. |
* a function 'f(texts) -> matrix', wrapped as a callable embedder; * an object already carrying an 'encode' element (returned unchanged); * a character model name, resolved through the registered backend (see [set_embedding_backend()]).
Every embedding source sits behind this seam, so matching, clustering and assessment share one implementation.
An object of class 'alethia_embedder'.
# Any function from character vector to matrix is an embedder. bag <- function(x) { t(sapply( strsplit(tolower(x), ""), function(ch) tabulate(match(ch, letters), 26) )) } e <- as_embedder(bag, name = "letter-count") e# Any function from character vector to matrix is an embedder. bag <- function(x) { t(sapply( strsplit(tolower(x), ""), function(ch) tabulate(match(ch, letters), 26) )) } e <- as_embedder(bag, name = "letter-count") e
Each model is scored on the metrics in [assess-metrics], z-scored **across models** and summed. The composite therefore needs at least two models; with one the score is 'NA' and only the component metrics are reported. Weights are normalised **within metric family**, so correlated views of one property cannot count it twice, and a family that could not be computed drops out.
assess_models( queries, references, models, weights = NULL, family_normalize = TRUE, variants = NULL ) DEFAULT_METRIC_WEIGHTSassess_models( queries, references, models, weights = NULL, family_normalize = TRUE, variants = NULL ) DEFAULT_METRIC_WEIGHTS
queries |
Character vector of dirty/query strings. May be empty, in which case the retrieval family is skipped and the reference geometry alone is scored. |
references |
Character vector of canonical strings. |
models |
Named list of model specifications, each anything [as_embedder()] accepts. Names are used for reporting. |
weights |
Optional named numeric vector overriding the per-metric weights. Interpreted within family when 'family_normalize' is 'TRUE'. |
family_normalize |
Rescale weights so each family contributes at most its family weight. 'FALSE' gives flat per-metric weighting. |
variants |
Optional list of 'source' and 'variant' character vectors, row 'i' a (clean, dirtied) pair, which enables the robustness family. Generate them with 'alethia.assess.generate_positive_pairs'; omitted, the family drops out. |
An object of class 'alethia_assessment' with '$table' (a data frame, best first) and '$best' (the winning model name, or 'NA' when not comparative).
bag <- function(x) { t(sapply( strsplit(tolower(x), ""), function(ch) tabulate(match(ch, letters), 26) )) } rev_bag <- function(texts) { bag(vapply(texts, function(s) { paste(rev(strsplit(s, "")[[1]]), collapse = "") }, character(1))) } refs <- c("aspirin", "ibuprofen", "paracetamol", "metformin", "atenolol") res <- assess_models( c("asprin", "ibuprofn"), refs, list(bag = bag, reversed = rev_bag) ) res$bestbag <- function(x) { t(sapply( strsplit(tolower(x), ""), function(ch) tabulate(match(ch, letters), 26) )) } rev_bag <- function(texts) { bag(vapply(texts, function(s) { paste(rev(strsplit(s, "")[[1]]), collapse = "") }, character(1))) } refs <- c("aspirin", "ibuprofen", "paracetamol", "metformin", "atenolol") res <- assess_models( c("asprin", "ibuprofn"), refs, list(bag = bag, reversed = rev_bag) ) res$best
These score an embedding model on your own data without ground-truth labels, by measuring the geometry it produces. Ports of 'alethia.assess.metrics'; each formula is stated so the two implementations can be checked against one another.
[reference_separability()] after removing the set mean, which strips the shared direction responsible for uniformly high cosines.
centered_separability(ref_emb)centered_separability(ref_emb)
ref_emb |
Numeric matrix of reference embeddings. |
Named list of 'centered_nn_similarity' and 'nn_margin_z'.
Other assess metrics:
alignment_loss(),
hubness(),
intrinsic_dimensionality(),
mutual_nn_rate(),
positive_pair_rank(),
reference_separability(),
retrieval_margin(),
uniformity_loss()
bag <- function(x) { t(sapply( strsplit(tolower(x), ""), function(ch) tabulate(match(ch, letters), 26) )) } refs <- bag(c("aspirin", "ibuprofen", "paracetamol", "metformin")) queries <- bag(c("asprin", "ibuprofn", "paracetmol", "metfornin")) centered_separability(refs)bag <- function(x) { t(sapply( strsplit(tolower(x), ""), function(ch) tabulate(match(ch, letters), 26) )) } refs <- bag(c("aspirin", "ibuprofen", "paracetamol", "metformin")) queries <- bag(c("asprin", "ibuprofn", "paracetmol", "metfornin")) centered_separability(refs)
Mirrors 'alethia.cluster.cluster_entities'. Edges require *mutual* nearest-neighbourhood above a cosine floor and each carries a confidence, so no hub chains the whole set into one cluster the way a plain threshold does.
cluster_entities( entities, model, floor = 0.8, k = 5, require_mutual = TRUE, min_confidence = NULL, canonical = c("shortest", "first") )cluster_entities( entities, model, floor = 0.8, k = 5, require_mutual = TRUE, min_confidence = NULL, canonical = c("shortest", "first") )
entities |
Character vector to group. De-duplicated on exact text first. |
model |
Anything [as_embedder()] accepts. |
floor |
Minimum cosine for an edge. |
k |
Neighbourhood size for the mutual-NN test. Clamped to '[1, n - 1]'. |
require_mutual |
Require each entity to be in the other's top-k. |
min_confidence |
Optional cutoff applied to edge confidence before clustering. |
canonical |
How to name each cluster: '"shortest"' member or '"first"' seen. |
An object of class 'alethia_clusters'.
bag <- function(x) { t(sapply( strsplit(tolower(x), ""), function(ch) tabulate(match(ch, letters), 26) )) } res <- cluster_entities(c("color", "colour", "zebra"), bag, floor = 0.8) res cluster_records(res)bag <- function(x) { t(sapply( strsplit(tolower(x), ""), function(ch) tabulate(match(ch, letters), 26) )) } res <- cluster_entities(c("color", "colour", "zebra"), bag, floor = 0.8) res cluster_records(res)
Cluster membership as a tidy data frame
cluster_records(x)cluster_records(x)
x |
An 'alethia_clusters' object. |
A data frame with 'entity', 'cluster' and 'canonical' columns.
bag <- function(x) { t(sapply( strsplit(tolower(x), ""), function(ch) tabulate(match(ch, letters), 26) )) } cluster_records(cluster_entities(c("color", "colour"), bag, floor = 0.8))bag <- function(x) { t(sapply( strsplit(tolower(x), ""), function(ch) tabulate(match(ch, letters), 26) )) } cluster_records(cluster_entities(c("color", "colour"), bag, floor = 0.8))
String matching does not use this; a cosine and a surface-overlap score of the same value do not mean the same thing.
DEFAULT_THRESHOLDDEFAULT_THRESHOLD
Encode text with an embedder
encode(embedder, texts)encode(embedder, texts)
embedder |
An 'alethia_embedder', from [as_embedder()]. |
texts |
Character vector. |
Numeric matrix with one row per input.
bag <- function(x) { t(sapply( strsplit(tolower(x), ""), function(ch) tabulate(match(ch, letters), 26) )) } dim(encode(as_embedder(bag), c("colour", "color")))bag <- function(x) { t(sapply( strsplit(tolower(x), ""), function(ch) tabulate(match(ch, letters), 26) )) } dim(encode(as_embedder(bag), c("colour", "color")))
Skewness of the k-occurrence distribution: how unevenly references are drawn as top-k neighbours. High values are pathological, a few "hub" references absorbing many queries. After Radovanovic et al. (2010), JMLR 11:2487-2531.
hubness(query_emb, ref_emb, k = 5)hubness(query_emb, ref_emb, k = 5)
query_emb, ref_emb
|
Numeric embedding matrices. |
k |
Neighbourhood size. |
Named list with 'hubness_skew'.
Other assess metrics:
alignment_loss(),
centered_separability(),
intrinsic_dimensionality(),
mutual_nn_rate(),
positive_pair_rank(),
reference_separability(),
retrieval_margin(),
uniformity_loss()
bag <- function(x) { t(sapply( strsplit(tolower(x), ""), function(ch) tabulate(match(ch, letters), 26) )) } refs <- bag(c("aspirin", "ibuprofen", "paracetamol", "metformin")) queries <- bag(c("asprin", "ibuprofn", "paracetmol", "metfornin")) hubness(queries, refs, k = 2)bag <- function(x) { t(sapply( strsplit(tolower(x), ""), function(ch) tabulate(match(ch, letters), 26) )) } refs <- bag(c("aspirin", "ibuprofen", "paracetamol", "metformin")) queries <- bag(c("asprin", "ibuprofn", "paracetmol", "metfornin")) hubness(queries, refs, k = 2)
Participation ratio '(sum(lambda))^2 / sum(lambda^2)' over the PCA eigenvalues, and the same divided by the ambient dimension. Higher means the model spreads the data over more directions.
intrinsic_dimensionality(emb)intrinsic_dimensionality(emb)
emb |
Numeric matrix of embeddings. |
Named list of 'participation_ratio' and 'normalized_pr'.
Other assess metrics:
alignment_loss(),
centered_separability(),
hubness(),
mutual_nn_rate(),
positive_pair_rank(),
reference_separability(),
retrieval_margin(),
uniformity_loss()
bag <- function(x) { t(sapply( strsplit(tolower(x), ""), function(ch) tabulate(match(ch, letters), 26) )) } refs <- bag(c("aspirin", "ibuprofen", "paracetamol", "metformin")) queries <- bag(c("asprin", "ibuprofn", "paracetmol", "metfornin")) intrinsic_dimensionality(refs)bag <- function(x) { t(sapply( strsplit(tolower(x), ""), function(ch) tabulate(match(ch, letters), 26) )) } refs <- bag(c("aspirin", "ibuprofen", "paracetamol", "metformin")) queries <- bag(c("asprin", "ibuprofn", "paracetmol", "metfornin")) intrinsic_dimensionality(refs)
The single canonical normalisation used by matching, clustering and assessment, mirroring 'alethia.embedder.l2_normalize'. Zero rows stay zero, never producing 'NaN'.
l2_normalize(x)l2_normalize(x)
x |
Numeric matrix, one embedding per row. |
Matrix of the same shape with unit-norm rows.
l2_normalize(rbind(c(3, 4), c(0, 0)))l2_normalize(rbind(c(3, 4), c(0, 0)))
The single embedding-based matching primitive, mirroring 'alethia.embedder.match_by_embeddings'. Every embedding backend routes through it.
match_by_embeddings(queries, references, embedder, threshold = NULL)match_by_embeddings(queries, references, embedder, threshold = NULL)
queries |
Character vector of strings to match. |
references |
Character vector of candidate strings. |
embedder |
Anything [as_embedder()] accepts. |
threshold |
Optional minimum cosine. Matches below it become 'NA'. |
A data frame with 'given_entity', 'alethia_prediction', 'alethia_score'.
bag <- function(x) { t(sapply( strsplit(tolower(x), ""), function(ch) tabulate(match(ch, letters), 26) )) } match_by_embeddings(c("colour"), c("color", "zebra"), bag)bag <- function(x) { t(sapply( strsplit(tolower(x), ""), function(ch) tabulate(match(ch, letters), 26) )) } match_by_embeddings(c("colour"), c("color", "zebra"), bag)
The counterpart of Python's 'run_rapidfuzz_matching()': for each query, the highest-scoring reference.
match_by_strings(queries, references, threshold = NULL)match_by_strings(queries, references, threshold = NULL)
queries |
Character vector of strings to match. |
references |
Character vector of candidate strings. |
threshold |
Minimum score in '[0, 1]'; below it an entry is 'NA'. 'NULL' returns the best match whatever it scores. |
A data frame with 'given_entity', 'alethia_prediction' and 'alethia_score'. Scores are rescaled to '[0, 1]'.
match_by_strings( c("Bombay", "Calcutta"), c("Mumbai", "Kolkata", "Chennai") ) # Demand a close match, and report anything weaker as no match. match_by_strings("Bombay", c("Mumbai", "Kolkata"), threshold = 0.9)match_by_strings( c("Bombay", "Calcutta"), c("Mumbai", "Kolkata", "Chennai") ) # Demand a close match, and report anything weaker as no match. match_by_strings("Bombay", c("Mumbai", "Kolkata"), threshold = 0.9)
Candidate merge edges from mutual nearest neighbours
mutual_nn_edges(emb, floor = 0.8, k = 5, require_mutual = TRUE)mutual_nn_edges(emb, floor = 0.8, k = 5, require_mutual = TRUE)
emb |
Numeric matrix, one embedding per row. L2-normalised internally. |
floor |
Minimum cosine similarity for an edge. |
k |
Neighbourhood size. |
require_mutual |
Keep only reciprocated edges. |
A data frame of edges with 'i', 'j', 'cosine', 'margin_i', 'margin_j', 'mutual' and 'confidence', sorted by decreasing confidence.
emb <- rbind(c(1, 0), c(0.99, 0.14), c(0, 1)) mutual_nn_edges(emb, floor = 0.5, k = 1)emb <- rbind(c(1, 0), c(0.99, 0.14), c(0, 1)) mutual_nn_edges(emb, floor = 0.5, k = 1)
Fraction of query-to-reference top-1 assignments reciprocated within the reference's own top-k. Higher means more stable, more trustworthy matches.
mutual_nn_rate(query_emb, ref_emb, k = 5)mutual_nn_rate(query_emb, ref_emb, k = 5)
query_emb, ref_emb
|
Numeric embedding matrices. |
k |
Neighbourhood size on the reference side. |
Scalar rate.
Other assess metrics:
alignment_loss(),
centered_separability(),
hubness(),
intrinsic_dimensionality(),
positive_pair_rank(),
reference_separability(),
retrieval_margin(),
uniformity_loss()
bag <- function(x) { t(sapply( strsplit(tolower(x), ""), function(ch) tabulate(match(ch, letters), 26) )) } refs <- bag(c("aspirin", "ibuprofen", "paracetamol", "metformin")) queries <- bag(c("asprin", "ibuprofn", "paracetmol", "metfornin")) mutual_nn_rate(queries, refs, k = 2)bag <- function(x) { t(sapply( strsplit(tolower(x), ""), function(ch) tabulate(match(ch, letters), 26) )) } refs <- bag(c("aspirin", "ibuprofen", "paracetamol", "metformin")) queries <- bag(c("asprin", "ibuprofn", "paracetmol", "metfornin")) mutual_nn_rate(queries, refs, k = 2)
Number of clusters found
n_clusters(x)n_clusters(x)
x |
An 'alethia_clusters' object. |
Integer count of distinct clusters.
bag <- function(x) { t(sapply( strsplit(tolower(x), ""), function(ch) tabulate(match(ch, letters), 26) )) } n_clusters(cluster_entities(c("color", "colour", "zebra"), bag, floor = 0.8))bag <- function(x) { t(sapply( strsplit(tolower(x), ""), function(ch) tabulate(match(ch, letters), 26) )) } n_clusters(cluster_entities(c("color", "colour", "zebra"), bag, floor = 0.8))
Checks that the three R packages are installed and that ONNX Runtime itself has been downloaded. The runtime is a separate one-time download, not an R dependency, so installing the packages is not enough.
onnx_available()onnx_available()
'TRUE' when local ONNX inference will work.
onnx_available()onnx_available()
Runs a sentence-embedding model on the CPU with no Python installation, using 'onnxr' for inference, 'tok' for tokenisation, and 'hfhub' to fetch the weights.
onnx_embedder( model_id, backend = c("cpu", "coreml", "cuda", "xnnpack", "openvino"), threads = NULL, max_length = 256L, batch_size = 32L, onnx_file = "onnx/model.onnx", tokenizer_file = "tokenizer.json" )onnx_embedder( model_id, backend = c("cpu", "coreml", "cuda", "xnnpack", "openvino"), threads = NULL, max_length = 256L, batch_size = 32L, onnx_file = "onnx/model.onnx", tokenizer_file = "tokenizer.json" )
model_id |
Hugging Face model id, e.g. '"sentence-transformers/all-MiniLM-L6-v2"'. |
backend |
Execution provider. '"cpu"' by default; the others are only useful if you have the matching hardware and an ONNX Runtime built for it. |
threads |
Inference threads. 'NULL' leaves it to ONNX Runtime. |
max_length |
Sequence length above which inputs are truncated. |
batch_size |
Texts per forward pass. Lower this if memory is tight; it changes speed and memory, not results. |
onnx_file |
Path within the repository to the ONNX weights. |
tokenizer_file |
Path within the repository to the tokeniser. |
Pooling reproduces sentence-transformers (an attention-masked mean over token vectors, then L2 normalisation), so embeddings agree with the Python package to floating-point tolerance for the same model.
The model must publish ONNX weights. Most sentence-transformers models do, under 'onnx/model.onnx'; the 'onnx-community' and 'Xenova' organisations on Hugging Face publish conversions of many others.
An 'alethia_embedder', usable anywhere a model is accepted.
[use_onnx_backend()] to resolve bare model names through this path.
# Downloads ~90 MB on first use, then caches. if (onnx_available()) { embedder <- onnx_embedder("sentence-transformers/all-MiniLM-L6-v2") alethia(c("Bombay", "Calcutta"), c("Mumbai", "Kolkata"), model = embedder) }# Downloads ~90 MB on first use, then caches. if (onnx_available()) { embedder <- onnx_embedder("sentence-transformers/all-MiniLM-L6-v2") alethia(c("Bombay", "Calcutta"), c("Mumbai", "Kolkata"), model = embedder) }
How highly each synthesized positive ranks among *all* variants. 'src_emb[i]' and 'var_emb[i]' are a positive pair; for each source this is the percentile rank of its own variant among every variant, averaged. Higher is better. Ties take their mid-rank.
positive_pair_rank(src_emb, var_emb)positive_pair_rank(src_emb, var_emb)
src_emb, var_emb
|
Matched embedding matrices, row 'i' a positive pair. |
Unlike [alignment_loss()], this uses only the *ordering* of each source's similarities, so it is invariant to any strictly increasing transformation of cosine, including the affine shift from adding a common cone direction and renormalising. Hence its weight in the composite.
Scalar mean percentile rank in '[0, 1]'.
Other assess metrics:
alignment_loss(),
centered_separability(),
hubness(),
intrinsic_dimensionality(),
mutual_nn_rate(),
reference_separability(),
retrieval_margin(),
uniformity_loss()
Equally similar references are common in real data: shared tokens, anagrams, duplicated records. Whether such a pair is *seen* as tied otherwise depends on the last bits of a dot product, which differ between BLAS builds and languages. Rounding first makes "equally similar" mean the same thing everywhere, so the tie-break by ascending index gives the same answer. Reported scores are left unrounded.
RANK_DECIMALSRANK_DECIMALS
How well distinct references are kept apart. Returns the mean and median cosine to the nearest *other* reference (lower is better), and the fraction whose nearest other reference exceeds 'confusion_threshold'.
reference_separability(ref_emb, confusion_threshold = 0.9)reference_separability(ref_emb, confusion_threshold = 0.9)
ref_emb |
Numeric matrix of reference embeddings. |
confusion_threshold |
Cosine above which two references count as confusable. |
These are raw cosines, so they partly reflect anisotropy: transformer embeddings occupy a narrow cone, pushing every cosine high. Retrieval depends on the ordering of similarities, not their level, so [centered_separability()] gives the anisotropy-corrected view.
Named list of 'mean_nn_similarity', 'median_nn_similarity', 'confusability_rate'.
Other assess metrics:
alignment_loss(),
centered_separability(),
hubness(),
intrinsic_dimensionality(),
mutual_nn_rate(),
positive_pair_rank(),
retrieval_margin(),
uniformity_loss()
bag <- function(x) { t(sapply( strsplit(tolower(x), ""), function(ch) tabulate(match(ch, letters), 26) )) } refs <- bag(c("aspirin", "ibuprofen", "paracetamol", "metformin")) queries <- bag(c("asprin", "ibuprofn", "paracetmol", "metfornin")) reference_separability(refs)bag <- function(x) { t(sapply( strsplit(tolower(x), ""), function(ch) tabulate(match(ch, letters), 26) )) } refs <- bag(c("aspirin", "ibuprofen", "paracetamol", "metformin")) queries <- bag(c("asprin", "ibuprofn", "paracetmol", "metfornin")) reference_separability(refs)
How decisively the top reference beats the runner-up for each query, both in raw cosine and in per-query standard deviations ('_z', which is scale-free and so comparable across models with different cosine ranges).
retrieval_margin(query_emb, ref_emb, low_margin = 0.02)retrieval_margin(query_emb, ref_emb, low_margin = 0.02)
query_emb, ref_emb
|
Numeric embedding matrices. |
low_margin |
Margin below which a match counts as ambiguous. |
Named list of 'mean_margin', 'low_margin_rate', 'mean_margin_z', 'low_margin_z_rate'.
Other assess metrics:
alignment_loss(),
centered_separability(),
hubness(),
intrinsic_dimensionality(),
mutual_nn_rate(),
positive_pair_rank(),
reference_separability(),
uniformity_loss()
bag <- function(x) { t(sapply( strsplit(tolower(x), ""), function(ch) tabulate(match(ch, letters), 26) )) } refs <- bag(c("aspirin", "ibuprofen", "paracetamol", "metformin")) queries <- bag(c("asprin", "ibuprofn", "paracetmol", "metfornin")) retrieval_margin(queries, refs)bag <- function(x) { t(sapply( strsplit(tolower(x), ""), function(ch) tabulate(match(ch, letters), 26) )) } refs <- bag(c("aspirin", "ibuprofen", "paracetamol", "metformin")) queries <- bag(c("asprin", "ibuprofn", "paracetmol", "metfornin")) retrieval_margin(queries, refs)
The package ships no model runtime of its own. Point this at whatever you use to produce embeddings, e.g. a local ONNX session, an API client, or a cached matrix.
set_embedding_backend(backend) get_embedding_backend()set_embedding_backend(backend) get_embedding_backend()
backend |
A function taking a model name and returning a function 'f(texts) -> matrix'. 'NULL' clears the registration. |
The previous backend, invisibly.
bag <- function(x) { t(sapply( strsplit(tolower(x), ""), function(ch) tabulate(match(ch, letters), 26) )) } old <- set_embedding_backend(function(model_name) bag) as_embedder("any-model-name") set_embedding_backend(old)bag <- function(x) { t(sapply( strsplit(tolower(x), ""), function(ch) tabulate(match(ch, letters), 26) )) } old <- set_embedding_backend(function(model_name) bag) as_embedder("any-model-name") set_embedding_backend(old)
The preprocessing step inside [token_sort_ratio()], exported because the same normalisation is useful when caching or de-duplicating inputs.
token_sort(x)token_sort(x)
x |
Character vector. |
Character vector with tokens sorted and single-space separated.
token_sort(" South 24 Parganas ")token_sort(" South 24 Parganas ")
Reproduces 'rapidfuzz.fuzz.token_sort_ratio', the scorer behind the default '"rapidfuzz"' backend.
token_sort_ratio(a, b)token_sort_ratio(a, b)
a, b
|
Character vectors, recycled against each other. |
Exact agreement rests on an identity: RapidFuzz's 'ratio' is the normalised indel (insert/delete-only edit) similarity,
ratio(a, b) = 100 * (1 - indel(a, b) / (nchar(a) + nchar(b)))
and 'stringdist(method = "lcs")' computes that indel distance. Both sides evaluate the same rational number in double precision, so the scores agree to within an ULP.
Tokens are split on whitespace, sorted, and rejoined with a single space. Case and punctuation are left alone, so '"bombay"' and '"Bombay"' score 83.33, not 100.
Numeric vector of scores in '[0, 100]'.
# Token order does not matter, but case does. token_sort_ratio("New Delhi", "Delhi New") token_sort_ratio("Bombay", "Mumbai")# Token order does not matter, but case does. token_sort_ratio("New Delhi", "Delhi New") token_sort_ratio("Bombay", "Mumbai")
The labelled counterpart to the label-free metrics: given the correct reference for each query, how often does the model retrieve it? [assess_models()] predicts this without labels.
true_accuracy(queries, references, truth, model, nil_threshold = NULL)true_accuracy(queries, references, truth, model, nil_threshold = NULL)
queries |
Character vector of query strings. |
references |
Character vector of candidate strings. |
truth |
Correct reference per query, or 'NA' for an open-world NIL query with no correct answer. Real matching input is full of these: a cause of death that is not a diagnosis, a drug outside the formulary. |
model |
Anything [as_embedder()] accepts. |
nil_threshold |
Cosine below which a top-1 match counts as an abstention. Required when 'truth' contains 'NA'; ignored otherwise. |
A named list with 'top1' and 'mrr', plus 'nil_recall' and 'answerable_top1' when the dataset contains NIL queries. Those two are separate because one accuracy number hides the trade-off: a model can look good by always abstaining, or never.
bag <- function(x) { t(sapply( strsplit(tolower(x), ""), function(ch) tabulate(match(ch, letters), 26) )) } true_accuracy( c("colour", "flavour"), c("color", "flavor"), c("color", "flavor"), bag )bag <- function(x) { t(sapply( strsplit(tolower(x), ""), function(ch) tabulate(match(ch, letters), 26) )) } true_accuracy( c("colour", "flavour"), c("color", "flavor"), c("color", "flavor"), bag )
'log E exp(-t * ||x - y||^2)' over all pairs. More negative means the embeddings spread more evenly over the sphere, wasting less of the representation. After Wang & Isola (2020).
uniformity_loss(emb, t = 2, max_points = 2000)uniformity_loss(emb, t = 2, max_points = 2000)
emb |
Numeric matrix of embeddings. |
t |
Temperature. |
max_points |
Subsample size when there are more rows than this. |
Scalar loss.
Other assess metrics:
alignment_loss(),
centered_separability(),
hubness(),
intrinsic_dimensionality(),
mutual_nn_rate(),
positive_pair_rank(),
reference_separability(),
retrieval_margin()
bag <- function(x) { t(sapply( strsplit(tolower(x), ""), function(ch) tabulate(match(ch, letters), 26) )) } refs <- bag(c("aspirin", "ibuprofen", "paracetamol", "metformin")) queries <- bag(c("asprin", "ibuprofn", "paracetmol", "metfornin")) uniformity_loss(refs)bag <- function(x) { t(sapply( strsplit(tolower(x), ""), function(ch) tabulate(match(ch, letters), 26) )) } refs <- bag(c("aspirin", "ibuprofen", "paracetamol", "metformin")) queries <- bag(c("asprin", "ibuprofn", "paracetmol", "metfornin")) uniformity_loss(refs)
After calling this, a model name passed to [alethia()], [cluster_entities()] or [as_embedder()] is loaded and run locally on the CPU.
use_onnx_backend(...)use_onnx_backend(...)
... |
Passed to [onnx_embedder()], e.g. 'threads' or 'batch_size'. |
The previously registered backend, invisibly.
if (onnx_available()) { use_onnx_backend(threads = 4) alethia(c("Bombay"), c("Mumbai", "Delhi"), model = "sentence-transformers/all-MiniLM-L6-v2") }if (onnx_available()) { use_onnx_backend(threads = 4) alethia(c("Bombay"), c("Mumbai", "Delhi"), model = "sentence-transformers/all-MiniLM-L6-v2") }