The not unary operator always returns a bool value. It needs to, as it needs to return the boolean inverse of whatever value it was passed, which is always a new value (not its argument).
The real odd part of your chart is actually the third column, which shows that the and operator does return numbers if it's passed them on both sides. You might expect and to be a boolean operator just like not, but it's slightly different. The reason is that and always returns one of its arguments. If the first argument is falsey, that value is what is returned. If the first argument is truthy, the second argument is returned.
You can see this if you test with values other than just 0 and 1:
print(3 and 2) # prints 2
print([] and [1,2]) # prints []
The or operator also does this (in a slightly different manner), but that's covered up in your calculation of b by the not calls.
This behavior of and and or is called short-circuiting. It's big advantage is that it lets Python avoid interpreting expressions it doesn't need to get the value of the boolean expression. For instance, this expression will not call the slow_function:
result = falsey_value and slow_function()
It also lets you guard expressions that would cause exceptions if they were evaluated:
if node is not None and node.value > x:
...
If the node variable is None in that code, evaluating node.value would give an AttributeError (since None doesn't have a value attribute). But because and short circuits, the first expression prevents the second expression from being evaluated when it's invalid.