Following the same observation as @Jokilos, it is possible to solve this problem with indexing.
Assuming A = np.array([1, 10, 100, 1000]), the values to be added follow this pattern:
array([[[ 1, 1, 1, 1, 0, 0, 0, 0, 0, 0],
[ 0, 0, 0, 0, 1, 1, 1, 1, 0, 0],
[ 0, 0, 0, 0, 1, 1, 1, 1, 0, 0],
[ 0, 0, 0, 0, 1, 1, 1, 1, 0, 0]],
[[ 0, 10, 10, 10, 10, 0, 0, 0, 0, 0],
[ 0, 10, 10, 10, 10, 0, 0, 0, 0, 0],
[ 0, 0, 0, 0, 0, 10, 10, 10, 10, 0],
[ 0, 0, 0, 0, 0, 10, 10, 10, 10, 0]],
[[ 0, 0, 100, 100, 100, 100, 0, 0, 0, 0],
[ 0, 0, 100, 100, 100, 100, 0, 0, 0, 0],
[ 0, 0, 100, 100, 100, 100, 0, 0, 0, 0],
[ 0, 0, 0, 0, 0, 0, 100, 100, 100, 100]],
[[ 0, 0, 0, 1000, 1000, 1000, 1000, 0, 0, 0],
[ 0, 0, 0, 1000, 1000, 1000, 1000, 0, 0, 0],
[ 0, 0, 0, 1000, 1000, 1000, 1000, 0, 0, 0],
[ 0, 0, 0, 1000, 1000, 1000, 1000, 0, 0, 0]]])
Therefore, carefully designing an indexer should make it possible to assign the correct values directly in the output.
On can consider different points:
- the output array has a (N, 3*N-2) shape
- the first value appears once in the first row before being shifted, the second twice, the third three times, etc.
- the shift is equal to N
- in addition, each value is shifted by its own position (0 for the first row, 1 for the second, etc.)
One can express this as:
N = len(A) # 4
X = np.arange(N) # array([0, 1, 2, 3])
mask = (X[:, None] < X)
# array([[False, True, True, True],
# [False, False, True, True],
# [False, False, False, True],
# [False, False, False, False]])
# compute indexer
Y = (X[:, None] + mask*N)[..., None] + X[None, None]
# array([[[0, 1, 2, 3],
# [4, 5, 6, 7],
# [4, 5, 6, 7],
# [4, 5, 6, 7]],
#
# [[1, 2, 3, 4],
# [1, 2, 3, 4],
# [5, 6, 7, 8],
# [5, 6, 7, 8]],
#
# [[2, 3, 4, 5],
# [2, 3, 4, 5],
# [2, 3, 4, 5],
# [6, 7, 8, 9]],
#
# [[3, 4, 5, 6],
# [3, 4, 5, 6],
# [3, 4, 5, 6],
# [3, 4, 5, 6]]])
Which allows us to perform indexing. There are two approaches, using an intermediate 3D array and summing, or assigning and summing directly to an output 2D array with numpy.add.at:
tmp = np.zeros((N, N, 3*N-2), dtype=A.dtype)
tmp[X[:, None, None], X[None, :, None], Y] = A[:, None, None]
# array([[[ 1, 1, 1, 1, 0, 0, 0, 0, 0, 0],
# [ 0, 0, 0, 0, 1, 1, 1, 1, 0, 0],
# [ 0, 0, 0, 0, 1, 1, 1, 1, 0, 0],
# [ 0, 0, 0, 0, 1, 1, 1, 1, 0, 0]],
#
# [[ 0, 10, 10, 10, 10, 0, 0, 0, 0, 0],
# [ 0, 10, 10, 10, 10, 0, 0, 0, 0, 0],
# [ 0, 0, 0, 0, 0, 10, 10, 10, 10, 0],
# [ 0, 0, 0, 0, 0, 10, 10, 10, 10, 0]],
#
# [[ 0, 0, 100, 100, 100, 100, 0, 0, 0, 0],
# [ 0, 0, 100, 100, 100, 100, 0, 0, 0, 0],
# [ 0, 0, 100, 100, 100, 100, 0, 0, 0, 0],
# [ 0, 0, 0, 0, 0, 0, 100, 100, 100, 100]],
#
# [[ 0, 0, 0, 1000, 1000, 1000, 1000, 0, 0, 0],
# [ 0, 0, 0, 1000, 1000, 1000, 1000, 0, 0, 0],
# [ 0, 0, 0, 1000, 1000, 1000, 1000, 0, 0, 0],
# [ 0, 0, 0, 1000, 1000, 1000, 1000, 0, 0, 0]]])
out = tmp.sum(axis=0)
# array([[ 1, 11, 111, 1111, 1110, 1100, 1000, 0, 0, 0],
# [ 0, 10, 110, 1110, 1111, 1101, 1001, 1, 0, 0],
# [ 0, 0, 100, 1100, 1101, 1111, 1011, 11, 10, 0],
# [ 0, 0, 0, 1000, 1001, 1011, 1111, 111, 110, 100]])
Without the intermediate 3D array, using numpy.add.at:
out = np.zeros((N, 3*N-2), dtype=A.dtype)
np.add.at(out, (X[:, None], Y), A[:, None, None])
out
# array([[ 1, 11, 111, 1111, 1110, 1100, 1000, 0, 0, 0],
# [ 0, 10, 110, 1110, 1111, 1101, 1001, 1, 0, 0],
# [ 0, 0, 100, 1100, 1101, 1111, 1011, 11, 10, 0],
# [ 0, 0, 0, 1000, 1001, 1011, 1111, 111, 110, 100]])
full code and DataFrame creation
N = len(A)
X = np.arange(N)
Y = (X[:, None] + (X[:, None] < X)*N)[..., None] + X[None, None]
out = np.zeros((N, 3*N-2), dtype=A.dtype)
np.add.at(out, (X[:, None], Y), A[:, None, None])
df = pd.DataFrame(out).rename(columns=lambda x: x+1, index=lambda x: x+1)
print(df)
Output:
1 2 3 4 5 6 7 8 9 10
1 1 11 111 1111 1110 1100 1000 0 0 0
2 0 10 110 1110 1111 1101 1001 1 0 0
3 0 0 100 1100 1101 1111 1011 11 10 0
4 0 0 0 1000 1001 1011 1111 111 110 100
N, or always 3 rows, 8 columns? And what would the dataframe look like for e.g.N=4?