protected Pair<LIMEExplanation, List<Example<Regressor>>> explainWithSamples(Map<String, String> input) {
        Optional<Example<Label>> optExample = generator.generateExample(input,false);
        if (optExample.isPresent()) {
            Example<Label> example = optExample.get();
            if ((textDomain.size() == 0) && (binarisedCDFs.size() == 0)) {
                // Short circuit if there are no text or binarised fields.
                return explainWithSamples(example);
            } else {
                Prediction<Label> prediction = innerModel.predict(example);

                // Build the input example with simplified text features
                ArrayExample<Regressor> labelledExample = new ArrayExample<>(transformOutput(prediction));

                // Add the tabular features
                for (Feature f : example) {
                    if (tabularDomain.getID(f.getName()) != -1) {
                        labelledExample.add(f);
                    }
                }
                // Extract the tabular features into a SparseVector for later
                SparseVector tabularVector = SparseVector.createSparseVector(labelledExample,tabularDomain,false);

                // Tokenize the text fields, and generate the perturbed text representation
                Map<String, String> exampleTextValues = new HashMap<>();
                Map<String, List<Token>> exampleTextTokens = new HashMap<>();
                for (Map.Entry<String,FieldProcessor> e : textFields.entrySet()) {
                    String value = input.get(e.getKey());
                    if (value != null) {
                        List<Token> tokens = tokenizerThreadLocal.get().tokenize(value);
                        for (int i = 0; i < tokens.size(); i++) {
                            labelledExample.add(nameFeature(e.getKey(),tokens.get(i).text,i),1.0);
                        }
                        exampleTextValues.put(e.getKey(),value);
                        exampleTextTokens.put(e.getKey(),tokens);
                    }
                }

                // Sample a dataset.
                List<Example<Regressor>> sample = sampleData(tabularVector,exampleTextValues,exampleTextTokens);

                // Generate a sparse model on the sampled data.
                SparseModel<Regressor> model = trainExplainer(labelledExample, sample);

                // Test the sparse model against the predictions of the real model.
                List<Prediction<Regressor>> predictions = new ArrayList<>(model.predict(sample));
                predictions.add(model.predict(labelledExample));
                RegressionEvaluation evaluation = evaluator.evaluate(model,predictions,new SimpleDataSourceProvenance("LIMEColumnar sampled data",regressionFactory));

                return new Pair<>(new LIMEExplanation(model, prediction, evaluation),sample);
            }
        } else {
            throw new IllegalArgumentException("Label not found in input " + input.toString());
        }
    }
@Test
    public void testCategorical() throws URISyntaxException {
        Pair<RowProcessor<Label>,Dataset<Label>> pair = generateCategoricalDataset();

        RowProcessor<Label> rp = pair.getA();
        Dataset<Label> dataset = pair.getB();

        XGBoostClassificationTrainer trainer = new XGBoostClassificationTrainer(50);

        Model<Label> model = trainer.train(dataset);

        SparseTrainer<Regressor> sparseTrainer = new CARTJointRegressionTrainer(4,true);

        LIMEColumnar lime = new LIMEColumnar(new SplittableRandom(1),model,sparseTrainer,5000,rp,tokenizer);

        Map<String,String> testExample = new HashMap<>();

        testExample.put("A","Small");
        testExample.put("B","4.0");
        testExample.put("C","4.0");
        testExample.put("D","Red");
        testExample.put("TextField","The full text field has more words in it than other fields.");

        Pair<LIMEExplanation, List<Example<Regressor>>> explanation = lime.explainWithSamples(testExample);

        List<String> activeFeatures = explanation.getA().getActiveFeatures();
        assertNotNull(activeFeatures);

        int[] aSampleCount = new int[3];
        int[] dSampleCount = new int[3];

        int aPresentCounter = 0;
        int dPresentCounter = 0;

        for (Example<Regressor> e : explanation.getB()) {
            boolean aPresent = false;
            boolean dPresent = false;
            int aCounter = 0;
            int bCounter = 0;
            int cCounter = 0;
            int dCounter = 0;
            int textCounter = 0;

            for (Feature f : e) {
                String featureName = f.getName();
                if (featureName.equals("A"+ ColumnarFeature.JOINER+"Small")) {
                    aSampleCount[0]++;
                    aCounter++;
                    aPresent = true;
                } else if (featureName.equals("A"+ ColumnarFeature.JOINER+"Medium")) {
                    aSampleCount[1]++;
                    aCounter++;
                    aPresent = true;
                } else if (featureName.equals("A"+ ColumnarFeature.JOINER+"Large")) {
                    aSampleCount[2]++;
                    aCounter++;
                    aPresent = true;
                } else if (featureName.startsWith("B")) {
                    bCounter++;
                } else if (featureName.startsWith("C")) {
                    cCounter++;
                } else if (featureName.equals("D"+ ColumnarFeature.JOINER+"Red")) {
                    dSampleCount[0]++;
                    dCounter++;
                    dPresent = true;
                } else if (featureName.equals("D"+ ColumnarFeature.JOINER+"Yellow")) {
                    dSampleCount[1]++;
                    dCounter++;
                    dPresent = true;
                } else if (featureName.equals("D"+ ColumnarFeature.JOINER+"Green")) {
                    dSampleCount[2]++;
                    dCounter++;
                    dPresent = true;
                } else if (featureName.startsWith("TextField")) {
                    textCounter++;
                } else {
                    fail("Unknown feature with name " + featureName);
                }
            }
            if (!aPresent) {
                aPresentCounter++;
            }
            if (!dPresent) {
                dPresentCounter++;
            }

            // Real features should be sampled correctly
            if (bCounter != 1) {
                fail("Should only sample one B feature");
            }
            if (cCounter != 1) {
                fail("Should only sample one C feature");
            }

            // Categorical features may be sampled multiple times as they are not specified as BINARISED_CATEGORICAL
            if ((aCounter > 3) || (aCounter < 0)) {
                fail("Should sample between 0 and 3 A features");
            }
            if ((dCounter > 3) || (dCounter < 0)) {
                fail("Should sample between 0 and 3 D features");
            }
        }

        assertTrue(aSampleCount[0] > 1000);
        assertTrue(aSampleCount[0] < 2500);
        //System.out.println(aSampleCount[0]);
        assertTrue(aSampleCount[1] > 1000);
        assertTrue(aSampleCount[1] < 2500);
        //System.out.println(aSampleCount[1]);
        assertTrue(aSampleCount[2] > 1000);
        assertTrue(aSampleCount[2] < 2500);
        //System.out.println(aSampleCount[2]);
        assertTrue(aPresentCounter > 1000);
        assertTrue(aPresentCounter < 2500);
        //System.out.println(aPresentCounter);

        assertTrue(dSampleCount[0] > 1000);
        assertTrue(dSampleCount[0] < 2500);
        //System.out.println(dSampleCount[0]);
        assertTrue(dSampleCount[1] > 1000);
        assertTrue(dSampleCount[1] < 2500);
        //System.out.println(dSampleCount[1]);
        assertTrue(dSampleCount[2] > 1000);
        assertTrue(dSampleCount[2] < 2500);
        //System.out.println(dSampleCount[2]);
        assertTrue(dPresentCounter > 1000);
        assertTrue(dPresentCounter < 2500);
        //System.out.println(dPresentCounter);
    }