【问题标题】:Alternative to for() loop to compare very large data frame column entries to very large vector list替代 for() 循环将非常大的数据框列条目与非常大的向量列表进行比较
【发布时间】:2016-01-10 11:38:52
【问题描述】:

我正在尝试获取一个数据框profiles,其中包含一列email 地址,并添加一个由每个电子邮件地址的可注册域部分组成的新列domain

我单独创建了一个唯一registerable_domains 的向量,该过程过于复杂,无法针对数据框中的每一行运行,其结果是一个向量,该向量必然小于profiles 数据框。然后我检查registerable_domains 向量中的每个条目是否出现在profiles 数据帧中每个email 地址的末尾,并设置匹配的数据帧的domain 列条目。

下面的代码是可复制的数据,您可以复制粘贴并在 R 中执行,每行注释以解释其作用。

for() 循环正是我想做的事情:它在profiles 数据框的domain 列中创建适当的条目。问题是在这个例子中,profiles 数据框有 12 行,registerable_domains 向量有 8 个条目。在实际数据集中,profiles 数据框有大约 500,000 行,registerable_domains 向量有大约 110,000 个条目。结果,虽然for() 循环在小数据集上工作得很好,但对于非常大的数据集,我需要一种不同的方法(我估计这种方法需要大约 75 年才能在完整的数据集上完成! )。

非常感谢您帮助将这个 for() 循环转换为大型数据集的时间实用操作。我查看了许多其他线程,但找不到任何解决这种特殊情况的答案(尽管解决了许多其他类似但不同的情况)。谢谢!

# Data frame consisting of a column of 12 emails, and a column of 12 NA entries:

email <- c( "john@doe.com",
            "mary@smith.co.uk",
            "peter@microsoft.com",
            "jane@admins.microsoft.com",
            "luke@star.wars.com",
            "leia@star.wars.com",
            "yoda@masters.star.wars.com",
            "grandma@bletchly.ww2.wars.com",
            "searchfor@janedoe.com",
            "fan@mail.starwars.com",
            "city@toronto.ca",
            "area@toronto.canada.ca");

domain <- c(NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA);

profiles <- data.frame(email, domain);

profiles; # See what the initial data frame looks like

#                            email domain
# 1                   john@doe.com     NA
# 2               mary@smith.co.uk     NA
# 3            peter@microsoft.com     NA
# 4      jane@admins.microsoft.com     NA
# 5             luke@star.wars.com     NA
# 6             leia@star.wars.com     NA
# 7     yoda@masters.star.wars.com     NA
# 8  grandma@bletchly.ww2.wars.com     NA
# 9          searchfor@janedoe.com     NA
# 10         fan@mail.starwars.com     NA
# 11               city@toronto.ca     NA
# 12        area@toronto.canada.ca     NA

# Vector consisting of email addresses stripped to registerable domain component only, created through a separate process that is too complex to run on each row entry:

registerable_domains <- c(  "doe.com",
                            "smith.co.uk",
                            "microsoft.com",
                            "wars.com",
                            "janedoe.com",
                            "starwars.com",
                            "toronto.ca",
                            "canada.ca");

# Credit to Nick Kennedy for his help with this original solution (http://stackoverflow.com/users/4998761/nick-kennedy)

for (domains in registerable_domains) {                                             # Iterate through each of the registerable domains
    domains_pattern <- paste("[.@]", domains, "$", sep="");                         # Add regex characters to ensure that it's only the end part to deal with nested domain names
    found <- grepl(domains_pattern, profiles$email, ignore.case=TRUE, perl=TRUE);   # Grep for the current domain pattern in all of the emails and build a boolean table for entry locations
    profiles[which(found & is.na(profiles$domain)), "domain"] <- domains;           # Modify profile data table at TRUE entry locations not yet set
}

profiles; # Expected and desired outcome:

#                            email        domain
# 1                   john@doe.com       doe.com
# 2               mary@smith.co.uk   smith.co.uk
# 3            peter@microsoft.com microsoft.com
# 4      jane@admins.microsoft.com microsoft.com
# 5             luke@star.wars.com      wars.com
# 6             leia@star.wars.com      wars.com
# 7     yoda@masters.star.wars.com      wars.com
# 8  grandma@bletchly.ww2.wars.com      wars.com
# 9          searchfor@janedoe.com   janedoe.com
# 10         fan@mail.starwars.com  starwars.com
# 11               city@toronto.ca    toronto.ca
# 12        area@toronto.canada.ca     canada.ca

【问题讨论】:

  • 你可以尝试使用data.table的原位赋值运算符:=。您的代码在每次迭代时都会复制整个表,这就是它如此缓慢的原因之一。您也可以使用固定模式而不是 PCRE。你也可以试试看pmatch函数。

标签: regex r for-loop dataframe grepl


【解决方案1】:

这是使用dplyr的解决方案

library(dplyr)
person <- data_frame(Email = email) %>% 
  mutate(Domain = gsub("^.*@", "", Email)) # everything upto the last @
domain <- person %>% 
  select(Domain) %>% # select the Domain variable
  distinct() %>%  # keep only unique rows
  mutate(Original = Domain) # copy Domain into Original
extra <- domain %>% 
  mutate(Domain = gsub("^[[:alnum:]]*\\.", "", Domain)) %>% # remove all alphanumeric characters upto the first point and overwrite Domain
  filter(grepl("\\.", Domain)) # keep only observations where domain contains at least one point
while (nrow(extra) > 0){
  domain <- bind_rows(domain, extra) #add the rows from extra to domain
  extra <- extra %>% 
    mutate(Domain = gsub("^[[:alnum:]]*\\.", "", Domain)) %>% 
    filter(grepl("\\.", Domain))
}
register <- data_frame(Domain = registerable_domains)
register %>% 
  inner_join(domain, by = "Domain") %>% #join the two table on a common Domain
  inner_join(person, by = c("Original" = "Domain")) # join the resulting table to person where result.Original = person.Domain

【讨论】:

  • 这太棒了,@Thierry!它正是我需要的。我已经阅读了你的代码,我很确定我理解这些行的作用。但是,我能否麻烦您添加 cmets 来解释每行的作用,以便我可以确定我理解和学习并适应其他类似情况?非常感谢!我已经在我的源代码中记下了你的功劳。
  • 我已经添加了 cmets。看看dplyr 包中的小插曲。
  • 非常感谢@Thierry!这说明得很清楚。非常感谢!
【解决方案2】:

我认为你可以通过追求简单的结果并从你的 for 循环中删除一些易于矢量化的操作来显着减少你的时间。

profiles <- profiles %>% mutate(test_domains = sub(".*@", "", email))

很简单,只需为您提供一个新列即可使用,而无需在每次迭代中花费时间。

for (d in registerable_domains){
    profiles$domain[d == profiles$test_domains] <- d
}

将采用直接匹配,并且应该只为那些仍然具有NA 的行留下当前昂贵的循环,即,

profiles[is.na(profiles$domain)]

这将是一个适当的子集。我不知道它可以为您节省多少,我现在必须离开。我将回到这一点。感谢您提出一个写得很好的数据问题。

【讨论】:

  • 感谢您的建议,肖恩·梅汉。这确实降低了 for() 循环的复杂性,但它仍然像@liborm 提到的那样慢得不切实际,因为 for() 循环为每次迭代制作一个副本。
【解决方案3】:

不确定这是否有帮助,因为我完全改变了 for 循环的理念及其作用。另外,我没有意识到您是否真的需要可注册的域。但是,我的想法是使用这些域所具有的模式并将它们应用到您的电子邮件列表中,而不是使用可注册域的列表。

例如,如果域以comca 结尾,那么您保留这部分并且左边的内容,例如searchfor@janedoe.com 变为janedoe.com。如果域以uk 结尾,那么您需要这部分,还需要co 以及之前的内容。

如果您设法发现这些模式,您可以使用 if-else 规则创建一个简单的函数并执行类似的操作

x = c("luke@star.wars.com",
     "area@toronto.canada.ca",
     "mary@smith.co.uk")

dt = data.frame(x, stringsAsFactors = F)

dt

#                        x
# 1     luke@star.wars.com
# 2 area@toronto.canada.ca
# 3       mary@smith.co.uk

ff = function(x){
  x = strsplit(x, split = "[[:punct:]]")[[1]]

  ifelse(x[length(x)] %in% c("com","ca"),
         paste(x[(length(x)-1):length(x)], collapse = "."),
         paste(x[(length(x)-2):length(x)], collapse = "."))}

dt$v = sapply(dt$x, ff)

dt

#                        x           v
# 1     luke@star.wars.com    wars.com
# 2 area@toronto.canada.ca   canada.ca
# 3       mary@smith.co.uk smith.co.uk

【讨论】:

  • 感谢您的建议@AntoniosK。我确实需要可注册域部分,并且模式的识别是一个复杂的问题,随着新顶级域的添加而变化。我已经调整了一个单独的 PHP 脚本来为我完成这部分工作,它首先创建了向量。
猜你喜欢
  • 2017-05-02
  • 1970-01-01
  • 1970-01-01
  • 2016-12-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-05
  • 1970-01-01
相关资源
最近更新 更多