web-dev-qa-db-fra.com

Quelle est une bonne solution pour calculer une moyenne lorsque la somme de toutes les valeurs dépasse les limites d'une double?

J'ai besoin de calculer la moyenne d'un très grand ensemble de doubles (valeurs 10 ^ 9). La somme des valeurs dépasse la limite supérieure d'un double, de même que quelqu'un connaît donc des petits astuces soignées pour calculer une moyenne qui ne nécessite pas également de calculer la somme?

J'utilise Java 1.5.

41
Simon

Vous pouvez calculer la moyenne itérative . Cet algorithme est simple, rapide, vous devez traiter chaque valeur qu'une seule fois et que les variables ne deviennent jamais plus grandes que la plus grande valeur de l'ensemble. Vous ne recevrez donc pas de débordement.

double mean(double[] ary) {
  double avg = 0;
  int t = 1;
  for (double x : ary) {
    avg += (x - avg) / t;
    ++t;
  }
  return avg;
}

À l'intérieur de la boucle avg est toujours la valeur moyenne de toutes les valeurs traitées jusqu'à présent. En d'autres termes, si toutes les valeurs sont finies, vous ne devez pas obtenir un débordement.

170
martinus

La toute première question que j'aimerais vous demander est la suivante:

  • Connaissez-vous le nombre de valeurs à l'avance?

Sinon, vous n'avez que peu de choix que de sommeillir et de compter et de diviser, de faire la moyenne. Si Double n'est pas une précision suffisamment élevée pour gérer cela, puis la chance, vous ne pouvez pas utiliser Double, vous devez trouver un type de données pouvant le gérer.

Si, d'autre part, vous do connaît le nombre de valeurs à l'avance, vous pouvez regarder ce que vous faites et changez vraiment Comment Vous le faites, mais gardez le résultat global.

La moyenne des n valeurs, stockée dans une collection A, est la suivante:

A[0]   A[1]   A[2]   A[3]          A[N-1]   A[N]
---- + ---- + ---- + ---- + .... + ------ + ----
 N      N      N      N               N       N

Pour calculer les sous-ensembles de ce résultat, vous pouvez diviser le calcul en ensembles de taille égale. Vous pouvez donc le faire, pour les ensembles à 3 évaluations (en supposant que le nombre de valeurs est divisable par 3, sinon vous avez besoin d'un diviseur différent).

/ A[0]   A[1]   A[2] \   / A[3]   A[4]   A[5] \   //      A[N-1]   A[N] \
| ---- + ---- + ---- |   | ---- + ---- + ---- |   \\    + ------ + ---- |
\  3      3      3   /   \  3      3      3   /   //        3       3   /
 --------------------- +  --------------------  + \\      --------------
          N                        N                        N
         ---                      ---                      ---
          3                        3                        3

Notez que vous avez besoin ensembles de taille égale, sinon les numéros dans le dernier ensemble, qui n'auront pas suffisamment de valeurs par rapport à tous les ensembles avant de pouvoir, aura un impact plus élevé sur le résultat final.

Considérez les chiffres 1-7 en séquence, si vous choisissez une taille de 3, vous obtiendrez ce résultat:

/ 1   2   3 \   / 4   5   6 \   / 7 \ 
| - + - + - | + | - + - + - | + | - |
\ 3   3   3 /   \ 3   3   3 /   \ 3 /
 -----------     -----------     ---
      y               y           y

qui donne:

     2   5   7/3
     - + - + ---
     y   y    y

Si y est 3 pour tous les ensembles, vous obtenez ceci:

     2   5   7/3
     - + - + ---
     3   3    3

qui donne:

2*3   5*3    7
--- + --- + ---
 9     9     9

lequel est:

6   15   7
- + -- + -
9    9   9

quel total:

28
-- ~ 3,1111111111111111111111.........1111111.........
 9

La moyenne de 1-7 est 4. Évidemment, cela ne fonctionnera pas. Notez que si vous faites l'exercice ci-dessus avec les numéros 1, 2, 3, 4, 5, 6, 7, 0, 0 (notez les deux zéros à la fin là-bas), vous obtiendrez ainsi le résultat ci-dessus.

En d'autres termes, si vous ne pouvez pas diviser le nombre de valeurs dans des ensembles de taille égale, le dernier ensemble sera compté comme s'il a le même nombre de valeurs que tous les ensembles qui le précèdent, mais il sera rembourré avec des zéros pour toutes les valeurs manquantes.

Ainsi, vous avez besoin de jeux de taille égale. Travaille de chance si votre jeu d'entrée d'origine consiste en un nombre élevé de valeurs.

Ce que je suis inquiet ici, c'est une perte de précision. Je ne suis pas tout à fait sûr Double _ vous donnera une bonne précision dans un tel cas, si cela ne peut initialement pas contenir la totalité de la somme des valeurs.

13
Lasse V. Karlsen

IMHO, la manière la plus robuste de résoudre votre problème est

  1. trier votre ensemble
  2. divisé en groupes d'éléments dont la somme n'écoulerait pas - car elles sont triées, c'est rapide et facile
  3. faire la somme dans chaque groupe - et diviser par la taille du groupe
  4. faire la somme de la somme du groupe (éventuellement appeler ce même algorithme récursivement) - sachez que si les groupes ne seront pas de taille égale, vous devrez les pondérer de leur taille.

Une bonne chose de cette approche est que cela échoue bien si vous avez un très grand nombre d'éléments à résumer - et un grand nombre de processeurs/machines à utiliser pour faire les mathématiques

12
Davide

Outre l'utilisation des meilleures approches déjà suggérées, vous pouvez utiliser BigDecimal pour effectuer vos calculs. (Gardez à l'esprit qu'il est immuable)

11
Bozho

Veuillez clarifier les plages potentielles des valeurs.

Étant donné qu'un double a une plage ~ = +/- 10 ^ 308, et que vous sommez 10 ^ 9 valeurs, la plage apparente suggérée dans votre question est des valeurs de l'ordre de 10 ^ 299.

Cela semble quelque peu, bien, peu probable ...

Si vos valeurs vraiment sont ce grand, puis avec un double normal, vous n'avez que 17 chiffres décimaux significatifs pour jouer avec vous, vous jetez donc environ 280 chiffres d'informations avant que vous puissiez même Pensez à la moyenne des valeurs.

Je noterais également (puisque personne d'autre n'a) que pour tout ensemble de chiffres X:

mean(X) = sum(X[i] - c)  +  c
          -------------
                N

pour toute constante arbitraire c.

Dans ce problème particulier, réglage c = min(X) -peut-être réduire considérablement le risque de débordement pendant la somme.

Puis-je suggérer humblement que la déclaration de problème est incomplète ...?

10
Alnitak

Un double peut être divisé par une puissance de 2 sans perte de précision. Donc, si votre seul problème si la taille absolue de la somme, vous pouvez préciser vos chiffres avant de les résumer. Mais avec un ensemble de données de cette taille, il reste encore le risque que vous frappiez une situation où vous ajoutez de petits nombres à un grand nombre, et les petits chiffres finiront par être principalement ignorés (ou complètement) ignorés.

par exemple, lorsque vous ajoutez 2.2e-20 à 9.0E20, le résultat est de 9.0E20 car une fois que les échelles sont ajustées de sorte qu'elles peuvent être ajoutées ensemble, le nombre plus petit est de 0. Doubles ne peut contenir que 17 chiffres et vous Besoin de plus de 40 chiffres pour ajouter ces deux nombres ensemble sans perte.

Donc, en fonction de votre ensemble de données et du nombre de chiffres de précision, vous pouvez vous permettre de perdre autre chose. Casser les données en ensembles aidera, mais un meilleur moyen de préserver la précision pourrait être de déterminer une moyenne approximative (vous pouvez déjà savoir ce numéro). Puis soustrayez chaque valeur de la moyenne approximative avant de la résumer. De cette façon, vous résumez les distances de la moyenne, votre somme ne devrait donc jamais devenir très grande.

Ensuite, vous prenez le delta moyen et ajoutez-le à votre somme difficile pour obtenir la moyenne correcte. Garder une trace du Delta Min et Max vous indiquera également la quantité de précision que vous avez perdue pendant le processus de sommation. Si vous avez beaucoup de temps et avez besoin d'un résultat très précis, vous pouvez itérer.

6
John Knoeller

Vous pouvez prendre la moyenne des moyennes de sous-ensembles de taille égale de nombres qui ne dépassent pas la limite.

6
David M

divisez toutes les valeurs par la taille de jeu, puis résumez-le

5
Alon

L'option 1 est d'utiliser une bibliothèque de précision arbitraire afin de ne pas avoir de limite supérieure.

D'autres options (qui perdent la précision) sont de résumer en groupes plutôt que de tous à la fois, ou de diviser avant la somme.

5
Anon.

Donc, je ne me répète tant, laissez-moi dire que je pars du principe que la liste des numéros est normalement distribué, et que vous pouvez résumer beaucoup de chiffres avant de trop-plein. La technique fonctionne toujours pour distros non normaux, mais somethings ne répondra pas aux attentes que je décris ci-dessous.

-

Résumez une sous-série, en gardant une trace de combien de chiffres que vous mangez, jusqu'à ce que vous approchez le trop-plein, puis prendre la moyenne. Cela vous donnera une a0 moyenne, et compter n0. Répétez jusqu'à ce que vous avez épuisé la liste. Maintenant, vous devriez avoir beaucoup ai, ni.

Chaque ai et ni devrait être relativement proche, à l'exception possible de la dernière bouchée de la liste. Vous pouvez atténuer ce par le sous-mordre à la fin de la liste.

Vous pouvez combiner un sous-ensemble de ces ai, ni en choisissant une ni dans le sous-ensemble (appeler np) et en divisant toutes les ni dans le sous-ensemble de cette valeur. La taille maximale des sous-ensembles de combiner la valeur est à peu près constante de la n de.

Le ni/np devrait être proche de l'un. Maintenant somme ni/np * ai et multiple par np/(somme ni), garder une trace de somme ni. Cela vous donne une nouvelle ni, ma combinaison, si vous avez besoin de répéter la procédure.

Si vous devez répéter (à savoir le nombre d'ai, paires ni est beaucoup plus grande que ni typique), essayez de garder la constante de n par rapport en combinant toutes les moyennes à un n premier niveau, puis en combinant au niveau suivant, etc.

3
Carl

Un échantillonnage aléatoire d'un petit ensemble du jeu de données complet entraînera souvent une solution "assez bonne". Vous devez évidemment faire cette détermination vous-même basée sur les exigences du système. La taille de l'échantillon peut être remarquablement petite et obtient toujours des réponses raisonnablement bonnes. Cela peut être calculé de manière appropriée en calculant la moyenne d'un nombre croissant d'échantillons choisis au hasard - la moyenne convergera dans un certain intervalle.

L'échantillonnage traite non seulement de l'inquiétude à double débordement, mais est beaucoup plus rapide. Non applicable à tous les problèmes, mais certainement utile pour de nombreux problèmes.

2
Kevin Day

Considère ceci:

avg(n1)         : n1                               = a1
avg(n1, n2)     : ((1/2)*n1)+((1/2)*n2)            = ((1/2)*a1)+((1/2)*n2) = a2
avg(n1, n2, n3) : ((1/3)*n1)+((1/3)*n2)+((1/3)*n3) = ((2/3)*a2)+((1/3)*n3) = a3

Donc, pour tout ensemble de doubles de taille arbitraire, vous pourriez le faire (ceci est en C #, mais je suis sûr que cela pourrait être facilement traduit en Java):

static double GetAverage(IEnumerable<double> values) {
    int i = 0;
    double avg = 0.0;
    foreach (double value in values) {
        avg = (((double)i / (double)(i + 1)) * avg) + ((1.0 / (double)(i + 1)) * value);
        i++;
    }

    return avg;
}

En fait, cela simplifie bien (déjà fourni par Martinus):

static double GetAverage(IEnumerable<double> values) {
    int i = 1;
    double avg = 0.0;
    foreach (double value in values) {
        avg += (value - avg) / (i++);
    }

    return avg;
}

J'ai écrit un test rapide pour essayer cette fonction contre la méthode plus conventionnelle de résumant les valeurs et de la division par le compte (GetAverage_old). Pour mon entrée, j'ai écrit cette fonction rapide pour revenir autant de doubles positifs aléatoires que vous le souhaitez:

static IEnumerable<double> GetRandomDoubles(long numValues, double maxValue, int seed) {
    Random r = new Random(seed);
    for (long i = 0L; i < numValues; i++)
        yield return r.NextDouble() * maxValue;

    yield break;
}

Et voici les résultats de quelques essais:

long N = 100L;
double max = double.MaxValue * 0.01;

IEnumerable<double> doubles = GetRandomDoubles(N, max, 0);
double oldWay = GetAverage_old(doubles); // 1.00535024998431E+306
double newWay = GetAverage(doubles); // 1.00535024998431E+306

doubles = GetRandomDoubles(N, max, 1);
oldWay = GetAverage_old(doubles); // 8.75142021696299E+305
newWay = GetAverage(doubles); // 8.75142021696299E+305

doubles = GetRandomDoubles(N, max, 2);
oldWay = GetAverage_old(doubles); // 8.70772312848651E+305
newWay = GetAverage(doubles); // 8.70772312848651E+305

OK, mais qu'en est-il de 10 ^ 9 valeurs?

long N = 1000000000;
double max = 100.0; // we start small, to verify accuracy

IEnumerable<double> doubles = GetRandomDoubles(N, max, 0);
double oldWay = GetAverage_old(doubles); // 49.9994879713857
double newWay = GetAverage(doubles); // 49.9994879713868 -- pretty close

max = double.MaxValue * 0.001; // now let's try something enormous

doubles = GetRandomDoubles(N, max, 0);
oldWay = GetAverage_old(doubles); // Infinity
newWay = GetAverage(doubles); // 8.98837362725198E+305 -- no overflow

Naturellement, la manière acceptable de cette solution dépendra de vos exigences de précision. Mais ça vaut la peine d'être envisagé.

1
Dan Tao

Afin de garder la logique simple et de garder les performances non les meilleures mais acceptables, je vous recommande d'utiliser BigDecimal ensemble avec le type primitif. Le concept est très simple, vous utilisez ensemble des valeurs de somme primitives ensemble, chaque fois que la valeur se débordera ou débordera, vous déplacez la valeur de calcul sur le bigdecimal, puis réinitialisez-la pour le calcul de la somme suivante. Une autre chose que vous devriez savoir, c'est lorsque vous construisez BigDecimal, vous devez toujours utiliser une chaîne au lieu de double.

BigDecimal average(double[] values){
    BigDecimal totalSum = BigDecimal.ZERO;
    double tempSum = 0.00;
    for (double value : values){
        if (isOutOfRange(tempSum, value)) {
            totalSum = sum(totalSum, tempSum);
            tempSum = 0.00;
        }
        tempSum += value;
    }
    totalSum = sum(totalSum, tempSum);
    BigDecimal count = new BigDecimal(values.length);
    return totalSum.divide(count);
}

BigDecimal sum(BigDecimal val1, double val2){
    BigDecimal val = new BigDecimal(String.valueOf(val2));
    return val1.add(val);
}

boolean isOutOfRange(double sum, double value){
    // because sum + value > max will be error if both sum and value are positive
    // so I adapt the equation to be value > max - sum 
    if(sum >= 0.00 && value > Double.MAX - sum){
        return true;
    }

    // because sum + value < min will be error if both sum and value are negative
    // so I adapt the equation to be value < min - sum
    if(sum < 0.00 && value < Double.MIN - sum){
        return true;
    }
    return false;
}

À partir de ce concept, chaque fois que le résultat est en train de déborder ou de débordement, nous garderons cette valeur dans la variable plus grande, cette solution pourrait un peu ralentir la performance due au calcul de BigDecimal, mais il garantit la stabilité d'exécution.

0
Toomtarm Kung

Découvrez la section pour moyenne mobile cumulative

0
basszero