ROC曲线的Ruby实现

时间:2013-04-18 07:07:23

标签: ruby algorithm roc

我目前正尝试在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

如果有人可以快速查看我的代码并帮助我找到错误,那就太棒了。 THX!

2 个答案:

答案 0 :(得分: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

答案 1 :(得分: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变量。你可以用例如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 / fasle值替换用户得分可能是

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