Алгоритм скользящего среднего/суммы

Мне нужно отслеживать рабочие часы за последние 7 дней в цикле чтения плоского файла. Он используется для измерения «утомляемости» рабочих списков.

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

В настоящее время у меня есть класс Java со статическим массивом для хранения данных за последние x дней, затем, когда я читаю файл, я отсекаю первый элемент и перемещаю остальные 6 (для скользящего общего количества за неделю) назад на один. Обработка этого статического массива выполняется собственным методом, т.е.

/**
 * Generic rolling average/total method. Keeps adding to an array of 
 * last 'x' seen.
 * @param d Datum point you want to add/track.
 * @param i Number of rolling periods to keep track of eg. 7 = last 7 days
 *          NOT USED AT MOMENT DURING TESTING
 * @param initFlag A flag to initialize static data set back to empty.
 * @return The rolling total for i periods.
 */
private double rollingTotal(double d, boolean initFlag) {
    // Initialize running total array eg. for new Employyes
    if (initFlag) {
        runningTotal = null;
    }
    else {
        // move d+1 back to d eg. element 6 becomes element 5
        for (int x = 0; x< 6 ; x++) {
            runningTotal[x] = runningTotal[x+1];
        }
        // Put current datum point at end of array.
        runningTotal[6]= d;
    }
    // Always return sum of array when this method is called.
    double myTotal = 0.0;
    for (int x = 0; x<7; x++) {
        myTotal+= runningTotal[x];
    }
    System.err.print(Arrays.toString(runningTotal)+ '\n' );
    return myTotal;
}

Мой вопрос: это разумный подход к дизайну, или есть что-то ослепительно очевидное и простое для выполнения этой задачи? Спасибо, парни


person Pete855217    schedule 30.08.2011    source источник
comment
Хорошие четкие ответы ниже: в StackOverflow есть несколько вопросов типа «скользящего среднего», надеюсь, люди найдут этот вопрос (и его ответы) для общей, общей проблемы программирования.   -  person Pete855217    schedule 30.08.2011


Ответы (7)


Я бы сказал, используйте очередь, нажмите новую и вытащите старую. Для отслеживания среднего значения вы также можете просто вычесть полученное значение из промежуточной суммы и добавить новое (вам понадобится статическая переменная или переменная экземпляра или передача старой суммы). Нет необходимости обращаться к остальным элементам. Кроме того, где инициализируется runningTotal, если не тогда, когда initFlag имеет значение true?

private double rollingTotal(double d, boolean initFlag) {
    if(initFlag) vals = new Queue<Integer>();
    else {
        if(vals.size() == 7) // replace 7 with i.
            total -= vals.pop().intValue();
        }
        vals.push(d);
        total += d;
    }
    return total;
}

Я считаю, что очередь абстрактна, поэтому вам нужно выяснить, какую реализацию использовать. Я предлагаю вариант на основе связанного списка.

person Kevin    schedule 30.08.2011
comment
Большое спасибо, ребята: у меня есть сообщение: используйте объект более высокого уровня и используйте соответствующие методы или циклический буфер. Отличные ответы, все. Когда вы думаете об этом, вам всегда нужен доступ ко всему массиву, чтобы вы могли избавиться от этой первой записи, в чем я не был уверен на 100% самостоятельно. Я рад, что не пропустил ни одного лайнера и в основном был на разумном, если не эффективном и лаконичном пути! Это то, что мне нравится на этом сайте: качественные и актуальные ответы от людей, которые знают свое дело. - person Pete855217; 30.08.2011
comment
@Kevin: runningTotal инициализируется в основном цикле обработки файлов, когда файл попадает к новым сотрудникам. - person Pete855217; 30.08.2011
comment
@daniloqio: да, вы правы, его нужно установить и вернуть как ноль (хотя основная логика кода обрабатывает, т. Е. Игнорирует возвращаемое значение в случае, когда initFlag имеет значение true); - person Pete855217; 30.08.2011

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

Например:

// assume that currentIndex is where you want to add the new item
// You have another value, currentTotal, that is initialized at 0.
currentTotal = currentTotal - runningTotal[currentIndex] + d;
runningTotal[currentIndex] = d;
// increment the index.
currentIndex = (currentIndex + 1) % 7;

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

person Jim Mischel    schedule 30.08.2011

Вы можете попробовать использовать круговой буфер вместо перемещения всех данных при каждом добавлении:

runningTotal[nextIndex] = d;
nextIndex+=1;
if (nextIndex>=7) nextIndex = 0;

Таким образом, nextIndex всегда указывает на самое старое данное. Вы по-прежнему можете суммировать от начала до конца, как и раньше.

person JCooper    schedule 30.08.2011
comment
Еще одно изящное: я получаю сообщение, используя циклический буфер или объект более высокого уровня, который дает вам доступ к методам, которые просто вещи. Спасибо JCooper. - person Pete855217; 30.08.2011
comment
Вам нужно будет сохранить nextIndex в текстовом файле; Я думаю, что это не лучший подход к дизайну, как заданный вопрос. - person daniloquio; 30.08.2011

Вы можете использовать экспоненциально-взвешенное скользящее среднее. Его довольно долго писать, но код тривиален по сравнению с ним. Это, как правило, дает более гладкие результаты.

double previous;
static final double DAY = 1.0;
static final double WEEK = 6.0;
static final double ALPHA = DAY/WEEK;

private double movingAverage(double d) {
    return previous = ALPHA * d + (1 - ALPHA) * previous ;
}

Примечание: это оптимизированная версия формулы

double previous;
static final double DAY = 1.0;
static final double WEEK = 6.0;
static final double ALPHA = 1 - Math.exp(-DAY/WEEK);

private double movingAverage(double d) {
    return previous = ALPHA * d + (1 - ALPHA) * previous ;
}

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

person Peter Lawrey    schedule 30.08.2011
comment
Предоставленная реализация давала мне отрицательные результаты. Вместо этого использовали этот: stackoverflow.com/a/9201081/488489 - person Andrey Novikov; 13.11.2015
comment
@AndreyNovikov Помимо инициализации значения первым значением вместо 0, формула та же. - person Peter Lawrey; 17.11.2015
comment
@AndreyNovikov ... то же самое, что и после того, как я исправил это, изменив - на + Спасибо, что заметили, что это было неправильно. - person Peter Lawrey; 17.11.2015
comment
Да, я разобрался позже и закончил с твоей исправленной версией. :) - person Andrey Novikov; 17.11.2015
comment
@AndreyNovikov, если альфа не маленькая, я добавил формулу, которая точнее, но медленнее. Примечание: хотя формула была правильной, имя ALPHA было неправильным. Я исправил это сейчас. - person Peter Lawrey; 17.11.2015

Было бы проще использовать ArrayList вместо массива. Тогда вы могли бы просто использовать

ArrayList<Double> runningTotal = new ArrayList<Double>();

....

runningTotal.remove(0);
runningTotal.add(d);
person mamboking    schedule 30.08.2011
comment
Красивое, короткое аккуратное решение Мамбокинг! - person Pete855217; 30.08.2011

Почему вы инициализируете runningTotal нулем? Каков его тип? Где это заявлено? Было бы хорошо, если бы вы разместили несколько примеров кода, которые напоминают реальный код Java.

Двигаясь дальше, моя критика будет следующей: ваша функция делает слишком много. Функция или метод должны быть связными. Точнее, они должны делать одно и только одно.

Что еще хуже, что происходит в вашем цикле for, когда x = 5? Вы копируете runningTotal[6] в runningTotal[5], но тогда у вас есть две копии одного и того же значения в позиции 5 и 6.

В вашем дизайне ваша функция

  1. перемещает/перетасовывает элементы в вашем массиве
  2. вычисляет общую
  3. печатает материал со стандартной ошибкой
  4. возвращает общее количество

Это слишком много.

Мое первое предложение - не перемещать вещи в массиве. Вместо этого реализуйте циклический буфер и используйте его вместо массива. Это упростит ваш дизайн. Мое второе предложение состоит в том, чтобы разбить вещи на связанные функции:

  1. иметь структуру данных (круговой буфер), которая позволяет вам добавлять к ней (и которая удаляет самую старую запись всякий раз, когда она достигает своей емкости).
  2. иметь структуру данных, реализующую интератор
  3. есть функция, которая вычисляет итог на итераторе (вам все равно, вычисляете ли вы итог из массива, списка или циклического буфера.)
  4. не называйте это тотальным. Назовите это суммой, которую вы вычисляете.

Я бы так и сделал :)

// java pseudocode below - might not compile.

// assume you have a class called CircularBuffer, of say, doubles,
public class CircularBuffer
{
  public CircularBuffer(final int capacity) {...}
  public int getSize(){ ... return # of elements in it ... }
  public add(final Double d){ ... add to the end, drop from the front if we reach capacity... }
  public Iterator<Double> iterator(){ ... gets an interator over the content of the buffer ...}
}

// somewhere else, in another class... NOT ON CircularBuffer

public class Calculator
{
  //assume none of the double values is null
  static public Double sum(final Double ... doubles )
  {
    double sum= 0;
    for( Double d : doubles )
    {
      total += d.doubleValue();
    }
    return sum;
  }

 // you can calculate other things too
 static public Double avg(final Double ... doubles ){...}
 static public Double std(final Double ... doubles ){...}
}

/// somewhere else
{
  CircularBuffer buffer = new CircularBuffer(7);

  while( readingAndReadingAndReading )
  {
    // drops oldest values as it reaches capacity
    // always keeping the latest 7 readings
    buffer.add( getLatestValueFromSomewhere() );
  }

  System.out.println( "total=" + Calculator.sum() );
  System.out.println( "average=" + Calculator.avg() );
  System.out.println( "standard deviation=" + Calculator.std() );
}
person luis.espinal    schedule 30.08.2011
comment
Это отличная информация, Луис, однако помните, что эта функция является небольшой частью функциональности класса, и было бы излишним добавлять слишком много кода, чтобы сделать его идеальным. Вы технически правы, и я понимаю, что мой код делает «слишком много», но в то же время иногда лучше ошибиться в сторону меньшего и более четкого кода, чем стремиться к совершенству. Учитывая мои навыки работы с Java, даже компиляция псевдокода, который вы описываете, заставила бы меня потратить на это свой бюджет (!), Но спасибо за четкое описание. - person Pete855217; 31.08.2011
comment
Хм, дело не в совершенстве, а в устоявшихся производственных практиках, которые мы знаем последние 3 десятилетия. Чистый код всегда секционирован. У нас есть десятилетия доказательств того, что это путь в общем случае (с точки зрения экономической эффективности, уменьшения дефектов, понимания и т. д.)... если только это не одноразовый код для одноразовая вещь. Если таким образом начинать любой анализ проблемы, это никогда не будет затратным. Кодирование 101, разберитесь с проблемой, и последует код, не излишний и не сложный;) - person luis.espinal; 31.08.2011

Ваша задача слишком проста, и выбранный вами подход, безусловно, хорош для этой работы. Однако, если вы хотите использовать лучший дизайн, вы должны избавиться от всего этого движения чисел; вам лучше использовать очередь FIFO и хорошо использовать методы push и pop; таким образом код не будет отражать какое-либо перемещение данных, а только два логических действия «новые данные» и «удалить данные старше 7 дней».

person daniloquio    schedule 30.08.2011