web-dev-qa-db-fra.com

pyspark: moyenne mobile à l'aide de données de séries chronologiques

J'ai un ensemble de données composé d'une colonne d'horodatage et d'une colonne de dollars. Je voudrais trouver le nombre moyen de dollars par semaine se terminant à l'horodatage de chaque ligne. Au départ, je regardais la fonction pyspark.sql.functions.window, mais qui regroupe les données par semaine.

Voici un exemple:

%pyspark
import datetime
from pyspark.sql import functions as F

df1 = sc.parallelize([(17,"2017-03-11T15:27:18+00:00"), (13,"2017-03-11T12:27:18+00:00"), (21,"2017-03-17T11:27:18+00:00")]).toDF(["dollars", "datestring"])
df2 = df1.withColumn('timestampGMT', df1.datestring.cast('timestamp'))

w = df2.groupBy(F.window("timestampGMT", "7 days")).agg(F.avg("dollars").alias('avg'))
w.select(w.window.start.cast("string").alias("start"), w.window.end.cast("string").alias("end"), "avg").collect()

Il en résulte deux enregistrements:

|        start        |          end         | avg |
|---------------------|----------------------|-----|
|'2017-03-16 00:00:00'| '2017-03-23 00:00:00'| 21.0|
|---------------------|----------------------|-----|
|'2017-03-09 00:00:00'| '2017-03-16 00:00:00'| 15.0|
|---------------------|----------------------|-----|

La fonction de fenêtre a regroupé les données de série chronologique plutôt que d'effectuer une moyenne mobile.

Existe-t-il un moyen d'effectuer une moyenne mobile où je récupérerai une moyenne hebdomadaire pour chaque ligne avec une période se terminant à l'horodatage GMT de la ligne?

MODIFIER:

La réponse de Zhang ci-dessous est proche de ce que je veux, mais pas exactement de ce que j'aimerais voir.

Voici un meilleur exemple pour montrer ce que j'essaie de faire:

%pyspark
from pyspark.sql import functions as F
df = spark.createDataFrame([(17, "2017-03-10T15:27:18+00:00"),
                        (13, "2017-03-15T12:27:18+00:00"),
                        (25, "2017-03-18T11:27:18+00:00")],
                        ["dollars", "timestampGMT"])
df = df.withColumn('timestampGMT', df.timestampGMT.cast('timestamp'))
df = df.withColumn('rolling_average', F.avg("dollars").over(Window.partitionBy(F.window("timestampGMT", "7 days"))))

Il en résulte la trame de données suivante:

dollars timestampGMT            rolling_average
25      2017-03-18 11:27:18.0   25
17      2017-03-10 15:27:18.0   15
13      2017-03-15 12:27:18.0   15

Je voudrais que la moyenne se situe sur la semaine précédant la date dans la colonne timestampGMT, ce qui entraînerait ceci:

dollars timestampGMT            rolling_average
17      2017-03-10 15:27:18.0   17
13      2017-03-15 12:27:18.0   15
25      2017-03-18 11:27:18.0   19

Dans les résultats ci-dessus, le rolling_average pour 2017-03-10 est 17, car il n'y a aucun enregistrement précédent. La moyenne de roulement pour le 15/03/2017 est de 15 car elle est en moyenne le 13 du 15/03/2017 et le 17 du 10/03/2017 qui tombe avec la fenêtre de 7 jours précédente. La moyenne mobile du 2017-03-18 est de 19 car elle est en moyenne le 25 du 2017-03-18 et le 13 du 2017-03-10 qui tombe avec la fenêtre de 7 jours précédente, et elle n'inclut pas le 17 de 2017 -03-10 car cela ne tombe pas avec la fenêtre précédente de 7 jours.

Y a-t-il un moyen de le faire plutôt que la fenêtre de regroupement où les fenêtres hebdomadaires ne se chevauchent pas?

14
Bob Swain

J'ai trouvé la bonne façon de calculer une moyenne mobile/glissante en utilisant ce stackoverflow:

Fonctions de la fenêtre Spark - rangeBetween dates

L'idée de base est de convertir votre colonne d'horodatage en secondes, puis vous pouvez utiliser la fonction rangeBetween dans la classe pyspark.sql.Window pour inclure les lignes correctes dans votre fenêtre.

Voici l'exemple résolu:

%pyspark
from pyspark.sql import functions as F
from pyspark.sql.window import Window


#function to calculate number of seconds from number of days
days = lambda i: i * 86400

df = spark.createDataFrame([(17, "2017-03-10T15:27:18+00:00"),
                        (13, "2017-03-15T12:27:18+00:00"),
                        (25, "2017-03-18T11:27:18+00:00")],
                        ["dollars", "timestampGMT"])
df = df.withColumn('timestampGMT', df.timestampGMT.cast('timestamp'))

#create window by casting timestamp to long (number of seconds)
w = (Window.orderBy(F.col("timestampGMT").cast('long')).rangeBetween(-days(7), 0))

df = df.withColumn('rolling_average', F.avg("dollars").over(w))

Il en résulte la colonne exacte de moyennes mobiles que je cherchais:

dollars   timestampGMT            rolling_average
17        2017-03-10 15:27:18.0   17.0
13        2017-03-15 12:27:18.0   15.0
25        2017-03-18 11:27:18.0   19.0
21
Bob Swain

J'ajouterai une variante que j'ai personnellement trouvée très utile. J'espère que quelqu'un le trouvera également utile:

Si vous souhaitez regrouper, dans les groupes respectifs, calculez la moyenne mobile:

Exemple de trame de données:

from pyspark.sql.window import Window
from pyspark.sql import functions as func


df = spark.createDataFrame([("tshilidzi", 17.00, "2018-03-10T15:27:18+00:00"), 
  ("tshilidzi", 13.00, "2018-03-11T12:27:18+00:00"),   
  ("tshilidzi", 25.00, "2018-03-12T11:27:18+00:00"), 
  ("thabo", 20.00, "2018-03-13T15:27:18+00:00"), 
  ("thabo", 56.00, "2018-03-14T12:27:18+00:00"), 
  ("thabo", 99.00, "2018-03-15T11:27:18+00:00"), 
  ("tshilidzi", 156.00, "2019-03-22T11:27:18+00:00"), 
  ("thabo", 122.00, "2018-03-31T11:27:18+00:00"), 
  ("tshilidzi", 7000.00, "2019-04-15T11:27:18+00:00"),
  ("ash", 9999.00, "2018-04-16T11:27:18+00:00") 
  ],
  ["name", "dollars", "timestampGMT"])

# we need this timestampGMT as seconds for our Window time frame
df = df.withColumn('timestampGMT', df.timestampGMT.cast('timestamp'))

df.show(10000, False)

Production:

+---------+-------+---------------------+
|name     |dollars|timestampGMT         |
+---------+-------+---------------------+
|tshilidzi|17.0   |2018-03-10 17:27:18.0|
|tshilidzi|13.0   |2018-03-11 14:27:18.0|
|tshilidzi|25.0   |2018-03-12 13:27:18.0|
|thabo    |20.0   |2018-03-13 17:27:18.0|
|thabo    |56.0   |2018-03-14 14:27:18.0|
|thabo    |99.0   |2018-03-15 13:27:18.0|
|tshilidzi|156.0  |2019-03-22 13:27:18.0|
|thabo    |122.0  |2018-03-31 13:27:18.0|
|tshilidzi|7000.0 |2019-04-15 13:27:18.0|
|ash      |9999.0 |2018-04-16 13:27:18.0|
+---------+-------+---------------------+

Pour calculer la moyenne mobile en fonction de name et conserver toutes les lignes:

#create window by casting timestamp to long (number of seconds)
w = (Window()
     .partitionBy(col("name"))
     .orderBy(F.col("timestampGMT").cast('long'))
     .rangeBetween(-days(7), 0))

df2 = df.withColumn('rolling_average', F.avg("dollars").over(w))

df2.show(100, False)

Production:

+---------+-------+---------------------+------------------+
|name     |dollars|timestampGMT         |rolling_average   |
+---------+-------+---------------------+------------------+
|ash      |9999.0 |2018-04-16 13:27:18.0|9999.0            |
|tshilidzi|17.0   |2018-03-10 17:27:18.0|17.0              |
|tshilidzi|13.0   |2018-03-11 14:27:18.0|15.0              |
|tshilidzi|25.0   |2018-03-12 13:27:18.0|18.333333333333332|
|tshilidzi|156.0  |2019-03-22 13:27:18.0|156.0             |
|tshilidzi|7000.0 |2019-04-15 13:27:18.0|7000.0            |
|thabo    |20.0   |2018-03-13 17:27:18.0|20.0              |
|thabo    |56.0   |2018-03-14 14:27:18.0|38.0              |
|thabo    |99.0   |2018-03-15 13:27:18.0|58.333333333333336|
|thabo    |122.0  |2018-03-31 13:27:18.0|122.0             |
+---------+-------+---------------------+------------------+
2
Tshilidzi Mudau

Est-ce que tu veux dire ça :

df = spark.createDataFrame([(17, "2017-03-11T15:27:18+00:00"),
                            (13, "2017-03-11T12:27:18+00:00"),
                            (21, "2017-03-17T11:27:18+00:00")],
                           ["dollars", "timestampGMT"])
df = df.withColumn('timestampGMT', df.timestampGMT.cast('timestamp'))
df = df.withColumn('rolling_average', f.avg("dollars").over(Window.partitionBy(f.window("timestampGMT", "7 days"))))

Production:

+-------+-------------------+---------------+                                   
|dollars|timestampGMT       |rolling_average|
+-------+-------------------+---------------+
|21     |2017-03-17 19:27:18|21.0           |
|17     |2017-03-11 23:27:18|15.0           |
|13     |2017-03-11 20:27:18|15.0           |
+-------+-------------------+---------------+
1
Zhang Tong

Il convient de noter que si vous ne vous souciez pas des dates exactes, mais que vous souhaitez avoir la moyenne des 30 derniers jours disponibles, vous pouvez utiliser la fonction rowBetween comme suit:

w = Window.orderBy('timestampGMT').rowsBetween(-7, 0)

df = eurPrices.withColumn('rolling_average', F.avg('dollars').over(w))

Puisque vous commandez par les dates, cela prendra les 7 dernières occurrences. Vous enregistrez tout le casting.

0
user1919071