The initial public offerings of Facebook and Twitter saw the public rise of the “active user” metric. Over the past few years Wall Street and Silicon Valley have had a near obsession with the metric.

“Active users” at its core is a count of user having initiated some website activity (often a subset of GET, POST http requests) usually reported by calendar month. Event logs are a common analytics data structure and the technique shown below is broadly suitable for learning the stories hidden in such a log of customer events.

At Pydata - Seattle 2015, Cameron Davidson-Pilon was advertising the lifetimes library for modeling phenomena like periodic purchases. He linked to a few academic articles that showed a new set of methods for customer-base analysis (e.g. event logs) offered by Drs. Peter Faber and Bruce Hardie of the Wharton School and London Business School, respectively. I noticed that one of the set of mixture models would be perfect for modeling active users.

In this notebook, I’ll share a few basics around modeling active users. It’s a work in progress. I expect future extensions to include building in of detail using Bayesian (“Bayesian survival analysis for “Game of Thrones”) and probabilistic programming techniques (pymc3, rstan). I’ll also leave model validation and projection to a future example. I think regression could be combined with this technique to yield interpretive insights.

This method starts with a simple story, that if believed, provides a coherent framework for modeling and ultimately projecting active users.

The story goes like this: when a user signs up for a service they’re given two coins. The first coin is flipped until they see “heads.” The number of times required to see “heads” is the number of months before the user abandons the site. The number of times can vary substantially between customers. The second coin is flipped for each month the user hasn’t abandoned the site. Each “heads” represents an active month.

Formally, this could be called a Beta-Geometric/Beta-Binomial (BG/BB) model. Drs. Faber and Hardie present a classic case of theoretically sound statistics substituting for a large quantity of engineering requirement by using a spreadsheet for calculations. Much like space and time, engineering and statistics are highly substitutive. When viewed this way and combined they can help solve otherwise challenging problems.

Now, I’ll build the first coin,

months_till_single_user_abandons <-  function(a, b) { rgeom(1, rbeta(1, a, b)) }
months_till_many_users_abandon <- function(users, a, b) {
  replicate(users, months_till_single_user_abandons(a, b))
}

The geometric distribution has one parameter “p” where “p” is the probability of heads on a given flip. I mentioned customers vary significantly, so we want “p” to vary significantly. That’s where “a” and “b” come in, they’re from the beta distribution and have a mean of a / (a + b).

Next I’ll assume a ten user cohort where each month there’s a roughly 1 in 6 chance each user abandons the site. The higher the total of “a” and “b” the more certain you are about the coin flip. It’s possible to accumulate cases and built up more and more certainty using counters.

I’ll simulate 25 users so we can visualize the steps as we go,

suppressPackageStartupMessages({
  library(ggplot2)
  library(dplyr)
  library(tibble)
})
NUMBRER_OF_USERS <- 25
cohort <- tibble(months = months_till_many_users_abandon(NUMBRER_OF_USERS, 10, 50) + 1) %>%
  mutate(user_id = seq_len(n())) %>%
  group_by(user_id) %>%
  do({ tibble(months = seq_len(.$months), user_id = .$user_id) }) %>% ungroup() %>%
  arrange(user_id) %>% mutate(user_id = factor(user_id, levels = unique(user_id)))
cohort %>%
  ggplot(aes(x = months, y = user_id, fill = user_id)) +
  geom_tile(colour = "black") +
  ylab("user_id") +
  xlab("Months since cohort signup") +
  ggtitle("Cohort retention")

Now that we have the number of months until a user abandons the site, let’s model the number of months they’re active. This essentially involves flipping a coin for each month before abandonment.

I’ll build the second coin,

active_or_not <- function(a, b) {
  ifelse(rbinom(1, 1, rbeta(1, a, b)), "active", "inactive")
}

Below is a graph marking “active” and “inactive” months. It’s important to note that the event(s) mapped to “active” vary widely company-to-company. However, allowing for extreme heterogeneity is what this type of model does best.

users_by_activity <- cohort %>%
  rowwise() %>%
  mutate(active = active_or_not(8, 10))
users_by_activity %>%
  ggplot(aes(x = months, y = user_id, fill = factor(active))) +
  geom_tile(colour = "black") +
  ggtitle("Active or not") +
  xlab("Months since signup") +
  scale_fill_discrete(name = "") +
  theme(legend.position = "top")

Now we can group by months since the cohort signed up and count the active months,

users_by_activity %>%
  filter(active == "active") %>%
  count(months) %>%
  ggplot(aes(x = months, y = n)) +
  geom_bar(stat = "identity") +
  ggtitle("Active users (retention aka 'stickiness')") +
  xlab("Months since cohort signed up") +
  ylab("Number of active users") +
  scale_y_continuous(breaks = scales::pretty_breaks(10))
Grouping rowwise data frame strips rowwise nature

There are many questions I must leave unanswered for now. In the meantime I’d like to refer you to the lifetimes library where you’ll find Python code that applies these models.

Before closing, let me point you directly to resources by Drs. Faber and Hardie where they show that the model performs spectacularly against a real dataset:

This method for modeling active users is very powerful because it leverages probability and statistical theory. Much like engineering, statistics and probability are levers that when effectively used together can offer solutions to incredibly challenging problems. Far from constrained to active users, this method is also likely to work well for invoices and other valuable problems.

Source: github

LS0tCnRpdGxlOiAiQSBQYXJhYmxlIG9uIEN1c3RvbWVyLWJhc2UgQW5hbHlzaXMiCm91dHB1dDogaHRtbF9ub3RlYm9vawotLS0KClRoZSBpbml0aWFsIHB1YmxpYyBvZmZlcmluZ3Mgb2YgRmFjZWJvb2sgYW5kIFR3aXR0ZXIgc2F3IHRoZSBwdWJsaWMgcmlzZSBvZiB0aGUgImFjdGl2ZSB1c2VyIiBtZXRyaWMuIE92ZXIgdGhlIHBhc3QgZmV3IHllYXJzIFdhbGwgU3RyZWV0IGFuZCBTaWxpY29uIFZhbGxleSBoYXZlIGhhZCBhIG5lYXIgb2JzZXNzaW9uIHdpdGggdGhlIG1ldHJpYy4gCgoiQWN0aXZlIHVzZXJzIiBhdCBpdHMgY29yZSBpcyBhIGNvdW50IG9mIHVzZXIgaGF2aW5nIGluaXRpYXRlZCBzb21lIHdlYnNpdGUgYWN0aXZpdHkgKG9mdGVuIGEgc3Vic2V0IG9mIGBHRVRgLCBgUE9TVGAgW2h0dHAgcmVxdWVzdHNdKGh0dHBzOi8vZW4ud2lraXBlZGlhLm9yZy93aWtpL0h5cGVydGV4dF9UcmFuc2Zlcl9Qcm90b2NvbCNSZXF1ZXN0X21ldGhvZHMpKSB1c3VhbGx5IHJlcG9ydGVkIGJ5IGNhbGVuZGFyIG1vbnRoLiBFdmVudCBsb2dzIGFyZSBhIGNvbW1vbiBhbmFseXRpY3MgZGF0YSBzdHJ1Y3R1cmUgYW5kIHRoZSB0ZWNobmlxdWUgc2hvd24gYmVsb3cgaXMgYnJvYWRseSBzdWl0YWJsZSBmb3IgbGVhcm5pbmcgdGhlIHN0b3JpZXMgaGlkZGVuIGluIHN1Y2ggYSBsb2cgb2YgY3VzdG9tZXIgZXZlbnRzLgoKQXQgUHlkYXRhIC0gU2VhdHRsZSAyMDE1LCBDYW1lcm9uIERhdmlkc29uLVBpbG9uIHdhcyBhZHZlcnRpc2luZyB0aGUgW2xpZmV0aW1lc10oaHR0cDovL2dpdGh1Yi5jb20vY2FtRGF2aWRzb25QaWxvbi9saWZldGltZXMvKSBsaWJyYXJ5IGZvciBtb2RlbGluZyBwaGVub21lbmEgbGlrZSBwZXJpb2RpYyBwdXJjaGFzZXMuIEhlIGxpbmtlZCB0byBhIGZldyBhY2FkZW1pYyBhcnRpY2xlcyB0aGF0IHNob3dlZCBhIG5ldyBzZXQgb2YgbWV0aG9kcyBmb3IgW2N1c3RvbWVyLWJhc2UgYW5hbHlzaXNdKGh0dHA6Ly93d3cuYnJ1Y2VoYXJkaWUuY29tL3RhbGtzL2hvX2NiYV90dXRfYXJ0XzA5LnBkZikgKGUuZy4gZXZlbnQgbG9ncykgb2ZmZXJlZCBieSBEcnMuIFBldGVyIEZhYmVyIGFuZCBCcnVjZSBIYXJkaWUgb2YgdGhlIFdoYXJ0b24gU2Nob29sIGFuZCBMb25kb24gQnVzaW5lc3MgU2Nob29sLCByZXNwZWN0aXZlbHkuIEkgbm90aWNlZCB0aGF0IG9uZSBvZiB0aGUgc2V0IG9mIG1peHR1cmUgbW9kZWxzIHdvdWxkIGJlIHBlcmZlY3QgZm9yIG1vZGVsaW5nIGFjdGl2ZSB1c2Vycy4KCkluIHRoaXMgbm90ZWJvb2ssIEknbGwgc2hhcmUgYSBmZXcgYmFzaWNzIGFyb3VuZCBtb2RlbGluZyBhY3RpdmUgdXNlcnMuIEl0J3MgYSB3b3JrIGluIHByb2dyZXNzLiBJIGV4cGVjdCBmdXR1cmUgZXh0ZW5zaW9ucyB0byBpbmNsdWRlIGJ1aWxkaW5nIGluIG9mIGRldGFpbCB1c2luZyBCYXllc2lhbiAoIltCYXllc2lhbiBzdXJ2aXZhbCBhbmFseXNpcyBmb3IgIkdhbWUgb2YgVGhyb25lc10oaHR0cDovL2FsbGVuZG93bmV5LmJsb2dzcG90LmNvbS8yMDE1LzAzL2JheWVzaWFuLXN1cnZpdmFsLWFuYWx5c2lzLWZvci1nYW1lLW9mLmh0bWwpIikgYW5kIHByb2JhYmlsaXN0aWMgcHJvZ3JhbW1pbmcgdGVjaG5pcXVlcyAoW3B5bWMzXShodHRwOi8vcHltYy1kZXZzLmdpdGh1Yi5pby9weW1jMy8pLCBbcnN0YW5dKGh0dHBzOi8vZ2l0aHViLmNvbS9zdGFuLWRldi9yc3Rhbi93aWtpL1JTdGFuLUdldHRpbmctU3RhcnRlZCkpLiBJJ2xsIGFsc28gbGVhdmUgbW9kZWwgdmFsaWRhdGlvbiBhbmQgcHJvamVjdGlvbiB0byBhIGZ1dHVyZSBleGFtcGxlLiBJIHRoaW5rIHJlZ3Jlc3Npb24gY291bGQgYmUgY29tYmluZWQgd2l0aCB0aGlzIHRlY2huaXF1ZSB0byB5aWVsZCBpbnRlcnByZXRpdmUgaW5zaWdodHMuCgpUaGlzIG1ldGhvZCBzdGFydHMgd2l0aCBhIHNpbXBsZSBzdG9yeSwgdGhhdCBpZiBiZWxpZXZlZCwgcHJvdmlkZXMgYSBjb2hlcmVudCBmcmFtZXdvcmsgZm9yIG1vZGVsaW5nIGFuZCB1bHRpbWF0ZWx5IHByb2plY3RpbmcgYWN0aXZlIHVzZXJzLgoKVGhlIHN0b3J5IGdvZXMgbGlrZSB0aGlzOiB3aGVuIGEgdXNlciBzaWducyB1cCBmb3IgYSBzZXJ2aWNlIHRoZXkncmUgZ2l2ZW4gdHdvIGNvaW5zLiBUaGUgZmlyc3QgY29pbiBpcyBmbGlwcGVkIHVudGlsIHRoZXkgc2VlICJoZWFkcy4iIFRoZSBudW1iZXIgb2YgdGltZXMgcmVxdWlyZWQgdG8gc2VlICJoZWFkcyIgaXMgdGhlIG51bWJlciBvZiBtb250aHMgYmVmb3JlIHRoZSB1c2VyIGFiYW5kb25zIHRoZSBzaXRlLiBUaGUgbnVtYmVyIG9mIHRpbWVzIGNhbiB2YXJ5IHN1YnN0YW50aWFsbHkgYmV0d2VlbiBjdXN0b21lcnMuIFRoZSBzZWNvbmQgY29pbiBpcyBmbGlwcGVkIGZvciBlYWNoIG1vbnRoIHRoZSB1c2VyIGhhc24ndCBhYmFuZG9uZWQgdGhlIHNpdGUuIEVhY2ggImhlYWRzIiByZXByZXNlbnRzIGFuIGFjdGl2ZSBtb250aC4KCkZvcm1hbGx5LCB0aGlzIGNvdWxkIGJlIGNhbGxlZCBhIEJldGEtR2VvbWV0cmljL0JldGEtQmlub21pYWwgKEJHL0JCKSBtb2RlbC4gRHJzLiBGYWJlciBhbmQgSGFyZGllIHByZXNlbnQgYSBjbGFzc2ljIGNhc2Ugb2YgdGhlb3JldGljYWxseSBzb3VuZCBzdGF0aXN0aWNzIHN1YnN0aXR1dGluZyBmb3IgYSBsYXJnZSBxdWFudGl0eSBvZiBlbmdpbmVlcmluZyByZXF1aXJlbWVudCBbYnkgdXNpbmcgYSBzcHJlYWRzaGVldCBmb3IgY2FsY3VsYXRpb25zXShodHRwOi8vY2l0ZXNlZXJ4LmlzdC5wc3UuZWR1L3ZpZXdkb2Mvc3VtbWFyeT9kb2k9MTAuMS4xLjM3LjQ0MTApLiBNdWNoIGxpa2Ugc3BhY2UgYW5kIHRpbWUsIGVuZ2luZWVyaW5nIGFuZCBzdGF0aXN0aWNzIGFyZSBoaWdobHkgc3Vic3RpdHV0aXZlLiBXaGVuIHZpZXdlZCB0aGlzIHdheSBhbmQgY29tYmluZWQgdGhleSBjYW4gaGVscCBzb2x2ZSBvdGhlcndpc2UgY2hhbGxlbmdpbmcgcHJvYmxlbXMuCgpOb3csIEknbGwgYnVpbGQgdGhlIGZpcnN0IGNvaW4sCgpgYGB7cn0KbW9udGhzX3RpbGxfc2luZ2xlX3VzZXJfYWJhbmRvbnMgPC0gIGZ1bmN0aW9uKGEsIGIpIHsgcmdlb20oMSwgcmJldGEoMSwgYSwgYikpIH0KCm1vbnRoc190aWxsX21hbnlfdXNlcnNfYWJhbmRvbiA8LSBmdW5jdGlvbih1c2VycywgYSwgYikgewogIHJlcGxpY2F0ZSh1c2VycywgbW9udGhzX3RpbGxfc2luZ2xlX3VzZXJfYWJhbmRvbnMoYSwgYikpCn0KYGBgCgpUaGUgW2dlb21ldHJpYyBkaXN0cmlidXRpb25dKGh0dHBzOi8vZW4ud2lraXBlZGlhLm9yZy93aWtpL0dlb21ldHJpY19kaXN0cmlidXRpb24pIGhhcyBvbmUgcGFyYW1ldGVyICJwIiB3aGVyZSAicCIgaXMgdGhlIHByb2JhYmlsaXR5IG9mIGhlYWRzIG9uIGEgZ2l2ZW4gZmxpcC4gSSBtZW50aW9uZWQgY3VzdG9tZXJzIHZhcnkgc2lnbmlmaWNhbnRseSwgc28gd2Ugd2FudCAicCIgdG8gdmFyeSBzaWduaWZpY2FudGx5LiBUaGF0J3Mgd2hlcmUgImEiIGFuZCAiYiIgY29tZSBpbiwgdGhleSdyZSBmcm9tIHRoZSBbYmV0YSBkaXN0cmlidXRpb25dKGh0dHBzOi8vZW4ud2lraXBlZGlhLm9yZy93aWtpL0JldGFfZGlzdHJpYnV0aW9uKSBhbmQgaGF2ZSBhIG1lYW4gb2YgYGEgLyAoYSArIGIpYC4KCk5leHQgSSdsbCBhc3N1bWUgYSB0ZW4gdXNlciBjb2hvcnQgd2hlcmUgZWFjaCBtb250aCB0aGVyZSdzIGEgcm91Z2hseSAxIGluIDYgY2hhbmNlIGVhY2ggdXNlciBhYmFuZG9ucyB0aGUgc2l0ZS4gVGhlIGhpZ2hlciB0aGUgdG90YWwgb2YgImEiIGFuZCAiYiIgdGhlIG1vcmUgY2VydGFpbiB5b3UgYXJlIGFib3V0IHRoZSBjb2luIGZsaXAuIEl0J3MgcG9zc2libGUgdG8gYWNjdW11bGF0ZSBjYXNlcyBhbmQgYnVpbHQgdXAgbW9yZSBhbmQgbW9yZSBjZXJ0YWludHkgdXNpbmcgY291bnRlcnMuCgpJJ2xsIHNpbXVsYXRlIDI1IHVzZXJzIHNvIHdlIGNhbiB2aXN1YWxpemUgdGhlIHN0ZXBzIGFzIHdlIGdvLAoKYGBge3J9CnN1cHByZXNzUGFja2FnZVN0YXJ0dXBNZXNzYWdlcyh7CiAgbGlicmFyeShnZ3Bsb3QyKQogIGxpYnJhcnkoZHBseXIpCiAgbGlicmFyeSh0aWJibGUpCn0pCgpOVU1CUkVSX09GX1VTRVJTIDwtIDI1Cgpjb2hvcnQgPC0gdGliYmxlKG1vbnRocyA9IG1vbnRoc190aWxsX21hbnlfdXNlcnNfYWJhbmRvbihOVU1CUkVSX09GX1VTRVJTLCAxMCwgNTApICsgMSkgJT4lCiAgbXV0YXRlKHVzZXJfaWQgPSBzZXFfbGVuKG4oKSkpICU+JQogIGdyb3VwX2J5KHVzZXJfaWQpICU+JQogIGRvKHsgdGliYmxlKG1vbnRocyA9IHNlcV9sZW4oLiRtb250aHMpLCB1c2VyX2lkID0gLiR1c2VyX2lkKSB9KSAlPiUgdW5ncm91cCgpICU+JQogIGFycmFuZ2UodXNlcl9pZCkgJT4lIG11dGF0ZSh1c2VyX2lkID0gZmFjdG9yKHVzZXJfaWQsIGxldmVscyA9IHVuaXF1ZSh1c2VyX2lkKSkpCgpjb2hvcnQgJT4lCiAgZ2dwbG90KGFlcyh4ID0gbW9udGhzLCB5ID0gdXNlcl9pZCwgZmlsbCA9IHVzZXJfaWQpKSArCiAgZ2VvbV90aWxlKGNvbG91ciA9ICJibGFjayIpICsKICB5bGFiKCJ1c2VyX2lkIikgKwogIHhsYWIoIk1vbnRocyBzaW5jZSBjb2hvcnQgc2lnbnVwIikgKwogIGdndGl0bGUoIkNvaG9ydCByZXRlbnRpb24iKQpgYGAKCk5vdyB0aGF0IHdlIGhhdmUgdGhlIG51bWJlciBvZiBtb250aHMgdW50aWwgYSB1c2VyIGFiYW5kb25zIHRoZSBzaXRlLCBsZXQncyBtb2RlbCB0aGUgbnVtYmVyIG9mIG1vbnRocyB0aGV5J3JlIGFjdGl2ZS4gVGhpcyBlc3NlbnRpYWxseSBpbnZvbHZlcyBmbGlwcGluZyBhIGNvaW4gZm9yIGVhY2ggbW9udGggYmVmb3JlIGFiYW5kb25tZW50LgoKSSdsbCBidWlsZCB0aGUgc2Vjb25kIGNvaW4sCgpgYGB7cn0KYWN0aXZlX29yX25vdCA8LSBmdW5jdGlvbihhLCBiKSB7CiAgaWZlbHNlKHJiaW5vbSgxLCAxLCByYmV0YSgxLCBhLCBiKSksICJhY3RpdmUiLCAiaW5hY3RpdmUiKQp9CmBgYAoKQmVsb3cgaXMgYSBncmFwaCBtYXJraW5nICJhY3RpdmUiIGFuZCAiaW5hY3RpdmUiIG1vbnRocy4gSXQncyBpbXBvcnRhbnQgdG8gbm90ZSB0aGF0IHRoZSBldmVudChzKSBtYXBwZWQgdG8gImFjdGl2ZSIgdmFyeSB3aWRlbHkgY29tcGFueS10by1jb21wYW55LiBIb3dldmVyLCBhbGxvd2luZyBmb3IgZXh0cmVtZSBoZXRlcm9nZW5laXR5IGlzIHdoYXQgdGhpcyB0eXBlIG9mIG1vZGVsIGRvZXMgYmVzdC4KCmBgYHtyfQp1c2Vyc19ieV9hY3Rpdml0eSA8LSBjb2hvcnQgJT4lCiAgcm93d2lzZSgpICU+JQogIG11dGF0ZShhY3RpdmUgPSBhY3RpdmVfb3Jfbm90KDgsIDEwKSkKCnVzZXJzX2J5X2FjdGl2aXR5ICU+JQogIGdncGxvdChhZXMoeCA9IG1vbnRocywgeSA9IHVzZXJfaWQsIGZpbGwgPSBmYWN0b3IoYWN0aXZlKSkpICsKICBnZW9tX3RpbGUoY29sb3VyID0gImJsYWNrIikgKwogIGdndGl0bGUoIkFjdGl2ZSBvciBub3QiKSArCiAgeGxhYigiTW9udGhzIHNpbmNlIHNpZ251cCIpICsKICBzY2FsZV9maWxsX2Rpc2NyZXRlKG5hbWUgPSAiIikgKwogIHRoZW1lKGxlZ2VuZC5wb3NpdGlvbiA9ICJ0b3AiKQpgYGAKCk5vdyB3ZSBjYW4gZ3JvdXAgYnkgbW9udGhzIHNpbmNlIHRoZSBjb2hvcnQgc2lnbmVkIHVwIGFuZCBjb3VudCB0aGUgYWN0aXZlIG1vbnRocywKCmBgYHtyfQp1c2Vyc19ieV9hY3Rpdml0eSAlPiUKICBmaWx0ZXIoYWN0aXZlID09ICJhY3RpdmUiKSAlPiUKICBjb3VudChtb250aHMpICU+JQogIGdncGxvdChhZXMoeCA9IG1vbnRocywgeSA9IG4pKSArCiAgZ2VvbV9iYXIoc3RhdCA9ICJpZGVudGl0eSIpICsKICBnZ3RpdGxlKCJBY3RpdmUgdXNlcnMgKHJldGVudGlvbiBha2EgJ3N0aWNraW5lc3MnKSIpICsKICB4bGFiKCJNb250aHMgc2luY2UgY29ob3J0IHNpZ25lZCB1cCIpICsKICB5bGFiKCJOdW1iZXIgb2YgYWN0aXZlIHVzZXJzIikgKwogIHNjYWxlX3lfY29udGludW91cyhicmVha3MgPSBzY2FsZXM6OnByZXR0eV9icmVha3MoMTApKQpgYGAKClRoZXJlIGFyZSBtYW55IHF1ZXN0aW9ucyBJIG11c3QgbGVhdmUgdW5hbnN3ZXJlZCBmb3Igbm93LiBJbiB0aGUgbWVhbnRpbWUgSSdkIGxpa2UgdG8gcmVmZXIgeW91IHRvIHRoZSBbbGlmZXRpbWVzXSgpIGxpYnJhcnkgd2hlcmUgeW91J2xsIGZpbmQgUHl0aG9uIGNvZGUgdGhhdCBhcHBsaWVzIHRoZXNlIG1vZGVscy4KCkJlZm9yZSBjbG9zaW5nLCBsZXQgbWUgcG9pbnQgeW91IGRpcmVjdGx5IHRvIHJlc291cmNlcyBieSBEcnMuIEZhYmVyIGFuZCBIYXJkaWUgd2hlcmUgdGhleSBzaG93IHRoYXQgdGhlIG1vZGVsIHBlcmZvcm1zIHNwZWN0YWN1bGFybHkgYWdhaW5zdCBhIHJlYWwgZGF0YXNldDoKCi0gIFtGb3JlY2FzdGluZyBSZXBlYXQgU2FsZXMgYXQgQ0ROT1c6IEEgQ2FzZSBTdHVkeSAoMjAwMCldKGh0dHA6Ly9jaXRlc2VlcnguaXN0LnBzdS5lZHUvdmlld2RvYy9zdW1tYXJ5P2RvaT0xMC4xLjEuMzcuNDQxMCkKLSAgW0hhcmRpZSB3b3JraW5nIHBhcGVyc10oaHR0cDovL2JydWNlaGFyZGllLmNvbS9wYXBlcnMuaHRtbCkKClRoaXMgbWV0aG9kIGZvciBtb2RlbGluZyBhY3RpdmUgdXNlcnMgaXMgdmVyeSBwb3dlcmZ1bCBiZWNhdXNlIGl0IGxldmVyYWdlcyBwcm9iYWJpbGl0eSBhbmQgc3RhdGlzdGljYWwgdGhlb3J5LiBNdWNoIGxpa2UgZW5naW5lZXJpbmcsIHN0YXRpc3RpY3MgYW5kIHByb2JhYmlsaXR5IGFyZSBsZXZlcnMgdGhhdCB3aGVuIGVmZmVjdGl2ZWx5IHVzZWQgdG9nZXRoZXIgY2FuIG9mZmVyIHNvbHV0aW9ucyB0byBpbmNyZWRpYmx5IGNoYWxsZW5naW5nIHByb2JsZW1zLiBGYXIgZnJvbSBjb25zdHJhaW5lZCB0byBhY3RpdmUgdXNlcnMsIHRoaXMgbWV0aG9kIGlzIGFsc28gbGlrZWx5IHRvIHdvcmsgd2VsbCBmb3IgaW52b2ljZXMgYW5kIG90aGVyIHZhbHVhYmxlIHByb2JsZW1zLgoKU291cmNlOiBbZ2l0aHViXShodHRwczovL2dpdGh1Yi5jb20vc3RhdHdvbmsvY3VzdG9tZXItYmFzZS1hbmFseXNpcy8p