web-dev-qa-db-ja.com

Spark複雑な条件を持つSQLウィンドウ関数

これはおそらく例を通して説明するのが最も簡単でしょう。たとえば、WebサイトへのユーザーログインのDataFrameがあるとします。

scala> df.show(5)
+----------------+----------+
|       user_name|login_date|
+----------------+----------+
|SirChillingtonIV|2012-01-04|
|Booooooo99900098|2012-01-04|
|Booooooo99900098|2012-01-06|
|  OprahWinfreyJr|2012-01-10|
|SirChillingtonIV|2012-01-11|
+----------------+----------+
only showing top 5 rows

これに、サイトでアクティブユーザーになった時期を示す列を追加します。しかし、一つ注意点があります:彼らは再度ログインする場合は、そこにユーザーがアクティブであると見なされている期間があり、この期間の後、彼らbecame_active日付がリセットされます。この期間が5日であるとします。この場合、上記のテーブルから派生した目的のテーブルは次のようになります。

+----------------+----------+-------------+
|       user_name|login_date|became_active|
+----------------+----------+-------------+
|SirChillingtonIV|2012-01-04|   2012-01-04|
|Booooooo99900098|2012-01-04|   2012-01-04|
|Booooooo99900098|2012-01-06|   2012-01-04|
|  OprahWinfreyJr|2012-01-10|   2012-01-10|
|SirChillingtonIV|2012-01-11|   2012-01-11|
+----------------+----------+-------------+

したがって、特に、SirChillingtonIVのbecame_active日付は、アクティブ期間が終了した後に2回目のログインが来たためリセットされましたが、Booooooo99900098のbecame_active日付は、アクティブ期間。

私の最初の考えは、lagでウィンドウ関数を使用し、laggedの値を使用してbecame_active列を埋めることでした。例えば、大まかに次のように始まるもの:

import org.Apache.spark.sql.expressions.Window
import org.Apache.spark.sql.functions._

val window = Window.partitionBy("user_name").orderBy("login_date")
val df2 = df.withColumn("tmp", lag("login_date", 1).over(window))

次に、became_active日付を記入するルールは、tmpnullの場合(つまり、初めてログインする場合)、またはlogin_date - tmp >= 5の場合はbecame_active = login_dateになります。そうでない場合は、tmpの次の最新の値に移動し、同じルールを適用します。これは再帰的なアプローチを示唆しており、実装方法を想像するのに苦労しています。

私の質問:これは実行可能なアプローチですか?もしそうなら、どのようにして「戻って」、tmpの以前の値を見て、停止する場所を見つけることができますか?私の知る限り、Spark SQL Columnの値を反復処理することはできません。この結果を達成する別の方法はありますか?

22
user4601931

ここにトリックがあります。一連の関数をインポートします。

import org.Apache.spark.sql.expressions.Window
import org.Apache.spark.sql.functions.{coalesce, datediff, lag, lit, min, sum}

ウィンドウを定義する:

val userWindow = Window.partitionBy("user_name").orderBy("login_date")
val userSessionWindow = Window.partitionBy("user_name", "session")

新しいセッションが始まるポイントを見つけます。

val newSession =  (coalesce(
  datediff($"login_date", lag($"login_date", 1).over(userWindow)),
  lit(0)
) > 5).cast("bigint")

val sessionized = df.withColumn("session", sum(newSession).over(userWindow))

セッションごとの最も早い日付を見つける:

val result = sessionized
  .withColumn("became_active", min($"login_date").over(userSessionWindow))
  .drop("session")

データセットが次のように定義されている場合:

val df = Seq(
  ("SirChillingtonIV", "2012-01-04"), ("Booooooo99900098", "2012-01-04"),
  ("Booooooo99900098", "2012-01-06"), ("OprahWinfreyJr", "2012-01-10"), 
  ("SirChillingtonIV", "2012-01-11"), ("SirChillingtonIV", "2012-01-14"),
  ("SirChillingtonIV", "2012-08-11")
).toDF("user_name", "login_date")

結果は次のとおりです。

+----------------+----------+-------------+
|       user_name|login_date|became_active|
+----------------+----------+-------------+
|  OprahWinfreyJr|2012-01-10|   2012-01-10|
|SirChillingtonIV|2012-01-04|   2012-01-04| <- The first session for user
|SirChillingtonIV|2012-01-11|   2012-01-11| <- The second session for user
|SirChillingtonIV|2012-01-14|   2012-01-11| 
|SirChillingtonIV|2012-08-11|   2012-08-11| <- The third session for user
|Booooooo99900098|2012-01-04|   2012-01-04|
|Booooooo99900098|2012-01-06|   2012-01-04|
+----------------+----------+-------------+
36
user6910411

リファクタリング 他の答えPysparkを使用する

Pysparkでは、次のようにできます。

create data frame

df = sqlContext.createDataFrame(
[
("SirChillingtonIV", "2012-01-04"), 
("Booooooo99900098", "2012-01-04"), 
("Booooooo99900098", "2012-01-06"), 
("OprahWinfreyJr", "2012-01-10"), 
("SirChillingtonIV", "2012-01-11"), 
("SirChillingtonIV", "2012-01-14"), 
("SirChillingtonIV", "2012-08-11")
], 
("user_name", "login_date"))

上記のコードは、次のようなデータフレームを作成します

+----------------+----------+
|       user_name|login_date|
+----------------+----------+
|SirChillingtonIV|2012-01-04|
|Booooooo99900098|2012-01-04|
|Booooooo99900098|2012-01-06|
|  OprahWinfreyJr|2012-01-10|
|SirChillingtonIV|2012-01-11|
|SirChillingtonIV|2012-01-14|
|SirChillingtonIV|2012-08-11|
+----------------+----------+

ここで、最初にlogin_dateの違いが5日以上であることを確認したいと思います。

これについては以下のようにします。

必要な輸入

from pyspark.sql import functions as f
from pyspark.sql import Window


# defining window partitions  
login_window = Window.partitionBy("user_name").orderBy("login_date")
session_window = Window.partitionBy("user_name", "session")

session_df = df.withColumn("session", f.sum((f.coalesce(f.datediff("login_date", f.lag("login_date", 1).over(login_window)), f.lit(0)) > 5).cast("int")).over(login_window))

date_diffNULLである場合に上記のコード行を実行すると、coalesce関数はNULL0に置き換えます。

+----------------+----------+-------+
|       user_name|login_date|session|
+----------------+----------+-------+
|  OprahWinfreyJr|2012-01-10|      0|
|SirChillingtonIV|2012-01-04|      0|
|SirChillingtonIV|2012-01-11|      1|
|SirChillingtonIV|2012-01-14|      1|
|SirChillingtonIV|2012-08-11|      2|
|Booooooo99900098|2012-01-04|      0|
|Booooooo99900098|2012-01-06|      0|
+----------------+----------+-------+


# add became_active column by finding the `min login_date` for each window partitionBy `user_name` and `session` created in above step
final_df = session_df.withColumn("became_active", f.min("login_date").over(session_window)).drop("session")

+----------------+----------+-------------+
|       user_name|login_date|became_active|
+----------------+----------+-------------+
|  OprahWinfreyJr|2012-01-10|   2012-01-10|
|SirChillingtonIV|2012-01-04|   2012-01-04|
|SirChillingtonIV|2012-01-11|   2012-01-11|
|SirChillingtonIV|2012-01-14|   2012-01-11|
|SirChillingtonIV|2012-08-11|   2012-08-11|
|Booooooo99900098|2012-01-04|   2012-01-04|
|Booooooo99900098|2012-01-06|   2012-01-04|
+----------------+----------+-------------+
4
User12345