Remove Duplicates in R: duplicated, unique & distinct
Find and drop duplicate rows of a data frame — base R and dplyr, same result
Programming
Find and remove duplicate rows in R with base-R duplicated() and unique(), or dplyr distinct(). Remove duplicates across all columns or by a single key column, keep the first or last occurrence, and count how many duplicates you have — both engines, side by side.
Published
June 26, 2026
Modified
July 7, 2026
TipKey takeaways
Find duplicates: duplicated(df) returns a TRUE/FALSE vector flagging each repeated row.
Remove them — base R: unique(df) or df[!duplicated(df), ]; dplyr: distinct(df).
By one column:df[!duplicated(df$id), ] (base R) or distinct(df, id, .keep_all = TRUE) (dplyr).
duplicated() keeps the first occurrence; pass fromLast = TRUE to keep the last instead.
The base-R and dplyr routes give the same rows — pick the one that fits your workflow.
Introduction
You merged two data files, scraped a table, or appended new records, and now some rows appear more than once. Duplicate rows inflate counts, bias summaries, and break joins, so removing them is a routine first cleanup step.
There are two routes, and you’ll see both in real code:
base R — duplicated() flags repeats, unique() drops them; no package needed.
dplyr — distinct() keeps the unique rows in one readable call.
This lesson shows both, side by side, for finding duplicates, removing them across all columns, removing them by a key column, and choosing which copy to keep. The two give the same result.
We’ll build a small data frame with deliberate duplicates so the output is easy to follow:
id group score
1 1 a 10
2 2 b 20
3 2 b 20
4 3 c 30
5 3 c 30
6 3 d 40
Rows 2–3 are identical, and so are rows 4–5. Row 6 shares id 3 but has a different group and score.
Find duplicates
Before removing anything, look at what’s duplicated. duplicated() returns a logical vector: TRUE for every row that is a repeat of an earlier one.
duplicated(df)
[1] FALSE FALSE TRUE FALSE TRUE FALSE
The first time a row appears it’s FALSE; the second (and later) time it’s TRUE. Rows 3 and 5 are flagged because they repeat rows 2 and 4.
To see the duplicated rows, subset with that vector:
df[duplicated(df), ]
id group score
3 2 b 20
5 3 c 30
To count how many duplicates you have, sum the logical vector — TRUE counts as 1:
sum(duplicated(df))
[1] 2
Two duplicate rows. table() is handy when you want the count per repeated value of a single column:
table(df$id)[table(df$id) >1]
2 3
2 3
id 2 appears twice and id 3 appears three times.
Remove duplicate rows (all columns)
A row counts as a duplicate when every column matches an earlier row.
base R — unique() or !duplicated()
unique() is the most direct tool: it returns the data frame with duplicate rows removed.
unique(df)
id group score
1 1 a 10
2 2 b 20
4 3 c 30
6 3 d 40
df[!duplicated(df), ] does the same thing — keep the rows that are not flagged as duplicates. The ! negates the logical vector:
df[!duplicated(df), ]
id group score
1 1 a 10
2 2 b 20
4 3 c 30
6 3 d 40
Both drop rows 3 and 5 and keep the first copy of each. unique() is the shorthand; !duplicated() is the pattern to reach for when you want to dedupe by some of the columns (next section).
dplyr — distinct()
distinct() keeps the unique rows. With no column arguments it looks at all columns:
library(dplyr)distinct(df)
id group score
1 1 a 10
2 2 b 20
3 3 c 30
4 3 d 40
Same four rows. distinct() reads cleanly and slots into a pipeline.
Remove duplicates by one column
Often “duplicate” means the same id, even if other columns differ. Here you choose which column(s) define a duplicate.
base R — !duplicated() on the key column
Apply duplicated() to the key column only, then subset:
df[!duplicated(df$id), ]
id group score
1 1 a 10
2 2 b 20
4 3 c 30
This keeps the first row for each id — note that id 3 keeps row 4 (groupc), and rows 5 and 6 are dropped even though row 6 had different values. Deduping by a key always throws away information from the dropped rows, so be sure the key is what you mean.
dplyr — distinct(.keep_all = TRUE)
Name the key column(s) in distinct(). By default distinct() returns only the columns you name, so add .keep_all = TRUE to keep all the other columns of the first matching row:
library(dplyr)distinct(df, id, .keep_all =TRUE)
id group score
1 1 a 10
2 2 b 20
3 3 c 30
Same result as the base-R route — one row per id, keeping the first occurrence and all its columns.
Keep the first or the last duplicate
By default both engines keep the first occurrence of each duplicate. To keep the last instead, flag duplicates from the end with fromLast = TRUE:
df[!duplicated(df$id, fromLast =TRUE), ]
id group score
1 1 a 10
3 2 b 20
6 3 d 40
Now id 3 keeps row 6 (groupd) — the last row for that id — instead of the first. Use this when the most recent record is the one you want to keep. In dplyr, sort the data so the row you want comes first, then distinct().
Try it live
The blocks above ran at build time. Edit this one and press Run to remove duplicates on your own — it executes in your browser via webR (no install).
🟢 With an AI agent
Paste a messy data frame with repeated records and ask Prova“remove the duplicate rows, keeping the most recent per customer” — it writes the distinct() or duplicated(fromLast = TRUE) call, and because the runtime is right here you run it to confirm the right rows survived on your data. Ask Prova →
Working in Python? A pandas drop_duplicates guide is coming to the Python series.
Common issues
Duplicates aren’t removed even though rows “look” the same. A row is only a duplicate when every column matches exactly. Trailing spaces ("a" vs "a "), different capitalisation ("A" vs "a"), or a stray factor level all make rows distinct. Clean the offending column first (e.g. trimws(), tolower()), then dedupe.
Numbers that print the same but aren’t equal. Floating-point values computed different ways (0.1 + 0.2 vs 0.3) print identically but differ at the 16th decimal, so duplicated() treats them as distinct. Round the key column with round(x, 6) before deduping if “near-equal” should count as a duplicate.
distinct(df, id) dropped my other columns. That’s the default — distinct() with named columns returns only those columns. Add .keep_all = TRUE to keep the rest of each first matching row.
Frequently asked questions
NoteHow do I remove duplicate rows in R?
Use base-R unique(df) or df[!duplicated(df), ], or dplyr distinct(df). All three keep the first copy of each fully-duplicated row and drop the rest, giving the same result.
NoteHow do I find duplicates in R without removing them?
duplicated(df) returns a TRUE/FALSE vector flagging each repeated row. Subset with it — df[duplicated(df), ] — to see the duplicates, or sum(duplicated(df)) to count them.
NoteHow do I remove duplicates based on one column in R?
Apply the test to that column: df[!duplicated(df$id), ] (base R) or distinct(df, id, .keep_all = TRUE) (dplyr). Both keep the first row for each value of the key column and all its other columns.
NoteWhat’s the difference between unique() and distinct()?
They produce the same rows for whole-row deduplication. unique() is base R and needs no package; distinct() is dplyr, reads clearly, and lets you dedupe by named columns with .keep_all = TRUE. Use whichever fits your code base.
NoteHow do I keep the last duplicate instead of the first?
Pass fromLast = TRUE to duplicated(): df[!duplicated(df$id, fromLast = TRUE), ] keeps the last row for each key. In dplyr, sort so the row you want comes first, then call distinct().
Test your understanding
ImportantExercise: dedupe two ways
The data frame below has fully-duplicated rows and repeated id values. First remove only the rows that are duplicated across all columns (base R), then keep just one row per id with all columns (dplyr) — and check how many rows each leaves.
# 1. Negate duplicated() over the whole frame: !duplicated(orders)# 2. Name the key column and keep the rest: id, .keep_all = TRUE
NoteQuick check: which keeps the last record per id?
You have a data frame df with a column id and want one row per id, keeping the last occurrence. Which is correct?
A. df[!duplicated(df$id), ] B. df[!duplicated(df$id, fromLast = TRUE), ] C. distinct(df, id)
Show answer
B.fromLast = TRUE flags duplicates from the bottom up, so the last row for each id is the one kept. A keeps the first occurrence; C keeps the first occurrence and drops every column except id (you’d need .keep_all = TRUE).
Conclusion
Removing duplicates comes down to two equivalent toolsets: base-R unique() / df[!duplicated(df), ] or dplyr distinct(). Use duplicated() to find and count repeats first, dedupe by a key column with df[!duplicated(df$key), ] or distinct(df, key, .keep_all = TRUE), and reach for fromLast = TRUE when the last record is the one to keep.
This lesson is reproducible: every result on this page was produced by the code shown — edit any block and press Run to reproduce it in your browser. The runtime is the judge.