Django Rest Framework(DRF) + Django Pandas で ピボットテーブル的なデータ変換


## はじめに

### Django Rest Framework(DRF) で、出力レイアウトの変換をDjango Pandasで実現した記事となります。（EXCELで言うところのピボットテーブル）

Pandasのシリアライズ化するのにDjango REST Pandasを使っています。
https://django-rest-pandas.wq.io/
クライアント側はDanfo.jsを使っています。
https://danfo.jsdata.org/


### 説明
例えば下記のような、預金項目、登録年月、金額のレイアウトの資産テーブル(Tt_Assets)があるとします。

|資産ID(PK)|預金項目ID|登録年月|金額|
|--:|:---|:---|----:|
|1|1(積立)|2023/01|1,230|
|2|1(積立)|2023/02|500|
|3|1(積立)|2023/03|400|
|4|2(給与)|2023/01|100,000|
|5|2(給与)|2023/02|200,000|
|6|2(給与)|2023/03|300,000|
|7|3(支払)|2023/01|110,000|
|8|3(支払)|2023/02|210,000|
|9|3(支払)|2023/03|310,000|



下記のレイアウトのように、行を年月単位にして、列を預金項目ID、値を金額にしたい。
（預金項目IDは預金マスタと紐づいているため列数は固定にならない）
|登録年月|1(積立)|2(給与)|3(支払)|
|:--|---:|---:|---:|
|2023/01|1,230|100,000|110,000|
|2023/02|500|200,000|210,000|
|2023/03|100|300,000|310,000|

#### モデル定義
```model.py
#預金項目
class Tm_DepositItem(ModelBase):
    class Meta:
        db_table = 'Tm_DepositItem'
        verbose_name = '預金項目(Tm_DepositItem)'
        verbose_name_plural = '預金項目(Tm_DepositItem)'
        unique_together = (
            ('depositItem_name',),
        )
        ordering = ('order_dsp',)

    depositItem_key = models.AutoField(primary_key=True, verbose_name='預金項目ID')
    depositItem_name = models.CharField(max_length=40, verbose_name='預金項目名')
    order_dsp = models.IntegerField(verbose_name='表示順序')
    def __str__(self):
        return self.depositItem_name


#資産トラン
class Tt_Assets(ModelBase):
    class Meta:
        db_table = 'Tt_Assets'
        verbose_name = '資産トラン(Tt_Assets)'
        verbose_name_plural = '資産トラン(Tt_Assets)'

    assets_key = models.AutoField(primary_key=True, verbose_name='資産ID')
    depositItem_key = models.ForeignKey(Tm_DepositItem, on_delete=models.PROTECT, related_name='assets_deposititem_key', verbose_name='預金項目ID')
    deposit_value = models.IntegerField(verbose_name='金額')
    insert_yyyymm = models.CharField(max_length=7, verbose_name='登録年月')
```

### View実装
```view.py
from rest_pandas import PandasViewSet, PandasUnstackedSerializer

class AssetsPandasViewSet(PandasViewSet):
    '''
    資産トランのPivotデータ取得
    insert_yyyymm_from, insert_yyyymm_toが設定されている場合は絞込も行う    
    '''
    queryset = Tt_Assets.objects.all()
    serializer_class = Tt_AssetsPandasSerializer
    pandas_serializer_class = PandasUnstackedSerializer

    @log_decorate('AssetsPandasViewSet::get_queryset()')
    def get_queryset(self):
        insert_yyyymm_from = None
        insert_yyyymm_to = None
        if 'insert_yyyymm_from' in self.request.GET:
            insert_yyyymm_from = self.request.GET.get('insert_yyyymm_from')
        if 'insert_yyyymm_to' in self.request.GET:
            insert_yyyymm_to = self.request.GET.get('insert_yyyymm_to')
        records = Tt_Assets.objects.all()
        if insert_yyyymm_from :
            records = records.filter(insert_yyyymm__gte=insert_yyyymm_from)
        if insert_yyyymm_to :
            records = records.filter(insert_yyyymm__lte=insert_yyyymm_to)
        return records
```

### シリアライズ実装
```serializers.py
from rest_framework import serializers
from .models import Tt_Assets

class Tt_AssetsPandasSerializer(serializers.ModelSerializer):
    class Meta:
        model = Tt_Assets
        # 利用列
        fields = ['depositItem_key', 'deposit_value', 'insert_yyyymm']
        # 行
        pandas_index = ['insert_yyyymm']
        # 列
        pandas_unstacked_header = ['depositItem_key']
```
(fields、index, unstacked は、Pandasを理解するとなんとなくわかるのだと思います・・。)


### 確認
テストプログラムで中身を確認してみます。
パラメータにはJSONで受取るために?format=jsonを指定しています。
```test_AssetsPandas.py
class AssetsandasTestCase(TestCase):
  def test_AssetsPandasViewSet(self):
    #
    # テスト対象モジュールを呼出す
    #
    search_dates = ['2022/12', '2023/01', '2023/02', '2023/03', '2023/04', '2023/05', '2023/06']
    TEST_URL = '/api/assets_pandas/?format=json&insert_yyyymm_from='   \
      + search_dates[0] + '&insert_yyyymm_to=' + search_dates[-1]
    #TEST_URL = '/api/assets_pandas/?format=json'
    logger.debug(TEST_URL)
    res = self.client.get(TEST_URL)
    # 確認
    print('res.data.index')
    print(res.data.index)
    print('res.data.columns')
    print(res.data.columns)
    print('res.data')
    print(res.data)
```

実行すると下記のように出力されます。データ型もPandasとなります。
```console.py
res.data.index
RangeIndex(start=0, stop=7, step=1)
res.data.columns
MultiIndex([('insert_yyyymm', ''),
            ('deposit_value',  1),
            ('deposit_value',  2),
            ('deposit_value',  3),
            ('deposit_value',  4),
            ('deposit_value',  5),
            ('deposit_value',  6),
            ('deposit_value',  7),
            ('deposit_value',  8),
            ('deposit_value',  9),
            ('deposit_value', 10)],
           names=['', 'depositItem_key'])
res.data
<class 'pandas.core.frame.DataFrame'>
                insert_yyyymm deposit_value                                     
depositItem_key                           1   2   3   4   5   6   7   8   9   10
0                     2022/12             0  10  20  30  40  50  60  70  80   90
1                     2023/01             2  12  22  32  42  52  62  72  82   92
2                     2023/02             4  14  24  34  44  54  64  74  84   94
3                     2023/03             6  16  26  36  46  56  66  76  86   96
4                     2023/04             8  18  28  38  48  58  68  78  88   98
5                     2023/05            10  20  30  40  50  60  70  80  90  100
6                     2023/06            12  22  32  42  52  62  72  82  92  102


```

### クライアント側
クライアント(React)で受信してみるとこんな感じのデータを受信出来ました。
どうもPandasフォーマットらしいです。まぁこれでもいいっちゃー良いのだけど。

console.log出力結果
![スクリーンショット 2023-03-05 151312.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/538267/55ad21cf-6a0c-0e38-cb87-afcb493cbc23.jpeg)

そこでPandasをJavaScriptで利用出来るDanfo.jsというライブラリを使ってみました。
https://danfo.jsdata.org/
JSONフォーマットからDataFrameへの変換が出来ます。
https://danfo.jsdata.org/api-reference/dataframe/dataframe.to_json

記載のある通りにJSに書いてみました
```AssetsTable.js
import * as dfd from "danfojs"

console.debug('-*----------apiResult2Rowdata');
//console.debug(resultDatas);
// Response結果をPandasへ
const df = new dfd.DataFrame(resultDatas)
console.debug(df.print())

```
console.log出力結果をみるとちゃんとPandasとして扱えるのが確認出来ました。
![スクリーンショット 2023-03-05 152407.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/538267/f38c8149-cc0c-2854-712a-8ee5fa4999e3.jpeg)


列情報が、('insert_yyyymm')や('deposit_value')などになっているため多少の加工が必要となりますが
クラアントとしてデータ表示することが出来ました。
![スクリーンショット 2023-03-05 153018.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/538267/3b46c79b-4662-652b-73be-6e2d6083689f.jpeg)

### 注意事項
Django REST Pandas の列指定は、リレーション先の列情報を指定するのはダメそうでした。

### 参考サイト
https://django-rest-pandas.wq.io/serializers/PandasUnstackedSerializer
https://danfo.jsdata.org/api-reference/dataframe/dataframe.to_json