[MongoDB]サルでもわかるMapReduce 3 PVとUUを同時に集計する

PV(ページビュー)を集計するのはレコード数を数えるだけなので簡単ですが、ユニークユーザ数を数えるのはちょっと工夫がいります。

事例:ユーザアクセスログからPVとUUを集計する

このようなデータが入っているときに、日別のPVとUUを集計するにはどうすればよいでしょうか。
db.userlog.find();
{ "user_id" : 1, "path" : "mypage", "timestamp" : ISODate("2012-12-12T12:02:51.706Z") }
{ "user_id" : 2, "path" : "home" ,  "timestamp" : ISODate("2012-12-12T12:03:00.958Z") }
{ "user_id" : 1, "path" : "login",  "timestamp" : ISODate("2012-12-12T12:03:00.759Z") }
{ "user_id" : 3, "path" : "home",   "timestamp" : ISODate("2012-12-13T00:00:12.444Z") }
結論から言うと下記のようなMapReduceをやればOKです。

//まず集計で使うためのユーティリティ関数を自作する
//ここで定義した関数はMapReduce時にscopeオプションで渡してやります。

// 日付オブジェクトをYYYY-MM-DD形式に変換するユーティリティ関数
var  getYMD = function (d) {
    yyyy = d.getFullYear();
    mm = d.getMonth() + 1;
    dd = d.getDate();
    mm = (new Number(mm)).zeroPad(2);
    dd = (new Number(dd)).zeroPad(2);
    return yyyy + "-" + mm + "-" + dd;
};


// 配列のユニーク要素数を数えるユーティリティ関数
var uniqCount = function (ary) {
    var tmpHash = {};
    var count = 0;
    for (var i = 0; i < ary.length; i++) {
        tmpHash[ary[i]] = true;
    }
    for (var j in tmpHash) {
        count++;
    }
    return count;
};

// Map関数
// thisは個々のドキュメントを指す
var map = function(){
    var key = getYMD(this.timestamp);
    var value = { users:this.user_id};
    emit(key, value);
};

// Reduce関数
// 引数valuesには、上記Mapでemitに渡したオブジェクトのリストが来ると考えて差し支えない
var reduce = function(key,values) {
    var ret = {users:[]};
    values.forEach(function(value){
     ret.users.push(value.users);
    });
    return ret;
};


// reduceの結果を見やすく変形する
var finalize = function(key,value) {
    var ret = {};
    ret.uu = uniqCount(value.users); // UU数を数える
    ret.pv = value.users.length || 0; // PV数を数える
    return ret;
};
これで準備OKです。
では、定義した関数オブジェクトたちを使ってmapReduceを実行します。
> db.userlog.mapReduce(map,reduce, {finalize:finalize,scope: {getYMD:getYMD, uniqCount:uniqCount }, out:{inline:1}});
{
        "results" : [
                {
                        "_id" : "2013-01-23",
                        "value" : {
                                "uu" : 1,
                                "pv" : 4
                        }
                },
                {
                        "_id" : "2013-01-28",
                        "value" : {
                                "uu" : 1,
                                "pv" : 2
                        }
                },
                {
                        "_id" : "2013-04-04",
                        "value" : {
                                "uu" : 1,
                                "pv" : 6
                        }
                },

                [中略]

        ],
        "timeMillis" : 276,
        "counts" : {
                "input" : 320,
                "emit" : 320,
                "reduce" : 25,
                "output" : 28
        },
        "ok" : 1,
}
とこんな感じで結果が出ます。

解説

ポイントの1つ目は、Mapでemitするときのvalueを
var value = { users:this.user_id};
のような形にしておくことです。

usersという名前なのに単一のユーザIDが与えられている点が気持ち悪いですが、こうしないと動かないのでこういうものだと思ってください。

ポイント2つ目は、Reduceの中でそのuser_idを集めて配列を作ることです。
var reduce = function(key,values) {
    var ret = {users:[]};
    values.forEach(function(value){
     ret.users.push(value.users);
    });
    return ret;
};
value.usersは上で説明したように実際には単一の値が入ってきます。
よってreduce処理内では、
ret.users.push(1);
ret.users.push(2);
ret.users.push(3);
ret.users.push(2);
のようにユーザIDが次々にpushされます。
このpushはドキュメントの数だけ行われますので、最終的に
ret = {users:[1,2,3,2,4,2,1,4,..]};
のようなデータ構造が作られます。

このリストの要素数を数えると1日あたりのPV数になり、ユニーク要素数を数えると1日あたりのユニークユーザ数になります
この数え上げ処理をfinalizeでやれば、お望みの出力が得られます。

よくMapReduceの解説記事で「Mapのemitの出力とReduceの戻り値は同じデータ型にしなければいけない」みたいな解説をみかけますが、全く同じ型でなくても大丈夫みたいです。
(オブジェクトのキーが合ってれば良いっぽい?)

以上、MapReduceによるUU,PVの集計テクニックでした。

あわせて読みたい

カテゴリ:

人気記事