日本卸電力取引所の電力市場価格CSVを整形してGoogle Cloud Firestoreにアップロードした話

## はじめに

### 背景
皆さんの契約している電気契約プラン、市場連動型になりましたよね？　ね？

そこで電気代に最も影響するエリアプライス。
一応電力会社のWEBサイト/アプリでも確認できるのだが、いちいち開くのが面倒くさかったり、自動ログアウトされてしまったり…使い勝手が良いとは言えないのだ。

そこでこの度、指定した日のエリアプライスを取得できるWeb APIの開発に着手した。
これがあればマイコンなどで一目で分かったり、アプリのウィジェットで見れたりでき、一層節電に努められるであろう。

### 当記事の内容
当記事では前段階の、日本卸電力取引所（JEPX）が公開している電力市場価格のCSVファイルを整形して、Firestoreにアップロードするまでの手順を紹介する。
なおエリアプライスは前日に公開されるため、アップロードするデータは翌日分のみとする。
> 日本卸電力取引所
> 一般社団法人 日本卸電力取引所は、我が国で唯一の卸電力取引市場を開設・運営する取引所です。 
> https://www.jepx.jp/

なおCSVファイルからデータを取得するのは、日本卸電力取引所のサイトが表示内容をjsで切り替えており、スクレイピングではコードが冗長になりかねないためである。

## データ

今回整形に使用するCSVファイルは、[日本卸電力取引所>電力取引>市場情報]( https://www.jepx.jp/electricpower/market-data/spot/)から取得できる。
![スクリーンショット 2023-04-22 18.50.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3336887/538d0570-6d49-b318-f7d2-9dafdcbb1983.png)

↓実際に取得したCSVファイル
![スクリーンショット 2023-04-22 23.24.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3336887/3c8bb12d-c48d-137e-8dfc-9f2ebe86c0a6.png)
### 使用するカラム
使用するカラムは以下の通りである。

1. 受渡日：各エリアプライスが適用される日付。"yyyy/mm/dd"で格納されている。
1. 時刻データ：各エリアプライスが適用される時間。00:00~23:30を1~48にナンバリングされて格納されている。
1. システムプライス(円/kWh):取引業者間での参考価格なので、利用予定はないが一応アップロード内容に含める。
1. エリアプライス〇〇(円/kWh):北海道・東北・東京・中部・北陸・関西・中国・四国・九州エリアのエリアプライス。


### 参考）システムプライス・エリアプライスについて
> ⽇本卸電⼒取引所（JEPX）システムプライスとは、前日渡しスポット市場における1日24時間を30分単位（合計48個）に分け、JEPXで日本全国の売り手と買い手の入札情報を合成し、需要と供給が折り合う交点の価格をシステムプライスといいます。
https://kankyo-ichiba.jp/

> ⽇本卸電⼒取引所（JEPX）エリアプライスとは、電力市場として区分される地域は、各地域の大手電力会社（旧一般電気事業者）が管轄する沖縄電力管轄を除いた9地域です。各地域の価格差は隣接するエリア同士が連系線によって融通し合う電力量差異により生じるため計算を分けて行う必要があります。これにより算出された約定価格をエリアプライスといいます。
https://kankyo-ichiba.jp/

## 前処理

気になっていたのでデータ処理にはPandasよりも高速・書きやすいと噂のPolarsを使用する。

### 読み込み
今回使用するCSVファイルは2023年度分の`spot_summary_2023.csv`である。
読み込みカラムは消費メモリ削減のため、[使用するカラム](https://qiita.com/drafts/71f98eed279c1ca0c8dd/edit#%E4%BD%BF%E7%94%A8%E3%81%99%E3%82%8B%E3%82%AB%E3%83%A9%E3%83%A0)のみとする。

````python
import polars as pl

# 必要なカラムのみ取得
columns = ['受渡日', '時刻コード', 'システムプライス(円/kWh)', \
            'エリアプライス北海道(円/kWh)', 'エリアプライス東北(円/kWh)', 'エリアプライス東京(円/kWh)', \
            'エリアプライス中部(円/kWh)', 'エリアプライス北陸(円/kWh)', 'エリアプライス関西(円/kWh)', \
            'エリアプライス中国(円/kWh)', 'エリアプライス四国(円/kWh)', 'エリアプライス九州(円/kWh)']

df = pl.read_csv("spot_summary_2023.csv", columns=columns)
````

↓読み込まれたdataframe
![スクリーンショット 2023-04-22 19.37.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3336887/a435a1c5-49d3-624a-f54f-94c53e43aecb.png)

### 受渡日の絞り込み

Firestoreにアップロードするデータは翌日分のみなので、カラム`受渡日`が翌日のレコードのみに絞り込む。

````python
from datetime import datetime, timedelta

tomorrow = (datetime.today() + timedelta(days=1)).strftime('%Y/%m/%d')
df = df.filter(pl.col('受渡日') == tomorrow)
````

### 時刻の変換

時刻コードは00:00~23:30までの数字を1~48で表現した値なので、`HH:MM`に変換しカラム`time`に配置する。
 例) `1 → 00:00`, `2 → 00:30`, `48 → 23:30`

````python
# 時刻コードを00:00~23:30に変換する関数
def convert_timecode(timecode: int) -> str:
    hour = str((timecode -1)  // 2).zfill(2)
    minute = "00" if (timecode -1) % 2 == 0 else "30"
    return f"{hour}:{minute}"

df = df.with_columns(
    pl.col("時刻コード").apply(lambda x: convert_timecode(x)).alias("time")
)
````

↓時刻変換後のdataframe
![スクリーンショット 2023-04-22 21.29.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3336887/b3c5f8df-c85f-335a-6c09-35a7d486f341.png)


## Firestoreにアップロード
今回はGCPを使用したかったので、容易なセットアップ・スキーマレスの観点からFirestoreを採用した。

### Firestoreのファイル構成
日付毎のシステムプライス・各エリアプライスは以下ファイル構成とする。

````
electricity_market_price/    # collection
└── 2023-12-01/    # document
    └── system/    # subcollection
        └── {'00:00':'13.6', '00:30':'13.92', ..., '23:30':'12.39'}    # field
    └── hokkaido/
        └── {'00:00':'13.15', '00:30':'14.0', ..., '23:30':'11.8'}
    └── ...
````

システムプライス・各エリアプライスのdataframeのカラム名とFirestoreでのサブコレクション名を、以下の通りマップで定義する。
````python
area_map = {
  'システム・プライス' : {
    'column_name' : 'システムプライス(円/kWh)',
    'subcollection' : 'system'
  },
  '北海道' : {
    'column_name' : 'エリアプライス北海道(円/kWh)',
    'subcollection' : 'hokkaido'
  },
  '東北' : {
    'column_name' : 'エリアプライス東北(円/kWh)',
    'subcollection' : 'tohoku'
  },
  '東京' : {
    'column_name' : 'エリアプライス東京(円/kWh)',
    'subcollection' : 'tokyo'
  },
  '北陸' : {
    'column_name' : 'エリアプライス北陸(円/kWh)',
    'subcollection' : 'hokuriku'
  },
  '中部' : {
    'column_name' : 'エリアプライス中部(円/kWh)',
    'subcollection' : 'chubu'
  },
  '関西' : {
    'column_name' : 'エリアプライス関西(円/kWh)',
    'subcollection' : 'kansai'
  },
  '中国' : {
    'column_name' : 'エリアプライス中国(円/kWh)',
    'subcollection' : 'chugoku'
  },
  '四国' : {
    'column_name' : 'エリアプライス四国(円/kWh)',
    'subcollection' : 'shikoku'
  },
  '九州' : {
    'column_name' : 'エリアプライス九州(円/kWh)',
    'subcollection' : 'kyushu'
  },
}
````

### データ生成
定義したマップからエリア毎のdict`{時間:システムプライス/エリアプライス}`を作成してdata_dictに追加する。

````python
data_dict = {}
for area in area_map:
    data_dict[area_map[area]['subcollection']] = dict(zip(df['time'], df[area_map[area]['column_name']]))
````

### アップロード
data_dictに追加されたdictを、Firestoreに一括してアップロードを行う。
subcollection名は`yyyy/mm/dd`のままだと、勝手にファイル構成が深くなってしまうのと、データ数もそこまで多くないので`yyyy-mm-dd`に変換する。

※GCP以外からFirestoreにアップロードする際に認証を通す必要があるが、今回は省略する。

````python
from google.cloud import firestore

date = tomorrow.replace('/', '-')
db = firestore.Client(project=PROJECT_ID)
doc_ref = db.collection('electricity_market_price').document(date)
doc_ref.set(data_dict, merge=True)
````

Firestoreにファイルがアップロードされたことが確認できる。
![スクリーンショット 2023-04-22 22.27.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3336887/ba75eefa-b6ac-0075-5ddf-f0d2ccf42f23.png)


## おわりに
今回の記事では、日本卸電力取引所から取得した電力市場価格CSVデータを抽出し、Firestoreにアップロードする方法について紹介した。

GCPであれば、Cloud FunctionsにデプロイしCloud Scheduleで定期実行すれば、自動的に翌日のデータがFirestoreにアップロードされる仕組みが構築できる。
これは日本卸電力取引所のサイトから直接CSVファイルを取得させるようにすれば、さらに容易になるのでカスタムしてほしい。

### 今後の展開
Firestoreに当日、翌日分のエリアプライスをアップロードする仕組みが構築されたので、今後はWeb APIからこれらの値を取得する仕組みを開発する。
また公開されている2005年4月2日からのデータの蓄積ができたので、これらを活用した分析にも興味が湧いてきた。

### まとめ
日本卸電力取引所の電力市場価格CSVからデータを抽出し、Firestoreにアップロードすることに成功した。
またこれまで使用したことのなかったPolarsにも触れることができた。今回はデータ量が少なかったので恩恵に預かることができなかったが、今回を皮切りにガンガン使っていきたい。

## 参考記事
https://dev.classmethod.jp/articles/jepx-spot-csv-etl-lambda/

https://qiita.com/_jinta/items/fac13f09e8e8a5769b79