static ConfusionMatrixTuple tabulate(ImmutableOutputInfo<MultiLabel> domain, List<Prediction<MultiLabel>> predictions) {
        // this just keeps track of how many times [class x] was predicted to be [class y]
        DenseMatrix confusion = new DenseMatrix(domain.size(), domain.size());

        Set<MultiLabel> observed = new HashSet<>();

        DenseMatrix[] mcm = new DenseMatrix[domain.size()];
        for (int i = 0; i < domain.size(); i++) {
            mcm[i] = new DenseMatrix(2, 2);
        }

        int predIndex = 0;
        for (Prediction<MultiLabel> prediction : predictions) {
            MultiLabel predictedOutput = prediction.getOutput();
            MultiLabel trueOutput = prediction.getExample().getOutput();
            if (trueOutput.equals(MultiLabelFactory.UNKNOWN_MULTILABEL)) {
                throw new IllegalArgumentException("The sentinel Unknown MultiLabel was used as a ground truth label at prediction number " + predIndex);
            } else if (predictedOutput.equals(MultiLabelFactory.UNKNOWN_MULTILABEL)) {
                throw new IllegalArgumentException("The sentinel Unknown MultiLabel was predicted by the model at prediction number " + predIndex);
            }

            Set<Label> trueSet = trueOutput.getLabelSet();
            Set<Label> predSet = predictedOutput.getLabelSet();

            //
            // Count true positives and false positives
            for (Label pred : predSet) {
                int idx = domain.getID(new MultiLabel(pred.getLabel()));
                if (trueSet.contains(pred)) {
                    //
                    // true positive: mcm[i, 1, 1]++
                    mcm[idx].add(1, 1, 1d);
                } else {
                    //
                    // false positive: mcm[i, 1, 0]++
                    mcm[idx].add(1, 0, 1d);
                }
                observed.add(new MultiLabel(pred));
            }

            //
            // Count false negatives and populate the confusion table
            for (Label trueLabel : trueSet) {
                int idx = domain.getID(new MultiLabel(trueLabel.getLabel()));
                if (idx < 0) {
                    throw new IllegalArgumentException("Unknown label '" + trueLabel.getLabel() + "' found in the ground truth labels at prediction number " + predIndex
                            + ", this label is not known by the model which made the predictions.");
                }

                //
                // Doing two things in this loop:
                // 1) Checking if predSet contains trueLabel
                // 2) Counting the # of times [trueLabel] was predicted to be [predLabel] to populate the confusion table
                boolean found = false;
                for (Label predLabel : predSet) {
                    int jdx = domain.getID(new MultiLabel(predLabel.getLabel()));
                    confusion.add(jdx, idx, 1d);

                    if (predLabel.equals(trueLabel)) {
                        found = true;
                    }
                }

                if (!found) {
                    //
                    // false negative: mcm[i, 0, 1]++
                    mcm[idx].add(0, 1, 1d);
                }
                // else { true positive: already counted }
                observed.add(new MultiLabel(trueLabel));
            }

            //
            // True negatives everywhere else
            for (MultiLabel multilabel : domain.getDomain()) {
                Set<Label> labels = multilabel.getLabelSet();
                for (Label label : labels) {
                    if (!trueSet.contains(label) && !predSet.contains(label)) {
                        int ix = domain.getID(new MultiLabel(label));
                        mcm[ix].add(0, 0, 1d);
                    }
                }
            }
            predIndex++;
        }

        return new ConfusionMatrixTuple(mcm, confusion, observed);
    }
@Test
    public void testTabulateMultiLabel() {
        MultiLabel a = label("a");
        MultiLabel b = label("b");
        MultiLabel c = label("c");
        List<Prediction<MultiLabel>> predictions = Arrays.asList(
                mkPrediction(label("a"), label("a", "b")),
                mkPrediction(label("c", "b"), label("b")),
                mkPrediction(label("b"), label("b")),
                mkPrediction(label("b"), label("c"))
        );

        ImmutableOutputInfo<MultiLabel> domain = mkDomain(predictions);
        assertEquals(3, domain.size());

        DenseMatrix[] mcm = MultiLabelConfusionMatrix
                .tabulate(domain, predictions)
                .getMCM();

        int aIndex = domain.getID(a);
        int bIndex = domain.getID(b);
        int cIndex = domain.getID(c);

        assertEquals(domain.size(), mcm.length);
        assertEquals(3d, mcm[aIndex].get(0, 0));
        assertEquals(1d, mcm[aIndex].get(1, 1));

        assertEquals(1d, mcm[bIndex].get(0, 1));
        assertEquals(1d, mcm[bIndex].get(1, 0));
        assertEquals(2d, mcm[bIndex].get(1, 1));

        assertEquals(2d, mcm[cIndex].get(0, 0));
        assertEquals(1d, mcm[cIndex].get(0, 1));
        assertEquals(1d, mcm[cIndex].get(1, 0));
    }