ROC曲线的Ruby实现



我目前正在尝试在ruby中实现ROC曲线的计算。我尝试将伪代码从http://people.inf.elte.hu/kiss/13dwhdm/roc.pdf(参见第6个站点,第5章,算法1"生成ROC点的有效方法")转换为Ruby代码。

我做了一个简单的例子,但我总是得到1.0的值来回忆。我想我误解了什么,或者在编程时犯了一个错误。以下是我目前得到的结果:

# results from a classifier
# index 0: users voting
# index 1: estimate from the system
results = [[5.0,4.8],[4.6,4.2],[4.3,2.2],[3.1,4.9],[1.3,2.6],[3.9,4.3],[1.9,2.4],[2.6,2.3]]
# over a score of 2.5 an item is a positive one
threshold = 2.5
# sort by index 1, the estimate
l_sorted = results.sort { |a,b| b[1] <=> a[1] }
# count the real positives and negatives
positives, negatives = 0, 0
positives, negatives = 0, 0
l_sorted.each do |item|
  if item[0] >= threshold
    positives += 1
  else
    negatives += 1
  end
end
fp, tp = 0, 0
# the array that holds the points
r = []
f_prev = -Float::INFINITY
# iterate over all items
l_sorted.each do |item|
  # if the score of the former iteration is different,
  # add another point to r
  if item[1]!=f_prev
    r.push [fp/negatives.to_f,tp/positives.to_f]
    f_prev = item[1]
  end
  # if the current item is a real positive
  # (user likes the item indeed, and estimater was also correct)
  # add a true positive, otherwise, add a false positve
  if item[0] >= threshold && item[1] >= threshold
    tp += 1
  else
    fp += 1
  end
end
# push the last point (1,1) to the array
r.push [fp/negatives.to_f,tp/positives.to_f]
r.each do |point|
  puts "(#{point[0].round(3)},#{point[1].round(3)})"
end

基于一个results数组的数组,代码试图计算点。我不确定f_prev是关于什么的。是在f_prev中存储分类器的分数,还是仅当它是truefalse时?

如果有人能快速看一下我的代码,并帮助我找到我的错误,那就太棒了。谢谢!

我的第二个答案是分析你的代码,并指出我认为你犯了一些错误或困惑的地方。我假设你想复制一个图表,类似于你链接的PDF的第864页。

类似于p864上的ROC图,是显示您的预测模型中假阳性和真阳性率之间的可用折衷的图表。要查看所有可能的折衷方案,您需要访问阈值会产生差异的所有数据点,并绘制它们的假阳性率和真阳性率。

你的第一个困惑点似乎是你有一个"用户投票"浮点分数,而不是一个真/假类别。PDF中的示例已经确定了p/n个用于绘制ROC的案例。

# results from a classifier
# index 0: users voting
# index 1: estimate from the system
results = [[5.0,4.8],[4.6,4.2],[4.3,2.2],[3.1,4.9],[1.3,2.6],[3.9,4.3],[1.9,2.4],[2.6,2.3]]

所以我认为你最好有

results = [[true,4.8],[true,4.2],[true,2.2],[true,4.9],[false,2.6],[true,4.3],[false,2.4],[true,2.3]]

开始绘制ROC之前。内联进行这种转换是可以的,但是您需要从您的ROC图中分离出您如何生成测试数据的关注-例如,您的用户分数和机器估计分数在同一尺度上的事实是不相关的。

指向threshold变量。您可以使用例如2.5来转换您的用户数据,但这与您的ROC图无关。事实上,要获得完整的ROC图,您需要测试多个阈值,以了解它们如何影响真阳性率和假阳性率。

# over a score of 2.5 an item is a positive one
threshold = 2.5

将值按相反的顺序排序,得分最高的项排在前面。你可以用任何一种方式来做,但对我来说,这意味着你想从一个高阈值开始(你的所有分数都预测false),并在图表上的位置[0.0,0.0]

# sort by index 1, the estimate
l_sorted = results.sort { |a,b| b[1] <=> a[1] }

下面的代码看起来足够准确,但实际上它只是测试阳性和阴性的总和,所以不应该混淆阈值的概念:

# count the real positives and negatives
positives, negatives = 0, 0
positives, negatives = 0, 0
l_sorted.each do |item|
  if item[0] >= threshold
    positives += 1
  else
    negatives += 1
  end
end

放置相同逻辑的一种更好的Ruby方式,假设您将用户分数替换为其他地方的true/false值,可能是

positives = l_sorted.select { |item| item[0] }.count
negatives = l_sorted.count - positives

这看起来不错,你确实从[0.0,0.0]开始,

fp, tp = 0, 0
# the array that holds the points
r = []

但是,这看起来像起始阈值

f_prev = -Float::INFINITY
在我看来,

在逻辑上是正的Float::Infinity,这样你所有的预测最初都是false(因此fptp在逻辑上必须是0,因为根本不允许p)。不过没关系,因为您不需要使用。


在循环中,如果阈值刚好高于当前项,代码将跟踪假阳性和真阳性的总数。当您将此条降低到具有相同分数的项目组时,它们将预测正值(不需要将其与threshold变量进行测试,这将使您感到困惑)。您所要做的就是将这些正值排序到tpfp计数中。检查与f_prev只是帮助分组相似的项目,如果3个预测有相同的分数,你只绘制一个点。

# iterate over all items
l_sorted.each do |item|
  if item[1]!=f_prev
    # Plot a point, assuming all predictions with a score equal or lower than current
    # item are thresholded out as negative.
    r.push [fp/negatives.to_f,tp/positives.to_f]
    f_prev = item[1]
  end
  # Assume the current prediction is now positive, and calculate how that affects the curve
  # if the current test item is a real positive
  # add to true positives, otherwise, it has become a false positve
  if item[0]
    tp += 1
  else
    fp += 1
  end
end
# push the last point (1,1) to the array
r.push [fp/negatives.to_f,tp/positives.to_f]

除了改变测试,我删除了一个不准确的注释("估计器也是正确的")-我们在这段代码中不判断估计器是否对单个值"正确",我们只是看到它在特定截止点对fptp的评分有多好。排序列表上的单次传递过程依赖于这样一个事实,即根据fptp计数的变化,这将是与最后绘制的点相比的一个小增量变化。

现在应该从[0.0,0.0][1.0,1.0]

r.each do |point|
  puts "(#{point[0].round(3)},#{point[1].round(3)})"
end

这个答案是不正确的,因为它从OPs的评论中假设算法需要对每个项目进行假阳性和真阳性分配的评估。事实上,变量tpfp是跟踪整个数据集的总数,只是假设循环中的当前预测为正而进行调整。请看我的另一个答案。


  if item[0] >= threshold && item[1] >= threshold
    tp += 1
  else
    fp += 1
  end

你似乎把"真阳性"以外的任何东西都算作"假阳性"。

这是不正确的,您忽略了结果是真阴性或假阴性分类的可能性。试试这个:

  if item[0] >= threshold && item[1] >= threshold
    tp += 1
  elsif item[0] < threshold && item[1] >= threshold
    fp += 1
  end

或略干

  if item[1] >= threshold
    if item[0] >= threshold
      tp += 1
    else
      fp += 1
    end
  end

最新更新