MongoDb 用 mapreduce 统计留存率

(金庆的专栏)

留存的定义采用的是
新增账号第X日:某日新增的账号中,在新增日后第X日有登录行为记为留存

输出如下:(类同友盟的留存率显示)
留存用户
注册时间    新增用户  留存率
                      1天后   2天后   3天后   4天后   5天后  6天后  7天后  14天后  30天后
2015-09-17  2300      20.7 %  15.6 %  13 %    11.3 %  9.9 %               
2015-09-18  2694      21.8 %  14.8 %  11.5 %  10.5 %                  
2015-09-19  3325      19 %    11.4 %  10.3 %                      
2015-09-20  3093      16.2 %  11.9 %                          
2015-09-21  2303      20.5 %                              

服务器记录新建帐号到 retention.register 集合,
每日记录帐号登录到 retention.login 集合,
每日运行统计脚本,统计前一天的留存率。

以下为 mongoDB 留存率相关的集合,
除了 retention.register 和 retention.login 由服务器代码写入,
其他集合都是由统计脚本生成。

retention.register
========================
留存率统计用,新建帐号。
记录新建帐号的创建日期。
有以下字段:
platform, 平台名
account_id, 帐号
date, 注册日期,字符串,格式:“2015-01-01”
例如: {platform: "baidu", account_id: "jinqing", date: "2015-09-20"}
索引 (platform, account_id), (date)
用于统计每日新增帐号数。

retention.login
==================
留存率统计用,帐号登录记录。
有以下字段:
date, 登录日期
platform, 平台名
account_id, 帐号
register_date, 帐号注册日期
例如:{date: "2015-09-23", platform: "baidu", account_id: "jinqing", register_date: "2015-09-20"}
索引 (date, platform, account_id).

retention.result
===================
留存率结果。例如:
{date : "2015-09-01", register : 3344, 1 : 91.1, 2 : 82.2, 3 : 73.3, 4 : 64.4, 5 : 55.5, 6 : 46.6, 7 : 37.7, 14 : 14.0, 30 : 3.33}
{date : "2015-09-02", register : 3344, 1 : 91.1, 2 : 82.2, 3 : 73.3, 4 : 64.4, 5 : 55.5, 6 : 46.6, 7 : 37.7, 14 : 14.0, 30 : 3.33}
可用 mongoexport 导出为 csv 表格文件。
例如:
D:\mongodb\bin>mongoexport -h localhost -d mydb -c retention.result -f date,register,1,2,3,4,5,6,7,14,30 --csv -o d:\temp\retention.csv
其中
date: 注册日期
register: 新注册个数
1,2,...7,14,30: 第1日,2日,... 7日,14日,30日留存百分率

留存率统计脚本
--------------
linux下用crontab,
windows下用定时任务,
每日凌晨 00:30 运行统计脚本。

允许隔了几天没运行,运行时将从上次运行处一直统计到当天。
如果是首次运行,则从 retention.register 集合的最早日期开始统计。
一天运行多次也不会影响结果。
但是不能同时运行多个实例。

需 mongo 客户端。
可在 mongo 主机上运行。

mongo my.mongo.host retention.js
生成结果在 mydb.retention.result 集合中,可用 mongoexport 导出为 csv 文件。

#!/bin/sh
# retention.sh
# 每日凌晨定时执行,统计留存率。
# 需 mongo 客户端。

# 以下需更改为实际目录, 将在该目录下运行。
cd /home/jinq/retention/

# 以下地址应该改为 mongod 服务器地址。
MONGODB=192.168.8.9

mongo ${MONGODB} retention.js >> log.txt

echo Mongo export retention result...
mongoexport -h ${MONGODB} -d mydb -c retention.result \
  --sort '{"value.date" : 1}' \
  -f value.date,value.register,value.1,value.2,value.3,value.4,value.5,value.6,value.7,value.14,value.30 \
  --type=csv -o retention_tmp.csv

DATE=`date +%Y%m%d`
FILE=retention_${DATE}.csv

# csv替换列头
echo 日期,注册数,1日,2日,3日,4日,5日,6日,7日,14日,30日 > ${FILE}
tail -n +2 retention_tmp.csv >> ${FILE}

echo Done ${FILE}!

// 留存率统计脚本
// 参考文档:留存率统计.txt
// Usage:
// mongo my.mongo.host retention.js

print(Date());
db = db.getSisterDB("mydb");  // use mydb

var startDate = getStartDate();
var endDate = formatDate(new Date());
print("Calculating retention rate of [" + startDate + ", " + endDate + ")...");
if (startDate < endDate) {
    insertDefaultResult(startDate);
    calcRegisterCount(startDate);
    calcRetention(startDate);
    print(Date());
    print("Done.");
} else {
    print("Do nothing.");
}

// Internal functions.

// 获取统计开始日期,之前的已经统计完成,无需重做。
// 返回字符串,格式:"2015-01-01"
// 获取 retention.result 的最大 date + 1天, 仅须处理该天及以后的数据。
// 如果是初次运行,retention.result 为空,须读取 retention.register 的最早日期作为开始。
function getStartDate() {
    var lastResultDate = getLastResultDate();
    if (null == lastResultDate) {
        return getFirstRegisterDate();
    }

    // 加一天
    return getNextDate(lastResultDate);
}

// 获取最早的 retention.register 日期。
function getFirstRegisterDate() {
    var cursor = db.retention.register.find(
        {date : {$gt : "2015-09-01"}},  // 除去 null
        {_id : 0, date : 1}
    ).sort({date : 1}).limit(1);
    if (cursor.hasNext()) {
        return cursor.next().date;
    }
    return formatDate(new Date());
}

// 获取 retention.result 中最后的 date 字段。
// 无date字段则返回null。
// 正常返回如:"2015-01-01"
function getLastResultDate() {
    // _id 为日期串
    var cursor = db.retention.result.find(
        {}, {_id : 1}).sort({_id : -1}).limit(1);
    if (cursor.hasNext()) {
        return cursor.next()._id;
    }
    return null;
}

function add0(m) {
    return m < 10 ? '0' + m : m;
}

// Return likes: "2015-01-02"
function formatDate(date)
{
    var y = date.getFullYear();
    var m = date.getMonth() + 1;  // 1..12
    var d = date.getDate();
    return  y + '-' + add0(m) + '-' + add0(d);
}

// "2015-12-31" -> "2016-01-01"
function getNextDate(dateStr) {
    var dateObj = new Date(dateStr + " 00:00:00");
    var nextDayTime = dateObj.getTime() + 24 * 3600 * 1000;
    var nextDate = new Date(nextDayTime);
    return formatDate(nextDate);
}

assert(getNextDate("2015-12-31") == "2016-01-01");
assert(getNextDate("2015-01-01") == "2015-01-02");
assert(getNextDate("2015-01-31") == "2015-02-01");

// 插入缺省结果。
// 某些天无新注册,mapreduce就不会生成该条结果,须强制插入。
function insertDefaultResult(startDateStr) {
    var docs = new Array();
    var endDateStr = formatDate(new Date());
    for (var dateStr = startDateStr;
        dateStr < endDateStr;
        dateStr = getNextDate(dateStr)) {
        docs.push({_id : dateStr, value : {date : dateStr, register : 0}});
    }  // for
    db.retention.result.insert(docs);
}

// 读取 retention.register 集合,
// 计算每日新注册量, 记录于 retention.result.value.register 字段
// startDate is like: "2015-01-01"
function calcRegisterCount(startDate) {
    var mapFunction = function() {
        var key = this.date;
        var value = {date : key, register : 1};
        emit(key, value);
    };  // mapFunction

    var reduceFunction = function(key, values) {
        var reducedObject = {date : key, register : 0};
        values.forEach(
            function(value) {
                reducedObject.register += value.register;
            }
        )
        return reducedObject;
    };  // reduceFunction

    var endDate = formatDate(new Date());
    db.retention.register.mapReduce(mapFunction, reduceFunction,
        {
            query: {date: {$gte: startDate, $lt: endDate}},
            out: {merge: "retention.result"}
        }
    );  // mapReduce()
}  // function calcRegisterCount()

// 读取 retention.login 集合,
// 计算留存率,保存于 retention.result 集合。
// startDate is like: "2015-01-01"
function calcRetention(startDate) {
    var mapFunction = function() {
        var key = this.register_date;
        var registerDateObj = new Date(this.register_date + " 00:00:00");
        var loginDateObj = new Date(this.date + " 00:00:00");
        var days = (loginDateObj - registerDateObj) / (24 * 3600 * 1000);
        var value = {date : key, register : 0};
        var field = days + "_count";  // like: 1_count
        value[field] = 1;
        emit(key, value);
    };  // mapFunction

    var reduceFunction = function(key, values) {
        var reducedObject = {date : key, register : 0};
        for (var i = 1; i <= 60; i++) {
            var field = i + "_count";
            reducedObject[field] = 0;
        }

        values.forEach(
            function(value) {
                reducedObject.register += value.register;
                for (var i = 1; i <= 60; i++) {
                    var field = i + "_count";  // like: 1_count
                    var count = value[field];
                    if (null != count) {
                        reducedObject[field] += count;
                    }  // if
                }  // for
            }  // function
        )  // values.forEach()
        return reducedObject;
    };  // reduceFunction()

    var finalizeFunction = function(key, reducedVal) {
        if (0 == reducedVal.register)
            return reducedVal;
        for (var i = 1; i <= 60; i++) {
            var field = i + "_count";  // 1_count
            var count = reducedVal[field];
            reducedVal[String(i)] = count * 100 / reducedVal.register;
        }
        return reducedVal;
    };  // finalizeFunction

    var endDate = formatDate(new Date());
    db.retention.login.mapReduce(mapFunction, reduceFunction,
        {
            query: {date: {$gte: startDate, $lt: endDate}},
            out: {reduce: "retention.result"},
            finalize: finalizeFunction,
        }
    );  // mapReduce()
}  // function calcRetention()

参考
-----

用户留存率_百度百科
http://baike.baidu.com/link?url=28-agScaamT__jLEBdn5VW-a6CHRlf53bDUrVezkeaHd6TMhO0ULm_9JMmcOu541taQjWGe0JypERg2hIwJCAa

游戏玩家的留存率统计实现 - 流子的专栏 - 博客频道 - CSDN.NET
http://blog.csdn.net/jiangguilong2000/article/details/16119119

在Mongo数据库里怎么统计留存率呢? - SegmentFault
http://segmentfault.com/q/1010000000652638

MongoDb 用 mapreduce 统计留存率的更多相关文章

  1. MongoDB 的 MapReduce 大数据统计统计挖掘

    MongoDB虽然不像我们常用的mysql,sqlserver,oracle等关系型数据库有group by函数那样方便分组,但是MongoDB要实现分组也有3个办法: * Mongodb三种分组方式 ...

  2. mongoDB实现MapReduce

    一.MongoDB Map Reduce Map-Reduce是一种计算模型,简单的说就是将大批量的工作(数据)分解(MAP)执行,然后再将结果合并成最终结果(REDUCE).MongoDB提供的Ma ...

  3. mongodb 聚合(Map-Reduce)

    介绍 Map-reduce 是一种数据处理范式,用于将大量数据压缩为有用的聚合结果.对于 map-reduce 操作,MongoDB 提供MapReduce数据库命令. MongoDB中的MapRed ...

  4. Hadoop基础-Map端链式编程之MapReduce统计TopN示例

    Hadoop基础-Map端链式编程之MapReduce统计TopN示例 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.项目需求 对“temp.txt”中的数据进行分析,统计出各 ...

  5. MongoDB:Map-Reduce

    Map-reduce是一个考虑大型数据得到实用聚集结果的数据处理程式(paradigm).针对map-reduce操作,MongoDB提供来mapreduce命令. 考虑以下的map-reduce操作 ...

  6. MongoDB中mapReduce的使用

    MongoDB中mapReduce的使用 制作人:全心全意 mapReduce的功能和group by的功能类似,但比group by处理的数据量更大 使用示例: var map = function ...

  7. MongoDB进行MapReduce的数据类型

    有很长一段时间没更新博客了,因为最近都比较忙,今天算是有点空闲吧.本文主要是介绍MapReduce在MongoDB上的使用,它与sql的分组.聚集类似,也是先map分组,再用reduce统计,最后还可 ...

  8. mongoDB(3) mapReduce

    mapReduce是大数据的核心内容,但实际操作中别用这个,所谓的mapReduce分两步 1.map:将数据分别取出,Map函数调用emit(key,value)遍历集合中所有的记录,将key与va ...

  9. MongoDB中MapReduce介绍与使用

    一.简介 在用MongoDB查询返回的数据量很大的情况下,做一些比较复杂的统计和聚合操作做花费的时间很长的时候,可以用MongoDB中的MapReduce进行实现 MapReduce是个非常灵活和强大 ...

随机推荐

  1. Java 线程池原理分析

    1.简介 线程池可以简单看做是一组线程的集合,通过使用线程池,我们可以方便的复用线程,避免了频繁创建和销毁线程所带来的开销.在应用上,线程池可应用在后端相关服务中.比如 Web 服务器,数据库服务器等 ...

  2. openfire彻底卸载的方法

    最近百度找openfire彻底卸载的方法,很多都是三句命令行的答案.但是那三句真的无法完全卸载 终于从openfire官网找到了卸载的命令 终端执行下面的命令 sudo rm -rf /usr/loc ...

  3. 十大面试难题解惑,看完秒杀一切 HR 面。程序员必读!

    最能体现求职者能力的就是面试,能不能拿到Offer,取决于你面试时的表现,只有有准备才能在面试过程中游刃有余. 小编收集了10个面试官最爱提的问题,虽然题目千变万化,但是万变不离其宗,只要掌握了答题的 ...

  4. [TJOI 2016&HEOI 2016]排序

    Description 在2016年,佳媛姐姐喜欢上了数字序列.因而他经常研究关于序列的一些奇奇怪怪的问题,现在他在研究一个难题 ,需要你来帮助他.这个难题是这样子的:给出一个1到n的全排列,现在对这 ...

  5. GCD(ZYYS)

    [问题描述]在山的那边.海的那边有 n 个小矮人,他们生存的意义就是要保护他们的精神领袖——GCD.有一天,他们收到了一封恐吓信,说要在一个遥远的地方用维纳斯之箭射击 GCD,让他变成一根面条,n 个 ...

  6. NOI2006 郁闷的出纳员

    题目描述 OIER公司是一家大型专业化软件公司,有着数以万计的员工.作为一名出纳员,我的任务之一便是统计每位员工的工资.这本来是一份不错的工作,但是令人郁闷的是,我们的老板反复无常,经常调整员工的工资 ...

  7. ●BZOJ 1692 [Usaco2007 Dec]队列变换

    题链: http://www.lydsy.com/JudgeOnline/problem.php?id=1692 题解: 后缀数组,贪心由于每次可以取出旧队列的首部或尾部放在新队列的尾部.所以就需要比 ...

  8. NOIP2014-11-3模拟赛

    字符串 题目描述 现在给一个字符串,你要做的就是当这个字符串中存在两个挨着的字符是相同的时就将这两个字符消除.需要注意的是,当把这两个字符消除后,可能又产生一对新的挨着的字符是相同的.比如,初始的字符 ...

  9. 习题9-4 uva 1630

    题意: 给你一串数字,要求你对其进行折叠使其长度最短. 折叠情况:全是一个字母 & 重复的字符串 AAAAAAAAAABABABCCD    -->   9(A)3(AB)CCD NEE ...

  10. [bzoj4236]JOIOJI

    来自FallDream的博客,未经允许,请勿转载,谢谢. JOIOJI桑是JOI君的叔叔.“JOIOJI”这个名字是由“J.O.I”三个字母各两个构成的. 最近,JOIOJI桑有了一个孩子.JOIOJ ...