private String applyObjective(String expression) {
        if (objective.startsWith("binary") || objective.equals("cross_entropy")) {
            return "sigmoid(" + expression + ")";
        }
        if (objective.equals("poisson") || objective.equals("gamma") || objective.equals("tweedie")) {
            return "exp(" + expression + ")";
        }
        return expression;  // else: use expression directly
    }
As primary method, we try to determine which are categorical features from the "features_infos" structure
 and then later use the "pandas_categorical" structure to determine the categoryValues.
 If we can't find the categorical features in the "features_infos" structure (models exported without
 "categorical_feature" parameter, or older versions of lightgbm), we fall back to traversing the
 decision tree looking for categorical decisions ('==') and use that to determine which features are categorical.
 Due to lack of documentation, we can't _know_ that the "feature_infos" and "pandas_categorical" will always
 be of the same length and in same order, but we have tried to verify this and not managed to find a case where they are not.
 And it is at least a lot better than the fallback, where we know some features can be
 treated incorrectly.
model-integration/src/main/java/ai/vespa/rankingexpression/importer/lightgbm/LightGBMParser.java