Source: discreteARFF.js

/*
 * @fileoverview Clase para discretizar todos los atributos numéricos y modificar los datos 
 * correspondientes de un conjunto de datos ARFF almacenados en una instancia de ARFFDataSet.
 * Genera una copia de la instancia ARFFDataSet de entrada por lo que no modifica la instancia original.
 *
 * Generado como parte del proyecto CAIM+BAYES+CV K-FOLDS.
 * 
 * @author Fernando MM
 * @version 1.0
 * @date 2024-04-10
 * 
 * Dependencias:
 * - arffDataSet.js
 * - caimDiscretizer.js
 * 
 * Historial de cambios:
 * - 1.0 (2024-04-10): Creación de la librería.
 * 
 */

/**
 * @class La clase DiscreteARFF discretiza los datos no discretizados del archivo ARFF original basádose en los rangos calculados con el algoritmo CAIM
 */
class DiscreteARFF {
    constructor(arffData) {
        this.arffData = arffData;
    }

    /**
     * @description Genera un nuevo archivo ARFF con los nombres de los nuevos intervalos de los valores discretizados al crear la instancia del discretizador correspondiente a cada atributo
     * @returns {Blob} Nuevo ARFF discretizado
     */
    discretize() {
        let discretizedData = new ARFFDataSet();

        // Copia los datos de los atributos a la nueva instancia ARFFDataSet
        discretizedData.attributes = this.arffData.attributes.map(attr => ({
                ...attr,
                type: (attr.type === 'REAL' || attr.type === 'NUMERIC' || attr.type === 'INTEGER') ? 'NOMINAL' : attr.type,
                values: (attr.type === 'REAL' || attr.type === 'NUMERIC' || attr.type === 'INTEGER') ? [] : attr.values
            }));
        discretizedData.data = this.arffData.data.map(row => [...row]);
        discretizedData.labels = [...this.arffData.labels];

        // Para cada columna (atributo) ...
        this.arffData.attributes.forEach((attr, index) => {
            if (attr.type === 'REAL' || attr.type === 'NUMERIC' || attr.type === 'INTEGER') {

                // Crear la instancia del discretizador para cada atributo
                let attributeData = this.arffData.data.map(row => parseFloat(row[index]));
                let discretizer = new CAIMDiscretizer(attributeData, this.arffData.labels);
                let cuts = discretizer.discretize();

                // Genera los nombres de intervalos y almacena los cortes en la estructura de los atributos
                let intervalNames = this.generateIntervalNames(cuts);
                discretizedData.attributes[index].values = intervalNames;
                discretizedData.attributes[index].cuts = cuts;

                // Actualizar los datos con los nombres de intervalo
                discretizedData.data.forEach(row => {
                    let value = row[index];
                    let intervalIndex = this.findIntervalIndex(cuts, value);
                    row[index] = intervalNames[intervalIndex];
                });
            }
        });

        return discretizedData;
    }


    /**
     * 
     * @param {Array} cuts Arreglo de cortes que se realizarán a los datos para crear los nuevos intervalos según CAIM
     * @returns {string} Lista de nombres de los intervalos
     */
    generateIntervalNames(cuts) {
        let names = [];
        // Agregar el intervalo para valores menores que el primer corte
        names.push(`x < ${cuts[0].toFixed(2)}`);

        // Generar intervalos intermedios
        for (let i = 0; i < cuts.length - 1; i++) {
            const lowerBound = cuts[i];
            const upperBound = cuts[i + 1];

            if (i === cuts.length - 2) {
                names.push(`${lowerBound.toFixed(2)} <= x <= ${upperBound.toFixed(2)}`);
            } else {
                names.push(`${lowerBound.toFixed(2)} <= x < ${upperBound.toFixed(2)}`);
            }
        }

        // Agregar el intervalo para valores mayores que el último corte
        names.push(`x > ${cuts[cuts.length - 1].toFixed(2)}`);

        return names;
    }

    /**
     * 
     * @param {Array} cuts Arreglo de cortes que se realizarán a los datos para crear los nuevos intervalos según CAIM
     * @param {Number/String} value Valores de los atributos a discretizar
     * @returns {number} La cantidad de cortes realizados
     */
    findIntervalIndex(cuts, value) {
        // Los valores podrían ser strings, deben convertirse a float
        value = parseFloat(value);

        // Si el valor es menor al índice, regresa el índice
        for (let i = 0; i < cuts.length; i++) {
            if (value < cuts[i]) {
                return i;
            }
        }

        // Si no, se asume que el valor es mayor que el máximo.
        return cuts.length;
    }
}

/* EJEMPLO JAVASCRIPT
var arffDataSet = new ARFFDataSet();
arffDataSet.readARFF(file, function (attributes, data, labels) {
    let discretizer = new DiscreteARFF(arffDataSet);
    let discretizedData = discretizer.discretize();

    processData(discretizedData);
});
*/