pyspark - left join with random row matching the key

Viewed 151

I am looking to a way to join 2 dataframes but with random rows matching the key. This strange request is due to a very long calculation to generate positions. I would like to do a kind of "random left join" in pyspark.

I have a dataframe with an areaID (string) and a count (int). The areaID is unique (around 7k).

+--------+-------+
| areaID | count |
+--------+-------+
| A      |    10 |
| B      |    30 |
| C      |     1 |
| D      |    25 |
| E      |    18 |
+--------+-------+

I have a second dataframe with around 1000 precomputed rows for each areaID with 2 positions columns x (float) and y (float). This dataframe is around 7 millions rows.

+--------+------+------+
| areaID |  x   |  y   |
+--------+------+------+
| A      |  0.0 |    0 |
| A      |  0.1 |  0.7 |
| A      |  0.3 |    1 |
| A      |  0.1 |  0.3 |
| ...    |      |      |
| E      | 3.15 | 4.17 |
| E      | 3.14 | 4.22 |
+--------+------+------+

I would like to end with a dataframe like:

+--------+------+------+
| areaID |  x   |  y   |
+--------+------+------+
| A      |  0.1 | 0.32 | < row 1/10 - randomly picked where areaID are the same
| A      |  0.0 | 0.18 | < row 2/10
| A      | 0.09 | 0.22 | < row 3/10
| ...    |      |      |
| E      | 3.14 | 4.22 | < row 1/18
| ...    |      |      |
+--------+------+------+

My first idea is to iterate over each areaID of the first dataframe, filter the second dataframe by areaID and sample count rows of this dataframe. The problem is that this is quite slow with 7k load/filtering/sampling processes.

The second approach is to do an outer join on areaID, then shuffle the dataframe (but seems quite complex), apply a rank and keep when the rank <= count but I don't like the approch to load a lot a data to filter afterward.

I am wondering if there is a way to do it using a "random" left join ? In that case, I'll duplicate each row count times and apply it.

Many thanks in advance,

Nicolas

1 Answers

One can interpret the question as stratified sampling of the second dataframe where the number of samples to be taken from each subpopulation is given by the first dataframe.

There is Spark function for stratified sampling.

df1 = ...
df2 = ...

#first calculate the fraction for each areaID based on the required number
#given in df1 and the number of rows for the areaID in df2
fractionRows = df2.groupBy("areaId").agg(F.count("areaId").alias("count2")) \
    .join(df1, "areaId") \
    .withColumn("fraction", F.col("count") / F.col("count2")) \
    .select("areaId", "fraction") \
    .collect()
fractions = {f[0]:f[1] for f in fractionRows}

#now run the statified samling
df2.stat.sampleBy("areaID", fractions).show()

There is caveat with this approach: as the sampling done by Spark is a random process, the exact number of rows given in the first dataframe will not always be met exactly.


Edit: fractions > 1.0 are not supported by sampleBy. Looking at the Scala code of sampleBy shows why: the function is implemented as filter with a random variable indicating whether to keep to row or not. Returning multiple copies of a single row will therefore not work.

A similar idea can be used to support fractions > 1.0: instead of using a filter, an udf is created that returns an array. The array contains one entry per copy of the row that should be contained in the result. After applying the udf, the array column is exploded and then dropped:

from pyspark.sql import functions as F
from pyspark.sql import types as T

fractions = {'A': 1.5, 'C': 0.5}
def ff(stratum,x):
    fraction =  fractions.get(stratum, 0.0)
    ret=[]
    while fraction >= 1.0:
        ret.append("x")
        fraction = fraction - 1
    if x < fraction:
        ret.append("x")
    return ret
f=F.udf(ff, T.ArrayType(T.StringType())).asNondeterministic()

seed=42

df2.withColumn("r", F.rand(seed)) \
    .withColumn("r",f("areaID", F.col("r")))\
    .withColumn("r", F.explode("r")) \
    .drop("r") \
    .show()
Related