Skip to content

Entity Alignment with a feature-based classifier (EntMatcher-style)

This aligns the same two toy knowledge graphs, but scores each candidate pair with a classical ML classifier (FeatureClassifierLinker, logistic regression by default) trained on several hand-crafted similarity features — several string-similarity metrics plus TF-IDF cosine similarity — rather than a single fixed metric, then resolves the final one-to-one mapping via a globally optimal assignment (the Hungarian algorithm) rather than independent per-source argmax. EntMatcherLinker is a preconfigured instance of FeatureClassifierLinker (matching=OptimalMatcher()), named after EntMatcher — not a dependency of this project, but the source of the "a globally optimal assignment can outperform independent per-source matching" idea reused here. Blocking here uses LabelOverlap(ngram_size=1, ...) rather than the max_matches=3 used in the StringSimilarityLinker example, so that fit() sees some incorrect candidate pairs to learn to reject, not just the true matches.

This is a pipeline-correctness demo, not a generalization benchmark — the toy dataset has only 3 ground-truth pairs, far too few to demonstrate real generalization.

"""Entity Alignment with a feature-based classifier, EntMatcher-style.

Aligns the same two toy knowledge graphs as `string_similarity_ea.py`, but
scores each candidate pair with a classical ML classifier (logistic
regression, by default) trained on several hand-crafted similarity
features rather than a single fixed metric, then resolves the final
one-to-one mapping via a globally optimal assignment
(`EntMatcherLinker`) rather than independent per-source argmax.

`LabelOverlap(ngram_size=1, ...)` is used for blocking here (rather than
the `max_matches=3` used elsewhere) so that `fit()` sees both correct and
incorrect candidate pairs to learn from -- on a dataset this small,
tighter blocking finds only the true matches, leaving nothing to
discriminate against.

This is a pipeline-correctness demo, not a generalization benchmark: the
toy dataset has only 3 ground-truth pairs, far too few to draw conclusions
about how well the classifier generalizes to unseen entities.

Run with: `uv run python examples/feature_classifier_ea.py`
"""

from linkingtk.algorithms.ea import EntMatcherLinker
from linkingtk.blocking import LabelOverlap
from linkingtk.datasets import ToyEADataset
from linkingtk.eval import Evaluator


def main() -> None:
    kg1, kg2, ground_truth = ToyEADataset().load()
    blocking = LabelOverlap(ngram_size=1, max_matches=10)

    linker = EntMatcherLinker().fit(kg1, kg2, ground_truth, blocking=blocking, random_state=0)
    results = linker.link(kg1, kg2, blocking=blocking)

    for result in results:
        print(f"{result.source_id} -> {result.target_id} (score={result.score:.2f})")

    predictions = [(r.source_id, r.target_id) for r in results]
    report = Evaluator.evaluate(predictions=predictions, ground_truth=ground_truth)
    print("Metrics:", report.metrics)


if __name__ == "__main__":
    main()

Run with:

uv run python examples/feature_classifier_ea.py
kg1:paris -> kg2:paris (score=0.91)
kg1:berlin -> kg2:berlin (score=0.91)
kg1:rome -> kg2:rome (score=0.90)
Metrics: {'precision@1': 1.0, 'recall': 1.0, 'f1': 1.0}