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?

Donald Duck
  • 8,409
  • 22
  • 75
  • 99
No94
  • 155
  • 1
  • 1
  • 7
  • 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.) – tobias_k Mar 23 '20 at 15:22
  • 1
    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 @tobias_k suggests it might work, but it would not be a real ISO week anymore. – bechtold Mar 23 '20 at 15:24
  • 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... – tobias_k Mar 23 '20 at 15:29
  • yes i tried to set the firstweek day with calendar but it doesn't change anything the results of my code – No94 Mar 23 '20 at 15:31
  • 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? – tobias_k Mar 23 '20 at 15:35

5 Answers5

9

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)
AnnieFromTaiwan
  • 3,845
  • 3
  • 22
  • 38
  • 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? – zabop Aug 28 '20 at 11:40
  • 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 wrong – zabop Aug 28 '20 at 12:15
  • Hi @zabop 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. – AnnieFromTaiwan Aug 28 '20 at 15:45
  • 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`. – AnnieFromTaiwan Aug 28 '20 at 16:05
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
vvg
  • 1,010
  • 7
  • 25
  • 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. – vvg Aug 28 '20 at 11:44
  • 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? – zabop Aug 28 '20 at 11:45
  • 2019 1 1 is a Tuesday. 1/1/yyyy of any year should give you week 1, regardless of any day it is. – vvg Aug 28 '20 at 11:46
  • `datetime.date(2010, 1, 1).isocalendar()[1]`, from [here](https://stackoverflow.com/a/2600864/8565438) gives me 53 - nevertheless, I like the simplicity of your code! – zabop Aug 28 '20 at 11:51
  • 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? – vvg Aug 28 '20 at 11:51
  • 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? – zabop Aug 28 '20 at 12:03
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)))
AnnieFromTaiwan
  • 3,845
  • 3
  • 22
  • 38
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.

Flair
  • 2,609
  • 1
  • 29
  • 41
Nicolas Z
  • 11
  • 2
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.

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
zabop
  • 6,750
  • 3
  • 39
  • 84
  • 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. – AnnieFromTaiwan Aug 26 '20 at 09:56
  • Hmmm that indeed seems bad. Any better idea? – zabop Aug 26 '20 at 10:03