Given a table of dates and values.
How to average by day of week?
I found AVERAGEIF to average and TEXT(E4, "dddd") to convert to day of week.
But how to combine those two functions?
Date
Value
1/1/2001
1
1/2/2001
2
1/3/2001
3
1/4/2001
3
1/5/2001
6
1/6/2001
3
Day of Week
Average
Sunday
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
Go with QUERY() function. Try-
=QUERY({INDEX(TEXT(A1:A15,"dddd")),B1:B15},
"select Col1, avg(Col2) group by Col1 label Col1 'Day', avg(Col2) 'Average'")
To make it dynamic, use-
=QUERY({INDEX(TEXT(TOCOL(A1:A,1),"dddd")),TOCOL(B1:B,1)},
"select Col1, avg(Col2) group by Col1 label Col1 'Day', avg(Col2) 'Average'")
Related
In PostgreSQL (I'm on version 9.6.6), what's the simplest way to get the week number, starting on Sunday?
DATE_PART('week',x) returns:
The number of the ISO 8601 week-numbering week of the year. By definition, ISO weeks start on Mondays and the first week of a year contains January 4 of that year. In other words, the first Thursday of a year is in week 1 of that year. (doc)
Say my query is like:
WITH dates as (SELECT generate_series(timestamp '2014-01-01',
timestamp '2014-01-31',
interval '1 day'
)::date AS date
)
SELECT
date,
TO_CHAR(date,'Day') AS dayname,
DATE_PART('week',date) AS weekofyear
FROM dates
Returns:
date dayname weekofyear
--------------------------------
2014-01-01 Wednesday 1
2014-01-02 Thursday 1
2014-01-03 Friday 1
2014-01-04 Saturday 1
2014-01-05 Sunday 1 <- I want this to be 2
2014-01-06 Monday 2
2014-01-07 Tuesday 2
2014-01-08 Wednesday 2
So far I have tried:
SELECT
date,
TO_CHAR(date,'Day') AS dayname,
DATE_PART('week',date) AS week_iso,
DATE_PART('week',date + interval '1 day') AS week_alt
FROM dates
which won't quite work if the year begins on a Sunday.
Also, I want week 1 to contain January 1 of that year. So if January 1 is a Saturday, I want week 1 to be one day long (instead of being week 53 in the ISO style). This behavior is consistent with the Excel WEEKNUM function.
To get the week number of the year, with weeks starting on Sunday, we need to know how many Sundays between the first day of the year and the target date.
I adapted the solution here by #Erwin Brandstetter. This solution counts Sundays inclusive of the first day of the year and exclusive of the target date.
Then, because I want the first (partial) week to be week one (not zero), I need to add 1 unless the first day of the year is a Sunday (in which case it's already week one).
WITH dates as (SELECT generate_series(timestamp '2014-01-01',
timestamp '2014-01-31',
interval '1 day'
)::date AS date
)
SELECT
date,
TO_CHAR(date,'Day') AS dayname,
DATE_PART('week',date) AS week_iso,
((date - DATE_TRUNC('year',date)::date) + DATE_PART('isodow', DATE_TRUNC('year',date)) )::int / 7
+ CASE WHEN DATE_PART('isodow', DATE_TRUNC('year',date)) = 7 THEN 0 ELSE 1 END
AS week_sundays
FROM dates
Returns
date dayname weekofyear week_sundays
--------------------------------
2014-01-01 Wednesday 1 1
2014-01-02 Thursday 1 1
2014-01-03 Friday 1 1
2014-01-04 Saturday 1 1
2014-01-05 Sunday 1 2
2014-01-06 Monday 2 2
2014-01-07 Tuesday 2 2
To show how this works for years starting on Sunday:
2017-01-01 Sunday 52 1
2017-01-02 Monday 1 1
2017-01-03 Tuesday 1 1
2017-01-04 Wednesday 1 1
2017-01-05 Thursday 1 1
2017-01-06 Friday 1 1
2017-01-07 Saturday 1 1
2017-01-08 Sunday 1 2
The task is not as daunting as it first appears. It mainly requires finding the first Sun on or after the 1-Jan. That date becomes the last day of the first week. From there calculation of subsequent weeks is merely. a matter of addition. The other significant point is with week definition there will always be 53 week per year and the last day of the last week is 31-Dec. The following generates an annual calendar for this week definition.
create or replace function non_standard_cal(year_in integer)
returns table (week_number integer, first_day_of_week date, last_day_of_week date)
language sql immutable leakproof strict rows 53
as $$
with recursive cal as
(select 1 wk, d1 start_of_week, ds end_of_week, de stop_date
from (select d1+substring( '0654321'
, extract(dow from d1)::integer+1
, 1)::integer ds
, d1, de
from ( select make_date (year_in, 1,1) d1
, make_date (year_in+1, 1,1) -1 de
) a
) b
union all
select wk+1, end_of_week+1, case when end_of_week+7 > stop_date
then stop_date
else end_of_week+7
end
, stop_date
from cal
where wk < 53
)
select wk, start_of_week, end_of_week from cal;
$$ ;
As a general rule I avoid magic numbers, but sometimes they're useful; as in this case. In magic number (actually a string) '0654321' each digit represents the number of days needed to reach the first Mon on or after 1-Jan when indexed by the standard day numbering system (0-6 as Sun-Sat). The result is the Mon being the last day of the first week. That generatess the 1st row of the recursive CTE. The remaining rows just add the appropriate number days for each week until the 53 weeks have been generated. The following shows the years needed to ensure each day of week gets it's turn to 1-Jan (yea some days duplicate). Run individual years to validate its calendar.
do $$
declare
cal record;
yr_cal cursor (yr integer) for
select * from non_standard_cal(2000+yr) limit 1;
begin
for yr in 18 .. 26
loop
open yr_cal(yr);
fetch yr_cal into cal;
raise notice 'For Year: %, week: %, first_day: %, Last_day: %, First day is: %'
, 2000+yr
,cal.week_number
,cal.first_day_of_week
,cal.last_day_of_week
,to_char(cal.first_day_of_week, 'Day');
close yr_cal;
end loop;
end; $$;
Following may work - tested with two cases in mind:
WITH dates as (SELECT generate_series(timestamp '2014-01-01',
timestamp '2014-01-10',
interval '1 day'
)::date AS date
union
SELECT generate_series(timestamp '2017-01-01',
timestamp '2017-01-10',
interval '1 day'
)::date AS date
)
, alt as (
SELECT
date,
TO_CHAR(date,'Day') AS dayname,
DATE_PART('week',date) AS week_iso,
DATE_PART('week',date + interval '1 day') AS week_alt
FROM dates
)
select date, dayname,
week_iso, week_alt, case when week_alt <> week_iso
then week_alt
else week_iso end as expected_week
from alt
order by date
Output:
date dayname week_iso week_alt expected_week
2014-01-01 Wednesday 1 1 1
2014-01-02 Thursday 1 1 1
2014-01-03 Friday 1 1 1
2014-01-04 Saturday 1 1 1
2014-01-05 Sunday 1 2 2
2014-01-06 Monday 2 2 2
2014-01-07 Tuesday 2 2 2
....
2017-01-01 Sunday 52 1 1
2017-01-02 Monday 1 1 1
2017-01-03 Tuesday 1 1 1
2017-01-04 Wednesday 1 1 1
2017-01-05 Thursday 1 1 1
2017-01-06 Friday 1 1 1
2017-01-07 Saturday 1 1 1
2017-01-08 Sunday 1 2 2
This query works perfectly replacing monday with sunday as the start of the week.
QUERY
SELECT CASE WHEN EXTRACT(day from '2014-01-05'::date)=4 AND
EXTRACT(month from '2014-01-05'::date)=1 THEN date_part('week',
'2014-01-05'::date) ELSE date_part('week', '2014-01-05'::date + 1)
END;
OUTPUT
date_part
-----------
2
(1 row)
I don't have timestamp table. I just need to convert using name string,
SELECT day_of_week;
Output should look like
day_of_week sum
Friday 5
Monday 1
Saturday 6
Sunday 7
Thursday 4
Tuesday 2
Wednesday 3
If it were timestamp then you can use the isodow. See doc
isodow
The day of the week as Monday(1) to Sunday(7)
SELECT day_of_week, EXTRACT(ISODOW FROM TIMESTAMP '2001-02-18 20:38:40') as sum FROM your_table;
But you don't have that column. So you can use CASE...WHEN like below-
SELECT day_of_week,
CASE WHEN day_of_week='Monday' THEN 1
WHEN day_of_week='Tuesday' THEN 2
WHEN day_of_week='Wednesday' THEN 3
WHEN day_of_week='Thursday' THEN 4
WHEN day_of_week='Friday' THEN 5
WHEN day_of_week='Saturday' THEN 6
ELSE 7
END as sum
FROM your_table;
See doc: https://www.postgresql.org/docs/8.4/functions-conditional.html
I am trying to write a query which gives the number of days in each month between two specified dates.
Example:
date 1: 2018-01-01
date 2: 2018-05-23
Expected Output:
month days
2018-01-01, 31
2018-02-01, 28
2018-03-01, 31
2018-04-01, 30
2018-05-01, 23
Use generate_series and group by date_trunc
SELECT date_trunc('month',dt) AS month,
COUNT(*) as days
FROM generate_series( DATE '2018-01-01',DATE '2018-05-23',interval '1 DAY' )
as dt group by date_trunc('month',dt)
order by month;
Demo
I've got date/ duration data, e.g.:
16-Apr (Mon) 30mins
16-Apr (Mon) 90mins
17 Apr (Tue) 60 mins
19-Apr (thu) 15 mins
19-Apr (thu) 20 mins
19-Apr (thu) 20 mins
21-April (Sat ) 120 mins
23-April (Mon) 60 mins
24-Apr (Tue)15 mins
I want to produce an average duration per weekday
A pivot table with date can give me a sum of the durations for each date.
A pivot table on (derived) Day of the week gives me an average for the number of entries for that day of the week, e.g. in the above, Monday = 3 entries and I want an average from 2 (dates).
What I want is Mondays = x mins, Tues= y mins, Wed =z mins, etc.
Can this be done in one step with a pivot table or array formula?
Suppose you have your dates in column E, and durations in F
This formula will give you a summary by day of week:
=query({arrayformula(weekday(E2:E7)), arrayformula(n(F2:F7))},
"select Col1, sum(Col2) group by Col1")
To get an average, use avg(Col2) instead of sum(Col2). You still have to format the second result column as Duration, and find a way to convert the days of week from numeric to text, but that can be left as an exercise to the reader :-)
You could do this to add labelling
=ArrayFormula(query({hlookup(weekday(A:A),{1,2,3,4,5,6,7;"Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"},2,false),B:B},"select Col1,avg(Col2) group by Col1 label Col1 'Day'"))
The day numbers in column C are just for checking.
If any of the dates are missing, you need to ignore them otherwise they will all be treated as Saturdays...
=ArrayFormula(query({A:A,hlookup(weekday(A:A),{1,2,3,4,5,6,7;"Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"},2,false),B:B},"select Col2,avg(Col3) where Col1 is not null group by Col2 label Col2 'Day'"))
EDIT
OK this is an answer for the actual requirement which is for sum of time durations for each weekday but divided by number of unique dates which fall on that weekday.
=ArrayFormula(query({query({C2:C,weekday(C2:C),choose(weekday(C2:C),"Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"),E2:E},"select Col3,sum(Col4) where Col1 is not null group by Col2,Col3"),
query({unique(C2:C),weekday(unique(C2:C))},"select count(Col2) where Col1 is not null group by Col2")},"Select Col1,Col2/Col3 label Col1 'Day',Col2/Col3 'Average'"))
Notes
(1) The number of groups based on unique dates is exactly the same as the number of groups based on dates with duplicates.
(2) To get the day names in the correct order (Sunday, Monday, Tuesday...) I grouped on weekday number (1-7) then weekday name (Col2,Col3 in the first inner query). This doesn't create any more groups than just using Col3, but has the effect of putting the days in weekday number order instead of weekday name (alphabetical) order while still allowing you to put Col3 (weekday name) in the select list.
(3) Have included #ttarchala's recommendation of using Choose rather than Hlookup.
I have a sales table and want to get the day of week for the orders and the number of orders placed on each day of the week. Everything seems to work but no matter what I try the 'week' is non standard. I've tried SET DATEFIRST 1; but still do not get the results I want.
SELECT DATENAME(Weekday,orderdate) AS Weekday, COUNT(orderid) AS NumOrders
FROM Sales.Orders
GROUP BY DATENAME(Weekday,orderdate);
The results:
Weekday | NumOrders
1. Wednesday 25
2. Saturday 33
3. Monday 100
4. Sunday 115
5. Thursday 87
6. Tuesday 42
Could the no orders for Friday be causing the output not to order by start of week? Thanks.
I think you just need an ORDER BY:
SELECT DATENAME(Weekday, orderdate) AS Weekday, COUNT(orderid) AS NumOrders
FROM Sales.Orders
GROUP BY DATENAME(Weekday, orderdate), DATEPART(Weekday, orderdate)
ORDER BY DATEPART(Weekday, orderdate);