はじめに
PySparkのTimeStampTypeの演算時に、不用意に日付形式の文字列型を使うと意図しない挙動をするパターンがあるという話。
そのため、TimeStampTypeに対してstringで演算することもできるが、datetimeを使う方が無難。
例
具体例で説明する。
ここで示す例は、PySPark 2.4.4によるものである。
検証用データ
次のコードで、2000/1/1 ~ 2000/1/5の日付データをもつSpark DataFrameを作成し、この日付データに対する条件処理を行ってみる。
import pandas as pd
from pyspark.sql import functions as F
pdf = pd.DataFrame(pd.date_range(start='1/1/2000', periods=5), columns=['date'])
sdf = spark.createDataFrame(pdf, ['timestamp'])
TimeStampTypeとdatetimeの演算
TimeStampTypeに対してdatetimeを用いた演算は、正常に動作する。
target_datetime = datetime.strptime('2000-01-03', '%Y-%m-%d')
print('== datetime(2000-01-03)')
sdf.where(F.col('timestamp') == datetime.strptime('2000-01-03', '%Y-%m-%d')).show()
print('> datetime(2000-01-03)')
sdf.where(F.col('timestamp') > datetime.strptime('2000-01-03', '%Y-%m-%d')).show()
print('>= datetime(2000-01-03)')
sdf.where(F.col('timestamp') >= datetime.strptime('2000-01-03', '%Y-%m-%d')).show()
print('< datetime(2000-01-03)')
sdf.where(F.col('timestamp') < datetime.strptime('2000-01-03', '%Y-%m-%d')).show()
print('<= datetime(2000-01-03)')
sdf.where(F.col('timestamp') <= datetime.strptime('2000-01-03', '%Y-%m-%d')).show()
print('between datetime(2000-01-02) and datetime(2000-01-04)')
sdf.where(F.col('timestamp').between(datetime.strptime('2000-01-02', '%Y-%m-%d'), datetime.strptime('2000-01-04', '%Y-%m-%d'))).show()
== datetime(2000-01-03)
+-------------------+
| timestamp|
+-------------------+
|2000-01-03 00:00:00|
+-------------------+
> datetime(2000-01-03)
+-------------------+
| timestamp|
+-------------------+
|2000-01-04 00:00:00|
|2000-01-05 00:00:00|
+-------------------+
>= datetime(2000-01-03)
+-------------------+
| timestamp|
+-------------------+
|2000-01-03 00:00:00|
|2000-01-04 00:00:00|
|2000-01-05 00:00:00|
+-------------------+
< datetime(2000-01-03)
+-------------------+
| timestamp|
+-------------------+
|2000-01-01 00:00:00|
|2000-01-02 00:00:00|
+-------------------+
<= datetime(2000-01-03)
+-------------------+
| timestamp|
+-------------------+
|2000-01-01 00:00:00|
|2000-01-02 00:00:00|
|2000-01-03 00:00:00|
+-------------------+
between datetime(2000-01-02) and datetime(2000-01-04)
+-------------------+
| timestamp|
+-------------------+
|2000-01-02 00:00:00|
|2000-01-03 00:00:00|
|2000-01-04 00:00:00|
+-------------------+
TimeStampTypeとstring(datetime形式)の演算
次に、datetime形式(yyyy-mm-dd hhss)でstringを与えたときの結果を示す。
stringは暗黙的にキャストされるらしく、問題なく演算が行える。
print('== string(2000-01-03 00:00:00)')
sdf.where(F.col('timestamp') == '2000-01-03 00:00:00').show()
print('> string(2000-01-03 00:00:00)')
sdf.where(F.col('timestamp') > '2000-01-03 00:00:00').show()
print('>= string(2000-01-03 00:00:00)')
sdf.where(F.col('timestamp') >= '2000-01-03 00:00:00').show()
print('< string(2000-01-03 00:00:00)')
sdf.where(F.col('timestamp') < '2000-01-03 00:00:00').show()
print('<= string(2000-01-03 00:00:00)')
sdf.where(F.col('timestamp') <= '2000-01-03 00:00:00').show()
print('between string(2000-01-02 00:00:00) and string(2000-01-04 00:00:00)')
sdf.where(F.col('timestamp').between('2000-01-02 00:00:00', '2000-01-04 00:00:00')).show()
== string(2000-01-03 00:00:00)
+-------------------+
| timestamp|
+-------------------+
|2000-01-03 00:00:00|
+-------------------+
> string(2000-01-03 00:00:00)
+-------------------+
| timestamp|
+-------------------+
|2000-01-04 00:00:00|
|2000-01-05 00:00:00|
+-------------------+
>= string(2000-01-03 00:00:00)
+-------------------+
| timestamp|
+-------------------+
|2000-01-03 00:00:00|
|2000-01-04 00:00:00|
|2000-01-05 00:00:00|
+-------------------+
< string(2000-01-03 00:00:00)
+-------------------+
| timestamp|
+-------------------+
|2000-01-01 00:00:00|
|2000-01-02 00:00:00|
+-------------------+
<= string(2000-01-03 00:00:00)
+-------------------+
| timestamp|
+-------------------+
|2000-01-01 00:00:00|
|2000-01-02 00:00:00|
|2000-01-03 00:00:00|
+-------------------+
between string(2000-01-02 00:00:00) and string(2000-01-04 00:00:00)
+-------------------+
| timestamp|
+-------------------+
|2000-01-02 00:00:00|
|2000-01-03 00:00:00|
|2000-01-04 00:00:00|
+-------------------+
TimeStampTypeとstring(date形式)の演算
最後に、date形式(yyyy-mm-dd)でstringを与えたときの結果を示す。
この場合、直感的に意図する挙動とは異なる結果になるパターンがある。
print('== string(2000-01-03)')
sdf.where(F.col('timestamp') == '2000-01-03').show()
print('> string(2000-01-03)') # 意図しないパターン
sdf.where(F.col('timestamp') > '2000-01-03').show()
print('>= string(2000-01-03)')
sdf.where(F.col('timestamp') >= '2000-01-03').show()
print('< string(2000-01-03)')
sdf.where(F.col('timestamp') < '2000-01-03').show()
print('<= string(2000-01-03)') # 意図しないパターン
sdf.where(F.col('timestamp') <= '2000-01-03').show()
print('between string(2000-01-02) and string(2000-01-04)') # 意図しないパターン
sdf.where(F.col('timestamp').between('2000-01-02', '2000-01-04')).show()
== string(2000-01-03)
+-------------------+
| timestamp|
+-------------------+
|2000-01-03 00:00:00|
+-------------------+
> string(2000-01-03)
+-------------------+
| timestamp|
+-------------------+
|2000-01-03 00:00:00|
|2000-01-04 00:00:00|
|2000-01-05 00:00:00|
+-------------------+
>= string(2000-01-03)
+-------------------+
| timestamp|
+-------------------+
|2000-01-03 00:00:00|
|2000-01-04 00:00:00|
|2000-01-05 00:00:00|
+-------------------+
< string(2000-01-03)
+-------------------+
| timestamp|
+-------------------+
|2000-01-01 00:00:00|
|2000-01-02 00:00:00|
+-------------------+
<= string(2000-01-03)
+-------------------+
| timestamp|
+-------------------+
|2000-01-01 00:00:00|
|2000-01-02 00:00:00|
+-------------------+
between string(2000-01-02) and string(2000-01-04)
+-------------------+
| timestamp|
+-------------------+
|2000-01-02 00:00:00|
|2000-01-03 00:00:00|
+-------------------+
挙動について
演算時に暗黙的にstringがTimeStampへキャストされた結果、TimeStamp(2000-01-01 00:00:00)
とTimeStamp(string(2000-01-01 00:00:00))
が等価であり、TimeStamp(2000-01-01 00:00:00) < TimeStamp(string(2000-01-01))
のような結果となっている。
このことから、時間分(上記の例における00:00:00
)の値が適切に処理されてないと想像できる。(厳密な仕様は、演算の詳細はScalaのソースを確認する必要がある)
(番外編) StringTypeからTimeStampTypeへのキャスト
ちなみに、StringTypeからTimeStampTypeへキャストする際は、正しくキャストできているようである。
ここでは例は示さないが、上記のパターンと同様に、TimeStampTypeとの演算処理を行っても、(TimeStampType型同士なので当然だが)正常に動作する。
df = spark.createDataFrame([('2000',), ('2000-01-01',), ('2000-01-01 00:00:00',) ], ['str'])
df = df.withColumn('timestamp', F.col('str').cast('timestamp'))
df.show()
+-------------------+-------------------+
| str| timestamp|
+-------------------+-------------------+
| 2000|2000-01-01 00:00:00|
| 2000-01-01|2000-01-01 00:00:00|
|2000-01-01 00:00:00|2000-01-01 00:00:00|
+-------------------+-------------------+