Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Clustering rows by group based on column value

Tags:

r

dplyr

seq

I have the following:

df <- data.frame(ID = c(1,1,1,1,1,1,1,1,1,1,2,2,2),
             Obs = c(0,1, 1, 0, 1,0,0, 1, 1, 1, 0,0,1))

And I want this:

df <- data.frame(ID = c(1,1,1,1,1,1,1,1,1,1,2,2,2),
             Obs = c(0,1, 1, 0, 1,0,0, 1, 1, 1, 0,0,1),
             Cluster = c(0,1,1,1,2,2,2,3,3,3,0,0,1))

How can I obtain 'Cluster' column in which I have to sequence the number of 1 until the first 0 appears, with dplyr?

Consecutive 0's have to maintain the value until a new one appears.

EDIT

How can I do that, with many columns?

Suppose that I have 99 obs columns, and I would like to create 99 clusters, one for each column. Like this:

df <- data.frame(ID = c(1,1,1,1,1,1,1,1,1,1,2,2,2),
Obs1 = c(0,1, 1, 0, 1,0,0, 1, 1, 1, 0,0,1),
Obs2 = c(0,0, 0, 1, 1,1,0, 1, 0, 1, 0,0,1),
ClusterObs1 = c(0,1,1,1,2,2,2,3,3,3,0,0,1),
ClusterObs2 = c(0,0,0,1,1,1,1,2,2,3,0,0,1))
like image 298
torakxkz Avatar asked Jan 03 '23 05:01

torakxkz


2 Answers

Here's an option using rle:

df %>% 
  group_by(ID) %>% 
  mutate(clust = with(rle(Obs), rep(cumsum(values == 1), lengths)))
# # A tibble: 13 x 4
# # Groups:   ID [2]
# ID   Obs Cluster clust
# <dbl> <dbl>   <dbl> <int>
# 1    1.    0.      0.     0
# 2    1.    1.      1.     1
# 3    1.    1.      1.     1
# 4    1.    0.      1.     1
# 5    1.    1.      2.     2
# 6    1.    0.      2.     2
# 7    1.    0.      2.     2
# 8    1.    1.      3.     3
# 9    1.    1.      3.     3
# 10    1.    1.      3.     3
# 11    2.    0.      0.     0
# 12    2.    0.      0.     0
# 13    2.    1.      1.     1

Here's the main part of it:

rle(df$Obs)
#Run Length Encoding
#  lengths: int [1:8] 1 2 1 1 2 3 2 1
#  values : num [1:8] 0 1 0 1 0 1 0 1

This tells you how long each stretch of 1s or 0s was in the Obs-column (I ignore the ID-grouping for now).

What we need now, is to count cumulatively how many times there were strectches of 1s and to do that we simply cumsum where the values are 1:

with(rle(df$Obs), cumsum(values == 1))
#[1] 0 1 1 2 2 3 3 4

So far so good, now we need to repeat those values as many times as those stretches were long, hence we use rep and the lengths information from rle:

with(rle(df$Obs), rep(cumsum(values == 1), lengths))
# [1] 0 1 1 1 2 2 2 3 3 3 3 3 4

Finally, we do this by group of ID.


If you need to create several cluster-columns for different obs-columns, you can easily do it as follows:

df %>% 
  group_by(ID) %>% 
  mutate_at(vars(starts_with("Obs")), 
            funs(cluster= with(rle(.), rep(cumsum(values == 1), lengths))))

# # A tibble: 13 x 7
# # Groups:   ID [2]
# ID  Obs1  Obs2 ClusterObs1 ClusterObs2 Obs1_cluster Obs2_cluster
# <dbl> <dbl> <dbl>       <dbl>       <dbl>        <int>        <int>
# 1    1.    0.    0.          0.          0.            0            0
# 2    1.    1.    0.          1.          0.            1            0
# 3    1.    1.    0.          1.          0.            1            0
# 4    1.    0.    1.          1.          1.            1            1
# 5    1.    1.    1.          2.          1.            2            1
# 6    1.    0.    1.          2.          1.            2            1
# 7    1.    0.    0.          2.          1.            2            1
# 8    1.    1.    1.          3.          2.            3            2
# 9    1.    1.    0.          3.          2.            3            2
# 10    1.    1.    1.          3.          3.            3            3
# 11    2.    0.    0.          0.          0.            0            0
# 12    2.    0.    0.          0.          0.            0            0
# 13    2.    1.    1.          1.          1.            1            1

where df is:

df <- data.frame(ID = c(1,1,1,1,1,1,1,1,1,1,2,2,2), Obs1 = c(0,1, 1, 0, 1,0,0, 1, 1, 1, 0,0,1), Obs2 = c(0,0, 0, 1, 1,1,0, 1, 0, 1, 0,0,1), ClusterObs1 = c(0,1,1,1,2,2,2,3,3,3,0,0,1), ClusterObs2 = c(0,0,0,1,1,1,1,2,2,3,0,0,1))
like image 187
talat Avatar answered Jan 17 '23 08:01

talat


This is such a fun problem so here comes a data.table solution:

# Packages used
library(data.table)
library(magrittr)

# Setup
setDT(df)
df[, Obs := as.integer(Obs)]

# Calculations
df[, Cluster := cumsum(!Obs), by = ID] %>%
  .[, Cluster := Cluster - rowid(Obs) * !Obs, by = rleid(Obs)] %>%
  .[, Cluster := frank(Cluster, ties.method = "dense") - 1L, by = ID]

df
    ID Obs Cluster
 1:  1   0       0
 2:  1   1       1
 3:  1   1       1
 4:  1   0       1
 5:  1   1       2
 6:  1   0       2
 7:  1   0       2
 8:  1   1       3
 9:  1   1       3
10:  1   1       3
11:  2   0       0
12:  2   0       0
13:  2   1       1
like image 37
sindri_baldur Avatar answered Jan 17 '23 07:01

sindri_baldur