1

I'm currently try to implement the calculation of a ROC curve in ruby. I tried to transform the pseudocode from http://people.inf.elte.hu/kiss/13dwhdm/roc.pdf (see 6th site, chapter 5, Algorithm 1 "Efficient Method for generating ROC points") into Ruby code.

I worked out a simple example, but I'm always getting values over 1.0 for recall. I think I misunderstood something, or made a mistake at programming. Here is what I gor so far:

# 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

Based on a results array of arrays, the code tries to calculate the points. I'm not sure what the f_prev is all about. Is in the f_prev the score of the classifier stored, or only if it's true or false?

It would be awesome, if someone could have a quick look at my code, and help me find my mistake. thx!

4

2 回答 2

1

我的第二个答案是分析您的代码,并指出我认为您在哪里犯了一些错误或感到困惑。我假设您想要重现与链接 PDF 的第 864 页上看到的图表相似的图表。

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

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

# 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变量。您可以使用 eg2.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 放置相同逻辑的方式,假设您将用户分数替换为其他地方的真/假值可能是

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(因此在逻辑上必须是,fp因为根本不允许)。不过没关系,因为您不使用该值。tp0p


在循环内部,代码正在跟踪如果阈值设置为刚好高于当前项目,则总误报和真阳性将是多少。当您将这个标准降低到具有相同分数的项目组时,它们将预测正值(无需测试这个与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
于 2013-04-18T14:14:04.767 回答
1

这个答案是不正确的,因为它从 OP 评论中假设该算法需要对误报和真正分配进行逐项评估。实际上,变量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
于 2013-04-18T08:39:26.970 回答