search
LoginSignup
20

More than 5 years have passed since last update.

posted at

updated at

Ruby+d3.js+garb(Google-analytics-API)でユーザーの経路(パス)を描画

d3.js Advent Calendar 2014
http://qiita.com/advent-calendar/2014/d3

この記事に影響を受けて、Rails4+d3.js+garb(Google-analytics-API)でユーザー経路(パス)描画を実装してみました。
http://maulik-kamdar.com/2012/12/visualizing-paths-using-google-analytics/

Google Analyticsにあるフローレポートを補完する位置付けのグラフになります。
https://support.google.com/analytics/answer/2519986?hl=ja

GoogleAnalyticsにあるユーザーの流れを表すフローレポート(上記URLに含まれる画像をもってきました)

FlowVizAnatomy.png

下画像が今回の記事で出力されるグラフの画像。青丸がnodeでページ。線がページからページへと移動したユーザーの多さです。

dev.seoanalytics.jp-3000_home-index.png

依存するライブラリ

d3.js

可視化ライブラリで今回はForce-Directed Graphを利用しています。
http://bl.ocks.org/mbostock/4062045

garb

A Ruby wrapper for the Google Analytics API
https://github.com/Sija/garb

GoogleAnalyticsのレポートAPIをRubyから簡単に使うためのgemです。

本題

GoogleAnalyticsAPIの認証

ユーザーとパスワードを利用するか、OAuthを利用する方法で認証を行います。以下はOAuthを使って認証するサンプルコードです。

client = OAuth2::Client.new(client_id, client_secret, :site => 'http://dev.xxxxxxxxxxx.jp:3000/')
session = Garb::Session.new
session.access_token = OAuth2::AccessToken.new(client, google_authentication.value)
profile = Garb::Management::Profile.all(session).find{|v| v.web_property_id == "UA-xxxxxx-x"} # 

サンプルで動かすにはユーザーとパスワードを使うのが良いと思います。

Garb::Session.api_key = api_key # required for 2-step authentication
Garb::Session.login(username, password)

今回はウェブアプリを作ってる途中で、Google APIs Console(https://code.google.com/apis/console/) への登録や、Googleとの認証に関する部分が既に実装してあるのでOAuthを使って認証しました。

GoogleAnalyticsAPIからデータを取得

grabを使うと以下のようにレポートのルールを分かりやすく定義できます。PV数を前ページと次に移動したページを軸に取得します。

class GaPageviews
  extend Garb::Model
  metrics :pageviews
  dimensions :previousPagePath, :nextPagePath
end

このクラスを使って、日付を2014/10/1~2014/10/30と指定した結果を取得します。

results = GaPageviews.results(profile, {
  :start_date => Date.new(2014, 10, 1),
  :end_date => Date.new(2014, 10, 30),
  :filters => {:page_path.substring => '/venue'},
  :sort => :pageviews.desc,
})

取得したデータの加工

resultsの内容は以下のようにOpenStructになっていて、ここからPV数と前ページ、次ページが取れます。

#<OpenStruct previous_page_path="(entrance)", next_page_path="/venue/17", pageviews="532">
#<OpenStruct previous_page_path="(entrance)", next_page_path="/venue/45", pageviews="522">
#<OpenStruct previous_page_path="(entrance)", next_page_path="/venue/42", pageviews="510">

次にこの値をd3.jsのForce-Directed Graphで表示するための、ノード一覧とリンク一覧にデータを変換して行きます。

keys = {}
nodes = {}
links = {}

results.each{|v|
  [v.previous_page_path, v.next_page_path].each{|path|
    keys[path] ||= nodes.size
    nodes[path] ||= {
      number: nodes.size,
      name: path,
      group: 1,
      radius: 26,
      type: "uri",
      description: path,
      value: 0,
    }
    nodes[path][:value] += v.pageviews.to_i # ここがノードの丸の大きさ
  }
}

results.each{|v|
  key = (v.previous_page_path + "_x_" + v.next_page_path)
  links[key] ||= {
    name: "#{v.previous_page_path} to #{v.next_page_path}",
    source: keys[v.previous_page_path],
    target: keys[v.next_page_path],
    source_origin: v.previous_page_path,
    target_origin: v.next_page_path,
    value: 0
  }
  links[key][:value] += v.pageviews.to_i # ここがラインの太さ
}

links.keys.each{|key|
  if links[key][:value] < 50 # PVが50に満たないのを切り捨てる
    links.delete(key)
  end
}

# links(線)があるノードを集めて、
active_node_paths = []
links.values.each{|link|
  active_node_paths << link[:source_origin]
  active_node_paths << link[:target_origin]
}

# links(線)がないノードを削除します。
(nodes.keys - active_node_paths.uniq).each{|path|
  nodes.delete(path)
}

keysの内容はこのようにパスとlinksのsourceとtarget(リンクのつながり)で使う数字が入ります。

["(entrance)", 0]
["/venue/17", 1]
["/venue/45", 2]
["/venue/42", 3]

nodesはこのようになります。

["(entrance)", {:number=>0, :name=>"(entrance)", :group=>1, :radius=>26, :type=>"uri", :description=>"(entrance)", :value=>8594}]
["/venue/17", {:number=>1, :name=>"/venue/17", :group=>1, :radius=>26, :type=>"uri", :description=>"/venue/17", :value=>1592}]
["/venue/45", {:number=>2, :name=>"/venue/45", :group=>1, :radius=>26, :type=>"uri", :description=>"/venue/45", :value=>1568}]

linksにはノードとノードを繋ぐための情報が入っていますね。

["(entrance)_x_/venue/17", {:name=>"(entrance) to /venue/17", :source=>0, :target=>1, :source_origin=>"(entrance)", :target_origin=>"/venue/17", :value=>532}]
["(entrance)_x_/venue/45", {:name=>"(entrance) to /venue/45", :source=>0, :target=>2, :source_origin=>"(entrance)", :target_origin=>"/venue/45", :value=>522}]
["(entrance)_x_/venue/42", {:name=>"(entrance) to /venue/42", :source=>0, :target=>3, :source_origin=>"(entrance)", :target_origin=>"/venue/42", :value=>510}]

これをjavascriptから扱えるようにjsonに変換します。

data = {
  nodes: nodes.values,
  links: links.values,
}.to_json

d3.jsでグラフの表示

さあd3に食べさせるデータの準備が出来ました。これでグラフを表示します。

var graph = JSON.parse('<%= data.html_safe %>');
var width = 2400;
var height = 1200;
var color = d3.scale.category20();
var force = d3.layout.force().charge(-360).linkDistance(160).size([width, height]);
var svg = d3.select("svg");

var drawGraph = function(graph) {

    force.nodes(graph.nodes).links(graph.links).start();
    var link = svg.selectAll(".link").data(graph.links).enter().append("line").attr("class", "link").style("stroke-width",
        function(d) {
            return Math.sqrt(d.value);
        });

    var gnodes = svg.selectAll('g.gnode').data(graph.nodes).enter().append('g').classed('gnode', true);

    var node = gnodes.append("circle").attr("class", "node").attr("r", function(d) {
        return Math.sqrt(d.value);
    }).style("fill", function(d) {
        return color(d.group);
    }).call(force.drag);

    var labels = gnodes.append("text").text(function(d) {
        return d.name;
    });

    force.on("tick", function() {
        link.attr("x1", function(d) {
            return d.source.x;
        }).attr("y1", function(d) {
            return d.source.y;
        }).attr("x2", function(d) {
            return d.target.x;
        }).attr("y2", function(d) {
            return d.target.y;
        });
        gnodes.attr("transform", function(d) {
            return 'translate(' + [d.x, d.y] + ')';
        });
    });
};
drawGraph(graph);

SVGを表示するHTMLと簡単なCSS

描画範囲が大きくなりがちなのでスクロールできるようにしています。

<div class="outer">
    <div class="inner">
        <svg></svg>
    </div>
</div>

cssはこんな感じ。

.node {
  stroke: #fff;
  stroke-width: 1.5px;
}

.link {
  stroke: #999;
  stroke-opacity: .6;
}

text {
  font-size: 11px;
}

.outer {
  width: 960px;
  height: 600px;
  overflow: scroll;
}

.inner {
  width: 2400px;
  height: 1200px;
}

svg {
  display: block;
  width: 100%;
  height: 100%;
}

これで冒頭に載せたd3.jsを使ったグラフが描画されます。

dev.seoanalytics.jp-3000_home-index.png

フィルターの指定で特定のパス下やカテゴリを指定すると、ディレクトリ単位でのユーザーの動きが分かるので、他ページに回遊して会員登録に至るフローに乗っているのかな?とかユーザーの動きで見えてくる部分があると思います。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
20