Как рассчитать скользящую медиану с процентили c () и окном? - PullRequest
1 голос
/ 25 апреля 2020

Окно работает с COUNT, AVG и т. Д. c. но не работает с процентами_дис c

  SELECT
       x,
       COUNT(*) OVER w AS w_count, -- fine
       AVG(x) OVER w   AS avg_x,     -- fine
       percentile_disc(0.5) within group (order by x) OVER w AS mdn_x  -- BUG!
  FROM t
  WINDOW w AS (ROWS BETWEEN 3 PRECEDING AND CURRENT ROW)
  ORDER BY 1

(отредактировано),

  • PostgreSQL v10.12 скажем синтаксическая ошибка на уровне или около "OVER" .
  • PostgreSQL v12.2 скажем OVER не поддерживается для упорядоченного набора агрегатов процентных_дисков c

Кажется, что это невозможно ... Есть какие-то обходные пути? Возможно боковое соединение с подзапросом.


Как объяснил здесь , , МЕДИАН важен , лучше AVG в анализе аномалий. Кроме того, скользящая медиана , лучшая (наиболее устойчивая) скользящая средняя.

Ответы [ 3 ]

2 голосов
/ 28 апреля 2020

Я просто подумал об еще одной возможности. Мы можем создать агрегат и использовать его так же, как встроенный avg или count.

Давайте начнем с агрегатора:

create function median_sfunc (
    state integer[], data integer
) returns integer[] as
$$
begin
    if state is null then
        return array[data];
    else
        return state || data;
    end if;
end;
$$ language plpgsql;

А затем финишер:

create function median_ffunc (
    state integer[]
) returns double precision as
$$
begin
    return (state[(array_length(state, 1) + 1)/ 2] + state[(array_length(state, 1) + 2) / 2]) / 2.;
end;
$$ language plpgsql;

Конечно, поставщик (начальное состояние) будет пустым запросом. Таким образом, мы получаем агрегат:

create aggregate median (integer) (
    sfunc     = median_sfunc,
    stype     = integer[],
    finalfunc = median_ffunc
    );

Теперь вы можете вызывать его элегантным способом независимо от используемого окна:

select x,
       count(*) over w as w_count,
       avg(x) over w as avg_x,
       median(x) over w as mdn_x
from tmp t
    window w as (order by x rows between 3 preceding and current row)
1 голос
/ 28 апреля 2020

К сожалению, агрегатные функции с упорядоченным набором не поддерживают windows. Вы можете вычислить его вручную в качестве обходного пути:

select x,
       count(*) over w as w_count,
       avg(x) over w   as avg_x,
       (lag(x, 2) over w + lag(x) over w) / 2. as mdn_x
from tmp t
    window w as (rows between 3 preceding and current row)
order by 1;

Вот рабочая демонстрация . Это не работает для первых 3 строк, хотя. Если вы хотите, чтобы он работал для каждой строки, то необходимо проверить угловые случаи:

select x,
       count(*) over w as w_count,
       avg(x) over w   as avg_x,
       case
           when lag(x) over w is null then x
           when lag(x, 2) over w is null then (x + lag(x) over w) / 2.
           when lag(x, 3) over w is null then lag(x) over w
           else (lag(x, 2) over w + lag(x) over w) / 2.
           end
from tmp t
    window w as (rows between 3 preceding and current row)
order by 1;

Вот демо .

Конечно, ваш пример довольно просто, потому что окно невелико (всего 4 элемента), но точный запрос становится действительно длинным для больших windows.

Редактировать:

Первый запрос можно обобщить так:

select x,
       count(*) over w as w_count,
       avg(x) over w   as avg_x,
       (lag(x, (N + 1) / 2) over w + lag(x, N / 2) over w) / 2. as mdn_x
from tmp t
    window w as (rows between N preceding and current row)
order by 1;

, где N - количество строк для просмотра. Это верно даже для N, но в этом случае последний столбец можно упростить до:

lag(x, N / 2) over w as mdn_x

Точный запрос должен быть переписан как:

select x,
       count(*) over w as w_count,
       avg(x) over w   as avg_x,
       case
           when lag(x) over w is null then x
           when lag(x, 2) over w is null then (x + lag(x) over w) / 2.
           -- other terms
           when lag(x, N) over w is null then (lag(x, (N - 1) / 2) over w + lag(x, N / 2) over w) / 2.
           else (lag(x, 2) over w + lag(x) over w) / 2.
           end
from tmp t
    window w as (rows between N preceding and current row)
order by 1;

с общая формула для угловых случаев:

when lag(x, M) over w is null then (lag(x, (m - 1) / 2) over w + lag(x, m / 2) over w) / 2.

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

0 голосов
/ 03 мая 2020

@ Андроник указал лучшие «стратегии обхода проблемы», но для общего решения нужны полиморфные типы данных c и немного точной настройки. В версии 9.3+ PostgreSQL мы также можем оптимизировать агрегированное исчисление на windows (режим с подвижным агрегированием), что важно для обходного пути.

Это правильное решение

Это Решение - лучшее, что мы можем сделать в настоящее время (2020 год), это всего лишь «хороший и надежный обходной путь»: пожалуйста, сообщите PostgreSQL разработчикам (потребление ОЗУ и ЦП).

create or replace FUNCTION smallseq_agg_sfunc (
    state anyarray, data anyelement
) RETURNS anyarray as $f$
  SELECT   state || data
$f$ language SQL IMMUTABLE;

create or replace FUNCTION array_median(state anyarray) returns anyelement as $f$
  SELECT percentile_cont(0.5) within group (ORDER BY s)
  FROM unnest(state) t(s)
$f$ language SQL IMMUTABLE;

create or replace AGGREGATE smallset_median (anyelement) (
  sfunc     = smallseq_agg_sfunc,
  stype     = anyarray,
  finalfunc = array_median,
  initcond  = '{}'
);

Префикс имени, такой как smallset_, важен, чтобы помнить, что это обходной путь, действительный только для небольших наборов или упорядоченных небольших последовательностей . Когда он маленький, нет проблем в переупорядочении упорядоченной последовательности. В контексте Больших Данных (больших windows или больших таблиц) мы должны проверить производительность этого обходного пути .

Порядок, характеризующий медианную операцию, очень важен и в выражении window или GROUP BY обычно смешивается много переменных (много порядков), поэтому order by полезен в функции библиотеки общего пользования. Вы можете заменить AVG без страха!

Didacti c комментарии

Математики определяют среднее значение как

enter image description here

поэтому мы можем реализовать его и избежать условия order by для упорядоченных последовательностей:

create or replace FUNCTION smallseq_percentile_cont (
    state anyarray 
) returns double precision as $f$
  SELECT ( s[floor((up+1)*0.5)] + s[ceil((up+1)*0.5)] )::double precision / 2.
  FROM ( SELECT array_agg(x) FROM unnest(state) t(x)) ) t1(s)
       , ( SELECT array_upper(state,1) ) t2(up)
$f$ language SQL IMMUTABLE;

Пожалуйста, избегайте упрощения на (s[(up + 1)/ 2] + s[(up + 2) / 2]), это не правильно. Вы можете заменить 0.5 параметром для получения аналога percentile_cont(fraction). Для неупорядоченных множеств (smallset_percentile_cont реализация) вы можете добавить ORDER BY x после t(x).

Более общие случаи

Важно помнить, что "classi c median" основан на на непрерывный процентиль (продолжение), но есть также дискретный процентиль (dis c).

Встроенная функция percentile_cont() не имеет смысла для text типа данных и других, а иногда нам нужно сохранить медиану в том же типе данных (например, целое число, а не двойное) или использовать элемент выборки. В этом контексте мы думаем о percentile_disc(). Предположим также, что в качестве полезной универсальной функции массива c мы предпочитаем параметр c one:

create or replace FUNCTION array_percentile_disc(
  state anyarray, p float DEFAULT 0.5
) returns anyelement as $f$
  SELECT percentile_disc(p) within group (ORDER BY s)
  FROM unnest(state) t(s)
$f$ language SQL IMMUTABLE;

create or replace FUNCTION array_median_disc(state anyarray)
returns anyelement as $wrap$
  SELECT array_percentile_disc($1)
$wrap$ language SQL IMMUTABLE;

create or replace AGGREGATE smallset_median_disc (anyelement) (
  sfunc     = smallseq_agg_sfunc,
  stype     = anyarray,
  finalfunc = array_median_disc,
  initcond  = '{}'
);

Есть еще одна проблема с PostgreSQL : невозможно определить второй параметр для finalfun c, поэтому, если вам нужно, например, определить AGGREGATE smallseq_percentile_disc(anyelement,float), вам нужно определить каждый, например, для «90% дискретных», определить больше одна функция обтекания array_perc90_disc() с использованием array_percentile_disc($1,0.9).

Опция режима движущегося агрегирования

См. Руководство по PG .

... пожалуйста, совместите здесь : этот ответ Wiki!

Временное решение для больших данных

Как и предположил @Andronicus, мы можем использовать lag(). В этом случае важно также помнить, чтобы проверить порядок окна, возможно, вам нужно указать c окно для медианы.

SELECT x, avg_x,
       CASE WHEN w_count<9001 THEN NULL ELSE mdn_x END mdn_x 
FROM (
  SELECT x,
       count(*) over w   AS w_count,
       avg(x)   over w   AS avg_x,
       (
         lag(x, floor(9002*0.5)::double precision / 2.) over w 
         + lag(x, ceil(9002*0.5)::double precision / 2.) over w 
       ) / 2.            AS mdn_x
  FROM t
  WINDOW w as (ORDER BY x rows between 9000 preceding and current row)
) t_aux

Мы можем использовать w_count, чтобы обобщить и избежать начальных нулей, и теперь, предполагая также «дискретную медиану», мы можем использовать:

SELECT x, w_count, avg_x,
       lag( x, floor(w_count*0.5) ) over w2 AS mdn_x
FROM (
  SELECT x,
       count(*) over w   AS w_count,
       avg(x)   over w   AS avg_x
  FROM t
  WINDOW w1 as (rows between 9000 preceding and current row)
) t_aux
WINDOW w2 as (ORDER BY x rows between 9000 preceding and current row)

Важно, что w2 является клоном w1, за исключением предложения ORDER.

...