为这个棘手的购买问题正确的特征聚合

机器算法验证 分类 造型 模型 监督学习 多级
2022-03-29 11:41:54

考虑以下问题,这是在我参加的一次采访中提出的(但不是针对我的)。看似简单,但实际上很难很好地回答:

每周 ,在线食品配送服务在其需要销售的大量产品中都有一定 为了激励人们购买这些食物,商店可以在一周开始时发送电子邮件。但是,如果客户收到此类电子邮件,他们会感到恼火,因此必须将收到电子邮件的客户数量控制在尽可能低的水平。只找到那些您认为倾向于购买所选产品的客户! 您可用的数据: 1) 每个客户在每个时间点的大量购买历史 2) 过去随机发送给一部分客户的所有电子邮件iPi




3)一些关于他作为一个人的一般数据,找到这样的客户。

对此有什么好的解决方案?

我已经花了几个小时思考这个问题,但我只能想出一个简单的解决方案:
将其视为没有任何时间成分的二进制分类问题(因此只需将所有几周的数据一起视为单个大型数据集)[这是因为我不知道时间序列]。因此,给定我们选择的产品,我们想知道哪些类型的客户会购买它。分离训练-验证-测试集中的数据。然后,我们按以下方式组织数据:使用 one-hot 编码对所有分类数据进行编码。使用以下集合功能(=包含所有数据的数据框中的列):P

  • 每个客户的可用信息来自 3)
  • 定义一个类(=feature=column),用 1 或 0 表示该客户在一周开始时收到电子邮件后的一周内购买了该产品(这意味着对于同一客户,我们将观察(=行),因为有一周)。因此,我不使用他们一般购买历史中的任何信息,只使用客户如何立即购买更改为由于电子邮件。

最后,使用您最喜欢的二元分类器并反复尝试,直到最大限度地提高预测准确性。


我认为这样一个模型来解决这个问题真的很可怕。以下是对该模型的批评,通过说明我认为它无法考虑的一些要点:

a) 客户在购买行为方面随着时间的推移改变了他们对电子邮件的反应[有没有办法在不使用时间序列的情况下解决这个问题?因为我对他们几乎一无所知]

b)个人客户的购买行为:-
那些从未购买过电子邮件中内容的客户,应该被分配对新电子邮件
做出购买响应的低概率-我不确定使用这种模型,即使是人总是购买一些在电子邮件中宣传的东西的人将获得购买新东西的高概率。这两点对我来说似乎很重要,模型应该捕捉它们!
- 一些客户可能对某些产品有偏好,这表明如果他们通常喜欢的产品在中,他们肯定会收到一封电子邮件,以激励他们购买更多 P

c)我不考虑电子邮件的任何长期影响:目前我隐含地使用截止值来确定客户从电子邮件中购买了其中一种产品的信息的有用性:如果他购买了其中的一些产品在其开始发送电子邮件的那一周内,该信息被假定为 100% 有用(因为它反映在我构建的特征中),如果他在以后的任何时间点购买该信息,则该信息被假定为0% 有用(因为我在功能中忽略了它)。与其隐式使用这样的截止函数,我认为如果我能以某种方式明确表示(但我不知道如何)并使用其他一些价值较低的函数,客户从邮件,以便模拟此信息随时间衰减的有用性。

1个回答

0) 和 2) 移动平均模型。假设我们得到的只是以下时间序列数据

     time          y
  1:    0 -12.070657
  2:    1   4.658008
  3:    2  14.604409
  4:    3 -17.835538
  5:    4  11.751944
  ...

看起来像这样:

在此处输入图像描述

就这个玩具示例而言,它本质上是罪(时间)+ 干扰(您可以在下面找到 R 代码)。这个奇怪的移动平均线或最简单的时间序列模型是什么意思?我谈论将 y 的过去值添加为新列。让我们来k=3例如,那么每次t我们增加yt1,yt2,yt3作为新列:

    time          y   y_past_1   y_past_2   y_past_3
 1:    0 -12.070657         NA         NA         NA
 2:    1   4.658008 -12.070657         NA         NA
 3:    2  14.604409   4.658008 -12.070657         NA
 4:    3 -17.835538  14.604409   4.658008 -12.070657
 5:    4  11.751944 -17.835538  14.604409   4.658008
 6:    5  14.331069  11.751944 -17.835538  14.604409

考虑t=3. 为了这,t1=2y_past_1(y 在当前时间点之前的值t=3) 是值yt1=y2=14.604409. 类似地,t2=1y_past_2(y 在当前两个时间步长之前的值t=3) 是yt2=y1=4.658008.

现在人们首先要做的是计算一个(线性)模型y作为目标变量,`y_past_1, ..., y_past_k$ 作为输入特征。这些也称为“滞后”变量,因为它们与目标变量相同,只是时间分量有一点滞后。

现在让我们计算一个线性模型。我得到的基本上是

y ~ 0.4320*y_past_1 + 0.2457*y_past_2 + y_past_3*0.2361 + 0.3070

在此处输入图像描述

咦,怎么可能我们计算了一个线性模型,结果却不是线性的呢?发生这种情况是因为函数time -> y_time不是线性的,即线性模型应用于“非线性值对”,(y_past_1, y_past_2, y_past_3)但仍然将它们线性相加。

这就是我所说的简单时间序列模型的意思:将某个变量的过去作为预测新状态的输入。

注意:我们没有讨论K. 这个参数作为一个平滑因子,即在时间序列方面,它决定了预测将是一个所谓的高通滤波器(K 小,不过滤掉突然的运动,即高频,K 大,那么预测如下sin 函数更平滑,不会被目标变量的突然移动“愚弄”:

K=1: 在此处输入图像描述

K=10 在此处输入图像描述 :)

1)我的意思是以下。假设我们考虑产品 PIZZA。我们有两个用户,A 和 B。在去年,我们向每个用户发送了 20 封披萨的广告电子邮件。用户 A 确实响应了 15 次购买比萨饼,而用户 B 完全没有响应。现在让我们说,在当天我们再次看到用户 A 和用户 B,并且我们有向他们发送广告电子邮件的触发器。我们遍历我们所有的产品,然后我们得到了产品披萨。我们应该向 A 发送披萨广告吗?乙呢?[当然我们应该向 A 发送电子邮件,因为它的回复率很高,但我们可能不应该向 B 发送披萨广告,因为显然他/她根本不喜欢我们的披萨或披萨广告,或者有其他原因不回复]。以这种方式在每个时间点t我们应该包括过去t1,...,tK对于每个请求和每个用户。这意味着我们没有每个用户的“单一过去”,而是训练,但对于训练集中的每个请求,我们都有一个新的唯一“过去”......如上例所示:对于每个ty_past_1有一个独特的价值,即yt1. 但是,在您的示例中,我们不简单地采用yt1,...,ytK考虑到但它的一些功能是这样的:

对于用户给出的每个请求u每周t我们迭代每一个产品p并且对于每个产品p我们回去K=52周并检查我们发送用户的频率u产品的广告电子邮件p(数字sent),我们计算频率u收到电子邮件(数字)后一周内购买广告产品做出了积极响应positiveResponses,然后我们计算affinity = positiveResponses/sent并将其包含在当前请求的列中。通过这种方式,模型应该提出这样的规则,例如“如果对这个产品的亲和力很高,那么我应该为这个产品发送广告”。

从这个意义上说:您不使用过去每周的列,而是使用返回 52 周的每个产品。

3)您似乎担心模型无法找出某个规则,例如“只有当 X列Y 列的值很高时才预测 TRUE,否则预测 FALSE”。但是,无论您从“复杂性第一联盟”中选择哪种模型(即,除了像神经网络这样的线性模型、像随机森林这样的树增强方法、梯度增强、像 SVM 这样的几何方法......)这些模型都可以计算如果只有数据告诉他们这样做(可证明!!!),则可以排除任意复杂的区域。例如:对于只有一个隐藏层的 NN(!),这 [我相信] 是著名的 Stone Weierstrass 定理(https://en.wikipedia.org/wiki/Stone%E2%80%93Weierstrass_theorem,只有一个隐藏层的 NN层形成代数)。

示例:支持向量机。将您的网络浏览器导航到https://www.csie.ntu.edu.tw/~cjlin/libsvm/向下滚动到 java 小程序并放置两组不同的彩色点,然后稍微调整一下超参数 C 和 ga,您将看到如下结果:

在此处输入图像描述

这意味着即使(更复杂!)像您上面制定的规则最终也会被模型捕获。这就是我不会担心太多的原因。

编辑:R代码:

library(data.table)
set.seed(1234)
dt = data.table(time = 0:250)
dt = dt[, y := sin(time/100*2*pi)*30 + rnorm(dt[, .N], mean=0, sd = 10)]
plot(dt$time, dt$y, type="l")



lag = function(x, k, fillUp = NA) {
  if (length(x) > k) {
    fillUpVector = x[1:k]
    fillUpVector[1:k] = fillUp
    return(c(fillUpVector, head(x, length(x)-k)))
  } else {
    if (length(x) > 0) {
      x[1:length(x)] = fillUp
      return(x)
    } else {
      return(x)
    }
  }
}



K = 3
for (k in 1:K) {
  eval(parse(text=paste0("dt = dt[, y_past_", k, " := lag(y, k)]")))
}
train = copy(dt)
train = train[K:dt[, .N]]
train = train[, time := NULL]
model = lm(y ~ ., data = train)
pred = predict.lm(object = model, newdata = train, se.fit = F)
train = train[, PREDICTION := pred]
plot(dt$time, dt$y, type="l")
train = train[, time := K:dt[, .N]]
lines(train$time, train$PREDICTION, col="red", lwd=2)

问候,

固件