【问题标题】:How to randomly delete a number of lines from a big file?如何从大文件中随机删除多行?
【发布时间】:2011-12-26 14:28:24
【问题描述】:

我有一个 13 GB 的大文本文件,包含 158,609,739 行,我想随机选择 155,000,000 行。

我试图打乱文件,然后剪切 155000000 第一行,但似乎我的 ram 内存 (16GB) 不够大,无法执行此操作。我尝试过的管道是:

shuf file | head -n 155000000
sort -R file | head -n 155000000

现在我认为不是选择行,而是更节省内存,从文件中删除 3,609,739 行随机行以获得 155000000 行的最终文件。

【问题讨论】:

  • 155000000是必须的吗,+-1000你能接受吗?
  • 是的,我正好需要 150000000 行,这是最大的问题之一......
  • 我无法想象是什么原因导致您需要这么多行...
  • @Karl Knechtel 我需要一个数据集来进行模拟。我希望有一天我会写一篇关于我的工作的论文,所以我更喜欢写“我们使用 1.55 亿次读取作为模拟的输入数据集”而不是“我们使用 154447842 次读取作为模拟的输入数据集”。 ..

标签: python bash random sed awk


【解决方案1】:

当您将文件的每一行复制到输出时,请评估其应该被删除的概率。第一行应该有 3,609,739/158,609,739 的机会被删除。如果您生成一个介于 0 和 1 之间的随机数并且该数字小于该比率,请不要将其复制到输出中。现在第二行的赔率为 3,609,738/158,609,738;如果不删除该行,则第三行的赔率是 3,609,738/158,609,737。重复直到完成。

由于处理的每一行的几率都会发生变化,因此该算法可以保证准确的行数。一旦你删除了 3,609,739,几率就会变为零;如果在任何时候您需要删除文件中剩余的每一行,则几率为 1。

【讨论】:

  • 啊对,也叫reservoir sampling
  • @DarrenYin:不,这不是水库采样。在水库采样中,您事先并不知道项目总数。
  • 我喜欢这个算法,但并不完美,因为文件的第一部分有更大的机会被删除......不是吗?
  • @Geparada,一行被删除的几率会随机波动,但平均应该保持不变。考虑一下,一旦您完成文件的一半,应该删除的总行数的一半已经消失,现在赔率将是 1804869/79304869。
  • @Geparada,如果赔率不为零,最后一行的赔率将是 1/1,因此该行将被删除。这让我想起了我在答案中遗漏的一些细微之处:使用不超过 但不包括 1.0 的随机数范围,并与random < odds(不是<=)进行比较。
【解决方案2】:

您始终可以预先生成您计划删除的行号(选择不替换的 3,609,739 个随机数的列表),然后只需遍历文件并复制到另一个文件,必要时跳过行。只要您有新文件的空间,这将起作用。

您可以使用random.sample 选择随机数 例如,

random.sample(xrange(158609739), 3609739)

【讨论】:

    【解决方案3】:

    Mark Ransom 回答的证明

    让我们使用更容易思考的数字(至少对我而言!):

    • 10 项
    • 删除其中 3 个

    第一次通过循环时,我们将假设前三个项目被删除——这就是概率的样子:

    • 第一项:3 / 10 = 30%
    • 第二项:2 / 9 = 22%
    • 第三项:1 / 8 = 12%
    • 第四项:0 / 7 = 0 %
    • 第五项:0 / 6 = 0 %
    • 第六项:0 / 5 = 0 %
    • 第七项:0 / 4 = 0 %
    • 第八项:0 / 3 = 0 %
    • 第九项:0 / 2 = 0 %
    • 第十项:0 / 1 = 0 %

    如您所见,一旦达到零,它就会保持为零。但是如果什么都没有被删除呢?

    • 第一项:3 / 10 = 30%
    • 第二项:3 / 9 = 33%
    • 第三项:3 / 8 = 38%
    • 第四项:3 / 7 = 43%
    • 第五项:3 / 6 = 50%
    • 第六项:3 / 5 = 60%
    • 第七项:3 / 4 = 75%
    • 第八项:3 / 3 = 100%
    • 第九项:2 / 2 = 100%
    • 第十项:1 / 1 = 100%

    因此,即使每行的概率各不相同,但总体而言,您会得到想要的结果。我更进一步,用 Python 编写了一百万次迭代的测试,作为对自己的最终证明——从 100 项列表中删除 7 项:

    # python 3.2
    from __future__ import division
    from stats import mean  # http://pypi.python.org/pypi/stats
    import random
    
    counts = dict()
    for i in range(100):
        counts[i] = 0
    
    removed_failed = 0
    
    for _ in range(1000000):
        to_remove = 7
        from_list = list(range(100))
        removed = 0
        while from_list:
            current = from_list.pop()
            probability = to_remove / (len(from_list) + 1)
            if random.random() < probability:
                removed += 1
                to_remove -= 1
                counts[current] += 1
        if removed != 7:
            removed_failed += 1
    
    print(counts[0], counts[1], counts[2], '...',
          counts[49], counts[50], counts[51], '...',
          counts[97], counts[98], counts[99])
    print("remove failed: ", removed_failed)
    print("min: ", min(counts.values()))
    print("max: ", max(counts.values()))
    print("mean: ", mean(counts.values()))
    

    这是我几次运行它的结果之一(它们都很相似):

    70125 69667 70081 ... 70038 70085 70121 ... 70047 70040 70170
    remove failed:  0
    min:  69332
    max:  70599
    mean:  70000.0
    

    最后一点:Python 的 random.random() 是 [0.0, 1.0)(不包括 1.0 作为可能性)。

    【讨论】:

      【解决方案4】:

      我相信您正在寻找"Algorithm S" from section 3.4.2 of Knuth (D. E. Knuth, The Art of Computer Programming. Volume 2: Seminumerical Algorithms, second edition. Addison-Wesley, 1981)。

      你可以在http://rosettacode.org/wiki/Knuth%27s_algorithm_S看到几个实现

      Perlmonks list has some Perl implementations of Algorithm S and Algorithm R 也可能有用。

      这些算法依赖于对浮点数(如 3609739/158609739、3609738/158609738 等)进行有意义的解释,可能对于标准 Float 数据类型没有足够的分辨率,除非Float 数据类型是使用 double precision 或更大的数字实现的。

      【讨论】:

        【解决方案5】:

        这是一个使用 Python 的可能解决方案:

        import random
        
        skipping = random.sample(range(158609739), 3609739)
        
        input = open(input)
        output = open(output, 'w')
        
        for i, line in enumerate(input):
            if i in skipping:
                continue
            output.write(line)
        
        input.close()
        output.close()
        

        这是另一个使用马克的方法:

        import random
        
        lines_in_file = 158609739
        lines_left_in_file = lines_in_file
        lines_to_delete = lines_in_file - 155000000
        
        input = open(input)
        output = open(output, 'w')
        
        try:
            for line in input:
                current_probability = lines_to_delete / lines_left_in_file
                lines_left_in_file -= 1
                if random.random < current_probability:
                    lines_to_delete -= 1
                    continue
                output.write(line)
        except ZeroDivisionError:
            print("More than %d lines in the file" % lines_in_file)
        finally:
            input.close()
            output.close()
        

        【讨论】:

          【解决方案6】:

          我在看到Darren Yin表达了它的原理之前就写了这段代码。

          我已经修改了我的代码以使用名称skipping(我不敢选择kangaroo ...)和来自Ethan Furman的关键字continue,其代码原理也相同。

          我为函数的参数定义了默认参数,以便函数可以多次使用,而不必在每次调用时重新赋值。

          import random
          import os.path
          
          def spurt(ff,skipping):
              for i,line in enumerate(ff):
                  if i in skipping:
                      print 'line %d excluded : %r' % (i,line)
                      continue
                  yield line
          
          def randomly_reduce_file(filepath,nk = None,
                                   d = {0:'st',1:'nd',2:'rd',3:'th'},spurt = spurt,
                                   sample = random.sample,splitext = os.path.splitext):
          
              # count of the lines of the original file
              with open(filepath) as f:  nl = sum(1 for _ in f)
          
              # asking for the number of lines to keep, if not given as argument
              if nk is None:
                  nk = int(raw_input('  The file has %d lines.'
                                     '  How many of them do you '
                                     'want to randomly keep ? : ' % nl))
          
              # transfer of the lines to keep,
              # from one file to another file with different name
              if nk<nl:
                  with open(filepath,'rb') as f,\
                       open('COPY'.join(splitext(filepath)),'wb') as g:
                      g.writelines(  spurt(f,sample(xrange(0,nl),nl-nk) )  )
                      # sample(xrange(0,nl),nl-nk) is the list
                      # of the counting numbers of the lines to be excluded 
              else:
                  print '   %d is %s than the number of lines (%d) in the file\n'\
                        '   no operation has been performed'\
                        % (nk,'the same' if nk==nl else 'greater',nl)
          

          【讨论】:

            【解决方案7】:

            使用 $RANDOM 变量,您可以获得 0 到 32,767 之间的随机数。

            这样,您可以读取每一行,看看 $RANDOM 是否小于155,000,000 / 158,609,739 * 32,767(即 32,021),如果是,则让该行通过。

            当然,这不会给您正好 150,000,000 行,但根据随机数生成器的正态性,它非常接近。

            编辑:以下是一些帮助您入门的代码:

            #!/bin/bash
            while read line; do
              if (( $RANDOM < 32021 ))
              then
                echo $line
              fi
            done
            

            这样称呼它:

            thatScript.sh <inFile.txt >outFile.txt
            

            【讨论】:

            • Mark 的回答展示了如何修改它以使其正好为 150M 行。
            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2018-11-08
            • 2023-03-25
            • 1970-01-01
            • 1970-01-01
            • 2015-12-23
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多