26 December 2013
Rubyで不揃いのデータを集計する Enumerable#chunkの紹介
─ 問題 ─
ブログ記事に関する次のようなテキストデータがあって、Rubyを使って年別の記事数を集計したいとします。あなたならどうしますか?
2013
Dec 25 Blog Post22
Nov 10 Blog Post21
Aug 09 Blog Post20
Feb 06 Blog Post19
Jun 09 Blog Post18
Mar 11 Blog Post17
Jan 21 Blog Post16
Jan 02 Blog Post15
2012
Nov 20 Blog Post14
Oct 09 Blog Post13
Oct 05 Blog Post12
Sep 15 Blog Post11
Sep 10 Blog Post10
Feb 02 Blog Post9
2011
Dec 24 Blog Post8
Dec 03 Blog Post7
Nov 04 Blog Post6
Oct 12 Blog Post5
Aug 12 Blog Post4
Aug 02 Blog Post3
May 11 Blog Post2
Mar 18 Blog Post1
─ 僕のやり方 ─
一見簡単そうで、でもよく考えるとちょっと厄介そうな問題ですよね。状態遷移を管理する必要がありそうな…。
でも安心してください、RubyにはEnumerable#chunk
があります!
lines = File.readlines('blog_entries.txt')
.map(&:strip).reject(&:empty?)
chunks = lines.chunk { |line| line.match(/^\d{4}/).nil? }
chunks.to_a # => [[false, ["2013"]], [true, ["Dec 26 Blog Post23", "Dec 25 Blog Post22", "Nov 10 Blog Post21", "Aug 09 Blog Post20", "Feb 06 Blog Post19", "Jun 09 Blog Post18", "Mar 11 Blog Post17", "Jan 21 Blog Post16", "Jan 02 Blog Post15"]], [false, ["2012"]], [true, ["Nov 20 Blog Post14", "Oct 09 Blog Post13", "Oct 05 Blog Post12", "Sep 15 Blog Post11", "Sep 10 Blog Post10", "Feb 02 Blog Post9"]], [false, ["2011"]], [true, ["Dec 24 Blog Post8", "Dec 03 Blog Post7", "Nov 04 Blog Post6", "Oct 12 Blog Post5", "Aug 12 Blog Post4", "Aug 02 Blog Post3", "May 11 Blog Post2", "Mar 18 Blog Post1"]]]
chunkは各lineに対するブロックの評価結果が変化(遷移)する点を監視し、その点を区切りとしてlineを複数のチャンク(塊)に分けます。ここでは年のラベルにマッチする正規表現でチャンクを分けています。
こうなればあとは簡単ですね!
chunks.each_slice(2) do |(_, year), (_, entries)|
puts "%d => %d" % [year.first, entries.size]
end
# >> 2013 => 9
# >> 2012 => 6
# >> 2011 => 8
ちなみにブロック内におけるマッチ結果に対するnil評価(nil?)は必須です。chunkではブロックの返り値がnilになる場合はその行をチャンクの対象外にするからです(非マッチはnilを返します)。
lines = File.readlines('blog_entries.txt')
.map(&:strip).reject(&:empty?)
chunks = lines.chunk { |line| line.match(/^\d{4}/) }
chunks.to_a # => [[#<MatchData "2013">, ["2013"]], [#<MatchData "2012">, ["2012"]], [#<MatchData "2011">, ["2011"]]]
併せて各月の集計もしてみます。これはEnumerable#group_byで一発です。
chunks.each_slice(2) do |(_, year), (_, entries)|
puts "%d => %d" % [year.first, entries.size]
months = entries.group_by { |gr| gr.match(/^[A-Z][a-z]{2}/).to_s }
puts months.map { |mon, ents| " %s: %d" % [mon, ents.size] }
end
# >> 2013 => 9
# >> Dec: 2
# >> Nov: 1
# >> Aug: 1
# >> Feb: 1
# >> Jun: 1
# >> Mar: 1
# >> Jan: 2
# >> 2012 => 6
# >> Nov: 1
# >> Oct: 2
# >> Sep: 2
# >> Feb: 1
# >> 2011 => 8
# >> Dec: 2
# >> Nov: 1
# >> Oct: 1
# >> Aug: 2
# >> May: 1
# >> Mar: 1
このブログの記事数を集計してみる
そんなわけで…
このブログのトップページをコピペして記事数を集計してみます。
lines = File.readlines('hp12c_entries.txt')
.map(&:strip).reject(&:empty?)
chunks = lines.chunk { |line| line.match(/^\d{4}/).nil? }
chunks.each_slice(2) do |(_, year), (_, entries)|
puts "%d => %d" % [year.first, entries.size]
months = entries.group_by { |gr| gr.match(/^[A-Z][a-z]{2}/).to_s }
puts months.map { |mon, ents| " %s: %d" % [mon, ents.size] }
end
# >> 2013 => 75
# >> Dec: 7
# >> Nov: 4
# >> Oct: 11
# >> Sep: 8
# >> Aug: 7
# >> Jun: 1
# >> May: 3
# >> Apr: 8
# >> Mar: 6
# >> Feb: 11
# >> Jan: 9
# >> 2012 => 83
# >> Dec: 11
# >> Nov: 2
# >> Oct: 10
# >> Sep: 7
# >> Aug: 5
# >> Jul: 5
# >> Jun: 8
# >> May: 5
# >> Apr: 6
# >> Mar: 1
# >> Feb: 11
# >> Jan: 12
# >> 2011 => 60
# >> Dec: 13
# >> Nov: 4
# >> Oct: 5
# >> Sep: 4
# >> Aug: 6
# >> Jul: 6
# >> Jun: 7
# >> May: 2
# >> Feb: 6
# >> Jan: 7
# >> 2010 => 39
# >> Dec: 1
# >> Nov: 6
# >> Oct: 2
# >> Jul: 2
# >> Jun: 5
# >> May: 1
# >> Mar: 5
# >> Feb: 13
# >> Jan: 4
# >> 2009 => 56
# >> Oct: 1
# >> Sep: 1
# >> Aug: 1
# >> May: 2
# >> Apr: 20
# >> Mar: 5
# >> Feb: 8
# >> Jan: 18
# >> 2008 => 41
# >> Oct: 3
# >> Sep: 5
# >> Aug: 5
# >> Jul: 1
# >> Jun: 1
# >> Apr: 2
# >> Mar: 11
# >> Feb: 9
# >> Jan: 4
# >> 2007 => 31
# >> Dec: 2
# >> Nov: 2
# >> Oct: 2
# >> Sep: 2
# >> Aug: 2
# >> Jun: 3
# >> Apr: 3
# >> Mar: 5
# >> Feb: 3
# >> Jan: 7
うん、今年もよく書いた。
みんなもチャンクしようぜ。チャンク!チャンク!
blog comments powered by Disqus