2008年声優言及数 作業メモ(3) 言及数グラフデータを集計

グラフデータの取得が終わったので、取得したデータを解析。

PNGから言及数を解析する

この辺。
「2006年声優言及数」の検索結果 - XXXannex

結構泥臭い感じで、libpngを使って解析。こういうのはCでやった方が簡単かなー。

今年の罠としては、グラフのボーダーの色が変わってるというところ。

こっちが去年のグラフ。

PNGのパレット情報(PLTEチャンク)。

chunk PLTE:
---------------------------------
entries: 6
  0: (ff, ff, ff)
  1: (00, ff, 00)
  2: (ff, 00, 00)
  3: (7f, 7f, 7f)
  4: (00, 00, 00)
  5: (52, 79, e7)

こっちが今年のグラフ。

パレット情報。

chunk PLTE:
---------------------------------
entries: 6
  0: (ff, ff, ff)
  1: (00, ff, 00)
  2: (ff, 00, 00)
  3: (7f, 7f, 7f)
  4: (00, 00, 00)
  5: (33, 33, 33)

おそらくキーワードの仕様変更に伴ってグラフ周りも変わったと思うんだけど、ボーダーが水色(52, 79, e7)から黒(33, 33, 33)に変わっている!去年のプログラムがうまく動かなくて焦ったけど、どうやらそういうことらしい。ボーダーの色の変更をプログラムに反映させるとうまく動きました。しかし、ボーダーの色と文字の色が同じになってしまっていたら、もっと大規模な変更を余儀なくされたなあ・・・。ぱっと見、色の区別が付かなかったから心配したよ。

グラフの正当性チェック

作りたてのキーワードだと「情報を取得できません」という画像が表示されてしまうようです。
pngcheck Home Page からpngcheckをダウンロードして*1チェックしてみる。

$ pngcheck -pv %C6%A3%B5%C8%B5%D7%C8%FE%BB%D2_2008-12-31_365.png
File: %C6%A3%B5%C8%B5%D7%C8%FE%BB%D2_2008-12-31_365.png (2187 bytes)
  chunk IHDR at offset 0x0000c, length 13
    400 x 200 image, 8-bit colormap, non-interlaced
  chunk gAMA at offset 0x00025, length 4: 0.45000
  chunk tEXt at offset 0x00035, length 25, keyword: Software
  chunk PLTE at offset 0x0005a, length 12  PLTE chunk: 4 palette entries
    0:  ( 82,121,231) = (0x52,0x79,0xe7)
    1:  (179,179,179) = (0xb3,0xb3,0xb3)
    2:  (  0,  0,  0) = (0x00,0x00,0x00)
    3:  (255,255,255) = (0xff,0xff,0xff)
  chunk IDAT at offset 0x00072, length 2053
    zlib:  deflated, 32K window, maximum compression
  chunk IEND at offset 0x00883, length 0
No errors detected in %C6%A3%B5%C8%B5%D7%C8%FE%BB%D2_2008-12-31_365.png (97.3% compression).

普通の(?)グラフデータのヘッダー構造はこんな感じ。色々違います。

$ pngcheck -pv %A1%BA%CC%EE%BD%E1%BB%D2_2008-12-31_365.png
File: %A1%BA%CC%EE%BD%E1%BB%D2_2008-12-31_365.png (1651 bytes)
  chunk IHDR at offset 0x0000c, length 13
    400 x 200 image, 4-bit colormap, interlaced
  chunk PLTE at offset 0x00025, length 18  PLTE chunk: 6 palette entries
    0:  (255,255,255) = (0xff,0xff,0xff)
    1:  (  0,255,  0) = (0x00,0xff,0x00)
    2:  (255,  0,  0) = (0xff,0x00,0x00)
    3:  (127,127,127) = (0x7f,0x7f,0x7f)
    4:  (  0,  0,  0) = (0x00,0x00,0x00)
    5:  ( 51, 51, 51) = (0x33,0x33,0x33)
  chunk tRNS at offset 0x00043, length 1  tRNS chunk: 1 transparency entry
    0:    0 = 0x00
  chunk IDAT at offset 0x00050, length 1551
    zlib:  deflated, 32K window, default compression
  chunk IEND at offset 0x0066b, length 0
No errors detected in %A1%BA%CC%EE%BD%E1%BB%D2_2008-12-31_365.png (95.9% compression).

藤吉久美子とは - はてなキーワードを見ると、キーワードが作成されたのが1/1だから、その辺の都合のようですね。

一通りチェックしたら解析ツールにかける。

$ ls | xargs readpng > readpng.out

こんなかんじで。前は1ファイルずつやっていたんだけど、ファイル数が多くなると時間がかかってしまうので、複数ファイルを受け入れられるようにしました。

月ごとにマージ

readpngは、こんな感じの出力を出します。

input file: [%B9%E2%B3%C0%BA%CC%CD%DB_2008-12-31_365.png]
png signature: 89 50 4e 47 0d 0a 1a 0a
graph border: t=17 b=161 l=27 r=389
max_value area: sh=13 sw=0 eh=20 ew=27
            33  3333
           3  3 3
              3 333
             3     3
            3      3
           3    3  3
           3333  33
max_value bit_pattern:
00000000000011001111000000000000
00000000000100101000000000000000
00000000000000101110000000000000
00000000000001000001000000000000
00000000000010000001000000000000
00000000000100001001000000000000
00000000000111100110000000000000
max_value hex_pattern: {0x000cf000, 0x00128000, 0x0002e000, 0x00041000, 0x00081000, 0x00109000, 0x001e6000, },
count_max: 25.0
start point:(h=16, w=28)
end point  :(h=161, w=389)
count:1.03 2.07 2.41 2.93 5.52 8.97 4.48 1.55 2.07 1.03 1.38 3.97 5.00 3.45 1.03 1.03 1.03 2.93 3.97 3.45 1.38 0.00 ......

グラフの最大値を求めるために、ビットマップのマッピングを決め打ちで見る、という結構泥臭い感じですが、これでも結構うまく行くんだよね。

それはともかく。出力中のinput fileとcountの項目にデータが入っているのですが、これは単に数字を読み取っただけなので、ここから月ごとのカウントをマージする必要があります。プログラム内部でそこまでやってもいいんだけど、まあ、作業は分割した方がシンプルに行くので、ここはスクリプトで解析させることに。

use strict;
use warnings;
use File::Basename;
use POSIX;
use Date::Simple qw(:all);
use List::Util qw(sum);
use List::MoreUtils qw(any);
use URI::Escape;
# use Smart::Comments;

## usage: $0 start[YYYYMM] end[YYYYMM] < readpng.out

my $start = shift;
my $end   = shift;
my @count_range = make_range_keys($start, $end);
die("count_range is null") unless(@count_range);

## data structure
## %count{$name} = {
##    YYYYMM => count  # by month
##    or
##    YYYYWW => count  # by week
## }

# notice: filename must be "<URI-escaped name>_YYYY-MM-DD_RANGE.png"
my ($name, $year, $month, $day, $range, @lines);
while(<>){
  tr/\x0D\x0A//d;
  if(/^input file: \[(.+)\]$/){ 
    scalar(fileparse($1, '.png')) =~ /(.+)_(\d{4})\-(\d{2})\-(\d{2})_(\d+)/o;
    ($name, $year, $month, $day, $range) = ($1, $2, $3, $4, $5);
    die('specify "<URI-escaped name>_YYYY-MM-DD_RANGE.png" as input file!') if any { !$_ } ($name, $year, $month, $day, $range);
  }
  if(/^count:(.+)/){
    my @count = map { ($_ < 0) ? 0 : $_} split(/\s+/, $1);  # count=-1 if no data
    push(@lines, [$name, $year, $month, $day, $range, @count]);
    ($name, $year, $month, $day, $range) = (-1, -1, -1, -1, -1);
  }
}

my $count = count_by_month(\@lines);

print join("\t", (qw(key name), @count_range)) . "\n";
foreach my $name (keys %$count){
  print "$name\t" . URI::Escape::uri_unescape($name);
  foreach my $ym (@count_range){
    my $v = $count->{$name}->{$ym};
    print "\t", (($v) ? $v : 0);
  }
  print "\n";
}

sub make_range_keys{
  my $start = d8($_[0] . "01") || return;
  my $end   = d8($_[1] . "01") || return;
  my @range;
  for(my $i=$start; $i<=$end; $i += days_in_month($i->year, $i->month)){
    push(@range, $i->format('%Y%m'));
  }
  return @range;
}

sub count_by_month{
  my $lines = shift;
  my %count;

  foreach(@$lines){
    my ($name, $year, $month, $day, $range, @count) = @$_;
    my $d_end   = date(sprintf("%04d%02d%02d", $year, $month, $day));
    my $d_start = $d_end - $range;

    my %m_range;
    for(my $i=$d_start; $i<=$d_end; $i=$i->next()){
      $m_range{$i->format('%Y%m')}++;
    }
    my @m_range_keys  = sort keys(%m_range);
    my @m_range_value = @m_range{@m_range_keys};
    @m_range_value    = map{$_ * @count/sum(@m_range_value)} cumsum(@m_range_value);
    my @count_cum     = (0, cumsum(@count));
    my @count_by_month;
    foreach my $n (@m_range_value){
      my $p = ceil($n) - 1;  # 1.5 -> 1+0.5, 2.0 -> 1+1.0 : 1 = right hand side of cumsum(count)[0]
      push(@count_by_month, $count_cum[$p] + $count[$p]*($n-$p));
    }
    @count_by_month = ($count_by_month[0], diff(@count_by_month));
    # store to table
    my %h; @h{@m_range_keys} = @count_by_month;
    $count{$name} = {%h};
  }
  return \%count;
}

sub cumsum{ my $v = 0; map{ $v+=$_; } @_; }
sub diff{ map { $_[$_] - $_[$_-1] } (1..$#_) }

シンプルにこんな感じですかね。(ちょっと早くした →)

$ perl readpng2r.pl 200801 200812 < readpng.out > readpng2r.out

readpng2rのネーミングの由来は、確かこのあとRで解析する予定だったから・・・のはず。

key     name    200801  200802  200803 ...
%B9%E2%B3%C0%BA%CC%CD%DB        高垣彩陽        68.7397814207651        35.5718579234972        119.016475409836 ...

こんな感じで出力されてきます。

過去データとマージ

過去のデータとマージします。まあ、この辺もシンプルに作っておきます。

use strict;
use warnings;
use List::MoreUtils qw(uniq);
use URI::Escape;

# usage: $0 file1 file2

# input data format:
# key     name    200801  200802  200803 ...
# %B9%E2%B3%C0%BA%CC%CD%DB        高垣彩陽        68.7397814207651        35.5718579234972        119.016475409836 ...

my ($head_0, $count_0) = readfile($ARGV[0]);
my ($head_1, $count_1) = readfile($ARGV[1]);

print join("\t", qw(key name), @$head_0, @$head_1) . "\n";
foreach my $n (sort(uniq(keys %$count_0, keys %$count_0))){
  print "$n\t" . URI::Escape::uri_unescape($n) . "\t";
  print join("\t", ($count_0->{$n} ? @{$count_0->{$n}} : (0) x @$head_0));
  print "\t";
  print join("\t", ($count_1->{$n} ? @{$count_1->{$n}} : (0) x @$head_1));
  print "\n";
}

sub readfile{
  my $infile = shift;
  open(F, $infile) || die("can't open $infile");
  my ($head, @lines) = map{ tr/\x0A\x0D//d; $_ } <F>;
  my @header = split(/\t/, $head);
  my %count  = map{
    my ($key, undef, @cnt) = split(/\t/);
    ($key => [@cnt]);
  } @lines;
  return([@header[2 .. $#header]], \%count);  # exclude first two data(key,name) in header
}

*1:他のものでもいいけど・・・libpngのお手本としては見ておくべき