在数据框中对多列进行变异

19
我有一个数据集,看起来像这样。
bankname    bankid   year    totass    cash    bond    loans
Bank A      1        1881    244789    7250    20218   29513
Bank B      2        1881    195755    10243   185151  2800
Bank C      3        1881    107736    13357   177612  NA
Bank D      4        1881    170600    35000   20000   5000
Bank E      5        1881    3200000   351266  314012  NA

我想根据银行资产负债表计算一些比率。我希望数据集的格式如下所示。
bankname    bankid   year    totass    cash    bond    loans    CashtoAsset   BondtoAsset    LoanstoAsset
Bank A      1        1881    2447890   7250    202100  951300   0.002         0.082          0.388
Bank B      2        1881    195755    10243   185151  2800     0.052         0.945          0.014
Bank C      3        1881    107736    13357   177612  NA       0.123         1.648585431    NA
Bank D      4        1881    170600    35000   20000   5000     0.205         0.117          0.029
Bank E      5        1881    32000000  351266  314012  NA       0.0109        0.009          NA

这是复制数据的代码。
bankname <- c("Bank A","Bank B","Bank C","Bank D","Bank E")
bankid <- c( 1, 2,  3,  4,  5)
year<- c( 1881, 1881,   1881,   1881,   1881)
totass  <- c(244789,    195755, 107736, 170600, 32000000)
cash<-c(7250,10243,13357,35000,351266)
bond<-c(20218,185151,177612,20000,314012)
loans<-c(29513,2800,NA,5000,NA)
bankdata<-data.frame(bankname, bankid,year,totass, cash, bond, loans)

首先,我清除了资产负债表中的缺失值。
cols <- c("totass", "cash", "bond", "loans")
bankdata[cols][is.na(bankdata[cols])] <- 0

然后我计算比率
library(dplyr)
bankdata<-mutate(bankdata,CashtoAsset = cash/totass)
bankdata<-mutate(bankdata,BondtoAsset = bond/totass)
bankdata<-mutate(bankdata,loanstoAsset =loans/totass)

但是,我不想逐行计算所有这些比率,而是想一次性创建一个查找来完成这个任务。在Stata中,我会这样做:
foreach x of varlist cash bond loans {
by bankid: gen `x'toAsset = `x'/ totass
}

怎么做呢?

1
元注释:在翻译时,不必过于字面。在R中,基于数组的计算通常比Stata中的循环更有效。 (反之亦然:从其他语言转到Stata的新手经常尝试对观测值进行循环,但这很少需要。) - Nick Cox
我在这里简化了我的变量,但是在我的数据集中,我有超过20个资产类别,因此使用循环很有帮助。 - H Park
我对循环没有任何意见;同样,典型的R用户肯定对20列感到满意... - Nick Cox
@NickCox 像往常一样,非常感谢你的帮助。我从你这里学到了很多。 - H Park
@akrun 感谢您让我知道。从现在开始我会这样做的。 - H Park
6个回答

50

2019年3月18日更新

有一个变化。我们一直在使用.funs中的funs()(funs(name = f(.)),但这已经改变了(在dplyr 0.8.0以上)。现在我们使用list代替funslist(name = ~f(.)))。请参考以下新示例。

bankdata %>%
mutate_at(.funs = list(toAsset = ~./totass), .vars = vars(cash:loans))

bankdata %>%
mutate_at(.funs = list(toAsset = ~./totass), .vars = c("cash", "bond", "loans"))

bankdata %>%
mutate_at(.funs = list(toAsset = ~./totass), .vars = 5:7)

更新(截至2017年12月2日)

自从我回答了这个问题后,我意识到一些SO用户一直在查看这个答案。dplyr包已经发生了变化。因此,我留下以下更新。我希望这可以帮助一些R用户学习如何使用mutate_at()

mutate_each()现在已经弃用。您应该使用mutate_at()代替。您可以在.vars中指定要应用函数的列。一种方法是使用vars()。另一种方法是使用一个包含列名的字符向量,在其中您想要在.fun中应用自定义函数。另一种方法是通过数字指定列(例如,在此示例中使用5:7)。请注意,如果您使用某一列进行group_by(),则需要更改列位置的数字。请参考此问题

bankdata %>%
mutate_at(.funs = funs(toAsset = ./totass), .vars = vars(cash:loans))

bankdata %>%
mutate_at(.funs = funs(toAsset = ./totass), .vars = c("cash", "bond", "loans"))

bankdata %>%
mutate_at(.funs = funs(toAsset = ./totass), .vars = 5:7)

#  bankname bankid year   totass   cash   bond loans cash_toAsset bond_toAsset loans_toAsset
#1   Bank A      1 1881   244789   7250  20218 29513   0.02961734  0.082593581    0.12056506
#2   Bank B      2 1881   195755  10243 185151  2800   0.05232561  0.945830247    0.01430359
#3   Bank C      3 1881   107736  13357 177612    NA   0.12397899  1.648585431            NA
#4   Bank D      4 1881   170600  35000  20000  5000   0.20515826  0.117233294    0.02930832
#5   Bank E      5 1881 32000000 351266 314012    NA   0.01097706  0.009812875            NA

我故意在.fun自定义函数中将toAsset传递给它,因为这有助于我整理新的列名。以前我使用rename()。但是我认为现在使用gsub()来清理列名要容易得多。如果上述结果保存为out,你需要运行以下代码以删除列名中的_

names(out) <- gsub(names(out), pattern = "_", replacement = "")

原始回答

我认为你可以使用dplyr以这种方式节省一些输入。 缺点是您将覆盖现金,债券和贷款。

bankdata %>%
    group_by(bankname) %>%
    mutate_each(funs(whatever = ./totass), cash:loans)

#  bankname bankid year   totass       cash        bond      loans
#1   Bank A      1 1881   244789 0.02961734 0.082593581 0.12056506
#2   Bank B      2 1881   195755 0.05232561 0.945830247 0.01430359
#3   Bank C      3 1881   107736 0.12397899 1.648585431         NA
#4   Bank D      4 1881   170600 0.20515826 0.117233294 0.02930832
#5   Bank E      5 1881 32000000 0.01097706 0.009812875         NA

如果您希望达到预期的结果,我认为需要一些打字。重命名部分似乎是必须要做的。

bankdata %>%
    group_by(bankname) %>%
    summarise_each(funs(whatever = ./totass), cash:loans) %>%
    rename(cashtoAsset = cash, bondtoAsset = bond, loanstoAsset = loans) -> ana;
    ana %>%
    merge(bankdata,., by = "bankname")

#  bankname bankid year   totass   cash   bond loans cashtoAsset bondtoAsset loanstoAsset
#1   Bank A      1 1881   244789   7250  20218 29513  0.02961734 0.082593581   0.12056506
#2   Bank B      2 1881   195755  10243 185151  2800  0.05232561 0.945830247   0.01430359
#3   Bank C      3 1881   107736  13357 177612    NA  0.12397899 1.648585431           NA
#4   Bank D      4 1881   170600  35000  20000  5000  0.20515826 0.117233294   0.02930832
#5   Bank E      5 1881 32000000 351266 314012    NA  0.01097706 0.009812875           NA

嗨,我正在尝试这里发布的所有不同选项。当我尝试您的代码时,我收到了“错误:找不到对象'ana'”。您能向我解释发生了什么吗?谢谢。 - H Park
@HPark 我正在将输出分配给对象ana,并在管道过程中使用。如果这种方法对您不起作用,您可以执行以下操作:ana <- bankdata %>% group_by(bankname) %>% summarise_each(funs(whatever = ./totass), cash:loans) %>% rename(cashtoAsset = cash, bondtoAsset = bond, loanstoAsset = loans); ana %>% merge(bankdata,., by = "bank name") - jazzurro
2022年的提醒:以上所有内容都已被弃用;现在最先进的方法是 dplyr::mutate(dplyr::across(.cols = c(cash:loans), .fns=~.x/totass, .names="{.col}ToAsset"))。值得注意的变化是:不再需要使用 vars(),只需简单地使用 c() 即可;也不再需要覆盖列了。 - Lukas

4

Apply and cbind

cbind(bankdata,apply(bankdata[,5:7],2, function(x) x/bankdata$totass))
names(bankdata)[8:10] <- paste0(names(bankdata)[5:7], 'toAssest’)

> bankdata
  bankname bankid year   totass   cash   bond loans cashtoAssest bondtoAssest loanstoAssest
1   Bank A      1 1881   244789   7250  20218 29513   0.02961734  0.082593581    0.12056506
2   Bank B      2 1881   195755  10243 185151  2800   0.05232561  0.945830247    0.01430359
3   Bank C      3 1881   107736  13357 177612    NA   0.12397899  1.648585431            NA
4   Bank D      4 1881   170600  35000  20000  5000   0.20515826  0.117233294    0.02930832
5   Bank E      5 1881 32000000 351266 314012    NA   0.01097706  0.009812875            NA

在代码cbind(bankdata,apply(bankdata[,5:7],2, function(x) x/bankdata$totass))中,数字2代表什么意思? - Skurup
1
@Skurup,“2”是apply函数中的“margin”参数。它意味着该函数将应用于列向量。“1”则会将该函数应用于行。 (请查看?apply - hvollmeier

3
这里提供一个 data.table 的解决方案。
library(data.table)
setDT(bankdata)
bankdata[, paste0(names(bankdata)[5:7], "toAsset") := 
           lapply(.SD, function(x) x/totass), .SDcols=5:7]
bankdata
#    bankname bankid year   totass   cash   bond loans cashtoAsset bondtoAsset loanstoAsset
# 1:   Bank A      1 1881   244789   7250  20218 29513  0.02961734 0.082593581   0.12056506
# 2:   Bank B      2 1881   195755  10243 185151  2800  0.05232561 0.945830247   0.01430359
# 3:   Bank C      3 1881   107736  13357 177612     0  0.12397899 1.648585431   0.00000000
# 4:   Bank D      4 1881   170600  35000  20000  5000  0.20515826 0.117233294   0.02930832
# 5:   Bank E      5 1881 32000000 351266 314012     0  0.01097706 0.009812875   0.00000000

1
这是的一个很大的缺点之一:据我所知,没有直接的方法可以在编程时使用它,而不需要像可悲的这样的“hack”。

最简单的方法与Stata方法相同,但在R中进行字符串操作比在Stata(或任何其他脚本语言中)更加冗长。

for (x in c("cash", "bond", "loans")) {
  bankdata[sprintf("%stoAsset", x)] <- bankdata[x] / bankdata$totass  # or, equivalently, bankdata["totass"] for a consistent "look"
  ## can also replace `sprintf("%stoAsset", x)` with `paste0(c(x, "toAsset"))` or even `paste(x, "toAsset", collapse="") depending on what makes more sense to you.
}

为了让整个事情更像Stata,您可以像这样将整个内容包装在within中:
bankdata <- within(bankdata, for (x in c("cash", "bond", "loans")) {
  assign(x, get(x) / totass)
})

但这涉及到一些使用getassign函数的黑客技巧,通常情况下它们并不安全,尽管在您的情况下这可能并不是什么大问题。我不建议尝试类似的dplyr技巧,因为dplyr滥用了R的非标准评估功能,这可能更麻烦而不值得。对于一个更快、可能更优越的解决方案,请查看data.table包,它(我想)将允许您使用类似Stata的循环语法,但具有dplyr一样的速度。在CRAN上查看包vignette。

另外,你真的非常确定要将NA条目重新分配为0吗?


0
您可能把这个问题想得过于复杂了。只需要尝试一下这个方法,看看它是否能够得到你需要的结果。
bankdata$CashtoAsset <- bankdata$cash / bankdata$totass
bankdata$BondtoAsset <- bankdata$bond / bankdata$totass
bankdata$loantoAsset <- bankdata$loans / bankdata$totass
bankdata

产生这个:

bankname bankid year   totass   cash   bond loans CashtoAsset BondtoAsset loantoAsset 
1   Bank A      1 1881   244789   7250  20218 29513  0.02961734 0.082593581  0.12056506 
2   Bank B      2 1881   195755  10243 185151  2800  0.05232561 0.945830247  0.01430359 
3   Bank C      3 1881   107736  13357 177612     0  0.12397899 1.648585431  0.00000 
4   Bank D      4 1881   170600  35000  20000  5000  0.20515826 0.117233294  0.02930832 
5   Bank E      5 1881 32000000 351266 314012     0  0.01097706 0.009812875  0.00000000

这应该能让你朝着正确的方向开始。


0

尝试:

for(i in 5:7){
     bankdata[,(i+3)] = bankdata[,i]/bankdata[,4]
}
names(bankdata)[(5:7)+3] =  paste0(names(bankdata)[5:7], 'toAssest')

输出:

bankdata
  bankname bankid year   totass   cash   bond loans cashtoAssest bondtoAssest loanstoAssest
1   Bank A      1 1881   244789   7250  20218 29513   0.02961734  0.082593581    0.12056506
2   Bank B      2 1881   195755  10243 185151  2800   0.05232561  0.945830247    0.01430359
3   Bank C      3 1881   107736  13357 177612     0   0.12397899  1.648585431    0.00000000
4   Bank D      4 1881   170600  35000  20000  5000   0.20515826  0.117233294    0.02930832
5   Bank E      5 1881 32000000 351266 314012     0   0.01097706  0.009812875    0.00000000

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接