【2. テーブルの加工編】Databricksで生データの加工から可視化まで一気通貫しておこなってみた

# はじめに
本記事は全4編によって構成されるうち、2つ目の記事となります。
[前回](https://qiita.com/Tsubasa-Koyama/items/1a943ff8b265daa9953e)ではCSVデータをDelta形式に変換したテーブルを作成を行いました。
    本記事のゴールとしてDelta形式化したテーブルを用いて可視化に向けた加工を完了します。

# 全4編の概要
全4編かけてBacklogで記録されたCSVデータ(検証用に一部編集済み)を元に、ノートブック上でテーブルの加工を行い、Databricks SQL(Redash)を用いて可視化まで行っていきます。

[1. Delta形式のテーブル作成編](https://qiita.com/Tsubasa-Koyama/items/1a943ff8b265daa9953e)
[2. テーブルの加工編]()
[3. SQLクエリ作成編](https://qiita.com/Tsubasa-Koyama/items/730af8bf32008ba630c2)
[4. ビジュアル作成編](https://qiita.com/Tsubasa-Koyama/items/290f2e05c758827f2347)

Databricksを利用してデータ加工をしていきたいという方向けに、加工の流れを一気通貫してざっくりと理解していただくことを想定しております。
＊ただし一部複雑な加工や考え方が含まれていることご了承ください。
# 全4編のゴール
最終的なゴールはDatabricks SQLのダッシュボード上で以下のような要件を満たすグラフを作成します。
・日にちごとの残タスク数の推移を表示
・残タスクの数はプロジェクトごとに積み上げて表示
・グラフに表示する残タスク数を、タスクの保持者と難易度でパラメーター機能を用いて絞り込み可能

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2704291/f9c5a17e-f63e-10dd-9a84-39127dcb1838.png)
# テーブルの加工
必要なモジュールをインポートします。
```python
import pandas as pd
from pyspark.sql.functions import *
from pyspark.sql.window import Window
```
前回にDelta化したテーブルが保存されているデータベースへ移動します。
```sql
%sql
USE for_sample
```
テーブルを読み込みます。
```python
df = spark.sql("select * from backlog_data")
display(df)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2704291/57febbb8-03a8-0467-0240-13453aafe7af.png)

このテーブルではタスク保持者、難易度のパラーメーター機能を持ったプロジェクトごとに残タスク数が積み上がるようなビジュアルを表現できません。

なぜなら、以下のような問題から時系列に沿って残タスク数を導くことができないからです。
・タスク完了日以外は歯抜けになっている
・テーブルには完了したタスクの情報のみしかない
・残タスク数を導く情報が不足している

上記の問題を解消するために以下のようなテーブルの加工をおこなっていきます。
・歯抜けの日付を補完する
・各日付について完了したタスク以外の情報を持たせられる状態をつくる
・タスクが完了数を累積値で格納していく
・残タスク数を示すための総タスク数を格納する

まず、日付未満はこの先使わないので切り捨てます。
```python
# 時刻の切り捨て
df = df.withColumn("completion_date",date_trunc("day", "completion_date"))
display(df)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2704291/a7812492-2ecc-c532-3647-dacf86237f40.png)
日付の最小を取ります。
```python
# 日付の最小値
df.agg(min("completion_date")).show()
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2704291/e973ecba-57eb-89d2-d5e1-eb2a352865bc.png)
日付の最大も取ります。
```python
# 日付の最大値
df.agg(max("completion_date")).show()
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2704291/a05d2df2-18a8-77dc-421d-5565a2a47e5e.png)

一旦今までのテーブルは置いておき(後に利用)、先ほど取得した日付の最大値と最小値の範囲で、新しい一日ごとの日付のテーブルを作成します。
これで歯抜けの日付がなくなります。
```python
# 日付データの作成
date = pd.date_range("2022-02-28","2022-06-27")
date_column = "date_range"

date = pd.DataFrame({date_column:date})
date = spark.createDataFrame(date)

display(date)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2704291/af4bb49f-4c92-db9a-aa7e-412842c499ce.png)
次に、日付ごとに各プロジェクト、名前、難易度のすべての組み合わせでタスクが完了した、していないにかかわらず情報を持たせられるような加工を行います。

カテゴリーのみの入ったデータを作成します。
以下、実行するとdistinct()で重複を除き、10種類のプロジェクトが存在することがわかります。
```python
# カテゴリーデータ
category = df.select("category").distinct()
display(category)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2704291/d48d9294-b2bb-b223-16ec-0088098d7ad1.png)
名前のみ、難易度のみの入ったデータも同様に作成します。
同じく重複を除き、名前は5人、難易度は3種類あります。
```python
# 名前テーブル
name = df.select("name").distinct()
display(name)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2704291/834f5a32-fa6d-2c21-efb7-719a86bcb60d.png)
```python
# 難易度を示すテーブル
level = df.select("level").distinct()
display(level)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2704291/77dce0e7-7347-8907-2be1-78bd91999478.png)

作成した日付、カテゴリー、名前、難易度のみのテーブルで交差結合を行います。
これにより、どの日付であっても各プロジェクト、名前、難易度のすべての組み合わせでタスク状況について情報を追加できるようになります。
```python
# 交差結合
tbl = date.crossJoin(category)
tbl = tbl.crossJoin(name)
tbl = tbl.crossJoin(level)

display(tbl)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2704291/f8e17afc-63f1-30d1-9cc7-a0cbfcdaeaf1.png)

各組み合わせで交差結合を行ったので、テーブルは120日(4か月)×10プロジェクト×5人×3難易度=18000行になりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2704291/9bebad42-f374-cf37-67a1-6481104d9837.png)
次のステップとして、新しく作ったテーブルに元のテーブルを結合させ、タスクの完了した情報を持たせます。

元テーブルのカラムの名前を新しく作成したテーブルと重複しないように変更を行います。
```python
# カラム名を重複しないよう変更
df = df.withColumnRenamed("category","category_o")\
       .withColumnRenamed("name","name_o")\
       .withColumnRenamed("level","level_o")
display(df)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2704291/79595f0d-5147-99f6-42db-20c9d0711b9a.png)

新しいテーブルを基準に元テーブルを左外側結合します。

実行すると以下の画像にあるテーブルの116行目のように、タスクが完了した行には欠損がないようなテーブルが出来上がります。
欠損がある行に関しては特にその日においてタスクが完了されていないという意味になります。
```python
# 元テーブルと結合
df = tbl.join(df,(tbl.date_range == df.completion_date)\
               & (tbl.category == df.category_o)\
               & (tbl.name == df.name_o)\
               & (tbl.level == df.level_o),"left_outer")
display(df.orderBy(col('date_range').asc()))
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2704291/29d15916-f7b4-7ffa-1d9c-cf8a2a273ed5.png)

データの数をもう一度確認すると以下の画像から結合前の18000より若干増えていることがわかります。
これはある日付でプロジェクト、名前、難易度の組み合わせが同じタスクが複数完了されることによって外部結合した時に重複した行がでてきたと考えられます。
一旦、次の作業に進み、最後に重複した行について処理します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2704291/eb66f3b7-aa3a-4b29-eafc-d89010e59a2f.png)

タスクが完了した行に1というラベルを付けます。
テーブルの108行目の「is_done」という名前のカラムに1とついているのが確認できます。
```python
# タスクが完了した日付を含む行にラベル付け
df = df.withColumn("is_done",when(df.completion_date.isNotNull(),1).otherwise(0))
display(df)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2704291/e19b4816-db89-00ef-7c74-a0d912c9c6ad.png)
ここで、この先必要のないカラムを削除します。
```python
# 不要なカラムの削除
df = df.drop("completion_date","task_id","category_o","name_o","level_o")
display(df)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2704291/bfae6a1f-e80c-99fc-9ef8-7f72ad0aedc6.png)
ここからタスク完了数の累積値と総タスク数を求めるステップに入ります。

まず、準備としてウィンドウを定義します。
時系列で並び変えてから、カテゴリー、名前、難易度で「partition」を行い、計算する範囲を「rangeBetween」で指定します。
「w1」は累積値に、「w2」は総タスク数を求める際に使用します。
```python
# 日付ごとに業者名、指摘者、ファイル名でpartitionを行う
w1 = Window.partitionBy(df.category,df.name,df.level).orderBy(df.date_range).rangeBetween(-sys.maxsize,0)
w2 = Window.partitionBy(df.category,df.name,df.level).orderBy(df.date_range).rangeBetween(0,sys.maxsize)
```
まず累積値を計算します。
定義したウィンドウごとにタスク完了ラベルの付いたカラムの合計値をとることで、累積値を計算することができます。
実行できると各組み合わせでの累積値の格納が完了しました。
```python
# 累積カラムを追加し、ウィンドウごとに累積値を格納する
df_total = sum(df.is_done).over(w1)
df = df.withColumn("accumulation",df_total)

display(df)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2704291/f684a7cd-eb0e-cef0-fe80-a6fd287ed152.png)

次に総タスク数も計算します。
2つ目に定義したウィンドウでつい先ほど作成した累積カラムの最後の行をとることによって、タスクの総数を導くことができます。
```python
# 総タスク数を示すカラムを追加し、ウィンドウごとに総タスク数を格納する
all_task_nums = last(df.accumulation).over(w2)
df = df.withColumn("task_nums",all_task_nums)
display(df)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2704291/fa573582-34cf-4b60-fdf4-f5a59df2d921.png)

重複した行を削除し、データの数を確認すると元の18000行に戻っていることが確認できます。
```python
# 重複行の削除
df = df.distinct()
df.count()
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2704291/096da507-8f57-00a7-2f81-2e8591ed1e2b.png)

必要な加工をすべて終えたので、加工済みテーブルをデータベース保存します。
```python
path = "dbfs:/FileStore/tables/backlog_processed"
table_name = "backlog_proccessed"

# 加工済みテーブルの保存
df.write.format("delta").option("path",path).saveAsTable(table_name)
```
# まとめ
これで加工済みテーブルが完成しました。
[次回](https://qiita.com/Tsubasa-Koyama/items/730af8bf32008ba630c2)はDatabricks SQLを利用し、加工済みのテーブルを使用してクエリを作成していきます。
# 技術面の参考資料
## 結合処理
今回交差結合と左外結合を使用しました。
他にもテーブルの結合の種類はたくさん存在し、[こちら](https://blog.kozakana.net/2020/12/try-all-how-options-of-join-with-pyspark/)の記事が参考になります。
## window関数を使用した累積値の計算
累積値を計算する範囲としてwindowを作成しましたが、[こちら](https://qiita.com/bwtakacy/items/ef089361b11f36965790)の記事を参考にしております。