We can leverage broadcasting to create the mask with a ranged array comparison against ar3 for assigning into those places and then assign NaNs. Since, the input is an int array, we need to make a float copy of ar2 and then assign, like so -
out = ar2.astype(float, copy=True) # convert to float as NaNs are to be assigned
mask = ar3[:,None] <= np.arange(ar2.shape[1])
out[mask] = np.nan
For a case with a large number of rows and a decent number of cols, this should be a good method, otherwise slice into each row and assign NaNs limited by the corresponding ar3 values.
Bit more explanation on the mask creation -
In [38]: ar3
Out[38]: array([1, 2, 3, 1, 2])
In [39]: ar3[:,None] <= np.arange(ar2.shape[1])
Out[39]:
array([[False, True, True, True, True],
[False, False, True, True, True],
[False, False, False, True, True],
[False, True, True, True, True],
[False, False, True, True, True]])
Comparing each element of ar3 with the range(5) with that outer comparison gives us each row of the mask. If we look closely it's all False until that corresponding index value (ar3 value) and True thereafter. We need those True places for assigning NaNs and hence, this mask directly helps us in assgning NaNs in the entire output array.