🦁

Python:日付同士の間の稼働日数の計算

に公開

Pandasを用いて、日付同士の間の稼働日数の計算を行う

条件

あるデータフレームdfと、休日情報が入っているシリーズsr_holidayを用意する。
dfには、「開始日」と「終了日」という列それぞれに日時データが入っているものとする。
sr_holidayはシリーズなので休日の日付情報が1列に入っているだけとする。

想定としては、プロジェクトの作業タスク毎に開始日と終了日が与えられていて、
その間の営業日数を調べたい、と思うような場合である。

私の回答

    # データフレームの2つの列間の休日を計算してデータフレームに追加する
    def count_holidays(self, sr_holidays, df, from_date_col, to_date_col):
        """
        |休日のSeriesデータから日付間の休日数をデータフレーム列に追加する
        
        Parameters
        ----------
        df : DataFrame
            2つの日付列を持つデータフレーム
        sr_holiday : Series
            休日列のみのSeriesデータ
        from_date_col : str
            開始日列の列名
        to_date_col : str
            終了日列の列名

        Returns
        -------
        df :DataFrame
            元のデータフレームに休日数列を追加したデータフレーム
        """

        # NaNの日付を置き換え
        df[from_date_col] = pd.to_datetime( df[from_date_col] )
        df[to_date_col] = pd.to_datetime( df[to_date_col] )
        
        df[from_date_col] = df[from_date_col].fillna(pd.Timestamp('2100-12-31'))
        df[to_date_col] = df[to_date_col].fillna(pd.Timestamp('1900-01-01'))
        
        # 休みの日情報を日付型に統一
        def parse_date(date_str):
            formats = ['%Y/%m/%d', '%Y-%m-%d']
            for fmt in formats:
                try:
                    return pd.to_datetime(date_str, format=fmt)
                except ValueError:
                    continue
            return pd.NaT
        
        sr_holidays = sr_holidays.apply(parse_date)
        sr_holidays = sr_holidays.dropna()
        
        holidays_set = set(sr_holidays.dt.date)
        
        def count_holidays_in_range(row):
            start_date = row[from_date_col].date()
            end_date = row[to_date_col].date()
            
            # 開始日と終了日を含まない期間内の休みの日をフィルタリング
            count = sum(1 for holiday in holidays_set if start_date < holiday < end_date)
            return count

        df['土日祝数'] = df.apply(count_holidays_in_range, axis=1)

        return df

解説

sr_holidayについて

休日を一般的な土日にしていないのは長期連休などや会社独自の休日などに
臨機応変に対応できるようにするため。まぁ、休日データなんて年に1回程度作るだけだし、
会社の規模が大きくなってくれば誰かが作ってくれているだろう。

なぜデータフレームが入力されているか

よく、AとBの日付の間の日数を求める的な記事を見かけるのだが、
実際に単発でそんな計算をする事ないんじゃね?と思ったのがきっかけ。
というか、私の業務で実際に必要になったこの計算は、
別の目的で使われているのだが、数万行分の日付計算が必要だったのと、
他の列情報も必要だったので、データフレームを入力に持ってきた

コード解説

基本的にコメント文があるので、それを見てもらえれば良いが、自分のためにもメモする

# NaNの日付を置き換え
df[from_date_col] = pd.to_datetime( df[from_date_col] )
df[to_date_col] = pd.to_datetime( df[to_date_col] )

df[from_date_col] = df[from_date_col].fillna(pd.Timestamp('2100-12-31'))
df[to_date_col] = df[to_date_col].fillna(pd.Timestamp('1900-01-01'))

# 休みの日情報を日付型に統一
def parse_date(date_str):
    formats = ['%Y/%m/%d', '%Y-%m-%d']
    for fmt in formats:
        try:
            return pd.to_datetime(date_str, format=fmt)
        except ValueError:
            continue
    return pd.NaT

sr_holidays = sr_holidays.apply(parse_date)
sr_holidays = sr_holidays.dropna()

この部分は単なる空欄(Nan)値の処理である。
といってもその後の処理でNanのまま計算できない事があるので、
今を生きている人がいない日時をMagicNumberとして記載した。
休日のシリーズデータも表示形式に揺らぎがあると嫌なのでパースしている

holidays_set = set(sr_holidays.dt.date)

def count_holidays_in_range(row):
    start_date = row[from_date_col].date()
    end_date = row[to_date_col].date()
    
    # 開始日と終了日を含まない期間内の休みの日をフィルタリング
    count = sum(1 for holiday in holidays_set if start_date < holiday < end_date)
    return count

df['土日祝数'] = df.apply(count_holidays_in_range, axis=1)

1行目で休日情報をsetに放り込んでいる。これで休日の集合が出来上がった。
それを各行毎に休みに該当する日数の合計を求めているだけである。

本当はmask/whereを使ったベクトル演算で処理したかったのだが、
何故か計算誤り(というか順列誤り)が起きてしまったので、
正しく計算できるこちらの方法に収束した。

データフレームを丸ごと入れて取り出す系のコードを紹介している記事は
意外と多くないと思っているので、誰かの一助になればと思う。

Discussion