dplyr dense_rank() in R: Rank With Ties Sharing Same Position
The dense_rank() function in dplyr ranks values where tied entries share the same rank, and the NEXT rank does NOT skip. The output covers a contiguous range 1, 2, 3, ... up to the number of distinct values.
dense_rank(c(10, 20, 20, 30)) # 1, 2, 2, 3 (no gap) min_rank(c(10, 20, 20, 30)) # 1, 2, 2, 4 (gap) dense_rank(desc(x)) # rank descending df |> mutate(level = dense_rank(score)) df |> group_by(g) |> mutate(level = dense_rank(score)) length(unique(x)) # max rank from dense_rank
Need explanation? Read on for examples and pitfalls.
What dense_rank() does in one sentence
dense_rank(x) returns the rank of each element where TIED values share the same rank and subsequent ranks DO NOT SKIP. For c(10, 20, 20, 30), the result is c(1, 2, 2, 3): distinct values get distinct, contiguous ranks.
This is the "dense" or "no-gap" ranking style. The maximum rank equals length(unique(x)).
Syntax
dense_rank(x). Use desc(x) for descending. NAs stay as NA in the output.
dense_rank is the right tool when you want to bucket values into ordered categories. Distinct values get distinct ranks; ties get the same rank. The result is a small contiguous integer range, perfect for category mapping.Five common patterns
1. Rank with ties sharing position
The two 80s share rank 3. The 90 (next distinct value) is rank 4, not 5.
2. Descending dense rank
3. Count distinct values via dense_rank
A useful identity: max(dense_rank(x)) always equals length(unique(x)) (excluding NAs).
4. Per-group dense rank
5. Bucket continuous values into ordered levels
price_level is now a small integer range (1 to 4) suitable as a categorical input.
dense_rank is the only rank function whose output forms a contiguous integer range. min_rank and row_number can leave gaps or break ties. dense_rank always covers 1..k where k is the number of distinct values. Useful for mapping continuous values to ordinal categories.dense_rank() vs min_rank() vs row_number() vs ntile()
Four ranking-style functions in dplyr.
| Function | Output for c(10, 20, 20, 30) | Best for |
|---|---|---|
dense_rank() |
1, 2, 2, 3 | Categorical encoding; no gaps |
min_rank() |
1, 2, 2, 4 | Standard rank with gaps |
row_number() |
1, 2, 3, 4 | Unique sequential IDs |
ntile(x, 3) |
1, 2, 2, 3 | Bin into n equal-sized groups |
When to use which:
dense_rankfor "ordinal level" or "tier" assignment.min_rankfor sports / standings.row_numberfor unique row IDs.ntilefor binning into quantiles.
A practical workflow
The "tier assignment" pattern is the dense_rank sweet spot.
A 5-star product and a 4.9-star product get tier 1 and 2; ties on the same rating share a tier. The maximum tier is the number of distinct ratings.
For per-category tiers:
Each category has its own contiguous tier range.
Common pitfalls
Pitfall 1: confusing dense_rank with min_rank. min_rank(c(10, 20, 20, 30)) is c(1, 2, 2, 4) (gap). dense_rank(c(10, 20, 20, 30)) is c(1, 2, 2, 3) (no gap). Pick based on whether downstream code expects gaps.
Pitfall 2: NA propagation. NAs stay as NA in the rank vector. Filter NAs first or set na.last = NA (not directly supported; use dense_rank(if_else(is.na(x), -Inf, x)) as a workaround).
dense_rank returns the SAME rank for ALL ties. If you need unique ranks even when values are equal, use row_number instead.Try it yourself
Try it: Encode the unique cyl values in mtcars as small integers 1, 2, 3 (lowest cyl = 1). Save to ex_cyl_level.
Click to reveal solution
Explanation: dense_rank(cyl) maps cyl 4 -> 1, cyl 6 -> 2, cyl 8 -> 3. Each unique cyl gets a contiguous integer level.
Related dplyr functions
After mastering dense_rank, look at:
min_rank(): gaps after tiesrow_number(): unique sequentialpercent_rank()/cume_dist(): percentile rankingsntile(): bin into n equal-sized groupsfactor(): convert dense ranks to factor levelsforcats::fct_inseq(): factor in numerical order
For converting dense ranks to factors with sensible levels, factor(dense_rank(x)) works.
FAQ
What does dense_rank do in dplyr?
dense_rank(x) ranks values where tied entries share the same rank, and subsequent ranks DO NOT skip. The output is a contiguous integer range 1..k.
What is the difference between dense_rank and min_rank?
dense_rank produces no gaps after ties (1, 2, 2, 3). min_rank leaves gaps (1, 2, 2, 4). dense_rank is useful when counting distinct rank levels matters.
How do I count the number of distinct ranks?
max(dense_rank(x)) returns the count of distinct values (ignoring NAs). Equivalent to length(unique(x[!is.na(x)])).
How do I rank descending with dense_rank?
dense_rank(desc(x)). Highest value gets rank 1.
Can dense_rank be used as a categorical encoder?
Yes. dense_rank(x) maps distinct values of x to a contiguous integer range, which is the ordinal-encoding pattern.