¿La mejor manera de generar una secuencia de Lista <Double> de valores dados inicio, final y paso?

14

De hecho, estoy muy sorprendido de que no haya podido encontrar la respuesta a esto aquí, aunque tal vez solo estoy usando los términos de búsqueda incorrectos o algo así. Lo más cercano que pude encontrar es esto , pero preguntan sobre la generación de un rango específico de doubles con un tamaño de paso específico, y las respuestas lo tratan como tal. Necesito algo que genere los números con inicio arbitrario, final y tamaño de paso.

Calculo que hay tiene que haber algún método como este en una biblioteca en algún lugar ya, pero si es así yo no era capaz de encontrar fácilmente (de nuevo, tal vez sólo estoy usando los términos de búsqueda mal o algo). Así que esto es lo que he cocinado solo en los últimos minutos para hacer esto:

import java.lang.Math;
import java.util.List;
import java.util.ArrayList;

public class DoubleSequenceGenerator {


     /**
     * Generates a List of Double values beginning with `start` and ending with
     * the last step from `start` which includes the provided `end` value.
     **/
    public static List<Double> generateSequence(double start, double end, double step) {
        Double numValues = (end-start)/step + 1.0;
        List<Double> sequence = new ArrayList<Double>(numValues.intValue());

        sequence.add(start);
        for (int i=1; i < numValues; i++) {
          sequence.add(start + step*i);
        }

        return sequence;
    }

    /**
     * Generates a List of Double values beginning with `start` and ending with
     * the last step from `start` which includes the provided `end` value.
     * 
     * Each number in the sequence is rounded to the precision of the `step`
     * value. For instance, if step=0.025, values will round to the nearest
     * thousandth value (0.001).
     **/
    public static List<Double> generateSequenceRounded(double start, double end, double step) {

        if (step != Math.floor(step)) {
            Double numValues = (end-start)/step + 1.0;
            List<Double> sequence = new ArrayList<Double>(numValues.intValue());

            double fraction = step - Math.floor(step);
            double mult = 10;
            while (mult*fraction < 1.0) {
                mult *= 10;
            }

            sequence.add(start);
            for (int i=1; i < numValues; i++) {
              sequence.add(Math.round(mult*(start + step*i))/mult);
            }

            return sequence;
        }

        return generateSequence(start, end, step);
    }

}

Estos métodos ejecutan un bucle simple multiplicando el steppor el índice de secuencia y agregando al startdesplazamiento. Esto mitiga los errores de coma flotante compuestos que ocurrirían con un incremento continuo (como agregar el stepa una variable en cada iteración).

Agregué el generateSequenceRoundedmétodo para aquellos casos en que un tamaño de paso fraccional puede causar errores notables de punto flotante. Requiere un poco más de aritmética, por lo que en situaciones extremadamente sensibles al rendimiento como la nuestra, es bueno tener la opción de usar el método más simple cuando el redondeo es innecesario. Sospecho que en la mayoría de los casos de uso general, la sobrecarga de redondeo sería insignificante.

Tenga en cuenta que Excluí intencionadamente lógica para el manejo de argumentos "anormales", tales como Infinity, NaN, start> end, o un negativo steptamaño de la simplicidad y el deseo de centrarse en la cuestión que nos ocupa.

Aquí hay un ejemplo de uso y salida correspondiente:

System.out.println(DoubleSequenceGenerator.generateSequence(0.0, 2.0, 0.2))
System.out.println(DoubleSequenceGenerator.generateSequenceRounded(0.0, 2.0, 0.2));
System.out.println(DoubleSequenceGenerator.generateSequence(0.0, 102.0, 10.2));
System.out.println(DoubleSequenceGenerator.generateSequenceRounded(0.0, 102.0, 10.2));
[0.0, 0.2, 0.4, 0.6000000000000001, 0.8, 1.0, 1.2000000000000002, 1.4000000000000001, 1.6, 1.8, 2.0]
[0.0, 0.2, 0.4, 0.6, 0.8, 1.0, 1.2, 1.4, 1.6, 1.8, 2.0]
[0.0, 10.2, 20.4, 30.599999999999998, 40.8, 51.0, 61.199999999999996, 71.39999999999999, 81.6, 91.8, 102.0]
[0.0, 10.2, 20.4, 30.6, 40.8, 51.0, 61.2, 71.4, 81.6, 91.8, 102.0]

¿Existe ya una biblioteca que brinde este tipo de funcionalidad?

Si no, ¿hay algún problema con mi enfoque?

¿Alguien tiene un mejor enfoque para esto?

NanoWizard
fuente

Respuestas:

17

Las secuencias se pueden generar fácilmente utilizando Java 11 Stream API.

El enfoque directo es usar DoubleStream:

public static List<Double> generateSequenceDoubleStream(double start, double end, double step) {
  return DoubleStream.iterate(start, d -> d <= end, d -> d + step)
      .boxed()
      .collect(toList());
}

En rangos con un gran número de iteraciones, doublese puede acumular un error de precisión que da como resultado un error mayor cerca del final del rango. El error se puede minimizar cambiando IntStreamy utilizando enteros y un único multiplicador doble:

public static List<Double> generateSequenceIntStream(int start, int end, int step, double multiplier) {
  return IntStream.iterate(start, i -> i <= end, i -> i + step)
      .mapToDouble(i -> i * multiplier)
      .boxed()
      .collect(toList());
}

Para deshacerse de un doubleerror de precisión, BigDecimalse puede utilizar:

public static List<Double> generateSequenceBigDecimal(BigDecimal start, BigDecimal end, BigDecimal step) {
  return Stream.iterate(start, d -> d.compareTo(end) <= 0, d -> d.add(step))
      .mapToDouble(BigDecimal::doubleValue)
      .boxed()
      .collect(toList());
}

Ejemplos:

public static void main(String[] args) {
  System.out.println(generateSequenceDoubleStream(0.0, 2.0, 0.2));
  //[0.0, 0.2, 0.4, 0.6000000000000001, 0.8, 1.0, 1.2, 1.4, 1.5999999999999999, 1.7999999999999998, 1.9999999999999998]

  System.out.println(generateSequenceIntStream(0, 20, 2, 0.1));
  //[0.0, 0.2, 0.4, 0.6000000000000001, 0.8, 1.0, 1.2000000000000002, 1.4000000000000001, 1.6, 1.8, 2.0]

  System.out.println(generateSequenceBigDecimal(new BigDecimal("0"), new BigDecimal("2"), new BigDecimal("0.2")));
  //[0.0, 0.2, 0.4, 0.6, 0.8, 1.0, 1.2, 1.4, 1.6, 1.8, 2.0]
}

El método iterar con esta firma (3 parámetros) se agregó en Java 9. Entonces, para Java 8, el código se ve así

DoubleStream.iterate(start, d -> d + step)
    .limit((int) (1 + (end - start) / step))
Evgeniy Khyst
fuente
Este es un mejor enfoque.
Vishwa Ratna
Estoy viendo varios errores de compilación (JDK 1.8.0): error: method iterate in interface DoubleStream cannot be applied to given types; return DoubleStream.iterate(start, d -> d <= end, d -> d + step) required: double,DoubleUnaryOperator. found: double,(d)->d <= end,(d)->d + step. reason: actual and formal argument lists differ in length. Errores similares para IntStream.iteratey Stream.iterate. Además, non-static method doubleValue() cannot be referenced from a static context.
NanoWizard
1
La respuesta contiene código Java 11
Evgeniy Khyst
@NanoWizard amplió la respuesta con una muestra para Java 8
Evgeniy Khyst
El iterador de tres argumentos se agregó en Java 9
Thorbjørn Ravn Andersen el
3

Yo personalmente, acortaría un poco la clase DoubleSequenceGenerator para otras cosas y usaría solo un método generador de secuencia que contiene la opción de utilizar la precisión deseada o no utilizar ninguna precisión:

En el siguiente método del generador, si no se proporciona nada (o cualquier valor menor que 0) al parámetro opcional setPrecision , entonces no se lleva a cabo el redondeo de precisión decimal. Si se proporciona 0 para un valor de precisión, los números se redondean a su número entero más cercano (es decir: 89.674 se redondea a 90.0). Si se proporciona un valor de precisión específico mayor que 0 , los valores se convierten a esa precisión decimal.

BigDecimal se usa aquí para ... bueno ... precisión:

import java.util.List;
import java.util.ArrayList;
import java.math.BigDecimal;
import java.math.RoundingMode;

public class DoubleSequenceGenerator {

     public static List<Double> generateSequence(double start, double end, 
                                          double step, int... setPrecision) {
        int precision = -1;
        if (setPrecision.length > 0) {
            precision = setPrecision[0];
        }
        List<Double> sequence = new ArrayList<>();
        for (double val = start; val < end; val+= step) {
            if (precision > -1) {
                sequence.add(BigDecimal.valueOf(val).setScale(precision, RoundingMode.HALF_UP).doubleValue());
            }
            else {
                sequence.add(BigDecimal.valueOf(val).doubleValue());
            }
        }
        if (sequence.get(sequence.size() - 1) < end) { 
            sequence.add(end); 
        }
        return sequence;
    }    

    // Other class goodies here ....
}

Y en main ():

System.out.println(generateSequence(0.0, 2.0, 0.2));
System.out.println(generateSequence(0.0, 2.0, 0.2, 0));
System.out.println(generateSequence(0.0, 2.0, 0.2, 1));
System.out.println();
System.out.println(generateSequence(0.0, 102.0, 10.2, 0));
System.out.println(generateSequence(0.0, 102.0, 10.2, 0));
System.out.println(generateSequence(0.0, 102.0, 10.2, 1));

Y la consola muestra:

[0.0, 0.2, 0.4, 0.6000000000000001, 0.8, 1.0, 1.2, 1.4, 1.5999999999999999, 1.7999999999999998, 1.9999999999999998, 2.0]
[0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 2.0, 2.0, 2.0]
[0.0, 0.2, 0.4, 0.6, 0.8, 1.0, 1.2, 1.4, 1.6, 1.8, 2.0]

[0.0, 10.2, 20.4, 30.599999999999998, 40.8, 51.0, 61.2, 71.4, 81.60000000000001, 91.80000000000001, 102.0]
[0.0, 10.0, 20.0, 31.0, 41.0, 51.0, 61.0, 71.0, 82.0, 92.0, 102.0]
[0.0, 10.2, 20.4, 30.6, 40.8, 51.0, 61.2, 71.4, 81.6, 91.8, 102.0]
DevilsHnd
fuente
Ideas interesantes, aunque veo algunos problemas. 1. Al agregar a valcada iteración, se obtiene una pérdida de precisión aditiva. Para secuencias muy grandes, el error en los últimos números podría ser significativo. 2. Las llamadas repetidas a BigDecimal.valueOf()son relativamente caras. Obtendrá un mejor rendimiento (y precisión) al convertir las entradas a BigDecimalsy usarlas BigDecimalpara val. De hecho, al usar un doublefor val, realmente no obtienes ningún beneficio de precisión al usarlo, BigDecimalexcepto quizás con el redondeo.
NanoWizard
2

Prueba esto.

public static List<Double> generateSequenceRounded(double start, double end, double step) {
    long mult = (long) Math.pow(10, BigDecimal.valueOf(step).scale());
    return DoubleStream.iterate(start, d -> (double) Math.round(mult * (d + step)) / mult)
                .limit((long) (1 + (end - start) / step)).boxed().collect(Collectors.toList());
}

Aquí,

int java.math.BigDecimal.scale()

Devuelve la escala de este BigDecimal. Si es cero o positivo, la escala es el número de dígitos a la derecha del punto decimal. Si es negativo, el valor sin escala del número se multiplica por diez a la potencia de la negación de la escala. Por ejemplo, una escala de -3 significa que el valor sin escala se multiplica por 1000.

En main ()

System.out.println(generateSequenceRounded(0.0, 102.0, 10.2));
System.out.println(generateSequenceRounded(0.0, 102.0, 10.24367));

Y salida:

[0.0, 10.2, 20.4, 30.6, 40.8, 51.0, 61.2, 71.4, 81.6, 91.8, 102.0]
[0.0, 10.24367, 20.48734, 30.73101, 40.97468, 51.21835, 61.46202, 71.70569, 81.94936, 92.19303]
Maddy
fuente
2
  1. ¿Existe ya una biblioteca que brinde este tipo de funcionalidad?

    Lo siento, no lo sé, pero a juzgar por otras respuestas y su relativa simplicidad, no, no la hay. No hay necesidad. Bueno, casi...

  2. Si no, ¿hay algún problema con mi enfoque?

    Si y no. Tiene al menos un error y algo de espacio para aumentar el rendimiento, pero el enfoque en sí es correcto.

    1. Su error: error de redondeo (sólo cambia while (mult*fraction < 1.0)a while (mult*fraction < 10.0)y que debe solucionarlo)
    2. Todos los demás no llegan a end... bueno, tal vez simplemente no fueron lo suficientemente observadores para leer los comentarios en su código
    3. Todos los demás son más lentos.
    4. Simplemente cambiando la condición en el bucle principal de int < Doublea int < intaumentará notablemente la velocidad de su código
  3. ¿Alguien tiene un mejor enfoque para esto?

    Hmm ... ¿De qué manera?

    1. ¿Sencillez? generateSequenceDoubleStreamde @Evgeniy Khyst parece bastante simple. Y debería usarse ... pero tal vez no, debido a los siguientes dos puntos
    2. ¿Preciso? generateSequenceDoubleStream¡no es! Pero aún se puede guardar con el patrón start + step*i. Y el start + step*ipatrón es preciso. Solo BigDoubley la aritmética de punto fijo puede vencerlo. Pero los BigDoubles son lentos, y la aritmética manual de punto fijo es tediosa y puede ser inapropiada para sus datos. Por cierto, en materia de precisión, puede entretenerse con esto: https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html
    3. Velocidad ... bueno, ahora estamos en terrenos inestables. Echa un vistazo a esta respuesta https://repl.it/repls/RespectfulSufficientWorker No tengo un banco de pruebas decente en este momento, así que utilicé repl.it ... que es totalmente inadecuado para las pruebas de rendimiento, pero no es el punto principal. El punto es que no hay una respuesta definitiva. Excepto que tal vez en su caso, que no está totalmente claro en su pregunta, definitivamente no debe usar BigDecimal (lea más).

      He intentado jugar y optimizar para grandes entradas. Y su código original, con algunos cambios menores, el más rápido. ¿Pero tal vez necesitas enormes cantidades de pequeños Lists? Entonces esa puede ser una historia totalmente diferente.

      Este código es bastante simple para mi gusto y lo suficientemente rápido:

        public static List<Double> genNoRoundDirectToDouble(double start, double end, double step) {
        int len = (int)Math.ceil((end-start)/step) + 1;
        var sequence = new ArrayList<Double>(len);
        sequence.add(start);
        for (int i=1 ; i < len ; ++i) sequence.add(start + step*i);
        return sequence;
        }

    Si prefiere una forma más elegante (o deberíamos llamarlo idiomático), personalmente, sugeriría:

    public static List<Double> gen_DoubleStream_presice(double start, double end, double step) {
        return IntStream.range(0, (int)Math.ceil((end-start)/step) + 1)
            .mapToDouble(i -> start + i * step)
            .boxed()
            .collect(Collectors.toList());
    }

    De todos modos, los posibles aumentos de rendimiento son:

    1. Intente cambiar de Doublea double, y si realmente los necesita, puede volver a cambiar, a juzgar por las pruebas, aún puede ser más rápido. (Pero no confíes en mi, pruébalo tú mismo con tus datos en tu entorno. Como dije, repl.it apesta a los puntos de referencia)
    2. Un poco de magia: bucle separado para Math.round()... tal vez tenga algo que ver con la localidad de datos. No lo recomiendo, el resultado es muy inestable. Pero es divertido.

      double[] sequence = new double[len];
      for (int i=1; i < len; ++i) sequence[i] = start + step*i;
      List<Double> list = new ArrayList<Double>(len);
      list.add(start);
      for (int i=1; i < len; ++i) list.add(Math.round(sequence[i])/mult);
      return list;
    3. Definitivamente debería considerar ser más vago y generar números a pedido sin almacenarlos en Lists

  4. Sospecho que en la mayoría de los casos de uso general, la sobrecarga de redondeo sería insignificante.

    Si sospecha algo, pruébelo :-) Mi respuesta es "Sí", pero de nuevo ... no me crea. Pruébalo.

Entonces, volviendo a la pregunta principal: ¿hay una mejor manera?
¡Sí, por supuesto!
Pero eso depende.

  1. Elija Big Decimal si necesita números muy grandes y números muy pequeños . Pero si los devuelve Double, y más que eso, úselo con números de magnitud "cercana", ¡no los necesita! Consulte la misma respuesta: https://repl.it/repls/RespectfulSufficientWorker : la última prueba muestra que no habrá diferencias en los resultados , sino una pérdida de velocidad en la excavación.
  2. Realice algunas microoptimizaciones basadas en sus propiedades de datos, su tarea y su entorno.
  3. Prefiere código corto y simple si no hay mucho que ganar con un aumento de rendimiento del 5-10%. No pierdas tu tiempo
  4. Tal vez use aritmética de punto fijo si puede y vale la pena.

Aparte de eso, estás bien.

PS . También hay una implementación de Kahan Summation Formula en la respuesta ... solo por diversión. https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html#1346 y funciona: puede mitigar los errores de suma

x00
fuente