PandasとSparkのPivotとMelt

こちらの記事にも関係するお話です。

https://kakehashi-dev.hatenablog.com/entry/2022/12/24/090000

とてもざっくりした説明としては、横長のテーブルを縦長に変換するのが`melt(unpivot)`、その逆が`pivot`です。

![](https://www.salesanalytics.co.jp/wp-content/uploads/2021/04/795316b92fc766b0181f6fef074f03fa-5.jpg)

詳しくはこちらの記事をご覧ください。

https://www.salesanalytics.co.jp/datascience/datascience021/

で、少し昔はSparkでは`melt`がサポートされていなかったので、pandasに変換して処理していました。ところが、Spark 3.4.0で[サポート](https://spark.apache.org/docs/latest/api/python/reference/pyspark.sql/api/pyspark.sql.DataFrame.melt.html)されていました。

これはこれで嬉しいのですが、pandasとの比較が気になるところです。ということで、pandas、Spark(PySpark/Pandas on Spark)でのmelt/pivotをウォークスルーしました。実際にコーディングする際には、これらを使い分けするケースが出てくると思いますので、ご参考になれば。

# 概要

横型のテーブルを縦型に変換。

||IDとして用いる列|melt(unpivot)する列|variable変数の列|value変数の列|
|---|---|---|---|---|
|pandas ([pandas.melt](https://pandas.pydata.org/docs/reference/api/pandas.melt.html))|id_vars|value_vars|var_name|value_name|
|Pandas on Spark([pyspark.pandas.DataFrame.melt](https://pandas.pydata.org/docs/reference/api/pandas.melt.html))|d_vars|value_vars|var_name|value_name|
|PySpark([pyspark.sql.DataFrame.melt](https://spark.apache.org/docs/latest/api/python/reference/pyspark.sql/api/pyspark.sql.DataFrame.melt.html))|ids|value|variableColumnName|valueColumnName|

**pivot**

縦型のテーブルを横型に変換。

||ピボットデーブルの列を構成する列|インデックスの列|新規の列に分配される値を持つ列|
|---|---|---|---|
|pandas([pandas.pivot](https://pandas.pydata.org/docs/reference/api/pandas.pivot.html))|columns|index|values|
|Pandas on Spark([pyspark.pandas.DataFrame.pivot](https://spark.apache.org/docs/latest/api/python/reference/pyspark.pandas/api/pyspark.pandas.DataFrame.pivot.html))|columns|index|values|
|PySpark([pyspark.sql.GroupedData.pivot](https://spark.apache.org/docs/latest/api/python/reference/pyspark.sql/api/pyspark.sql.GroupedData.pivot.html))|pivot_col|-|values|

[こちら](https://covid19.mhlw.go.jp/public/opendata/newly_confirmed_cases_daily.csv)で公開されている日本のCOVID-19の感染者数データを使います。

# pandas

## melt

```py
import pandas as pd

pdf = pd.read_csv("https://covid19.mhlw.go.jp/public/opendata/newly_confirmed_cases_daily.csv")
display(pdf)
```

日付と都道府県名の列から構成されている横長のテーブルです。都道府県名の列の値は日付における感染者数となっています。

![Screenshot 2025-02-19 at 9.28.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/e3d78855-dcc3-442a-b303-8e465c79d407.png)

- id_vars：IDとして利用する変数（カラム）
- value_vars：melt する変数（カラム）、無指定の場合はid_vars以外の変数全部
- var_name：variable変数の変数（カラム）名、無指定の場合はvariableが変数（カラム）名になる
- value_name：value変数の変数（カラム）名、無指定の場合はvalueが変数（カラム）名になる

ID列は`Date`、variableは`Prefecture`、valueは`Cases`としてmeltします。

```py
# 縦長に変換
pdf = pdf.melt(id_vars=["Date"], var_name="Prefecture", value_name="Cases")
display(pdf)
```

`Date`、`Prefercture`、`Cases`から構成される縦長のテーブルに変換されました。

![Screenshot 2025-02-19 at 9.30.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/6fe44d96-26aa-40bb-a47f-2f03ca10dd77.png)

## pivot

元に戻します。列名などに手を加えています。

```py
pdf = pdf.pivot(index=["Date"], columns="Prefecture", values="Cases")
pdf = pdf.reset_index()
pdf.columns.name = None
display(pdf)
```

`Date`は文字列になっていますが横長の列に戻りました。

![Screenshot 2025-02-19 at 9.32.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/cdfb44ce-2306-4862-a25e-f4674d7f5b5b.png)

# Pandas on Spark

基本的にpandasと同じです。そもそもそれを目指して開発されたものですので。ただ、Pandas on Sparkの`read_csv`でURLの読み込みはできませんでした。

```py
import pandas as pd
import pyspark.pandas as ps

pdf = pd.read_csv("https://covid19.mhlw.go.jp/public/opendata/newly_confirmed_cases_daily.csv")
psdf = ps.from_pandas(pdf)
display(psdf)
```
![Screenshot 2025-02-19 at 9.33.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/0348b863-e71b-4772-9c2d-3e3a6ddd1fa1.png)

## melt

[pyspark\.pandas\.DataFrame\.melt — PySpark 3\.5\.4 documentation](https://spark.apache.org/docs/latest/api/python/reference/pyspark.pandas/api/pyspark.pandas.DataFrame.melt.html)

```py
melted_psdf = psdf.melt(id_vars=["Date"], var_name="Prefecture", value_name="Cases")
display(melted_psdf)
```
![Screenshot 2025-02-19 at 9.34.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/4772eea4-b2c0-4195-9a42-4600a5488823.png)


## pivot

[pyspark\.pandas\.DataFrame\.pivot — PySpark 3\.5\.4 documentation](https://spark.apache.org/docs/latest/api/python/reference/pyspark.pandas/api/pyspark.pandas.DataFrame.pivot.html)

```py
pivoted_psdf = melted_psdf.pivot(index="Date", columns="Prefecture", values="Cases")
pivoted_psdf = pivoted_psdf.reset_index()
pivoted_psdf.columns.name = None
display(pivoted_psdf)
```
![Screenshot 2025-02-19 at 9.35.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/5a886513-b64d-4003-8df1-dc1230fcd39b.png)


# PySpark

```py
import pandas as pd

pdf = pd.read_csv("https://covid19.mhlw.go.jp/public/opendata/newly_confirmed_cases_daily.csv")
sdf = spark.createDataFrame(pdf)
display(sdf)
```

## melt

[pyspark\.sql\.DataFrame\.melt — PySpark 3\.5\.4 documentation](https://spark.apache.org/docs/latest/api/python/reference/pyspark.sql/api/pyspark.sql.DataFrame.melt.html)

> New in version 3.4.0.

- **ids** str, Column, tuple, list
識別子として使用する列。単一の列または列名、または複数の列の場合はリストまたはタプルで指定できます。
- **values** str, Column, tuple, list, optional
アンピボットする列。単一の列または列名、または複数の列の場合はリストまたはタプルで指定できます。指定されている場合、空であってはなりません。指定されていない場合、識別子として設定されていないすべての列を使用します。
- **variableColumnName** str
変数列の名前。
- **valueColumnName** str
値列の名前。

`values`を`None`にすることでid列以外の全ての列がmeltされます。

```py
melted_sdf = sdf.melt(ids=["Date"], values=None, variableColumnName="Prefecture", valueColumnName="Cases")
display(melted_sdf)
```
![Screenshot 2025-02-19 at 9.37.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/0db5680e-8c7e-4ef3-98d3-c203f285bb1a.png)

## pivot

[pyspark\.sql\.GroupedData\.pivot — PySpark 3\.5\.4 documentation](https://spark.apache.org/docs/latest/api/python/reference/pyspark.sql/api/pyspark.sql.GroupedData.pivot.html)

- **pivot_col** str
ピボットする列の名前。
- **values** list, optional
出力DataFrameで列に変換される値のリスト。

pandasとの違いですが、こちらのpivotは`pyspark.sql.GroupedData.pivot`とあるように、`groupBy`して`GroupedData`に適用する必要があるという点です。グルーピング不要のデータフレームでも`GroupBy`が必要となります。[こちら](https://qiita.com/Tsubasa-Koyama/items/349b104f3f5c7452517d)でもしょうさいg説明されています。

あと、以下では文字列の`Date`をDate型に変換してます。

```py
from pyspark.sql.functions import *
from pyspark.sql.types import StringType, BooleanType, DateType

pivoted_sdf = (
    melted_sdf.groupBy("Date")
    .pivot("Prefecture")
    .sum("Cases")
    .withColumn("Date", to_date(col("Date"),"yyyy/M/d"))
    .orderBy("Date")
)
display(pivoted_sdf)
```
![Screenshot 2025-02-19 at 9.41.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/40485cfb-ecb5-4ae3-be6d-3574f75caef5.png)

横長に戻りました

### はじめてのDatabricks

[はじめてのDatabricks](https://qiita.com/taka_yayoi/items/8dc72d083edb879a5e5d)

### Databricks無料トライアル

[Databricks無料トライアル](https://databricks.com/jp/try-databricks)