Search code examples
sqlpostgresqlwindow-functionsgaps-and-islands

How to form groups of consecutive dates allowing for a given maximum gap?


Given a table like:

person_id contact_day days_last_contact dash_group
1 2015-02-09 1
1 2015-05-01 81 2
1 2015-05-02 1 2
1 2015-05-03 1 2
1 2015-06-01 29 3
1 2015-08-01 61 4
1 2015-08-04 3 4
1 2015-09-01 28 5
2 2015-05-01 1
2 2015-06-01 31 2
2 2015-07-01 30 3
3 2015-05-01 1
3 2015-05-02 1 1
3 2015-05-04 2 1
3 2015-06-01 28 2
3 2015-06-02 1 2
3 2015-06-06 4 3

See also DB Fiddle example.

How can I identify streaks of days - consecutive but allowing for a maximum gap?

Original columns in the data are person_id and contact_day. I would like to partition by person_id and the "streak" (group of days close by). My approach so far is to first calculate the number of days since last contact (days_last_contact) and then try to use that to calculate the column dash_group, which tags rows within the maximum threshold - 3 days in the example.

How can I calculate dash_group? I calculate days_last_contact by subtracting contact_day and it's 1-lag, partitioned by person_id and sorted by date).

SELECT 
  contact_day - lag(contact_day, 1, NULL) 
    OVER (PARTITION BY person_id ORDER BY contact_day ASC) 
    AS days_last_contact
FROM mydata
;

But how can I then use that to group together the rows where days_last_contact is below some threshold? (3 days in this example). So, in this example, dash_group 2 for person_id 1 identify the days May 1, 2 and 3 are close by, but then the next date for that person is June 1, which is too far (29 days since last contact, which is greater than the threshold of 3), so it gets a new dash_group. Similarly, dash_group 4, groups together August 1 and August 4, because the difference is 3, but in the case of June 2 and June 6 (person 3), the difference is 4 and then they are classified in different groups.

After looking around, I've found for example this SO question where they point to the 'trick' #4 here, which is nicely hacky, but only works for consecutive dates / gapless series, and I need to allow for arbitrary gaps.


Solution

  • Counting the gaps (greater than given tolerance) in a second window function forms the group numbers you are after:

    SELECT person_id, contact_day
         , count(*) FILTER (WHERE gap > 3) OVER (PARTITION BY person_id ORDER BY contact_day) AS dash_group
    FROM  (
       SELECT person_id, contact_day
            , contact_day - lag(contact_day) OVER (PARTITION BY person_id ORDER BY contact_day) AS gap
       FROM   mydata
       ) sub
    ORDER  BY person_id, contact_day;  -- optional
    

    db<>fiddle here

    About the aggregate FILTER clause:

    It's short and intuitive, and typically fastest. See:

    It's the classic topic of "gaps and islands". Once you know to look for the tag , you'll find plenty of related or nigh identical questions and answers like:

    Etc.

    I tagged accordingly now.