trim np arrays according to a list of starting points

Viewed 31

I have a table, represented by an np.array like the following:

A = [[12,412,42,54],
     [144,2,42,4],
     [2,43,22,10]]

And a list that contains the desired starting point of each row in A:

L=[0,2,1]

The desired output would be:

B = [[12,412,42,54],
     [42,4,np.nan,np.nan],
     [43,22,10,np.nan]]

Edit
I prefer to avoid using a for-loop for obvious reasons.

2 Answers

Try compare the L with column index, then use boolean set/get items:

# convert A to numpy array for advanced indexing
A = np.array(A)
ll = A.shape[1]


keep = np.arange(ll) >= np.array(L)[:,None]

out = np.full(A.shape, np.nan)
out[keep[:,::-1]] = A[keep]
print(out)

Output:

[[ 12. 412.  42.  54.]
 [ 42.   4.  nan  nan]
 [ 43.  22.  10.  nan]]

My guess would be that a vectorized approach for this would be less efficient than explicit looping, because the result is fundamentally a jagged array, which NumPy does not support well.

However, a loop-based solution is simple, that can be made faster with Numba's nb.njit(), if needed.:

import numpy as np
import numba as nb


@nb.njit
def jag_nb(arr, starts, empty=np.nan):
    result = np.full(arr.shape, empty)
    for i, x in enumerate(starts):
        if x != 0:
            result[i, :-x] = arr[i, x:]
        else:
            result[i, :] = arr[i, :]
    return result


A = np.array([[12,412,42,54], [144,2,42,4], [2,43,22,10]])
L = np.array([0,2,1])
jag(A, L)
# array([[ 12., 412.,  42.,  54.],
#        [ 42.,   4.,  nan,  nan],
#        [ 43.,  22.,  10.,  nan]])

Compared to the pure NumPy vectorized approach proposed in @QuangHoang's answer:

def jag_np(arr, starts, empty=np.nan):
    m, _ = arr.shape
    keep = np.arange(m) >= starts[:, None]
    result = np.full(arr.shape, np.nan)
    result[keep[:, ::-1]] = arr[keep]
    return result

The Numba based approach is noticeably faster, as shown with the following benchmarks:

import pandas as pd
import matplotlib.pyplot as plt


def benchmark(
    funcs,
    ii=range(4, 10, 1),
    is_equal=lambda x, y: np.allclose(x, y, equal_nan=True),
    seed=0,
    unit="ms",
    verbose=True,
    use_str=True
):
    labels = [func.__name__ for func in funcs]
    units = {"s": 0, "ms": 3, "µs": 6, "ns": 9}
    assert unit in units
    np.random.seed(seed)

    timings = {}
    for i in ii:
        m = n = 2 ** i
        if verbose:
            print(f"i={i}, n={n}")

        arr = np.random.random((m, n))
        starts = np.random.randint(0, n, m)
        base = funcs[0](arr, starts)
        timings[n] = []
        for func in funcs:
            res = func(arr, starts)
            is_good = is_equal(base, res)
            timed = %timeit -n 64 -r 8 -q -o func(arr, starts)
            timing = timed.best
            timings[n].append(timing if is_good else None)
            if verbose:
                print(
                    f"{func.__name__:>24}"
                    f"  {is_good!s:5}"
                    f"  {timing * (10 ** units[unit]):10.3f} {unit}"
                    f"  {timings[n][0] / timing:5.1f}x")
    return timings, labels


def plot(timings, labels, title=None, xlabel="Input Size / #", unit="ms"):
    n_rows = 1
    n_cols = 3
    fig, axs = plt.subplots(n_rows, n_cols, figsize=(8 * n_cols, 6 * n_rows), squeeze=False)
    units = {"s": 0, "ms": 3, "µs": 6, "ns": 9}
    df = pd.DataFrame(data=timings, index=labels).transpose()
    
    base = df[[labels[0]]].to_numpy()
    (df * 10 ** units[unit]).plot(marker="o", xlabel=xlabel, ylabel=f"Best timing / {unit}", ax=axs[0, 0])
    (df / base * 100).plot(marker='o', xlabel=xlabel, ylabel='Relative speed / %', logx=True, ax=axs[0, 1])
    (base / df).plot(marker='o', xlabel=xlabel, ylabel='Speed Gain / x', ax=axs[0, 2])

    if title:
        fig.suptitle(title)
    fig.patch.set_facecolor('white')


funcs = jag_np, jag_nb
timings, labels = benchmark(funcs, ii=range(4, 11))
plot(timings, labels, unit="ms")

bm

Related