PR

Rubyのflattenを使って配列を1次元にする方法を解説

Ruby・Rails

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メソッドは、オブジェクトに含まれるデータを、すべて同じ系列として処理したいときに使用するとよいでしょう。

タイトルとURLをコピーしました