5

I would like to split a string at regular intervals. My question is virtually identical to this one: How to split a string into substrings of a given length? except that I have a column of strings in a data set instead of just one string.

Here is an example data set:

df = read.table(text = "
my.id   X1    
010101   1
010102   1
010103   1
010104   1
020101   1
020112   1
021701   0
021802   0
133301   0
133302   0  
241114   0
241215   0
", header = TRUE, colClasses=c('character', 'numeric'), stringsAsFactors = FALSE)

Here is the desired result. I would prefer to remove the leading zeroes, as shown:

desired.result = read.table(text = "
A1 A2 A3   X1
 1  1  1   1
 1  1  2   1
 1  1  3   1
 1  1  4   1
 2  1  1   1
 2  1 12   1
 2 17  1   0
 2 18  2   0
13 33  1   0
13 33  2   0
24 11 14   0
24 12 15   0
", header = TRUE, colClasses=c('numeric', 'numeric', 'numeric', 'numeric'), stringsAsFactors = FALSE)

Here is a loop that seems to come close and maybe I can use it. However, I am thinking that there is likely a more efficient way.

for(i in 1:nrow(df)) {
     print(substring(df$my.id[i], seq(1, 5, 2), seq(2, 6, 2)))
}

This apply statement does not work:

apply(df$my.id, 1,  function(x) substring(df$my.id[x], seq(1, 5, 2), seq(2, 6, 2))   )

Thank you for any suggestions. I prefer a solution in base R.

Community
  • 1
  • 1
Mark Miller
  • 12,483
  • 23
  • 78
  • 132

4 Answers4

10

I find that read.fwf applied to a textConnection is the most efficient and easy-to-understand of the various ways one could approach this. It has the advantage of the automatic class detection that is built into the read.* functions.

cbind( read.fwf(file=textConnection(df$my.id), 
              widths=c(2,2,2), col.names=paste0("A", 1:3)), 
     X1=df$X1)
#-----------
   A1 A2 A3 X1
1   1  1  1  1
2   1  1  2  1
3   1  1  3  1
4   1  1  4  1
5   2  1  1  1
6   2  1 12  1
7   2 17  1  0
8   2 18  2  0
9  13 33  1  0
10 13 33  2  0
11 24 11 14  0
12 24 12 15  0

(I believe I learned this from Gabor Grothendieck on Rhelp about 6 years ago.)

If you prefer a regex strategy then look at this which inserts a tab every two positions and runs it through read.table. Very compact:

read.table(text=gsub('(.{2})','\\1\t',df$my.id) )
#---------
   V1 V2 V3
1   1  1  1
2   1  1  2
3   1  1  3
4   1  1  4
5   2  1  1
6   2  1 12
7   2 17  1
8   2 18  2
9  13 33  1
10 13 33  2
11 24 11 14
12 24 12 15
IRTFM
  • 258,963
  • 21
  • 364
  • 487
3

You're almost there. Change your apply to sapply or vapply, and change what substring works on:

splt <- sapply(df$my.id, function(x) substring(x, seq(1, 5, 2), seq(2, 6, 2))   )
#this will produce the same thing
splt <- vapply(df$my.id, function(x) substring(x, seq(1, 5, 2), seq(2, 6, 2)),c("","","")   )
#     010101 010102 010103 010104 020101 020112 021701 021802 133301 133302 241114 241215
#[1,] "01"   "01"   "01"   "01"   "02"   "02"   "02"   "02"   "13"   "13"   "24"   "24"  
#[2,] "01"   "01"   "01"   "01"   "01"   "01"   "17"   "18"   "33"   "33"   "11"   "12"  
#[3,] "01"   "02"   "03"   "04"   "01"   "12"   "01"   "02"   "01"   "02"   "14"   "15"

You wanted to make these numeric. The matrix should also be transposed to work with the data frame. We can do both steps with:

splt <- apply(splt,1,as.numeric)
    # [,1] [,2] [,3]
 # [1,]    1    1    1
 # [2,]    1    1    2
 # [3,]    1    1    3
 # [4,]    1    1    4
 # [5,]    2    1    1
 # [6,]    2    1   12
 # [7,]    2   17    1
 # [8,]    2   18    2
 # [9,]   13   33    1
# [10,]   13   33    2
# [11,]   24   11   14
# [12,]   24   12   15

Now you need to put this back together with your old data frame. Possibly something like the following.

df <- cbind(splt,df)
#    1  2  3  my.id X1
#1   1  1  1 010101  1
#2   1  1  2 010102  1
#3   1  1  3 010103  1
#4   1  1  4 010104  1
#5   2  1  1 020101  1
#6   2  1 12 020112  1
#7   2 17  1 021701  0
#8   2 18  2 021802  0
#9  13 33  1 133301  0
#10 13 33  2 133302  0
#11 24 11 14 241114  0
#12 24 12 15 241215  0

You can change column names as necessary with something like names(df)[1:3] <- c("A1","A2","A3").

Blue Magister
  • 13,044
  • 5
  • 38
  • 56
2

Using gsub and some regular expression. I would do something like this (not very elegant but it do the job)

cbind(
as.numeric(gsub('([0-9]{2})([0-9]{2})([0-9]{2})','\\1',df$my.id)),
as.numeric(gsub('([0-9]{2})([0-9]{2})([0-9]{2})','\\2',df$my.id)),
as.numeric(gsub('([0-9]{2})([0-9]{2})([0-9]{2})','\\3',df$my.id)),
df$X1)

    [,1] [,2] [,3] [,4]
 [1,]    1    1    1    1
 [2,]    1    1    2    1
 [3,]    1    1    3    1
 [4,]    1    1    4    1
 [5,]    2    1    1    1
 [6,]    2    1   12    1
 [7,]    2   17    1    0
 [8,]    2   18    2    0
 [9,]   13   33    1    0
[10,]   13   33    2    0
[11,]   24   11   14    0
[12,]   24   12   15    0

EDIT

I said it is not very elegant , so I add @mnel proposition:

x <- gsub('([0-9]{2})([0-9]{2})([0-9]{2})','\\1-\\2-\\3',df$my.id)
do.call(rbind, lapply(strsplit(x,'-'), as.numeric)) 
agstudy
  • 119,832
  • 17
  • 199
  • 261
  • 2
    I'd suggest perhaps `x <- gsub('([0-9]{2})([0-9]{2})([0-9]{2})','\\1-\\2-\\3',df$my.id); do.call(rbind, lapply(strsplit(x,'-), as.numeric))` to avoid having to write out and perform the `regex` so many times. – mnel Feb 19 '13 at 01:11
  • Very nice! I added one keystroke: strsplit(x,'-') – Mark Miller Feb 19 '13 at 01:34
2

You can also use regex to extract each two-digit section.

I've used this in conjunction with str_extract_all from stringr

do.call(rbind,lapply(str_extract_all(as.character(df[['my.id']]), pattern = '[[:digit:]]{2}'), as.numeric))
mnel
  • 113,303
  • 27
  • 265
  • 254
  • If you want a base solution, you might be able to replace `str_extract_all` with `regmatches(gregexpr(pattern, x))` – sebastian-c Feb 19 '13 at 01:09