Matrices can represent linear transformations. NumPy is used to apply transformations like scaling and rotation to a set of data points. This application shows matrix multiplication for manipulating geometric data, a common task in areas like computer graphics and data preprocessing for machine learning.Setting Up the SceneFirst, ensure you have NumPy installed and import it. We'll define a small dataset of 2D points. Let's use points that form a simple shape, like an "L", to make the transformations easy to visualize.import numpy as np import math # Define our data points as row vectors in a matrix # Each row is a point (x, y) data_points = np.array([ [0, 0], [1, 0], [1, 1], [1, 2] ]) print("Original Data Points (each row is a point):") print(data_points)Our data_points matrix has 4 rows (points) and 2 columns (x and y coordinates).Visualizing the Original DataBefore transforming, let's visualize our starting points. A scatter plot is suitable for this.{"layout": {"xaxis": {"title": "X-axis", "range": [-3, 3], "gridcolor": "#dee2e6"}, "yaxis": {"title": "Y-axis", "range": [-3, 3], "scaleanchor": "x", "scaleratio": 1, "gridcolor": "#dee2e6"}, "title": "Original Data Points", "width": 500, "height": 500, "plot_bgcolor": "#f8f9fa"}, "data": [{"type": "scatter", "mode": "markers+lines", "x": [0, 1, 1, 1], "y": [0, 0, 1, 2], "marker": {"color": "#1c7ed6", "size": 10}, "line": {"color": "#1c7ed6"}, "name": "Original"}]}The initial L-shape formed by our data points.Transformation 1: ScalingLet's apply a scaling transformation. We'll scale the x-coordinates by 1.5 and the y-coordinates by 0.5. The scaling matrix $S$ for this is:$$ S = \begin{bmatrix} 1.5 & 0 \ 0 & 0.5 \end{bmatrix} $$When we multiply our data matrix $D$ (where points are rows) by this scaling matrix $S$, $D' = D @ S$, each point $(x, y)$ becomes $(1.5x, 0.5y)$.# Define the scaling matrix scaling_matrix = np.array([ [1.5, 0], [0, 0.5] ]) # Apply the transformation # Data points are rows (n x 2), Scaling matrix is (2 x 2) # Result is (n x 2) @ (2 x 2) = (n x 2) scaled_data = data_points @ scaling_matrix print("\nScaled Data Points:") print(scaled_data)Let's visualize the result alongside the original points.{"layout": {"xaxis": {"title": "X-axis", "range": [-3, 3], "gridcolor": "#dee2e6"}, "yaxis": {"title": "Y-axis", "range": [-3, 3], "scaleanchor": "x", "scaleratio": 1, "gridcolor": "#dee2e6"}, "title": "Scaling Transformation", "width": 500, "height": 500, "plot_bgcolor": "#f8f9fa"}, "data": [{"type": "scatter", "mode": "markers+lines", "x": [0.0, 1.5, 1.5, 1.5], "y": [0.0, 0.0, 0.5, 1.0], "marker": {"color": "#40c057", "size": 10}, "line": {"color": "#40c057"}, "name": "Scaled"}, {"type": "scatter", "mode": "markers+lines", "x": [0, 1, 1, 1], "y": [0, 0, 1, 2], "marker": {"color": "#adb5bd", "size": 8, "opacity": 0.6}, "line": {"color": "#adb5bd", "dash": "dot"}, "name": "Original"}]}The L-shape stretched horizontally (x-axis scaled by 1.5) and compressed vertically (y-axis scaled by 0.5).Transformation 2: RotationNow, let's rotate the original points counter-clockwise by 45 degrees (${\pi}/{4}$ radians). The rotation matrix $R$ for a counter-clockwise rotation by an angle $\theta$ is:$$ R = \begin{bmatrix} \cos(\theta) & \sin(\theta) \ -\sin(\theta) & \cos(\theta) \end{bmatrix} $$We apply this as $D' = D @ R$.# Define the rotation angle in radians angle_degrees = 45 angle_radians = math.radians(angle_degrees) cos_theta = math.cos(angle_radians) sin_theta = math.sin(angle_radians) # Define the rotation matrix rotation_matrix = np.array([ [cos_theta, sin_theta], [-sin_theta, cos_theta] ]) # Apply the transformation to the original data rotated_data = data_points @ rotation_matrix print(f"\nRotation Matrix ({angle_degrees} degrees CCW):") print(rotation_matrix) print("\nRotated Data Points:") print(rotated_data)Let's visualize the rotation.{"layout": {"xaxis": {"title": "X-axis", "range": [-3, 3], "gridcolor": "#dee2e6"}, "yaxis": {"title": "Y-axis", "range": [-3, 3], "scaleanchor": "x", "scaleratio": 1, "gridcolor": "#dee2e6"}, "title": "Rotation Transformation (45° CCW)", "width": 500, "height": 500, "plot_bgcolor": "#f8f9fa"}, "data": [{"type": "scatter", "mode": "markers+lines", "x": [0.0, 0.707, 0.0, -0.707], "y": [0.0, 0.707, 1.414, 2.121], "marker": {"color": "#f06595", "size": 10}, "line": {"color": "#f06595"}, "name": "Rotated"}, {"type": "scatter", "mode": "markers+lines", "x": [0, 1, 1, 1], "y": [0, 0, 1, 2], "marker": {"color": "#adb5bd", "size": 8, "opacity": 0.6}, "line": {"color": "#adb5bd", "dash": "dot"}, "name": "Original"}]}The L-shape rotated 45 degrees counter-clockwise around the origin (0,0). Notice the coordinates change according to the trigonometric functions in the rotation matrix.Combining TransformationsLinear transformations can be chained together. The effect of applying transformation $T_1$ followed by $T_2$ to data $D$ is equivalent to applying a single combined transformation $T_{combined} = T_1 @ T_2$. The order matters! Let's apply the scaling first, then the rotation.$D'' = (D @ S) @ R = D @ (S @ R)$# Combined transformation: Scale first, then Rotate # Option 1: Apply sequentially temp_data = data_points @ scaling_matrix # Apply scaling first scaled_then_rotated_data = temp_data @ rotation_matrix # Then apply rotation # Option 2: Combine matrices first combined_matrix_SR = scaling_matrix @ rotation_matrix scaled_then_rotated_data_combined = data_points @ combined_matrix_SR print("\nCombined Matrix (Scale then Rotate):") print(combined_matrix_SR) print("\nScaled then Rotated Data (Sequential):") print(scaled_then_rotated_data) print("\nScaled then Rotated Data (Combined Matrix):") print(scaled_then_rotated_data_combined) # Verify they are numerically close assert np.allclose(scaled_then_rotated_data, scaled_then_rotated_data_combined) Let's visualize the final result of scaling followed by rotation.{"layout": {"xaxis": {"title": "X-axis", "range": [-3, 3], "gridcolor": "#dee2e6"}, "yaxis": {"title": "Y-axis", "range": [-3, 3], "scaleanchor": "x", "scaleratio": 1, "gridcolor": "#dee2e6"}, "title": "Combined: Scale then Rotate (45° CCW)", "width": 500, "height": 500, "plot_bgcolor": "#f8f9fa"}, "data": [{"type": "scatter", "mode": "markers+lines", "x": [0.0, 1.061, 0.707, 0.354], "y": [0.0, 1.061, 0.354, -0.354], "marker": {"color": "#fd7e14", "size": 10}, "line": {"color": "#fd7e14"}, "name": "Scaled+Rotated"}, {"type": "scatter", "mode": "markers+lines", "x": [0, 1, 1, 1], "y": [0, 0, 1, 2], "marker": {"color": "#adb5bd", "size": 8, "opacity": 0.6}, "line": {"color": "#adb5bd", "dash": "dot"}, "name": "Original"}]}The L-shape after being scaled (stretched horizontally, compressed vertically) and then rotated 45 degrees counter-clockwise. Compare this to applying rotation first, then scaling (exercise for the reader!).Summary and RelevanceThis hands-on exercise demonstrated how matrix multiplication provides a concise and computationally efficient way to apply geometric transformations to data. We used NumPy's @ operator for matrix multiplication to perform scaling and rotation on a set of 2D points.Understanding these operations is significant for:Data Augmentation: In computer vision, images (collections of pixel data) are often rotated, scaled, or sheared to artificially increase the size and diversity of the training dataset, helping models generalize better.Feature Engineering: Transforming existing features using matrix operations can sometimes create new, more informative features for a machine learning model.Dimensionality Reduction: Techniques like Principal Component Analysis (PCA), which we'll explore later, rely heavily on linear transformations (specifically rotations) guided by the data's structure to reduce the number of features while preserving important information.Understanding Model Internals: Some models implicitly perform linear transformations on input data. Visualizing these transformations helps build intuition about how data flows through a model.Here's the complete Python code for reference:import numpy as np import math # 1. Define Original Data data_points = np.array([ [0, 0], [1, 0], [1, 1], [1, 2] ]) print("Original Data Points:\n", data_points) # 2. Define Scaling Transformation scaling_factor_x = 1.5 scaling_factor_y = 0.5 scaling_matrix = np.array([ [scaling_factor_x, 0], [0, scaling_factor_y] ]) scaled_data = data_points @ scaling_matrix print("\nScaling Matrix:\n", scaling_matrix) print("Scaled Data:\n", scaled_data) # 3. Define Rotation Transformation angle_degrees = 45 angle_radians = math.radians(angle_degrees) cos_theta = math.cos(angle_radians) sin_theta = math.sin(angle_radians) rotation_matrix = np.array([ [cos_theta, sin_theta], [-sin_theta, cos_theta] ]) rotated_data = data_points @ rotation_matrix print(f"\nRotation Matrix ({angle_degrees} degrees CCW):\n", rotation_matrix) print("Rotated Data:\n", rotated_data) # 4. Combined Transformation (Scale then Rotate) # Option 1: Sequential scaled_then_rotated_data = (data_points @ scaling_matrix) @ rotation_matrix # Option 2: Combine matrices first combined_matrix_SR = scaling_matrix @ rotation_matrix scaled_then_rotated_data_combined = data_points @ combined_matrix_SR print("\nCombined Matrix (Scale @ Rotate):\n", combined_matrix_SR) print("Scaled then Rotated Data:\n", scaled_then_rotated_data) # Verification assert np.allclose(scaled_then_rotated_data, scaled_then_rotated_data_combined) print("\nSequential and combined matrix application results are consistent.")Experiment with different transformation matrices (e.g., shearing, reflections, different scaling factors, or rotation angles) and observe their effects on the data points. Consider applying transformations in a different order to see how the results change.