6

I have a numeric vector that I want to convert to five numeric levels. I can get the five levels using cut

dx <- data.frame(x=1:100)
dx$cut <- cut(dx$x,5)

But I am now having problems extracting the lower and upper boundaries of the levels. So for example (0.901,20.8] would be 0.901 in dx$min and 20.8 in dx$max.

I tried:

dx$min <- pmin(dx$cut)
dx$max <- pmax(dx$cut)
dx

But this does not work.

Ronak Shah
  • 377,200
  • 20
  • 156
  • 213
adam.888
  • 7,686
  • 17
  • 70
  • 105
  • `dx$cut` is a factor variable. You would need to split / extract the numbers from it to get numerical values – talat Sep 08 '16 at 10:11

3 Answers3

10

you can try splitting the labels (converted to character beforehand and modified to suppress the punctuation except , and .) according to the comma and then create 2 columns:

min_max <- unlist(strsplit(gsub("(?![,.])[[:punct:]]", "", as.character(dx$cut), perl=TRUE), ",")) # here, the regex ask to replace every punctuation mark except a . or a , by an empty string

dx$min <- min_max[seq(1, length(min_max), by=2)]
dx$max <- min_max[seq(2, length(min_max), by=2)]

head(dx)
#  x          cut   min  max
#1 1 (0.901,20.8] 0.901 20.8
#2 2 (0.901,20.8] 0.901 20.8
#3 3 (0.901,20.8] 0.901 20.8
#4 4 (0.901,20.8] 0.901 20.8
#5 5 (0.901,20.8] 0.901 20.8
#6 6 (0.901,20.8] 0.901 20.8
Cath
  • 23,906
  • 5
  • 52
  • 86
  • 2
    I find that surprising that there is not a kind of default way to do that, this is something I often end up doing and I always need to use some hack like the one given here, maybe I'm not looking for the right keywords... – Simon C. Nov 20 '20 at 16:00
  • 1
    @SimonC. I guess you might get the breaks from your data using `seq` and `range` of your data, defining `length.out` as 1+number of factors in your `cut` call. (I usually use `cut` with predefined breaks so I don't encounter this problem) – Cath Nov 20 '20 at 16:07
  • For anyone who might be interested on how to implement @Cath's answer, you could do: `breaks=seq(from=range(my.x)[1]-1, to=range(my.x)[2], length.out=1+my.n.factors)`. I subtract one at `from` because `cut`'s default doesn't start at the minimum. It doesn't start at `min-1` either, but it was the easiest way for me. – Andres Silva Apr 29 '21 at 22:11
8

Below is tidyverse style solution.

library(tidyverse)

tibble(x = seq(-1000, 1000, length.out = 10),
       x_cut = cut(x, 5)) %>% 
  mutate(x_tmp = str_sub(x_cut, 2, -2)) %>% 
  separate(x_tmp, c("min", "max"), sep = ",") %>% 
  mutate_at(c("min", "max"), as.double)
#> # A tibble: 10 x 4
#>         x x_cut           min   max
#>     <dbl> <fct>         <dbl> <dbl>
#>  1 -1000  (-1e+03,-600] -1000  -600
#>  2  -778. (-1e+03,-600] -1000  -600
#>  3  -556. (-600,-200]    -600  -200
#>  4  -333. (-600,-200]    -600  -200
#>  5  -111. (-200,200]     -200   200
#>  6   111. (-200,200]     -200   200
#>  7   333. (200,600]       200   600
#>  8   556. (200,600]       200   600
#>  9   778. (600,1e+03]     600  1000
#> 10  1000  (600,1e+03]     600  1000

Created on 2019-01-10 by the reprex package (v0.2.1)

Bryan Shalloway
  • 748
  • 7
  • 15
0

While we are at it, a very similar idea using stringr::str_sub and split, but applied to a data.frame using across()

clean_cut_labels <- function(cut_labels){
  mat <- stringr::str_split(string = stringr::str_sub(cut_labels, start = 2, end = -2), 
                     pattern = ",",
                     simplify = T,
                     n = 2)
  mat <- apply(mat, 2, as.numeric)
  colnames(mat) <- c("low", "high")
  return(mat)
}

mtcars %>% 
  mutate(cd = cut(disp, 5)) %>% 
  mutate(across(cd, clean_cut_labels))

                     mpg cyl  disp  hp drat    wt  qsec vs am gear carb cd.low cd.high
Mazda RX4           21.0   6 160.0 110 3.90 2.620 16.46  0  1    4    4  151.0   231.0
Mazda RX4 Wag       21.0   6 160.0 110 3.90 2.875 17.02  0  1    4    4  151.0   231.0
Datsun 710          22.8   4 108.0  93 3.85 2.320 18.61  1  1    4    1   70.7   151.0
Hornet 4 Drive      21.4   6 258.0 110 3.08 3.215 19.44  1  0    3    1  231.0   312.0
Hornet Sportabout   18.7   8 360.0 175 3.15 3.440 17.02  0  0    3    2  312.0   392.0
Valiant             18.1   6 225.0 105 2.76 3.460 20.22  1  0    3    1  151.0   231.0
Duster 360          14.3   8 360.0 245 3.21 3.570 15.84  0  0    3    4  312.0   392.0
Merc 240D           24.4   4 146.7  62 3.69 3.190 20.00  1  0    4    2   70.7   151.0
Merc 230            22.8   4 140.8  95 3.92 3.150 22.90  1  0    4    2   70.7   151.0
Merc 280            19.2   6 167.6 123 3.92 3.440 18.30  1  0    4    4  151.0   231.0
Merc 280C           17.8   6 167.6 123 3.92 3.440 18.90  1  0    4    4  151.0   231.0
Merc 450SE          16.4   8 275.8 180 3.07 4.070 17.40  0  0    3    3  231.0   312.0
Merc 450SL          17.3   8 275.8 180 3.07 3.730 17.60  0  0    3    3  231.0   312.0
Merc 450SLC         15.2   8 275.8 180 3.07 3.780 18.00  0  0    3    3  231.0   312.0
Cadillac Fleetwood  10.4   8 472.0 205 2.93 5.250 17.98  0  0    3    4  392.0   472.0
Lincoln Continental 10.4   8 460.0 215 3.00 5.424 17.82  0  0    3    4  392.0   472.0
Chrysler Imperial   14.7   8 440.0 230 3.23 5.345 17.42  0  0    3    4  392.0   472.0
Fiat 128            32.4   4  78.7  66 4.08 2.200 19.47  1  1    4    1   70.7   151.0
Honda Civic         30.4   4  75.7  52 4.93 1.615 18.52  1  1    4    2   70.7   151.0
Toyota Corolla      33.9   4  71.1  65 4.22 1.835 19.90  1  1    4    1   70.7   151.0
Toyota Corona       21.5   4 120.1  97 3.70 2.465 20.01  1  0    3    1   70.7   151.0
Dodge Challenger    15.5   8 318.0 150 2.76 3.520 16.87  0  0    3    2  312.0   392.0
AMC Javelin         15.2   8 304.0 150 3.15 3.435 17.30  0  0    3    2  231.0   312.0
Camaro Z28          13.3   8 350.0 245 3.73 3.840 15.41  0  0    3    4  312.0   392.0
Pontiac Firebird    19.2   8 400.0 175 3.08 3.845 17.05  0  0    3    2  392.0   472.0
Fiat X1-9           27.3   4  79.0  66 4.08 1.935 18.90  1  1    4    1   70.7   151.0
Porsche 914-2       26.0   4 120.3  91 4.43 2.140 16.70  0  1    5    2   70.7   151.0
Lotus Europa        30.4   4  95.1 113 3.77 1.513 16.90  1  1    5    2   70.7   151.0
Ford Pantera L      15.8   8 351.0 264 4.22 3.170 14.50  0  1    5    4  312.0   392.0
Ferrari Dino        19.7   6 145.0 175 3.62 2.770 15.50  0  1    5    6   70.7   151.0
Maserati Bora       15.0   8 301.0 335 3.54 3.570 14.60  0  1    5    8  231.0   312.0
Volvo 142E          21.4   4 121.0 109 4.11 2.780 18.60  1  1    4    2   70.7   151.0

A downside of this approach is that it is likely to need a renaming strategy which is dependent on whether you are mutating a data.frame or a tibble

> mtcars %>% as_tibble() %>% 
+     mutate(cd = cut(disp, 5)) %>% 
+     mutate(across(cd, clean_cut_labels))
# A tibble: 32 × 12
     mpg   cyl  disp    hp  drat    wt  qsec    vs    am  gear  carb cd[,"low"] [,"high"]
   <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>      <dbl>     <dbl>
 1  21       6  160    110  3.9   2.62  16.5     0     1     4     4      151         231
 2  21       6  160    110  3.9   2.88  17.0     0     1     4     4      151         231
 3  22.8     4  108     93  3.85  2.32  18.6     1     1     4     1       70.7       151
 4  21.4     6  258    110  3.08  3.22  19.4     1     0     3     1      231         312
 5  18.7     8  360    175  3.15  3.44  17.0     0     0     3     2      312         392
 6  18.1     6  225    105  2.76  3.46  20.2     1     0     3     1      151         231
 7  14.3     8  360    245  3.21  3.57  15.8     0     0     3     4      312         392
 8  24.4     4  147.    62  3.69  3.19  20       1     0     4     2       70.7       151
 9  22.8     4  141.    95  3.92  3.15  22.9     1     0     4     2       70.7       151
10  19.2     6  168.   123  3.92  3.44  18.3     1     0     4     4      151         231
# … with 22 more rows

It's better to use it like this, coercing to tibble

mtcars %>% as_tibble() %>% 
         mutate(cd = cut(disp, 5)) %>% 
         mutate(clean_cut_labels(cd) %>% as_tibble())
Matias Andina
  • 4,029
  • 4
  • 26
  • 58