tril_indices() may be the obvious approach here, which generates the lower triangular indices, and then you can use them to set in the input array up to NaNs .
Now, if you care about performance, you can use boolean indexing after creating a mask of such a lower triangular shape, and then set these to NaNs . The implementation will look like this:
m[np.arange(m.shape[0])[:,None] > np.arange(m.shape[1])] = np.nan
So np.arange(m.shape[0])[:,None] > np.arange(m.shape[1]) is a mask created using broadcasting .
Run Example -
In [51]: m Out[51]: array([[ 11., 49., 23., 30.], [ 40., 41., 19., 26.], [ 32., 36., 30., 25.], [ 15., 27., 25., 40.], [ 33., 18., 45., 43.]]) In [52]: np.arange(m.shape[0])[:,None] > np.arange(m.shape[1]) # mask Out[52]: array([[False, False, False, False], [ True, False, False, False], [ True, True, False, False], [ True, True, True, False], [ True, True, True, True]], dtype=bool) In [53]: m[np.arange(m.shape[0])[:,None] > np.arange(m.shape[1])] = np.nan In [54]: m Out[54]: array([[ 11., 49., 23., 30.], [ nan, 41., 19., 26.], [ nan, nan, 30., 25.], [ nan, nan, nan, 40.], [ nan, nan, nan, nan]])
Runtime Tests -
This section compares the boolean indexing approach specified in this solution with np.tril_indices , based on the value of the other solution for performance.
In [38]: m = np.random.randint(10,50,(1000,1100)).astype(float) In [39]: %timeit m[np.tril_indices(m.shape[0], -1)] = np.nan 10 loops, best of 3: 62.8 ms per loop In [40]: m = np.random.randint(10,50,(1000,1100)).astype(float) In [41]: %timeit m[np.arange(m.shape[0])[:,None] > np.arange(m.shape[1])] = np.nan 100 loops, best of 3: 8.03 ms per loop