【问题标题】:Count occurrences of combinations of columns计算列组合的出现次数
【发布时间】:2014-04-09 03:11:21
【问题描述】:

我有不同公司的每日时间序列(实际上是工作日),我使用 PostgreSQL。还有一个指标变量(称为标志)在大多数情况下取值为 0,在一些罕见的事件日取值为 1。如果一个公司的指标变量取值为 1,我想进一步调查相应公司从该事件前两天到事件后一天的条目。让我将其称为 [-2,1] 窗口,事件日为第 0 天。

我正在使用以下查询

CREATE TABLE test AS
WITH cte AS (
   SELECT *
        , MAX(flag) OVER(PARTITION BY company ORDER BY day
                         ROWS BETWEEN 1 preceding AND 2 following) Lead1
   FROM mytable)
SELECT *
FROM cte
WHERE Lead1 = 1 
ORDER BY day,company

查询取事件发生前2天到事件发生后1天的条目,为发生事件的公司。 查询对所有事件执行此操作。

这是结果表的一小部分。

day              company    flag     
2012-01-23       A          0        
2012-01-24       A          0         
2012-01-25       A          1         
2012-01-25       B          0         
2012-01-26       A          0         
2012-01-26       B          0        
2012-01-27       B          1        
2012-01-30       B          0        
2013-01-10       A          0        
2013-01-11       A          0              
2013-01-14       A          1              

现在我想分别对每个 [-2,1] 窗口进行进一步的计算。所以我需要一个变量来识别每个 [-2,1] 窗口。这个想法是我用变量“发生”计算每个公司的窗口数量,以便在进一步计算中我可以使用该子句

    GROUP BY company, occur

因此我想要的输出如下所示:

day              company    flag     occur
2012-01-23       A          0        1
2012-01-24       A          0        1 
2012-01-25       A          1        1 
2012-01-25       B          0        1 
2012-01-26       A          0        1 
2012-01-26       B          0        1
2012-01-27       B          1        1
2012-01-30       B          0        1
2013-01-10       A          0        2
2013-01-11       A          0        2
2013-01-14       A          1        2 

在示例中,公司 B 仅出现一次 (occur = 1)。但是A公司出现了两次。第一次从 2012-01-23 到 2012-01-26。第二次是从 2013-01-10 到 2013-01-14。公司 A 的第二个时间范围不包括事件日 (-2,-1,0,1) 周围的所有四天,因为该公司在该时间范围结束之前离开了数据集。

正如我所说,我正在处理工作日。我不在乎假期,我有周一到周五的数据。之前我写了以下函数:

CREATE OR REPLACE FUNCTION addbusinessdays(date, integer)
  RETURNS date AS
$BODY$ 
WITH alldates AS (
    SELECT i,
    $1 + (i * CASE WHEN $2 < 0 THEN -1 ELSE 1 END) AS date
    FROM generate_series(0,(ABS($2) + 5)*2) i
),
days AS (
    SELECT i, date, EXTRACT('dow' FROM date) AS dow
    FROM alldates
),
businessdays AS (
    SELECT i, date, d.dow FROM days d
    WHERE d.dow BETWEEN 1 AND 5
    ORDER BY i
)

-- adding business days to a date --
SELECT date FROM businessdays WHERE
        CASE WHEN $2 > 0 THEN date >=$1 WHEN $2 < 0
             THEN date <=$1 ELSE date =$1 END
    LIMIT 1
    offset ABS($2)
$BODY$
  LANGUAGE 'sql' VOLATILE;

它可以从给定日期添加/减去工作日,并像这样工作:

    select * from addbusinessdays('2013-01-14',-2)

2013-01-10 提供结果。所以在 Jakub 的方法中,我们可以将倒数第二行和倒数第三行改为

      w.day BETWEEN addbusinessdays(t1.day, -2) AND addbusinessdays(t1.day, 1)

并且可以处理工作日。

【问题讨论】:

  • 你如何“定义”一个窗口?
  • 窗口是指给定公司的 flag = 1 那天周围的日子。因此,将标志日视为第 0 天,[-2,1] 窗口是从前 2 天到后 1 天的时间段。 @MarkLaREZZA
  • 那么一个窗口总是有4天?
  • 是的,理论上窗口总是 4 天,但对于某些公司,我可能没有全部 4 天的数据。 @MarkLaREZZA
  • 知道了 - 谢谢。我想我最初误读了这篇文章。

标签: sql postgresql time-series window-functions


【解决方案1】:

功能

在使用函数addbusinessdays() 时,请考虑以下情况:

CREATE OR REPLACE FUNCTION addbusinessdays(date, integer)
  RETURNS date AS
$func$ 
SELECT day
FROM  (
    SELECT i, $1 + i * sign($2)::int AS day
    FROM   generate_series(0, ((abs($2) * 7) / 5) + 3) i
    ) sub
WHERE  EXTRACT(ISODOW FROM day) < 6  -- truncate weekend
ORDER  BY i
OFFSET abs($2)
LIMIT  1
$func$  LANGUAGE sql IMMUTABLE;

要点

  • 切勿引用语言名称sql。它是一个标识符,而不是一个字符串。

  • 为什么函数是VOLATILE?将其设为IMMUTABLE 以获得更好的重复使用性能和更多选项(例如在功能索引中使用它)。

  • (ABS($2) + 5)*2) 填充太多。替换为((abs($2) * 7) / 5) + 3)

  • 多个级别的 CTE 毫无用处。

  • 上次 CTE 中的ORDER BY 也没有用。

  • 正如我之前的回答中提到的,extract(ISODOW FROM ...) 更方便截断周末。

查询

也就是说,我根本不会对这个查询使用上述函数。一次构建完整的相关日期网格,而不是计算每一行的天数范围。

基于评论中的这个断言(应该在问题中,真的!):

同一家公司的两个后续窗口永远不能重叠。

WITH range AS (              -- only with flag
   SELECT company
        , min(day) - 2 AS r_start
        , max(day) + 1 AS r_stop
   FROM   tbl t 
   WHERE  flag <> 0
   GROUP  BY 1
   )
, grid AS (
   SELECT company, day::date
   FROM   range r
         ,generate_series(r.r_start, r.r_stop, interval '1d') d(day)
   WHERE  extract('ISODOW' FROM d.day) < 6
   )
SELECT *, sum(flag) OVER(PARTITION BY company ORDER BY day
                         ROWS BETWEEN UNBOUNDED PRECEDING
                         AND 2 following) AS window_nr
FROM  (
   SELECT t.*, max(t.flag) OVER(PARTITION BY g.company ORDER BY g.day
                           ROWS BETWEEN 1 preceding
                           AND 2 following) in_window
   FROM   grid     g
   LEFT   JOIN tbl t USING (company, day)
   ) sub
WHERE  in_window > 0      -- only rows in [-2,1] window
AND    day IS NOT NULL    -- exclude missing days in [-2,1] window
ORDER  BY company, day;

如何?

  • 构建所有工作日的网格:CTE grid

  • 为了将网格保持在尽可能小的大小,请提取每家公司的最短和最长(加上缓冲区)天数:CTE range

  • LEFT JOIN 的实际行数。现在,后续窗口函数的框架可以使用静态数字。

  • 要获得每个标志和公司 (window_nr) 的不同数字,只需从网格开始计算标志(考虑缓冲区)。

  • 仅在 [-2,1] 窗口 (in_window &gt; 0) 内保留天数。

  • 只保留表格中实际行的天数。

瞧。

SQL Fiddle.

【讨论】:

  • 绝妙的答案,完美运行!再次感谢您的帮助和解释,以帮助我更好地理解它!非常慷慨地分享你的知识@ErwinBrandstetter
【解决方案2】:

基本上,策略是先列举标志日,然后与其他人一起加入:

WITH windows AS(
SELECT t1.day
       ,t1.company
       ,rank() OVER (PARTITION BY company ORDER BY day) as rank
FROM table1 t1
WHERE flag =1)

SELECT t1.day
      ,t1.company
      ,t1.flag
      ,w.rank
FROM table1 AS t1
JOIN windows AS w
ON
  t1.company = w.company
  AND
  w.day BETWEEN 
 t1.day - interval '2 day' AND t1.day + interval '1 day'
ORDER BY t1.day, t1.company;

Fiddle.

但是,工作日存在一个问题,因为工作日可能意味着什么(假期算吗?)。

【讨论】:

  • 非常感谢您的帮助,请原谅我不够精确。我不关心假期,在我的数据集中是周一到周五的条目。我编辑了这个问题,以便更准确地了解工作日。 @JakubKania
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-04-07
  • 2017-03-08
  • 2012-08-20
  • 1970-01-01
相关资源
最近更新 更多