[Spark] DataFrameで "" と null と [ ] のトラップにハマった話


Spark2の本当は怖い?DataFrameの話。

開発環境

ちょっと前に書いていた記事なのでバージョン古めです。

  • Python 2.7.15
  • Spark 2.1.2

CSV読み込みでハマる例

input.csv
x,y,z
1,,2
pyspark
>>> df = spark.read.csv("input.csv", header=True)
>>> df
DataFrame[x: string, y: string, z: string]
>>> df.show()
+---+----+---+
|  x|   y|  z|
+---+----+---+
|  1|null|  2|
+---+----+---+

こういうファイルを読み込むと空のフィールドには "" ではなくnullが入ります。
言い換えると、CSVで保存すると "" とnullは区別できなくなります。ご注意を。
もっとも、別のSparkアプリで読み込むようなデータなら、CSVなんか使わずにParquetとかAvroで保存するなど、やりようはあるでしょうけど。

文字列関数がらみでハマる例

先ほどの df を使いまして。

pyspark
>>> df.select(pyspark.sql.functions.length("y")).show()
+---------+
|length(y)|
+---------+
|     null|
+---------+
# わかる。

>>> df.select(pyspark.sql.functions.split("y", " ")).show()
+-----------+
|split(y,  )|
+-----------+
|       null|
+-----------+
# まあわかる。

>>> df.select(pyspark.sql.functions.size(pyspark.sql.functions.split("y", " "))).show()
+-----------------+
|size(split(y,  ))|
+-----------------+
|               -1|
+-----------------+
# -1なの? まあいいけど...

>>> df.fillna("").show()
+---+---+---+
|  x|  y|  z|
+---+---+---+
|  1|   |  2|
+---+---+---+
# nullを "" に置き換えました。

>>> df.fillna("").select(pyspark.sql.functions.length("y")).show()
+---------+
|length(y)|
+---------+
|        0|
+---------+
# "" だもんね。

>>> df.fillna("").select(pyspark.sql.functions.split("y", " ")).show()
+-----------+
|split(y,  )|
+-----------+
|         []|
+-----------+
# せやな。

>>> df.fillna("").select(pyspark.sql.functions.size(pyspark.sql.functions.split("y", " "))).show()
+-----------------+
|size(split(y,  ))|
+-----------------+
|                1|
+-----------------+
# えっ0じゃないの??

>>> df2 = spark.createDataFrame([[[]]], "arr: array<string>")
>>> df2
DataFrame[arr: array<string>]
>>> df2.show()
+---+
|arr|
+---+
| []|
+---+

>>> df2.select(pyspark.sql.functions.size("arr")).show()
+---------+
|size(arr)|
+---------+
|        0|
+---------+
# なんであっちが1でこっちが0なんだ...

>>> df.fillna("").select(pyspark.sql.functions.split("y", " ")).collect()
[Row(split(y,  )=[u''])]
# Oh... 確かにPythonでも len("".split(" ")) == 1
# ということは勝手に引っ掛かっただけか...orz

show()の出力では、空の配列 [] と空文字列が1個入った配列 [""] は区別できないのですね…
意外とこういう仕様はドキュメントにちゃんと書かれていないのです。焦った。