web-dev-qa-db-fra.com

Réduire une paire clé-valeur en une paire liste-clés avec Apache Spark

J'écris une application Spark et souhaite combiner un ensemble de paires clé-valeur (K, V1), (K, V2), ..., (K, Vn) en une paire clé-valeur multiples (K, [V1, V2, ..., Vn]). Je pense que je devrais pouvoir faire cela en utilisant la fonction reduceByKey avec quelque chose de la saveur:

My_KMV = My_KV.reduce(lambda a, b: a.append([b]))

L'erreur que je reçois quand cela se produit est la suivante:

L'objet 'NoneType' n'a pas d'attribut 'append'.

Mes clés sont des entiers et les valeurs V1, ..., Vn sont des n-uplets. Mon objectif est de créer une seule paire avec la clé et une liste des valeurs (n-uplets).

39
TravisJ

Carte et réductionPar clé

Le type d'entrée et le type de sortie de reduce doivent être identiques. Par conséquent, si vous souhaitez agréger une liste, vous devez map l'entrée aux listes. Ensuite, vous combinez les listes en une seule.

Combinaison de listes

Vous aurez besoin d'une méthode pour combiner des listes en une seule liste. Phyton fournit quelques méthodes pour combiner des listes .

append modifie la première liste et retournera toujours None.

x = [1, 2, 3]
x.append([4, 5])
# x is [1, 2, 3, [4, 5]]

extend fait la même chose, mais ouvre les listes:

x = [1, 2, 3]
x.extend([4, 5])
# x is [1, 2, 3, 4, 5]

Les deux méthodes retournent None, mais vous aurez besoin d'une méthode qui retourne la liste combinée. Par conséquent, juste utilisez le signe plus .

x = [1, 2, 3] + [4, 5]
# x is [1, 2, 3, 4, 5]

Spark

file = spark.textFile("hdfs://...")
counts = file.flatMap(lambda line: line.split(" ")) \
         .map(lambda actor: (actor.split(",")[0], actor)) \ 

         # transform each value into a list
         .map(lambda nameTuple: (nameTuple[0], [ nameTuple[1] ])) \

         # combine lists: ([1,2,3] + [4,5]) becomes [1,2,3,4,5]
         .reduceByKey(lambda a, b: a + b)

CombineByKey

Il est également possible de résoudre ce problème avec combineByKey, utilisé en interne pour implémenter reduceByKey, mais il est plus complexe et "utiliser l'un des combinateurs spécialisés par clé de Spark peut être beaucoup plus rapide" . Votre cas d'utilisation est assez simple pour la solution supérieure.

GroupByKey

Il est également possible de résoudre ce problème avec groupByKey, mais cela réduit la parallélisation et pourrait donc être beaucoup plus lent pour les grands ensembles de données.

44

Je suis un peu en retard dans la conversation, mais voici ma suggestion:

>>> foo = sc.parallelize([(1, ('a','b')), (2, ('c','d')), (1, ('x','y'))])
>>> foo.map(lambda (x,y): (x, [y])).reduceByKey(lambda p,q: p+q).collect()
[(1, [('a', 'b'), ('x', 'y')]), (2, [('c', 'd')])]
13
alreich

Vous pouvez utiliser la méthode RDD groupByKey .

Contribution:

data = [(1, 'a'), (1, 'b'), (2, 'c'), (2, 'd'), (2, 'e'), (3, 'f')]
rdd = sc.parallelize(data)
result = rdd.groupByKey().collect()

Sortie:

[(1, ['a', 'b']), (2, ['c', 'd', 'e']), (3, ['f'])]
11
Marius Ion

Si vous voulez faire un réducteurByKey où le type des paires de KV réduites est différent de celui des paires de KV d'origine, vous pouvez utiliser la fonction combineByKey. Ce que la fonction fait est de prendre des paires KV et de les combiner (par clé) en paires KC où C est d'un type différent de V.

On spécifie 3 fonctions, createCombiner, mergeValue, mergeCombiners. La première spécifie comment transformer un type V en un type C, la seconde explique comment combiner un type C avec un type V et la dernière indique comment combiner un type C avec un autre type C. Mon code crée les paires K-V:

Définissez les 3 fonctions comme suit:

def Combiner(a):    #Turns value a (a Tuple) into a list of a single Tuple.
    return [a]

def MergeValue(a, b): #a is the new type [(,), (,), ..., (,)] and b is the old type (,)
    a.extend([b])
    return a

def MergeCombiners(a, b): #a is the new type [(,),...,(,)] and so is b, combine them
    a.extend(b)
    return a

Alors, My_KMV = My_KV.combineByKey(Combiner, MergeValue, MergeCombiners)

La meilleure ressource que j'ai trouvée sur l'utilisation de cette fonction est: http://abshinn.github.io/python/Apache-spark/2014/10/11/using-combinebykey-in-Apache-spark/

Comme d'autres l'ont fait remarquer, a.append(b) ou a.extend(b) return None. Donc, la reduceByKey(lambda a, b: a.append(b)) renvoie None sur la première paire de paires KV, puis échoue sur la seconde paire car None.append (b) échoue. Vous pouvez contourner ce problème en définissant une fonction distincte:

 def My_Extend(a,b):
      a.extend(b)
      return a

Ensuite, appelez reduceByKey(lambda a, b: My_Extend(a,b)) (L'utilisation de la fonction lambda ici peut être inutile, mais je n'ai pas testé ce cas.)

3
TravisJ

Le message d'erreur provient du type pour 'a' dans votre fermeture.

 My_KMV = My_KV.reduce(lambda a, b: a.append([b]))

Laissez pySpark évaluer explicitement a en tant que liste. Par exemple,

My_KMV = My_KV.reduceByKey(lambda a,b:[a].extend([b]))

Dans de nombreux cas, reductionByKey sera préférable à groupByKey, voir: http://databricks.gitbooks.io/databricks-spark-knowledge-base/content/best_practices/prefer_reducebykey_over_groupbykey.html

1
Seung-Hwan Lim

J'ai essayé avec combineByKey, voici mes étapes 

combineddatardd=sc.parallelize([("A", 3), ("A", 9), ("A", 12),("B", 4), ("B", 10), ("B", 11)])

combineddatardd.combineByKey(lambda v:[v],lambda x,y:x+[y],lambda x,y:x+y).collect()

Sortie:

[('A', [3, 9, 12]), ('B', [4, 10, 11])]
  1. Définit une fonction pour le combineur qui définit l'accumulateur sur la première paire de valeurs de clé rencontrée à l'intérieur de la partition convertit la valeur en liste à cette étape.

  2. Définit une fonction qui fusionne la nouvelle valeur de la même clé avec la valeur de l'accumulateur capturée à l'étape 1. Remarque: -convertir la valeur à la liste dans cette fonction car la valeur de l'accumulateur a été convertie en liste à la première étape. 

  3. Définit la fonction pour fusionner les sorties de combineurs de partitions individuelles.

1
krishna rachur

D'accord. J'espère que j'ai bien compris. Votre contribution ressemble à ceci:

kv_input = [("a", 1), ("a", 2), ("a", 3), ("b", 1), ("b", 5)]

et vous voulez obtenir quelque chose comme ça:

kmv_output = [("a", [1, 2, 3]), ("b", [1, 5])]

Ensuite, cela pourrait faire le travail (voir ici ):

d = dict()
for k, v in kv_input:
    d.setdefault(k, list()).append(v)
kmv_output = list(d.items())

Si je me trompe, dites-le-moi, je pourrais l’adapter à vos besoins.

P.S .: a.append([b]) renvoie toujours None. Vous voudrez peut-être observer [b] ou a mais pas le résultat de append.

1
Dave J

J'ai cliqué sur cette page en cherchant un exemple Java pour le même problème. (Si votre cas est similaire, voici mon exemple)

L'astuce est la suivante: vous devez regrouper les clés.

import org.Apache.spark.SparkConf;
import org.Apache.spark.api.Java.JavaPairRDD;
import org.Apache.spark.api.Java.JavaRDD;
import org.Apache.spark.api.Java.JavaSparkContext;
import scala.Tuple2;

import Java.util.Arrays;
import Java.util.List;
import Java.util.stream.Collectors;
import Java.util.stream.StreamSupport;

public class SparkMRExample {

    public static void main(String[] args) {
        // spark context initialisation
        SparkConf conf = new SparkConf()
                .setAppName("WordCount")
                .setMaster("local");
        JavaSparkContext context = new JavaSparkContext(conf);

        //input for testing;
        List<String> input = Arrays.asList("Lorem Ipsum is simply dummy text of the printing and typesetting industry.",
                "Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.",
                "It has survived not only for centuries, but also the leap into electronic typesetting, remaining essentially unchanged.",
                "It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing");
        JavaRDD<String> inputRDD = context.parallelize(input);


        // the map phase of Word count example
        JavaPairRDD<String, Integer> mappedRDD =
                inputRDD.flatMapToPair( line ->                      // for this input, each string is a line
                        Arrays.stream(line.split("\\s+"))            // splitting into words, converting into stream
                                .map(Word -> new Tuple2<>(Word, 1))  // each Word is assigned with count 1
                                .collect(Collectors.toList()));      // stream to iterable

        // group the tuples by key
        // (String,Integer) -> (String, Iterable<Integer>)
        JavaPairRDD<String, Iterable<Integer>> groupedRDD = mappedRDD.groupByKey();

        // the reduce phase of Word count example
        //(String, Iterable<Integer>) -> (String,Integer)
        JavaRDD<Tuple2<String, Integer>> resultRDD =
                groupedRDD.map(group ->                                      //input is a Tuple (String, Iterable<Integer>)
                        new Tuple2<>(group._1,                              // the output key is same as input key
                        StreamSupport.stream(group._2.spliterator(), true)  // converting to stream
                                .reduce(0, (f, s) -> f + s)));              // the sum of counts
        //collecting the RRD so that we can print
        List<Tuple2<String, Integer>> result = resultRDD.collect();
        // print each Tuple
        result.forEach(System.out::println);
    }
}
0
Thamme Gowda