web-dev-qa-db-fra.com

SPARK Remplacement SQL pour la fonction d'agrégation mysql GROUP_CONCAT

J'ai un tableau de deux colonnes de type chaîne (nom d'utilisateur, ami) et pour chaque nom d'utilisateur, je veux collecter tous ses amis sur une ligne, concaténés sous forme de chaînes (`` nom d'utilisateur1 '', `` amis1, amis2, amis3 ''). Je sais que MySql le fait par GROUP_CONCAT, est-il possible de le faire avec SPARK SQL?

Merci

28
Zahra I.S

Avant de continuer: Cette opération est encore une autre groupByKey. Bien qu'il possède plusieurs applications légitimes, il est relativement coûteux, alors assurez-vous de ne l'utiliser que lorsque cela est nécessaire.


Solution pas exactement concise ou efficace mais vous pouvez utiliser UserDefinedAggregateFunction introduit dans Spark 1.5.0:

object GroupConcat extends UserDefinedAggregateFunction {
    def inputSchema = new StructType().add("x", StringType)
    def bufferSchema = new StructType().add("buff", ArrayType(StringType))
    def dataType = StringType
    def deterministic = true 

    def initialize(buffer: MutableAggregationBuffer) = {
      buffer.update(0, ArrayBuffer.empty[String])
    }

    def update(buffer: MutableAggregationBuffer, input: Row) = {
      if (!input.isNullAt(0)) 
        buffer.update(0, buffer.getSeq[String](0) :+ input.getString(0))
    }

    def merge(buffer1: MutableAggregationBuffer, buffer2: Row) = {
      buffer1.update(0, buffer1.getSeq[String](0) ++ buffer2.getSeq[String](0))
    }

    def evaluate(buffer: Row) = UTF8String.fromString(
      buffer.getSeq[String](0).mkString(","))
}

Exemple d'utilisation:

val df = sc.parallelize(Seq(
  ("username1", "friend1"),
  ("username1", "friend2"),
  ("username2", "friend1"),
  ("username2", "friend3")
)).toDF("username", "friend")

df.groupBy($"username").agg(GroupConcat($"friend")).show

## +---------+---------------+
## | username|        friends|
## +---------+---------------+
## |username1|friend1,friend2|
## |username2|friend1,friend3|
## +---------+---------------+

Vous pouvez également créer un wrapper Python comme indiqué dans Spark: comment mapper Python avec Scala ou Java Fonctions définies par l'utilisateur?

En pratique, il peut être plus rapide d'extraire RDD, groupByKey, mkString et de reconstruire DataFrame.

Vous pouvez obtenir un effet similaire en combinant collect_list fonction (Spark> = 1.6.0) avec concat_ws:

import org.Apache.spark.sql.functions.{collect_list, udf, lit}

df.groupBy($"username")
  .agg(concat_ws(",", collect_list($"friend")).alias("friends"))
39
zero323

Vous pouvez essayer la fonction collect_list

sqlContext.sql("select A, collect_list(B), collect_list(C) from Table1 group by A

Ou vous pouvez enregistrer un UDF quelque chose comme

sqlContext.udf.register("myzip",(a:Long,b:Long)=>(a+","+b))

et vous pouvez utiliser cette fonction dans la requête

sqlConttext.sql("select A,collect_list(myzip(B,C)) from tbl group by A")
15
iec2011007

Voici une fonction que vous pouvez utiliser dans PySpark:

import pyspark.sql.functions as F

def group_concat(col, distinct=False, sep=','):
    if distinct:
        collect = F.collect_set(col.cast(StringType()))
    else:
        collect = F.collect_list(col.cast(StringType()))
    return F.concat_ws(sep, collect)


table.groupby('username').agg(F.group_concat('friends').alias('friends'))

En SQL:

select username, concat_ws(',', collect_list(friends)) as friends
from table
group by username
5
rikturr

Une façon de le faire avec pyspark <1.6, qui malheureusement ne prend pas en charge la fonction d'agrégation définie par l'utilisateur:

byUsername = df.rdd.reduceByKey(lambda x, y: x + ", " + y)

et si vous voulez en faire à nouveau une trame de données:

sqlContext.createDataFrame(byUsername, ["username", "friends"])

Depuis 1.6, vous pouvez utiliser collect_list puis rejoindre la liste créée:

from pyspark.sql import functions as F
from pyspark.sql.types import StringType
join_ = F.udf(lambda x: ", ".join(x), StringType())
df.groupBy("username").agg(join_(F.collect_list("friend").alias("friends"))
3
ksindi

Langue: Scala version Spark: 1.5.2

J'ai eu le même problème et j'ai également essayé de le résoudre à l'aide de udfs mais, malheureusement, cela a entraîné plus de problèmes plus tard dans le code en raison d'incohérences de type. J'ai pu contourner ce problème en convertissant d'abord le DF en RDD puis regroupement par et en manipulant les données de la manière souhaitée, puis en convertissant le RDD retour à un DF comme suit:

val df = sc
     .parallelize(Seq(
        ("username1", "friend1"),
        ("username1", "friend2"),
        ("username2", "friend1"),
        ("username2", "friend3")))
     .toDF("username", "friend")

+---------+-------+
| username| friend|
+---------+-------+
|username1|friend1|
|username1|friend2|
|username2|friend1|
|username2|friend3|
+---------+-------+

val dfGRPD = df.map(Row => (Row(0), Row(1)))
     .groupByKey()
     .map{ case(username:String, groupOfFriends:Iterable[String]) => (username, groupOfFriends.mkString(","))}
     .toDF("username", "groupOfFriends")

+---------+---------------+
| username| groupOfFriends|
+---------+---------------+
|username1|friend2,friend1|
|username2|friend3,friend1|
+---------+---------------+
2

Ci-dessous le code basé sur python qui permet d'atteindre la fonctionnalité group_concat.

Des données d'entrée:

Cust_No, Cust_Cars

1, Toyota

2, BMW

1, Audi

2, Hyundai

from pyspark.sql import SparkSession
from pyspark.sql.types import StringType
from pyspark.sql.functions import udf
import pyspark.sql.functions as F

spark = SparkSession.builder.master('yarn').getOrCreate()

# Udf to join all list elements with "|"
def combine_cars(car_list,sep='|'):
  collect = sep.join(car_list)
  return collect

test_udf = udf(combine_cars,StringType())
car_list_per_customer.groupBy("Cust_No").agg(F.collect_list("Cust_Cars").alias("car_list")).select("Cust_No",test_udf("car_list").alias("Final_List")).show(20,False)

Données de sortie: Cust_No, Final_List

1, Toyota | Audi

2, BMW | Hyundai

0
Akshay Patel