---
title: "Clipping & Spreading... "
author: "Vicens Gaitan"
date: "Friday, October 16, 2015"
output: html_document
---

```{r}
library(xgboost)
library(Matrix)
library(caret)
library(ggplot2)
library(readr)

```
```{r,echo=FALSE}
library(pracma)
#Rolling window: take window with definite size through the array
#param data: array-like
#param window_size: size
#return: the sequence of windows
#Example: data = array(1, 2, 3, 4, 5, 6), window_size = 4
#Then this function return array(array(1, 2, 3, 4), array(2, 3, 4, 5), array(3, 4, 5, 6))

rolling_window <- function(data,window_size){
  rw <- NULL
  for(i in 1:(length(data)-window_size+1)){
    rw[[i]] <- data[i:(i+window_size-1)]
  }
  rw
}

#Following function has been copied from R package "WeightedROC
# because it is not available on CRAN. and not available on Kaggle R Scripts
# for using on a local machine, users are recommended to install the relevant package 
# by running following command:
# devtools::install_github("tdhock/WeightedROC")
# Author: Toby Dylan Hocking
# library(WeightedROC)

WeightedROC <- function (guess, label, weight = rep(1, length(label))) 
{
  if (is.factor(label)) {
    label <- as.integer(label)
  }
  stopifnot(is.numeric(label))
  if (all(label %in% c(0, 1))) {
    label[label == 0] <- -1
  }
  if (all(label %in% c(1, 2))) {
    label[label == 1] <- -1
    label[label == 2] <- 1
  }
  stopifnot(label %in% c(-1, 1))
  stopifnot(is.numeric(guess))
  stopifnot(length(label) == length(guess))
  stopifnot(is.numeric(weight))
  stopifnot(length(label) == length(weight))
  #stopifnot(weight > 0)
  ord <- order(guess)
  y <- label[ord]
  w <- weight[ord]
  y.hat <- guess[ord]
  is.positive <- y == 1
  is.negative <- y == -1
  w.positive <- w.negative <- w
  w.positive[is.negative] <- 0
  w.negative[is.positive] <- 0
  cum.positive <- cumsum(w.positive)
  cum.negative <- cumsum(w.negative)
  is.end <- c(diff(y.hat) != 0, TRUE)
  n <- length(y)
  TPR <- c(1, 1 - cum.positive[is.end]/cum.positive[n])
  FPR <- c(1, 1 - cum.negative[is.end]/cum.negative[n])
  d <- data.frame(TPR, FPR)
  d
}


cvm <- function (subindices,total_events){
  #   Compute Cramer-von Mises metric.
  #   Compared two distributions, where first is subset of second one.
  #   Assuming that second is ordered by ascending
  #   :param subindices: indices of events which will be associated with the first distribution
  #   :param total_events: count of events in the second distribution
  #   :return: cvm metric
  target_distribution = (1:(1+total_events))/total_events
  subarray_distribution = cumsum((hist(x = subindices,breaks = seq(0.5,total_events+1.5,1),plot = FALSE))$counts)
  subarray_distribution = subarray_distribution/subarray_distribution[length(subarray_distribution)]
  mean((target_distribution-subarray_distribution)^2)
}


compute_cvm <- function(predictions,masses, n_neighbours = 200,step = 50){
  # Computing Cramer-von Mises (cvm) metric on background events: take average of cvms calculated for each mass bin.
  # In each mass bin global prediction's cdf is compared to prediction's cdf in mass bin.
  # :param predictions: array-like, predictions
  # :param masses: array-like, in case of Kaggle tau23mu this is reconstructed mass
  # :param n_neighbours: count of neighbours for event to define mass bin
  # :param step: step through sorted mass-array to define next center of bin
  # :return: average cvm value
  
  # First, reorder by masses
  predictions = predictions[order(masses)]
  # Second, replace probabilities with order of probability among other events
  predictions = order(order(predictions))
  # Now, each window forms a group, and we can compute contribution of each group to CvM
  cvms = c()
  rw = rolling_window(predictions,window_size = n_neighbours)
  for (window in rw[seq(1,length(rw),step)]){
    cvms = c(cvms,cvm(subindices = window,total_events = length(predictions)))
  }
  #mean(cvms)
  cvms
}

roc_curve_splitted <- function(data_zero, data_one, sample_weights_zero, sample_weights_one){
  #   Compute roc curve
  #   :param data_zero: 0-labeled data
  #   :param data_one:  1-labeled data
  #   :param sample_weights_zero: weights for 0-labeled data
  #   :param sample_weights_one:  weights for 1-labeled data
  #   :return: roc curve
  labels = c(rep(0,length(data_zero)),rep(1,length(data_one)))
  weights = c(sample_weights_zero, sample_weights_one)
  data_all = c(data_zero, data_one)
  tpr.fpr = WeightedROC(guess = data_all,label = labels,weight = weights)
  tpr.fpr
}
compute_ks <- function(data_prediction, mc_prediction, weights_data, weights_mc){
  #   Compute Kolmogorov-Smirnov (ks) distance between real data predictions cdf and Monte Carlo one.
  #   :param data_prediction: array-like, real data predictions
  #   :param mc_prediction: array-like, Monte Carlo data predictions
  #   :param weights_data: array-like, real data weights
  #   :param weights_mc: array-like, Monte Carlo weights
  #   :return: ks value
  weights_data = weights_data/sum(weights_data)
  weights_mc = weights_mc/sum(weights_mc)
  tpr.fpr = roc_curve_splitted(data_prediction,mc_prediction,weights_data,weights_mc)
  Dnm = max(abs(tpr.fpr[,1]-tpr.fpr[,2]))
  Dnm
}

roc_auc_truncated <- function(labels, predictions, tpr_thresholds=c(0.2, 0.4, 0.6, 0.8),
                              roc_weights=c(4, 3, 2, 1, 0)){
  #   labels = as.numeric(valResponse)
  #   predictions = predValxgbTree_caret[,2]
  #   tpr_thresholds=c(0.2, 0.4, 0.6, 0.8)
  #   roc_weights=c(4, 3, 2, 1, 0)
  #   Compute weighted area under ROC curve.
  #   :param labels: array-like, true labels
  #   :param predictions: array-like, predictions
  #   :param tpr_thresholds: array-like, true positive rate thresholds delimiting the ROC segments
  #   :param roc_weights: array-like, weights for true positive rate segments
  #   :return: weighted AUC
  tpr.fpr = WeightedROC(guess = predictions,label = labels)
  tpr.fpr = tpr.fpr[order(tpr.fpr$TPR,tpr.fpr$FPR),]
  area = 0
  tpr_thresholds = c(0,tpr_thresholds,1)
  for(index in (2:length(tpr_thresholds))){
    #     index = 6
    tpr.cut = sapply(tpr.fpr[,1],function(x){min(x,tpr_thresholds[index])})
    tpr.previous = sapply(tpr.fpr[,1],function(x){min(x,tpr_thresholds[index-1])})
    area = area+roc_weights[index-1]*(trapz(tpr.fpr$FPR,tpr.cut)-trapz(tpr.fpr$FPR,tpr.previous))
  }
  area = area/sum((tpr_thresholds[-1] - tpr_thresholds[-length(tpr_thresholds)])*roc_weights)
  area
}

ptau<-function(d){
  # muon mass in MeV/c^2
  mmu = 105.6583715
 
  # calculate tau energy
  tau_e = sqrt(d$p0_p ** 2 + mmu**2) + sqrt(d$p1_p ** 2 + mmu**2) +sqrt(d$p2_p ** 2 + mmu**2)
  # calculate pz of tau candidate
  tau_pz = d$p0_pt * sinh(d$p0_eta) + d$p1_pt * sinh(d$p1_eta)+d$p2_pt * sinh(d$p2_eta)
  # calculate momentum of tau candidate
  tau_p =sqrt(d$pt ** 2 + tau_pz ** 2)
  # calculate eta of tau candidate
  tau_eta = asinh(tau_pz / d$pt)
  # calculate mass of tau candidate
  tau_m2=tau_e ** 2 - tau_p ** 2
  tau_m2[tau_m2<0]=0
  tau_m=sqrt(tau_m2)
  #M = tau_p*LifeTime*c/FlightDistance
  #c Speed of light
  c= 299.792458
  tau_m=tau_p*d$LifeTime*c/d$FlightDistance
  return(list(tau_e,tau_pz,tau_p,tau_eta,tau_m))
}  
ftrain<-function(data,y_d,params,folds,mask2,nrounds,max,pre=F,scan=F){
  # do the folds
  for(i in 1: length(folds)){
    # get the sampling
    # and construct train and test matrix out of train data
    smpl = folds[[i]]
    g_train = data[-smpl,]
    g_test = data[smpl,]
    y_train = y_d[-smpl]
    y_test = y_d[smpl]
    
    dtrain0=xgb.DMatrix(as.matrix(g_train[,mask2]), label =y_train)
    dtest0=xgb.DMatrix(as.matrix(g_test[,mask2]), label =y_test)

    watchlist0=list(validation=dtest0,train=dtrain0)
    bst0 = xgb.train(param=params, data = dtrain0,nrounds=nrounds,watchlist0,print.every.n = 100,verbose=0,maximize=max,early.stop.round = 100)
    
    #importance_matrix <- xgb.importance(model = bst0,feature_names=names(data[,mask2]))
    #print(importance_matrix)
    
    best0=bst0$bestInd
    
    ptrain[smpl]<-predict(bst0,dtest0,ntreelimit=best0)
    if(pre){
      pagree[,i]=predict(bst0,as.matrix(agree[,mask2]),ntreelimit=best0)
      pcorr[,i]=predict(bst0,as.matrix(corr[,mask2]),ntreelimit=best0)
      #ptest[,i]=predict(bst0,as.matrix(test[,mask2]),ntreelimit=best0)
    }
  }   
  
  return(list(ptrain,pagree,pcorr))
}

fscan<-function(data,y_d,params,folds,mask2,nrounds,max){
  # do the folds
  i=1
    # get the sampling
    # and construct train and test matrix out of train data
    smpl = folds[[i]]
    g_train = data[-smpl,]
    g_test = data[smpl,]
    y_train = y_d[-smpl]
    y_test = y_d[smpl]
    
    dtrain0=xgb.DMatrix(as.matrix(g_train[,mask2]), label =y_train)
    dtest0=xgb.DMatrix(as.matrix(g_test[,mask2]), label =y_test)

    watchlist0=list(validation=dtest0,train=dtrain0)
    bst0 = xgb.train(param=params, data = dtrain0,nrounds=nrounds,watchlist0,print.every.n = 100,verbose=0,maximize=max,early.stop.round = 100)
    
    #importance_matrix <- xgb.importance(model = bst0,feature_names=names(data[,mask2]))
    #print(importance_matrix)
    
    best0=bst0$bestInd
    out=rep(0,best0/10)
    out=cbind(out,out,out)
    names(out)=c("ntree","AUCw","KS")
    out=as.data.frame(out)
    k=1
    for(j in seq(1,best0,10)){
         pred<-predict(bst0,dtest0,ntreelimit=j)
         pagree[,i]=predict(bst0,as.matrix(agree[,mask2]),ntreelimit=j)
         mask=(g_test$min_ANNmuon>.4 )
         ah=roc_auc_truncated(y_test[mask],pred[mask])
         d0=pagree[agree$signal==0,i]
         d1=pagree[agree$signal==1,i]
         w0=agree$weight[agree$signal==0]
         w1=agree$weight[agree$signal==1]
         kks=compute_ks(d0,d1,w0,w1)
         out[k,1]=j
         out[k,2]=ah
         out[k,3]=kks  
         k=k+1
      }
  
  return(out)
}


```

The previous script was an attempt to understand the role of "reconstructed mass" as a way to discriminate between the signal (a tau decaying to 3 muons) and events with similar "shape". The good news were that "reconstructed mass" is **THE** Golden Feature, allowing a weighted AUC differing from one in less than 1.e-5. If we are able to bypass in some way the CVM test, measuring the correlation between the classifier result and the mass, then we can easily build a top 10 classifier. The bad news is that the classifier  accuracy is the result of realizing that the invariant mass spectrum of signal and background are not overlapping in the training set ( an artifact of the data), so this selection method has no physical meaning. 

Without an explicit reconstructed mass, a classifier has to discover other features in the data, because build from scratch an estimation for the mass is not easy a was shown in the previous script, even having at hand all the ingredients in the formula.
Here comes into scene the second "trap" in the competition: Some variables are not well simulated/calibrated in the Monte Carlo simulation... and the classifier learn to discriminate real data from Montecarlo ( instead of signal and background) : This is OK for the training sample, because the signal is Monte Carlo and he background is real data, but fatal to pass the agreement test, in which we have real D decaying into Phi , and simulated ones that must be identical to the classifier, not being the case due to the simulation problems. 

Lets start to see in more detail the results using the reconstructed mass and without it. We add the reconstructed mass as feature.

```{r}
train<- read_csv("../input/training.csv")
m=ptau(train)
train$mm=as.data.frame(m[5])[,1]
agree <- read_csv("../input/check_agreement.csv")

m=ptau(agree)
agree$mm=as.data.frame(m[5])[,1]

corr <- read_csv("../input/check_correlation.csv")
m=ptau(corr)
corr$mm=as.data.frame(m[5])[,1]

y_d=train$signal
ptrain=y_d

pagree=as.data.frame(agree[,1])
pcorr=as.data.frame(corr[,1])

cv_fold_count=2
set.seed(2603)

folds = createFolds(as.factor(y_d),cv_fold_count);

param <- list(
  "objective" = "binary:logistic",
  "eval_metric"="auc",
  "max_depth"=5,
  "eta"=.1
)

data=train

```

In the first model, let's use the reconstructed mass as a feature, and evaluate weighted AUC, CVM and K-S test

```{r,warning=FALSE, message=FALSE}

evaluation<-function(holdout,ragg,rcorr){
  mask=(data$min_ANNmuon>.4 )
  ah=roc_auc_truncated(y_d[mask],holdout[mask])
  cc=compute_cvm(rcorr,corr$mass)
  d0=ragg[agree$signal==0]
  d1=ragg[agree$signal==1]
  w0=agree$weight[agree$signal==0]
  w1=agree$weight[agree$signal==1]
  kks=compute_ks(d0,d1,w0,w1)
  print(paste0("AUCw=",ah," CVM=",mean(cc)," K-S=",kks))
}
```

```{r,warning=FALSE, message=FALSE}
features=names(data)[2:47]
mask2=c("mm",features)

res=ftrain(data,y_d,param,folds,mask2,1000,T,T)
holdout0=as.data.frame(res[1])[,1]
ragree0=rowMeans(as.data.frame(res[2]))
rcorr0=rowMeans(as.data.frame(res[3]))
evaluation(holdout0,ragree0,rcorr0)
```

The weighted AUC is 0.9999920, but nor the correlation nor the agreement test are passed.
This is evident from the probability plots as a function of the reconstructed mass. Signal and background are well separated in train, but also the signal in the agreement test can be easily picked up from the real data. Notice also that probability for agreement events is very low, so its can easily be selected by a cut in the probability.

```{r}
par(mfrow=c(3,1)) 
plot(train$mm,holdout0,pch='.',col=rgb(train$signal,0,0,.3),xlim=c(1700,2100))
plot(agree$mm,ragree0,pch='.',col=rgb(agree$signal,0,0,.2),xlim=c(1700,2100),ylim=c(0,.002))
plot(corr$mm,rcorr0,pch='.',col=rgb(0,0,0,1),xlim=c(1700,2100))
par(mfrow=c(1,1)) 

```

The correlation test verify the correlation between classifier result and mass over a rolling window of ordered events by mass.
```{r}
cc=compute_cvm(rcorr0,corr$mass)
plot(cc)
```

The CVM index is the mean of these values, .85 in this case.

But raising the probability to a high power (clipping to 0 small probabilities, and spreading high probabilities between 0 and 1) adding a small amount of noise, the CVM value drops drastically

```{r}
rcorr0a=.99*rcorr0^5000 + .01*runif(length(rcorr0),0,1)
cc=compute_cvm(rcorr0a,corr$mass)
plot(cc)

```

This simple trick allow to hack also the agreement test simultaneously because all the agreement probabilities are clipped to 0, making trivial to pass the K-S test.

This trick has been discovered accidentally, by intuition o from first principles in several variants including combining weak models, raising results to high powers... and for sure has been used for the top performers in the competition.



Lets start by building a random predictor
```{r}
holdout1=runif(length(holdout0),0,1)
ragree1=runif(length(ragree0),0,1)
rcorr1=runif(length(rcorr0),0,1)
evaluation(holdout1,ragree1,rcorr1)
```

Obviously it passes both test, with low AUCw...
```{r}
par(mfrow=c(3,1)) 
plot(train$mm,holdout1,pch='.',col=rgb(train$signal,0,0,.3),xlim=c(1700,2100))
plot(agree$mm,ragree1,pch='.',col=rgb(agree$signal,0,0,.2),xlim=c(1700,2100))
plot(corr$mm,rcorr1,pch='.',col=rgb(0,0,0,1),xlim=c(1700,2100))
par(mfrow=c(1,1)) 

cc=compute_cvm(rcorr1,corr$mass)
plot(cc)
```

Now, build a combination between the random classifier and the Clipped & Spread "Reconstructed mass"" classifier 
```{r}
alpha=.007
beta=5000

  holdout3=alpha*holdout1+(1-alpha)*holdout0^beta
  ragree3=alpha*ragree1+(1-alpha)*ragree0^beta
  rcorr3=alpha*rcorr1+(1-alpha)*rcorr0^beta
  evaluation(holdout3,ragree3,rcorr3)
```

And voila: a top ten model performer with a single classifier :) Using only training.csv, obviously, so within the law...

But useless form a physical point of view, at least for two reasons: Only discriminates the mass gap in the background, totally unable to separate signal from background in this gap, and, fully correlated with the mass, escaping the CVM test exploiting some weakness of this test.

So... what can be done, useful from a physical point of view?

This is the topic for the next chapter

