【问题标题】:How to calculate moving median with percentile_disc() and window?如何使用 percentile_disc() 和窗口计算移动中位数?
【发布时间】:2020-05-03 20:31:07
【问题描述】:

窗口正在使用 COUNT、AVG 等,但不适用于 percentile_disc

  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 说 有序集聚合 percentile_disc 不支持 OVER

似乎不可能...有一些解决方法吗?可能是带有子查询的横向连接。


作为explained hereMEDIAN 很重要,在异常分析中优于 AVG。还有moving median,比移动平均线更好(最具弹性)。

【问题讨论】:

  • 我得到“OVER is not supported for ordered-set aggregate percentile_discdbfiddle.uk/…
  • 谢谢@a_horse_with_no_name,我已编辑...您建议解决方法?在现实世界中,在同一个查询中需要moving averagemoving median

标签: sql postgresql window-functions


【解决方案1】:

我只是想到了另一种可能性。我们可以创建一个聚合并像内置的avgcount 一样使用它。

让我们从聚合器开始:

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)

【讨论】:

  • 嗨,完美!我为在现实生活中使用所做的唯一修改,推广到“任何数字类型”,是FUNCTION median_sfunc (state anyarray, data anyelement) RETURNS anyarray 等,并添加了IMMUTABLE 子句。这不相关,我也使用了language SQL(简单的SELECT CASE),因为我尽可能喜欢标准SQL ...并且还重命名为smallSeq_median,以记住这是小数字序列的解决方法,采取关注大数据的大窗口。
  • @PeterKrauss 你是对的,这更笼统,请随时编辑我的答案以更改类型;)
  • 感谢@Andronicus,还有另一个有趣的问题和您的简化中的小错误,请检查/与Wiki answer below 合作。
【解决方案2】:

不幸的是,有序集聚合函数不支持窗口。您可以手动计算它作为一种解决方法:

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;

这是working demo。不过,这不适用于前 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;

这是demo

当然,您的示例非常简单,因为窗口不大(只有 4 个元素),但是对于较大的窗口,确切的查询会变得很长。

编辑:

第一个查询可以概括为:

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.

在这种情况下,我想不出除了元编程/动态查询之外的任何方法。当窗口到达后面的行时,公式会变得更复杂,因为根据前后行的符号 - 应该使用laglead

【讨论】:

  • 嗨@Andronicus,谢谢。似乎完美的解决方法!我可以编辑将 3 替换为 5 的问题,使其更具指导性吗?
  • @PeterKrauss 很高兴你喜欢它!老实说,我宁愿您不要更改它,您可以看到模式的整个定义,并且查询的可读性会降低。唯一的变化是奇数长度的窗口 - 这将是第一个查询中的中间元素
  • 嗯...细节,不重要,可能只是第一个(主要)解决方案的小问题... 1. 因为我使用order by x,它可以使用在w2. 理想的,教学或使用模板解决方案,是注释 L 的奇偶性,窗口的长度,以及 floor 的选择(L/2) 在参数中,例如lag(x, floor(L/2.))L 为奇数时。 ...或者也许从通用解决方案开始,将in the Wikipedia定义为( lag(x, floor((L+1)/2.)) + lag(x, ceil((L+1)/2.)) )/2,因此您可以将奇数和偶数情况表示为简化。
  • @PeterKrauss 我已经详细说明了更一般的情况,我希望它已经足够了:)
  • 这个答案对 Big Data 上的大窗口有好处,并且要记住 PostgreSQL 开发人员 可以优化 aggregate median()
【解决方案3】:

@Andronicus 指出了最好的“变通策略”,但一般解决方案需要多态数据类型和一些精细调整。使用 PostgreSQL 9.3+ 版本,我们还可以优化 windows 上的聚合计算(移动聚合模式),这对于解决方法很重要。

这是正确的解决方案

这个解决方案是我们现在(2020 年)能做的最好的解决方案,它只是一个“好的和可靠的解决方法”:请让 PostgreSQL 开发人员知道(RAM 和 CPU 消耗)。

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_ 这样的名称前缀很重要,记住它是一种变通方法,仅对小集合 或有序小序列 有效。当它很小时,重新排序有序序列没有问题。在大数据(大窗口或大表)的上下文中,我们必须check performance of this workaround

顺序表征中位数运算,非常重要,在windowGROUP BY 子句中通常混合许多变量(许多顺序),因此order by 在通用函数中很有用图书馆。你可以replace AVG无所畏惧!

教学法

数学家define the mean as

所以,我们可以实现它并避免有序序列的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 实现),您可以在t(x) 之后添加ORDER BY x

更一般的情况

重要的是要记住,“经典中位数”基于连续百分位数(续),但也有离散百分位数(圆盘)。

内置函数 percentile_cont()text 数据类型和其他数据类型没有意义,有时我们需要在同一数据类型中保留中位数(例如整数而不是双精度),或者使用示例元素。在这种情况下,我们正在考虑percentile_disc()。还假设,作为有用的通用数组函数,我们更喜欢参数函数:

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  = '{}'
);

another problem with PostgreSQL:不可能为finalfunc定义一个第二个参数,所以,如果你需要例如定义AGGREGATE smallseq_percentile_disc(anyelement,float)你需要定义每一个,例如为“ 90% 离散" 使用array_percentile_disc($1,0.9) 定义更多的包装函数array_perc90_disc()

移动聚合模式选项

Pg Guide

...请在此处合作:此答案是 Wiki!

大数据解决方法

正如@Andronicus 建议的那样,我们可以使用lag()。在这种情况下,记住检查窗口的顺序也很重要,也许您需要一个特定的窗口作为中位数。

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)

重要的是,w2w1 的克隆,除了 ORDER 子句。

【讨论】:

猜你喜欢
  • 2014-07-15
  • 2011-12-12
  • 2020-07-05
  • 2018-09-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-04-06
相关资源
最近更新 更多