21

您可能已经注意到,我们现在在社区 Wiki 帖子上显示编辑摘要:

社区 wiki
220 次修订,48 位用户

我还想向“最拥有”页面上显示的最终内容的用户显示剩余文本的百分比:

社区 wiki
220 次修订,48 位用户
kronoz 87%

是的,可能有前 (n) 个“所有者”,但现在我想要前 1 个。

假设你有这个数据结构,一个用户/文本对列表,按发布时间顺序排列:

用户 ID 后文本
-------- ---------
12 敏捷的棕色狐狸跳过了懒惰的狗。
27 敏捷的棕色狐狸有时会跳跃。
30 我总是看到一只敏捷的棕色狐狸跳过懒惰的狗。

这些用户中谁最“拥有”最终文本?

我正在寻找一种合理的算法——它可以是一个近似值,它不必是完美的——来确定所有者。理想情况下以百分比分数表示。

请注意,我们需要考虑编辑、删除和插入,因此最终结果感觉合理且正确。您可以使用任何具有良好修订历史的 stackoverflow 帖子(不仅仅是重新标记,而是频繁的帖子正文更改)作为测试语料库。这是一个很好的版本,有来自 14 位不同作者的 15 次修订。谁是“主人”?

https://stackoverflow.com/revisions/327973/list

单击“查看源代码”以获取每个修订的原始文本。

我应该警告您,纯算法解决方案可能最终成为最长公共子串问题的一种形式。但正如我所提到的,如果它们运行良好,近似值和估计值也很好。

欢迎使用任何语言的解决方案,但我更喜欢以下解决方案

  1. 相当容易翻译成c#。
  2. 免于依赖。
  3. 将简单置于效率之前。

在 SO 上发表超过 25 次修订的帖子非常罕见。但它应该“感觉”准确,所以如果你仔细观察编辑,你会同意最终决定。我鼓励您在带有修订历史的堆栈溢出帖子上测试您的算法,看看您是否同意最终输出。


我现在已经部署了以下近似值,您可以在 Community Wiki 帖子上的每个保存的修订版本中看到它的实际效果

  • 对正文更改的每个修订版进行基于行的差异
  • 将每个修订的插入和删除行相加为“editcount”
  • 每个用户 ID 都会得到他们贡献的“editcount”的总和
  • 第一次修订作者获得 2x * "editcount" 作为初始分数,作为主要作者奖金
  • 确定最终所有权百分比:每个用户的编辑行总数除以所有修订中的编辑行总数

(对于常见的简单条件,例如 1 个修订版、只有 1 个作者等,还有一些保护条款。基于行的差异使得重新计算所有修订版的速度相当快;在 10 个修订版的典型情况下,它约为 50 毫秒。)

这在我的测试中效果很好。当你有几个人编辑的小 1 或 2 行帖子时,它确实会有点崩溃,但我认为这是不可避免的。接受乔尔·尼利(Joel Neely)的回答在精神上与我的想法最接近,并赞成其他所有似乎可行的事情。

4

15 回答 15

25

我认为这个想法从根本上是有缺陷的。

如果有人用糟糕的拼写和不清楚的例子写了一篇精彩的分析,而我大量复制编辑它,我创造了 60% 的工作吗?显然不是;结果是一个衍生品,其中大部分价值来自初始海报。基于字符或字数的有用度量是不可能的,但需要强大的 AI 级语义分析。

除此之外,根据文章的“所有权”来寻求信用可能完全没有帮助并且是反维基的。例如,在维基百科上,假装自己拥有文章的人是最具破坏性的影响之一。

于 2009-01-08T14:41:22.577 回答
20

之前看过你的推文。从 327973 链接的显示来看,您似乎已经有了一个单步差异。基于此,我将专注于多编辑组合:

  1. A、原发帖人拥有帖子100%的所有权。

  2. 当第二张贴者B进行编辑使得例如90%的文本没有改变时,所有权是A:90%,B:10%。

  3. 现在,第三方 C 更改了 50% 的文本。(A:45%, B:5%, C:50%)

    换句话说,当发布者进行编辑以使 x% 发生更改且 y = (100-x)% 未更改时,则该发布者现在拥有 x% 的文本,并且所有先前的所有权乘以 y%。

    为了让它变得有趣,现在假设......

  4. A 进行 20% 的编辑。然后 A 拥有“新”的 20%,剩余所有权现在乘以 80%,剩下(A:36%,B:4%,C:40%)。因此,“净”所有权为 (A:56%, B:4%, C:40%)。

将此应用于您的样本 (327973),并将所有内容四舍五入到最接近的百分比:

版本 0:原始帖子。

  • 保罗牡蛎:100%

版本 1:您当前的差异工具显示纯文本添加,因此所有这些字符都属于第二张海报。

  • 保罗牡蛎:91%
  • 一个人:9%

版本 2:差异显示替换了一个单词。新词属于第三张海报,其余文字属于之前的海报。

  • 保罗牡蛎:90%
  • 一个人:9%
  • 博客胡子:1%

版本 3:仅标记编辑。由于您的问题是关于文本的,因此我忽略了标签。

  • 保罗牡蛎:90%
  • 一个人:9%
  • 博客胡子:1%

版本 4:添加文本。

  • 保罗牡蛎:45%
  • 一个人:4%
  • 博客胡子:1%
  • 马克哈里森:50%

我希望这足以说明这个提议的意义。它确实有一些限制,但我将这些限制在您的声明中,即近似值是可以接受的。;-)

  1. 它强行将更改的影响分配给所有先前的所有者。如果 A 发帖,B 进行纯添加,C 编辑 B 添加的一半,这种简单的方法只是将 C 的所有权应用于整个帖子,而不试图解析出哪个先前的所有权发生了最大的变化。

  2. 它考虑了添加或更改,但不为删除提供任何所有权信用,因为删除器将 0% 添加到剩余文本中。您可以将其视为错误或功能。我选择了 2 号门。

更新:关于上面的问题 #1 的更多信息。我相信完全跟踪已编辑帖子部分的所有权需要两件事之一(网页的边距不足以提供正式证明;-):

  • 更改文本的存储方式以反映文本各个部分的所有权(例如,A 拥有单词 1-47,B 拥有单词 48-59,A 拥有单词 60-94,...),应用“剩余多少”在我的建议中对每个部分进行处理,并更新部分所有权数据。

  • 考虑从第一个版本到当前版本的所有版本(实际上是动态重新计算部分所有权数据)。

所以这是一个很好的例子,在快速和肮脏的近似(以精度为代价)、对整个数据库的更改(以空间为代价)或每次计算都必须查看整个数据库之间进行权衡历史(以时间为代价)。

于 2009-01-08T13:47:07.040 回答
5

以下是我如何查看示例帖子的所有权(注意,我根本忘记在文本中添加标签,所以这里的示例中不计算简单的标签更改,但可以轻松添加):

打额头:伙计,我用的是修订号而不是用户号。结果重做如下:

User 38193 owns 42% (922 / 2171) of the final post
User 2635 owns 28% (625 / 2171) of the final post
User 116 owns 24% (529 / 2171) of the final post
User 745 owns 3% (76 / 2171) of the final post
User 13005 owns 0% (11 / 2171) of the final post
User 18941 owns 0% (5 / 2171) of the final post
User 8562 owns 0% (3 / 2171) of the final post

53 ms

所以根据我的算法,用户 38193(@Paul Oyster)拥有 42% 的帖子,而帖子 2635(@Simucal)拥有 28%,用户 116( @Mark Harrison)拥有 24%,其余的可以忽略不计。

修订版中,我们可以看到原作者 Paul 仍然拥有大部分问题,而 Simucal 和 Mark 获得了良好的 nr。2 和 3。这些对应于修订版 nr。1(原帖),天然橡胶。14,这是 Simucal 的一个大型编辑,看起来它很好地显示了我的算法中的缺陷(见下文),并且 nr. 5 Mark 添加了 bash 脚本的地方。

那么我是如何得出这个答案的呢?好吧,该算法有一个缺陷,但我会回到它,但它是这样的:

基本上,原始帖子中的每个字节都被分配了编写它的用户的用户 ID。然后我使用一个可以处理乱序副本的 diff 算法,然后它会带来新作者复制的字节的用户 ID。新作者添加的任何内容都被分配了新作者的用户 ID。

例如,如果原作者写了两个句子,这些将被标记为他的用户 ID。然后另一位作者对其进行了修改,并在原两句之间添加了第三句。对于 diff 算法,这看起来像是新作者复制了第一句话,添加了新数据,然后复制了第二句话。因此,这些句子将正确地归因于它们的作者。

由于 diff 算法适用于字节,因此添加缺失标点符号或字母等微小的文本更改对所有权的影响可以忽略不计,并且几乎所有原始文本仍应归属于原作者。但是,在某些情况下,由于内部优化,即使只添加了一个字节,它也会使用“添加数据”操作。该算法及其实现最初是为处理文件差异而创建的,并在文件版本之间生成尽可能小的补丁,并且有时会优化小步骤,以便将其组合成兄弟操作,如果它会减小文件大小。

算法中的缺陷来自回滚。我注意到 Jeff 在评论中写道不会考虑回滚,但是如果用户编辑帖子而不是将其回滚,并且只是粘贴旧内容,实际上是撤销前作者的更改,那么所有文本归因于“回滚”的人,而不是提出信息的原作者。

此实现的源代码可在此处找到,适用于 Visual Studio 2008。请注意,该解决方案不执行任何操作,例如屏幕截图或其他任何操作,并且帖子内容被硬编码到 TestData 类中的源代码中,正确转义为引号等. 要替换文本,您需要更改该文件,或者实现一种从程序外部读取内容的方法。

无论如何,这里的算法更详细一点。


  1. 创建一个整数数组,只要原来的修改(其实我是通过UTF8编码成字节的,这就是我使用的长度)。用他现在拥有 100% 修订版的原作者的用户 ID 填充这个数组,每个字符/字节都是他的
  2. 将第一个修订版与下一个修订版进行比较。这种比较将产生一个操作列表。这些操作可用于获取原始修订版,对其应用操作,您将获得下一个修订版(更多内容见下文)。
  3. 假设原始帖子是您准备的用户 id 数组(当前仅包含一堆等于第一作者 id 的值),并将操作应用于此。这将产生一个新的 id 列表,其中一些是原作者,一些是第二作者。
  4. 保留第二个修订版和这个新的用户 id 列表,并在这个数据、下一个修订版和下一个修订版等之间运行步骤 2+3,直到你到达底部

此时,您有一个用户 ID 列表,它告诉您哪个用户添加了每个字符。

比较中的操作是以下两种操作之一:

  • 插入新字节
  • 复制一些字节

比较结果是如何使用的,你取旧的内容(第一篇文章),并对其进行操作,然后生成下一个修订版。它基本上是一个差异。

当我将操作应用于我的用户 id 列表时,当我复制时,我只是复制,当我插入时,我总是插入一些 id 等于存储在操作中的长度。

让我举个例子吧:

原帖:

This is the first post

下一篇:

This is the next post, it is based on the first post.

因此,操作列表将是:

  1. 复制 12 个字符 'This is the '
  2. 插入“下一个”
  3. 复制 5 个字符的“帖子”
  4. 插入17个字符',它基于'
  5. 复制14个字符'第一篇文章'
  6. 插入 1 个字符 '.'

如果我改为使用用户 ID,我将首先拥有这个数组:

0000000000000000000000
This is the first post

现在我应用这些操作,并且对于每个插入,我插入一个 1 代替:

00000000000011110000011111111111111111000000000000001
This is the next post, it is based on the first post.

现在我只计算我有多少个 0 和 1:

  1. 0 的:31
  2. 1 个:22

用户 0 拥有 31/(31+22) 个帖子,用户 1 拥有 22/(31+22) 个帖子。

换算成百分比:用户 0 拥有 58%,用户 1 拥有 42%。

现在,这个算法的问题在于回滚,以及添加丢失/删除的内容。

例如,如果您有用户 A、B 和 C,并且用户 A 发布的内容确实让用户 B 不屑一顾,那么用户 B 会进入并删除所有内容,并仅添加“这就是垃圾”。当用户 C 看到这一点时,他编辑了帖子,并添加了 A 发布的所有内容,可能还有修复。用户 C 现在拥有 100% 的帖子。

我不知道如何解决上述问题。

如果有趣的话,我将在今晚晚些时候发布执行此操作的代码。


应用于“quick brown fox”示例,将用户重新编号为 1-3,我得到以下信息:

User 3 owns 75% (45 / 60) of the final post
User 1 owns 25% (15 / 60) of the final post

请注意,仅添加了“有时”部分(随后被删除)的用户 2 已从列表中删除。

帖子的ID如下:

The quick brown fox jumps over the lazy dog.
11111111111111111111111111111111111111111111 (44 = 100%)

The quick brown fox jumps, sometimes.
1111111111111111111111111222222222222 (25 / 12 ~ 68% / 32%)

I always see the speedy brown fox jumping over the lazy dog.
333333333333333333333331111111111111113333333333333333333333 (45 / 15 = 75% / 25%)

算法将处理的事情:

如果我在发布新帖子时复制了某些内容,即使我现在复制了我还添加的部分,算法也会正确地归因于复制的元素。例子:

This is the first post, which is cool
1111111111111111111111111111111111111

This is the second post, which is the second post.
11111111111122222211111111111111112222222222111111
                               ^-- copied ------^

这个算法的唯一问题,以及这篇文章的全部内容是,我不完全确定它会产生我称之为直观公平的结果。可能是,我只是将程序组合在一起,但是可能需要对大量边缘情况进行更多测试,以确定它是否真的产生了人们会接受的东西。

另外,如果我只是从最初的帖子中转移东西,那么只有很小的部分,比如百分之几,将是我的。由于该算法本质上是一种差异算法,有时仅输出 1 或几个字节的复制操作的成本超过了仅将其原始插入的成本,因此有时它会将短字节序列视为已插入,尽管它们可能是从原始帖子中复制的。

于 2009-01-08T15:48:20.087 回答
4

如果我正确理解了您的问题,那么您似乎正在尝试做 IBM 不久前通过 Wikipedia 研究项目所做的事情。即,查看谁对其他用户最接受的文本的修订以及整体文本如何随时间变化。该项目的名称是历史流历史流 - 它的工作原理很好地概述了他们的算法是如何工作的。

于 2009-01-08T14:39:37.777 回答
4

简单地计算每个人的编辑到以前版本的Levenshtein 距离怎么样。然后将每个用户的距离得分相加,计算出该用户占所有用户距离得分总和的百分比。

这是一些计算距离的 C# 代码。

于 2009-01-08T14:47:30.257 回答
3

在我的脑海中,我会做这样的事情:

  • 我认为计算单词而不是行或字符是有道理的
  • 将原始修订标记为单词,并将作者附加到每个
  • 逐步浏览修订历史,并在添加单词时将作者附加到它们。
  • 如果单词被删除,就忘记它们。
  • 如果单词被更改,您可以将它们视为删除/插入,或者具有某种基于字符的阈值,以便拼写更正不会归因于新作者。
  • 你最终会得到一个单词列表,反对最初写它们的人
于 2009-01-08T13:46:56.370 回答
3

如果您想实现/利用 Python 的 diff 算法,这将起作用difflib——在任何情况下,您都可能需要做某种 diff。此代码段将文本流失最多的用户称为获胜者。

请原谅我的硬编码。

#!/usr/bin/env python

import collections
import difflib
import logging
import pprint
import urllib2
import re

class OwnageDeterminer(object):

    add_coefficient = 1
    remove_coefficient = .5

    def __init__(self, edits):
        self.edits = edits
        self.counts_by_username = {}

    def __call__(self):
        edits, counts_by_username = self.edits, self.counts_by_username
        for i, edit in enumerate(edits):
            username = edit['username']
            unique_counts = {'added': 0, 'removed': 0}
            existing_text = edits[i-1]['text'] if i > 0 else ''
            new_text = edits[i]['text']
            for char_diff in difflib.ndiff(existing_text, new_text):
                if char_diff.startswith('+'):
                    unique_counts['added'] += 1
                elif char_diff.startswith('-'):
                    unique_counts['removed'] += 1
            user_counts = counts_by_username.get(username, collections.defaultdict(int))
            user_counts['removed'] += self.remove_coefficient * unique_counts['removed']
            user_counts['added'] += self.add_coefficient * unique_counts['added']
            counts_by_username[username] = user_counts
        winner = None
        winning_score = 0
        score_by_username = {}
        for username, counts in counts_by_username.iteritems():
            score = counts['removed'] + counts['added']
            if score > winning_score:
                winner = username
                winning_score = score
            score_by_username[username] = score
        logging.debug('Scores: %s', pprint.pformat(score_by_username))
        return winner


if __name__ == '__main__':
    logging.basicConfig(level=logging.DEBUG)
    site = urllib2.urlopen('http://stackoverflow.com/revisions/327973/list')
    contents = site.read()
    regex = re.compile(r'(/revisions/viewmarkup/\d+).*?/users/\d+/([\w-]+)',
                       re.MULTILINE|re.DOTALL)
    revisions = regex.findall(contents)
    print revisions
    edits = []
    for reluri, username in sorted(revisions, key=lambda t: t[0]):
        text = urllib2.urlopen('http://stackoverflow.com{0}'.format(reluri)).read()
        edit = {'username': username, 'text': text}
        edits.append(edit)
    od = OwnageDeterminer(edits)
    print od()

输出:

DEBUG:root:Scores: {'blorgbeard': 0.5,
 'dave-markle': 0.5,
 'dbr': 1172.0,
 'gatekiller': 69.5,
 'joseph-ferris': 0.0,
 'lkessler': 0.0,
 'mark-harrison': 592.0,
 'mdb': 3.0,
 'onebyone-livejournal-com': 0.0,
 'paul-oyster': 482.0,
 'rob-wells': 0.0,
 'simucal': 1070.5,
 'skiphoppy': 0.0,
 'thesoftwarejedi': 701.0}
dbr

关于复杂性的Difflib 文档:

时序:基本的 Ratcliff-Obershelp 算法在最坏情况下是三次时间,在预期情况下是二次时间。SequenceMatcher 是最坏情况的二次时间,其预期情况的行为以复杂的方式取决于序列有多少共同元素;最佳情况时间是线性的。

另一个好处是这个获胜者计算是线性的,因此您可以缓存原始结果并对新编辑进行增量更新,尽管初始化负载很大。

于 2009-01-08T13:49:23.807 回答
3

没有人拥有它。授予所有权违反了“社区维基”的精神,并可能导致适得其反的编辑战。

于 2009-01-08T15:24:29.560 回答
2

一个问题是删除字符是否与添加字符一样有效。使用 Diff 可能效果很好,但并不总是意味着添加最多的人就是最有效的编辑器(有些人可以用 10 个字符写出其他人花一页写的内容)。

所以我认为你有两个因素:

  • 变化量
  • 变化的质量

因此,我很想写一些简单地将添加/删除的单词映射到分数的东西。再加上某种质量因子(这必须是一个外部参数),然后可以将组合值用于排序。

您可以根据在项目被编辑之前所花费的距离生成某种原始的“质量因子”。因此,如果我写的东西在第 12 次编辑之前没有改变,那么它就不会错误(相对于质量较低的东西,一旦添加就改变了)。

于 2009-01-08T13:58:55.393 回答
2

这是一个 wiki——为什么不让每个编辑选择他或她的变化的意义呢?提供一个下拉菜单,例如...

请限定您的编辑:

  1. 一些小的编辑(拼写、语法等)
    • 许多小的编辑
    • 一些重要的编辑(对特定事实、数据等的更改)
    • 许多重要的编辑
    • 一些主要的编辑(对论文、结论等的修改)
    • 许多重大修改

然后使用组合的响应来估计所有权。

于 2009-01-09T06:03:45.523 回答
2

这个想法怎么样?

  • 显示原始发布者的名称,直到帖子与原始文本的更改超过 x%
  • 这个百分比显示为“x 个用户,y 个编辑,z% of original”
  • 当前存在的最后一个编辑内容也可能有差异 - 例如,它清楚地表明对您的问题的第一次编辑是一个小的添加

关键是,经过一定程度的更改后,谁拥有它并不重要。我同意用户的说法,试图将“所有者”分配给 wiki 内容会适得其反。

于 2009-01-09T22:29:45.153 回答
1

我认为字符数很棘手。

怎么样:

Break latest revision into a set of sentences. 
//Sentence is any text fragment surrounded by punctuation

For each Sentence
    Find which user created that sentence. 
    Add 1 to the user who created the sentence 
    Add 1 to the number of sentences

For Each user 
    % ownership = Count for that user / Number of sentences. 

查找哪个用户创建了该句子。
如果您想要完全匹配,将句子与修订版匹配很容易,但部分匹配我会更满意。

要进行此部分匹配...

从句子片段中去除常用词,并在每个修订的去除版本中搜索去除的片段。最早命中的是车主写的那句话。

选项:(而不是常见的单词剥离)

  • 对于句子中的每个单词,将每个单词压缩为第一个和最后一个字符。这将解释拼写错误等。
  • 只使用句子片段中的第一个和最后两个单词。
  • 匹配 80% 的单词可能足以证明所有权。(这对计算机来说是一件困难的事情——而且它不能被缓存)

剥离常用词

您已经为搜索执行此操作,因此库等已经存在。即使您使用其他人的公式,您也应该在开始之前进行 CommonWordStrip 每次修订。

于 2009-01-08T13:59:20.697 回答
1

对这个问题有一个好的解决方案的关键是获得关于编辑的额外信息。该媒体中可用的额外信息是投票和对问题的响应率。因此,如果有人进行的编辑导致问题获得大量支持、评论和回复,那么他们的编辑就非常有价值。一个全新的未回答问题可能存在特殊情况,该问题在网站上存在很长时间之前就已被编辑。

您应该使用 Levenshtein 距离算法查看帖子的更改量,然后根据投票数、评论数和编辑后问题收到的答案来衡量他们的编辑。

Let n = total revisions
Let m = the revision number of a poster
Let post[it] = array with text of post at revision 'it'
Let votes[it] = votes that revision 'it' received (also add bonus for comments/answers)
value = 0
for (it = m; it < n; ++it) {
  value += (Levenshtein(post[it-1], post[m]) / average_length_post) * (votes[it])
}

如果您计算每个帖子的价值,则帖子的所有权是该用户所有编辑的总价值除以该帖子的所有编辑值的总和。

于 2009-01-08T16:26:04.597 回答
0

不确定是否可能,但您可以计算添加的字符数。

例子:

  • 用户 1 提交了一个 400 个字符的问题(用户 1 有 400 个字符,共 400 个字符)
  • 用户 2 删除 40 并添加 60(用户 1 有 360,用户 2 有 60)

如果您恢复,您还应该恢复到以前的用户/字符数。

但是……也许给原海报起个名字更简单、更公平……

再想一想,我编辑了很多,但我从不认为自己是 tekst 的“所有者”,因为我只是更改了演示文稿(格式和语法)而不是内容。

于 2009-01-08T13:38:39.973 回答
0

最初的概念必须被赋予权重。拼写更正必须具有权重。语法/结构必须被赋予权重。措辞必须有分量。简洁必须被重视。ETC...

权重是唯一公平的方法,但也无法确定。

于 2009-01-08T15:44:38.333 回答