如何从 percentile_approx 代码编写自定义函数,该函数在 excel 中给出与 percentile.inc 相同的结果?

Posted

技术标签:

【中文标题】如何从 percentile_approx 代码编写自定义函数,该函数在 excel 中给出与 percentile.inc 相同的结果?【英文标题】:How to write custom function from percentile_approx code which gives as equal result as percentile.inc in excel? 【发布时间】:2020-04-11 10:59:26 【问题描述】:

我在 Java 8 中使用 spark-sql-2.4.1v。我需要为某些给定数据计算百分位数,例如 25、75、90。

我尝试使用 Spark-sql 中的 percentile_approx() 来执行此操作。 但是percentile_approx() 的结果与使用PERCENTILE.INC() 的excel 表的小数百分比不匹配。

因此,我想知道如何修复或调整 percentile_approx() 函数。无论如何要覆盖或编写修改percentile_approx()的自定义函数,它可以正确计算小数百分位数?如何编写/修改percentile_approx()

给定数据集:

val df = Seq(
    (10, "1/15/2018", 0.010680705, 10,0.619875458, "east"),
    (10, "1/15/2018", 0.006628853,  4,0.16039063, "west"),
    (10, "1/15/2018", 0.01378215,  20,0.082049528, "east"),
    (10, "1/15/2018", 0.810680705,  6,0.819875458, "west"),
    (10, "1/15/2018", 0.702228853, 30,0.916039063, "east"))     
  .toDF("id", "date", "revenue", "con_dist_1", "con_dist_2", "zone")


val percentiles = Seq(0.25, 0.75,0.90)  // Which percentiles to calculate
val cols = Seq("con_dist_1", "con_dist_2")  // The columns to use

我需要计算给定列的每个区域的给定百分位数。 如何实现?

预期结果:

+---+---------+-----------+----+------------+--------------+--------------+-------------+
| id|     date|    revenue|zone|perctile_col|qunantile_0.25|qunantile_0.75|qunantile_0.9|
+---+---------+-----------+----+------------+--------------+--------------+-------------+
| 10|1/15/2018|0.006628853|west|  con_dist_1|           4.5|           5.5|          5.8|
| 10|1/15/2018|0.010680705|west|  con_dist_1|           4.5|           5.5|          5.8|
| 10|1/15/2018|0.010680705|east|  con_dist_1|            15|            25|         28.0|
| 10|1/15/2018| 0.01378215|east|  con_dist_1|            15|            25|         28.0|
| 10|1/15/2018|0.006628853|east|  con_dist_1|            15|            25|         28.0|
| 10|1/15/2018|0.006628853|west|  con_dist_2|   0.325261837|   0.655004251| 0.7539269752|
| 10|1/15/2018|0.010680705|west|  con_dist_2|   0.325261837|   0.655004251| 0.7539269752|
| 10|1/15/2018|0.010680705|east|  con_dist_2|   0.350962493|  0.4990442955|  0.749241156|
| 10|1/15/2018| 0.01378215|east|  con_dist_2|   0.350962493|  0.4990442955|  0.749241156|
| 10|1/15/2018|0.006628853|east|  con_dist_2|   0.350962493|  0.4990442955|  0.749241156|
+---+---------+-----------+----+------------+--------------+--------------+-------------+

您可以使用此网址的“定义 2”验证结果 https://www.translatorscafe.com/unit-converter/en-US/calculator/percentile/

【问题讨论】:

你试过percentile而不是percentile_approx吗? 【参考方案1】:

使用 Spark 解决此问题的一种简单方法是手动查找与指定百分位值最接近的两个值。那么小数部分就很容易计算出来了。

在 Scala 中,这可以按如下方式完成:

首先我们得到按zone分组的每一行的排名,然后除以每组的最大排名。

val w = Window.partitionBy($"zone").orderBy($"date")
val df_zone = df.withColumn("zone_rn", row_number().over(w) - 1)
  .withColumn("zone_rn", $"zone_rn" / max($"zone_rn").over(w))

这给出了:

+---+---------+-----------+----------+-----------+----+-------+
|id |date     |revenue    |con_dist_1|con_dist_2 |zone|zone_rn|
+---+---------+-----------+----------+-----------+----+-------+
|10 |1/15/2018|0.006628853|4         |0.16039063 |west|0.0    |
|10 |1/15/2018|0.810680705|6         |0.819875458|west|1.0    |
|10 |1/15/2018|0.010680705|10        |0.619875458|east|0.0    |
|10 |1/15/2018|0.01378215 |20        |0.082049528|east|0.5    |
|10 |1/15/2018|0.702228853|30        |0.916039063|east|1.0    |
+---+---------+-----------+----------+-----------+----+-------+

我们遍历所有列以考虑并在百分位数上执行foldLeft 以添加每个列的下限和上限(lower_valupper_val)。我们同时计算分数,然后通过将分数添加到下限来计算分位数。

最后,由于我们循环遍历列,我们使用reduce(_.union(_)) 将所有内容带回单个数据帧。

val percentiles = Seq(0.25, 0.75, 0.90)     // Which percentiles to calculate
val cols = Seq("con_dist_1", "con_dist_2")  // The columns to use

val df_percentiles = cols.map c => 
    percentiles.foldLeft(df_zone) case(df, p) =>  
      df.withColumn("perctile_col", lit(c))
        .withColumn("zone_lower", max(when($"zone_rn" <= p, $"zone_rn")).over(w))
        .withColumn("zone_upper", min(when($"zone_rn" >= p, $"zone_rn")).over(w))
        .withColumn("lower_val", max(when($"zone_lower" === $"zone_rn", col(c))).over(w))
        .withColumn("upper_val", min(when($"zone_upper" === $"zone_rn", col(c))).over(w))
        .withColumn("fraction", (lit(p) - $"zone_lower") / ($"zone_upper" - $"zone_lower"))
        .withColumn(s"quantile_$p", $"lower_val" + $"fraction" * ($"upper_val" - $"lower_val"))
  
  .drop((cols ++ Seq("zone_rn", "zone_lower", "zone_upper", "lower_val", "upper_val", "fraction")): _*)
.reduce(_.union(_))

结果:

+---+---------+-----------+----+------------+-------------+------------------+------------------+
| id|     date|    revenue|zone|perctile_col|quantile_0.25|     quantile_0.75|      quantile_0.9|
+---+---------+-----------+----+------------+-------------+------------------+------------------+
| 10|1/15/2018|0.006628853|west|  con_dist_1|          4.5|               5.5|               5.8|
| 10|1/15/2018|0.810680705|west|  con_dist_1|          4.5|               5.5|               5.8|
| 10|1/15/2018|0.010680705|east|  con_dist_1|         15.0|              25.0|              28.0|
| 10|1/15/2018| 0.01378215|east|  con_dist_1|         15.0|              25.0|              28.0|
| 10|1/15/2018|0.702228853|east|  con_dist_1|         15.0|              25.0|              28.0|
| 10|1/15/2018|0.006628853|west|  con_dist_2|  0.325261837|0.6550042509999999|      0.7539269752|
| 10|1/15/2018|0.810680705|west|  con_dist_2|  0.325261837|0.6550042509999999|      0.7539269752|
| 10|1/15/2018|0.010680705|east|  con_dist_2|  0.350962493|      0.4990442955|0.7492411560000001|
| 10|1/15/2018| 0.01378215|east|  con_dist_2|  0.350962493|      0.4990442955|0.7492411560000001|
| 10|1/15/2018|0.702228853|east|  con_dist_2|  0.350962493|      0.4990442955|0.7492411560000001|
+---+---------+-----------+----+------------+-------------+------------------+------------------+

【讨论】:

@BdEngineer:太好了。我认为它不起作用,因为您的评论说它不适用于浮动类型。也许你可以在这里添加一个答案,因为它应该比我在这里的方法更简单。 @BdEngineer:.reduce(_.union(_)) 会将多个数据帧合并为一个。我上面所做的是分别处理每一列(con_dist_1 和 con_dist_2)。这会产生两个数据框,然后将它们合并为一个带有.reduce(_.union(_)) 的数据框。 @BdEngineer: df_zone 在第一个代码块中创建。折叠在 percentiles 上完成以添加所有 quantile_$p 列(p 是百分位数之一,例如 0.25)。对于case (df,p),这就是foldLeft 的工作方式。开始 df 是 df_zone 并且在每次迭代中,来自上一次迭代的数据帧被用作 df(p 将更改为下一个)。总而言之,foldLeft 将为每个百分位数的 p 添加一个“quantile_”列。在block里面,你可以随意调用数据库,不需要传递sparkSession,你可以简单的使用。 @BdEngineer:你应该不需要通过sparkSession。您可以在 foldLeft 内使用它(它不是数据框操作,因此是允许的)。如果您想要每个特定百分位数的更多变量,那么您可以将它们压缩到百分位数列表以获取元组列表。例如,percentiles.zip(Seq(1,2,3)).foldLeft(df_zone)case (df, (p,value)) =&gt; ...。但是,如果我正确理解了您的问题,则无需这样做。只需在 foldLeft 中使用您想要的任何变量。 @BdEngineer: 1. df_zone 数据框显示在第二个代码框中。这是输入数据框,因为我们要基于此数据框构造新的分位数列,此外,我们希望保持 id、日期和收入的值不变。 foldLeft 只会向此数据框添加新列。 2. 这是由于列表是元组列表,列表中的第一个元组将是 (p, value)。我们把它写成foldLeft 的语法,即foldLeft[B](z: B)(op: (B, A) ⇒ B): B。如您所见,op 被定义为两个值。 (您可以将其视为语法限制。)

以上是关于如何从 percentile_approx 代码编写自定义函数,该函数在 excel 中给出与 percentile.inc 相同的结果?的主要内容,如果未能解决你的问题,请参考以下文章

Hive:percentile_approx 原理与实现

hive计算分位数

hive 分位数函数 percentile(col, p)

如何根据百分位数过滤表格,然后在 HQL 中随机抽样?

如何将 .NET EXE 反编译为可读的 C# 源代码?

如何将javascript代码编译为c++或java