16450


Ruby初心者です．  
元のCSVファイル "test.CSV" A, 100, test があるとします．  
2列目の値を10~0に変更し，計11個のCSVを新たに出力するために，  
以下のプログラムを作成しました．

[code]

    require 'csv'
    
    # CSVを読み込んで二次元配列にする
    array = CSV.read('filepath\test.csv')
    puts(array.to_s)
    
    # x=ループ処理回数
    x =10
    while x >= 0 
    
        array[0][1]= x
    
        #,ごとに分割して出力
        strAry = array.to_s.split(",")
    
        #保存するファイル名
        #配列1行目を取得
        y = array[0][0]
        file_name = y.to_s + "+" + x.to_s + '.csv'
    
        s = strAry.to_csv
        p s
        File.write(file_name, s)
    
        x = x - 1
    end
    
[/code]

上記を実行すると，A+0.csv ～A+10.csvまでの計11個のファイルが作成されます．  
(出力ファイルに [[" が含まれてしまうなど不完全ですが，ここまでは作成できました．)

これを応用して，元のCSVファイル(3行)  
"test2.CSV" A1, 100, test  
A2, 100, test  
A3, 100, test  
がある場合に，各行の2列目に数字を割り当てるファイルを作成したいと考えています，  
例えば，6を割り当てる場合，  
A1 A2 A3  
6 0 0  
5 1 0  
5 0 1  
4 2 0  
4 0 2  
4 1 1  
・  
・  
1 0 5  
0 1 5  
0 0 6 (3H6= 28通り)

計28個のCSVファイルを作成したいと考えています．

例：1個目の出力ファイル "A1_600.csv"  
内容  
A1, 6, test  
A2, 0, test  
A3, 0, test  
例：2個目の出力ファイル "A1_510.csv"  
内容  
A1, 5, test  
A2, 1, test  
A3, 0, test

この場合のコードの書き方をご教示頂ければ幸いです．

**追加情報**  
  
実際にはA1...A6まで用意し，最初の質問文以上の数(5)を割り当てようとしています．  
  
A1 A2 A3 A4 A5 A6  
5 0 0 0 0 0  
4 1 0 0 0 0  
4 0 1 0 0 0  
・  
・  
0 0 0 0 0 4 (6H5= 252通り)

252通りなら，ゴリ押しでもいけるように思います．  
今回，私が実行していることは，

"test3.CSV"

A1, 0, test  
A2, 0, test  
A3, 0, test  
A4, 0, test  
A5, 0, test  
A6, 0, test  
B1, 0, test  
B2, 0, test  
B3, 0, test  
B4, 0, test  
B5, 0, test  
B6, 0, test  
C1, 0, test  
C2, 0, test  
C3, 0, test  
C4, 0, test  
D1, 0, test  
・  
・  
おそらく，合計44行程度

を用意した場合に，  
A 6,0,0,0,0,0 (252通り)  
B 6,0,0,0,0,0 (252通り)  
C 6,0,0,0 (56通り)  
D …

252×252×56… 通りのCSVファイルを作成しようとしています．  
そうなると，ゴリ押しではおそらく難しく，  
別の方法を検討する必要があるかもしれません．

**追加情報2**

元の管理ファイル"test3.csv" 例  
![元の管理ファイル"test3.csv" 例](https://i.stack.imgur.com/uEwC7.png)

変更したいのは緑色，太枠で囲った部分のみです．  
1,2列目のTEST,A はお伝えしやすくするために記載してありますが，  
値の変更は行いません．  
  
この場合，出力CSVは  
タイトル "A500000_B50000_C5000_D5000_E500_F500~.csv"  
となるのが理想です．

"A500000_B50000_C5000_D5000_E500_F500~.csv"  
"A410000_B50000_C5000_D5000_E500_F500~.csv"  
・  
・  
・  
"A000005_B00005_C0005_D0005_E005_F005~.csv"

ファイル数は，252*252*56*56*21*... 出来上がる．という感じです．

元の管理ファイル"test3.csv"内のA,B,C,D~の行数は変更しないため，  
地道にループ処理をしていくという方法でも良いように考えます．


CC BY-SA 3.0
2015-09-09T08:44:18.640
16434
2015-09-11T03:51:30.357
2015-09-10T12:58:49.120
12266
12266
question
ruby アルゴリズム
重複組み合わせ・CSVファイルへの記載方法


とりあえず「ゴリ押し実装」で作ってみました。  
組み合わせの作成は「ループで回しながら合致するものを抽出」というロジックになっています。  
この質問の個数ぐらいであれば問題にはならないですが、もっと個数が増えると非常に遅くなるかもしれません。（が、増える見込みがないのであれば、これでもいいのでは？と思います）

以下、実装とテストコード（RSpec）です。

[code]

    require 'csv'
    require 'spec_helper'
    
    class CombinationCsv
      def self.generate_csv(input_path, output_dir, assigned_number, col_size)
        input_arrays = CSV.read(input_path)
        generate_combination(assigned_number, col_size).each do |numbers|
          output_path = File.join(output_dir, "A1_#{numbers.join}.csv")
          CSV.open(output_path, 'w') do |csv|
            input_arrays.each_with_index do |input_cols, i|
              output_cols = input_cols.dup
              output_cols.each(&:strip!)
              output_cols[1] = numbers[i]
              csv << output_cols
            end
          end
        end
      end
    
      # ゴリ押し実装なので、もっとスマートなロジックがあるはず
      def self.generate_combination(assigned_number, col_size)
        max = "#{assigned_number}#{'0' * (col_size - 1)}".to_i
        (assigned_number..max).map { |number|
          target = number.to_s.chars.map(&:to_i).inject(:+) == assigned_number
          number.to_s.rjust(3, '0').chars.map(&:to_i) if target
        }.compact
      end
    end
    
    describe CombinationCsv do
      describe '::generate_csv' do
        let(:input_dir) { File.expand_path('../input', __FILE__) }
        let(:input_path) { File.join(input_dir, 'test2.csv') }
        let(:output_dir) { File.expand_path('../output', __FILE__) }
    
        def output_files
          Dir.glob(File.join(output_dir, '*.csv'))
        end
    
        before do
          FileUtils.rm(output_files)
        end
        example do
          expect {
            CombinationCsv.generate_csv(input_path, output_dir, 6, 3)
          }.to change { output_files.size }.from(0).to(28)
    
          output_path = File.join(output_dir, 'A1_600.csv')
          result = CSV.read(output_path)
          expect(result).to eq([
                                   %w(A1 6 test),
                                   %w(A2 0 test),
                                   %w(A3 0 test)
                               ])
    
          output_path = File.join(output_dir, 'A1_510.csv')
          result = CSV.read(output_path)
          expect(result).to eq([
                                   %w(A1 5 test),
                                   %w(A2 1 test),
                                   %w(A3 0 test)
                               ])
        end
      end
    
      describe '::generate_combination' do
        let(:expected) do
          [
              [6, 0, 0],
              [5, 1, 0],
              [5, 0, 1],
              [4, 2, 0],
              [4, 0, 2],
              [4, 1, 1],
              [3, 3, 0],
              [3, 0, 3],
              [3, 2, 1],
              [3, 1, 2],
              [2, 4, 0],
              [2, 0, 4],
              [2, 3, 1],
              [2, 1, 3],
              [2, 2, 2],
              [1, 5, 0],
              [1, 0, 5],
              [1, 4, 1],
              [1, 1, 4],
              [1, 3, 2],
              [1, 2, 3],
              [0, 6, 0],
              [0, 0, 6],
              [0, 5, 1],
              [0, 1, 5],
              [0, 4, 2],
              [0, 2, 4],
              [0, 3, 3]
          ]
        end
        example do
          result = CombinationCsv.generate_combination(6, 3)
          expect(result).to contain_exactly(*expected)
        end
      end
    end
    
[/code]

Githubにもコードをアップしているので、手元で動かすことも可能です。

<https://github.com/JunichiIto/combination-csv>

以上、ご参考までに。

### EDIT

最初のバージョンだと10以上の数を割り当てたときにおかしくなるので、ロジックを修正しました。

[code]

    require 'csv'
    require 'spec_helper'
    
    class CombinationCsv
      def self.generate_csv(input_path, output_dir, assigned_number, col_size)
        input_arrays = CSV.read(input_path)
        generate_combination(assigned_number, col_size).each do |numbers|
          output_path = File.join(output_dir, "A1_#{numbers.join}.csv")
          CSV.open(output_path, 'w') do |csv|
            input_arrays.each_with_index do |input_cols, i|
              output_cols = input_cols.dup
              output_cols.each(&:strip!)
              output_cols[1] = numbers[i]
              csv << output_cols
            end
          end
        end
      end
    
      def self.generate_combination(assigned_number, col_size)
        return [assigned_number] if col_size == 1
        assigned_number.downto(0).each_with_object([]) do |n, result|
          next_number = assigned_number - n
          child_results = generate_combination(next_number, col_size - 1)
          child_results.each do |numbers|
            result << [n, *numbers]
          end
        end
      end
    end
    
    describe CombinationCsv do
      describe '::generate_csv' do
        let(:input_dir) { File.expand_path('../input', __FILE__) }
        let(:input_path) { File.join(input_dir, 'test2.csv') }
        let(:output_dir) { File.expand_path('../output', __FILE__) }
    
        def output_files
          Dir.glob(File.join(output_dir, '*.csv'))
        end
    
        before do
          FileUtils.rm(output_files)
        end
        example do
          expect {
            CombinationCsv.generate_csv(input_path, output_dir, 6, 3)
          }.to change { output_files.size }.from(0).to(28)
    
          output_path = File.join(output_dir, 'A1_600.csv')
          result = CSV.read(output_path)
          expect(result).to eq([
                                   %w(A1 6 test),
                                   %w(A2 0 test),
                                   %w(A3 0 test)
                               ])
    
          output_path = File.join(output_dir, 'A1_510.csv')
          result = CSV.read(output_path)
          expect(result).to eq([
                                   %w(A1 5 test),
                                   %w(A2 1 test),
                                   %w(A3 0 test)
                               ])
        end
      end
    
      describe '::generate_combination' do
        let(:expected) do
          [
              [6, 0, 0],
              [5, 1, 0],
              [5, 0, 1],
              [4, 2, 0],
              [4, 0, 2],
              [4, 1, 1],
              [3, 3, 0],
              [3, 0, 3],
              [3, 2, 1],
              [3, 1, 2],
              [2, 4, 0],
              [2, 0, 4],
              [2, 3, 1],
              [2, 1, 3],
              [2, 2, 2],
              [1, 5, 0],
              [1, 0, 5],
              [1, 4, 1],
              [1, 1, 4],
              [1, 3, 2],
              [1, 2, 3],
              [0, 6, 0],
              [0, 0, 6],
              [0, 5, 1],
              [0, 1, 5],
              [0, 4, 2],
              [0, 2, 4],
              [0, 3, 3]
          ]
        end
        example do
          result = CombinationCsv.generate_combination(6, 3)
          expect(result).to contain_exactly(*expected)
        end
    
        context 'assigned_number is 10' do
          let(:expected) do
            [
              [10, 0],
              [9, 1],
              [8, 2],
              [7, 3],
              [6, 4],
              [5, 5],
              [4, 6],
              [3, 7],
              [2, 8],
              [1, 9],
              [0, 10]
            ]
          end
          example do
            result = CombinationCsv.generate_combination(10 ,2)
            expect(result).to contain_exactly(*expected)
          end
        end
      end
    end
    
[/code]

コードはこちらにも置いています。

<https://github.com/JunichiIto/combination-csv/tree/not-loop>

### EDIT2

A, B, Cといったグループごとに組み合わせを作り、さらにそれを掛け合わせてそれぞれ別ファイルとして出力する場合のサンプルコードを作ってみました。  
テストコードで使っているtest3-1.csvはA1,A2,B1,B2,C1の5行で、割り当てる数字は2です。  
3x3x1で9通りのファイルが生成されます。

[code]

    require 'csv'
    require 'spec_helper'
    
    class CombinationCsv
      def self.generate_csv(input_path, output_dir, assigned_number)
        input_arrays = CSV.read(input_path)
        all_combinations = generate_all_combinations(input_arrays, assigned_number)
        all_combinations.each do |combination|
          file_name = combination.map { |k, v| "#{k}#{v.join}" }.join('_') + '.csv'
          output_path = File.join(output_dir, file_name)
          CSV.open(output_path, 'w') do |csv|
            numbers = combination.values.flatten
            write_csv_rows(csv, input_arrays, numbers)
          end
        end
      end
    
      def self.generate_all_combinations(input_arrays, assigned_number)
        count_by_group = generate_count_by_group(input_arrays)
        combination_hash = generate_combination_by_group(count_by_group, assigned_number)
        all_arrays = combination_hash.values
        first = all_arrays.shift
        all_combinations = first.product(*all_arrays)
        names = combination_hash.keys
        all_combinations.map {|combinations| names.zip(combinations).to_h }
      end
    
      def self.generate_combination_by_group(count_by_group, assigned_number)
        count_by_group.map {|name, count|
          [name, generate_combination(assigned_number, count)]
        }.to_h
      end
    
      def self.generate_count_by_group(input_arrays)
        name_and_numbers = input_arrays.map{|cols|
          /(?<name>[A-Z]+)(?<number>\d+)/ =~ cols.first
          [name, number.to_i]
        }
        groups_by_name = name_and_numbers.each_with_object(Hash.new { |h,k| h[k] = [] }) do |(name, number), hash|
          hash[name] << number
        end
        groups_by_name.map{|name, values| [name, values.max]}.to_h
      end
    
      def self.write_csv_rows(csv, input_arrays, numbers)
        input_arrays.each_with_index do |input_cols, i|
          csv << input_cols.dup.tap do |output_cols|
            output_cols.each(&:strip!)
            output_cols[1] = numbers[i]
          end
        end
      end
    
      def self.generate_combination(assigned_number, col_size)
        return [[assigned_number]] if col_size == 1
        assigned_number.downto(0).flat_map do |n|
          child_results = generate_combination(assigned_number - n, col_size - 1)
          child_results.map { |numbers| [n, *numbers] }
        end
      end
    end
    
    describe CombinationCsv do
      describe '::generate_csv' do
        let(:input_dir) { File.expand_path('../input', __FILE__) }
        let(:input_path) { File.join(input_dir, 'test3-1.csv') }
        let(:output_dir) { File.expand_path('../output', __FILE__) }
    
        def output_files
          Dir.glob(File.join(output_dir, '*.csv'))
        end
    
        before do
          FileUtils.rm(output_files)
        end
        example do
          expect {
            CombinationCsv.generate_csv(input_path, output_dir, 2)
          }.to change { output_files.size }.from(0).to(9)
    
          output_path = File.join(output_dir, 'A20_B20_C2.csv')
          result = CSV.read(output_path)
          expect(result).to eq([
                                   %w(A1 2 test),
                                   %w(A2 0 test),
                                   %w(B1 2 test),
                                   %w(B2 0 test),
                                   %w(C1 2 test),
                               ])
    
          output_path = File.join(output_dir, 'A02_B02_C2.csv')
          result = CSV.read(output_path)
          expect(result).to eq([
                                   %w(A1 0 test),
                                   %w(A2 2 test),
                                   %w(B1 0 test),
                                   %w(B2 2 test),
                                   %w(C1 2 test),
                               ])
        end
      end
    
      # 以下省略
    end
    
[/code]

ソースコードはこちらにあります。

<https://github.com/JunichiIto/combination-csv/blob/multiple-
output/spec/combination_csv_spec.rb>

実際にやろうとしているのはこれよりももっと大きな組み合わせのようですが、252*252*56*56*21を計算するだけでも4,182,119,424(400億以上！)のファイルが作成されることになります。  
これだけのファイルを一気に作成しようとすると、普通のPCでは処理能力の限界を超えてしまいそうです。（怖いので試していません）

なので、そもそものアプローチを変える必要があるんじゃ無いかと思います。


CC BY-SA 3.0
2015-09-09T14:29:02.637
16450
2015-09-10T23:51:29.387
2015-09-10T23:51:29.387
85
85
16434
answer


こんな感じでどうでしょうか。ちょっとややこしい箇所はコメントを付けてますので参考にしてください。  
全体的に「Array祭り」みたいな作りになってますので、見慣れないメソッドは適宜お調べになってみてください。

[code]

    require 'csv'
    
    class CombinationCsvGenerator
      def initialize(src_path, total)
        @src = CSV.read(src_path).each { |columns| columns.each(&:strip!) }
        @total = total
        @rows = @src.size
      end
    
      def generate(dest_path)
        combination.each do |numbers|
          filepath = File.join(dest_path, "A1_#{numbers.join}.csv")
          # 元ネタ(@src)と組み合わせ数字(numbers)を組み合わせて、２列目だけをすり替える
          body = @src.zip(numbers).map {|columns| columns.flatten.values_at(0, -1, 2..-2) }
          output filepath, body
        end
      end
    
      private
    
      def combination
        # 重複順列を作成し、合計値が指定した値になるもののみselectする
        (0..@total).to_a
          .repeated_permutation(@rows)
          .select { |values| values.reduce(:+) == @total }
      end
    
      def output(path, body)
        CSV.open(path, 'w') do |csv|
          body.each { |columns| csv << columns }
        end
      end
    end
    
    
    if __FILE__ == $0
      generator = CombinationCsvGenerator.new('test2.csv', 6)
      generator.generate('output/')
    end
    
[/code]


CC BY-SA 3.0
2015-09-10T05:01:35.130
16460
2015-09-10T05:01:35.130
9608
16434
answer


ruby は不案内なのでアルゴリズムの説明だけします。

A1, A2, A3 に 6を割り当てる場合を例に説明します。これは A1, A2, A3 という三つの箱に
6つのボールを、個々のボールは区別せずに割り振る問題と同じです。これを解くために、ボールを一列に並べ、さらに三つに分けるために仕切りを二つ、S1 と S2
を用意します。仕切りをボールの間に置き三つに分けたら、一番左の組を A1 に、真ん中を A2 に、一番右を A3 に入れます。例えば ボールを O
で表すと、

[code]

    O S1 O O S2 O O O
    
[/code]

の場合はA1 = 1, A2 = 2, A3 = 3 で、

[code]

    O O S1 S2 0 0 0 0
    
[/code]

の場合は A1 = 2, A2 = 0, A3 = 4 になります。

仕切りの置ける場所は、一番左、個々のボールの間、一番右の、合計 ７ヶ所です。この
７ヶ所に置く、仕切りの置き方をすべて数え上げれば、三つの箱へのボールの割り振り方がすべて求められることになります。ただし、仕切りを単に入れ替えただけの場合は重複してしまうので、S2
は S1 の左には来ないという制約が必要になります。

具体的には、まず仕切りを二つとも左端に置き、S2 を一つずつ右に動かしていきます。

[code]

    A1 A2 A3
     0  0  6   S1 S2 O O O O O O
     0  1  5   S1 O S2 O O O O O
     0  2  4   S1 O O S2 O O O O
    ...
     0  6  0   S1 O O O O O O S2
    
[/code]

これで仕切り S1 が一番左、つまり A1 が 0 の場合がすべて求まりました。

次に S1 を一つ右に動かします。S2 は 、S1 の左には来れないので S1 と同じ場所に置き、前と同じように一つずつ右に動かしていきます。

[code]

    A1 A2 A3
     1  0  5   O S1 S2 O O O O O
     1  1  4   O S1 O S2 O O O O
    ...
     1  5  0   O S1 O O O O O S2
    
[/code]

これで A1 が 1 の場合がすべて求まりました。

このように S1 を一つ右に動かしては S2 を動かし、ということを繰り返し、最後に

[code]

    A1 A2 A3
     6  0  0   O O O O O O S1 S2
    
[/code]

となったところで終わりです。

以上を擬似コードで表すと、

[code]

    for S1 = 0 to 6 do
      for S2 = S1 to 6 do
        A1 = S1
        A2 = S2 - S1
        A3 = 6 - S2
      end for
    end for
    
[/code]

A1...A6 に 5 を割り当てる場合は

[code]

    for S1 = 0 to 5 do
      for S2 = S1 to 5 do
        for S3 = S2 to 5 do
          for S4 = S3 to 5 do
            for S5 = S4 to 5 do
              A1 = S1
              A2 = S2 - S1
              A3 = S3 - S2
              A4 = S4 - S3
              A5 = S5 - S4
              A6 = 5 - S5
            end for
          end for
        end for
      end for
    end for
    
[/code]

です。

この方法なら、きっちり必要な回数だけの繰り返しになるので、速く求められると思います。  
任意の個数の A に、任意の数を割り当てられるようにするのも、少し面倒ですができます。

ただ Junichi Ito さんもおっしゃっているように、252*252*56*56*21 はほぼ 4G です。test ファイルが 1 block
しか使わなくても、1block 1k バイトとしても、4Tバイトのディスク容量が必要になります。また 1秒間に1000 ファイル処理したとしても、4G
個だと 50日近く掛かる計算になります。

ファイルを経由せずに、パラメータを生成しては、シミュレーションのモジュールに直接渡せるなら、そっちの方がいいような気がします。


CC BY-SA 3.0
2015-09-11T03:51:30.357
16506
2015-09-11T03:51:30.357
3605
16434
answer