J'ai un rdd d'entiers (c'est-à-dire RDD[Int]
) et ce que j'aimerais faire, c'est calculer les dix centiles suivants: [0th, 10th, 20th, ..., 90th, 100th]
. Quel est le moyen le plus efficace de le faire?
Vous pouvez :
Pour calculer la médiane et le 99e centile: GetPercentiles (rdd, new double [] {0.5, 0.99}, size, numPartitions);
En Java 8:
public static double[] getPercentiles(JavaRDD<Double> rdd, double[] percentiles, long rddSize, int numPartitions) {
double[] values = new double[percentiles.length];
JavaRDD<Double> sorted = rdd.sortBy((Double d) -> d, true, numPartitions);
JavaPairRDD<Long, Double> indexed = sorted.zipWithIndex().mapToPair((Tuple2<Double, Long> t) -> t.swap());
for (int i = 0; i < percentiles.length; i++) {
double percentile = percentiles[i];
long id = (long) (rddSize * percentile);
values[i] = indexed.lookup(id).get(0);
}
return values;
}
Notez que cela nécessite de trier le jeu de données, O(n.log(n)) et peut être coûteux sur des jeux de données volumineux.
L'autre réponse suggérant que le simple calcul d'un histogramme ne calculerait pas correctement le centile: voici un exemple de compteur: un ensemble de données composé de 100 nombres, 99 nombres étant 0 et un nombre étant 1. Vous vous retrouvez avec tous les 99 0 du premier bin, et le 1 dans le dernier bac, avec 8 bacs vides au milieu.
Que diriez-vous de t-digest?
https://github.com/tdunning/t-digest
Une nouvelle structure de données pour une accumulation en ligne précise de statistiques basées sur les rangs telles que les quantiles et les moyennes ajustées. L'algorithme t-digest est également très convivial en parallèle, ce qui le rend utile dans les applications de streaming de carte et de réduction parallèle.
L'algorithme de construction t-digest utilise une variante de la classification en k-moyennes à une dimension pour produire une structure de données liée au Q-digest. Cette structure de données t-digest peut être utilisée pour estimer des quantiles ou pour calculer d'autres statistiques de rang. L'avantage du t-digest par rapport au Q-digest est qu'il peut gérer des valeurs à virgule flottante alors que Q-digest est limité à des entiers. Avec de petits changements, le t-digest peut gérer toutes les valeurs de n'importe quel ensemble ordonné ayant une ressemblance avec une moyenne. La précision des estimations quantiles produites par les résumés t peut être des ordres de grandeur plus précis que celles produites par les résumés Q bien que ces derniers soient plus compacts lorsqu'ils sont stockés sur disque.
En résumé, les caractéristiques particulièrement intéressantes du t-digest sont qu’il
- a des résumés plus petits que Q-digest
- fonctionne aussi bien sur les doubles que sur les entiers.
- fournit une précision partie par million pour les quantiles extrêmes et généralement une précision inférieure à 1 000 ppm pour les quantiles moyens
- est rapide
- est très simple
- a une implémentation de référence avec une couverture de test> 90%
- peut être utilisé avec map-réduire très facilement car les résumés peuvent être fusionnés
Il devrait être assez facile d'utiliser l'implémentation Java de référence de Spark.
J'ai découvert ce Gist
https://Gist.github.com/felixcheung/92ae74bc349ea83a9e29
qui contient la fonction suivante:
/**
* compute percentile from an unsorted Spark RDD
* @param data: input data set of Long integers
* @param tile: percentile to compute (eg. 85 percentile)
* @return value of input data at the specified percentile
*/
def computePercentile(data: RDD[Long], tile: Double): Double = {
// NIST method; data to be sorted in ascending order
val r = data.sortBy(x => x)
val c = r.count()
if (c == 1) r.first()
else {
val n = (tile / 100d) * (c + 1d)
val k = math.floor(n).toLong
val d = n - k
if (k <= 0) r.first()
else {
val index = r.zipWithIndex().map(_.swap)
val last = c
if (k >= c) {
index.lookup(last - 1).head
} else {
index.lookup(k - 1).head + d * (index.lookup(k).head - index.lookup(k - 1).head)
}
}
}
}
Voici mon implémentation Python sur Spark pour le calcul du centile d’un RDD contenant des valeurs d’intérêt.
def percentile_threshold(ardd, percentile):
assert percentile > 0 and percentile <= 100, "percentile should be larger then 0 and smaller or equal to 100"
return ardd.sortBy(lambda x: x).zipWithIndex().map(lambda x: (x[1], x[0])) \
.lookup(np.ceil(ardd.count() / 100 * percentile - 1))[0]
# Now test it out
import numpy as np
randlist = range(1,10001)
np.random.shuffle(randlist)
ardd = sc.parallelize(randlist)
print percentile_threshold(ardd,0.001)
print percentile_threshold(ardd,1)
print percentile_threshold(ardd,60.11)
print percentile_threshold(ardd,99)
print percentile_threshold(ardd,99.999)
print percentile_threshold(ardd,100)
# output:
# 1
# 100
# 6011
# 9900
# 10000
# 10000
Séparément, j'ai défini la fonction suivante pour obtenir le dixième au centième centile.
def get_percentiles(rdd, stepsize=10):
percentiles = []
rddcount100 = rdd.count() / 100
sortedrdd = ardd.sortBy(lambda x: x).zipWithIndex().map(lambda x: (x[1], x[0]))
for p in range(0, 101, stepsize):
if p == 0:
pass
# I am not aware of a formal definition of 0 percentile,
# you can put a place holder like this if you want
# percentiles.append(sortedrdd.lookup(0)[0] - 1)
Elif p == 100:
percentiles.append(sortedrdd.lookup(np.ceil(rddcount100 * 100 - 1))[0])
else:
pv = sortedrdd.lookup(np.ceil(rddcount100 * p) - 1)[0]
percentiles.append(pv)
return percentiles
randlist = range(1,10001)
np.random.shuffle(randlist)
ardd = sc.parallelize(randlist)
get_percentiles(ardd, 10)
# [1000, 2000, 3000, 4000, 5000, 6000, 7000, 8000, 9000, 10000]
Convertissez votre RDD en un RDD de Double, puis utilisez l’action .histogram(10)
. Voir DoubleRDD ScalaDoc
Si N pour cent est petit comme 10, 20% alors je ferai ce qui suit:
Calculez la taille de l'ensemble de données, rdd.count (), ignorez-le, vous le connaissez peut-être déjà et prenez comme argument.
Plutôt que de trier tout le jeu de données, je découvrirai le top (N) de chaque partition. Pour cela, je devrais trouver N = ce qui est N% de rdd.count, puis trier les partitions et prendre top (N) de chaque partition. Vous avez maintenant un ensemble de données beaucoup plus petit à trier.
3.rdd.sortBy
4.zipWithIndex
5.filter (index <topN)
Une autre solution consiste à utiliser top et last sur RDD ou double. Par exemple, val percentile_99th_value = scores.top ((count/100) .toInt) .last
Cette méthode est plus adaptée aux centiles individuels.
Basé sur la réponse donnée ici UDAF médian dans Spark/Scala , j'ai utilisé un UDAF pour calculer les centiles sur les fenêtres à étincelles (spark 2.1):
D'abord un UDAF générique abstrait utilisé pour d'autres agrégations
import org.Apache.spark.sql.Row
import org.Apache.spark.sql.expressions.{MutableAggregationBuffer, UserDefinedAggregateFunction}
import org.Apache.spark.sql.types._
import scala.collection.mutable
import scala.collection.mutable.ArrayBuffer
abstract class GenericUDAF extends UserDefinedAggregateFunction {
def inputSchema: StructType =
StructType(StructField("value", DoubleType) :: Nil)
def bufferSchema: StructType = StructType(
StructField("window_list", ArrayType(DoubleType, false)) :: Nil
)
def deterministic: Boolean = true
def initialize(buffer: MutableAggregationBuffer): Unit = {
buffer(0) = new ArrayBuffer[Double]()
}
def update(buffer: MutableAggregationBuffer,input: org.Apache.spark.sql.Row): Unit = {
var bufferVal = buffer.getAs[mutable.WrappedArray[Double]](0).toBuffer
bufferVal+=input.getAs[Double](0)
buffer(0) = bufferVal
}
def merge(buffer1: MutableAggregationBuffer, buffer2: org.Apache.spark.sql.Row): Unit = {
buffer1(0) = buffer1.getAs[ArrayBuffer[Double]](0) ++ buffer2.getAs[ArrayBuffer[Double]](0)
}
def dataType: DataType
def evaluate(buffer: Row): Any
}
Puis le UDAF en centile personnalisé pour les déciles:
import org.Apache.spark.sql.Row
import org.Apache.spark.sql.expressions.{MutableAggregationBuffer, UserDefinedAggregateFunction}
import org.Apache.spark.sql.types._
import scala.collection.mutable
import scala.collection.mutable.ArrayBuffer
class DecilesUDAF extends GenericUDAF {
override def dataType: DataType = ArrayType(DoubleType, false)
override def evaluate(buffer: Row): Any = {
val sortedWindow = buffer.getAs[mutable.WrappedArray[Double]](0).sorted.toBuffer
val windowSize = sortedWindow.size
if (windowSize == 0) return null
if (windowSize == 1) return (0 to 10).map(_ => sortedWindow.head).toArray
(0 to 10).map(i => sortedWindow(Math.min(windowSize-1, i*windowSize/10))).toArray
}
}
Le UDAF est ensuite instancié et appelé sur une fenêtre partitionnée et ordonnée:
val deciles = new DecilesUDAF()
df.withColumn("mt_deciles", deciles(col("mt")).over(myWindow))
Vous pouvez ensuite scinder le tableau résultant en plusieurs colonnes avec getItem:
def splitToColumns(size: Int, splitCol:String)(df: DataFrame) = {
(0 to size).foldLeft(df) {
case (df_arg, i) => df_arg.withColumn("mt_decile_"+i, col(splitCol).getItem(i))
}
}
df.transform(splitToColumns(10, "mt_deciles" ))
Le UDAF est plus lent que les fonctions d'allumage natives, mais tant que chaque sac groupé ou chaque fenêtre est relativement petite et s'adapte à un seul exécutant, tout devrait bien se passer. Le principal avantage est l’utilisation du parallélisme d’étincelles… .. Ce code pourrait être étendu aux n-quantiles avec peu d’effort.
J'ai testé le code en utilisant cette fonction:
def testDecilesUDAF = {
val window = W.partitionBy("user")
val deciles = new DecilesUDAF()
val schema = StructType(StructField("mt", DoubleType) :: StructField("user", StringType) :: Nil)
val rows1 = (1 to 20).map(i => Row(i.toDouble, "a"))
val rows2 = (21 to 40).map(i => Row(i.toDouble, "b"))
val df = spark.createDataFrame(spark.sparkContext.makeRDD[Row](rows1++rows2), schema)
df.withColumn("deciles", deciles(col("mt")).over(window))
.transform(splitToColumns(10, "deciles" ))
.drop("deciles")
.show(100, truncate=false)
}
3 premières lignes de sortie:
+----+----+-----------+-----------+-----------+-----------+-----------+-----------+-----------+-----------+-----------+-----------+------------+
|mt |user|mt_decile_0|mt_decile_1|mt_decile_2|mt_decile_3|mt_decile_4|mt_decile_5|mt_decile_6|mt_decile_7|mt_decile_8|mt_decile_9|mt_decile_10|
+----+----+-----------+-----------+-----------+-----------+-----------+-----------+-----------+-----------+-----------+-----------+------------+
|21.0|b |21.0 |23.0 |25.0 |27.0 |29.0 |31.0 |33.0 |35.0 |37.0 |39.0 |40.0 |
|22.0|b |21.0 |23.0 |25.0 |27.0 |29.0 |31.0 |33.0 |35.0 |37.0 |39.0 |40.0 |
|23.0|b |21.0 |23.0 |25.0 |27.0 |29.0 |31.0 |33.0 |35.0 |37.0 |39.0 |40.0 |