On Computers

Monday, December 18, 2017

Churn Prediction Using Machine Learning -- Take Two


This is another attempt to use tenant metrics to predict if tenant is going to churn.
The thinking is, that normal tenant’s lifecycle is:
  1. Ramp-up (irregular behavior)
  2. Normal behavior
  3. Optional churn (deviation from normal)

Approach:
Learn by looking at churned tenants:
Take one year of tenant’s metrics after the tenant has ramped up and before it churned.  First 6 months of that year is normal behavior.  Fit normal behavior into a linear function e.g.
y = a + xb

Look at the last 6 months of that year.  Does it still fit the same function?  If it’s growing less or declining it’s a sign of churn.

Specifically look at metrics such as active accounts, active subscriptions.

Example

This is an example of tenant metrics chart:
Chart shows that tenant metric stopped growing considerably earlier than churn event (churn is end of chart’s X axis).

Tools

Anaconda python distribution https://anaconda.org/anaconda/python
Jupyter notebooks http://jupyter.org/
Sklearn toolkit http://scikit-learn.org/
Redshift (Tenant metrics was put into redshift where it was transformed as needed and then exported for analysis.)

Results

Results were not conclusive, and model accuracy wasn’t high enough.  In some cases data was not helpful (meaning that tenant metrics were not indicative of tenant’s churn).


On the positive note, learned a little about sklearn and pandas.  When used in a jupyter notebook, it’s the most awesome and simple interactive machine learning environment I’ve seen, here are a couple of illustrative screenshots:


Using Machine Learning to Detect Customer Churn


My company collects daily tenant metrics that include variables like Payments, Total Amount Invoiced, Tenant Status.  Metrics are collected daily for all tenants, and metrics history is kept indefinitely.


Can we use tenant metrics to predict which tenants will churn next?

High-Level Approach 

Divide the metrics into two parts:
  • Metrics for tenants that churned (Churned Dataset)
  • Metrics for tenants that didn’t churn (Active Datase

Use Churned Dataset to train the model about churn.
AWS ML automatically subdivides the dataset into two parts, by default:
  • 70% for training
  • 30% for validation
Use Active Dataset to predict churn.




Detailed Approach

Obtain Tenant Metrics

We obtained a sample of these metrics (500M) from production.  


Dataset contains daily metrics about:
614 tenants
116 active
145 churned
352 ignored (e.g. trial, employee test tenants)
 

Prepare Data


Write a program to divide the dataset in 2:
  • Churned tenants
  • Active tenants
Same program also
  • Converts date into account length, and into “days to churn”
  • Normalizes all metrics by converting them “percentage growth from yesterday” times 1,000
  • Limits growth or decline to +/- 100,000
  • Removes trailing records for churned tenants only keeping records until day of churn
  • Removes tenant name from churn dataset
  • Remove “days to churn” from active dataset

Create AWS Machine Learning Datasource

Target attribute (the one we’re trying to predict) is “days to churn” (named Date in the model).

Create “Churned Tenants” datasource and use Date as the target attribute (i.e. predict days to churn).
 

Create AWS Machine Learning Model

Create “Churned Tenants” ML Model

Train AWS Machine Learning Model


We trained the model on the churned tenants dataset.  

AWS uses 70% of the data to train, and 30% to validate the model.

Recipe

Model had a fairly high error: RMSE of 583 days, which means individual predictions can be off by that many days.  However the thought is, for a given tenant, given many records, average prediction would be a little more accurate.
AWS ML chose linear regression with multiple variables to predict days to churn.  The below is the “recipe” it came up with, showing which columns were used, and which had higher weight.

{
 "groups" : {
   "NUMERIC_VARS_QB_50" : "group('Total_Payments_Received_converted')",
   "NUMERIC_VARS_QB_500" : "group('Orders','Products','Total_Electronic_Payments_Received_converted','Subscriptions','Active_Accounts','Amendments','Electronic_Payments','Adjustments','Invoices','Users','Active_Subscriptions','Payment_Methods','Bill_Runs','Usage_Record_Uploads','Cancelled_Subscriptions','Total_Amount_Invoiced_converted','Total_Accounts','Data_Sources_Exports','Active_Payment_Gateways','Refunds')",
   "NUMERIC_VARS_QB_200" : "group('Rate_Plans','Payments','Payment_Gateways','Total_Amount_Refunded_converted','Total_Account_Balance_converted','Currencies')",
   "NUMERIC_VARS_QB_10" : "group('Edition')"
 },
 "assignments" : { },
 "outputs" : [ "ALL_CATEGORICAL", "quantile_bin(NUMERIC_VARS_QB_50,50)", "quantile_bin(NUMERIC_VARS_QB_500,500)", "quantile_bin(NUMERIC_VARS_QB_200,200)", "quantile_bin(NUMERIC_VARS_QB_10,10)" ]
}

Generate Predictions for Active Tenants

We passed metrics for active tenants into the model, and computed the days to churn for each daily metric of every active tenant.

Then we averaged the predictions for each tenant.

Summary: what was learned

It remains to be understood what is the best approach to transform tenant metrics into best shape for predicting churn.  The weakness of our approach was assumption that days_to_churn has some correlation to today’s changes in tenant metrics (even though smoothed out and scaled).  While there is some correlation (e.g. less money going through the system, less usage) the key challenge is how to build a machine learning model that is accurate.

We ran out of time allotted for the hackathon, and didn’t try out these potentially promising ideas:
  • Normalize days to churn so it has the same range for all tenants (would require us to scale data for tenants)
  • Smooth out metrics using approaches like moving averages or other
  • Brainstorm other ideas for analyzing time series
  • Consider taking only last N days before churn for churned tenants, and active tenants
  • Consider other machine learning approaches beside linear regression
    • Clustering (e.g. find similarity to churned tenants rather than predict days)
    • Neural networks

Tools Used

Java for cleaning and preparing data
Excel (pivot table)
AWS Machine Learning
Csvkit (command line csv tools)
Unix command line (cut, paste, grep)


Friday, January 02, 2015

Functional Programming Principles in Scala

Attaching coursera certificate (statement of accomplishment) here for recordkeeping purposes.

Microservices: Netflix OSS Reference Architecture Study

Netflix provides sample distributed microservices application (RSS server) that is comprised of:
  • edge server (responds to requests from RSS clients and gets feeds from middle-tier server)
  • middle-tier server (pulls RSS feeds from content providers)
  • services registry (eureka)

How to Build

RSS middle-tier and edge servers can be built and run using instructions provided here:
Eureka can be built using instructions here:

How to Run

Follow instructions here:
Note that:
  • Default configuration requires that eureka be running on port 80, which would require you to launch the tomcat using sudo (as well as modify server.xml).
  • Eureka services registry may be persisted using cassandra, or in-memory without using cassandra.  The default is to use the in-memory persistence.
  • Eureka by default is configured to talk to AWS, and there will be exceptions during start up if AWS properties aren't configured – however these exceptions may be safely ignored.
At the end the application is running at 

Application Architecture

Deployment

There are 3 processes
  • Eureka is running in tomcat container.
  • RSS middle-tier is running in jetty container
    • port 9191 is netty server port
      # Netty Configuration
      netty.http.port=9191
      netty.http.host=0.0.0.0
       
    • port 9192 is healthcheck server port
      # Health Check Handler
      com.netflix.karyon.health.check.handler.classname=com.netflix.recipes.rss.manager.MiddleTierHealthCheckHandler
      netflix.platform.admin.resources.port=9192
      com.netflix.karyon.unify.health.check.with.eureka=true
       
  • RSS edge server is running in jetty container 
    • port 9090 is jetty port (this is the server user interacts with via browser http://localhost:9090/jsp/rss.jsp)
      # Jetty Configuration
      jetty.http.port=9090
       
    • port 9092 is healthcheck server port
      # Health Check Handler
      com.netflix.karyon.health.check.handler.classname=com.netflix.recipes.rss.server.EdgeHealthCheckHandler
      netflix.platform.admin.resources.port=9092
      com.netflix.karyon.unify.health.check.with.eureka=true

Middle-tier Server

Class MiddleTierResource.java implements REST Service using jax-ws annotations.  Servo metrics are maintained by the same class and registered with servo which exposes them.
Example GET method in AddRSSCommand.java:
@GET
@Path("/rss/user/{user}")
@Produces({MediaType.APPLICATION_JSON})
public Response fetchSubscriptions (final @PathParam("user") String user) {
// Start timer
Stopwatch stopwatch = getRSSStatsTimer.start();
try {
getRSSRequestCounter.increment();
Subscriptions subscriptions = RSSManager.getInstance().getSubscriptions(user);
return Response.ok(subscriptions).build();
} catch (Exception e) {
logger.error("Exception occurred when fetching subscriptions", e);
getRSSErrorCounter.increment();
return Response.serverError().build();
} finally {
stopwatch.stop();
getRSSStatsTimer.record(stopwatch.getDuration(TimeUnit.MILLISECONDS), TimeUnit.MILLISECONDS);
}
}
Communication between edge server and middle tier server.  Example: edge server requests subscriptions:
GET /middletier/rss/user/default HTTP/1.1
Netflix.NFHttpClient.Version: 1.0
X-netflix-httpclientname: middletier-client
Host: C02LG18GF1G3.local:9191
Connection: Keep-Alive
User-Agent: Apache-HttpClient/4.1.2 (java 1.5)
HTTP/1.0 200 OK
Content-Type: application/json
{"subscriptions":[{"items":[{"description":"
 Communication between middle-tier server and RSS content servers.  Middle-tier server acts as a client and uses ribbon to pull RSS feeds.  Example in RSSManager.java:
 

/**
* Fetch the RSS feed content using Ribbon
*/
private RSS fetchRSSFeed(String url) {
 
RestClient client = (RestClient) ClientFactory.getNamedClient(RSSConstants.MIDDLETIER_REST_CLIENT);
HttpClientResponse response;
String rssData = null;
 
try {
HttpClientRequest request = HttpClientRequest.newBuilder().setUri(new URI(url)).build();
response = client.execute(request);
 
if (response != null) {
rssData = IOUtils.toString(response.getRawEntity(), Charsets.UTF_8);
logger.info("Status code for " + response.getRequestedURI() + " : " + response.getStatus());
}
} catch (URISyntaxException e) {
logger.error("Exception occurred when setting the URI", e);
} catch (Exception e) {
logger.error("Exception occurred when executing the HTTP request", e);
}
 
return parseRSS(url, rssData);
}

Technologies used by application: 
  • jetty (http/servlet container, e.g. like tomcat)
  • jersey (REST services framework)
  • netty (NIO client framework) 

Edge Server

Acts as a client talking to the middle-tier server, see sample JSON request in the section above.
Each request to the middle-tier server is wrapped in a hystrix command.  Example:

/**
* Calls the middle tier Add RSS entry point
*/
public class AddRSSCommand extends HystrixCommand {
// RSS Feed Url (encoded)
private final String url;
public AddRSSCommand(String url) {
super (
Setter.withGroupKey(
HystrixCommandGroupKey.Factory.asKey(RSSConstants.HYSTRIX_RSS_MUTATIONS_GROUP))
.andCommandKey(HystrixCommandKey.Factory.asKey(RSSConstants.HYSTRIX_RSS_ADD_COMMAND_KEY))
.andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey(RSSConstants.HYSTRIX_RSS_THREAD_POOL)
)
);
this.url = url;
}
@Override
protected String run() {
try {
/*
* The named client param must match the prefix for the ribbon
* configuration specified in the edge.properties file
*/
RestClient client = (RestClient) ClientFactory.getNamedClient(RSSConstants.MIDDLETIER_REST_CLIENT);
HttpClientRequest request = HttpClientRequest
.newBuilder()
.setVerb(Verb.POST)
.setUri(new URI("/"
+ RSSConstants.MIDDLETIER_WEB_RESOURCE_ROOT_PATH
+ RSSConstants.RSS_ENTRY_POINT
+ "?url=" + url))
.build();
HttpClientResponse response = client.executeWithLoadBalancer(request);
return IOUtils.toString(response.getRawEntity(), Charsets.UTF_8);
} catch (Exception exc) {
throw new RuntimeException("Exception occurred when adding a RSS feed", exc);
}
}
@Override
protected String getFallback() {
// Empty json
return "{}";
}
}

JMX

Both the edge server and the middletier server expose JMX metrics (mbeans in package com.netflix.servo).

Wednesday, July 23, 2014

Streaming mode for camel-splunk consumer

Apache Camel has a splunk connector.  However it has an issue, that it runs out of memory if result set is too large.  I fixed it by adding streaming support. 

Here is the patch for streaming mode:
https://github.com/dmitrimedvedev/camel/commit/7e4b5e9b206c7a969e6012d9afa40ac7024ee515
https://github.com/dmitrimedvedev/camel/commit/f7063f760f5abf867fa2a6bbbd187220c941fa00

Tracked on ASF jira

Tuesday, July 08, 2014

Comparison of NoSQL systems

Here is an interesting spreadsheet comparing NoSQL systems:

Source:
University of Washington
Introduction to Data Science
https://class.coursera.org/datasci-001

Next trendy open source projects

If past is any indication of future trends, surely next trendy open source projects will be what's currently developed at technology leaders like google.  Similar to hadoop becoming popular after google published its map-reduce paper, and then big table implemented as hbase by open source community, surely there is a cool factor as well as value and following in building open source implementations of:
  • tenzing
  • dremel
  • pregel
  • megastore
  • spanner

Thursday, July 03, 2014

Bash trick to reference all args from previous command

!!:1- is a nice shortcut for referencing all arguments from the previous bash command.

command                equivalent
---------------------------------------
ls                           
sudo !!                    sudo ls
!$                            ls
ls 1.txt 2.txt           
ls !$                        ls 2.txt
ls 1.txt 2.txt           
cat !!:1-                  cat 1.txt 2.txt  

Wednesday, July 02, 2014

NoSQL databases and CAP theorem

CAP theorem claims that it's possible to achieve only two of the three in a distributed system:
  • consistency
  • availability
  • partition tolerance (# of nodes)
How do NoSQL databases stand on this?  There is a nice illustration from Shashank Tiwari: