docs: private TreeNodeVisitor train(OnLineStatistics setScore, RegressionDataSet subSet, List<Integer> features, CategoricalData[] catInfo, Random rand)
      {
          //Should we stop? Stop split(S)
          if(subSet.size() < stopSize || setScore.getVarance() <= 0.0 || Double.isNaN(setScore.getVarance()))
              return new NodeR(setScore.getMean());
          
          double bestGain = Double.NEGATIVE_INFINITY;
          double bestThreshold = Double.NaN;
          int bestAttribute = -1;
          OnLineStatistics[] bestScores = null;
          List<RegressionDataSet> bestSplit = null;
          Set<Integer> bestLeftSide = null;
          
          
          
          /*
           * TODO use smarter random feature selection based on how many features 
           * we need relative to how many are available
           */ 
          Collections.shuffle(features);
          
          //It is possible, if we test all attribute - that one was categorical and no longer an option
          final int goTo = Math.min(selectionCount, features.size());
          for(int i = 0; i < goTo; i++)
          {
              double gain;
              double threshold = Double.NaN;
              Set<Integer> leftSide = null;
              OnLineStatistics[] stats;
              int a = features.get(i);
              
              List<RegressionDataSet> aSplit;
              
              if(a < catInfo.length)
              {
                  final int vals = catInfo[a].getNumOfCategories();
                  
                  if(binaryCategoricalSplitting || vals == 2)
                  {
                      stats = createStats(2);
                      Set<Integer> catsValsInUse = new IntSet(vals*2);
                      for(int j = 0; j < subSet.size(); j++)
                          catsValsInUse.add(subSet.getDataPoint(j).getCategoricalValue(a));
                      if(catsValsInUse.size() == 1)
                          return new NodeR(setScore.getMean());
                      leftSide = new IntSet(vals);
                      int toUse = rand.nextInt(catsValsInUse.size()-1)+1;
                      ListUtils.randomSample(catsValsInUse, leftSide, toUse, rand);
                      //Now we have anything in leftSide is path 0, we can do the bining
                      
                      aSplit = new ArrayList<>(2);
                      aSplit.add(subSet.emptyClone());
  		    aSplit.add(subSet.emptyClone());
  		    
                      for(int j = 0; j < subSet.size(); j++)
                      {
                          DataPoint dp = subSet.getDataPoint(j);
  			double w_j = subSet.getWeight(j);
  			double y_j = subSet.getTargetValue(j);
                          int dest = leftSide.contains(dp.getCategoricalValue(a)) ? 0 : 1;
                          stats[dest].add(y_j, w_j);
                          aSplit.get(dest).addDataPoint(dp, y_j, w_j);
                      }
                      
                  }
                  else//split on each value
                  {
  		    stats = createStats(vals);
  		    //Bin all the points to get their scores
  		    aSplit = new ArrayList<>(vals);
  		    for(int z = 0; z < vals; z++)
  			aSplit.add(subSet.emptyClone());
  		    
  		    for (int j = 0; j < subSet.size(); j++)
  		    {
  			DataPoint dp = subSet.getDataPoint(j);
  			double w_j = subSet.getWeight(j);
  			double y_j = subSet.getTargetValue(j);
  			stats[dp.getCategoricalValue(a)].add(y_j, w_j);
  			aSplit.get(dp.getCategoricalValue(a)).addDataPoint(dp, y_j, w_j);
  		    }
  		}
              }
              else
              {
                  int numerA = a - catInfo.length;
                  double min = Double.POSITIVE_INFINITY, max = Double.NEGATIVE_INFINITY;
                  for(int j = 0; j < subSet.size(); j++)
                  {
  		    DataPoint dp = subSet.getDataPoint(j);
  		    double val = dp.getNumericalValues().get(numerA);
  		    min = Math.min(min, val);
  		    max = Math.max(max, val);
                  }
                  
                  //Uniform random threshold
                  threshold = rand.nextDouble()*(max-min)+min;
                  stats = createStats(2);
                  
                  aSplit = new ArrayList<>(2);
  		aSplit.add(subSet.emptyClone());
  		aSplit.add(subSet.emptyClone());
  		
  		for (int j = 0; j < subSet.size(); j++)
  		{
  		    DataPoint dp = subSet.getDataPoint(j);
  		    double w_j = subSet.getWeight(j);
  		    double y_j = subSet.getTargetValue(j);
  		    double val = dp.getNumericalValues().get(numerA);
  
  		    int toAddTo = val <= threshold ? 0 : 1;
  
  		    aSplit.get(toAddTo).addDataPoint(dp, y_j, w_j);
  		    stats[toAddTo].add(y_j, w_j);
  		}
                  
              }
              
              gain = 1;
              double varNorm = setScore.getVarance();
              double varSum = setScore.getSumOfWeights();
              for(OnLineStatistics stat : stats)
                  gain -= stat.getSumOfWeights()/varSum*(stat.getVarance()/varNorm);
              if(gain > bestGain)
              {
                  bestGain = gain;
                  bestAttribute = a;
                  bestThreshold = threshold;
                  bestScores = stats;
                  bestSplit = aSplit;
                  bestLeftSide = leftSide;
              }
              
          }
          
          //Best attribute has been selected
          NodeBase toReturn;
          if (bestAttribute >= 0)
          {
              if (bestAttribute < catInfo.length)
                  if (bestSplit.size() == 2)//2 paths only
                      toReturn = new NodeRCat(bestAttribute, bestLeftSide, setScore.getMean());
                  else
                  {
                      toReturn = new NodeRCat(goTo, bestSplit.size(), setScore.getMean());
                      features.remove(new Integer(bestAttribute));//Feature nolonger viable in this case
                  }
              else
                  toReturn = new NodeRNum(bestAttribute - catInfo.length, bestThreshold, setScore.getMean());
  
              for (int i = 0; i < toReturn.children.length; i++)
              {
                  toReturn.children[i] = train(bestScores[i], bestSplit.get(i), features, catInfo, rand);
              }
              return toReturn;
          }
          return new NodeR(setScore.getMean());
      }