PySpark書いてて混乱した、ArrayType、MapType、StructTypeを整理する

## 概要
表題の通りです。

データ構造としてどの型をどう使うのが適切なのか整理したかったので今回の記事にしてみました。

https://spark.apache.org/docs/latest/api/python/reference/pyspark.sql/api/pyspark.sql.types.StructType.html

https://spark.apache.org/docs/latest/api/python/reference/pyspark.sql/api/pyspark.sql.types.MapType.html

https://spark.apache.org/docs/latest/api/python/reference/pyspark.sql/api/pyspark.sql.types.ArrayType.html

具体例だけ見て納得したい方は[こちら](https://qiita.com/ishidao/items/58703aaf8bf0845c19ae#%E3%81%9D%E3%81%AE%E9%81%95%E3%81%84%E5%85%B7%E4%BD%93%E7%9A%84%E3%81%AB%E3%81%A9%E3%81%86%E3%81%84%E3%81%86%E3%81%93%E3%81%A8%E3%82%84%E3%81%AD%E3%82%93)。
あとは追加で、それぞれのデータ型の展開の仕方も書いてあるので気になる方は直接[こちら](https://qiita.com/ishidao/items/58703aaf8bf0845c19ae#%E3%83%87%E3%83%BC%E3%82%BF%E3%81%AE%E5%B1%95%E9%96%8B%E3%81%AE%E4%BB%95%E6%96%B9%E3%81%AE%E9%81%95%E3%81%84)へ。

## 整理
特徴を列挙するとこんな感じ。
### ArrayType(配列)

- 配列の各要素はデータ型が同じ必要がある
- 配列は順序を保有する
-  同じ要素を複数回含むことができる

### MapType(マップ型)
- キーバリューでデータを保存する
- 値は任意のデータ型を持てる
- 検索が高速

### StructType(構造体型)
- 各フィールドに名前がつけられる
- フィールドにアクセスするときはドット演算子やフィールド名でアクセス可能


ここだけ見ると、MapとStructの用途の違いがいまいちピンとこないので、それぞれ似ている特徴のデータ型2つを比較して、さらに具体的な用途まで落とし込んでみたいと思います。

## それぞれの違いに注目
### MapとStructの違い
- MapTypeはキーと値のペアを動的に保持するのでキー自体がデータの一部としてみなされる
- StructTypeはフィールドが固定されており、フィールド名があらかじめ決まっている。定義されたフィールドに対してデータの構造が明確に固定される

### ArrayとStructの違い
- Arrayは同じ方の複数の値を保持するが、個々の要素に名前やラベルがない
- Structは異なる型を持つ複数のフィールドを保持でき、フィールドに名前をつけられる


## その違い、具体的にどういうことやねん

### MapTypeの具体例
まず、MapTypeの用途を考えてみます。例えばユーザーのレビュー情報を保存するデータ構造を考えるとき、各商品ごとに評価やコメントなどの属性がキーとして動的に関連付けられます。

動的にと言っているくらいなので、データごとに異なるキーがあっても対応できます。

例えば下のように、user_1, user_2までは`rating`, `comment`しかキーはありませんでしたが、新たに`recommend`というキーを生やしたり、逆に`comment`というキーがなかったりがあり得ます。

```python
schema = MapType(StringType(), StringType())

data = [
    ("user_1", {"rating": "5", "comment": "Great product"}),
    ("user_2", {"rating": "3", "comment": "Average quality"}),
    ("user_3", {"rating": "4", "comment": "Good value", "recommend": "yes"}),
    ("user_4", {"rating": "2", "recommend": "no"})
]

df = spark.createDataFrame(data, ["user_id", "review_details"])
df.show(truncate=False)
```

![スクリーンショット 2024-09-17 15.44.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/845778/9cfda43d-1f6f-b59b-614f-2c974de21110.png)

データ構造として柔軟に持ちたいときは嬉しそう。

とはいえデータの一貫性を保つのが大変なので、例えば常に`recommend`があるかどうかを確認する処理を入れておくみたいな気遣いが必要そうですね。

```python
df = df.withColumn(
    "recommend", 
    when(df["review_details"].getItem("recommend").isNull(), "unknown")
    .otherwise(df["review_details"].getItem("recommend"))
)
```

### StructTypeの具体例
Mapとは違い、あらかじめ定義されたフィールド名とデータ型を持ちます。ここで、商品情報を保持するスキーマを考えたとき、このようなデータの利用例が考えられます。

```python
from pyspark.sql.types import StructType, StructField, StringType, FloatType

schema = StructType([
    StructField("item_id", StringType(), True),
    StructField("name", StringType(), True),
    StructField("price", FloatType(), True),
    StructField("category", StringType(), True)
])

data = [
    ("001", "Apple", 0.99, "Fruit"),
    ("002", "Banana", 0.79, "Fruit")
]

df = spark.createDataFrame(data, schema=schema)
df.show(truncate=False)
```
![スクリーンショット 2024-09-17 15.50.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/845778/a3c32515-8869-ab60-50f8-56ecc2b4e4f5.png)

Mapと違うのは、各フィールドのデータの型が事前に決まっていたり、動的なキーが存在しないことです。明らかに構造を定義できるときは適してそう。

あとは、よくネストされた形でStructをみることも多い様な気がします。

```python
schema = StructType([
    StructField("item_id", StringType(), True),
    StructField("details", StructType([
        StructField("name", StringType(), True),
        StructField("price", FloatType(), True),
        StructField("category", StringType(), True)
    ]))
])

data = [
    ("001", ("Apple", 0.99, "Fruit")),
    ("002", ("Banana", 0.79, "Fruit"))
]

df = spark.createDataFrame(data, schema=schema)
df.show(truncate=False)
```

商品の詳細をネストされた構造体で定義したものです。

![スクリーンショット 2024-09-17 16.09.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/845778/ca07d77d-f617-1e4f-7a82-5179ac85c1d5.png)

データにアクセスするときはドット表記やブラケット(`df["item_id"]`)みたいな形でアクセスできます。

## データの展開の仕方の違い

`array`と`map`だと、explodeで展開できます。`struct`は`explode`で展開できないのですが、`select`で愚直に展開することができます。

### structの場合
例えば以下のようなスキーマがある場合
```schema
root
 |-- user: struct (nullable = true)
 |    |-- name: string (nullable = true)
 |    |-- address: struct (nullable = true)
 |    |    |-- city: string (nullable = true)
 |    |    |-- state: string (nullable = true)
```

```python
df.select("user.name", "user.address.city", "user.address.state").show()
```
これで展開できます。各フィールドを直接指定する感じですね。なので `withColumn`を利用しても展開することができます。

### array<struct>の場合
いきなりですが、arrayがexplodeで展開できるのはいいとして、structのデータ構造をarrayで持っている場合のデータの展開方法を考えます。

下記のようなデータを定義してみます。

```python
from pyspark.sql import Row
from pyspark.sql.functions import explode

data = [
    Row(order_id="001", items=[
        Row(item_id="A001", content_type="Type1", date_modified="2024-09-01", no=1),
        Row(item_id="A002", content_type="Type2", date_modified="2024-09-02", no=2)
    ]),
    Row(order_id="002", items=[
        Row(item_id="A003", content_type="Type3", date_modified="2024-09-03", no=3)
    ])
]

df = spark.createDataFrame(data)
df.show(truncate=False)
```

![スクリーンショット 2024-09-17 16.25.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/845778/8e8b1886-c10d-3f50-3c19-d4c61367d647.png)

この形のデータ構造に対して、まずはarrayをならす必要があるので `explode`します。
```python
df_exploded = df.select("order_id", explode("items").alias("item"))
df_exploded.show(truncate=False)
```

![スクリーンショット 2024-09-17 16.26.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/845778/a0292fe7-3316-8e32-ce5d-db6d2ab67818.png)

展開するとこんな感じですが、各orderの詳細を見るためには結局`order_id`でgroupbyする必要があります。今回はどうデータを使うかは考えずに展開していきます。

ここまでくるとitemはstruct型なので、先ほどのように各フィールドを指定すれば展開できます。

```python
df_final = df_exploded.select(
    "order_id",
    "item.item_id",
    "item.content_type",
    "item.date_modified",
    "item.no"
)
df_final.show(truncate=False)
```

![スクリーンショット 2024-09-17 16.28.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/845778/cf1f23ef-5a99-c882-46a0-428dd28a4b6c.png)





## まとめ
| データ型      | 主な用途                                      | 特徴                                                                  | 例                          |
|---------------|-----------------------------------------------|-----------------------------------------------------------------------|-----------------------------|
| **ArrayType** | 同じ型の複数の値をリストで保持する場合          | 同じデータ型の要素が順序付きで格納される                              | `["apple", "banana"]`       |
| **MapType**   | キーと値のペアを保持し、キーで高速に検索したい場合 | キーが一意で、キーと値がペアになって格納される。そしてキーを動的に生成可能                        | `{"age": 30, "name": "A"}`  |
| **StructType**| レコード形式で複数の属性を持つデータを保持する場合| 固定された名前を持つフィールドを複数持ち、フィールドごとに型が異なる   | `{"name": "apple", "price": 1.99}` |


## ちなみに
上記で紹介したのはかなりシンプルなデータ構造であり、データは複雑に持たないに限るのですが、、、


とはいえ現実には `array<struct>` や `struct`が入り混じってるデータ構造もザラにあります。データは無闇にexplodeするのではなく、使える手札をきちんと理解している上でデータ要件に適切なデータ型を定義していきたいですね。