PySpark 開発時に知っておくべき７つのテーマ

## 概要

PySpark 開発時に知っておくべき７つのテーマである次の項目を説明します。

1. SparkSession 、 Spark Dataframe 、および、 Spark table
2. `Dataframe in, Dataframe out`
3. 変数とクラスの利用
4. 変数からデータフレームを作成
5. データフレームにおけるメタデータとデータを取得
6. Spark SQL を PySpark で利用
7. 制御フローによる PySpark処理の実行

本記事のコードを含むノートブックを以下のリンクに保存しておりますので、興味がある方は Databricks 等の環境にインポートして実行してみてください。

https://github.com/manabian-/databricks_development_practices/blob/main/docs/jp/notebooks/T10__L100__010__PySpark%20%E9%96%8B%E7%99%BA%E6%99%82%E3%81%AB%E7%9F%A5%E3%81%A3%E3%81%A6%E3%81%8A%E3%81%8F%E3%81%B9%E3%81%8D%EF%BC%97%E3%81%A4%E3%81%AE%E3%83%86%E3%83%BC%E3%83%9E.ipynb

## 本記事の位置付け

次の開発ガイドシリーズにおけるSpark概要分野の１記事であり、リンク先には記事にて記事の全体像を整理している。

| GroupID | 分野               |
| ------- | ---------------------- |
| **T10**     | **Spark概要**                  |
| T20     | データエンジニアリング |
| T30     | データ品質チェック     |
| T40     | データサイエンス       |                                         |
| T50     | メタデータデプロイ     |
| T60     | テスト                 |
| T70     | DevOps                 |


https://qiita.com/manabian/items/67bcb6c5823bc4d5d944

## 前提知識

- Spark （PySpark、Spark SQL）の基本的な知識
- データエンジニアリング の基本的な知識
- Python の基本的な知識

## 1. SparkSession 、 Spark Dataframe 、および、 Spark table

Sparkの処理は、`SparkSesson`にデータ処理を定義していき、アクションによりデータ処理が行われること（遅延評価）が特徴。Databricks 利用時にはノートブックをクラスターにアタッチする際に自動で定義されるため意識することはあまりないが、ノートブック外からテスト実行を行う際には手動の定義が必要となる。遅延評価により、処理内容の分割・共通化を分割することで単体テスト実施に合わせた構成とすることができ、システムにおける品質や拡張性が高くなる。

```python
# Databricks では、ノートブックのアタッチ時に自動で`Spark`という変数に`SparkSession`が設定される
spark
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/d2f4279a-fcf3-2c49-c91e-34e26ce2d097.png)


```python
# 外部から実行する場合には、`SparkSession.builder.getOrCreate`にて定義することが前提
# 定義済みの`SparkSession`がある場合には、それを取得
from pyspark.sql import SparkSession
spark = SparkSession.builder.getOrCreate()
spark
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/143d3d03-e637-7874-e4d8-ef8b4187ea48.png)


```python
data = [
    {'int_col': '1'},
]

# `SparkSession`に処理（データの読み込み）を追加
spark.createDataFrame(data)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/4b438594-997e-cf35-3c81-3c40fef32e71.png)


```python
# `show`というアクションを含めることでデータ処理(`Spark jobs`)が実行される
spark.createDataFrame(data).show()
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/f22b22d0-7ddd-d87a-822d-b2ee4b09d766.png)

### 1-2. `SparkSession` を再利用した関数（メソッド）の定義

Spark処理開始時に`SparkSession`を定義して、処理を共通化した関数内で`getActiveSession`により定義済みの`SparkSession`を取得。

```python
# データフレームを作成するメソッドを定義
from pyspark.sql import SparkSession
def create_test_DataFrame():
    # `getActiveSession`にて定義済みの`SparkSession`が取得のみも可能
    spark = SparkSession.getActiveSession()
    data = [
        {'int_col': '1'},
    ]
    return spark.createDataFrame(data)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/7cfbd940-5b0d-c9cf-0d3a-d59a08ae8b67.png)


```python
# `SparkSession`を定義
from pyspark.sql import SparkSession
spark = SparkSession.builder.getOrCreate()

# メソッドを利用してデータを表示
create_test_DataFrame().show()
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/e21fd47e-21f9-6127-b007-3d66b54edaa1.png)

### 1-3. Spark.conf の設定

Spark の処理実行時のパラメータとして`spark.conf`があり、`SparkSession`にて設定を変更可能。

- [Configuration - Spark 3.3.0 Documentation (apache.org)](https://spark.apache.org/docs/latest/configuration.html)


```python
try:
    spark.conf.get('spark.databricks.delta.schema.autoMerge')
except:
    print('not set')
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/bf68980f-7ab0-c44c-e721-37dd21b59aec.png)

```python
# `spark.conf.set`にて値を設定
spark.conf.set('spark.databricks.delta.schema.autoMerge', 'true')
print(spark.conf.get('spark.databricks.delta.schema.autoMerge'))
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/723b89d2-f86b-c949-1271-7debe215ce25.png)


```python
# 設定をリセット
spark.conf.unset('spark.databricks.delta.schema.autoMerge')
try:
    spark.conf.get('spark.databricks.delta.schema.autoMerge')
except:
    print('not set')
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/c1a50bc5-8ec3-e8ca-9aa2-4640c1b5a4db.png)


```python
# Spark SQL でも設定可能
spark.sql('set spark.databricks.delta.schema.autoMerge = true')
print(spark.conf.get('spark.databricks.delta.schema.autoMerge'))
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/fa03e008-c260-6a42-81c8-6aeb83fdbb26.png)

### 1-4. SparkSession の並列化処理時の注意事項

定義した`SparkSession`を multiprocessing などの並列化ライブラリーで共有すると正常に動作しないことがあるため、処理ごとに`SparkSession`の定義が必要となる。<br>
Databricks では`dbutils.notebook.run()`メソッドの利用が推奨されているなど、Spark プロバイダーごとに対応方法が異なる。

- [ノートブックでコードをモジュール化またはリンクする - Azure Databricks | Microsoft Docs](https://docs.microsoft.com/ja-jp/azure/databricks/notebooks/notebook-workflows)
- [Microsoft Spark Utilities の概要 - Azure Synapse Analytics | Microsoft Docs](https://docs.microsoft.com/ja-jp/azure/synapse-analytics/spark/microsoft-spark-utilities?pivots=programming-language-python#notebook-utilities)

### 1-5. Spark table の永続化

Spark tableは、メタストアに格納されることで、セッション終了後にも利用すること（テーブルの永続化）ができる。

メタストアとして、 Hive メタストアが利用されることが多いが、Spark プロバイダー固有のメタストアが提供されていることもある。

- [Unity Catalog - Azure Databricks | Microsoft Docs](https://docs.microsoft.com/ja-jp/azure/databricks/data-governance/unity-catalog/)
- [共有メタデータ モデル - Azure Synapse Analytics | Microsoft Docs](https://docs.microsoft.com/ja-jp/azure/synapse-analytics/metadata/overview)

### 1-6. テーブルとデータの分離

データレイクを理解するために、テーブルとデータが分離されているという概念を理解することが重要である。リレーショナルデータベース（RDB）ではテーブル = データとなっていることが多く、RDBに慣れている方はテーブルを管理すればデータも管理できると考える傾向にある。データレイクでは、１つのデータ（ディレクトリ）に複数のテーブルが紐づくことから、テーブルで管理する方法では重複操作が実施される可能性がある。管理単位を、テーブルとして捉えるのではなく、ディレクトリとして捉える必要がある。

```python
# データベースとテーブルを作成
db_name = 'T10__L100__010'
spark.sql(f'''
CREATE DATABASE IF NOT EXISTS {db_name}
''')

tbl_name = 'nation'
tbl_full_name = f'{db_name}.{tbl_name}'

spark.sql(f'''
CREATE OR REPLACE TABLE {tbl_full_name}
(
  N_NATIONKEY  integer
  ,N_NAME       string
  ,N_REGIONKEY  integer 
  ,N_COMMENT    string
)
USING delta
''')
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/b5cd7e9a-64b8-ea90-d3a1-087309c419c5.png)

```python
tbl_desc = spark.sql(f"DESC TABLE EXTENDED {tbl_full_name}")

tbl_desc.display()
tbl_location = (
    tbl_desc
    .filter('col_name = "Location"')
    .select("data_type")
    .collect()[0][0]
)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/a48e9268-ec18-6c32-42d5-3ad83a95d08a.png)

```python
# テーブルへのデータ書き込み
filepath = "dbfs:/databricks-datasets/tpch/data-001/nation/"

schema = """
  N_NATIONKEY  integer
  ,N_NAME       string
  ,N_REGIONKEY  integer 
  ,N_COMMENT    string
"""
   
df = (spark
       .read
       .format("csv")
       .schema(schema)
       .option("inferSchema", "True")
       .option("sep", "|")
       .load(filepath)
    )
df.write.format('delta').mode('overwrite').saveAsTable(tbl_full_name)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/f6e17338-79c6-beb9-2a7e-be15d0f48d44.png)


```python
# データとディレクトリの確認
spark.table(tbl_full_name).display()
display(dbutils.fs.ls(tbl_location))
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/9d3a338f-e8ed-f728-7ace-5d83c6f77919.png)


```python
# 既存のデータからテーブルを作成
tbl_name_2 = 'nation_2'
tbl_full_name_2 = f'{db_name}.{tbl_name_2}'

spark.sql(f'''
DROP TABLE IF EXISTS {tbl_full_name_2}
''')
spark.sql(f'''
CREATE TABLE {tbl_full_name_2}
USING delta
LOCATION '{tbl_location}'
''')

print(spark.sql(f'SHOW CREATE TABLE {tbl_full_name_2}').collect()[0][0])
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/f34cf9fb-8899-1a8d-7365-7294f4e2c628.png)


```python
# テーブルにデータを書き込んでしまうと。。。
df.write.format('delta').mode('append').saveAsTable(tbl_full_name_2)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/df3bae98-dccc-6636-34b1-0ff48ea49414.png)

```python
# 同じデータが書き込まれてしまう
spark.table(f'delta.`{tbl_location}`').display()
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/42acd2fa-7bb8-ba46-9912-00f79e6a3efc.png)

## 2. `Dataframe in, Dataframe out`

Spark のデータエンジニアリングを実施する際には、`Dataframe in, Dataframe out`、つまり、データフレームを引数としてデータフレームをリターンとするメソッドを組み合わせて開発を行うことが推奨されている。その理由の１つは、テストのやりやすさがある。

### 2-1. `Dataframe in, Dataframe out`の実施例

```python
# 1で作成したデータフレームの`int_col`が`string`型であるため、`int` 型にする必要がある
from pyspark.sql import SparkSession
spark = SparkSession.builder.getOrCreate()
df = create_test_DataFrame()
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/d863fb30-e150-fc62-0d34-9f5972b63a48.png)


```python
# データフレームの`int_col`カラムを`integer`型に変更する関数を定義
def cast_int_col_as_int(
    df,
):
    return df.withColumn('int_col', df.int_col.cast('int'))
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/839d7cd6-1e45-ae73-ad91-ac7edefeb1f6.png)


```python
# 関数により`int_col`カラムが`integer`型となることを確認
cast_int_col_as_int(df).printSchema()
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/6d100c8f-6ac4-f9ac-2cfd-0c4ae3702d85.png)


### 2-2. 単体テストの実施例

```python
# 関数に対する単体テストを実行

import unittest
from pyspark.sql import SparkSession

class Test__cast_int_col_as_int(unittest.TestCase):
    """`cast_int_col_as_int`関数に対する単体テスト"""
    def test__cast_int_col_as_int(self):
        """
        正常系検証
        """
        # テストデータを準備
        spark = SparkSession.builder.getOrCreate()
        data = [
            {'int_col': '1'},
            {'int_col': '12.3'},
            {'int_col': 'string'},
        ]
        test_df = spark.createDataFrame(data)
        
        # 想定結果を準備
        expect_date =[
            {'int_col': 1},
            {'int_col': 12},
            {'int_col': None},
        ]
        expected_schema = '''
        int_col integer
        '''
        expected_result = spark.createDataFrame(expect_date,expected_schema)
        
        # テスト対象の関数を実行
        result_df = cast_int_col_as_int(test_df)

        # テストの実行結果を確認
        self.assertEqual(
            result_df.collect(),
            expected_result.collect(),
        )

#
suite = unittest.TestLoader().loadTestsFromTestCase(Test__cast_int_col_as_int)
runner = unittest.TextTestRunner(
    verbosity=2,
)
runner.run(suite)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/9bfa940c-b497-021a-a8bb-2cc835e44e8d.png)

## 3. 変数とクラス

### 3-1. Spark Dataframeの不変性

PySpark における Spark データフレームは不変であるという特徴があり、処理の定義をリスト型変数や辞書型変数に格納できる。１つの変数に格納することで、開発時のデバックの実施が容易となる。

```python
# `spark_dataframes` 変数に定義した Spark データフレームを格納

spark_dataframes = {}
df = create_test_DataFrame()
spark_dataframes['first_dataframe'] = df
df_2 = cast_int_col_as_int(df)
spark_dataframes['second_dataframe'] = df_2
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/b96c17d7-c425-bedd-1fd0-14602be4d4d7.png)

```python
# `spark_dataframes`にて保持しているデータフレームのスキーマをループ処理で表示
print(spark_dataframes)
for spark_dataframe in spark_dataframes.values():
    spark_dataframe.printSchema()
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/c2a206eb-4365-a572-dfbc-c9f33313e0b3.png)


### 3-2. f-stringによりリテラルの置換

```python
view_name = 'test'

create_test_DataFrame().createOrReplaceTempView(view_name)

spark.sql(f'''
SELECT
  *
  FROM
    {view_name}
''').show()
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/3ba489fa-e904-dbb0-7989-33dc9cdbbdad.png)

### 3-3. Spark SQLにて変数を利用

spark.conf 経由で変数を渡すことが可能。

```python
spark.conf.set('dq.val.view_name', view_name)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/273de3fa-30c8-b52f-bef8-94638a0e9865.png)


```sql
SELECT
  *
  FROM 
    ${dq.val.view_name}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/6a2981d3-ab18-0e01-8669-1a8482dfc37d.png)


### 3-4. staticmethod によるメソッドの整理

ノートブック型の開発環境では`%run`コマンドにより他ファイルで定義したクラスや関数を読み込むことができるが、関数をクラスの`staticmethod` （暗黙の第一引数を受け取らないメソッド）として定義することで管理が容易となる。

参考リンク

- [@staticmethod](https://docs.python.org/ja/3.8/library/functions.html#staticmethod)

```python
class DataEngineeringSample:
    @staticmethod
    def create_test_DataFrame():
        # `getActiveSession`にて定義済みの`SparkSession`が取得のみも可能
        spark = SparkSession.getActiveSession()
        data = [
            {'int_col': '1'},
        ]

        # `SparkSession`に処理（データの読み込み）を追加
        return spark.createDataFrame(data)    
    
    @staticmethod
    def cast_int_col_as_int(
        df,
    ):
        return df.withColumn('int_col', df.int_col.cast('int'))
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/0cfd1013-cb2d-2d84-c4e6-ac1138b0a03e.png)

```python
DataEngineeringSample.create_test_DataFrame().show()
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/52353e4f-3698-1dc9-aadd-c9750796b889.png)

### 3-5. クラス変数を config として利用

```python
# クラス変数を保持したクラスを定義
class SparkUtilities:
    spark_provider = 'databricks'
    @staticmethod
    def display(tgt_df):
        if SparkUtilities.spark_provider == 'databricks':
            tgt_df.display()
        if SparkUtilities.spark_provider == 'apache_spark':
            tgt_df.show()
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/3f073c4b-eede-8e7e-2977-96d69c407340.png)

```python
# クラス変数を変更して処理内容を変更
SparkUtilities.spark_provider = 'apache_spark'
SparkUtilities.display(
    spark.createDataFrame([{'int_col': '1'},]),
)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/44933762-ea0a-6f2b-ccac-01138c560f54.png)

## 4. 変数からデータフレームを作成

変数からデータフレームを作成する主な方法としては、次の３種類がある。単体テストのデータを用意する際などに利用する。

1. 辞書型リストを用いる方法
2. 多次元リストを用いる方法
3. `pyspark.sql.Row`メソッドを用いる方法

### 4-1. 辞書型リストを用いる方法

```python
import datetime

schema = '''
str_col string,
int_col integer,
date_col date
'''


data_001 = [
    {
        'str_col': 'abc',
        'int_col': 123,
        'date_col': datetime.date(2020, 1, 1),
    },
    {
        'str_col': 'def',
        'int_col': 456,
        'date_col': datetime.date(2020, 1, 2),
    },
]

df = spark.createDataFrame(
    data_001,
    schema,
)


df.show()
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/02c23c9a-cdaf-73c6-d0a3-3d9bb1e72772.png)


辞書型リストを用いる方法を、`collect`メソッドにより簡単に確認できる。

```python
df_2 = spark.createDataFrame(
    df.collect(),
    schema,
)

df_dict = []
for row_data in df.collect():
    df_dict.append(row_data.asDict())

df_2.show()
print(df_dict)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/534c44d3-77e0-6b83-65b6-03dc2501a373.png)


### 4-2. 多次元リストを用いる方法

```python
import datetime

schema = '''
str_col string,
int_col integer,
date_col date
'''

data_002 = [
    [
        'abc',
        123,
        datetime.date(2020, 1, 1)
    ],
    [
        'def',
        456,
        datetime.date(2020, 1, 2)
    ],
]

df = spark.createDataFrame(
    data_002,
    schema,
)


df.show()
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/5bd822e1-e1a9-bbe7-f92a-fa4d79cf6d22.png)


```python
# tuple in list でも同様のことが可能
import datetime

schema = '''
str_col string,
int_col integer,
date_col date
'''

data_002 = [
    (
        'abc',
        123,
        datetime.date(2020, 1, 1)
    ),
    (
        'def',
        456,
        datetime.date(2020, 1, 2)
    ),
]

df = spark.createDataFrame(
    data_002,
    schema,
)


df.show()
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/96a84545-aa6b-78e0-68b5-5a78b878e88c.png)

### 4-3. `pyspark.sql.Row`メソッドを用いる方法

```python
import datetime
from pyspark.sql import Row

schema = '''
str_col string,
int_col integer,
date_col date
'''

data_003 = [
    Row(
        str_col='abc',
        int_col=123,
        date_col=datetime.date(2020, 1, 1),
    ),
    Row(
        str_col='def',
        int_col=456,
        date_col=datetime.date(2020, 1, 2),
    ),
]

df = spark.createDataFrame(
    data_003,
    schema,
)

df.show()
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/2b791053-02a6-cacf-28b6-14a7ad00d89a.png)

`pyspark.sql.Row`の値の設定方法を、`collect`メソッドにより簡単に確認できる。

```python
df_2 = spark.createDataFrame(
    df.collect(),
    schema,
)

df_2.show()
print(df.collect())
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/116896b1-71e8-c0ef-53e5-1145805f7a62.png)

### 4-4. Spark のデータ型と Python 型の対応

Spark データフレームのデータ型に対応する Python のデータ型は次のドキュメントに記載。
- [Data Types - Spark 3.3.0 Documentation (apache.org)](https://spark.apache.org/docs/latest/sql-ref-datatypes.html)

```python
# サンプルデータフレームを作成
from pyspark.sql import Row
from decimal import *
import datetime

sample_Data = [
    Row(
        string_column='AAA',
        byte_column=1,
        integer_column=1,
        bigint_column=1,
        float_column=12.300000190734863,
        double_column=12.3,
        numeric_column=Decimal('12'),
        boolean_column=True,
        date_column=datetime.date(2020, 1, 1),
        timestamp_column=datetime.datetime(2021, 1, 1, 0, 0),
        binary_column=bytearray(b'A'),
        struct_column=Row(struct_string_column='AAA', struct_int_column=1),
        array_column=['AAA', 'BBB', 'CCC'],
        map_column={'AAA': 1},
    )
]
schema = '''
--文字型
string_column string,

--整数型
byte_column byte,
integer_column integer,
bigint_column bigint,

--浮動小数点型
float_column float,
double_column double,
numeric_column numeric,

--真偽型
boolean_column boolean,

--日付時刻
date_column date, 
timestamp_column timestamp,

--バイナリー型
binary_column binary,

--複合型
struct_column struct<
    struct_string_column :string,
    struct_int_column    :int
>,
array_column array<string>, 
map_column map<string, int>
'''

spark.createDataFrame(sample_Data, schema).show()
spark.createDataFrame(sample_Data, schema).printSchema()
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/1d136019-1da7-8476-2b26-402b6447f14b.png)


## 5. データフレームにおけるメタデータとデータを取得

Spark データフレームからカラム名等のメタデータを、次のメソッドを用いることで取得できる。２つのデータフレームを比較してその差分を変更するメソッドを開発する際などに利用。

- `DataFrame.schema.jsonValue()['fields']`
- `DataFrame.columns`
- `DataFrame.dtypes`


Scala のみで利用できる DDL 文字列を取得する場合には、次のように記載する。

```python
json_data = df.schema.json()
ddl_string = spark.sparkContext._jvm.org.apache.spark.sql.types.DataType.fromJson(json_data).toDDL()
`````

```python
# サンプルデータフレームを作成
from pyspark.sql import Row
from decimal import *
import datetime

sample_Data = [
    Row(
        string_column='AAA',
        byte_column=1,
        integer_column=1,
        bigint_column=1,
        float_column=12.300000190734863,
        double_column=12.3,
        numeric_column=Decimal('12'),
        boolean_column=True,
        date_column=datetime.date(2020, 1, 1),
        timestamp_column=datetime.datetime(2021, 1, 1, 0, 0),
        binary_column=bytearray(b'A'),
        struct_column=Row(struct_string_column='AAA', struct_int_column=1),
        array_column=['AAA', 'BBB', 'CCC'],
        map_column={'AAA': 1},
    )
]
schema = '''
--文字型
string_column string,

--整数型
byte_column byte,
integer_column integer,
bigint_column bigint,

--浮動小数点型
float_column float,
double_column double,
numeric_column numeric,

--真偽型
boolean_column boolean,

--日付時刻
date_column date, 
timestamp_column timestamp,

--バイナリー型
binary_column binary,

--複合型
struct_column struct<
    struct_string_column :string,
    struct_int_column    :int
>,
array_column array<string>, 
map_column map<string, int>
'''

df = spark.createDataFrame(sample_Data, schema)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/de53b7e7-2ada-20a0-e640-57ee3dbbd626.png)


### 5-1. カラムの詳細情報を取得

```python
df.schema.jsonValue()['fields']
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/845bd029-c9b1-6e46-0620-fe60f807cb2a.png)


### 5-2. カラム名のリストを取得

```python
df.columns
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/f2ce9a4b-1d23-2ca2-4392-0fd78ba43105.png)

### 5-3. カラム名とデータ型のタプルのリストを取得

```python
df.dtypes
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/752fb60e-dd64-7c50-220d-0649148bd45f.png)

### 5-4. DDL 文字列の取得

```python
json_data = df.schema.json()
ddl_string = spark.sparkContext._jvm.org.apache.spark.sql.types.DataType.fromJson(json_data).toDDL()
print(ddl_string)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/a5d24ee1-3013-e2c7-86ab-d7de16922549.png)

### 5-5. データフレームの単一のデータを取得

`collect`メソッドを利用するため、事前にデータフレーム操作によりデータを絞ることが重要

```python
# データを準備
import datetime
from pyspark.sql import Row

sample_Data = [
    Row(
        string_column='AAA',
        bigint_column=1,
        date_column=datetime.date(2020, 1, 1),
    ),
    Row(
        string_column='bbb',
        bigint_column=2,
        date_column=datetime.date(2020, 2, 2),
    ),
    Row(
        string_column='CCC',
        bigint_column=3,
        date_column=datetime.date(2020, 3, 3),
    ),
]

df = spark.createDataFrame(sample_Data)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/bd7a792e-a763-53d9-9917-ff204c3bb9c5.png)

```python
# データを絞る
single_value_df = (
    df
    .filter('string_column = "AAA"')
    .select('string_column')
)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/8a2529af-fa70-c91b-e8fc-9d221e30d612.png)


```python
# データフレームの単一値を取得
# 位置で指定する方法
print(single_value_df.collect()[0][0])

# カラム名をキーとして指定
print(single_value_df.collect()[0]['string_column'])

# カラム名を指定
print(single_value_df.collect()[0].string_column)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/568ce2a6-f6be-de6d-3b11-94003a56414f.png)

### 5-6. データフレームの複数のデータを取得

```python
# データを準備
import datetime
from pyspark.sql import Row

sample_Data = [
    Row(
        string_column='AAA',
        bigint_column=1,
        date_column=datetime.date(2020, 1, 1),
    ),
    Row(
        string_column='bbb',
        bigint_column=2,
        date_column=datetime.date(2020, 2, 2),
    ),
    Row(
        string_column='CCC',
        bigint_column=3,
        date_column=datetime.date(2020, 3, 3),
    ),
]

df = spark.createDataFrame(sample_Data)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/5aa1fa9b-8f16-95d2-58ad-57729c75f52e.png)


```python
# データフレームの複数値を取得

# 位置で指定する方法
multi_values_df = (
    df
    .select('string_column')
)

df_values = []
for row_data in multi_values_df.collect():
    df_values.append(row_data[0])
print(df_values)


# カラム名をキーとして指定
tgt_col = 'string_column'
df_values = []
for row_data in df.collect():
    df_values.append(row_data[tgt_col])
print(df_values)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/000cab1a-4ab3-761c-6776-071ddc111d3f.png)

## 6. Spark SQL を PySpark で利用

PySparkでは主に`pyspark.sql.functions`を利用するのですが、次のメソッドでは Spark SQL(Spark SQL, Built-in Functions)が利用可能。
- spark.sql
- selectExpr
- expr

次のメソッドでは、SQL とほぼ同じ記法が利用可能。
- filter

データフレームに別名（alias）を設定する際には次のメソッドを利用
- alias

### 6-1. `spark.sql`の利用例

```python
df = spark.sql('''
SELECT 1 as int_col
UNION ALL
SELECT 12.3 as int_col
UNION ALL
SELECT "string" as int_col
''')

df.show()
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/3e647eb7-93f3-c79a-1d25-1da4883f4dd1.png)


### 6-2. `selectExpr`の利用例

```python
df.selectExpr('CAST(int_col as int) as int_col').show()
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/2aa47f7c-fa3e-5bb3-6d13-4d54f80f3c5e.png)

### 6-3. `expr`の利用例

```python
from pyspark.sql.functions import expr
df.withColumn('int_col', expr('CAST(int_col as int)')).show()
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/7cb9fa86-d5b3-8fb4-a0af-35785088774c.png)


### 6-4. `filter`の利用例

```python
(
    df
    .filter('int_col = "string"')
    .show()
)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/ff3edb30-21c3-b046-9b3f-b3c22c09a347.png)


### 6-5. `alias`の利用例

```python
(
    df
    .alias('test')
    .filter('test.int_col = "string"')
    .show()
)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/d665da0a-d234-6e92-5cc9-a1ba1b06ac34.png)

## 7. 制御フローによる PySpark 処理の実行

### 7-1. データフレームのデータ型を他のデータフレームに基づき変換

```python
schema_01 = '''
str_col string,
int_col string,
date_col string
'''

schema_02 = '''
str_col string,
int_col integer,
date_col date
'''

schema_03 = '''
str_col string,
int_col integer
'''

data_01 = [
    [
        'abc',
        '123',
        '2020-01-01'
    ],
    [
        'def',
        '456',
        '2020-01-01'
    ],
]

df_1 = spark.createDataFrame(data_01,schema_01)
df_2 = spark.createDataFrame([],schema_02)
df_3 = spark.createDataFrame([],schema_03)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/d95b8fb2-7831-edee-6e8c-c450af962009.png)


```python
# `df_2`のデータ型に、`df_1`のデータ型を変更

df_2_schema = df_2.schema.jsonValue()['fields']

df_2_col_names_and_data_types = {}
for df_2_col in df_2_schema:
    df_2_col_names_and_data_types[df_2_col['name']] = df_2_col['type']

print(df_2_col_names_and_data_types)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/07474725-c8ae-bdfc-58ea-5086b25f081e.png)


```python
# 変換対象のデータフレームを準備
tgt_df = df_1
tgt_df.printSchema()


# `df_1`のスキーマと`df_2`のスキーマが異なるカラムのデータ型を変更
df_1_schema = df_1.schema.jsonValue()['fields']
for df_1_col in df_1_schema:
    df_1_col_name = df_1_col['name']
    df_1_col_type = df_1_col['type']
    df_2_col_type =  df_2_col_names_and_data_types.get(df_1_col_name, '')
    if df_1_col_type != df_2_col_type:
        if df_1_col_type == 'date':
            tgt_df = tgt_df.withColumn(df_1_col_name, to_date(tgt_df[df_1_col_name], date_format))
        else:
            tgt_df = tgt_df.withColumn(df_1_col_name, tgt_df[df_1_col_name].cast(df_2_col_type))

tgt_df.printSchema()
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/c565cb8d-9042-d838-28d6-9cde74ccd48f.png)

### 7-2. カラムのリストから SELECT 文の カラム名を生成

```python
# `df_3`でのみ保持している２つのカラムを取得する SELECT 文を生成
df_3_cols = df_3.columns
print(df_3_cols)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/ca62d7d9-21af-ca80-059d-d74230eb2b84.png)


```python
select_cols = '\n  ,'.join(df_3_cols)
print(select_cols)
print('\n')

sql = f'''
SELECT
  {select_cols}
  FROM
    TEST
'''
print(sql)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/1d7e2d47-4892-3379-30e4-902e519b95a5.png)


```python
df_1.createOrReplaceTempView('test')
spark.sql(sql).show()
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/dcf69207-dd89-ae87-20dc-4d58e7ba675f.png)