Get week number with week start day different than monday - Python
Asked Answered
S

5

11

I have a dataset with a date column. I want to get the week number associated with each date. I know I can use:

x['date'].isocalendar()[1]

But it gives me the week num with start day = monday. While I need the week to start on a friday.

How do you suggest I go about doing that?

Swirl answered 23/3, 2020 at 15:19 Comment(5)
Add three days to the date, then get the week number? (Hm, this way the week numbers may be off by one if the year starts on a saturday or something like that, though.)Selfsacrifice
As per ISO standard 8601 and ISO standard 2015, an ISO week has Thursday as the middle of the week. So with that you can't really change the day. With an approach like @Selfsacrifice suggests it might work, but it would not be a real ISO week anymore.Tenney
The calendar module allows to set the firstweekday but TBH I did not get how to go from a datetime date to this sort of calendar...Selfsacrifice
yes i tried to set the firstweek day with calendar but it doesn't change anything the results of my codeSwirl
Or add days until you are on friday, then get the day of the year (using strptime and %j if there is no direct way) and divide by 7?Selfsacrifice
B
10

tl;dr

The sections "ISO Standard" and "What you want" is to clarify your need.

You could just copy paste the code in the section "Solution" and see if the result is what you want.


ISO Standard

Definition

  • Weeks start with Monday.
  • Each week's year is the Gregorian year in which the Thursday falls.

Result of Python Standard Library datetime

>>> datetime(2020, 1, 1).isocalendar()
(2020, 1, 3)  # The 3rd day of the 1st week in 2020
>>> datetime(2019, 12, 31).isocalendar()
(2020, 1, 2)  # The 2nd day of the 1st week in 2020
>>> datetime(2019, 1, 1).isocalendar()
(2019, 1, 2)
>>> datetime(2017, 1, 1).isocalendar()
(2016, 52, 7)
>>> datetime(2016, 12, 26).isocalendar()
(2016, 52, 1)
>>> datetime(2015, 12, 31).isocalendar()
(2015, 53, 4)
>>> datetime(2016, 1, 1).isocalendar()
(2015, 53, 5)

Calendar Sketch

#                 Mo Tu Wd Th Fr Sa Sn
# [2019-52w] DEC/ 23 24 25 26 27 28 29 /DEC
# [2020-1w]  DEC/ 30 31  1  2  3  4  5 /JAN

# [2019-1w]  DEC/ 31  1  2  3  4  5  6 /JAN

# [2016-52w] DEC/ 26 27 28 29 30 31  1 /JAN

# [2015-53w] DEC/ 28 29 30 31  1  2  3 /JAN
# [2016-1w]  JAN/  4  5  6  7  8  9 10 /JAN 

What You Want

Definition

  • Weeks start with Friday.
  • Each week's year is the Gregorian year in which the Monday falls.

Calendar Sketch

#                 Fr Sa Sn. Mo Tu Wd Th 
# [2019-51w] DEC/ 20 21 22. 23 24 25 26  /DEC
# [2019-52w] DEC/ 27 28 29. 30 31  1  2  /JAN
# [2020-1w]  JAN/  3  4  5.  6  7  8  9  /JAN

# [2018-53w] DEC/ 28 29 30. 31  1  2  3  /JAN
# [2019-1w]  JAN/  4  5  6.  7  8  9 10  /JAN

# [2016-52w] DEC/ 23 24 25. 26 27 28 29  /DEC
# [2017-1w]  DEC/ 30 31  1.  2  3  4  5  /JAN

# [2015-52w] DEC/ 25 26 27. 28 29 30 31  /DEC
# [2016-1w]  JAN/  1  2  3.  4  5  6  7  /JAN 

Solution

from datetime import datetime, timedelta
from enum import IntEnum

WEEKDAY = IntEnum('WEEKDAY', 'MON TUE WED THU FRI SAT SUN', start=1)

class CustomizedCalendar:

    def __init__(self, start_weekday, indicator_weekday=None):
        self.start_weekday = start_weekday
        self.indicator_delta = 3 if not (indicator_weekday) else (indicator_weekday - start_weekday) % 7

    def get_week_start(self, date):
        delta = date.isoweekday() - self.start_weekday
        return date - timedelta(days=delta % 7)

    def get_week_indicator(self, date):
        week_start = self.get_week_start(date)
        return week_start + timedelta(days=self.indicator_delta)

    def get_first_week(self, year):
        indicator_date = self.get_week_indicator(datetime(year, 1, 1))
        if indicator_date.year == year:  # The date "year.1.1" is on 1st week.
            return self.get_week_start(datetime(year, 1, 1))
        else:  # The date "year.1.1" is on the last week of "year-1".
            return self.get_week_start(datetime(year, 1, 8))
    
    def calculate(self, date):
        year = self.get_week_indicator(date).year
        first_date_of_first_week = self.get_first_week(year)
        diff_days = (date - first_date_of_first_week).days
        return year, (diff_days // 7 + 1), (diff_days % 7 + 1)

if __name__ == '__main__':
    # Use like this:
    my_calendar = CustomizedCalendar(start_weekday=WEEKDAY.FRI, indicator_weekday=WEEKDAY.MON)
    print(my_calendar.calculate(datetime(2020, 1, 2)))

To Test

We could simply initialize CustomizedCalendar with original ISO settings, and verify if the outcome is the same with original isocalendar()'s result.

my_calendar = CustomizedCalendar(start_weekday=WEEKDAY.MON)
s = datetime(2019, 12, 19)
for delta in range(20):
    print my_calendar.calculate(s) == s.isocalendar()
    s += timedelta(days=1)
Boondoggle answered 26/8, 2020 at 11:21 Comment(4)
Thank you much! If I execute your code, I get: my_calendar.calculate(datetime(2019, 1, 1)) gives me (2018, 53.57142857142857, 5) - is this expected?Recollection
Ok I get it: np.floor(my_calendar.calculate(datetime(2019, 12, 28))[1]) is giving me the week-number as int. Each day which is not a Friday is part of a week belonging to the year where the previous Friday belonged to. Each Friday belongs to a week which belongs to a year which that Friday is part of - let me know if this assessment is wrongRecollection
Hi @Recollection the code was originally written in Py2, so the result would be a floating number if you use Py3 to execute it. I have modified the code to Py3 compatible and it should be an integer now. Please try it again.Boondoggle
I'm not sure if I perfectly understand your assessment. We could use the case 2017/1/1 to verify. If the week starts from Friday, the week of 2017/1/1 is 2016/12/30(Fri) ~ 2017/1/5(Thu). If the indicator_weekday is set to Monday, then because that week's Monday is *2017*/1/2, so 2017/1/1 would be at the 1st week of 2017; if the indicator_weekday is set to Friday, then because that week's Friday is *2016*/12/30, so 2017/1/1 would be at the last week of 2016.Boondoggle
I
2

Here's the minimal logic:

You just need to add 3 days to a Monday to get to a Thursday. Just add the days to Monday and call the ISO Weeknumber. You'll get the shifted weeknumber.

from datetime import datetime, timedelta

x = datetime(2020, 1, 2) # this is Thursday and week 1 in ISO calendar; should be 1 in custom calendar w/ week starting Thu
y = datetime(2020, 1, 3) # this is Friday and week 1 in ISO calendar; should be 2 in custom calendar
print(x)
print(y)

def weeknum(dt):
    return dt.isocalendar()[1]

def myweeknum(dt):
    offsetdt = dt + timedelta(days=3);  # you add 3 days to Mon to get to Thu 
    return weeknum(offsetdt);

print(weeknum(x));
print(myweeknum(x));

print(weeknum(y));
print(myweeknum(y));

Output:

2020-01-02 00:00:00
2020-01-03 00:00:00
1
1
1
2
Irresolute answered 28/8, 2020 at 11:39 Comment(6)
A subtle explanation required. You need to get from Mon to Fri which is 4 days. You actually need to subtract 4 days. But -4 mod 7 is 3 and hence you add. 3 in timedelta() above.Irresolute
Thanks! myweeknum(datetime(2019,1,1)) gives me 1, which, if a week starts on Thursday, is not the correct result, or am I missing something?Recollection
2019 1 1 is a Tuesday. 1/1/yyyy of any year should give you week 1, regardless of any day it is.Irresolute
datetime.date(2010, 1, 1).isocalendar()[1], from here gives me 53 - nevertheless, I like the simplicity of your code!Recollection
Or is your definition that the year begins on the first Friday that begins a full 'week' as per your definition of the week from Fri-Thu?Irresolute
Yeah I haven't thought this aspect through carefully enough. Experimenting with your code: myweeknum(datetime(2018,12,28)) (which is the last Friday of 2018) gives me 1. weeknum(datetime(2019,12,27)) (last friday of 2019) gives me 52. - would you be able to give a definition, how can we decide which year a day belongs to?Recollection
B
1

If you want every date's year is exactly the date itself's year, there's another form of week definition as follows.

If a week starts from Monday

#                 Mo Tu Wd Th Fr Sa Sn
# [2019-52w] DEC/ 23 24 25 26 27 28 29
# [2019-53w] DEC/ 30 31
# [2020-1w]  JAN/        1  2  3  4  5
# [2020-2w]  JAN/  6  7  8  9 10 11 12

# [2018-53w] DEC/ 31  
# [2019-1w]  JAN/     1  2  3  4  5  6

If a week starts from Friday

#                 Fr Sa Sn. Mo Tu Wd Th 
# [2019-53w] DEC/ 27 28 29. 30 31
# [2020-1w]  JAN/                  1  2
# [2020-2w]  JAN/  3  4  5.  6  7  8  9

# [2018-53w] DEC/ 28 29 30. 31  
# [2019-1w]  JAN/               1  2  3
# [2019-2w]  JAN/  4  5  6.  7  8  9 10

Solution

from datetime import datetime, timedelta
from enum import IntEnum

WEEKDAY = IntEnum('WEEKDAY', 'MON TUE WED THU FRI SAT SUN', start=1)

def get_week_number(start, date):
    year_start = datetime(date.year, 1, 1) - timedelta(days=(datetime(date.year, 1, 1).isoweekday() - start) % 7)
    return date.year, (date-year_start).days // 7 + 1, (date-year_start).days % 7 + 1

if __name__ == '__main__':
    # usage:
    print(get_week_number(WEEKDAY.FRI, datetime(2018, 12, 19)))
Boondoggle answered 28/8, 2020 at 16:57 Comment(0)
A
1

A bit late to the party, here's how we solved it

def get_week(date, weekday_start):
    # Number of days since day 1 of the year. First day is day 0.
    number_of_days = date.timetuple().tm_yday - 1
    # Get the first day of the week in int. Monday is 0, Tuesday is 1, etc.
    first_day_of_week = time.strptime(weekday_start if weekday_start else 'MONDAY', "%A").tm_wday
    # Get the first day of the year. isoweekday considers Monday as 1 and Sunday as 2, thus why -1.
    first_day_of_year = (datetime(date.year, 1, 1).isoweekday()) - 1
    # Get the week number
    return (number_of_days + abs(first_day_of_week - first_day_of_year)) // 7 + 1

We considered the first fraction of the week as week 1. Meaning if the year starts on Tuesday and Monday was set as weekday_start, the first week will be only 6 days.

weekday_start is a string that takes MONDAY, TUESDAY, WEDNESDAY, etc. as inputs.

Aladdin answered 8/12, 2021 at 21:53 Comment(0)
R
0

Copy the functions from below, then weeknumber(2020, 8, 21, 'Tuesday') will give you the number of the week August 21, 2020 falls into, week count starting on Tuesday, days in 2020 before the first Tuesday will have week number 0.

# necessary imports
from datetime import date, timedelta
import time

You can use this answer (which relies on this answer) to the question How can I select all of the Sundays for a year using Python? to get all Mondays, Tuesdays, Wednesdays, ... Sundays in a given year.

A helper function:

def weeknum(dayname):
    if dayname == 'Monday':   return 0
    if dayname == 'Tuesday':  return 1
    if dayname == 'Wednesday':return 2
    if dayname == 'Thursday': return 3
    if dayname == 'Friday':   return 4
    if dayname == 'Saturday': return 5
    if dayname == 'Sunday':   return 6

alternatively, (using this):

def weeknum(dayname):
    return time.strptime('Sunday', "%A").tm_wday

The main function we are going to use:

def alldays(year, whichDayYouWant):
    d = date(year, 1, 1)
    d += timedelta(days = (weeknum(whichDayYouWant) - d.weekday()) % 7)
    while d.year == year:
        yield d
        d += timedelta(days = 7)

Now to get the number of week of a given date, do:

def weeknumber(year, month, day, weekstartsonthisday):
    specificdays = [d for d in alldays(year, weekstartsonthisday)]
    return len([specificday for specificday in specificdays if specificday <= datetime.date(year,month,day)])

specificdays is a list of datetime.date objects in the year being the same weekday as weekstartsonthisday. For example, [d for d in alldays(2020,'Tuesday')] starts like this:

[datetime.date(2020, 1, 7),
 datetime.date(2020, 1, 14),
 datetime.date(2020, 1, 21),
 datetime.date(2020, 1, 28),
 datetime.date(2020, 2, 4),
...

As a reminder, 2020 started like this:

enter image description here

The list [specificday for specificday in specificdays if specificday <= datetime.date(year,month,day)] will contain the list of specificdays (ie Mondays, Tuesdays, ..., whichever you specify) which happened in a given year before your date. The len() of this will give us the number of the week. Days in the year before the first specificday will be in the 0 week.

Few examples:

  • weeknumber(2020,1,1,'Tuesday') returns: 0

  • weeknumber(2020,1,6,'Tuesday') returns: 0

  • weeknumber(2020,1,7,'Tuesday') returns: 1

  • weeknumber(2020,12,31,'Tuesday') returns: 52

  • weeknumber(2020,1,1,'Wednesday') returns: 1

Seems good.

Recollection answered 21/8, 2020 at 13:9 Comment(2)
Your program would have (2015,12,23,'Friday') to (2015,12,29,'Friday') returning 52, (2015,12,30,'Friday') to (2015,12,31,'Friday') returning 53, and (2016,1,1,'Friday') to (2016,1,7,'Friday') returning 1, which is not correct.Boondoggle
Hmmm that indeed seems bad. Any better idea?Recollection

© 2022 - 2024 — McMap. All rights reserved.