第4章:結合

4-1 マスタテーブルの結合

  • マスタデータ:ある要素に対する共通のデータをまとめたデータ。顧客マスタなら、顧客の氏名、年齢、住所など。
  • レコードテーブルとマスタテーブル結合処理(join)の注意点:結合するテーブルの大きさをなるべく小さくして、利用するメモリ量を少なくすること。
  • 言語仕様:R / Pythonは、一度抽出前のデータをメモリ上にすべて乗せねばならないので、データサイズが大きい場合はSQLを使う。

例)予約テーブルとホテルテーブルを結合して、宿泊人数が1人のビジネスホテルの予約レコードのみ取り出す。

  • SQL

      • SELECT
         -- 必要な列の抽出
         rsv.reserve_id, rsv.hotel_id, rsv.customer_id, rsv.reserve_datetime,
         rsv.checkin_date, rsv_checkin_time, rsv.checkout_date, rsv.people_num,
         rsv.total_price,
         hotel.base_price, hotel.big_area_name, hotel.small_area_name,
         hotel.hotel_latitude, hotel.hotel_longitude, hotel.is_business
        FROM
         work.reserve_table rsv
        JOIN
         work.hotel_tb hotel
         ON rsv.hotel_id = hotel.hotel_id
           AND
           -- ホテルテーブルからビジネスホテルのデータのみ抽出
           hotel.is_business is True
           AND
           -- 予約テーブルからビジネスホテルのデータのみ抽出
           AND rsv.people_num = 1;
  • Python

      • pd.merge(reserve_tb.query('people_num == 1'),
             hotel_tb.query('is_business'),
             on = 'hotel_id', how = 'inner')


4-2 条件に応じた結合テーブルの切り替え

  • 方法:結合元のテーブルに結合キー用の列として、条件式で参照するためのそれぞれ値が異なる新たな列を生成する。次に、結合するマスタテーブル2つの必要な共通項を取り出し、1つのテーブルにする。最後に、テーブル同士を結合する。

例)ホテルの予約レコード。ホテルテーブルのすべてのホテルに対して、レコメンド候補のホテルを紐付けたデータを作成する。レコメンド候補のホテルは、同じ小地域(small_area_nameが等しい)のホテルが20件以上ある場合は、同じ小地域のホテルをレコメンド候補とする。同じ小地域のホテルが20件に満たない場合は、同じ大地域(big_area_nameが等しい)のホテルをレコメンド候補とする。

  • SQL

    • -- small_area_nameごとにホテル数をカウント、結合キーを判定するためのテーブル
      WITH small_area_mst AS(
       SELECT
        small_area_name,
        -- 20件以上であればjoin_area_idをsmall_area_nameとして設定
        -- 20件未満であればjoin_area_idをbig_area_nameとして設定
        -- -1は、自ホテルを引いている
        CASE
         WHEN COUNT(hotel_id) -1 >= 20 THEN small_area_name
         ELSE  big_area_name 
        END AS join_area_id
       FROM
        work.hotel_tb
       GROUP BY 
        big_area_name, small_area_name
      ),
      -- recommend_hotel_mstはレコメンド候補のためのテーブル
      recommend_hotel_mst AS(
       -- join_area_idをbig_area_nameとしてレコメンド候補マスタ
       SELECT
        big_area_name AS join_area_id,
        hotel_id AS rec_hotel_id
       FROM
        work.hotel_tb
       -- unionで、テーブル同士を連結
       UNION
       -- join_area_idをsmall_area_nameとしたレコメンド候補マスタ
       SELECT
        small_area_name AS join_area_id,
        hotel_id AS rec_hotel_id
       FROM
        work.hotel_tb
      )
      SELECT
       hotels.hotel_id,
       r_hotel_mst.rec_hotel_id
      -- レコメンド元のhotel_tbを読み込み
      FROM
       work.hotel_tb hotels
      -- 各ホテルのレコメンド候補の対象エリアを判断するためにsmall_area_mstを統合
      INNER JOIN small_area_mst s_area_mst
       ON hotels.small_area_name = s_area_mst.small_area_name
      -- 対象エリアのレコメンド候補を統合する
      INNER JOIN recommend_hotel_mst r_hotel_mst
       ON s_area_mst.join_area_id = r_hotel_mst.join_area_id
      -- レコメンド候補から自分ホテルを除く
      AND hotels.hotel_id != r_hotel_mst.rec_hotel_id;
  • Python

    • # ガベージコレクションライブラリ
      import gc

      # small_area_nameごとにホテル数をカウント
      small_area_mst = hotel_tb \
        .groupby(['big_area_name', 'small_area_name'], as_index=False) \
        .size().reset_index()
      small_area_mst.columns = ['big_area_name', 'small_area_name', 'hotel_cnt']

      # 20件以上であればjoin_area_idをsmall_area_nameとして設定
      # 20件未満であればjoin_area_idをbig_area_nameとして設定
      # -1は自ホテルを引いている
      small_area_mst['join_area_id'] = \
        np.where(small_area_mst['hotel_cnt'] - 1 >= 20,
              small_area_mst['small_area_name'],
                small_area_mst['big_area_name'])

      # 必要無くなった列を削除
      small_area_mst.drop(['hotel_cnt', 'big_area_name'], axis=1, inplace=True)

      # レコメンド元になるホテルにsmall_area_mstを結合することで、join_area_idを設定
      base_hotel_mst = pd.merge(hotel_tb, small_area_mst, on='small_area_name') \
                   .loc[:, ['hotel_id', 'join_area_id']

      # メモリ解放
      del small_area_mst
      gc.collect()

      # recommend_hotel_mstはレコメンド候補のためのテーブル
      recommend_hote_mst = pd.concat([
        # join_area_idをbig_are_nameとしたレコメンド候補マスタ
        hotel_tb[['small_area_name', 'hotel_id']] \
            .rename(columns={'small_area_name': 'join_area_id'}, inplace=False),
        # join_area_idをsmall_area_nameとしたレコメンド候補マスタ
        hotel_tb[['big_area_name', 'hoteo_id']] \
            .rename(columns={'big_area_name': 'join_area_id'}, inplace=False)
      ])

      # hotel_idの列名が結合すると重複するので変更
      recommend_hotel_mst.rename(columns={'hotel_id': 'rec_hotel_id', inplace=True)

      # base_hotel_mstとrecommend_hotel_mstを結合し、レコメンド候補の情報を付与
      # query関数によりレコメンド候補から自分を除く
      pd.merge(base_hotel_mst, recommend_hotel_mst, on='join_area_id') \
           .loc[:, ['hotel_id', 'rec_hotel_id']] _
           .query('hotel_id != rec_hotel_id')


4-3 過去データの結合

  • 注意点:やみくもに過去データを取り出すと膨大な数になる。
  • 対策:
    • 結合対象とする過去の期間を絞る
    • 結合した過去データに集約関数を利用して、データ数を増やさないようにする

例1)n件前のデータ取得

ホテルの予約テーブルの全ての行に、同じ顧客の2回前の予約金額の情報を付与する。予約無い場合は値なしとする。

  • SQL

      • SELECT 
         *,
         -- LAG関数で、2件前のtotal_priceをbefore_priceとして取得
         -- LAG関数により参照する際のグループをcustomer_idに指定
         -- LAG関数により参照する際のグループ内のデータをreserve_datetimeの古い順に指定
         LAG(total_price, 2) OVER
          (PARTITION BY customer_id ORDER BY reserve_datetime) AS before_price
        FROM
         work.reserve_tb;

        LAG(対象列, n件前) OVER(  ) : n件前のデータを取得するウィンドウ関数
  • Python

      • # customerごとにreserve_datetimeで並べ替え
        # groupby関数のあとにapply関数を適用し、groupごとに並べ替え
        # sort_values関数によりデータを並び替え
        result = reserve_tb.groupby('customer_id') \
                  .apply(lambda group:
                 group.sort_values(by='reserve_datetime', axis=0, inplace=True)

        # customerごとに2つ前のtotal_priceをbefore_priceとして保存
        # shift関数は、periodsの引数の数だけデータ行を下にずらす関数
        result['before_price'] = \
         result['totla_price'].groupby('customer_id').shift(period=2) 

例2)過去n件の合計値

予約テーブルのすべての行に、自身の行から2件前までの3回の合計金額の情報を付与する。過去の予約が3回未満の場合は、値なしとする。

  • SQL

      • SELECT
          *,
          CASE
           -- COUNT関数で何件の合計を計算したかをカウントし、3件あるか判定
           COUNT(total_price) OVER
           (PARTITION BY customer_id ORDER BY reserve_datetime
           ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) = 3
          THEN
           -- 自身を含めた3件の合計金額を計算
           SUM(total_price) OVER
           (PARTITION BY customer_id ORDER BY reserve_datetime
           ROWS BETWEEN 2 PRECEDING AND CURRENT ROW)
          ELSE
           NULL 
          END AS price_sum
        FROM
         work.reserve_tb;
  • Python
      • 複数のデータ集合に区切るrolling関数の利用

        # customer_idごとにreserve_datetimeでデータを並び替え
        result = reserve_tb.groupby('customer_id') \
            .apply(lambda x: x.sort_values(by='reserve_datetime', ascending=True)) \
               .reset_index(drop=True)

        # 新たな列としてprice_avgを追加
        result['price_avg'] = pd.Series(
          result
           # customer_idごとにtotal_priceのwindow3件にまとめ、その平均値を計算
           # min_periodsを1に設定し、1件以上あった場合には計算するように設定
           .groupby('customer_id')
           ['total_price'].rolling(center=False, window=3, min_periods=1).mean()
           # gourp化を解除すると同時に、customer_idの列を削除
           .reset_index(drop=True)
        )
        # customer_idごとにprice_avgを1行下にずらす
        result['price_avg'] = \
         result.groupby('customer_id')['price_avg'].shit(periods=1)

例3)過去n件の平均値

ホテルの予約テーブルの全ての行に、自身の行を含めないで1件前から3件前までの3回の平均予約金額の情報を付与する。過去の予約が3回未満の場合は、満たない回数内で平均予約金額を計算する。予約が1回も無い場合は、値無しとする。

  • SQL

      • SELECT
          *,
          AVG(total_price) OVER
          (PARTITION BY customer_id ORDER BY checkin_data
           ROWS BETWEEN 3 PRECEDING AND 1 PRECEDING) AS price_avg
        FROM
          work.reserve_tb;

例4)過去n日間の合計値

ホテルの予約テーブルのすべてのデータ行に対して、自身の行を含めないで同じ顧客の過去90日間の合計予約金額の情報を付与する。予約が1回も無い場合は0とする。

    • SQL
        • 結合範囲が件数ではなく、結合条件に従う場合には、Window関数は利用できないので、JOIN句を用いた結合を利用する。
          SELECT
           -- 結合元のデータ列をすべて取得
           base.*,
           -- 対象の件数が0件の場合は0、1件以上ある場合は合計金額を計算
           COALESCE(SUM(combine.total_price), 0) AS price_sum
          -- 結合元の予約テーブルの指定
          From
           work.reserve_tb base
          -- 過去の情報として結合する予約テーブルの指定
          LEFT JOIN 
           work.reserve_tb combine
          -- 同じcustomer_id同士で結合
           ON
            base.customer_id = combine.customer_id
           -- 過去のデータのみを結合対象として指定
           AND
            base.reserve_datetime > combine.reserve_datetime
           -- 90日前までの過去のデータのみを結合対象として指定
           AND
            DATEADD(day, -90, base.reserve_datetime) <= combine.reserve_datetime
          -- 結合元の予約テーブルのすべてのデータ列で集約
          GROUP BY
           base.reserve_id, base.hotel_id, base.customer_id, base.reserve_datetime,
           base.checkin_date, base.checkin_time, base.checkout_date,
           base.people_num, base.total_price;


    4-4 全結合

    • 全結合:結合するテーブル同士の全ての組み合わせを掛け合わせて生成する結合。
      たとえば、顧客の月ごとの利用金額を集計する。予約テーブルがあれば、顧客と月ごとに利用金額の値を集約して、合計利用金額を計算できる。しかし、この計算方法では、ある顧客のある月の利用が無い場合は予約レコードが存在しないので、期待する利用料金が0円という結果は生成されない。このような問題を全結合により回避できる。事前に顧客IDと集計対象期間の月を全結合してから、予約レコードを結合することにより、利用料金が0円という結果が生成される。

    例)ホテルの予約レコードにおいて、顧客ごとに2017年1月〜2017年3月の月間合計利用料金を計算する。利用がない月は0とする。日付はチェックイン日付を用いる。

    • SQL
        • 全結合を行う場合は、CROSS JOIN句を利用する。CROSS JOINでは、ON句で結合条件を指定しない。また、SQLで一時的な年月マスタを作成するのは難しいので、事前に汎用的なテーブルとしてデータベースに常に年月マスタを準備するほうが良い。
          年月マスタ:年、月、月開始日、月終了日が一覧になっているテーブル
          • 年月マスタテーブル : work.month_mst
            year_num | moth_mun | month_first_day | month_last_day
                    2016 |               1 |        2016-01-01 |        2016-01-31
                    2016 |               2 |        2016-02-01 |        2016-02-29
                    2016 |               3 |        2016-03-01 |        2016-03-31
                   ~ 省略 ~

          • SELECT
             cus.customer_id,
             -- 年月マスタから年を取得
             mst.year_num,
             -- 年月マスタから月を取得
             mst.month_num
             -- 該当のtotal_priceがある場合は足し合わせ、無い場合は0を足し合わせる
             SUM(COALESCE(rsv.total_price, 0)) AS total_price_month
            FROM
             work.customer_tb cus
            -- 顧客テーブルと年月マスタを全結合
            CROSS JOIN
             work.month_mst mst
            -- 顧客テーブルと年月マスタと予約テーブルを結合
            -- ( work.month_mst1行に対してwork.reserve_tbの該当行(その月のレコード)全部
            LEFT JOIN work.reserve_tb rsv 
             ON 
              cus.customer_id = rsv.customer_id
             AND
              mst.month_first_day <= rsv.chekin_date
             AND
              mst.month_last_day >= rev.chekin_date
            -- 年月マスタの対象期間を絞り込み
            WHERE
             mst.month_first_day >= '2017-01-01'
             AND
             mst.month_first_day <= '2017-04-01'
            GROUP BY
             cus.customer_id, mst.year_num, mst.month_num;
      • Python
        • Pythonでは、全結合用の関数がない。なので、すべてが同じ値である結合キーを準備する必要がある。

          # 日付型用のライブラリ
          import datetime
          # 日付の計算用ライブラリ
          from dateutil.relativedelta import relativedelta

          # 年月マスタの生成
          month_mst = pd.DataFrame({
            'year_month':
              # relativedeltaで2017-01-01をx月間進める。x=0,1,2
              # 2017-01-01, 2017-02-01, 2017-03-01のリストを生成
              [(datetime.date(2017, 1, 1) + relativedelta(months=x)).strftime('%Y%m')
                                       for x in range(0, 3)]
            })

          # cross join のためにすべて同じ値の結合キーを準備
          customer_tb['join_key'] = 0
          month_mst['join_key'] = 0

          # customer_tbとmonth_mstを準備した結合キーで内部結合し、全結合を実現
          custmer_mst = pd.merge( 
            customer_bt[['customer_id', 'join_key']], month_mst, on='join_key'
          )

          # 年月の結合キーを予約テーブルで準備
          reserve_tb['year_month'] = reserve_tb['checkin_date'] \
              .apply(lambda x: pd.to_datetime(x, format='%Y-%m-%d').strftime('%Y%m'))

          # 予約レコードと結合し、合計利用金額を計算
          summary_result = pd.merge(
            customer_mst,
            reserve_tb[['customer_id', 'year_month', 'total_price']],
            on=['customer_id', 'year_month'], how='left
            ).groupby((['customer_id', 'year_month'])['total_price'].sum().reset_index()

          # 予約レコードが無かった場合の合計金額を値なしから0に変換
          summary_result.fillna(0, inplace=True)

    機械学習Tips保管庫

    データ解析、機械学習のための学習内容の保管庫。復習用。

    0コメント

    • 1000 / 1000