Какое хорошее решение для вычисления среднего, где сумма всех значений превышает пределы двойного? - PullRequest
37 голосов
/ 18 декабря 2009

У меня есть требование рассчитать среднее значение очень большого набора двойных чисел (10 ^ 9 значений). Сумма значений превышает верхнюю границу двойного, поэтому кто-нибудь знает какие-нибудь изящные маленькие хитрости для вычисления среднего, которые не требуют также вычисления суммы?

Я использую Java 1.5.

Ответы [ 18 ]

3 голосов
/ 19 декабря 2009

Прежде всего, ознакомьтесь с внутренним представлением значений double. Википедия должна быть хорошей отправной точкой.

Затем рассмотрим, что двойные числа выражаются как «значение плюс показатель степени», где показатель степени является степенью двойки. Предел наибольшего двойного значения - это верхний предел показателя степени, а не предел значения! Таким образом, вы можете разделить все большие входные числа на достаточно большую степень двух. Это должно быть безопасно для всех достаточно больших чисел. Вы можете повторно умножить результат на коэффициент, чтобы проверить, потеряли ли вы точность с умножением.

Здесь мы идем с алгоритмом

public static double sum(double[] numbers) { 
  double eachSum, tempSum;
  double factor = Math.pow(2.0,30); // about as large as 10^9
  for (double each: numbers) {
    double temp = each / factor;
    if (t * factor != each) {
      eachSum += each;
    else {
      tempSum += temp;
    }
  }
  return (tempSum / numbers.length) * factor + (eachSum / numbers.length);
}

и не беспокойтесь о дополнительном делении и умножении. FPU оптимизирует их, так как они выполняются со степенью двойки (для сравнения представьте добавление и удаление цифр в конце десятичного числа).

PS: Кроме того, вы можете использовать Суммирование Кахана для повышения точности. Суммирование по Кахану позволяет избежать потери точности при суммировании очень больших и очень маленьких чисел.

2 голосов
/ 19 декабря 2009

Я отправил ответ на вопрос , породивший этот вопрос, после чего я понял, что мой ответ лучше подходит для этого вопроса, чем для этого. Я воспроизвел это ниже. Однако я замечаю, что мой ответ похож на комбинацию Божо и Анон . .

Поскольку другой вопрос был помечен как независимый от языка, я выбрал C # для примера кода, который я включил. Его относительная простота использования и понятный синтаксис, а также включение нескольких функций, облегчающих эту процедуру (функция DivRem в BCL и поддержка функций итераторов), а также мое собственное знакомство с ней, сделали это хороший выбор для этой проблемы. Поскольку OP здесь заинтересован в Java-решении, но я недостаточно владею Java, чтобы эффективно его написать, было бы неплохо, если бы кто-то мог добавить перевод этого кода в Java.


Некоторые из математических решений здесь очень хороши. Вот простое техническое решение.

Используйте больший тип данных. Это разбивается на две возможности:

  1. Использование высокоточной библиотеки с плавающей точкой. У того, кто сталкивается с необходимостью усреднить миллиард чисел, вероятно, есть ресурсы для покупки или умственные способности для написания 128-битной (или более длинной) библиотеки с плавающей запятой.

    Я понимаю недостатки здесь. Конечно, это будет медленнее, чем использование внутренних типов. Вы все еще можете переполнить / потерять, если число значений становится слишком большим. Яда Яда.

  2. Если ваши значения являются целыми числами или могут быть легко масштабированы до целых чисел, сохраните вашу сумму в списке целых чисел. Когда вы переполняете, просто добавьте еще одно целое число. По сути это упрощенная реализация первого варианта. Простой (непроверенный) пример в C # следует

class BigMeanSet{
    List<uint> list = new List<uint>();

    public double GetAverage(IEnumerable<uint> values){
        list.Clear();
        list.Add(0);

        uint count = 0;

        foreach(uint value in values){
            Add(0, value);
            count++;
        }

        return DivideBy(count);
    }

    void Add(int listIndex, uint value){
        if((list[listIndex] += value) < value){ // then overflow has ocurred
            if(list.Count == listIndex + 1)
                list.Add(0);
            Add(listIndex + 1, 1);
        }
    }

    double DivideBy(uint count){
        const double shift = 4.0 * 1024 * 1024 * 1024;

        double rtn       = 0;
        long   remainder = 0;

        for(int i = list.Count - 1; i >= 0; i--){
            rtn *= shift;
            remainder <<= 32;
            rtn += Math.DivRem(remainder + list[i], count, out remainder);
        }

        rtn += remainder / (double)count;

        return rtn;
    }
}

Как я уже сказал, это не проверено - у меня нет миллиарда значений, которые я действительно хочу усреднить - поэтому я, вероятно, допустил одну или две ошибки, особенно в функции DivideBy, но это должно быть продемонстрировать общую идею.

Это должно обеспечить такую ​​точность, которую может представлять двойное число, и должно работать для любого числа 32-битных элементов, вплоть до 2 32 - 1. Если требуется больше элементов, то count переменная должна быть расширена, а функция DivideBy усложнится, но я оставлю это в качестве упражнения для читателя.

С точки зрения эффективности, он должен быть таким же быстрым или быстрым, чем любая другая техника, поскольку здесь требуется только итерация по списку один раз, только одна операция деления (ну, один из них) и большая часть работать с целыми числами. Я не оптимизировал его, хотя, и я уверен, что это можно сделать немного быстрее, если это необходимо. Отказ от рекурсивного вызова функции и индексация списка были бы хорошим началом. Опять же, упражнение для читателя. Код предназначен для легкого понимания.

Если кому-то более мотивированному, чем я, в данный момент хочется проверить правильность кода и устранить все возможные проблемы, пожалуйста, будьте моим гостем.


Я сейчас протестировал этот код и внес несколько небольших исправлений (отсутствующая пара скобок в вызове конструктора List<uint> и неверный делитель в конечном делении функции DivideBy).

Я проверил его, сначала пропустив 1000 наборов случайной длины (в диапазоне от 1 до 1000), заполненных случайными целыми числами (в диапазоне от 0 до 2 32 - 1). Это были наборы, для которых я мог легко и быстро проверить точность, запустив на них каноническое среднее.

Затем я проверил с большой серией 100 * со случайной длиной от 10 5 до 10 9 Нижняя и верхняя границы этих рядов также выбирались случайным образом, ограничиваясь так, чтобы ряды подходили к диапазону 32-разрядного целого числа. Для любой серии результаты легко проверяются как (lowerbound + upperbound) / 2.

* Хорошо, это маленькая белая ложь. Я прервал тест большой серии примерно после 20 или 30 успешных прогонов. Серия длиной 10 9 занимает чуть меньше полутора минут для запуска на моей машине, так что примерно полчаса тестирования этой процедуры было достаточно для моих вкусов. * Для тех, кто заинтересован, мой тестовый код ниже:

static IEnumerable<uint> GetSeries(uint lowerbound, uint upperbound){
    for(uint i = lowerbound; i <= upperbound; i++)
        yield return i;
}

static void Test(){
    Console.BufferHeight = 1200;
    Random rnd = new Random();

    for(int i = 0; i < 1000; i++){
        uint[] numbers = new uint[rnd.Next(1, 1000)];
        for(int j = 0; j < numbers.Length; j++)
            numbers[j] = (uint)rnd.Next();

        double sum = 0;
        foreach(uint n in numbers)
            sum += n;

        double avg = sum / numbers.Length;
        double ans = new BigMeanSet().GetAverage(numbers);

        Console.WriteLine("{0}: {1} - {2} = {3}", numbers.Length, avg, ans, avg - ans);

        if(avg != ans)
            Debugger.Break();
    }

    for(int i = 0; i < 100; i++){
        uint length     = (uint)rnd.Next(100000, 1000000001);
        uint lowerbound = (uint)rnd.Next(int.MaxValue - (int)length);
        uint upperbound = lowerbound + length;

        double avg = ((double)lowerbound + upperbound) / 2;
        double ans = new BigMeanSet().GetAverage(GetSeries(lowerbound, upperbound));

        Console.WriteLine("{0}: {1} - {2} = {3}", length, avg, ans, avg - ans);

        if(avg != ans)
            Debugger.Break();
    }
}
2 голосов
/ 19 декабря 2009

Случайная выборка небольшого набора полного набора данных часто приводит к «достаточно хорошему» решению. Очевидно, что вы должны сделать это определение самостоятельно на основе системных требований. Размер выборки может быть удивительно небольшим и при этом получить достаточно хорошие ответы. Это может быть адаптивно вычислено путем вычисления среднего возрастающего числа случайно выбранных выборок - среднее будет сходиться в течение некоторого интервала.

Выборка не только решает проблему двойного переполнения, но и намного, намного быстрее. Не применимо для всех проблем, но, безусловно, полезно для многих проблем.

1 голос
/ 20 декабря 2009

Примите во внимание следующее:

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

Так что для любого набора двойных чисел произвольного размера вы можете сделать это (это в C #, но я уверен, что его можно легко перевести на 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;
}

На самом деле, это хорошо упрощается в (уже предоставленный 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;
}

Я написал быстрый тест, чтобы опробовать эту функцию в сравнении с более обычным методом суммированиязначения и деления на количество (GetAverage_old).Для моего ввода я написал эту быструю функцию, которая возвращает столько случайных положительных двойных чисел, сколько нужно:

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

А вот результаты нескольких тестовых испытаний:

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

ОК, нокак насчет 10 ^ 9 значений?

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

Естественно, насколько приемлемо это решение, будет зависеть от ваших требований к точности.Но стоит задуматься.

0 голосов
/ 13 июня 2018

Чтобы логика была простой и производительность не была лучшей, но приемлемой, я рекомендую вам использовать BigDecimal вместе с примитивным типом. Концепция очень проста: вы используете примитивный тип для суммирования значений вместе, когда значение будет недопустимым или переполненным, вы перемещаете вычисляемое значение в BigDecimal, а затем сбрасываете его для вычисления следующей суммы. Еще одна вещь, которую вы должны знать, когда вы создаете BigDecimal, вы должны всегда использовать String вместо 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;
}

Исходя из этой концепции, каждый раз, когда результатом является недостаточный или переполненный результат, мы сохраняем это значение в большей переменной, это решение может немного замедлять производительность из-за вычисления BigDecimal, но оно гарантирует стабильность времени выполнения.

0 голосов
/ 02 июня 2010

Почему так много сложных длинных ответов. Вот самый простой способ найти скользящее среднее значение до сих пор без необходимости знать, сколько элементов или размера и т.

long int i = 0; двойное среднее = 0; пока (есть еще элементы) { среднее = среднее * (i / i + 1) + X [i] / (i + 1); я ++; } возврат среднего;

0 голосов
/ 19 декабря 2009

Ознакомьтесь с разделом для накопленной скользящей средней

0 голосов
/ 19 декабря 2009

(n 1 + n 2 + ... + n k ) / k = (n 1 + n 2 ) / k + (n 3 + n 4 ) / k + ... + (n k-1 + n k ) / k, если k четное

(n 1 + n 2 + ... + n k ) / k = n 1 / k + ( n 2 + n 3 ) / k + ... + (n k-1 + n k ) / k, если k нечетно

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...