Re: [GENERAL] Select ranges based on sequential breaks
On Tue, 23 Jun 2009 04:41:44 +1000, Mike Toews wrote: Window functions appear to be the best solution for this style of problem, and I'm looking forward to their applications. However, I'm sticking with 8.3 for at least a year, so I'm not able to explore this solution yet. For now, I can only post-process the output in a non-SQL environment. I also need to do other fun stuff, like cumulative sums, which is also challenging with SQL, but much easier and intuitive with R. As a largely procedural programmer, the PL/SQL solution is quite appealing to me, and would be similarly simple to calculate cumulative sums. The integration of SELECT statements within PL/SQL also seems much tighter than with other PL languages. Unfortunately, one can't send a cursor or a set of results directly as a PL argument. I'm having a skim through Celko's chapter 24, but it doesn't seem to be close to my needs either. On Tue, 23 Jun 2009 08:05:14 +1000, Mike Toews wrote: ... # Determine where the rows are different; 1=different rows, 0=same rows dat$breaks <- ifelse(dat$bin != c(TRUE, as.character(dat$bin[-nrow(dat)])), 1, 0) # Determine where the continuous parts are: dat$part <- factor(cumsum(dat$breaks)) Yes, as far as I can tell, this is almost identical to my WINDOW-based solution in finding when there is a change, marking it with 0 or 1 and the using cumulative sum to number the partitions. This could be similarly done in PL/SQL but it seemed more sensible to just do the whole thing rather than using GROUP BY after enumeration. - Joel -- Sent via pgsql-general mailing list (pgsql-general@postgresql.org) To make changes to your subscription: http://www.postgresql.org/mailpref/pgsql-general
Re: [GENERAL] Select ranges based on sequential breaks
Scott Marlowe wrote: On Mon, Jun 15, 2009 at 12:23 PM, Mike Toews wrote: This is easy to compute using a spreadsheet or in R, but how would I do this with SQL? I'm using 8.3. Advice is appreciated. FYI (and I'm no expert in this area) R is available as a pl for postgres, look for pl/R or plR FYI, here is how I implement ranges on sequential breaks in R. Sorry, I haven't meddled with plR yet, although I'm experience with both R and postgres. This is all R code: # Randomly sampled bins: "red", "blue" dat <- data.frame(date=seq(as.Date("2009-01-01"), by="days", length.out=20)) dat$bin <- factor(sample(c("red","blue"), 10, replace=TRUE, prob=c(0.4,0.6))) # Determine where the rows are different; 1=different rows, 0=same rows dat$breaks <- ifelse(dat$bin != c(TRUE, as.character(dat$bin[-nrow(dat)])), 1, 0) # Determine where the continuous parts are: dat$part <- factor(cumsum(dat$breaks)) # Results vary due to random sampling print(dat) ... and on the SQL side, simple aggregates like min(), max(etc) can be used with "GROUP BY part" to determine the start/end dates, length of duration, etc. -Mike -- Sent via pgsql-general mailing list (pgsql-general@postgresql.org) To make changes to your subscription: http://www.postgresql.org/mailpref/pgsql-general
Re: [GENERAL] Select ranges based on sequential breaks
On Mon, Jun 22, 2009 at 12:41 PM, Mike Toews wrote: > Hi Joel, > An excellent book that I recently stumbled on is /Joe Celko's SQL for > Smarties/ (recommended by someone on this list), which is a heavy read, but > has an amazing depth to ANSI SQL. Of particular interest is "Chapter 24: > Regions, Runs, Gaps, Sequences, and Series". I'm slowly digesting this. I need to buy a fourth or fifth copy (they keep growing feet / I keep forgetting who I loaned them to) of that book. -- Sent via pgsql-general mailing list (pgsql-general@postgresql.org) To make changes to your subscription: http://www.postgresql.org/mailpref/pgsql-general
Re: [GENERAL] Select ranges based on sequential breaks
Hi Joel, Window functions appear to be the best solution for this style of problem, and I'm looking forward to their applications. However, I'm sticking with 8.3 for at least a year, so I'm not able to explore this solution yet. For now, I can only post-process the output in a non-SQL environment. I also need to do other fun stuff, like cumulative sums, which is also challenging with SQL, but much easier and intuitive with R. An excellent book that I recently stumbled on is /Joe Celko's SQL for Smarties/ (recommended by someone on this list), which is a heavy read, but has an amazing depth to ANSI SQL. Of particular interest is "Chapter 24: Regions, Runs, Gaps, Sequences, and Series". I'm slowly digesting this. -Mike Joel Nothman wrote: Hi Mike, I happened upon your query, which is related to some stuff I've been playing with. Firstly, David's solution below doesn't work. I haven't yet tried to work out why. Secondly, I was hoping to be able to solve your problem nicely with Postgres 8.4's window functions [1,2], which can provide access to data in sequentially-related rows. Given the following setup: CREATE TABLE foo (dt date, bin varchar(4)); INSERT INTO foo VALUES ('2009-01-01', 'red'), ('2009-01-02', 'red'), ('2009-01-03', 'blue'), ('2009-01-04', 'blue'), ('2009-01-05', 'blue'), ('2009-01-06', 'red'), ('2009-01-07', 'blue'), ('2009-01-08', 'blue'), ('2009-01-09', 'red'), ('2009-01-10', 'red'); I had hoped the following would suffice: SELECT first_value(dt) OVER w, last_value(dt) OVER w, bin FROM foo WINDOW w AS (ORDER BY dt PARTITION BY bin); Apparently, this is bad syntax. ORDER BY cannot precede PARTITION BY in a WINDOW declaration, and yet I wanted a grouping of date-consecutive bins, which (PARTITION BY bin ORDER BY dt) would not give me. I was able to produce the required result with: SELECT MIN(dt) AS first, MAX(dt) AS last, MAX(bin) AS bin FROM (SELECT dt, bin, CASE WHEN newbin IS NULL THEN 0 ELSE SUM(newbin) OVER (ORDER BY dt) END AS binno FROM (SELECT *, (bin !=3D lag(bin, 1) OVER (ORDER BY dt))::int A= S newbin FROM foo ) AS newbins ) AS binnos GROUP BY binno ORDER BY first; This relies on a middle step in which I create an enumeration of the bins in sequence: SELECT dt, bin, CASE WHEN newbin IS NULL THEN 0 ELSE sum(newbin) OVER (ORDER BY dt) END AS binno FROM (SELECT *, (bin !=3D lag(bin, 1) OVER (ORDE= R BY dt))::int AS newbin FROM foo) AS newbins; dt | bin | binno +--+--- 2009-01-01 | red | 0 2009-01-02 | red | 0 2009-01-03 | blue | 1 2009-01-04 | blue | 1 2009-01-05 | blue | 1 2009-01-06 | red | 2 2009-01-07 | blue | 3 2009-01-08 | blue | 3 2009-01-09 | red | 4 2009-01-10 | red | 4 I would hope there is a neater way to do this with window functions. The best way to solve your problem may be with PL/SQL, which is also good at dealing with sequences (it's not as complicated as it looks!): CREATE TYPE bindates AS (first date, last date, bin varchar(5)); CREATE OR REPLACE FUNCTION bindates() RETURNS SETOF bindates AS $$ DECLARE row record; res bindates; BEGIN FOR row IN SELECT * FROM foo ORDER BY dt LOOP IF res.bin IS NULL OR res.bin !=3D row.bin THEN IF res.bin IS NOT NULL THEN RETURN NEXT res; END IF; res.bin :=3D row.bin; res.first :=3D row.dt; END IF; res.last :=3D row.dt; END LOOP; IF res.bin IS NOT NULL THEN RETURN NEXT res; END IF; END; $$ LANGUAGE plpgsql; Finally, I'll try to sort out David's solution. Once we correct his typo (t1.order -> t1.date) and add an 'ORDER BY first' to the end, we get: first|last| bin ++-- 2009-01-03 | 2009-01-05 | blue 2009-01-06 | 2009-01-06 | red 2009-01-07 | 2009-01-08 | blue 2009-01-09 || red This includes correct output, but it fails on both edge cases. The non-appearance of the first row is due to the WHERE clause on the main SELECT statement: WHERE (SELECT bin FROM foo t2 WHERE t2.dt < t1.dt ORDER BY dt DESC LIMIT 1) <> t1.bin If we drop this WHERE clause, we get: first|last| bin ++-- 2009-01-01 | 2009-01-02 | red 2009-01-02 | 2009-01-02 | red 2009-01-03 | 2009-01-05 | blue 2009-01-04 | 2009-01-05 | blue 2009-01-05 | 2009-01-05 | blue 2009-01-06 | 2009-01-06 | red 2009-01-07 | 2009-01-08 | blue 2009-01-08 | 2009-01-08 | blue 2009-01-09 || red 2009-01-10 || red We can therefore get the result including the first row by selecting from this table with 'GROUP BY last,
Re: [GENERAL] Select ranges based on sequential breaks
On Mon, Jun 15, 2009 at 12:23 PM, Mike Toews wrote: > This is easy to compute using a spreadsheet or in R, but how would I do this > with SQL? I'm using 8.3. Advice is appreciated. FYI (and I'm no expert in this area) R is available as a pl for postgres, look for pl/R or plR -- Sent via pgsql-general mailing list (pgsql-general@postgresql.org) To make changes to your subscription: http://www.postgresql.org/mailpref/pgsql-general
Re: [GENERAL] Select ranges based on sequential breaks
Hi Mike, I happened upon your query, which is related to some stuff I've been playing with. Firstly, David's solution below doesn't work. I haven't yet tried to work out why. Secondly, I was hoping to be able to solve your problem nicely with Postgres 8.4's window functions [1,2], which provide functions relating. Given the following setup: CREATE TABLE foo (dt date, bin varchar(4)); INSERT INTO foo VALUES ('2009-01-01', 'red'), ('2009-01-02', 'red'), ('2009-01-03', 'blue'), ('2009-01-04', 'blue'), ('2009-01-05', 'blue'), ('2009-01-06', 'red'), ('2009-01-07', 'blue'), ('2009-01-08', 'blue'), ('2009-01-09', 'red'), ('2009-01-10', 'red'); I had hoped the following would suffice: SELECT first_value(dt) OVER w, last_value(dt) OVER w, bin FROM foo WINDOW w AS (ORDER BY dt PARTITION BY bin); Apparently, this is bad syntax. ORDER BY cannot precede PARTITION BY in a WINDOW declaration, and yet I wanted a grouping of date-consecutive bins, which (PARTITION BY bin ORDER BY dt) would not give me. I was able to produce the required result with: SELECT MIN(dt) AS first, MAX(dt) AS last, MAX(bin) AS bin FROM (SELECT dt, bin, CASE WHEN newbin IS NULL THEN 0 ELSE SUM(newbin) OVER (ORDER BY dt) END AS binno FROM (SELECT *, (bin != lag(bin, 1) OVER (ORDER BY dt))::int AS newbin FROM foo ) AS newbins ) AS binnos GROUP BY binno ORDER BY first; This relies on a middle step in which I create an enumeration of the bins in sequence: SELECT dt, bin, CASE WHEN newbin IS NULL THEN 0 ELSE sum(newbin) OVER (ORDER BY dt) END AS binno FROM (SELECT *, (bin != lag(bin, 1) OVER (ORDER BY dt))::int AS newbin FROM foo) AS newbins; dt | bin | binno +--+--- 2009-01-01 | red | 0 2009-01-02 | red | 0 2009-01-03 | blue | 1 2009-01-04 | blue | 1 2009-01-05 | blue | 1 2009-01-06 | red | 2 2009-01-07 | blue | 3 2009-01-08 | blue | 3 2009-01-09 | red | 4 2009-01-10 | red | 4 I would hope there is a neater way to do this with window functions. The best way to solve your problem may be with PL/SQL, which is also good at dealing with sequences (it's not as complicated as it looks!): CREATE TYPE bindates AS (first date, last date, bin varchar(5)); CREATE OR REPLACE FUNCTION bindates() RETURNS SETOF bindates AS $$ DECLARE row record; res bindates; BEGIN FOR row IN SELECT * FROM foo ORDER BY dt LOOP IF res.bin IS NULL OR res.bin != row.bin THEN IF res.bin IS NOT NULL THEN RETURN NEXT res; END IF; res.bin := row.bin; res.first := row.dt; END IF; res.last := row.dt; END LOOP; IF res.bin IS NOT NULL THEN RETURN NEXT res; END IF; END; $$ LANGUAGE plpgsql; Finally, I'll try to sort out David's solution. Once we correct his typo (t1.order -> t1.date) and add an 'ORDER BY first' to the end, we get: first|last| bin ++-- 2009-01-03 | 2009-01-05 | blue 2009-01-06 | 2009-01-06 | red 2009-01-07 | 2009-01-08 | blue 2009-01-09 || red This includes correct output, but it fails on both edge cases. The non-appearance of the first row is due to the WHERE clause on the main SELECT statement: WHERE (SELECT bin FROM foo t2 WHERE t2.dt < t1.dt ORDER BY dt DESC LIMIT 1) <> t1.bin If we drop this WHERE clause, we get: first|last| bin ++-- 2009-01-01 | 2009-01-02 | red 2009-01-02 | 2009-01-02 | red 2009-01-03 | 2009-01-05 | blue 2009-01-04 | 2009-01-05 | blue 2009-01-05 | 2009-01-05 | blue 2009-01-06 | 2009-01-06 | red 2009-01-07 | 2009-01-08 | blue 2009-01-08 | 2009-01-08 | blue 2009-01-09 || red 2009-01-10 || red We can therefore get the result including the first row by selecting from this table with 'GROUP BY last, bin'. And we can hack in a value for those final NULLs as a special case. The following statement works: SELECT MIN(first), CASE WHEN last IS NULL THEN (SELECT MAX(dt) FROM foo) ELSE last END, bin FROM ( SELECT dt AS first, (SELECT dt FROM foo t3 WHERE t3.dt < ( SELECT dt FROM foo t5 WHERE t5.dt > t1.dt AND t5.bin <> t1.bin ORDER BY dt ASC LIMIT 1) ORDER BY dt DESC LIMIT 1) AS last, bin FROM foo t1 ) t0 GROUP BY last, bin ORDER BY last; Finally, what's efficient? With 1,000,000 random rows, we get: Enumeration: 13s PL/SQL: 12s Modified David: minutes. [I used the f
Re: [GENERAL] Select ranges based on sequential breaks
Hi Mike, I happened upon your query, which is related to some stuff I've been playing with. Firstly, David's solution below doesn't work. I haven't yet tried to work out why. Secondly, I was hoping to be able to solve your problem nicely with Postgres 8.4's window functions [1,2], which can provide access to data in sequentially-related rows. Given the following setup: CREATE TABLE foo (dt date, bin varchar(4)); INSERT INTO foo VALUES ('2009-01-01', 'red'), ('2009-01-02', 'red'), ('2009-01-03', 'blue'), ('2009-01-04', 'blue'), ('2009-01-05', 'blue'), ('2009-01-06', 'red'), ('2009-01-07', 'blue'), ('2009-01-08', 'blue'), ('2009-01-09', 'red'), ('2009-01-10', 'red'); I had hoped the following would suffice: SELECT first_value(dt) OVER w, last_value(dt) OVER w, bin FROM foo WINDOW w AS (ORDER BY dt PARTITION BY bin); Apparently, this is bad syntax. ORDER BY cannot precede PARTITION BY in a WINDOW declaration, and yet I wanted a grouping of date-consecutive bins, which (PARTITION BY bin ORDER BY dt) would not give me. I was able to produce the required result with: SELECT MIN(dt) AS first, MAX(dt) AS last, MAX(bin) AS bin FROM (SELECT dt, bin, CASE WHEN newbin IS NULL THEN 0 ELSE SUM(newbin) OVER (ORDER BY dt) END AS binno FROM (SELECT *, (bin !=3D lag(bin, 1) OVER (ORDER BY dt))::int A= S newbin FROM foo ) AS newbins ) AS binnos GROUP BY binno ORDER BY first; This relies on a middle step in which I create an enumeration of the bins in sequence: SELECT dt, bin, CASE WHEN newbin IS NULL THEN 0 ELSE sum(newbin) OVER (ORDER BY dt) END AS binno FROM (SELECT *, (bin !=3D lag(bin, 1) OVER (ORDE= R BY dt))::int AS newbin FROM foo) AS newbins; dt | bin | binno +--+--- 2009-01-01 | red | 0 2009-01-02 | red | 0 2009-01-03 | blue | 1 2009-01-04 | blue | 1 2009-01-05 | blue | 1 2009-01-06 | red | 2 2009-01-07 | blue | 3 2009-01-08 | blue | 3 2009-01-09 | red | 4 2009-01-10 | red | 4 I would hope there is a neater way to do this with window functions. The best way to solve your problem may be with PL/SQL, which is also good at dealing with sequences (it's not as complicated as it looks!): CREATE TYPE bindates AS (first date, last date, bin varchar(5)); CREATE OR REPLACE FUNCTION bindates() RETURNS SETOF bindates AS $$ DECLARE row record; res bindates; BEGIN FOR row IN SELECT * FROM foo ORDER BY dt LOOP IF res.bin IS NULL OR res.bin !=3D row.bin THEN IF res.bin IS NOT NULL THEN RETURN NEXT res; END IF; res.bin :=3D row.bin; res.first :=3D row.dt; END IF; res.last :=3D row.dt; END LOOP; IF res.bin IS NOT NULL THEN RETURN NEXT res; END IF; END; $$ LANGUAGE plpgsql; Finally, I'll try to sort out David's solution. Once we correct his typo (t1.order -> t1.date) and add an 'ORDER BY first' to the end, we get: first|last| bin ++-- 2009-01-03 | 2009-01-05 | blue 2009-01-06 | 2009-01-06 | red 2009-01-07 | 2009-01-08 | blue 2009-01-09 || red This includes correct output, but it fails on both edge cases. The non-appearance of the first row is due to the WHERE clause on the main SELECT statement: WHERE (SELECT bin FROM foo t2 WHERE t2.dt < t1.dt ORDER BY dt DESC LIMIT 1) <> t1.bin If we drop this WHERE clause, we get: first|last| bin ++-- 2009-01-01 | 2009-01-02 | red 2009-01-02 | 2009-01-02 | red 2009-01-03 | 2009-01-05 | blue 2009-01-04 | 2009-01-05 | blue 2009-01-05 | 2009-01-05 | blue 2009-01-06 | 2009-01-06 | red 2009-01-07 | 2009-01-08 | blue 2009-01-08 | 2009-01-08 | blue 2009-01-09 || red 2009-01-10 || red We can therefore get the result including the first row by selecting from this table with 'GROUP BY last, bin'. And we can hack in a value for those final NULLs as a special case. The following statement works: SELECT MIN(first), CASE WHEN last IS NULL THEN (SELECT MAX(dt) FROM foo) ELSE last END, bin FROM ( SELECT dt AS first, (SELECT dt FROM foo t3 WHERE t3.dt < ( SELECT dt FROM foo t5 WHERE t5.dt > t1.dt AND t5.bin <> t1.bin ORDER BY dt ASC LIMIT 1) ORDER BY dt DESC LIMIT 1) AS last, bin FROM foo t1 ) t0 GROUP BY last, bin ORDER BY last; Finally, what's efficient? With 1,000,000 random rows, we get: Enumeration: 13s PL/SQL
Re: [GENERAL] Select ranges based on sequential breaks
On Mon, Jun 15, 2009 at 2:23 PM, Mike Toews wrote: > Hi, > > I'm having difficulty constructing a query that will find breaks where data > change in a time-series. I've done some searching for this too, but I > haven't found anything. > > Here is my example situation, consider my source table: > date bin > 2009-01-01 red > 2009-01-02 red > 2009-01-03 blue > 2009-01-04 blue > 2009-01-05 blue > 2009-01-06 red > 2009-01-07 blue > 2009-01-08 blue > 2009-01-09 red > 2009-01-10 red > > > I would like to get the first and last of each consecutive series based on > column "bin". My result for the table would look like: > first last bin > 2009-01-01 2009-01-02 red > 2009-01-03 2009-01-05 blue > 2009-01-06 2009-01-06 red > 2009-01-07 2009-01-08 blue > 2009-01-09 2009-01-10 red > > > This is easy to compute using a spreadsheet or in R, but how would I do this > with SQL? I'm using 8.3. Advice is appreciated. (Written in email and untested- also, someone will probably provide a better way, I hope, but this should at least work) select date as first, (select date from table t3 where t3.date<(select date from table t5 where t5.date>t1.date and t5.bin<>t1.bin order by date asc limit 1) order by date desc limit 1) as last, bin from table t1 where (select bin from table t2 where t2.datet1.bin; Ugly, and I'm pretty sure there's a much better way, but my brain is failing me right now- hopefully this'll at least get you started, though. -- - David T. Wilson david.t.wil...@gmail.com -- Sent via pgsql-general mailing list (pgsql-general@postgresql.org) To make changes to your subscription: http://www.postgresql.org/mailpref/pgsql-general
[GENERAL] Select ranges based on sequential breaks
Hi, I'm having difficulty constructing a query that will find breaks where data change in a time-series. I've done some searching for this too, but I haven't found anything. Here is my example situation, consider my source table: datebin 2009-01-01 red 2009-01-02 red 2009-01-03 blue 2009-01-04 blue 2009-01-05 blue 2009-01-06 red 2009-01-07 blue 2009-01-08 blue 2009-01-09 red 2009-01-10 red I would like to get the first and last of each consecutive series based on column "bin". My result for the table would look like: first lastbin 2009-01-01 2009-01-02 red 2009-01-03 2009-01-05 blue 2009-01-06 2009-01-06 red 2009-01-07 2009-01-08 blue 2009-01-09 2009-01-10 red This is easy to compute using a spreadsheet or in R, but how would I do this with SQL? I'm using 8.3. Advice is appreciated. Thanks, -Mike -- Sent via pgsql-general mailing list (pgsql-general@postgresql.org) To make changes to your subscription: http://www.postgresql.org/mailpref/pgsql-general