Glue DynamicFrameでParquet変換におけるDecimalの扱い注意点

金額計算などで利用する固定小数点数 decimal をGlueで扱う場合の注意点を書きます。

GlueのGUIでそのまま設定すると、decimalにはなるのですが、 decimal(10,2) という型に固定されてしまうといった落とし穴もありますので、そのあたりも。

例として以下のCSVをParquetに変換する方式で考えようと思います。

decimal_col,decimal_38_20_col
0.1,99999999999999999.99999999999999999999
0.2,99999999999999999.9999999999999999999

Crawlerによるスキーマの自動判別に注意

S3にCSVを置いてGlueのCrawlerを回してから、上記のCSVを読み込ませると、いきなり decimal_38_20_col の値が丸められてしまいます。

+-----------+-----------------+
|decimal_col|decimal_38_20_col|
+-----------+-----------------+
|0.1        |1.0E17           |
|0.2        |1.0E17           |
+-----------+-----------------+

この原因はCrawlerが自動で少数をDoubleとして判別してしまったことで、DynamicFrameで読み込む際に double にCastされてしまったことが原因となります。

f:id:yomon8:20190423104322p:plain

解決策としては以下のように手動やAPIスキーマdouble から decimal または string に変更してあげることです。decimal に設定してもDynamicFrame上は string として認識されるので、機能的にはどちらでも同じなのでドキュメンテーション等の観点から選択で良いと思います。

root
 |-- decimal_col: string (nullable = true)
 |-- decimal_38_20_col: string (nullable = true)

+-----------+--------------------------------------+
|decimal_col|decimal_38_20_col                     |
+-----------+--------------------------------------+
|0.1        |99999999999999999.99999999999999999999|
|0.2        |99999999999999999.99999999999999999999|
+-----------+--------------------------------------+

Crawlerの設定によっては、Crawler回す度にスキーマ上書きされてしまうので注意してください。

高精度のdecimalへの変換はDataFrameを利用する

GlueのGUIから設定できるDynamicFrameによる変換の場合は、decimal の精度(桁数)を決めるパラメータが、 precision=10scale=2 に固定されてしまいます。型の表現で言うと decimal(10,2) です。

原因はDynamicFrameのコード見るとわかります。以下のようにハードコードされてしまっているためです。

class DecimalType(AtomicType):
    def __init__(self, precision=10, scale=2, properties={}):
        super(DecimalType, self).__init__(properties)
        self.precision = precision
        self.scale = scale

https://github.com/awslabs/aws-glue-libs/blob/master/awsglue/gluetypes.py

対応方法としてはDataFrameを利用してCASTする方法があります。

以下は一例ですが、以下のような関数を定義します。

def cast_to_decimal(data_frame,column_name,precision,scale):
    decimal_type = 'decimal({precision},{scale})'.format(precision=precision,scale=scale)
    casted_df = data_frame.withColumn(column_name,data_frame[column_name].cast(decimal_type))
    return casted_df

DynamicFrameとして読み取った datasource0 をDataFrameに変換して上記の関数、decimalのパラメータと一緒に渡して変換をかけます。

from awsglue.dynamicframe import DynamicFrame
casted1 = cast_to_decimal(datasource0.toDF(),'decimal_col',18,3)
casted2 = cast_to_decimal(casted1,'decimal_38_20_col',38,20)
decimalcasteddyf = DynamicFrame.fromDF(casted2,glueContext,'decimalcasteddf')

正しく decimal(10,2) より精度上げたスキーマが反映されました。

f:id:yomon8:20190423111601p:plain

Redshift Spectrumでクエリしたところです。

f:id:yomon8:20190423111746p:plain

Doubleでは誤差が出てしまう、0.1と0.2のSUMを行っても正しく計算可能です。

select sum(decimal_col) from db.table;
  • 0.1 + 0.2 はdoubleでは誤差がでる

f:id:yomon8:20190423112255p:plain

  • decimalでは正しく計算可能

f:id:yomon8:20190423111936p:plain

precision=38 にしているので、こちらも問題ありません。

select sum(decimal_38_20_col) from db.table;

f:id:yomon8:20190423112926p:plain

今回関連したコンポーネントのdecimal実装

それぞれdecimal型の記載があるページへのリンクです。

Redshift

数値型 - Amazon Redshift

Glue(Spark)

DecimalType (Spark 2.2.1 JavaDoc)

Parquet

parquet-format/LogicalTypes.md at master · apache/parquet-format · GitHub