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


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, но без потери огромного кода. Могу ли я избежать хранения всех значений?


Вы хотите рассчитать скользящую среднюю по конкретным окнам или оценка / приближение среднего будет делать?
— 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

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. Он основан на том факте, что предыдущее измерение достаточно близко к текущему.


Еще одна интересная идея. К сожалению, я не уверен, что потребление энергии всегда будет таким, поскольку это система с тепловым насосом, и один день может потребовать 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 или около того недель.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.