web-dev-qa-db-fra.com

Fonction de fenêtre Spark SQL avec condition complexe

C'est probablement le plus facile à expliquer par l'exemple. Supposons que j'ai un DataFrame de connexions d'utilisateurs à un site Web, par exemple:

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

J'aimerais ajouter à cela une colonne indiquant quand ils sont devenus un utilisateur actif du site. Mais il y a une mise en garde: il y a une période de temps pendant laquelle un utilisateur est considéré comme actif, et après cette période, s'il se reconnecte, sa date became_active est réinitialisée. Supposons que cette période est de 5 jours . Ensuite, la table souhaitée dérivée de la table ci-dessus ressemblerait à ceci:

+----------------+----------+-------------+
|       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|
+----------------+----------+-------------+

Donc, en particulier, la date became_active de SirChillingtonIV a été réinitialisée car leur deuxième connexion est arrivée après l'expiration de la période active, mais la date became_active de Booooooo99900098 n'a pas été réinitialisée la deuxième fois qu'il s'est connecté, car elle est tombée dans la période active. 

Mon idée initiale était d'utiliser les fonctions de fenêtre avec lag, puis d'utiliser les valeurs lagged pour remplir la colonne became_active; par exemple, quelque chose commençant à peu près comme:

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))

Ensuite, la règle pour renseigner la date became_active est la suivante: si tmp est null (c’est-à-dire si c’est la première connexion), ou si login_date - tmp >= 5, puis became_active = login_date; sinon, passez à la valeur la plus récente suivante dans tmp et appliquez la même règle. Cela suggère une approche récursive, que j'ai du mal à imaginer un moyen de le mettre en œuvre. 

Mes questions: Est-ce une approche viable, et si oui, comment puis-je "revenir en arrière" et regarder les valeurs antérieures de tmp jusqu'à ce que j'en trouve une où je m'arrête? À ma connaissance, je ne peux pas parcourir les valeurs de Spark SQL Column. Y a-t-il un autre moyen d'atteindre ce résultat? 

17
user4601931

Voici le truc. Importer un tas de fonctions:

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

Définir les fenêtres:

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

Trouvez les points où les nouvelles sessions commencent:

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))

Trouvez la date la plus proche par session:

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

Avec le jeu de données défini comme:

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")

Le résultat est:

+----------------+----------+-------------+
|       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|
+----------------+----------+-------------+
27
user6910411

Refactoriser la réponse ci-dessus pour travailler avec Pyspark

Dans Pyspark vous pouvez faire comme ci-dessous.

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"))

Le code ci-dessus crée un bloc de données comme ci-dessous

+----------------+----------+
|       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|
+----------------+----------+

Maintenant, nous voulons d’abord déterminer la différence entre login_date et plus de 5 jours.

Pour cela faire comme ci-dessous.

Importations nécessaires

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))

Lorsque nous exécutons la ligne de code ci-dessus si le date_diff est NULL, la fonction coalesce remplacera NULL par 0.

+----------------+----------+-------+
|       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|
+----------------+----------+-------------+
1
User12345