lm()和predict.lm()的奇怪行为,取决于是否使用显式命名空间访问器

8
我对R语言中lm函数和相关的predict.lm函数的某些异常行为感兴趣。基础包splines提供了bs函数来生成b样条展开式,然后可以使用多功能线性模型拟合函数lm来拟合样条模型。 lmpredict.lm函数具有许多内置便利功能,利用公式和项。如果将bs()调用嵌套在lm调用中,则用户可以向predict提供单变量数据,这些数据将自动扩展为适当的b样条基础。然后像往常一样预测这个扩展的数据矩阵。
library(splines)

x <- sort(runif(50, 0, 10))
y <- x^2

splineModel <- lm(y ~ bs(x, y, degree = 3, knots = c(3, 6)))

newData <- data.frame(x = 4)
prediction <- predict(splineModel, newData) # 16

plot(x, y)
lines(x, splineModel$fitted.values, col = 'blue3')
points(newData$x, prediction, pch = 3, cex = 3, col = 'red3')
legend("topleft", legend = c("Data", "Fitted Values", "Predicted Value"),
       pch = c(1, NA, 3), col = c('black', 'blue3', 'red3'), lty = c(NA, 1, NA))

正如我们所看到的,这个工作完美:

enter image description here

当使用::运算符明确指示bs函数从splines包的命名空间中导出时,奇怪的事情发生了。以下代码片段除了这个更改外完全相同:
library(splines)

x <- sort(runif(50, 0, 10))
y <- x^2

splineModel <- lm(y ~ splines::bs(x, y, degree = 3, knots = c(3, 6)))

newData <- data.frame(x = 4) 
prediction <- predict(splineModel, newData) # 6.40171

plot(x, y)
lines(x, splineModel$fitted.values, col = 'blue3')
points(newData$x, prediction, pch = 3, cex = 3, col = 'red3')
legend("topleft", legend = c("Data", "Fitted Values", "Predicted Value"),
       pch = c(1, NA, 3), col = c('black', 'blue3', 'red3'), lty = c(NA, 1, NA))

enter image description here

如果第一个片段中未使用library附加splines软件包,则第二个片段将产生完全相同的结果。我无法想象其他情况下在已加载软件包上使用::运算符会改变程序行为。
使用来自splines的其他函数(如自然样条基础实现ns)会导致相同的行为。有趣的是,两种情况下的“y hat”或拟合值都是合理的,并且相互匹配。就我所知,除了属性名称之外,拟合模型对象是相同的。
我一直无法确定这种行为的来源。虽然这可能读起来像一个错误报告,但我的问题是:
1.为什么会发生这种情况?我一直在尝试跟进predict.lm,但无法确定发散发生的位置。
2.这是否是某种打算中的行为,如果是,我可以在哪里了解更多信息?

另一个奇怪的事情是,如果您查看每个模型的系数,它们是相同的,但预测却不同。顺便说一下,您不应该两次创建相同的数据,因为每次数据都会有所不同(除非每次设置相同的种子)。在这里没有区别,因为无论哪种方式数据都是完全确定性的,导致相同的模型输出,但最好设置一个种子并仅创建一次数据。 - eipi10
你是对的,设定一个种子或重复使用数据会是更好的做法。但我想强调第二个片段是自包含的、自相矛盾的,与第一段无关 —— 第二张图中预测值不可能远离拟合到原始数据的值。 - mb7744
是的,系数是相同的,以及两个模型对象中的所有数字内容。问题出现在预测步骤中,该步骤使用拟合模型对象的“调用”和“项”元素的组合,自动将新的x值扩展为b样条向量。 - mb7744
2个回答

9
因此,问题在于模型需要跟踪使用原始数据计算出的节点,并在预测新数据时使用这些值。通常在lm()调用中的model.frame()调用中发生这种情况。 bs()函数返回"bs"类,当进行模型框架时,该列被分派到splines:::makepredictcall.bs以尝试捕获边界节点。(您可以在model.frame.default函数中看到makepredictcall调用。)

但是,如果我们比较结果

splineModel1 <- lm(y ~ bs(x, y, degree = 3, knots = c(3, 6)))
attr(terms(splineModel1), "predvar")
# list(y, bs(x, degree = 3L, knots = c(3, 6), Boundary.knots =  c(0.275912734214216, 
# 9.14309860439971), intercept = FALSE))

splineModel2 <- lm(y ~ splines::bs(x, y, degree = 3, knots = c(3, 6)))
attr(terms(splineModel2), "predvar")
# list(y, splines::bs(x, y, degree = 3, knots = c(3, 6)))

注意第二个不捕获Boundary.knots。这是因为splines:::makepredictcall.bs函数实际上查看调用的名称。
function (var, call) {
    if (as.character(call)[1L] != "bs") 
        return(call)
    ...
}

当您在公式中使用 splines::bs 时,as.character(call)[1L] 返回的是 "splines::bs",而不是 "bs",因此什么也不会发生。我不清楚为什么要进行这个检查。看起来方法调度已足够假设它是一个 bs 对象。
在我看来,这似乎不是期望的行为,可能需要修复。但是,函数 bs() 不应该在未加载包的情况下被调用,因为像 makepredictcall.bs 这样的函数也没有被导入,因此这些对象的自定义调度将会失效。

啊,太好了。看起来用字符串比较来“分派”任务确实有些奇怪。考虑到我的新预测点不仅在边界结点内,而且在内部结点内,所以我在 x = 4 处的特定预测会受到影响似乎仍然很奇怪。边界结点的位置不应该影响这些点的估计。 - mb7744
干得好。话虽如此,我认为在得出判断该检查不必要且应该修复之前,了解代码作者为什么包含了这个初始检查是非常重要的。 - Josh O'Brien
@JoshO'Brien 对的。我可以看到用户直接将bs对象传递给函数,所以我能理解它为什么存在。我想问题实际上是它没有检查"splines::bs"或"splines:::bs"。 - MrFlick
但是那样行不通,对吧?我所说的“行不通”是指允许用户仅提供向量“x”,而不是展开形式。 - mb7744
@mb7744 嗯,我的意思是你可以用那种方式来拟合模型,但这并不意味着它容易预测,因为公式中没有任何指示它的来源。我相信肯定有一些情况是设计成避免的,但我不知道具体是什么。 - MrFlick
显示剩余4条评论

1
似乎与splineModel中'terms'部分的'predvars'属性中的边界节点值有关。
如果我们称它们为splineModel_1和splineModel_2。
predict(splineModel_1, newData)
16
predict(splineModel_2, newData)
6.969746

attr(splineModel_2[["terms"]], "predvars") <- attr(splineModel_1[["terms"]], "predvars")

predict(splineModel_1, newData)
16
predict(splineModel_2, newData)
16

attr(splineModel_1[["terms"]], "predvars")
list(y, bs(x, degree = 3L, knots = c(3, 6), Boundary.knots = c(0.323248628992587, 9.84225275926292), intercept = FALSE))

attr(splineModel_2[["terms"]], "predvars")
list(y, splines::bs(x, y, degree = 3, knots = c(3, 6)))

正如您所看到的,区别在于Boundary.knots。唯一的其他区别是截距默认为FALSE,因此可能与此无关。Boundary.knots取自x的最小值和最大值。至于它由一个版本的bs设置而不是另一个版本,我只能假设这是lm代码中寻找“bs”而不是“splines :: bs”以正确设置Boundary.knots的遗物。

很好地注意到了不同的边界节点,但是关于“lm代码中寻找'bs'而不是'splines :: bs'来正确设置Boundary.knots的遗留问题”,请注意基本R中没有其他bs函数。如果在未附加splines库的情况下调用我的第一个片段会导致错误。 - mb7744
@mb7744 的确。我猜想过去可能有一个名为 bs 的函数(在基础 R 或其他常用包中),这使得当时使用 splines::bs 是个好主意。 - Gladwell

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