docs: private TreeNodeVisitor trainC(ImpurityScore setScore, ClassificationDataSet subSet, List<Integer> features, CategoricalData[] catInfo, Random rand)
      {
          //Should we stop? Stop split(S)
          if(subSet.size() < stopSize || setScore.getScore() == 0.0)
          {
              if(subSet.isEmpty())
                  return null;
              return new NodeC(setScore.getResults());
          }
          
          double bestGain = Double.NEGATIVE_INFINITY;
          double bestThreshold = Double.NaN;
          int bestAttribute = -1;
          ImpurityScore[] bestScores = null;
          List<ClassificationDataSet> 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;
              ImpurityScore[] scores;
              int a = features.get(i);
              
              List<ClassificationDataSet> aSplit;
              
              if(a < catInfo.length)
              {
                  final int vals = catInfo[a].getNumOfCategories();
                  
                  if(binaryCategoricalSplitting || vals == 2)
                  {
                      scores = createScores(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 NodeC(setScore.getResults());
                      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++)
                      {
                          int dest = leftSide.contains(subSet.getDataPoint(j).getCategoricalValue(a)) ? 0 : 1;
                          scores[dest].addPoint(subSet.getWeight(j), subSet.getDataPointCategory(j));
                          aSplit.get(dest).addDataPoint(subSet.getDataPoint(j), subSet.getDataPointCategory(j), subSet.getWeight(j));
                      }
                      
                  }
                  else//split on each value
                  {
                      scores = createScores(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);
  			int y_j = subSet.getDataPointCategory(j);
  			double w_j = subSet.getWeight(j);
  			scores[dp.getCategoricalValue(a)].addPoint(w_j, y_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++)
                  {
                      double val = subSet.getDataPoint(j).getNumericalValues().get(numerA);
                      min = Math.min(min, val);
                      max = Math.max(max, val);
                  }
                  
                  //Uniform random threshold
                  threshold = rand.nextDouble()*(max-min)+min;
  		scores = createScores(2);
  
  		aSplit = new ArrayList<>(2);
  		aSplit.add(subSet.emptyClone());
  		aSplit.add(subSet.emptyClone());
  
  		for (int j = 0; j < subSet.size(); j++)
  		{
  		    double val = subSet.getDataPoint(j).getNumericalValues().get(numerA);
  		    double w_j = subSet.getWeight(j);
  		    int y_j = subSet.getDataPointCategory(j);
  
  		    int toAddTo = val <= threshold ? 0 : 1;
  
  		    aSplit.get(toAddTo).addDataPoint(subSet.getDataPoint(j), y_j, w_j);
  		    scores[toAddTo].addPoint(w_j, y_j);
  		}
                  
              }
              
              gain = ImpurityScore.gain(setScore, scores);
              if(gain > bestGain)
              {
                  bestGain = gain;
                  bestAttribute = a;
                  bestThreshold = threshold;
                  bestScores = scores;
                  bestSplit = aSplit;
                  bestLeftSide = leftSide;
              }
              
          }
          
          //Best attribute has been selected
          NodeBase toReturn;
          if(bestAttribute < 0)
              return null;
          if(bestAttribute < catInfo.length)
              if(bestSplit.size() == 2)//2 paths only
                  toReturn = new NodeCCat(bestAttribute, bestLeftSide, setScore.getResults());
              else
              {
                  toReturn = new NodeCCat(goTo, bestSplit.size(), setScore.getResults());
                  features.remove(new Integer(bestAttribute));//Feature nolonger viable in this case
              }
          else
              toReturn = new NodeCNum(bestAttribute-catInfo.length, bestThreshold, setScore.getResults());
          for(int i = 0; i < toReturn.children.length; i++)
          {
              toReturn.children[i] = trainC(bestScores[i], bestSplit.get(i), features, catInfo, rand);
          }
          return toReturn;
      }