2014年11月9日日曜日

『集合知プログラミング』のグラフを jQuery と Underscore.js で Canvas に書いてみる

最近、O'REILLY の『集合知プログラミング』って本を読んでいて、Python で書かれているサンプルコードを Haskell で書きなおしたりしているのだけど、もっとビジュアライズされた形で捉えてみたくて、Javascript でも書いてみた。

図は、2章の「推薦を行う」のユークリッド距離とピアソン相関の辺りに載っているグラフだけど、散布図の様子と2種類の類似性スコアの違いを、評者の組み合わせを選択して確認できるようにしてみた。

     (function(ch02Plot01, $, undefined) {
        var critics = {
          'Lisa': {'Lady': 2.5, 'Snakes': 3.5, 'Luck': 3.0, 'Superman': 3.5, 'Dupree': 2.5, 'Listener': 3.0},
          'Gene': {'Lady': 3.0, 'Snakes': 3.5, 'Luck': 1.5, 'Superman': 5.0, 'Listener': 3.0, 'Dupree': 3.5}, 
          'Michael': {'Lady': 2.5, 'Snakes': 3.0, 'Superman': 3.5, 'Listener': 4.0},
          'Claudia': {'Snakes': 3.5, 'Luck': 3.0, 'Listener': 4.5, 'Superman': 4.0, 'Dupree': 2.5},
          'Mick': {'Lady': 3.0, 'Snakes': 4.0, 'Luck': 2.0, 'Superman': 3.0, 'Listener': 3.0, 'Dupree': 2.0}, 
          'Jack': {'Lady': 3.0, 'Snakes': 4.0, 'Listener': 3.0, 'Superman': 5.0, 'Dupree': 3.5},
          'Toby': {'Snakes':4.5,'Dupree':1.0,'Superman':4.0}};

        var $this = ch02Plot01;

        var ox = 40;
        var oy = 390;
        var size = 64;
        $this.drawLine = function(ctx, x1, y1, x2, y2) {
          ctx.moveTo(x1, y1);
          ctx.lineTo(x2, y2);
        };
        $this.drawAxis = function() {
          var ctx = $('#pci-ch02-plot').get(0).getContext('2d');
          ctx.beginPath();
          $this.drawLine(ctx, ox, oy, ox, oy-(size*11/2));
          $this.drawLine(ctx, ox, oy, ox+(size*11/2), oy);
          _.each(_.range(1, 6), function(e){
           console.log($this);
           $this.drawLine(ctx, ox-2, oy-e*size, ox+2, oy-e*size);
            $this.drawLine(ctx, ox+e*size, oy+2, ox+e*size, oy-2);
            ctx.font = "bold 10px Gorgia";
            ctx.fillStyle = 'white';
            ctx.fillText(e, ox+e*size-5, oy + 15);
            ctx.fillText(e, ox-15, oy-e*size+5);
          });
          ctx.strokeStyle = 'white';
          ctx.stroke();
          console.log(ox + ":" + oy);
        };
        function plot(critic1, critic2) {
      var pv = critics[critic1];
      var ph = critics[critic2];
          var duplications = {};
      var watchedByBoth = _.intersection(_.keys(pv), _.keys(ph));
          var ctx = $('#pci-ch02-plot').get(0).getContext('2d');
          ctx.clearRect(ox+1, oy-(size*11/2), size*11/2, size*11/2);
          ctx.font = "12px Gorgia";
      _.each(watchedByBoth, function(title) {
       var pos = [ph[title], pv[title]];
   var dupLevel = duplications[pos];       
            if (!dupLevel) dupLevel = 0;
       var cx = ox+ph[title]*size;
       var cy = oy-pv[title]*size;
   duplications[pos] = dupLevel + 1;

            ctx.beginPath();
            ctx.arc(cx, cy, 2, 2*Math.PI, false);
            ctx.stroke();
            var metrix = ctx.measureText(title);
            ctx.fillText(title, cx-metrix.width/2, cy-5-dupLevel*13);
      });
        }
        $this.onSelectionChanged = function() {
          var $radio = $(this);
          var ctx = $('#pci-ch02-plot').get(0).getContext('2d');
          var fullName = $radio.next().text();
          if ($radio.attr('name') == 'critic1') {
            ctx.clearRect(0, 0, 100, oy-size*11/2);
            ctx.font = "bold 12px Gorgia";
            ctx.fillText(fullName, 10, oy-size*11/2-10);            
          }
          if ($radio.attr('name') == 'critic2') {
            ctx.clearRect(ox, oy+16, 1400, 100);
            ctx.font = "bold 12px Gorgia";
            var metrix = ctx.measureText(fullName);
            ctx.fillText(fullName, ox+size*11/2-metrix.width, oy+30);            
          }
          var critic1 = $('input:radio:checked[name="critic1"]').attr('value');
          var critic2 = $('input:radio:checked[name="critic2"]').attr('value');
          if (critic1 && critic2) {
            plot(critic1, critic2);
            calcSimilarity(critic1, critic2);
          }
        };
        function calcSimilarity(critic1, critic2) {
        var pv = critics[critic1];
        var ph = critics[critic2];
        var watchedByBoth = _.intersection(_.keys(pv), _.keys(ph));
        if (watchedByBoth==[]) {
          console.log(0);
        } else {
          var m = _.chain(watchedByBoth)
            .map(function(title){
              var a = pv[title];
              var b = ph[title];
                  return [a, b, a*a, b*b, a*b, (a-b)*(a-b)];})
            .reduce(function(acc, cur) {
                return [acc[0]+cur[0], acc[1]+cur[1], acc[2]+cur[2], acc[3]+cur[3], acc[4]+cur[4], acc[5]+cur[5]];
            }, [0, 0, 0, 0, 0, 0])
            .value();
          var n      = watchedByBoth.length;
          var sum1   = m[0];
          var sum2   = m[1];
          var sum1Sq = m[2];
          var sum2Sq = m[3];
          var pSum   = m[4];
          var diffSq = m[5];
            var num    = pSum - sum1*sum2/n;
            var den    = Math.pow((sum1Sq-sum1*sum1/n)*(sum2Sq-sum2*sum2/n), 0.5);
            if (den != 0) {
              $('#peason').text(num/den);
            } else { $('#peason').text(0); }
            $('#eucrid').text(1/(1+diffSq));
        }
        }
     } (window.ch02Plot01 = window.ch02Plot01 || {}, jQuery));

     $(function() {
       ch02Plot01.drawAxis();
       $('input:radio').change(ch02Plot01.onSelectionChanged);
       $('input:radio#Lisa1').prop('checked', true).trigger('change');
       $('input:radio#Gene2').prop('checked', true).trigger('change');
     });
Eucrid Distance:
Peason Correlation:















技術的には、Canvas と、jQuery と underscore.js を使ってみた。

underscore.js は初めて使ってみたが悪くない。

0 件のコメント:

コメントを投稿