hp12c
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があります!

instance method 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

うん、今年もよく書いた。

みんなもチャンクしようぜ。チャンク!チャンク!



Please enable JavaScript to view the comments powered by Disqus. blog comments powered by Disqus
ruby_pack8

100円〜で好評発売中!
M'ELBORNE BOOKS


AltStyle によって変換されたページ (->オリジナル) /