a is (2,3,4). To concatenate on the last axis, b has to be (2,3,1) (or more generally (2,3,n)). b.reshape(2,3) gets you part way there, add a np.newaxis to get the rest of the way. Or include the 3rd axis in the reshape: b.reshape(2,3,1).
In [21]: np.concatenate((a, b.reshape(2,3)[:,:,None]),-1)
Out[21]:
array([[[1., 1., 1., 1., 9.],
[1., 1., 1., 1., 9.],
[1., 1., 1., 1., 9.]],
[[1., 1., 1., 1., 9.],
[1., 1., 1., 1., 9.],
[1., 1., 1., 1., 9.]]])
np.c_ works with the same reshaping, np.c_[a, b.reshape(2,3,1)].
np.c_ (or np.r_) takes a string parameter that tells it how to expand dimensions if needed. np.c_ is the equivalent of np.r_['-1,2,0',a, b.reshape(2,3)[:,:,None]]. That string parameter is a bit hard to follow, but playing around I found this works:
In [27]: np.c_['-1,3,0',a, b.reshape(2,3)]
Out[27]:
array([[[1., 1., 1., 1., 9.],
[1., 1., 1., 1., 9.],
[1., 1., 1., 1., 9.]],
[[1., 1., 1., 1., 9.],
[1., 1., 1., 1., 9.],
[1., 1., 1., 1., 9.]]])
Keep in mind that np.c_ uses np.concatenate (as do all the 'stack' functions), so In [21] is the most direct version. Still some people like the convenience of the np.c_ format.
(np.column_stack joins on axis 1; the docs explicitly talk of returning a 2d array. You have 3d case.)
dstack ('depth' stack) works: np.dstack((a, b.reshape(2,3))). It creates the 3d arrays with:
In [49]: np.atleast_3d(b.reshape(2,3)).shape
Out[49]: (2, 3, 1)
which in this case is the same as what the '-1,3,0' string does.
np.concatenate([a, b.reshape(*a.shape[:-1], -1)], -1)