web-dev-qa-db-fra.com

Comment trouver la médiane et les quantiles à l'aide de Spark

Comment trouver la médiane d'une RDD d'entiers à l'aide d'une méthode distribuée, IPython et Spark? La RDD correspond à environ 700 000 éléments et est donc trop volumineuse pour pouvoir collecter et trouver la médiane.

Cette question est similaire à cette question. Cependant, la réponse à la question utilise Scala, que je ne connais pas.

Comment calculer la médiane exacte avec Apache Spark?

En utilisant la pensée pour la réponse Scala, j'essaie d'écrire une réponse similaire en Python.

Je sais que je veux d’abord trier la RDD. Je ne sais pas comment. Je vois les méthodes sortBy (trie ce RDD selon la variable keyfunc) et sortByKey (trie cette RDD, supposée être constituée de paires (clé, valeur).). Je pense que les deux utilisent la valeur clé et que ma RDD ne contient que des éléments entiers.

  1. Premièrement, je pensais faire myrdd.sortBy(lambda x: x)
  2. Ensuite, je trouverai la longueur du rdd (rdd.count()).
  3. Enfin, je veux trouver l’élément ou les 2 éléments au centre du rdd. J'ai aussi besoin d'aide avec cette méthode.

MODIFIER:

J'ai eu une idée. Peut-être que je peux indexer ma RDD puis key = index et value = element. Et puis je peux essayer de trier par valeur? Je ne sais pas si cela est possible car il n'y a qu'une méthode sortByKey.

42
pr338

Spark 2.0+:

Vous pouvez utiliser la méthode approxQuantile qui implémente Algorithme de Greenwald-Khanna :

Python:

df.approxQuantile("x", [0.5], 0.25)

Scala:

df.stat.approxQuantile("x", Array(0.5), 0.25)

où le dernier paramètre est une erreur relative. Plus le nombre est bas, plus les résultats sont précis et les calculs plus coûteux.

Depuis Spark 2.2 ( SPARK-14352 ), il prend en charge l’estimation sur plusieurs colonnes:

df.approxQuantile(["x", "y", "z"], [0.5], 0.25)

et 

df.approxQuantile(Array("x", "y", "z"), Array(0.5), 0.25)

Étincelle <2.0

Python

Comme je l'ai mentionné dans les commentaires, cela ne vaut probablement pas la peine. Si les données sont relativement petites, comme dans votre cas, vous devez simplement collecter et calculer la médiane localement:

import numpy as np

np.random.seed(323)
rdd = sc.parallelize(np.random.randint(1000000, size=700000))

%time np.median(rdd.collect())
np.array(rdd.collect()).nbytes

Cela prend environ 0,01 seconde sur mon ordinateur de quelques années et environ 5,5 Mo de mémoire.

Si les données sont beaucoup plus volumineuses, le tri sera un facteur limitant. Il est donc probablement préférable d’échantillonner, de recueillir et de calculer localement au lieu d’obtenir une valeur exacte. Mais si vous voulez vraiment utiliser Spark, quelque chose comme ceci devrait faire l'affaire (si je ne me suis pas trompé):

from numpy import floor
import time

def quantile(rdd, p, sample=None, seed=None):
    """Compute a quantile of order p ∈ [0, 1]
    :rdd a numeric rdd
    :p quantile(between 0 and 1)
    :sample fraction of and rdd to use. If not provided we use a whole dataset
    :seed random number generator seed to be used with sample
    """
    assert 0 <= p <= 1
    assert sample is None or 0 < sample <= 1

    seed = seed if seed is not None else time.time()
    rdd = rdd if sample is None else rdd.sample(False, sample, seed)

    rddSortedWithIndex = (rdd.
        sortBy(lambda x: x).
        zipWithIndex().
        map(lambda (x, i): (i, x)).
        cache())

    n = rddSortedWithIndex.count()
    h = (n - 1) * p

    rddX, rddXPlusOne = (
        rddSortedWithIndex.lookup(x)[0]
        for x in int(floor(h)) + np.array([0L, 1L]))

    return rddX + (h - floor(h)) * (rddXPlusOne - rddX)

Et quelques tests:

np.median(rdd.collect()), quantile(rdd, 0.5)
## (500184.5, 500184.5)
np.percentile(rdd.collect(), 25), quantile(rdd, 0.25)
## (250506.75, 250506.75)
np.percentile(rdd.collect(), 75), quantile(rdd, 0.75)
(750069.25, 750069.25)

Enfin, définissons la médiane:

from functools import partial
median = partial(quantile, p=0.5)

Jusqu'ici tout va bien, mais il faut 4,66 s en mode local sans aucune communication réseau. Il y a probablement moyen d'améliorer cela, mais pourquoi même s'en donner la peine?

Indépendant de la langue (Hive UDAF): 

Si vous utilisez HiveContext, vous pouvez également utiliser des UDAF de Hive. Avec des valeurs intégrales:

rdd.map(lambda x: (float(x), )).toDF(["x"]).registerTempTable("df")

sqlContext.sql("SELECT percentile_approx(x, 0.5) FROM df")

Avec des valeurs continues:

sqlContext.sql("SELECT percentile(x, 0.5) FROM df")

Dans percentile_approx, vous pouvez passer un argument supplémentaire qui détermine le nombre d'enregistrements à utiliser.

71
zero323

Ajouter une solution si vous voulez une méthode RDD uniquement et que vous ne voulez pas passer à DF . Cet extrait peut vous donner un centile pour un RDD de double.

Si vous entrez un centile égal à 50, vous devriez obtenir la médiane requise ..___ Faites-moi savoir s'il y a des cas en coin non pris en compte.

/**
  * Gets the nth percentile entry for an RDD of doubles
  *
  * @param inputScore : Input scores consisting of a RDD of doubles
  * @param percentile : The percentile cutoff required (between 0 to 100), e.g 90%ile of [1,4,5,9,19,23,44] = ~23.
  *                     It prefers the higher value when the desired quantile lies between two data points
  * @return : The number best representing the percentile in the Rdd of double
  */    
  def getRddPercentile(inputScore: RDD[Double], percentile: Double): Double = {
    val numEntries = inputScore.count().toDouble
    val retrievedEntry = (percentile * numEntries / 100.0 ).min(numEntries).max(0).toInt


    inputScore
      .sortBy { case (score) => score }
      .zipWithIndex()
      .filter { case (score, index) => index == retrievedEntry }
      .map { case (score, index) => score }
      .collect()(0)
  }
6
Vedant

Voici la méthode que j'ai utilisée en utilisant les fonctions de fenêtre (avec pyspark 2.2.0).

from pyspark.sql import DataFrame

class median():
    """ Create median class with over method to pass partition """
    def __init__(self, df, col, name):
        assert col
        self.column=col
        self.df = df
        self.name = name

    def over(self, window):
        from pyspark.sql.functions import percent_rank, pow, first

        first_window = window.orderBy(self.column)                                  # first, order by column we want to compute the median for
        df = self.df.withColumn("percent_rank", percent_rank().over(first_window))  # add percent_rank column, percent_rank = 0.5 coressponds to median
        second_window = window.orderBy(pow(df.percent_rank-0.5, 2))                 # order by (percent_rank - 0.5)^2 ascending
        return df.withColumn(self.name, first(self.column).over(second_window))     # the first row of the window corresponds to median

def addMedian(self, col, median_name):
    """ Method to be added to spark native DataFrame class """
    return median(self, col, median_name)

# Add method to DataFrame class
DataFrame.addMedian = addMedian

Appelez ensuite la méthode addMedian pour calculer la médiane de col2:

from pyspark.sql import Window

median_window = Window.partitionBy("col1")
df = df.addMedian("col2", "median").over(median_window)

Enfin, vous pouvez regrouper par si nécessaire.

df.groupby("col1", "median")
5
Benoît Carne

J'ai écrit la fonction qui prend un cadre de données en entrée et renvoie un cadre de données dont la sortie est médiane sur une partition et order_col est la colonne pour laquelle nous voulons calculer la médiane pour part_col est le niveau auquel nous voulons calculer :

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

def calculate_median(dataframe, part_col, order_col):
    win = Window.partitionBy(*part_col).orderBy(order_col)
#     count_row = dataframe.groupby(*part_col).distinct().count()
    dataframe.persist()
    dataframe.count()
    temp = dataframe.withColumn("rank", F.row_number().over(win))
    temp = temp.withColumn(
        "count_row_part",
        F.count(order_col).over(Window.partitionBy(part_col))
    )
    temp = temp.withColumn(
        "even_flag",
        F.when(
            F.col("count_row_part") %2 == 0,
            F.lit(1)
        ).otherwise(
            F.lit(0)
        )
    ).withColumn(
        "mid_value",
        F.floor(F.col("count_row_part")/2)
    )

    temp = temp.withColumn(
        "avg_flag",
        F.when(
            (F.col("even_flag")==1) &
            (F.col("rank") == F.col("mid_value"))|
            ((F.col("rank")-1) == F.col("mid_value")),
            F.lit(1)
        ).otherwise(
        F.when(
            F.col("rank") == F.col("mid_value")+1,
            F.lit(1)
            )
        )
    )
    temp.show(10)
    return temp.filter(
        F.col("avg_flag") == 1
    ).groupby(
        part_col + ["avg_flag"]
    ).agg(
        F.avg(F.col(order_col)).alias("median")
    ).drop("avg_flag")
1
Ankit Kumar Namdeo