Maurizio
Maurizio

Reputation: 737

Average each associative pair found in a 2d array

Consider this collection below:

$collection = [
    [1 => 10.0, 2 => 20.0, 3 => 50.0, 4 => 80.0, 5 => 100.0],
    [3 => 20.0, 5 => 20.0, 6 => 100.0, 7 => 10.0],
    [1 => 30.0, 3 => 30.0, 5 => 10.0, 8 => 10.0]
];

Consider this theorical output based on the intersection of the Arrays contained into $collection, considering their array keys with respective values based on the average of the single values:

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

Can this problem be resolved with a native PHP function like array_intersect_* in an elegant way?

If not, can you suggest me an elegant solution that doesn't necessarily need an outer ugly foreach?

Keep in mind that the number of arrays that need to be intersected is not fixed. It can be 2 input arrays as it can be 1000 input arrays. Keys will be integers at all times, and Values will be floats or integers at all times.

In other words:

$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 ];

Upvotes: 0

Views: 165

Answers (5)

mickmackusa
mickmackusa

Reputation: 48031

Can this problem be resolved with a native PHP function like array_intersect_* in an elegant way?

Well, elegance is in the eye of the developer. If functional-style programming with no new globally-scoped variables equals elegance, then I have something tasty for you. Can a native array_intersect_*() call be leveraged in this task? You bet!

There's a big lack in PHP native functions on intersects - @Maurizio

I disagree. PHP has a broad suite of powerful, optimized, native array_intersect*() and array_diff*() functions. I believe that too few developers are well-acquainted with them all. I've even build a comprehensive demonstration of the different array_diff*() functions (which can be easily inverted to array_intersect*() for educational purposes).


Now, onto your task. First, the code, then the explanation.

Code: (Demo)

var_export(
    array_reduce(
        array_keys(
            array_intersect_ukey(
                ...array_merge($collection, [fn($a, $b) => $a <=> $b])
            )
        ),
        fn($result, $k) => $result + [$k => array_sum(array_column($collection, $k)) / count($collection)],
        []
    )
);
  1. The first subtask is to isolate the keys which are present in every row. array_intersect_ukey() is very likely the best qualified tool. The easy part is the custom function -- just write the two parameters with the spaceship in between. The hard part is setting up the variable number of leading input parameters followed by the closure. For this, temporarily merge the closure as an array element onto the collection variable, then spread the parameters into the the native function.
  2. The payload produced by #1 is an array consisting of the associative elements from the first row where the keys were represented in all rows ([3 => 50.0, 5 => 100.0]). To prepare the data for the next step, the keys must be converted to values -- array_keys() is ideal because the float value are of no further use.
  3. Although there is an equal number of elements going into and returning in the final "averaging step", the final result must be a flat associative array -- so array_map() will not suffice. Instead, array_reduce() is better suited. With the collection variable accessible thanks to PHP7.4's arrow function syntax, array_column() can isolate the full column of data then the averaging result pushed as an associative element into the result array.

Upvotes: 1

Don&#39;t Panic
Don&#39;t Panic

Reputation: 41820

Count the input array once.

$n = count($collection);

Compute the intersection of all the sub-arrays by key.

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

Build your result by averaging the column from the input array for each key from the intersection.

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

If you really want to completely avoid foreach you can use array_map instead.

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

But in my opinion, this just adds unnecessary complexity.


Note: The values in $intersection will be single values from the first sub-array, but they don't really matter; they're disregarded when generating the output. If it bothers you to have a useless $value variable in the foreach, then you can do foreach (array_keys($intersection) as $key) instead, but I opted for avoiding an unnecessary function call.

Upvotes: 2

Andreas
Andreas

Reputation: 23968

You can merge the arrays to one and use array_sum and count() to get the average.

$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


Edit to follow $collection array.

Try this then. Use array column to grab the correct key and use array_sum and count to get the average.

$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


Final edit.

Here I loop through the first subarray and use array column to find all the matching keys.
If the count of keys is the same as the count of collection the key exsists in all subarrays and should be "saved".

$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

Upvotes: 1

C3roe
C3roe

Reputation: 96417

Ok, with an unknown number of input arrays, I would definitively go with two nested foreach loops to combine them first - getting an unknown number into array_merge_recursive or similar is going to be difficult.

$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

Note that this solution doesn't need to check for array vs single integer in the array_map callback, because unlike array_merge_recursive, $combined[$key][] inside the loops sees to it that even the keys with just one value will have that value in an array.


EDIT:

but keep in mind that not all the keys are going to be taken into account

Ah, ok, so you want averages only for those keys that occurred more than once. That can easily be fixed by filtering the combined array before using array_map on it:

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

Integrated into above solution: https://3v4l.org/dn5ro


EDIT #2

[Andreas' comment] I think "one" should not be in output since it is not in all three arrays.

Ah, I see ... couldn't tell that was the actually desired result even from the example :-) Then my filtering has to be modified a little bit again, and take the number of input arrays into account:

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

https://3v4l.org/9H086

Upvotes: 1

Roman
Roman

Reputation: 577

I guess it could be done like this:

<?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

In this manner it doesn't depend on how much arrays you have. Here you get the intersection of keys in all arrays and then count an average using collected keys.

Upvotes: 1

Related Questions