🆗

【Python・PySparkで学ぶ!】datediff(),months_between()で日数・月数を計算しよう

2025/02/11に公開
1

↓イベントマスタ(2025_event_mst_1.csv)のサンプル

上記のようなイベント情報を集約したCSVファイルが存在すると仮定します。

◾️要望

とある日の朝会MTGにて、クライアントから次のような要望を頂きました。

『イベントマスタにキャンペーン日数と月数の情報を追加したい』

本稿では、クライアントからの要望に答えながら、 datediff(),months_between() について学びます。
よろしくお願いいたします。

◾️AsIs(現状把握)

エンジニアとクライアント間の認識に相違があるとアウトプットのイメージに相違が発生します。
はじめに、 データアセスメントの観点から論点を提示し、クライアントと集計ロジックの認識を擦り合わせるタッチポイント を設けましょう。

◾️タッチポイント議事録(追加項目)

  • "日数"項目

    • 合意『日数は、開始日と終了日を含む期間とする』
  • "月数"項目

    • 合意『月数は、開始月と終了月を含む期間とする』

◾️タッチポイント議事録(データソース)

  • CSVファイルの情報

    • 合意『データソースの特性を以下の通りとする』
      • エントリーリストCSVファイル(2025_event_mst_1.csv): 静的データ
    • 合意『データソースの格納先を以下の通りとする』
      • エントリーリストCSVファイル(2025_event_mst_1.csv)
        • 格納先:s3://data/content/2025_event_mst_1.csv
    • 合意『データソースのオプション情報を以下の通りとする』
      • エントリーリストCSVファイル(2025_event_mst_1.csv)
        • ヘッダー名称とデータ型
          • キャンペーンID STRING型
          • キャンペーン名称 STRING型
          • キャンペーン開始日 STRING型
          • キャンペーン終了日 STRING型
          • メール本文 STRING型
        • データソースのヘッダー行の有無
          • ヘッダー行:無し
        • 空文字の対応
          • 空文字はnullに置換。その後、0埋めする。
        • 複数行が1セル内に存在する場合の対応
          • 複数行が1セル内に存在する場合がそもそもない。
        • 文字コード
          • データソースの文字コードがUTF-8なので、変換する必要なし
        • Indexの必要性
          • 必要なし

◾️アウトプットイメージ
 タッチポイントより、クライアントとアウトプットイメージを次の通り合意いたしました。
例)

◾️ToBe(スクリプト作成)

タッチポイント議事録をもとに、スクリプトを作成します。
 はじめに、CSVファイルを読み込みます。

CSVファイルを読み込み
from pyspark.sql.types import StructType, StructField, StringType

# スキーマ定義
event_mst_schema = StructType([
    StructField("キャンペーンID", StringType(), False),
    StructField("キャンペーン名称", StringType(), False),
    StructField("キャンペーン開始日", StringType(), False),
    StructField("キャンペーン終了日", StringType(), False),
    StructField("メール本文", StringType(), False)
])

# データの読み込み
event_mst_sdf = spark.read.csv(
    path="s3a://data/content/2025_event_mst_1.csv",
    header=True,
    schema=event_schema,
    multiLine=True
)

次に、タッチポイントの合意に従い、"日数"項目"月数"項目を作成します。
作成手順は以下の通りです。

  1. "キャンペーン開始日"項目と"キャンペーン終了日"の日付型項目を用意
  2. fn.datediff()でキャンペーン開始日とキャンペーン終了日の日数差を算出
  3. fn.months_between()でキャンペーン開始日とキャンペーン終了日の月数差を算出
"日数"項目と"月数"項目を作成
# 日付型に変換
event_date_sdf = (
    event_mst_sdf
    .withColumn("キャンペーン開始日(日付型)", fn.to_date(fn.col("キャンペーン開始日"), "yyyy/MM/dd"))
    .withColumn("キャンペーン終了日(日付型)", fn.to_date(fn.col("キャンペーン終了日"), "yyyy/MM/dd"))
)

# 日数、月数の計算
output_sdf = (
    event_date_sdf
    .withColumn("日数", fn.datediff(fn.col("キャンペーン終了日"), fn.col("キャンペーン開始日")) + 1)
    .withColumn("月数", fn.ceil(fn.months_between(fn.col("キャンペーン終了日"), fn.col("キャンペーン開始日"))))
    .select(
        "キャンペーンID",
        "キャンペーン名称",
        "キャンペーン開始日",
        "キャンペーン終了日",
        "日数",
        "月数",
        "メール本文"
    )
)
キャンペーンID キャンペーン名称 キャンペーン開始日 キャンペーン終了日 日数 月数 メール本文
cp202501 お正月セール! 2025-01-01 2025-01-14 14 1 <body><h1>あけましておめでとうございます!</h1>\n<div>今年も元日から1月中旬まで特別セールを開催いたします。\nぜひお越しください!</div></body>
cp202502 謹賀新年セール 2025-01-01 2025-01-31 31 1 <body><h1>明けましておめでとうございます。</h1>\n<div>今年は巳年ということで、巳にちなんだ縁起物を取り揃えております。\n<a href=(url)>詳細はこちらからご確認ください。</a></div></body>
cp202503 バレンタインキャンペーン 2025-02-07 2025-02-21 15 1 <body><h2>バレンタインキャンペーン!</h2>\n<div>今年はフランスのチョコレート特集!\n友人から恋人まで喜ばれるチョコレートはいかがですか?</div></body>
cp202504 新生活応援キャンペーン 2025-03-17 2025-05-18 63 3 <body><h2>新生活応援キャンペーン</h2>\n<div>新入生・新入社員の皆様のために、\n人気の雑貨をお手頃価格のラインナップ!</div></body>
cp202505 夏季に嬉しい涼しい食事 2025-07-01 2025-09-30 92 3 <body><h2>夏季に嬉しい涼しい食事</h2>\n<div>猛暑が続く日中に、\n涼しい美味しい料理はいかがですか?</div></body>

上記の結果から、操作が意図した通りであることが確認できました。
最後に、スクリプト全量をご紹介します。

スクリプト全量
from pyspark.sql import SparkSession
from pyspark.sql import functions as fn
from pyspark.sql.types import StructType, StructField, StringType
import pandas as pd

# セッション作成
spark = SparkSession.builder.getOrCreate()

# スキーマ定義
event_mst_schema = StructType([
    StructField("キャンペーンID", StringType(), False),
    StructField("キャンペーン名称", StringType(), False),
    StructField("キャンペーン開始日", StringType(), False),
    StructField("キャンペーン終了日", StringType(), False),
    StructField("メール本文", StringType(), False)
])

# データの読み込み
event_mst_sdf = spark.read.csv(
    path="s3a://data/content/2025_event_mst_1.csv",
    header=True,
    schema=event_schema,
    multiLine=True
)

# 日付型に変換
event_date_sdf = (
    event_mst_sdf
    .withColumn("キャンペーン開始日(日付型)", fn.to_date(fn.col("キャンペーン開始日"), "yyyy/MM/dd"))
    .withColumn("キャンペーン終了日(日付型)", fn.to_date(fn.col("キャンペーン終了日"), "yyyy/MM/dd"))
)

# 日数、月数の計算
output_sdf = (
    event_date_sdf
    .withColumn("日数", fn.datediff(fn.col("キャンペーン終了日"), fn.col("キャンペーン開始日")) + 1)
    .withColumn("月数", fn.ceil(fn.months_between(fn.col("キャンペーン終了日"), fn.col("キャンペーン開始日"))))
    .select(
        "キャンペーンID",
        "キャンペーン名称",
        "キャンペーン開始日",
        "キャンペーン終了日",
        "日数",
        "月数",
        "メール本文"
    )
)

『結論』

◾️期間差分を算出するロジックを習得するメリット(ビジネスサイド)

期間差分を算出するロジックを習得するメリットは以下の通りです。

  • キャンペーンやプロモーションの分析
    • キャンペーン期間ごとの売上や顧客反応を正確に把握できる。
  • 期間ベースのパフォーマンス分析
    • 四半期や月単位の業績比較において、期間が異なるデータの整合性を担保できる。

◾️期間差分を算出するロジックを習得するメリット(エンジニアリングサイド)

期間差分を算出するロジックを習得するメリットは以下の通りです。

  • パフォーマンス最適化
    • datediff() や months_between() はSparkのネイティブ関数のため、パフォーマンスが高い。
  • データクレンジングと正規化
    • 日付差分処理はデータパイプラインの重要な前処理ステップとして活用できる。

Discussion

fact601fact601
fn.datediff(fn.col("キャンペーン終了日"), fn.col("キャンペーン開始日")) + 1

このコードの+1はどういうことですか?