Getting diagonals of 2D numpy array that affect original array

Viewed 409

I have a 2D numpy array. I would like to modify it by creating an array of its diagonal elements and modifying the diagonal array so that those changes reflect on the original 2D array.

I have tried with:

>>> a = np.ones(shape=(3,3))
>>> d1 = a[np.diag_indices_from(a)]
>>> d1
array([1., 1., 1.])
>>> d1[0] = 2
>>> d1
array([2., 1., 1.])
>>>a
array([[1., 1., 1.],
       [1., 1., 1.],
       [1., 1., 1.]])

It can be seen that the changes do not affect the original array.

Is there any way to create a diagonal array that will also affect the original 2D array?

EDIT: I get the effect I'm looking for when I work with rows or columns:

>>> row0 = a[0]
>>> row0[0] = 0
>>> a
array([[0., 1., 1.],
       [1., 1., 1.],
       [1., 1., 1.]])
>>> column0=a[:,0]
>>> column0[2]=3
>>> a
array([[0., 1., 1.],
       [1., 1., 1.],
       [3., 1., 1.]])
5 Answers

You can use np.einsum to get precisely what you want:

.. versionadded:: 1.10.0

Views returned from einsum are now writeable whenever the input array
is writeable. For example, ``np.einsum('ijk...->kji...', a)`` will now
have the same effect as :py:func:`np.swapaxes(a, 0, 2) <numpy.swapaxes>`
and ``np.einsum('ii->i', a)`` will return a writeable view of the diagonal
of a 2D array.
a = np.ones((3,3))
b = np.einsum("ii->i",a)
b[:] = 2,3,4
a
# array([[2., 1., 1.],
#        [1., 3., 1.],
#        [1., 1., 4.]])

When you index like that (fancy indexing) you are getting a copy not a view. You just need to assign d1 back to a the same way you defined it

a = np.ones(shape=(3,3))
d1 = a[np.diag_indices_from(a)]
d1[0] = 2
a[np.diag_indices_from(a)] = d1

The right way to do it is using np.diagonal and np.fill_diagonal. There is a way to twist numpy that is added below as well:

a = np.ones(shape=(3,3))
d1 = np.diagonal(a).copy()
d1[0] = 2
np.fill_diagonal(a,d1)

output:

a

[[2. 1. 1.]
 [1. 1. 1.]
 [1. 1. 1.]]

Note that based on numpy doc for np.diagonal: Starting in NumPy 1.9 it returns a read-only view on the original array. Attempting to write to the resulting array will produce an error. In some future release, it will return a read/write view and writing to the returned array will alter your original array. The returned array will have the same type as the input array.

In order to twist numpy, you can do this, but I would suggest using above solution:

a = np.ones(shape=(3,3))
d1 = np.diagonal(a)
d1.setflags(True)
d1[0] = 2
np.fill_diagonal(a,d1)

output:

a

[[2. 1. 1.]
 [1. 1. 1.]
 [1. 1. 1.]]

With basic indexing of a flattened version of the array, it is possible:

In [150]: a = np.ones((3,3))                                                                         
In [151]: d1 = a.ravel()[::4]                                                                        
In [152]: d1[0] *= 2                                                                                 
In [153]: a                                                                                          
Out[153]: 
array([[2., 1., 1.],
       [1., 1., 1.],
       [1., 1., 1.]])
In [154]: d1[:] = [1,2,3]                                                                            
In [155]: a                                                                                          
Out[155]: 
array([[1., 1., 1.],
       [1., 2., 1.],
       [1., 1., 3.]])
   

np.diag sort of does this, but sets the result to read-only.

In [157]: np.diag(a)                                                                                 
Out[157]: array([1., 2., 3.])
In [158]: d2 = np.diag(a)                                                                            
In [159]: d2[1] = 0                                                                                  
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-159-64d5edb77b4c> in <module>
----> 1 d2[1] = 0

ValueError: assignment destination is read-only
In [160]: a[1,1] = 10                                                                                
In [161]: a                                                                                          
Out[161]: 
array([[ 1.,  1.,  1.],
       [ 1., 10.,  1.],
       [ 1.,  1.,  3.]])
In [162]: d2                                                                                         
Out[162]: array([ 1., 10.,  3.])
In [163]: d1                                                                                         
Out[163]: array([ 1., 10.,  3.])

Modifying a changes d2, but we can't do it the other way around.

I'm afraid not. A NumPy array is a class object, with it's own indexing interface. When you create a diagonal (a vector) as a separate object, you break that original indexing paradigm and replace it with another -- the vector is a separate object.

If you want this functionality, you could write your own class, creating a new element object for each matrix element. Then you can readily alter those elements (through the class methods), changing the element value, but keeping the element instance intact.

This way, whenever you extract a diagonal -- or any other matrix slice, reference, etc. -- you work with the original object, not an immutable value.

Related