--- title: Views vs Copies (Shallow vs Deep Copy) description: duration: 600 card_type: cue_card --- # Views vs Copies (Shallow vs Deep Copy) - Numpy **manages memory very efficiently**, - which makes it really **useful while dealing with large datasets**. #### But how does it manage memory so efficiently? - Let's create some arrays to understand what's happening in memory while using numpy. Code ```python= import numpy as np ``` Code ```python= # We'll create a np array a = np.arange(4) a ``` array([0, 1, 2, 3]) Code ```python= # Reshape array `a` and store in `b` b = a.reshape(2, 2) b ``` array([[0, 1], [2, 3]]) #### Now we will make some changes to our original array `a`. Code ```python= a[0] = 100 a ``` array([100, 1, 2, 3]) #### What will be values if we print array `b`? Code ```python= b ``` array([[100, 1], [ 2, 3]]) Array **`b` got automatically updated** This is an example of numpy using "Shallow Copy" of data. #### Now, what happens here? - Numpy **re-uses data** as much as possible **instead of duplicating** it. - This helps numpy to be efficient. #### When we created `b=a.reshape(2,2)` - Numpy **did NOT make a copy of `a` to store in `b`**, as we can clearly see. - It is **using the same data as in `a`**. - It **just looks different (reshaped)** in `b`. - That is why, **any changes in `a` automatically gets reflected in `b`**. ### Now, let's see an example where Numpy will create a "Deep Copy" of data. Code ```python= a = np.arange(4) a ``` array([0, 1, 2, 3]) Code ```python= # Create `c` c = a + 2 c ``` array([2, 3, 4, 5]) Code ```python= # We make changes in `a` a[0] = 100 a ``` array([100, 1, 2, 3]) Code ```python= c ``` array([2, 3, 4, 5]) Code ```python= np.shares_memory(a, c) # Deep Copy ``` False #### As we can see, `c` did not get affected on changing `a`. - Because it is an operation. - A more **permanent change in data**. - So, Numpy **had to create a separate copy for `c`** - i.e., **deep copy of array `a` for array `c`**. ### Conclusion: - Numpy is able to **use same data** for **simpler operations** like **reshape** ---> **Shallow Copy**. - It creates a **copy of data** where operations make **more permanent changes** to data ---> **Deep Copy**. --- title: Checking memory sharing using `np.shares_memory()` description: duration: 300 card_type: cue_card --- #### Is there a way to check whether two arrays are sharing memory or not? Yes, there is. `np.shares_memory()` function to the rescue!!! Code ```python= a= np.arange(10) a ``` array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) Code ```python= b = a[::2] b ``` array([0, 2, 4, 6, 8]) Code ```python= np.shares_memory(a,b) ``` True > Notice that Slicing creates shallow copies. Code ```python= a[0] = 1000 ``` Code ```python= b ``` array([1000, 2, 4, 6, 8]) Code ```python= a = np.arange(6) a ``` array([0, 1, 2, 3, 4, 5]) Code ```python= b = a[a % 1 == 0] b ``` array([0, 1, 2, 3, 4, 5]) Code ```python= b[0] = 10 ``` Code ```python= a[0] ``` 0 Code ```python= np.shares_memory(a,b) ``` False ``` Memory in Numpy -> - Shallow Copy - Reshaping, Slicing... - Deep Copy - Arithmetic Operations, Masking... ``` Code ```python= a = np.arange(10) ``` Code ```python= a_shallow_copy = a.view() # Creates a shallow copy of a ``` Code ```python= np.shares_memory(a_shallow_copy, a) ``` True Code ```python= a_deep_copy = a.copy() # Creates a deep copy of a ``` Code ```python= np.shares_memory(a_deep_copy, a) ``` False --- title: Quiz-1 description: Quiz-1 duration: 60 card_type: quiz_card --- # Question ```python= a = [0,1,2,3,4,5] b = a[a%1 == 0] b[0] = 10 a[:2] = ? ``` # Choices - [x] [0,1] - [ ] [0,1,2] - [ ] [10,1] - [ ] [10,1,2] --- title: Understanding `.view()` description: duration: 300 card_type: cue_card --- #### `.view()` Returns view of the original array - Any changes made in new array will be reflected in original array. Documentation: <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.view.html> Code ```python= arr = np.arange(10) arr ``` array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) Code ```python= view_arr = arr.view() view_arr ``` array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) Let's modify the content of `view_arr` and check whether it modified the original array as well. Code ```python= view_arr[4] = 420 view_arr ``` array([ 0, 1, 2, 3, 420, 5, 6, 7, 8, 9]) Code ```python= arr ``` array([ 0, 1, 2, 3, 420, 5, 6, 7, 8, 9]) Code ```python= np.shares_memory(arr, view_arr) ``` True Notice that changes in view array are reflected in original array. --- title: Making Deep Cody using `.copy()` description: duration: 300 card_type: cue_card --- #### How do we make deep copy? Numpy has `.copy()` function for that purpose. #### `.copy()` Returns copy of the array. Documentation (`.copy()`): <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.copy.html#numpy.ndarray.copy> Documentation: (`np.copy()`): <https://numpy.org/doc/stable/reference/generated/numpy.copy.html> Code ```python= arr = np.arange(10) arr ``` array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) Code ```python= copy_arr = arr.copy() copy_arr ``` array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) Let's modify the content of `copy_arr` and check whether it modified the original array as well. Code ```python= copy_arr[3] = 45 copy_arr ``` array([ 0, 1, 2, 45, 4, 5, 6, 7, 8, 9]) Code ```python= arr ``` array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) Notice that the content of original array were not modified as we changed our copy array. ### Summarizing - `.view()` returns shallow copy of an array. - `.copy()` returns deep copy of an array except for object type array. - `copy.deepcopy()` returns deep copy of an array. --- title: Array Splitting description: duration: 300 card_type: cue_card --- # Splitting #### `np.split()` - Splits an array into multiple sub-arrays as views. #### It takes an argument `indices_or_sections`. - If `indices_or_sections` is an **integer, n**, the array will be **divided into n equal arrays along axis**. - If such a split is not possible, an error is raised. - If `indices_or_sections` is a **1-D array of sorted integers**, the entries indicate **where along axis the array is split**. - If an index **exceeds the dimension of the array along axis**, an **empty sub-array is returned** correspondingly. Code ```python= x = np.arange(9) x ``` array([0, 1, 2, 3, 4, 5, 6, 7, 8]) Code ```python= np.split(x, 3) ``` [array([0, 1, 2]), array([3, 4, 5]), array([6, 7, 8])] > **IMPORTANT REQUISITE** -> Number of elements in the array should be divisible by number of sections Code ```python= b = np.arange(10) np.split(b, 3) ``` --------------------------------------------------------------------------- ValueError Traceback (most recent call last) <ipython-input-30-5033f171e13f> in <cell line: 2>() 1 b = np.arange(10) ----> 2 np.split(b, 3) /usr/local/lib/python3.10/dist-packages/numpy/core/overrides.py in split(*args, **kwargs) /usr/local/lib/python3.10/dist-packages/numpy/lib/shape_base.py in split(ary, indices_or_sections, axis) 870 N = ary.shape[axis] 871 if N % sections: --> 872 raise ValueError( 873 'array split does not result in an equal division') from None 874 return array_split(ary, indices_or_sections, axis) ValueError: array split does not result in an equal division Code ```python= b[0:-1] np.split(b[0:-1], 3) ``` [array([0, 1, 2]), array([3, 4, 5]), array([6, 7, 8])] Code ```python= # Splitting on the basis of exact indices c = np.arange(16) ``` Code ```python= np.split(c, [3, 5, 6]) ``` [array([0, 1, 2]), array([3, 4]), array([5]), array([ 6, 7, 8, 9, 10, 11, 12, 13, 14, 15])] --- title: Understanding horizontal and vertical split description: duration: 300 card_type: cue_card --- <img src="https://d2beiqkhq929f0.cloudfront.net/public_assets/assets/000/054/735/original/hvsp1.png?1698041133" width="500" height="350"> #### `np.hsplit()` - Splits an array into multiple sub-arrays **horizontally (column-wise)**. Code ```python= x = np.arange(16.0).reshape(4, 4) x ``` array([[ 0., 1., 2., 3.], [ 4., 5., 6., 7.], [ 8., 9., 10., 11.], [12., 13., 14., 15.]]) #### Think of it this way: - There are 2 axis to a 2D array 1. **1st axis - Vertical axis** 2. **2nd axis - Horizontal axis** #### Along which axis are we splitting the array? - The split we want happens across the **2nd axis (Horizontal axis)** - That is why we use `hsplit()` #### So, try to think in terms of "whether the operation is happening along vertical axis or horizontal axis" - We are splitting the horizontal axis in this case. Code ```python= np.hsplit(x, 2) ``` [array([[ 0., 1.], [ 4., 5.], [ 8., 9.], [12., 13.]]), array([[ 2., 3.], [ 6., 7.], [10., 11.], [14., 15.]])] Code ```python= np.hsplit(x, np.array([3, 6])) ``` [array([[ 0., 1., 2.], [ 4., 5., 6.], [ 8., 9., 10.], [12., 13., 14.]]), array([[ 3.], [ 7.], [11.], [15.]]), array([], shape=(4, 0), dtype=float64)] #### `np.vsplit()` - Splits an array into multiple sub-arrays **vertically (row-wise)**. Code ```python= x = np.arange(16.0).reshape(4, 4) x ``` array([[ 0., 1., 2., 3.], [ 4., 5., 6., 7.], [ 8., 9., 10., 11.], [12., 13., 14., 15.]]) #### Now, along which axis are we splitting the array? - The split we want happens across the **1st axis (Vertical axis)** - That is why we use `vsplit()` #### Again, always try to think in terms of "whether the operation is happening along vertical axis or horizontal axis" - We are splitting the vertical axis in this case. Code ```python= np.vsplit(x, 2) ``` [array([[0., 1., 2., 3.], [4., 5., 6., 7.]]), array([[ 8., 9., 10., 11.], [12., 13., 14., 15.]])] Code ```python= np.vsplit(x, np.array([3])) ``` [array([[ 0., 1., 2., 3.], [ 4., 5., 6., 7.], [ 8., 9., 10., 11.]]), array([[12., 13., 14., 15.]])] --- title: Stacking (`vtack`) description: duration: 200 card_type: cue_card --- # Stacking (`vstack`) Code ```python= a = np.arange(1, 5) b = np.arange(2, 6) c = np.arange(3, 7) ``` #### `np.vstack()` - Stacks a list of arrays **vertically (along axis 0 or 1st axis)**. - For **example**, **given a list of row vectors, appends the rows to form a matrix**. Code ```python= np.vstack([b, c, a]) ``` array([[2, 3, 4, 5], [3, 4, 5, 6], [1, 2, 3, 4]]) Code ```python= a = np.arange(1, 5) b = np.arange(2, 4) c = np.arange(3, 10) ``` Code ```python= np.vstack([b, c, a]) ``` --------------------------------------------------------------------------- ValueError Traceback (most recent call last) <ipython-input-40-5148cb6ebc5f> in <cell line: 1>() ----> 1 np.vstack([b, c, a]) /usr/local/lib/python3.10/dist-packages/numpy/core/overrides.py in vstack(*args, **kwargs) /usr/local/lib/python3.10/dist-packages/numpy/core/shape_base.py in vstack(tup) 280 if not isinstance(arrs, list): 281 arrs = [arrs] --> 282 return _nx.concatenate(arrs, 0) 283 284 /usr/local/lib/python3.10/dist-packages/numpy/core/overrides.py in concatenate(*args, **kwargs) ValueError: all the input array dimensions for the concatenation axis must match exactly, but along dimension 1, the array at index 0 has size 2 and the array at index 1 has size 7 --- title: Quiz-2 description: Quiz-2 duration: 60 card_type: quiz_card --- # Question What will be the output of following code? ```python= a = np.array([[1], [2], [3]]) b = np.array([[4], [5], [6]]) np.vstack((a, b)) ``` # Choices - [ ] `array([1, 2, 3, 4, 5, 6])` - [ ] `array([[1, 4], [2, 5], [3, 6]])` - [x] `array([[1], [2], [3], [4], [5], [6]])` - [ ] Error --- title: Stacking (`htack`) description: duration: 200 card_type: cue_card --- ### Explanation of Quiz-2: Code ```python= a = np.array([[1], [2], [3]]) b = np.array([[4], [5], [6]]) np.vstack((a, b)) ``` array([[1], [2], [3], [4], [5], [6]]) # Stacking (`hstack`) Code ```python= a = np.arange(5).reshape(5, 1) ``` Code ```python= b = np.arange(15).reshape(5, 3) ``` Code ```python= np.hstack([a, b]) ``` array([[ 0, 0, 1, 2], [ 1, 3, 4, 5], [ 2, 6, 7, 8], [ 3, 9, 10, 11], [ 4, 12, 13, 14]]) --- title: Quiz-3 description: Quiz-3 duration: 60 card_type: quiz_card --- # Question what will be the output of this? a = np.array([[1], [2], [3]]) b = np.array([[4], [5], [6]]) np.hstack((a, b)) # Choices - [ ] `[[1] [2] [3] [4] [5] [6]]` - [ ] `[[1 2] [3 4] [5 6]]` - [x] `[[1 4] [2 5] [3 6]]` - [ ] `[[4 1] [5 2] [6 3]]` --- title: Understanding `np.concatenate()` description: duration: 300 card_type: cue_card --- ### Explanation of Quiz-3: Code ```python= a = np.array([[1], [2], [3]]) a ``` array([[1], [2], [3]]) Code ```python= b = np.array([[4], [5], [6]]) b ``` array([[4], [5], [6]]) Code ```python= np.hstack((a, b)) ``` array([[1, 4], [2, 5], [3, 6]]) #### This time both `a` and `b` are column vectors. - So, the stacking of `a` and `b` along horizontal axis is more clearly visible. ## `np.concatenate()` - can perform both vstack and hstack - Creates a new array by appending arrays after each other, along a given axis. - Provides similar functionality, but it takes a **keyword argument `axis`** that specifies the **axis along which the arrays are to be concatenated**. #### Input array to `concatenate()` needs to be of dimensions atleast equal to the dimensions of output array. Code ```python= a = np.array([1,2,3]) b = np.array([[1,2,3], [4,5,6]]) ``` Code ```python= a ``` array([1, 2, 3]) Code ```python= b ``` array([[1, 2, 3], [4, 5, 6]]) Code ```python= np.concatenate([a, b], axis = 0) ``` --------------------------------------------------------------------------- ValueError Traceback (most recent call last) <ipython-input-47-1a93c4fe21df> in <cell line: 1>() ----> 1 np.concatenate([a, b], axis = 0) /usr/local/lib/python3.10/dist-packages/numpy/core/overrides.py in concatenate(*args, **kwargs) ValueError: all the input arrays must have same number of dimensions, but the array at index 0 has 1 dimension(s) and the array at index 1 has 2 dimension(s) > **Note**: concatenate can only work if both a and b have the same number of dimensions Code ```python= a = np.array([[1,2,3]]) b = np.array([[1,2,3], [4,5,6]]) ``` Code ```python= np.concatenate([a, b], axis = 0) # axis = 0 -> vstack ``` array([[1, 2, 3], [1, 2, 3], [4, 5, 6]]) Code ```python= a = np.arange(6).reshape(3, 2) b = np.arange(9).reshape(3, 3) ``` Code ```python= np.concatenate([a, b], axis = 1) # axis = 1 -> hstack ``` array([[0, 1, 0, 1, 2], [2, 3, 3, 4, 5], [4, 5, 6, 7, 8]]) Code ```python= a = np.array([[1,2], [3,4]]) b = np.array([[5,6,7,8]]) ``` Code ```python= np.concatenate([a, b], axis = None) # axis = None joins and converts to 1D ``` array([1, 2, 3, 4, 5, 6, 7, 8]) #### Let's look at a few more examples using `np.concatenate()`. #### Question: What will be the output of this? a = np.array([[1, 2], [3, 4]]) b = np.array([[5, 6]]) np.concatenate((a, b), axis=0) Code ```python= a = np.array([[1, 2], [3, 4]]) a ``` array([[1, 2], [3, 4]]) Code ```python= b = np.array([[5, 6]]) b ``` array([[5, 6]]) Code ```python= np.concatenate((a, b), axis=0) ``` array([[1, 2], [3, 4], [5, 6]]) #### How did it work? - Dimensions of `a` is 2 × 2 #### What is the dimensions of `b` ? - 1-D array ?? - **NO** - Look carefully!! - **`b` is a 2-D array of dimensions 1 × 2** #### `axis = 0` ---> It's a vertical axis - So, **changes will happen along vertical axis**. - So, **`b` gets concatenated below `a`**. #### What if we do NOT provide an axis along which to concatenate? Code ```python= a = np.array([[1, 2], [3, 4]]) b = np.array([[5, 6]]) np.concatenate((a, b), axis=None) ``` array([1, 2, 3, 4, 5, 6]) #### Can you see what happened here? - When we **don't specify the axis (`axis=None`)**, - `np.concatenate()` **flattens the arrays and concatenates them as 1-D row array.** --- title: Quiz-4 description: Quiz-4 duration: 60 card_type: quiz_card --- # Question What will be the result of this concatenation operation? ```python= a = np.array([[1, 2], [3, 4]]) b = np.array([[5, 6]]) np.concatenate((a, b.T), axis=1) ``` # Choices - [ ] `[[1, 2], [3, 4], [5, 6]]` - [x] `[[1, 2, 5], [3, 4, 6]]` - [ ] Error --- title: Quiz-4 Explanation description: duration: 60 card_type: cue_card --- ## Explanation: Code ```python= a = np.array([[1, 2], [3, 4]]) b = np.array([[5, 6]]) np.concatenate((a, b.T), axis=1) ``` array([[1, 2, 5], [3, 4, 6]]) #### What happened here? - **Dimensions of `a`** is again 2 × 2 - **Dimensions of `b`** is again 1 × 2 - So, **Dimensions of `b.T`** will be 2 × 1 #### This time, **`axis = 1`** ---> Changes will happen along horizontal axis - So, **`b.T` gets concatenated horizontally to `a`**