Struggling with reduce(): Functional Programming: Use the reduce Method to Analyze Data

Tell us what’s happening:
I’m having trouble using reduce to filter out only movies directed by Christopher Nolan.

watchList[0]["Director"] is returning returning the director as a string (which is what I want), but this is not behaving the way I intended in the reduce function.

Can someone point me in the right direction?

Your code so far

const reducer = function(x){
  return watchList[x]["Director"]=="Christopher Nolan";
}
var nolan = watchList.reduce(reducer);
return nolan;

Your browser information:

User Agent is: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36.

Link to the challenge:
https://learn.freecodecamp.org/javascript-algorithms-and-data-structures/functional-programming/use-the-reduce-method-to-analyze-data/

Whoops that was supposed to be a console.log() statement.

Thanks for pointing me in the right direction for the two arguments to reduce’s callback. I did some more reading and came up with this solution:

let averageRating = watchList.reduce((acc, val) => {
   return val.Director == 'Christopher Nolan' ? acc + Number(val.imdbRating) : acc;
},0)

// Add your code above this line
console.log(averageRating/4); 

I took a shortcut by manually counting the 4 Nolan movies and printing my result /4. Wasn’t sure how to reference the number of values that met the condition within my function.

Anyways, in my chrome console I’m getting the output 8.675 but I’m not passing the test. Any idea why?

Thanks for your help!

hey bro I just finished the code. It can be solved in a single line of code:

// Add your code below this line

var averageRating = watchList.filter(x => x.Director === “Christopher Nolan”).map(x => Number(x.imdbRating)).reduce((x1, x2) => x1 + x2) / watchList.filter(x => x.Director === “Christopher Nolan”).length;

// Add your code above this line
What you see here is just usage of the filter(), map() and reduce() methods. Hope you understand it!

  1. First we filter the watchList array by director. This is the first part watchList.filter(x => x.Director === “Christopher Nolan”): this will return an array that only contains objects (x) that fit the condition where x.Director is equal to “Christopher Nolan”. The array we’re working with now is of length 4 and only contains objects with “Director” : “Christopher Nolan”

  2. We now map the objects in the array to single values. Instead of having an array containing 4 objects with a lot of information we don’t need, we map it to single values (numbers, not strings) representing the imdbRating number. We use .map(x => Number(x.imdbRating)). Just read this as: "take each element (object) and return only the “imdbRating” value of that element BUT as a number and not a string.

  3. Great! Now we have an array that looks like this: [8.8, 8.6, 9.0, 8.3]
    Just use .reduce((x1, x2) => x1 + x2) to add them all up! What we see in the parentheses is just the notation for using reduce().

  4. Now we need to get the average. We need to divide the number returned by .reduce() (a single number) by the length of the filtered array (after filtering the Christopher Nolan movies). We divide by watchList.filter(x => x.Director === “Christopher Nolan”).length;

And that’s it! solved in one line of code. Hope it helps

6 Likes

If he did, would that be “better”?

I’ve just finished the JS algorithms/data structures cert, and have been thinking about code quality as I try to evaluate my progress.

This was my solution to this challenge:

/*
What's the average IMDB rating for all of the Christopher Nolan Movies in the watchlist?
*/

// Find subset of movies directed by Christopher Nolan
let cNolanMovies = watchList.filter( item => item.Director == "Christopher Nolan" );

// Get the ratings for those movies
let movieRatings = cNolanMovies.map( item => Number(item.imdbRating) );

// calculate average
let averageRating = movieRatings.reduce( (total, sum) =>  total + sum )/movieRatings.length;

I always tend to comment my code like this, and use these almost as pesudocode to help me as I’m coming up with the final solution. So as a result, my code tends to be longer overall and split into small chunks, but very clear and easy to comprehend.

A solution that was based on a single use of reduce() would be more efficient, but it would certainly also be more complex.

How do you make the decision on which side of this tradeoff to favor? Which strategy is more appropriate in a “professional” context?

3 Likes

Thanks a lot. It helped to filter for only movies directed by Nolan.

Thank you homie! This made a lot of sense. After some reading I filtered for only Nolan movies, and tried to use reduce on this new object by referencing object.imdbRating, but it’s way more comprehensible (at my skill level) to use map to create an array of ratings.

Thanks for responding, and especially for commenting out the logic behind each step. This was super helpful and I really appreciate it!!

that was clearest explanation for me !! thanks

// Add your code below this line
var i = watchList.filter(x => x.Director === “Christopher Nolan”);
var j = i.map(x => Number(x.imdbRating));
var k = j.reduce((x1,x2)=>x1+x2);

var averageRating = k / i.length;
// Add your code above this line

console.log(averageRating);

1 Like

var averageRating=watchList.filter(movies=>movies.Director==="Christopher Nolan").map(movies=>Number(movies.imdbRating)).reduce((sum, current)=>(sum+current),0)/4;

How can I remove the 4 above and reference the length of array of numbers coming into reduce in the code above?
I know I can initialize another variable but I’m wondering if it’s possible to do it in this single line, not introducing too much difficulty of understanding the code later?

If you look at what the callback for reduce can be, it can have up to four arguments: the third is the current index, the fourth is the array you are iterating over.

Then you can either

  1. divide by the array length as you go,
  2. or you can check if you’re on the last index and divide by the length then.

However a. this will make the code complex and hard to read, b. if you divide as you go you’re gonna get the wrong result without some shenanigans to round numbers, which will complicate the code further (this is to do with floating point numbers).

Ideally, functions should do one thing and one thing well. As soon as you start to give them each ancillary jobs to do, it gets increasingly hard to debug the code. There is a reasonable reason for doing it in this case, but the increased difficulty of understanding your code outweighs any benefit imo.

(Btw you can do the entire operation in reduce, map and filter are just specialised versions of reduce anyway, and it’s fairly trivial to just move the logic from their callbacks into the reduce)

1 Like

Thanks for the code commenting and step by step. Really helped!

It’s a bit tricky because we need to capture the Cumulative moving average for each iteration if we want to avoid making extra variables.

let averageRating = watchList.filter( (movies) => {
	return movies.Director === "Christopher Nolan";   // array of 'Nolan' movie objects.
}).reduce( (acc, currVal, currIndex, arr) => {
	return acc + (+currVal.imdbRating - acc) / ( currIndex + 1);  // capture cumulative moving average
}, 0).toFixed(3);

console.log(averageRating);

Hey Guys I tried on this quiz and I came up with this but always returns “NaN”
This is my code::

var filterMovies = watchList.filter(function(e) {
  if (e.Director === "Christopher Nolan") {
    return true;
  }
});
var mappingItems = filterMovies.map(function(x) {
  if (Number(x["imdbRating"]) === true) {
    return true;
  }
});
var averageRating = mappingItems.reduce(function(total, sum) {
  return((total + sum) / mappingItems.length);
})

console.log(averageRating); 

can any body help me?

Here’s the solution for using .reduce() only to get the sum:

// Store name in CN for quick coding and testing
const CN = 'Christopher Nolan';
const averageRating = watchList.reduce((a, c) => {
  if (typeof a === 'object' && a.Director === CN && c.Director === CN) {
      return parseFloat(a.imdbRating) + parseFloat(c.imdbRating);
  }
  // Even if c.Director is not CN, we still need to return a for the next iteration evaluation
  return c.Director === CN ? a + parseFloat(c.imdbRating) : a;
})

This will only return the sum of all ratings, not the average. From this point, you can divide it by 4 if you manually count the movies by Christopher Nolan. Otherwise, you can use .filter() to get the length:

const length = watchList.filter(movie => movie.Director === 'Christopher Nolan').length;

However, there’s a better way to avoid another loop. That is to return an array with the first element as the sum and the second one as the counter.

// Store name in CN for quick coding and testing
const CN = 'Christopher Nolan';

const sumAndCountArr = watchList.reduce((a, c) => {
  // First if condition only evaluates to true the first time because 'a' 
  // will equal to an array in the second iteration

  if (typeof a === 'object' && a.Director === CN && c.Director === CN) {
    // The second element of the array is 2 
    // because a.Director and c.Director are both CN
    // which means there are 2 movies by CN already at this point
    return [parseFloat(a.imdbRating) + parseFloat(c.imdbRating), 2];
  } else if (Array.isArray(a) && c.Director === CN) {
    // From the second iteration on, 'a' will be an array
    // And the second if condition will be true from this point
    // We need to check it c.Director is CN or not
    // If so we mutate the array 'a' to update new rating sum
    // and count of movies by CN

    // You can clone array a to avoid mutation
    // but I don't think it's necessary
    a = [a[0] + parseFloat(c.imdbRating), a[1] + 1];
  }

  // Finally we returned the mutated array 'a'
  // What you need to know is that though the second condition is false
  // in some cases and 'a' is not mutated, 'a' still needs to be returned
  // So in the next iteration we have 'a' to keep evaluate the if conditions
  // Try put a in the else if and you'll see what I mean
  // I've made many mistakes at this point, so warn you is necessary.
  return a;
});

const averageRating = sumAndCountArr[0] / sumAndCountArr[1];

Great explanation! I am still confused about the x1 and x2. in step 3.