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.
myrdd.sortBy(lambda x: x)
? rdd.count()
).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
.
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)
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.
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)
}
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")
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")