First Steps
Introduction
Code blocks shown in this tutorial assume that a DataFrame, labelled df, will be available at runtime.
Rarely are efforts are made to specify the nature of the data in df. Instead, the emphasis is on how to set up near deduplication correctly with Liken.
There are datasets available for experimentation in the liken.datasets module for easy access to dummy data.
Instantiating
A DataFrame must be passed to the top-level dedupe function.
The Dedupe Lifecycle
Every Liken workflow follows the same formula:
- Construct.
lk.dedupe(df)wraps your DataFrame. Liken detects which backend to use from the DataFrame's type. - Stage.
.apply(...)adds dedupers to a collection. You can stage one deduper, a dict of per-column rules, or a pipeline. - Enact.
.drop_duplicates()or.canonicalize()runs the staged rules, matches records, and returns a new DataFrame.
import liken as lk
deduper = lk.dedupe(df) # construct
deduper = deduper.apply(lk.fuzzy()) # stage
df = deduper.drop_duplicates("address") # enact
Every later tutorial builds on this formula.
The Simplest Example
For the simplest use cases, Liken provides familiar-feeling exact deduplication, without ceremony:
With no deduper applied, drop_duplicates falls back to exact matching on the columns you pass: two rows are duplicates when their values are identical. Rows missing a value count as equal too — two rows with no address are duplicates of each other, and one is removed.
However, dataframe records may not be exactly repeated:
| id | address | |
|---|---|---|
| 1 | london | fizzpop@yahoo.com |
| 2 | tokyo | FizzPop@yahoo.com |
| 3 | paris | a@msn.fr |
"fizzpop" and "FizzPop" aren't exactly the same, but likely are.
Using drop_duplicates straight from pandas won't do anything here, as "fizzpop@yahoo.com" and "FizzPop@yahoo.com" are not the same strings. Neither will the exact matching above: these two emails are distinct strings, so exact matching keeps all three rows. For records like these you need near deduplication.
Near Deduplication
When things aren't exactly the same, you can still deduplicate data. Liken is built so that you can focus on defining what you want out of a near-deduplication process. The goal will be to be able to define neat and clear-cut ways to deduplicate data with the least amount of code possible. Before looking at how to use dedupers, let's look at what dedupers are available.
Built-in Dedupers
Liken comes with many deduplication methods built-in:
| Deduper | Description | ||
|---|---|---|---|
| Similarity | single-column | exact |
You've already seen this in use implicitly in The Simplest Example. |
| Similarity | single-column | fuzzy |
Fuzzy string matching. |
| Similarity | single-column | edit_distance |
Values within a maximum edit (Levenshtein) distance. |
| Similarity | single-column | tfidf |
String token matching with Tf-Idf. |
| Similarity | single-column | lsh |
String token matching with Locality Sensitive Hashing (LSH). |
| Similarity | compound-column | jaccard |
Multi column similarity based on intersection of categorical data. |
| Similarity | compound-column | cosine |
Multi column similarity based on dot product of numerical data. |
| Predicate | single-column | isna |
Records where the column value is null/None. |
| Predicate | single-column | isin |
Records where the column value is in a list of members. |
| Predicate | single-column | str_startswith |
Records where the string starts with a pattern. |
| Predicate | single-column | str_endswith |
Records where the string ends with a pattern. |
| Predicate | single-column | str_contains |
Records where the string contains a pattern. Accepts Regex. |
| Predicate | single-column | str_len |
Records where the string length is bounded by a minimum and maximum length. |
| Predicate | single-column | num_range |
Records where the numeric value falls inside the inclusive interval. |
Single-column dedupers apply to single columns and are implementation of near string matching. Compound-column dedupers are set operations where the values of the set are the values of the columns in a given record. Similarity dedupers have a threshold argument. Predicate dedupers choose an outcome based on a discrete outcome (e.g. is null / not null).
To use dedupers, you have to apply them, which is covered in the next tutorial.
Choosing a Threshold
Similarity dedupers compare values pairwise and keep the pairs whose score meets their threshold. All thresholds are on a 0–1 scale, and matching is inclusive: a pair scoring exactly at the threshold matches. At the extreme, threshold=0 matches every pair, including pairs with no similarity at all. Thresholds differ in what they measure:
fuzzyuses rapidfuzz string similarity. The default scorer,simple_ratio, compares whole strings character by character:"london"and"londn"score about 0.91. Matching is case-sensitive:"LONDON"and"london"score 0.0. Other scorers (token_sort_ratio,token_set_ratio, and more) change how strings are compared, not the scale.tfidfsplits each value into character n-grams (ngram, default 3), weights them, and compares the resulting vectors by cosine similarity. It tolerates more drift thanfuzzy, but its defaulttopn=2keeps only the two best candidate matches per row, one of which is the row itself. In dense clusters of near-duplicates, raisetopn.lshbuilds MinHash signatures over character n-grams and matches approximately: the threshold estimates the Jaccard similarity of the n-gram sets. It scales well to very large data, but a match is probabilistic — pairs near the threshold can be missed.
explore shows where your data's duplicate rates actually fall, so you can pick a threshold from evidence rather than guesswork.
Exploring Your Data
Before choosing a threshold, measure. explore reports, for each column you name, the share of records that would be removed under exact matching and under each fuzzy threshold:
| metric | address | |
|---|---|---|
| exact | 0.0 | 0.000000 |
| 0.5 | 0.0 | 0.333333 |
| 0.75 | 0.0 | 0.333333 |
| 0.9 | 0.0 | 0.000000 |
| 0.95 | 0.0 | 0.000000 |
| 0.99 | 0.0 | 0.000000 |
Duplicate rates for the dataset above. Email variants registers at 0.5 and 0.75, but not at >0.9.
Which can read as:
- The
exactrow is the exact-matching duplicate rate. It is 0.0 for both columns: all three emails are distinct strings, as are the addresses. - Each numbered row is one fuzzy threshold. The default sweep is 0.5, 0.75, 0.9, 0.95 and 0.99. At 0.5 and 0.75 the two email variants match, so one of three records is a redundant duplicate: a rate of 0.333. At 0.9 they no longer match.
- A rate of 0.333 on 3 records means
drop_duplicateswould remove exactly one record under that rule.
On larger data, use a sample instead of scanning everything, for a faster exploration. frac takes a fraction where the default of 1.0 uses every row:
By default, each column is profiled with fuzzy. To profile a column with a different similarity deduper, pass a dict instead of a list: keys are column labels, values are single-column threshold dedupers:
explore runs on the pandas, polars, modin and pyarrow backends. On dask, ray or pyspark, explore is not supported and raises a ValueError.
Missing Values
Real data has nulls. Liken does not drop null values and treats a missing value as None or any NaN. For the
similarity dedupers, missing values group with each other:
| Deduper | What happens to missing values |
|---|---|
exact |
Group with each other |
fuzzy |
Group with each other |
edit_distance |
Group with each other |
tfidf |
Group with each other at any given ngram; the vectoriser receives non-missing values only. |
lsh |
Same as tfidf. |
isna |
Matches exactly the missing values; ~isna matches exactly the non-missing values. |
num_range |
Never matches: a missing value is outside any interval, positive or negated (~num_range). |
| Other predicates | A missing value never satisfies the predicate, on either polarity — except positive isin, where a missing value matches iff None is listed in values. |
Two practical consequences are worth noting:
- Missing handling is backend-independent: the pandas-family backends convert NaN to null on the way into Arrow, while polars and pyarrow keep float NaN. Both are one missing class.
exacton compound columns collapses missing members:(None, "x")and(NaN, "x")share one key.jaccardexcludesNonevalues when building each record's set;cosinefills numeric NaNs with 0.- The empty string is a value, not a missing one.
str_lennever matches it, whatever the bounds; under~str_lenit matches, like every non-matching value.