Le meilleur moyen de générer une séquence List <Double> de valeurs étant donné le début, la fin et l'étape?

14

Je suis en fait très surpris de ne pas avoir pu trouver la réponse à cela ici, bien que j'utilise peut-être simplement les mauvais termes de recherche ou quelque chose. Le plus proche que j'ai pu trouver est celui-ci , mais ils demandent de générer une plage spécifique de doubles avec une taille de pas spécifique, et les réponses le traitent comme tel. J'ai besoin de quelque chose qui générera les nombres avec une taille de début, de fin et de pas arbitraire.

Je pense qu'il doit déjà y avoir une méthode comme celle-ci dans une bibliothèque, mais si c'est le cas, je n'ai pas pu la trouver facilement (encore une fois, j'utilise peut-être les mauvais termes de recherche ou quelque chose). Voici donc ce que j'ai préparé par moi-même au cours des dernières minutes pour le faire:

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);
    }

}

Ces méthodes exécutent une boucle simple multipliant le steppar l'index de séquence et ajoutant au startdécalage. Cela atténue les erreurs de virgule flottante qui se produiraient avec une incrémentation continue (comme l'ajout de la stepà une variable à chaque itération).

J'ai ajouté la generateSequenceRoundedméthode pour les cas où une taille de pas fractionnaire peut provoquer des erreurs notables en virgule flottante. Cela nécessite un peu plus d'arithmétique, donc dans des situations extrêmement sensibles aux performances comme la nôtre, il est agréable d'avoir la possibilité d'utiliser la méthode plus simple lorsque l'arrondi n'est pas nécessaire. Je soupçonne que dans la plupart des cas d'utilisation générale, les frais généraux d'arrondi seraient négligeables.

Notez que je la logique intentionnellement exclue pour le traitement des arguments « anormaux » tels que Infinity, NaN, start> endou négatif steptaille pour la simplicité et le désir de se concentrer sur la question à portée de main.

Voici un exemple d'utilisation et la sortie correspondante:

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-t-il une bibliothèque existante qui fournit déjà ce type de fonctionnalité?

Sinon, y a-t-il des problèmes avec mon approche?

Quelqu'un at-il une meilleure approche à ce sujet?

NanoWizard
la source

Réponses:

17

Les séquences peuvent être facilement générées à l'aide de l'API Java 11 Stream.

L'approche simple consiste à utiliser 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());
}

Sur les plages avec un grand nombre d'itérations, doubleune erreur de précision peut s'accumuler, entraînant une erreur plus importante plus près de la fin de la plage. L'erreur peut être minimisée en passant à IntStreamet en utilisant des entiers et un multiplicateur double simple:

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());
}

Pour se débarrasser d'une doubleerreur de précision, vous BigDecimalpouvez utiliser:

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());
}

Exemples:

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]
}

La méthode itérative avec cette signature (3 paramètres) a été ajoutée dans Java 9. Ainsi, pour Java 8, le code ressemble à

DoubleStream.iterate(start, d -> d + step)
    .limit((int) (1 + (end - start) / step))
Evgeniy Khyst
la source
C'est une meilleure approche.
Vishwa Ratna,
Je vois plusieurs erreurs de compilation (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. Erreurs similaires pour IntStream.iterateet Stream.iterate. Aussi non-static method doubleValue() cannot be referenced from a static context.
NanoWizard
1
La réponse contient du code Java 11
Evgeniy Khyst
@NanoWizard a étendu la réponse avec un exemple pour Java 8
Evgeniy Khyst
L'itérateur à trois arguments a été ajouté dans Java 9
Thorbjørn Ravn Andersen
3

Personnellement , je raccourcirais un peu la classe DoubleSequenceGenerator pour d'autres goodies et n'utiliserais qu'une seule méthode de générateur de séquence qui contient l'option d'utiliser la précision souhaitée souhaitée ou de n'utiliser aucune précision du tout:

Dans la méthode de générateur ci-dessous, si rien (ou toute valeur inférieure à 0) n'est fourni au paramètre optionnel setPrecision , aucun arrondi de précision décimale n'est effectué. Si 0 est fourni pour une valeur de précision, les nombres sont arrondis à leur nombre entier le plus proche (par exemple: 89,674 est arrondi à 90,0). Si une valeur de précision spécifique supérieure à 0 est fournie, les valeurs sont converties en cette précision décimale.

BigDecimal est utilisé ici pour ... eh bien ... précision:

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 ....
}

Et dans 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));

Et la console affiche:

[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
la source
Des idées intéressantes, bien que je vois quelques problèmes. 1. En ajoutant à valchaque itération, vous obtenez une perte de précision additive. Pour de très grandes séquences, l'erreur sur les derniers nombres pourrait être significative. 2. Les appels répétés vers BigDecimal.valueOf()sont relativement coûteux. Vous obtiendrez de meilleures performances (et précision) en convertissant les entrées en BigDecimals et en utilisant BigDecimalfor val. En fait, en utilisant un doublefor val, vous n'obtenez vraiment aucun avantage de précision, BigDecimalsauf peut-être avec l'arrondi.
NanoWizard
2

Essaye ça.

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());
}

Ici,

int java.math.BigDecimal.scale()

Renvoie l'échelle de ce BigDecimal. Si zéro ou positif, l'échelle est le nombre de chiffres à droite de la virgule décimale. S'il est négatif, la valeur non mise à l'échelle du nombre est multipliée par dix à la puissance de la négation de l'échelle. Par exemple, une échelle de -3 signifie que la valeur non mise à l'échelle est multipliée par 1000.

En principal ()

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

Et sortie:

[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
la source
2
  1. Existe-t-il une bibliothèque existante qui fournit déjà ce type de fonctionnalité?

    Désolé, je ne sais pas, mais à en juger par les autres réponses et leur relative simplicité - non, il n'y en a pas. Ce n'est pas nécessaire. Enfin, presque ...

  2. Sinon, y a-t-il des problèmes avec mon approche?

    Oui et non. Vous avez au moins un bogue et une certaine marge d'amélioration des performances, mais l'approche elle-même est correcte.

    1. Votre bug: erreur d'arrondi (il suffit de passer while (mult*fraction < 1.0)à while (mult*fraction < 10.0)et cela devrait le corriger)
    2. Tous les autres n'atteignent pas le end... eh bien, peut-être qu'ils n'étaient tout simplement pas assez observateurs pour lire les commentaires dans votre code
    3. Tous les autres sont plus lents.
    4. Le simple changement de condition dans la boucle principale de int < Doubleà int < intaugmentera sensiblement la vitesse de votre code
  3. Quelqu'un at-il une meilleure approche à ce sujet?

    Hmm ... De quelle manière?

    1. Simplicité? generateSequenceDoubleStreamde @Evgeniy Khyst semble assez simple. Et devrait être utilisé ... mais peut-être pas, à cause des deux points suivants
    2. Précis? generateSequenceDoubleStreamn'est pas! Mais peut encore être enregistré avec le motif start + step*i. Et le start + step*imotif est précis. Seule BigDoubleet l'arithmétique à virgule fixe peut le battre. Mais BigDoubleles opérations sont lentes et l'arithmétique manuelle à virgule fixe est fastidieuse et peut être inappropriée pour vos données. Soit dit en passant, sur les questions de précision, vous pouvez vous divertir avec ceci: https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html
    3. La vitesse ... eh bien maintenant nous sommes sur un terrain fragile. Découvrez cette repl https://repl.it/repls/RespectfulSufficientWorker Je n'ai pas de banc d'essai décent en ce moment, j'ai donc utilisé repl.it ... qui est totalement inadéquat pour les tests de performances, mais ce n'est pas le point principal. Le fait est - il n'y a pas de réponse définitive. Sauf que peut-être dans votre cas, ce qui ne ressort pas totalement de votre question, vous ne devriez certainement pas utiliser BigDecimal (lire plus loin).

      J'ai essayé de jouer et d'optimiser pour les grosses entrées. Et votre code d'origine, avec quelques modifications mineures - le plus rapide. Mais peut-être avez-vous besoin d'énormes quantités de petits Lists? Cela peut alors être une histoire totalement différente.

      Ce code est assez simple à mon goût, et assez rapide:

        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 vous préférez une manière plus élégante (ou nous devrions l'appeler idiomatique), je suggère personnellement:

    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());
    }

    Quoi qu'il en soit, les améliorations possibles des performances sont les suivantes:

    1. Essayez de passer de Doubleà double, et si vous en avez vraiment besoin, vous pouvez revenir en arrière, à en juger par les tests, cela peut encore être plus rapide. (Mais ne vous fiez pas à moi, essayez-le vous-même avec vos données dans votre environnement. Comme je l'ai dit - repl.it craint pour les repères)
    2. Un peu de magie: boucle séparée pour Math.round()... peut-être que cela a quelque chose à voir avec la localisation des données. Je ne le recommande pas - le résultat est très instable. Mais c'est amusant.

      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. Vous devriez certainement considérer être plus paresseux et générer des nombres à la demande sans les stocker ensuite dans Lists

  4. Je soupçonne que dans la plupart des cas d'utilisation générale, les frais généraux d'arrondi seraient négligeables.

    Si vous suspectez quelque chose - testez-le :-) Ma réponse est "Oui", mais encore une fois ... ne me croyez pas. Essaye-le.

Donc, revenons à la question principale: existe-t-il une meilleure façon?
Oui bien sûr!
Mais ça dépend.

  1. Choisissez Big Decimal si vous avez besoin de très grands nombres et de très petits nombres. Mais si vous les réinjectez Double, et plus encore, utilisez-les avec des nombres de magnitude "proche" - pas besoin d'eux! Vérifiez la même réponse: https://repl.it/repls/RespectfulSufficientWorker - le dernier test montre qu'il n'y aura pas de différence de résultats , mais une perte de vitesse.
  2. Faites des micro-optimisations en fonction de vos propriétés de données, de votre tâche et de votre environnement.
  3. Préférez un code court et simple s'il n'y a pas grand-chose à gagner d'une amélioration des performances de 5 à 10%. Ne perdez pas votre temps
  4. Utilisez peut-être l'arithmétique à virgule fixe si vous le pouvez et si cela en vaut la peine.

A part ça, tu vas bien.

PS . Il y a aussi une implémentation Kahan Summation Formula dans la repl ... juste pour le plaisir. https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html#1346 et cela fonctionne - vous pouvez atténuer les erreurs de sommation

x00
la source