计算列组合的出现次数

时间:2014-03-06 12:56:16

标签: sql postgresql time-series window-functions

我有不同公司的日常时间序列(实际上是工作日),我使用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天到事件发生后一天的条目。 该查询为所有事件执行此操作。

这是结果表的一小部分。

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仅出现一次(出现= 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)

并且可以处理工作日。

2 个答案:

答案 0 :(得分:1)

基本上,策略是首先将旗帜日纳入其中,然后与其他人一起加入:

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

然而,工作日存在问题,因为这些可能意味着什么(节假日计数?)。

答案 1 :(得分: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(ISODOWFROM ...)更容易截断周末。

查询

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

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

  

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

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 > 0)内保留数天。

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

VOILÀ。

SQL Fiddle.