【问题标题】:Weighted array intersect based on keys, with averages as values加权数组基于键相交,平均值为值
【发布时间】:2018-04-21 12:42:48
【问题描述】:

考虑下面的这个集合:

$collection = Array ( 
    $arr1 = Array ( 1 => 10.0, 2 => 20.0, 3 => 50.0, 4 => 80.0, 5 => 100.0 ),
    $arr2 = Array ( 3 => 20.0, 5 => 20.0, 6 => 100.0, 7 => 10.0 ),
    $arr3 = Array ( 1 => 30.0, 3 => 30.0, 5 => 10.0, 8 => 10.0 )
);

考虑这个基于包含在 $collection 中的数组的交集的理论输出,考虑到它们的数组键具有基于单个值的平均值的各自值:

$output = Array ( 3 => 33.3333, 5 => 43.3333 );

这个问题可以用像 array_intersect_* 这样的原生 PHP 函数以优雅的方式解决吗?

如果没有,你能建议我一个优雅的解决方案,不一定需要外部丑陋的 foreach 吗?

请记住,需要相交的数组数量不是固定的。它可以是 2 个输入数组,因为它可以是 1000 个输入数组。 键始终为整数,值始终为浮点数或整数。

换句话说:

$collection = [
    $arr1 = [ ... ];
    $arr2 = [ ... ];
    $arr3 = [ ... ];
    ...
    $arrn = [ ... ];
];
$output = [ intersected and weighted array based (on comparison) on keys from $arr1 to $arrn, and (on values) from the value averages ];

【问题讨论】:

  • 为什么这需要“未知数量的嵌套foreach”,每个数组都是非常一维的......?
  • 等等...什么?未知数量的数组?这是怎么发生的?
  • 并非如此。 未知数量的数组。但是这可能会发生......数组通常不只是“发生”。
  • 这些数组是从哪里来的呢?您至少可以将 them 放入一个数组开始,以便您可以轻松地循环它们吗? (因为输入数组的数量未知,您可能宁愿回到 foreach 循环,将其输入 array_merge_recursive 可能会更困难。)否则,您将不得不使用可变变量循环处理“编号”数组名字,简直丑陋。
  • 您的示例输出仍然与输入不匹配,键 1 出现两次,因此结果中也应该有 20 个平均值。 (对于仍然要求在现有解决方案下“美化一点”的人,你的要求相当草率...... ;-)

标签: php arrays average array-intersect


【解决方案1】:

对输入数组计数一次。

$n = count($collection);

按键计算所有子数组的交集。

$intersection = array_intersect_key(...$collection);
// PHP5: $intersection = call_user_func_array('array_intersect_key', $input);

通过对来自交叉点的每个键的输入数组中的列进行平均来构建结果。

$output = [];
foreach ($intersection as $key => $value) {
    $output[$key] = array_sum(array_column($collection, $key)) / $n;
}

如果您真的想完全避免使用foreach,可以改用array_map

$output = array_map(function($key) use ($collection, $n) {
    return array_sum(array_column($collection, $key)) / $n;
}, array_keys($intersection));

但在我看来,这只会增加不必要的复杂性。


注意:$intersection 中的值将是第一个子数组中的单个值,但它们并不重要;它们在生成输出时被忽略。如果在 foreach 中有一个无用的 $value 变量让您感到困扰,那么您可以改为使用 foreach (array_keys($intersection) as $key),但我选择避免不必要的函数调用。

【讨论】:

    【解决方案2】:

    我想可以这样:

    <?php
    
    $intersecting_arrays = Array (
        0 => Array ( 'one' => 10, 'two' => 20, 'three' => 50, 'four' => 80, 'five' => 100 ),
        1 => Array ( 'three' => 20, 'five' => 20, 'six' => 100, 'seven' => 10 ),
        2 => Array ( 'one' => 30, 'three' => 30, 'five' => 10, 'eight' => 10 )
        );
    
    $temp = $intersecting_arrays[0];
    for($i = 1; $i < count($intersecting_arrays); $i++) {
        $temp = array_intersect_key($temp, $intersecting_arrays[$i]);
    }
    
    $result = Array();
    foreach(array_keys($temp) as $key => $val) {
        $value = 0;
        foreach($intersecting_arrays as $val1) {
            $value+= $val1[$val];
        }
        $result[$key] = $value / count($intersecting_arrays);
    }
    
    print_r($temp);
    print_r($result);
    

    https://3v4l.org/j8o75

    以这种方式,它不取决于您拥有多少数组。 在这里,您可以获得所有数组中键的交集,然后使用收集的键计算平均值。

    【讨论】:

    • 按预期工作!谢谢!你觉得这样可以美化一点吗?我的意思是,这个函数首先与键相交,然后在第二次运行中计算权重。你认为这可以一次写完吗?
    • @Maurizio 我认为您可以将其覆盖在一个函数中,然后将其调用为输入数组。你需要“一次通行证”制度做什么? (:
    • @Maurizio,无论您以哪种方式查看它,都需要以某种形式或其他方式循环输入数据两次,因为要计算平均值,您首先需要知道多少值您正在计算 的平均值,所以我认为“一次性完成”是相当不可能的。
    【解决方案3】:

    好的,对于未知数量的输入数组,我肯定会先使用两个嵌套的 foreach 循环将它们组合起来 - 将未知数字放入 array_merge_recursive 或类似的会很困难。

    $input = [
      0 => [ 'one' => 10, 'two' => 20, 'three' => 50, 'four' => 80, 'five' => 100],
      1 => [ 'three' => 20, 'five' => 20, 'six' => 100, 'seven' => 10],
      2 => [ 'one' => 30, 'three' => 30, 'five' => 10, 'eight' => 10]
    ];
    
    $combined = [];
    foreach($input as $array) {
      foreach($array as $key => $value) {
        $combined[$key][] = $value;
      }
    }
    
    $averages = array_map(function($item) {
      return array_sum($item)/count($item);
    }, $combined);
    
    var_dump($averages);
    

    https://3v4l.org/hmtj5

    请注意,此解决方案不需要在 array_map 回调中检查数组与单个整数,因为与 array_merge_recursive 不同,循环内的$combined[$key][] 认为即使只有一个值的键也会在数组。


    编辑:

    但请记住,并非所有键都会被考虑在内

    啊,好的,所以您只需要那些多次出现的键的平均值。这可以通过在使用 array_map 之前过滤组合数组来轻松解决:

    $combined = array_filter($combined, function($v, $k) {
      return count($v) != 1;
    }, ARRAY_FILTER_USE_BOTH );
    

    集成到上述解决方案中:https://3v4l.org/dn5ro


    编辑#2

    [Andreas' comment] 我认为“一个”不应该出现在输出中,因为它不在所有三个数组中。

    啊,我明白了......即使从示例中也无法判断这是实际想要的结果 :-) 然后我的过滤必须再次修改一点,并考虑输入数组的数量:

    $combined = array_filter($combined, function($v, $k) use($input) {
      return count($v) == count($input);
    }, ARRAY_FILTER_USE_BOTH );
    

    https://3v4l.org/9H086

    【讨论】:

    • 谢谢您,但请记住,并非所有键都会被考虑在内,因为 all 数组中存在的唯一键是“三个”和“五”。我们还需要根据键做某种“array_intersect”。
    • @Maurizio 啊,好吧,从一开始就不是很清楚。请查看我的编辑 - 很容易修复,如果我们先过滤组合数组,丢弃数组中只有一个值的所有键。 (我会假设对于密钥 one,您也需要平均值,因为这也不止一次发生。)
    • 完美,按预期工作!请参阅下面我向 Roman 提出的问题,它也适用于您的解决方案:您认为可以以更优雅的方式实现相同的目标吗?换句话说,两种解决方案都使用非常相似的操作方式:首先交叉键,然后计算平均值。你怎么看?相交上的 PHP 原生函数非常缺乏,因为它们可以对比较进行回调,但不能对关联进行回调
    • @Maurizio 正如我在 Roman 的回答下所评论的那样,我认为您无法避免有效地循环两次数据,因为要计算平均值,您需要知道有多少个操作数,并且当您仍在“收集”这些值时,您知道这一点。
    • @Maurizio 第二次编辑,关于 Andreas 在 cmets 中提到的内容,您似乎不希望结果中出现 one/1,因为这不会发生在 all 输入数组。过滤功能在这方面需要一点更新。
    【解决方案4】:

    您可以将数组合并为一个并使用 array_sum 和 count() 来获取平均值。

    $arr1 = Array ( 'one' => 10, 'two' => 20, 'three' => 50, 'four' => 80, 'five' => 100 );
    $arr2 = Array ( 'three' => 20, 'five' => 20, 'six' => 100, 'seven' => 10 );
    $arr3 = Array ( 'one' => 30, 'three' => 30, 'five' => 10, 'eight' => 10 );
    $array = array_merge_recursive($arr1,$arr2,$arr3);
    
    $key= "two";
    If(is_array($array[$key])){
        $avg = array_sum($array[$key])/count($array[$key]);
    }Else{
        $avg = $array[$key];
    }
    
    Echo $avg;
    

    https://3v4l.org/pa3PH


    编辑以遵循 $collection 数组。

    那就试试这个吧。使用数组列来获取正确的键,并使用 array_sum 和 count 来获取平均值。

    $collection = array(
        Array ( 'one' => 10, 'two' => 20, 'three' => 50, 'four' => 80, 'five' => 100 ),
        Array ( 'three' => 20, 'five' => 20, 'six' => 100, 'seven' => 10 ),
        Array ( 'one' => 30, 'three' => 30, 'five' => 10, 'eight' => 10 ));
    
    $key= "three";
    $array = array_column($collection, $key);
    
    If(count($array) != 1){
        $avg = array_sum($array)/count($array);
    }Else{
        $avg = $array[0];
    }
    
    Echo $avg;
    

    https://3v4l.org/QPsiS


    最终编辑。

    这里我遍历第一个子数组并使用数组列来查找所有匹配的键。
    如果键的计数与集合的计数相同,则键存在于所有子数组中,应“保存”。

    $collection = array(
        Array ( 'one' => 10, 'two' => 20, 'three' => 50, 'four' => 80, 'five' => 100 ),
        Array ( 'three' => 20, 'five' => 20, 'six' => 100, 'seven' => 10 ),
        Array ( 'one' => 30, 'three' => 30, 'five' => 10, 'eight' => 10 ));
    
    Foreach($collection[0] as $key => $val){
        $array = array_column($collection, $key);
        If(count($array) == count($collection)){
            $avg[$key] = array_sum($array)/count($array);
        }
    }
    Var_dump($avg);
    

    https://3v4l.org/LfktH

    【讨论】:

    • 如果你将它用于two 键,这会抱怨 array_sum 被输入一个整数。我想在这方面我仍然更喜欢我的 foreach 循环而不是 array_merge_recursive,因为这会使two 成为一个包含单个值 20 的数组。
    • 但这也可以很容易地解决,方法是先检查它是否是一个数组,然后返回 sum/count 或值本身。然后这也可以通过 array_map 应用,我们会得到一个数组,其中包含每个键至少出现一次的平均值。
    • @Andreas $arr1, $arr2, $arr3 没有详细定义,而是包含在另一个未知维度的数组中。所以输入数组将从 $collection = [ $arr1, $arr2, $arr3, .... ];
    • @Maurizio 查看我的最后一次编辑。我做了一个循环来获得匹配子数组键的平均值。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-09-30
    • 2022-08-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多