Pyspark - How to aggregate over '4 hours' windows in groups

Viewed 543

I have a dataset as follows:

id  email   Date_of_purchase    time_of_purchase
1   abc@gmail.com   11/10/18    12:10 PM
2   abc@gmail.com   11/10/18    02:11 PM
3   abc@gmail.com   11/10/18    03:14 PM
4   abc@gmail.com   11/11/18    06:16 AM
5   abc@gmail.com   11/11/18    09:10 AM
6   def@gmail.com   11/10/18    12:17 PM
7   def@gmail.com   11/10/18    03:24 PM
8   def@gmail.com   11/10/18    08:16 PM
9   def@gmail.com   11/10/18    09:13 PM
10  def@gmail.com   11/11/18    12:01 AM

I want to calculate the number of transactions made by each email ids within 4 hours. For example, email ids: abc@gmail.com made 3 transactions starting from 11/10/18 12.10 PM to 11/10/18 4.10 PM and made 2 transactions starting from 11/11/18 6.16 AM to 11/11/18 10.16 AM. email ids: def@gmail.com made 2 transactions starting from 11/10/18 12.17 PM to 11/10/18 4.17 PM and made 3 transactions starting from 11/10/18 8.16 PM to 11/11/18 12.16 AM.

My desired output is:

 email          hour_interval                           purchase_in_4_hours
abc@gmail.com   [11/10/18 12.10 PM to 11/10/18 4.10 PM] 3
abc@gmail.com   [11/11/18 6.16 AM to 11/11/18 10.16 AM] 2
def@gmail.com   [11/10/18 12.17 PM to 11/10/18 4.17 PM] 2
def@gmail.com   [11/10/18 8.16 PM to 11/11/18 12.16 AM] 3

My dataset is having 1000k rows. I am very new in spark. Any help will be highly appreciated. P.S. The time interval can change from 4 hours to 1 hour, 6, hour, 1 day, etc.

TIA.

1 Answers

The idea is to partition the data by email, sort within each partition by date and time and then map each partition to the desired output. This approach will work if the data for each partition (=data for one email adress) fits into the memory of one Spark executor.

The acutal Spark logic follows the steps

  1. Create a new column which contains the timestamps
  2. Partition the data by email, so that all rows with the same email are part of the same partition. Note that there might be data from more than one email in one partition.
  3. Sort each partition by email and timestamp.
  4. Process each partition. If necessary produce multiple outputs per partition as required
from pyspark.sql.functions import *
from pyspark.sql import SparkSession
from pyspark.sql.types import Row
from datetime import datetime, timedelta

spark = SparkSession.builder.appName("test").getOrCreate()
df = spark.read.option("header", "true").csv(<path>) #or any other data source
df = df.withColumn("date_time", to_timestamp(concat(col("Date_of_purchase"), lit(" "), col("time_of_purchase")), "MM/dd/yy hh:mm aa")) \
    .drop("Date_of_purchase", "time_of_purchase") \
    .repartition(col("email")) \
    .sortWithinPartitions(col("email"), col("date_time"))

def process_partition(df_chunk):
    row_list = list(df_chunk)
    if len(row_list) == 0:
        return
    email = row_list[0]['email']
    start = row_list[0]['date_time']
    end = start + timedelta(hours=4)
    count = 0
    for row in row_list:
        if email == row['email'] and end > row['date_time']:
            count = count +1
        else:
            yield Row(email, start, end, count)
            email = row['email']
            start = row['date_time']
            end = start + timedelta(hours=4)
            count = 1
    yield Row(email, start, end, count)

result = df.rdd.mapPartitions(process_partition).toDF(["email", "from", "to", "count"])
result.show()

Output:

+-------------+-------------------+-------------------+-----+
|        email|               from|                 to|count|
+-------------+-------------------+-------------------+-----+
|def@gmail.com|2018-11-10 12:17:00|2018-11-10 16:17:00|    2|
|def@gmail.com|2018-11-10 20:16:00|2018-11-11 00:16:00|    3|
|abc@gmail.com|2018-11-10 12:10:00|2018-11-10 16:10:00|    3|
|abc@gmail.com|2018-11-11 06:16:00|2018-11-11 10:16:00|    2|
+-------------+-------------------+-------------------+-----+

To change the length of the period, the timedeltas can be set to any value.

Related