I'm pretty sure your problem is that you're grouping by products.department_id instead of departments.department_id. If a department has no products, then the left join will produce (before grouping) a single result row for that department with all the products.* columns set to NULL. If you group by one of these columns — even the department ID — then the grouped result will merge all departments with no products together, since products.department_id will be NULL for all of them.
Here's a simple example to demonstrate this:
CREATE TEMPORARY TABLE foo (
foo_id INTEGER PRIMARY KEY
);
CREATE TEMPORARY TABLE bar (
bar_id INTEGER PRIMARY KEY,
foo_id INTEGER NULL
);
INSERT INTO foo (foo_id) VALUES (10), (20), (30), (40), (50);
INSERT INTO bar (bar_id, foo_id) VALUES
(1, 10), (2, 10), (3, 10),
(4, 20), (5, 20), (6, 30);
SELECT foo.foo_id, COUNT(bar.bar_id)
FROM foo
LEFT JOIN bar USING(foo_id)
GROUP BY bar.foo_id
If you run this query, your results should look something like this:
foo.foo_id COUNT(bar.bar_id)
40 0
10 3
20 2
30 1
Uh, wait... what happened to foo_id 50? Well, what happened is that, before the grouping, the left join produced a result set like this:
foo.foo_id bar.foo_id bar.bar_id
10 10 1
10 10 2
10 10 3
20 20 4
20 20 5
30 30 6
40 NULL NULL
50 NULL NULL
Notice how there are two rows where bar.foo_id is NULL. If you then group the results on that column, those rows will get grouped together, even though they do have a different value for foo.foo_id.
While a stricter RDBMS might then complain that you're selecting the non-grouped column foo.foo_id without wrapping it in an aggregate function like COUNT() or SUM(), MySQL isn't that strict by default and will just pick an arbitrary value for that column from each group. In the example above, I got 40, but it could just as well have been 50.
Anyway, the fix is simple: just change the grouping column to departments.department_id and you'll get the results you expect. Or, to keep using my example schema, I can change the grouping column in the query above from bar.foo_id to foo.foo_id and get the following results:
foo.foo_id COUNT(bar.bar_id)
10 3
20 2
30 1
40 0
50 0