MongoDB Aggregate query to calculate ARPU When User and Payment Model are separated

Viewed 194

I want to write an aggregate query to calculate ARPU (Average Revenue Per User) but I'm stuck on how to use data from to separate models, I know that I can join to models using $lookup but I cannot get count of all users in this way.

These are my models:

// User:
{
    _id,
    username: String,
    os: String
}

and

// Payment:
{
    _id,
    user: ObjectId,
    price: Number
}

I need to calculate sum of all payments divided by count of all users.

After that I need to group them by OS, i.e. sum of all payments made by users who have os='os1' divided by count of all users who have os='os1' for each OS

EDIT

for example, sample data for User is:

{
    "_id" : ObjectId("5b77fdffcbbd830011dbc04e"),
    "username" : "user1",
    "os" : "android"
},
{
    "_id" : ObjectId("5b7856756aaf56001120816b"),
    "username" : "user2",
    "os" : "android"
},
{
    "_id" : ObjectId("5b824fa7234d1b0010310522"),
    "username" : "user3",
    "os" : "android"
},
{
    "_id" : ObjectId("5b8a242444074c0074b8a318"),
    "username" : "user4",
    "os" : "ios"
},
{
    "_id" : ObjectId("5b7801b0cbbd830011dbc050"),
    "username" : "user5",
    "os" : "ios"
}

and sample data for Payment is:

{
    "_id" : ObjectId("5bab61b617df7f173037fb1b"),
    "user" : ObjectId("5b77fdffcbbd830011dbc04e"), // user1
    "price" : 5
},
{
    "_id" : ObjectId("5bad4f980ab23100119300ec"),
    "user" : ObjectId("5b77fdffcbbd830011dbc04e"), // user1
    "price" : 10
},
{
    "_id" : ObjectId("5bad525edeed4e0011286842"),
    "user" : ObjectId("5b7856756aaf56001120816b"), // user2
    "price" : 5
},
{
    "_id" : ObjectId("5bad525edeed4e0011286848"),
    "user" : ObjectId("5b8a242444074c0074b8a318"), // user4
    "price" : 5
},
{
    "_id" : ObjectId("5bad525edeed4e0011286849"),
    "user" : ObjectId("5b8a242444074c0074b8a318"), // user4
    "price" : 15
}

My expected output is a Number that is achieved using aggregate query.

Initially what I want is Average Revenue Per User:

ARPU: {sum of all payments} / {count of all users} = 40 / 5 = 8

If I can achieve that, then I need to group them for each OS:

ios-ARPU: {sum of payments ios users made} / {count of ios users} = 20 / 2 = 10

android-ARPU: {sum of payments android users made} / {count of android usrs} = 20 / 3 = 6.67

  • Note that some users may not make any payments but they need to be counted in calculating ARPU.
2 Answers

You can start with $lookup and then use $reduce to calculate totalPayments per user. Then you can $group by null to get single document with total number of users and total payments. To get ARPU you need $divide in last step.

db.users.aggregate([
    {
        $lookup: {
            from: "payments",
            localField: "_id",
            foreignField: "user",
            as: "payments"
        }
    },
    {
        $project: {
            _id: 1,
            os: 1,
            totalPayments: {
                $reduce: {
                    input: "$payments",
                    initialValue: 0,
                    in: {
                        $add: [ "$$value", "$$this.price" ]
                    }
                }
            }
        }
    },
    {
        $group: {
            _id: null,
            totalUsers: { $sum: 1 },
            totalPayments: { $sum: "$totalPayments" }
        }
    },
    {
        $project: {
            arpu: { $divide: [ "$totalPayments", "$totalUsers" ] }
        }
    }
])

Outputs: { "arpu" : 8 }

To get arpu per os you just need to $group by $os:

{
    $group: {
        _id: "$os",
        totalUsers: { $sum: 1 },
        totalPayments: { $sum: "$totalPayments" }
    }
}

Prints:

{ "_id" : "ios", "arpu" : 10 }
{ "_id" : "android", "arpu" : 6.666666666666667 }

Leverage the use of $facet to get both set of calculations in one pipeline after the $lookup stage.

The following demonstrates the use of $facet for the above aggregate operation:

db.getCollection('Payment').aggregate([
    { "$lookup": { 
        "from": "users",
        "localField": "user",
        "foreignField": "_id",
        "as": "user"
    } },
    { "$addFields": {
        "user": { "$arrayElemAt": ["$user", 0] }
    } },
    { "$facet": {
        "users": [
            { "$group": {
                "_id": null,
                "ARPU": { "$avg": "$price" }
            } }
        ],
        "os": [
            { "$group": {
                "_id": "$user.os",
                "ARPU": { "$avg": "$price" }
            } },
            { "$group": {
                "_id": null,
                "avgs": {
                    "$push": {
                        "k": { "$concat": ["ARPU", "-", "$_id"] },
                        "v": "$ARPU"
                    }
                } 
            } },
            { "$replaceRoot": {
                "newRoot": { "$arrayToObject": "$avgs" }
            } }
        ]
    } }
])

This will yield for the given sample docs:

{
    "users" : [ 
        {
            "_id" : null,
            "ARPU" : 8.0
        }
    ],
    "os" : [ 
        {
            "ios-ARPU" : 10.0,
            "android-ARPU" : 6.66666666666667
        }
    ]
}
Related