Dan Dan - 1 month ago 10
R Question

assign data.table column from function with column inputs

I have a data table with a few columns that I using as inputs into a phone validation function that I have created.

library(data.table)
dt <- data.table(ID = c(1:6),
phone = c("0412 345 789","0438 123 456",
"041 2345 543", "(02) 1234 5678",
"9876 1234", "04123456789"),
state = c("NSW","QLD","SA"),
country = c("AU"),
phone_countries = c("AU","AU","AU","AU,US","AU,US","AU,US"))

# ID phone state country phone_countries
# 1: 1 0412 345 789 NSW AU AU
# 2: 2 0438 123 456 QLD AU AU
# 3: 3 041 2345 543 SA AU AU
# 4: 4 (02) 1234 5678 NSW AU AU,US
# 5: 5 9876 1234 QLD AU AU,US
# 6: 6 04123456789 SA AU AU,US


The function
isValidPhone
looks like this (it is designed to validate phone numbers in a few different locations. I have omitted some of the regex's for brevity.)

isValidPhone <- function(phone, state, country, validation_countries) {

if (!(country %in% unlist(strsplit(validation_countries, ","))))
return(FALSE)

# remove whitespace, hyphens and brackets
phone_clean <- gsub("[[:space:]]|-|\\.|\\(|\\)", "", phone)

if (is.na(phone_clean) | phone_clean == '' | is.na(iconv(phone_clean, "", "ASCII")))
return(FALSE)

if (country == "AU") {
# append state area code if length is 8 digits
#print(paste("phone:", phone_clean, "state:", state))
if (nchar(phone_clean, "width") == 8)
if (state %in% c('ACT', 'NSW', 'QLD', 'VIC', 'TAS', 'SA', 'NT', 'WA'))
phone_clean <- switch (state,
'ACT' = paste0("02",phone_clean),
'NSW' = paste0("02",phone_clean),
'QLD' = paste0("07",phone_clean),
'VIC' = paste0("03",phone_clean),
'TAS' = paste0("03",phone_clean),
'SA' = paste0("08",phone_clean),
'NT' = paste0("08",phone_clean),
'WA' = paste0("08",phone_clean))

if (nchar(phone_clean, "width") == 9)
if(substr(phone_clean,1,1) %in% c(2:4,7,8))
phone_clean <- paste0("0", phone_clean)

return(grepl("^(?:\\+?61|0)[23478](?:[ -]?[0-9]){8}$",
as.character(phone_clean), ignore.case=TRUE))
}
}


I am assigning a field in my
data.table
dt
called
validphone


dt[, validphone := isValidPhone(phone, state, country, phone_countries), by = 1:nrow(dt)]

# ID phone state country phone_countries validphone
# 1: 1 0412 345 789 NSW AU AU TRUE
# 2: 2 0438 123 456 QLD AU AU TRUE
# 3: 3 041 2345 543 SA AU AU TRUE
# 4: 4 (02) 1234 5678 NSW AU AU,US TRUE
# 5: 5 9876 1234 QLD AU AU,US TRUE
# 6: 6 04123456789 SA AU AU,US FALSE


Unfortunately I am having to use
by = 1:nrow(dt)
in its current guise as if I don't do that it passes in the full column data into the parameters which causes problems. This leads to a LOT of function calls on my real data set (~300K) and poor performance.

I did read that it would be better to use a vectorised function, however it is unclear to me how I can do this.

Is there a more efficient way to achieve my desired outcome?

Answer

There is some re-engineering needed to be able to use your function on vectors:

Mainly replace the if(...) return(FALSE) by assigning FALSE on filtered rows and evaluate them in reverse order (last word to first return => last word to last assignment)

The switch also needs to be replaced by ifelse.

You end up with something like this:

isValidPhone <- function(phone, state, country, validation_countries) {
  phone_clean <- gsub("[[:space:]]|-|\\.|\\(|\\)", "", phone)

  AddArea <- country == "AU" & nchar(phone_clean) == 8 & 
    state %in% c('ACT', 'NSW', 'QLD', 'VIC', 'TAS', 'SA', 'NT', 'WA')
  phone_clean[AddArea] <- ifelse(state[AddArea]%in%c('ACT','NSW'),
                                 paste0("02",phone_clean[AddArea]),
                                 ifelse(state[AddArea]%in%c('VIC','TAS'),
                                        paste0("03",phone_clean[AddArea]),
                                        ifelse(state[AddArea]%in%c('SA','NT', 'WA'),
                                               paste0("08",phone_clean[AddArea]),
                                               paste0("02",phone_clean[AddArea]))))

  AddZero <- nchar(phone_clean) == 9 & substr(phone_clean,1,1) %in% c(2:4,7,8)
  phone_clean[AddZero] <- paste0("0", phone_clean[AddZero])

  ans <- grepl("^(?:\\+?61|0)[23478](?:[ -]?[0-9]){8}$",
                       as.character(phone_clean), ignore.case=TRUE)

  ans[(!(country %in% unlist(strsplit(validation_countries, ",")))) |
      is.na(phone_clean) | phone_clean == '' | 
      is.na(iconv(phone_clean, "", "ASCII"))] <- FALSE
  return(ans)
}
Comments