Rubyでは、配列を1次元にするflattenメソッドが提供されています。このメソッドは、配列の中の入れ子をなくして、一つのデータ系列であるように扱うのに便利です。また、flattenは、ハッシュについても適用できますので、それについても紹介します。
Rubyのflattenメソッドとは
Rubyにおけるflattenメソッドとは、ある配列の中に、さらに入れ子で配列があるときに、その深さをなくして配列を1次元にするメソッドです。つまり、配列の途中の[]がない配列に変換されます。
flattenメソッドの使い方
Rubyにおけるflattenメソッドの構文は以下の通りです。
<オブジェクト>.flatten(<深度>)
引数を指定しない場合、すべての入れ子が取り除かれます。引数を指定した場合は、外側から数えて、指定した深度まで入れ子を取り除きます。
入れ子を含んだ配列を1次元配列にする
入れ子配列のままlengthをとった場合、最も外側の配列は1つと数えます。よって、すべての要素の数を調べるには、一度flattenしてからlengthをとる必要があります。
nested=[[1,3,7,[8,3],2,4],[9,6,[1,3,4],2]]
p nested.length #=> 2
p nested.flatten #=> [1, 3, 7, 8, 3, 2, 4, 9, 6, 1, 3, 4, 2]
p nested.flatten.length #=> 13
深度を指定して入れ子を取り除く
flattenメソッドでは、引数で深度を指定することもできます。この場合、指定した深度まで入れ子が取り除かれます。
nested=[[1,3,7,[8,3],2,4],[9,6,[1,3,4],2]]
p nested.flatten(1) #=> [1, 3, 7, [8, 3], 2, 4, 9, 6, [1, 3, 4], 2]
p nested.flatten(1).length #=> 10
mapメソッドと組み合わせて使用する
flattenメソッドは、mapメソッドと組み合わせると、別の配列要素を各要素の隣に追加することができます。以下のRubyスクリプトでは、配列の各要素の隣に、1足した値を追加します。
nums=[4, 7, 8, 8, 3, 1, 4, 2, 1, 6]
p nums.map{|n|[n,n+1]}.flatten #=> [4, 5, 7, 8, 8, 9, 8, 9, 3, 4, 1, 2, 4, 5, 2, 3, 1, 2, 6, 7]
ハッシュを1次元配列にする
ハッシュの1次元化とは、{キー1 => 値1, キー2 => 値2 … }となるハッシュに対し、[キー1, 値1, キー2, 値2, …]という1次元配列に変換することです。キーと値が交互に並ぶので、先頭インデックスを0としたとき、偶数インデックスがキー、奇数インデックスが値となります。
ハッシュにおいてflattenを使用する
Rubyにおいては、ハッシュ(Hashクラス)についてもflattenメソッドが定義されています。ハッシュ1つのアイテムにつき、2つの要素に変換されるので、lengthの値は2倍となります
prices={"Apple"=>100, "Banana"=>97, "Orange"=>113, "Peach"=>107, "Kiwi"=>120}
p prices.length #=> 5
p prices.flatten #=> ["Apple", 100, "Banana", 97, "Orange", 113, "Peach", 107, "Kiwi", 120]
p prices.length #=> 10
flatten!メソッドの使い方
flatten!は破壊的メソッドで、オブジェクトそのものが変更されます。データが行ごとに配列でまとめられているとき、要素がすべてデータの1次元配列に変換するのに便利なメソッドです。
flatten!を使用してファイルから数値データを得る
まず、入力データの作成のため、1から30までの整数をランダムに出力するRubyスクリプトを作成します。コマンドライン引数で、行(row)と列(clm)の値を指定してrand_nums.csvファイルに出力を行います。区切り文字はコンマです。
データ作成スクリプト
abort"行と列の値が指定されていません" unless ARGV[1]
row,col=ARGV.map{|i|i.to_i}
abort"行または列の値が不正です" unless row>0 && col>0
fout=File.open("rand_nums.csv","w")
for i in 1..row
fout.puts Array.new(col).map{|n|"%2d"%Random.rand(1..30)}.join(",")
end
fout.close
puts"書込みが完了しました"
puts"総数: #{row*col}"
rand_nums.csvの内容
以下は、row=10,col=10とした場合のrand_nums.csvの内容です。1行にある整数は10個で、10行から成るので、全部で100個の整数があります。行頭の()は行番号を表します。
( 1) 18,19,26,19,24,18,14, 9,17,17
( 2) 22,20,12, 4, 2, 6,29, 3, 9,30
( 3) 9,16,30,29,15,21,27,10,16,25
( 4) 3,29,15, 1,29, 6,14, 7, 5,23
( 5) 25,12, 8,16, 7, 6,20,19, 7,22
( 6) 14,17, 6,13,30, 1, 5, 4,29, 3
( 7) 25,23, 8, 9,19,19,22, 3,21, 1
( 8) 26,15, 6,28,15,25,28,15,20,20
( 9) 17,11,12,18,11,12,22,27, 8,27
(10) 6,19,17,25,11,19, 2,27,13,16
データ処理スクリプト
以下は、このデータファイルについて処理を行うRubyスクリプトです。まず、それぞれの行をタブで分割した文字列の配列に変換し、その配列をnumsに格納していきます。すべて読み終えた後に、flatten!で要素がすべて整数の1次元配列に変換しています。
nums=[]
begin
File.open("rand_nums.csv").each do |ln|
nums.push(ln.split(",").map{|i|i.to_i})
end
rescue => ex
abort ex.message
end
nums.each do |arr|
p arr
end
puts"要素数: #{nums.length}"
nums.flatten!
puts
p nums
puts"要素数: #{nums.length}"
count=Hash.new(0)
nums.each do |num|
count[num]+=1
end
puts
puts"<頻度>"
for k in 1..30 do
printf"(%2d) %d",k,count[k]
print k%5==0? "\n":" "
end
実行結果
[18, 19, 26, 19, 24, 18, 14, 9, 17, 17]
[22, 20, 12, 4, 2, 6, 29, 3, 9, 30]
[9, 16, 30, 29, 15, 21, 27, 10, 16, 25]
[3, 29, 15, 1, 29, 6, 14, 7, 5, 23]
[25, 12, 8, 16, 7, 6, 20, 19, 7, 22]
[14, 17, 6, 13, 30, 1, 5, 4, 29, 3]
[25, 23, 8, 9, 19, 19, 22, 3, 21, 1]
[26, 15, 6, 28, 15, 25, 28, 15, 20, 20]
[17, 11, 12, 18, 11, 12, 22, 27, 8, 27]
[6, 19, 17, 25, 11, 19, 2, 27, 13, 16]
要素数: 10
[18, 19, 26, 19, 24, 18, 14, 9, 17, 17, 22, 20, 12, 4, 2, 6, 29, 3, 9, 30, 9, 16, 30, 29, 15, 21, 27, 10, 16, 25, 3, 29, 15, 1, 29, 6, 14, 7, 5, 23, 25, 12, 8, 16, 7, 6, 20, 19, 7, 22, 14, 17, 6, 13, 30, 1, 5, 4, 29, 3, 25, 23, 8, 9, 19, 19, 22, 3, 21, 1, 26, 15, 6, 28, 15, 25, 28, 15, 20, 20, 17, 11, 12, 18, 11, 12, 22, 27, 8, 27, 6, 19, 17, 25, 11, 19, 2, 27, 13, 16]
要素数: 100
<頻度>
( 1) 3 ( 2) 2 ( 3) 4 ( 4) 2 ( 5) 2
( 6) 6 ( 7) 3 ( 8) 3 ( 9) 4 (10) 1
(11) 3 (12) 4 (13) 2 (14) 3 (15) 5
(16) 4 (17) 5 (18) 3 (19) 7 (20) 4
(21) 2 (22) 4 (23) 2 (24) 1 (25) 5
(26) 2 (27) 4 (28) 2 (29) 5 (30) 3
処理速度の比較
flatten!を使わなくても、行で分割し、eachメソッドを使ってそれぞれの値をnums配列にpushして追加するという方法も考えられます。では、この方法と比べて速度に違いはあるのでしょうか?row=20、clm=500,000として、以下のRubyスクリプトで比較してみます。
処理速度比較スクリプト
require"benchmark"
nums=[]
t1=Benchmark.realtime do |f|
File.open("rand_nums.csv").each do |ln|
nums.push(ln.split(",").map{|i|i.to_i})
end
nums.flatten!
end
puts"%.3f(s)"%t1
nums=[]
t2=Benchmark.realtime do |f|
File.open("rand_nums.csv").each do |ln|
ln.split(",").map{|i|i.to_i}.each do |n|
nums.push(n)
end
end
end
puts"%.3f(s)"%t2 #=> 4.151(s)
行ごとに、数値データを配列に追加する方が、やや速くなりました。flattenは全体としての配列が大きいときには、配列の1次元化に時間がかかることが分かります。
flattenメソッドでは、すべてを同じ系列として処理できる
Rubyにおけるflattenメソッドは、配列においては、入れ子になった配列を、すべてのデータを取り出した1次元の配列に変換することができます。また、ハッシュにおいては、すべてのキーと値を交互に並べた配列に変換できます。
よって、flattenメソッドは、オブジェクトに含まれるデータを、すべて同じ系列として処理したいときに使用するとよいでしょう。