PERCENTILE IF 对一组条件使用 ARRAYFORMULA

PERCENTILE IF using ARRAYFORMULA for a set of conditions

我需要使用 if 条件来计算百分位数,以按条件组计算它,但 Google 表格不提供 PERCENTILEIF 函数。非数组解决方案是可能的:

=ARRAYFORMULA(PERCENTILE(if(range=value,values),percentile))

但在我的例子中 value 应该是一个可能值的数组。

以下是突出显示预期结果的示例数据:

我尝试了几个选项来使用可能值的数组,但在所有情况下,我都得到了错误的结果:

G2中使用JOIN

=arrayformula(if(len(E2:E3),percentile(split(regexreplace(join(",",
   Arrayformula(A2:A12 & "_" & B2:B12)),E2:E3  & "_(\d+)|.",","),","),D2),))

H2中使用MATCH

=ARRAYFORMULA(if(len(E2:E3),
   PERCENTILE(IFNA(--(match(A2:A12,E2:E3,0) > 0) * B2:B12,),D2),))

这是电子表格文件: https://docs.google.com/spreadsheets/d/1VDJIYvmOC46DI_9u4zSEfmxSan5R5VKK772C_kP5rxA/edit?usp=sharing

您可以按如下方式获取每个值的百分位数

=sort(arrayformula(iferror(
{A2:A,B2:B,
(VLOOKUP(row(A2:A),{sort({row(A2:A),A2:B},2,1,3,1),row(A2:A)},4,false)-MATCH(A2:A,QUERY({sort({A2:B},1,1,2,0)},"select Col1"),0))/countif(A2:A,A2:A)}
)),1,1,3,1)

然后应用(或不应用)插值,您可以像 Tom Sharpe 那样通过线性公式或根据统计分布进行插值 (https://statisticsbyjim.com/basics/percentiles/)

请注意,idx 3 的百分位数 80% 显然是 20,因为只有 5 个值! excel 因为 google 工作表在这方面犯了错误

作为一个练习,我尝试从基于 the quantiles formula 的第一性原理来解决这个问题。 Excel 或 Google Sheets Percentile 和 Percentile.inc 函数使用参考文献中 Excel 下最后一个 table 中显示的 (N − 1)p + 1 变体以上。

所以对于第一组,

(N − 1)p + 1 = 3 * 0.8 + 1 = 3.4

这意味着您从第三点 (10) 到第四点 (30) 插入 0.4,从而得到

10 + 0.4 * (30 - 10) = 18.

数组公式为

=ArrayFormula(vlookup(vlookup(E2:E3,{sort(A2:B,1,1,2,1),sequence(ROWS(A2:A))},3,false)+floor((countif(A2:A,E2:E3)-1)*D2),{sequence(ROWS(A2:A)),sort(A2:B,1,1,2,1)},3,false)
+(vlookup(vlookup(E2:E3,{sort(A2:B,1,1,2,1),sequence(ROWS(A2:A))},3,false)+ceiling((countif(A2:A,E2:E3)-1)*D2),{sequence(ROWS(A2:A)),sort(A2:B,1,1,2,1)},3,false)
-vlookup(vlookup(E2:E3,{sort(A2:B,1,1,2,1),sequence(ROWS(A2:A))},3,false)+floor((countif(A2:A,E2:E3)-1)*D2),{sequence(ROWS(A2:A)),sort(A2:B,1,1,2,1)},3,false))*mod((countif(A2:A,E2:E3)-1)*D2,1))


我相信您也可以通过操纵 Percentile 函数的第二个参数的值来做到这一点 - 它会像这样:

=ArrayFormula(percentile(if(A2:A="",,B2:B+A2:A*1000),
D2*(countif(A2:A,E2:E3)-1)/(count(A2:A)-1)+(countif(A2:A,"<"&E2:E3))/(count(A2:A)-1))-E2:E3*1000)

说明

我觉得我最能用图表来展示逻辑:

所以我添加了一个常量(50 以便在图表上更容易看到第二组,100 到第三组)来分隔三组。我还在每个组内进行了排序,以便于可视化,但这在公式中不是必需的,因为百分位会进行排序。

如果您查看第三组,您可以通过选择转到整个数据的第 60 个百分位数来准确地落在该组的开头。然后,您可以通过将所需的百分位数乘以该组中第一个点和最后一个点之间的距离作为整个数据中第一个点和最后一个点之间的距离的一部分来转到这最后五个点的第 80 个百分位数。

在上面的公式中选择 1000 并没有什么神奇之处,只是一个足够大的数字来分隔组 - 如果它们都是正数,max(B2:B) 将是最安全的。

添加应用程序脚本选项,以防其他社区成员对此解决方案感兴趣。我认为 @TomSharpe 是最好的方法,但在某些情况下,它可能更适合使用自定义函数 percentileIf 的短公式,而不是大公式。它作为脚本包含在问题中提供的示例文件中,并且包含单元测试。

/**
 * Google Spreadsheet doesn´t offer percentileIf function. Here javascript solution, that works using Arrayformula
 * 
 * @param range {Array} Array of values to test the criterium. If the input is Spreadsheet range it will be a 2D-Array
 * @param criterium {Array} The criterium to match each element of range. It can be a single value
 *  If the input is Spreadsheet range it will be a 2D-Array
 * @values {Array} The set of value to calculate the percentile based on criterium
 * @param percentileValue {Number} The percentile to be calculated. It whould be a number in the range of [0,1], it accepts 0 and 1 as 
 *  a possible value
 * @return {Array} The percentile for each element of range that matches the criterium, if criterium ia single value, then it returns a single value
 * 
 */
function percentileIf(range, criterium, values, percentileValue) {

  /* Standardize comparision process for considering Numbers, Dates (excluding timestamp) and String, if String has a date representation it tries to 
  parse it to a number*/
  function cmp(a,b) {
      let result = false, aa,bb;
      if((typeof a) === (typeof b)) {
        if (("string" === typeof a) && ("string" === typeof b)) {// Trying to identify a possible date in string format
          aa = Date.parse(a);
          bb = Date.parse(b);
          if (aa && bb){ // Trying to identify a date
            a = aa;
            b = bb;
          }
        }
        if((a instanceof Date) && (b instanceof Date)) {// Comparing only dates, not considering timestamp
          a.setHours(0, 0, 0, 0);
          b.setHours(0, 0, 0, 0);
          result = (a - b) == 0;
        } else {
          result = a === b;
        }
      }
    return result;
  }

  function arraySortNumbers(inputarray) {
    return inputarray.sort(function (a, b) {
      return a - b;
    });
  }
  
  // Idea taken from here: 
  function percentileCalc(data, q) {
    data = arraySortNumbers(data);
    var pos = ((data.length) - 1) * q;
    var base = Math.floor(pos);
    var rest = pos - base;
    if ((data[base + 1] !== undefined)) {
      return data[base] + rest * (data[base + 1] - data[base]);
    } else {
      return data[base];
    }
  }

  let result = null;
  let validValues = [];
  // Checking preconditions
  if (!Array.isArray(range)) throw new Error("range input argument should be an array");
  if (!Array.isArray(values)) throw new Error("values input argument should be an array");
  if(percentileValue < 0 || percentileValue > 1) throw new Error("The percentile value should be a number between 0-1");

  if (Array.isArray(criterium)) {// Recursive invocation in case of more than one criterium
    result = [];
    criterium = criterium.filter(function(e){ return e !="" }); // removing empty elements (to optimize the function)
    criterium.forEach(item => {
      result.push(percentileIf(range, item, values, percentileValue));
    });
  } else {
    let array = range, numbers = values;
    if(Array.isArray(range[0])) array = range.map(x => x[0]); // Converting to a colum-array
    if(Array.isArray(values[0])) numbers = values.map(x => x[0]);
    array = array.filter(function(e){ return e !="" }); // removing empty elements (to optimize the function)
    numbers = numbers.filter(function(e){ return e !="" }); // removing empty elements (to optimize the function)
    if(array.length != numbers.length) throw new Error("range and values input arguments should have the same size");
    for (let i = 0; i < array.length; i++) {
      if(cmp(criterium, array[i])) {
        validValues.push(numbers[i]);
      } 
    }
    result = percentileCalc(validValues, percentileValue);
  }
  return result;
}

这里是如何使用在电子表格中创建的函数: