Pythonでいろいろな週始まりのウィークナンバー(週番号)を取得してみる

2021.01.25

データアナリティクス事業本部@札幌の佐藤です。

今回は、普段あまりなじみのない人もいるかもしれませんが、ウィークナンバー(週番号)について記載したいと思います。


そもそもウィークナンバー(週番号)とは

そもそもウィークナンバー(週番号)とは何かという話ですが、グレゴリオ暦の年(365日/閏年は366日)は月曜日~日曜日まで7日あり、それを1週間と呼ぶと思います。

1年間に何週間あるのか、今何週間目なのかというのを表現するのがウィークナンバーです。
1年は大体52週あります。

身近なものだと週刊少年漫画の背表紙の下のほうに書いてある数字ですね。


週の始まりは何曜日なのか

1週間は何曜日からスタートすると思いますか?

人によって日曜日だったり月曜日始まりだったりするかと思います。

カレンダーは大体日曜日が一番左になっていますが、週末は土日だったりはあいまいな感じですね。

日本の工業規格上は、ISO-8601JIS X 0301の暦日にて月曜日が週の始まりとして定めています。

逆に労働基準法では、「一週間とは、就業規則その他に別段の定めがない限り、日曜日から土曜日までのいわゆる暦週をいうものであること」と示されている通り、日曜日が週の始まりです。

日本の中でも週の始まりは何曜日なのは結構あいまいな要素のひとつになります。
とはいえ一般的には週末が土日である認識が強いため、週の始まりは月曜日であると考えておけば会話中にずれなどは起こるケースは少ないのではないかと思います。


日本以外の週の始まりは何曜日なのか

英語版のWikipediaでは週の始まりは3パターンによって分けられると記載されています。

週の始まり 1週目とする基準の曜日 使用している地域
月曜日 木曜日 EUおよびほとんどのヨーロッパ諸国、ほとんどのアジア、オセアニア
土曜日 金曜日 ほとんどの中東
日曜日 土曜日 北米、日本、台湾、タイ、香港、イスラエル、エジプト、南アフリカ、フィリピン、ほとんどのラテンアメリカ

Week - Week numbering

使用している地域はあくまで目安です。(日本も日曜日始まりかどうかはその基準によって異なるため)

1週目とする基準の曜日の列は、元旦からその年の第1週目とカウントするベースの曜日です。
例えば、2020年の第1週目であれば、月曜日始まりであれば、1月7日が一番最初にある木曜日ですので、1月4日~1月10日が第1週となります。

週の始まりが3種類あるということなので、実装するにあたってもその考慮が必要になるケースがあるかと思います。

特に日本以外の諸外国のデータを利用とする場合は、データの集計単位も異なる場合がありますので特に注意が必要かと思います。


Pythonでウィークナンバーを取得する

Pythonのdatetimeモジュールのデータ型ではISO-8601に則った週番号の変換(もしくはその逆)を用意していますので、月曜日を週の始まりとする場合は苦労しないかと思います。

日曜日始まりや土曜日始まりは、アレンジが必要であったりそもそも用意していなかったりするために実装が必要となります。


週の始まりが月曜日の場合

ISO-8601基準での月曜日始まりの話となります。


datetime.isocalendar()

日付から、何年の第何週の何番目の日かというのをTupleで返却してくれます。
何番目かの表現はdatetime.isoweekday()と同様で、1が月曜日、7が日曜日になります。

また、Tupleの中の値は数値型になるため、0で先頭パディングしたい場合は自分で埋めてあげる必要があります。

>>> from datetime import date
>>> # 2021年1月3日(日)は2020年第53週として扱われます
>>> print(date(2021, 1, 3).isocalendar())

(2020, 53, 7)

>>> # 2021年1月4日(月)は2021年第1週として扱われます
>>> print(date(2021, 1, 4).isocalendar())

(2021, 1, 1)


datetime.strptime()では正しく取得できないので注意が必要

datetime.isocalendar() と同じような記述として、datetime.strptime()を用いての記述があります。

引数のformatに%Wを設定することで、「strftime() と strptime() の書式コード」にあるように、「0埋めした10進数で表記した年中の週番号 (週の始まりは月曜日とする)。」と記載があるように、週番号を求めることができます。
こちらは返却値が文字列型になるため、0で先頭パディングは不要になります。

この処理で注意しなければならないのが、「新年の最初の月曜日に先立つ日は 0週に属するとします。」と書かれている点です。

このメッセージはdatetime.isocalendar() と同じ(ISO-8601と同じ)のような結果にならないことを示しています。

具体的には以下のふたつの要素による影響です。

  • 新年の最初の月曜日
  • 0週に属する

実装して実際に確かめてみます。

>>> from datetime import datetime
>>> # 2020年1月2日(木)は2020年第1週なのに0週として扱われてしまっている。
>>> print('週番号:'+datetime(2020, 1, 2).strftime('%W') )

週番号:00

>>> # 2020年12月31日(木)は、第53週なのに52週として扱われてしまっている。
>>> print('週番号:'+datetime(2020, 12, 31).strftime('%W') )

週番号:52

>>> # 2021年1月1日(金)~1月3日(日)2021年第0週として扱われてしまっている。
>>> print('週番号:'+datetime(2021, 1, 1).strftime('%W') )

週番号:00

>>> print('週番号:'+datetime(2021, 1, 3).strftime('%W') )

週番号:00

上の表で記載した通り、1週目とする基準の曜日は木曜日であるため、2020年1月2日(木)は2020年の第1週になるはずです。
ですが、1週分ずれています。

これは、「新年の最初の月曜日」というルールのため2020年1月6日(月)が2020年の第1週になってしまっている点、また、新年の最初の月曜日に来るまでは、0週に属してしまう点によるものです。、

そのため安直にこの実装を使用すると、ウィークナンバーのずれが発生しますので注意が必要です。


date.fromisocalendar()

date.fromisocalendar()を使用することで、datetime.isocalendar()の逆で週番号から日付を返却します。(Python3.8以降)

>>> from datetime import date
>>> date.fromisocalendar(2021, 1, 1)

datetime.date(2021, 1, 4)


日曜日始まりと土曜日始まり

日曜日始まりは、datetime.strptime() の引数formatに%Uを渡すことで週番号を求めることができます。
ですが、上でも記載した問題(「新年の最初の日曜日に先立つ日は 0週に属するとします」という記述にある通り)により正しく算出できない可能性があります。

また、土曜日始まりはそもそもウィークナンバーの変換を用意していないため自分で用意する必要があります。


ウィークナンバーを出力するロジックを実装してみる

from datetime import datetime, timedelta

class CustomizedCalendar:
    def __init__(self):
        # 月曜日:1~日曜日:7
        self.iso_target_day_weeknum = 6

    # 引数の日付から次の「1週目とする基準の曜日」の日付を取得する
    def get_next_target_day(self, date):  
        days_ahead = self.iso_target_day_weeknum - int(date.strftime("%u"))
        if days_ahead <= 0: 
            days_ahead += 7
        return date + timedelta(days_ahead)

    # ウィークナンバーを取得する
    def get_weeknum(self, date):
        # 引数の日付が「1週目とする基準の曜日」と同じ場合、元旦から引数の日付でウィークナンバーを取得
        if int(date.strftime("%u")) == self.iso_target_day_weeknum:
            new_years_date = datetime(date.year, 1, 1) 
            return date.year, (date - new_years_date).days // 7 + 1
        # 引数の日付が「1週目とする基準の曜日」と違う場合、
        # 元旦から引数の日付の次の「1週目とする基準の曜日」の日付でウィークナンバーを取得
        else:
            next_target_date = self.get_next_target_day(date)
            new_years_date = datetime(next_target_date.year, 1, 1) 
            return next_target_date.year, (next_target_date - new_years_date).days // 7 + 1

if __name__ == '__main__':
    my_calendar = CustomizedCalendar()
    #2020年12月26日(日)は2020年第52週として扱う
    print(my_calendar.get_weeknum(datetime(2020, 12, 26)))
    #2020年12月31日(木)は2021年第1週として扱う
    print(my_calendar.get_weeknum(datetime(2020, 12, 31)))
    #2021年1月3日(日)は2021年第2週として扱う
    print(my_calendar.get_weeknum(datetime(2021, 1, 3)))

上記は日曜日始まりを考慮したものになっていますが、self.iso_target_day_weeknum を「1週目とする基準の曜日」の番号に変更することで、土曜日始まりもできるかと思います。

実行すると以下のような結果になります。

(2020, 52)
(2021, 1)
(2021, 2)


最後に

個人的にウィークナンバーにあまりなじみがなく、月曜日始まりくらいしか知らず、土曜日始まりがあるということは今回初めて知りました。

日付がいろいろなルールがある上に正しく認識しないと、集計結果などが異なるのでちゃんと理解していきたいところです。


参考

国立天文台 - 暦Wiki -曜日の始まり

月曜日とは異なる週の開始日で週番号を取得する-Python

https://www.timeanddate.com/calendar/?wno=1

Wikipedia - 暦