I believe the reason for the NPE is that there is a starting vertex where both().outE() does not exist and count() is applied to it.
Another thing is that you can reference back to variables prior to a reducing barrier. e.g. fold().as('a').unfold().fold().as('b').select('a')
You have to account for cases where there are no results.
One way to do that is filter for where the full traversal exists:
g.V().where(__.both().outE()).groupCount().by(.....
Another option is to use count() which will always give a result:
gremlin> g.V().group().by(project('n','e').by(both().count()).by(both().outE().count()).math('n-e'))
==>[1.0:[v[1],v[6]],-2.0:[v[2]],0.0:[v[4]],-1.0:[v[5]],-3.0:[v[3]]]
There are probably other ways too.