PySparkのTmeStampTypeに対してstringで演算するときの注意点

# はじめに

PySparkのTimeStampTypeの演算時に、不用意に日付形式の文字列型を使うと意図しない挙動をするパターンがあるという話。

そのため、TimeStampTypeに対してstringで演算することもできるが、datetimeを使う方が無難。

# 例

具体例で説明する。

ここで示す例は、PySPark 2.4.4によるものである。

## 検証用データ

次のコードで、2000/1/1 ~ 2000/1/5の日付データをもつSpark DataFrameを作成し、この日付データに対する条件処理を行ってみる。

```python:検証用データ
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を用いた演算は、正常に動作する。

```python: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()
```

```terminal:出力結果
== 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 hh:mm:ss)でstringを与えたときの結果を示す。
stringは暗黙的にキャストされるらしく、問題なく演算が行える。

```python:TimeStampTypeとstring(datetime形式)の演算
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()
```

```terminal:出力結果
== 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を与えたときの結果を示す。
この場合、直感的に意図する挙動とは異なる結果になるパターンがある。

```python:TimeStampTypeとstring(date形式)の演算
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()
```

```terminal:出力結果
== 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型同士なので当然だが)正常に動作する。

```python:StringTypeから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()
```

```terminal:出力結果
+-------------------+-------------------+
|                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|
+-------------------+-------------------+
```