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 文件。
參考
-----
用戶留存率_百度百科
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
(金慶的專欄)
留存的定義采用的是
新增賬號第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}!
# 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()
// 參考文檔:留存率統計.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