Эффективный алгоритм / структура данных для расчета скользящих средних

9

В настоящее время я занимаюсь разработкой графической ЖК-системы для отображения температуры, потоков, напряжений, мощности и энергии в системе с тепловым насосом. Использование графического ЖК-дисплея означает, что половина моей SRAM и ~ 75% моей флэш-памяти были израсходованы экранным буфером и строками.

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

Я хотел бы отобразить среднесуточное значение за последнюю неделю и месяц (4 недели для простоты), то есть скользящее среднее. В настоящее время это включает в себя ведение массива значений за последние 28 дней и вычисление среднего значения по всему массиву за месяц и за последние 7 дней за неделю.

Первоначально я делал это с использованием массива чисел с плавающей запятой (поскольку энергия находится в форме «12,12 кВт · ч»), но при этом использовалось 28 * 4 байта = 112 байтов (5,4% от SRAM). Я не против иметь только одну десятичную точку разрешения, поэтому я перешел на использование uint16_t и умножил цифру на 100. Это означает, что 12.12 представляется как 1212, и я делю на 100 для целей отображения.

Размер массива теперь уменьшен до 56 байт (намного лучше!).

Нет никакого тривиального способа уменьшить число до uint8_t, которое я вижу. Я мог бы допустить потерю десятичного разряда («12,1 кВт / ч» вместо «12,12 кВт / ч»), но потребление часто выше, чем 25,5 кВт / ч (255 - самое высокое значение, представленное 8-разрядным целым числом без знака). Потребление никогда не было ниже 10,0 кВт или выше 35,0 кВт, поэтому, возможно, я мог бы вычесть 10 из сохраненных цифр, но я знаю, что однажды мы превысим эти пределы.

Затем я протестировал код для упаковки 9-битных значений в массив. Это дает диапазон от 0 до 51,2 кВт / ч и использует в общей сложности 32 байта. Однако доступ к массиву, подобному этому, довольно медленный, особенно когда вам приходится перебирать все значения, чтобы вычислить среднее значение.

Итак, мой вопрос - есть ли более эффективный способ расчета скользящего среднего с тремя окнами - срок службы, 28 дней и 7 дней? Эффективность означает меньшую с точки зрения использования SRAM, но без потери огромного кода. Могу ли я избежать хранения всех значений?

Cybergibbons
источник
Вы хотите рассчитать скользящую среднюю по конкретным окнам или оценка / приближение среднего будет делать?
asheeshr
Я хочу скользящую среднюю за 7 и 28 дней.
Cybergibbons
Вы можете использовать разрешение 0,2 кВт / ч (делить и умножать с коэффициентом 5), и вы все равно получите диапазон 0-51,2 кВт / ч в 8 битах
чокнутый урод
В конечном итоге вы можете поместить строки и другие константы во внешнюю память или во внешнюю флэш-память - см. «Что я могу сделать, если у меня заканчивается флэш-память или SRAM?» ,
Дэвид Кэри

Ответы:

2

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

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

//Pseudocode

count=0
while new_reading and count<7:
    sum += new_reading        //Calculate the sum of first 7 values
    count++

while new_reading:            //Loop till new readings available
    avg = sum / 7             //Calculate average
    sum -= avg                //Subtract average from sum
    sum += new_reading        //Add next reading to sum
    print avg
asheeshr
источник
2

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

average = (weight1*average+weight2*new_value)/(weight1+weight2);

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

для более эффективного метода вычисления для вашего решения 9 битов на значение вы можете сохранить 8 старших битов значений в массиве и выделить младшие биты:

uint8_t[28] highbits;
uint32_t lowbits;

чтобы установить значение, вам нужно разделить его

void getvalue(uint8_t index, uint16_t value){
    highbits[index] = value>>1;
    uint32_t flag = (value & 1)<<index;
    highbits|=flag;
    highbits&=~flag;
}

в результате 2 сдвигов И и ИЛИ и не

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

uint16_t getAverage(){
    uint16_t sum=0;
    for(uint8_t i=0;i<28;i++){
        sum+=highbits[i];
    }
    sum<<=1;//multiply by 2 after the loop
    sum+=bitcount(lowbits);
    return sum/28;
}

вы можете использовать эффективный параллельный битовый счет дляbitcount()

чокнутый урод
источник
1
Можете ли вы объяснить подробнее, как это позволило бы мне рассчитать среднее значение за 7 и 28 дней?
Cybergibbons
Я использовал этот подход для сглаживания шумовых аналоговых значений ранее, и он, безусловно, был довольно эффективным. Однако мне не нужно было много точности, так как полученные значения проходили через очень грубый квантователь. Мне также не нужны исторические средние.
Питер Блумфилд
Это не позволяет рассчитать среднее значение для конкретного окна.
asheeshr
@Cybergibbons, вы можете использовать различные веса для аппроксимации окна, чтобы старые значения стали незначительными раньше или позже, или оставить 7 дней для 7-дневного окна и это скользящее среднее для 28-дневного среднего
безумный урод
1

Как насчет сохранения только разницы от предыдущего значения? В электронике существует аналогичная концепция, называемая преобразователем Delta Sigma, которая используется для преобразователей DA / AD. Он основан на том факте, что предыдущее измерение достаточно близко к текущему.

jippie
источник
Еще одна интересная идея. К сожалению, я не уверен, что потребление энергии всегда будет таким, поскольку это система с тепловым насосом, и один день может потребовать 30 кВт, а следующие 10 кВт. Мне действительно нужно собрать данные и посмотреть.
Cybergibbons
0

Почему вы не можете просто добавить значения вместе, как только вы их получите. Итак, я имею в виду, что вы получаете значение для первого дня, делите его на 1 и сохраняете где-то 1 и 1. Затем вы умножаете 1 на значение и добавляете его к следующему значению и делите их на 2.

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

Адитья Сомани
источник
Я не понимаю, как это работает с 7-дневным и 28-дневным окном?
Cybergibbons
Следите за предыдущими и последующими значениями и продолжайте складывать и вычитать их из среднего значения ...
Aditya Somani
1
Итак, я вернулся в состояние необходимости помнить 27 дней истории, конечно?
Cybergibbons
Я думал и ты прав. Так что это технически делает мой ответ неверным. Я вкладываю в это больше времени и терпения. Может быть, что-то из коробки. Я дам вам знать, если что-нибудь придумаю. Мы делаем что-то подобное на моем рабочем месте. Позвольте мне спросить вокруг. Извините за путаницу.
Адитья Сомани
0

Есть ли более эффективный способ расчета скользящего среднего с ... 28 дней и 7 дней? ... нужно помнить 27 дней истории ...?

Вы можете получить достаточно близко, храня 11 значений, а не 28 значений, возможно, что-то вроде:

// untested code
// static variables
uint16_t daily_energy[7]; // perhaps in units of 0.01 kWh ?
uint16_t weekly_energy[4]; // perhaps in units of 0.1 kWh ?

void print_week_status(){
    Serial.print( F("last week's total energy :") );
    Serial.println( weekly_energy[0] );
    int sum = 0;
    for( int i=0; i<4; i++ ){
        sum += weekly_energy[i];
    };
    Serial.print( F("Total energy over last 4 complete weeks :") );
    Serial.println( sum );
    int average_weekly_energy = sum/4;
    int average_daily_energy = average_weekly_energy/7;
    Serial.print( F("Average daily energy over last 4 weeks :") );
    Serial.println( average_daily_energy );
}
void print_day_status(){
    Serial.print( F("Yesterday's energy :") );
    Serial.println( daily_energy[0] );
    Serial.print( F("average daily energy over the last 7 complete days: ") );
    int sum = 0;
    for( int i=0; i<7; i++ ){
        sum += daily_energy[i];
    };
    int average = sum/7;
    Serial.println( average );
}

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

Дэвид Кэри
источник