{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "127b2c4d",
   "metadata": {},
   "source": [
    "# Lesson 09: NumPy Challenge - SOLUTIONS\n",
    "\n",
    "This notebook contains complete solutions for the Lesson 09 NumPy Challenge activities.\n",
    "\n",
    "## Instructions:\n",
    "- Review each solution carefully\n",
    "- Compare with your own approach\n",
    "- Run the cells to see the output\n",
    "- Note that there are often multiple valid ways to solve each problem!"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "c8a3813c",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Import NumPy\n",
    "import numpy as np"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1b63cfde",
   "metadata": {},
   "source": [
    "---\n",
    "## Problem 1: The Grade Book Analyzer\n",
    "\n",
    "**Objective:** Create a grade analysis system using NumPy arrays and statistical functions."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "89a0f317",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Grade Analysis Results:\n",
      "class_average: 82.31\n",
      "student_averages: [86.25 81.   93.5  68.5 ]\n",
      "test_averages: [80.25 86.25 78.25 84.5 ]\n",
      "highest_score: 97\n",
      "lowest_score: 65\n",
      "passing_rate: 100.00%\n"
     ]
    }
   ],
   "source": [
    "# Problem 1: Solution\n",
    "\n",
    "def analyze_grades(grades):\n",
    "    \"\"\"\n",
    "    Analyzes a grade book and returns various statistics.\n",
    "    \n",
    "    Args:\n",
    "        grades (np.ndarray): 2D array of grades (students x tests)\n",
    "    \n",
    "    Returns:\n",
    "        dict: Dictionary containing various grade statistics\n",
    "    \"\"\"\n",
    "    \n",
    "    # Calculate overall class average (mean of all grades)\n",
    "    class_average = np.mean(grades)\n",
    "    \n",
    "    # Calculate average for each student (mean along axis=1, across columns)\n",
    "    student_averages = np.mean(grades, axis=1)\n",
    "    \n",
    "    # Calculate average for each test (mean along axis=0, down rows)\n",
    "    test_averages = np.mean(grades, axis=0)\n",
    "    \n",
    "    # Find highest and lowest scores\n",
    "    highest_score = np.max(grades)\n",
    "    lowest_score = np.min(grades)\n",
    "    \n",
    "    # Calculate passing rate (percentage of grades >= 60)\n",
    "    passing_grades = grades >= 60\n",
    "    passing_rate = (np.sum(passing_grades) / grades.size) * 100\n",
    "    \n",
    "    # Return all statistics in a dictionary\n",
    "    return {\n",
    "        'class_average': class_average,\n",
    "        'student_averages': student_averages,\n",
    "        'test_averages': test_averages,\n",
    "        'highest_score': highest_score,\n",
    "        'lowest_score': lowest_score,\n",
    "        'passing_rate': passing_rate\n",
    "    }\n",
    "\n",
    "# Test case\n",
    "grades = np.array([[85, 90, 78, 92],\n",
    "                   [76, 88, 81, 79],\n",
    "                   [93, 95, 89, 97],\n",
    "                   [67, 72, 65, 70]])\n",
    "\n",
    "results = analyze_grades(grades)\n",
    "\n",
    "print(\"Grade Analysis Results:\")\n",
    "print(f\"class_average: {results['class_average']:.2f}\")\n",
    "print(f\"student_averages: {results['student_averages']}\")\n",
    "print(f\"test_averages: {results['test_averages']}\")\n",
    "print(f\"highest_score: {results['highest_score']}\")\n",
    "print(f\"lowest_score: {results['lowest_score']}\")\n",
    "print(f\"passing_rate: {results['passing_rate']:.2f}%\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4365def4",
   "metadata": {},
   "source": [
    "---\n",
    "## Problem 2: The Array Transformer\n",
    "\n",
    "**Objective:** Create a flexible array manipulation function using NumPy operations."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "75b8c17f",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Mins: [[78]\n",
      " [76]\n",
      " [89]\n",
      " [65]]\n",
      "Maxes: [[90]\n",
      " [88]\n",
      " [95]\n",
      " [72]]\n"
     ]
    }
   ],
   "source": [
    "arr = np.array([[85, 90, 78],\n",
    "                [76, 88, 81],\n",
    "                [93, 95, 89],\n",
    "                [67, 72, 65]])\n",
    "\n",
    "min_val = np.min(arr, axis=1, keepdims=True)\n",
    "max_val = np.max(arr, axis=1, keepdims=True)\n",
    "\n",
    "print(f'Mins: {min_val}')\n",
    "print(f'Maxes: {max_val}')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "5e47cdd7",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[[0.58333333 1.         0.        ]\n",
      " [0.         1.         0.41666667]\n",
      " [0.66666667 1.         0.        ]\n",
      " [0.28571429 1.         0.        ]]\n"
     ]
    }
   ],
   "source": [
    "print((arr - min_val) / (max_val - min_val))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "bc366e51",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Test 1 (normalize):\n",
      "Original: [[1 2 3 4 5]\n",
      " [5 4 3 2 1]]\n",
      "Normalized: [[0.   0.25 0.5  0.75 1.  ]\n",
      " [1.   0.75 0.5  0.25 0.  ]]\n",
      "\n",
      "Test 2 (sqrt):\n",
      "Original: [ 1  4  9 16]\n",
      "Square root: [1. 2. 3. 4.]\n",
      "\n",
      "Test 3 (standardize):\n",
      "Original: [10 20 30 40 50]\n",
      "Standardized: [-1.41421356 -0.70710678  0.          0.70710678  1.41421356]\n",
      "\n",
      "Test 4 (square):\n",
      "Original: [1 2 3 4]\n",
      "Squared: [ 1  4  9 16]\n"
     ]
    }
   ],
   "source": [
    "# Problem 2: Solution\n",
    "\n",
    "def transform_array(arr, operation='normalize', axis=None):\n",
    "    \"\"\"\n",
    "    Transforms an array using various mathematical operations.\n",
    "    \n",
    "    Args:\n",
    "        arr (np.ndarray): Input array\n",
    "        operation (str): Type of transformation to apply\n",
    "        axis (int): Axis along which to apply the operation (if applicable)\n",
    "    \n",
    "    Returns:\n",
    "        np.ndarray: Transformed array\n",
    "    \"\"\"\n",
    "    \n",
    "    if operation == 'normalize':\n",
    "\n",
    "        # Min-max normalization: (x - min) / (max - min)\n",
    "        min_val = np.min(arr, axis=axis, keepdims=True)\n",
    "        max_val = np.max(arr, axis=axis, keepdims=True)\n",
    "        \n",
    "        # # Handle edge case where all values are the same\n",
    "        # if max_val - min_val == 0:\n",
    "        #     return np.zeros_like(arr)\n",
    "        \n",
    "        return (arr - min_val) / (max_val - min_val)\n",
    "    \n",
    "    elif operation == 'standardize':\n",
    "\n",
    "        # Z-score standardization: (x - mean) / std\n",
    "        mean = np.mean(arr, axis=axis, keepdims=True)\n",
    "        std = np.std(arr, axis=axis, keepdims=True)\n",
    "        \n",
    "        # Handle edge case where std is 0\n",
    "        if std == 0:\n",
    "            return np.zeros_like(arr)\n",
    "        \n",
    "        return (arr - mean) / std\n",
    "    \n",
    "    elif operation == 'square':\n",
    "\n",
    "        # Square all values\n",
    "        return arr ** 2\n",
    "    \n",
    "    elif operation == 'sqrt':\n",
    "\n",
    "        # Take square root of all values\n",
    "        return np.sqrt(arr)\n",
    "    \n",
    "    else:\n",
    "        return f\"Error: Unknown operation '{operation}'\"\n",
    "\n",
    "# Test cases\n",
    "print(\"Test 1 (normalize):\")\n",
    "arr1 = np.array([[1, 2, 3, 4, 5], [5, 4, 3, 2, 1]])\n",
    "print(f\"Original: {arr1}\")\n",
    "print(f\"Normalized: {transform_array(arr1, 'normalize')}\")\n",
    "\n",
    "print(\"\\nTest 2 (sqrt):\")\n",
    "arr2 = np.array([1, 4, 9, 16])\n",
    "print(f\"Original: {arr2}\")\n",
    "print(f\"Square root: {transform_array(arr2, 'sqrt')}\")\n",
    "\n",
    "print(\"\\nTest 3 (standardize):\")\n",
    "arr3 = np.array([10, 20, 30, 40, 50])\n",
    "print(f\"Original: {arr3}\")\n",
    "print(f\"Standardized: {transform_array(arr3, 'standardize')}\")\n",
    "\n",
    "print(\"\\nTest 4 (square):\")\n",
    "arr4 = np.array([1, 2, 3, 4])\n",
    "print(f\"Original: {arr4}\")\n",
    "print(f\"Squared: {transform_array(arr4, 'square')}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3e37c017",
   "metadata": {},
   "source": [
    "---\n",
    "## Problem 3: The Matrix Operations Toolkit\n",
    "\n",
    "**Objective:** Build a function that performs various matrix operations."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "bec3ba8a",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Matrix A:\n",
      "[[1 2]\n",
      " [3 4]]\n",
      "\n",
      "Matrix B:\n",
      "[[5 6]\n",
      " [7 8]]\n",
      "\n",
      "Transpose of A:\n",
      "[[1 3]\n",
      " [2 4]]\n",
      "\n",
      "Element-wise multiplication (A * B):\n",
      "[[ 5 12]\n",
      " [21 32]]\n",
      "\n",
      "Matrix multiplication (A @ B):\n",
      "[[19 22]\n",
      " [43 50]]\n",
      "\n",
      "Addition (A + B):\n",
      "[[ 6  8]\n",
      " [10 12]]\n",
      "\n",
      "Flatten A:\n",
      "[1 2 3 4]\n"
     ]
    }
   ],
   "source": [
    "# Problem 3: Solution\n",
    "\n",
    "def matrix_operations(matrix1, matrix2=None, operation='transpose'):\n",
    "    \"\"\"\n",
    "    Performs various matrix operations.\n",
    "    \n",
    "    Args:\n",
    "        matrix1 (np.ndarray): First matrix\n",
    "        matrix2 (np.ndarray): Second matrix (optional)\n",
    "        operation (str): The operation to perform\n",
    "    \n",
    "    Returns:\n",
    "        np.ndarray or str: Result of the operation or error message\n",
    "    \"\"\"\n",
    "    \n",
    "    if operation == 'transpose':\n",
    "        # Return the transpose of matrix1\n",
    "        return matrix1.T\n",
    "    \n",
    "    elif operation == 'flatten':\n",
    "        # Flatten matrix1 to 1D array\n",
    "        return matrix1.flatten()\n",
    "    \n",
    "    # For operations requiring two matrices, check if matrix2 is provided\n",
    "    if matrix2 is None:\n",
    "        return f\"Error: Operation '{operation}' requires two matrices\"\n",
    "    \n",
    "    if operation == 'multiply':\n",
    "\n",
    "        # Element-wise multiplication\n",
    "        if matrix1.shape != matrix2.shape:\n",
    "            return \"Error: Matrices must have the same shape for element-wise multiplication\"\n",
    "        \n",
    "        return matrix1 * matrix2\n",
    "    \n",
    "    elif operation == 'add':\n",
    "\n",
    "        # Matrix addition\n",
    "        if matrix1.shape != matrix2.shape:\n",
    "            return \"Error: Matrices must have the same shape for addition\"\n",
    "        \n",
    "        return matrix1 + matrix2\n",
    "    \n",
    "    elif operation == 'matmul':\n",
    "\n",
    "        # Matrix multiplication\n",
    "        # Check if dimensions are compatible: (m x n) @ (n x p) = (m x p)\n",
    "        if matrix1.shape[1] != matrix2.shape[0]:\n",
    "            return f\"Error: Incompatible shapes for matrix multiplication: {matrix1.shape} and {matrix2.shape}\"\n",
    "        \n",
    "        return matrix1 @ matrix2\n",
    "    \n",
    "    else:\n",
    "        return f\"Error: Unknown operation '{operation}'\"\n",
    "\n",
    "# Test cases\n",
    "A = np.array([[1, 2], [3, 4]])\n",
    "B = np.array([[5, 6], [7, 8]])\n",
    "\n",
    "print(\"Matrix A:\")\n",
    "print(A)\n",
    "print(\"\\nMatrix B:\")\n",
    "print(B)\n",
    "\n",
    "print(\"\\nTranspose of A:\")\n",
    "print(matrix_operations(A, operation='transpose'))\n",
    "\n",
    "print(\"\\nElement-wise multiplication (A * B):\")\n",
    "print(matrix_operations(A, B, operation='multiply'))\n",
    "\n",
    "print(\"\\nMatrix multiplication (A @ B):\")\n",
    "print(matrix_operations(A, B, operation='matmul'))\n",
    "\n",
    "print(\"\\nAddition (A + B):\")\n",
    "print(matrix_operations(A, B, operation='add'))\n",
    "\n",
    "print(\"\\nFlatten A:\")\n",
    "print(matrix_operations(A, operation='flatten'))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5965257b",
   "metadata": {},
   "source": [
    "---\n",
    "## Problem 4: Fixing NumPy Bugs\n",
    "\n",
    "**Objective:** Debug and fix code snippets that contain common NumPy-related errors."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8434dcf7",
   "metadata": {},
   "source": [
    "### Bug 1: The Shape Mismatch\n",
    "\n",
    "**Problem:** Array has 8 elements but trying to reshape to 3x3 (9 elements)\n",
    "\n",
    "**Solution:** Need 9 elements or reshape to compatible dimensions (e.g., 2x4, 4x2)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "533fff24",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[[1 2 3]\n",
      " [4 5 6]\n",
      " [7 8 9]]\n",
      "\n",
      "Alternative (2x4):\n",
      "[[1 2 3 4]\n",
      " [5 6 7 8]]\n"
     ]
    }
   ],
   "source": [
    "# Bug 1: Fixed code\n",
    "\n",
    "# Fix: Need 9 elements for a 3x3 array\n",
    "arr = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9])  # Added 9 to make it 9 elements\n",
    "reshaped = arr.reshape(3, 3)\n",
    "\n",
    "print(reshaped)\n",
    "\n",
    "# Alternative fix: Reshape to compatible dimensions with 8 elements\n",
    "arr2 = np.array([1, 2, 3, 4, 5, 6, 7, 8])\n",
    "reshaped2 = arr2.reshape(2, 4)  # or (4, 2)\n",
    "print(\"\\nAlternative (2x4):\")\n",
    "print(reshaped2)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5536fc71",
   "metadata": {},
   "source": [
    "### Bug 2: The Indexing Error\n",
    "\n",
    "**Problem:** Using `[:, 1]` gets the second column, not the second row\n",
    "\n",
    "**Solution:** Use `[1, :]` to get the second row (row index 1, all columns)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "beb0da63",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Matrix:\n",
      "[[1 2 3]\n",
      " [4 5 6]\n",
      " [7 8 9]]\n",
      "\n",
      "Second row: [4 5 6]\n",
      "Second column: [2 5 8]\n"
     ]
    }
   ],
   "source": [
    "# Bug 2: Fixed code\n",
    "\n",
    "matrix = np.array([[1, 2, 3],\n",
    "                   [4, 5, 6],\n",
    "                   [7, 8, 9]])\n",
    "\n",
    "print(\"Matrix:\")\n",
    "print(matrix)\n",
    "\n",
    "# Fix: Use [1, :] to get the second row (row index 1, all columns)\n",
    "# The original code [:, 1] gets the second column instead\n",
    "second_row = matrix[1, :]  # or simply matrix[1]\n",
    "print(\"\\nSecond row:\", second_row)\n",
    "\n",
    "# For comparison, here's how to get the second column:\n",
    "second_column = matrix[:, 1]\n",
    "print(\"Second column:\", second_column)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "59f63b2a",
   "metadata": {},
   "source": [
    "### Bug 3: The Data Type Problem\n",
    "\n",
    "**Problem:** Array is integer type by default, causing integer division in older Python/NumPy\n",
    "\n",
    "**Solution:** Convert array to float type before division, or use float in array creation"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "9e133843",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Option 1 - Using dtype=float:\n",
      "Numbers: [10. 15. 20. 25. 30.]\n",
      "Divided by 3: [ 3.33333333  5.          6.66666667  8.33333333 10.        ]\n",
      "Data type: float64\n",
      "\n",
      "Option 2 - Using astype(float):\n",
      "Divided by 3: [ 3.33333333  5.          6.66666667  8.33333333 10.        ]\n",
      "Data type: float64\n"
     ]
    }
   ],
   "source": [
    "# Bug 3: Fixed code\n",
    "\n",
    "# Fix Option 1: Explicitly specify float dtype when creating array\n",
    "numbers = np.array([10, 15, 20, 25, 30], dtype=float)\n",
    "divisor = 3\n",
    "\n",
    "result = numbers / divisor\n",
    "\n",
    "print(\"Option 1 - Using dtype=float:\")\n",
    "print(f\"Numbers: {numbers}\")\n",
    "print(f\"Divided by {divisor}: {result}\")\n",
    "print(f\"Data type: {result.dtype}\")\n",
    "\n",
    "# Fix Option 2: Convert to float after creation\n",
    "numbers2 = np.array([10, 15, 20, 25, 30])\n",
    "result2 = numbers2.astype(float) / divisor\n",
    "\n",
    "print(\"\\nOption 2 - Using astype(float):\")\n",
    "print(f\"Divided by {divisor}: {result2}\")\n",
    "print(f\"Data type: {result2.dtype}\")\n",
    "\n",
    "# Note: In Python 3 and modern NumPy, division with / always produces floats,\n",
    "# but explicitly setting the dtype is still good practice for clarity"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cfd4e21d",
   "metadata": {},
   "source": [
    "---\n",
    "## Bonus Challenge: The Data Filter\n",
    "\n",
    "**Objective:** Apply multiple NumPy concepts to solve a real-world data filtering problem."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "ad64e1b5",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Original data: [-5 10 -3 15  0 -8 20  3]\n",
      "\n",
      "Positive values: [10 15 20  3]\n",
      "Count: 4\n",
      "\n",
      "Negative values: [-5 -3 -8]\n",
      "Count: 3\n",
      "\n",
      "Values > 10: [15 20]\n",
      "Count: 2\n",
      "\n",
      "Values in range [-5, 5]: [-5 -3  0  3]\n",
      "Count: 4\n"
     ]
    }
   ],
   "source": [
    "# Bonus Challenge: Solution\n",
    "\n",
    "def filter_data(data, condition='positive', threshold=0):\n",
    "    \"\"\"\n",
    "    Filters data based on various conditions.\n",
    "    \n",
    "    Args:\n",
    "        data (np.ndarray): Input data array\n",
    "        condition (str): The filtering condition to apply\n",
    "        threshold (float): Threshold value for certain conditions\n",
    "    \n",
    "    Returns:\n",
    "        tuple: (filtered_data, count)\n",
    "    \"\"\"\n",
    "    \n",
    "    # Flatten the array if it's multi-dimensional\n",
    "    flat_data = data.flatten()\n",
    "    \n",
    "    # Apply the appropriate filter based on condition\n",
    "    if condition == 'positive':\n",
    "\n",
    "        # Keep only positive values\n",
    "        mask = flat_data > 0\n",
    "    \n",
    "    elif condition == 'negative':\n",
    "\n",
    "        # Keep only negative values\n",
    "        mask = flat_data < 0\n",
    "    \n",
    "    elif condition == 'threshold':\n",
    "\n",
    "        # Keep only values greater than threshold\n",
    "        mask = flat_data > threshold\n",
    "    \n",
    "    elif condition == 'range':\n",
    "\n",
    "        # Keep only values between -threshold and +threshold (inclusive)\n",
    "        mask = (flat_data >= -threshold) & (flat_data <= threshold)\n",
    "    \n",
    "    else:\n",
    "        return (np.array([]), 0)\n",
    "    \n",
    "    # Apply the mask to filter the data\n",
    "    filtered_data = flat_data[mask]\n",
    "    count = len(filtered_data)\n",
    "    \n",
    "    return (filtered_data, count)\n",
    "\n",
    "# Test cases\n",
    "data = np.array([-5, 10, -3, 15, 0, -8, 20, 3])\n",
    "print(f\"Original data: {data}\\n\")\n",
    "\n",
    "filtered, count = filter_data(data, condition='positive')\n",
    "print(f\"Positive values: {filtered}\")\n",
    "print(f\"Count: {count}\\n\")\n",
    "\n",
    "filtered, count = filter_data(data, condition='negative')\n",
    "print(f\"Negative values: {filtered}\")\n",
    "print(f\"Count: {count}\\n\")\n",
    "\n",
    "filtered, count = filter_data(data, condition='threshold', threshold=10)\n",
    "print(f\"Values > 10: {filtered}\")\n",
    "print(f\"Count: {count}\\n\")\n",
    "\n",
    "filtered, count = filter_data(data, condition='range', threshold=5)\n",
    "print(f\"Values in range [-5, 5]: {filtered}\")\n",
    "print(f\"Count: {count}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "43e8e52c",
   "metadata": {},
   "source": [
    "---\n",
    "## __Reflection Questions - Sample Answers__\n",
    "\n",
    "Here are thoughtful answers to the reflection questions:"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "61988ef9",
   "metadata": {},
   "source": [
    "### Your Reflections:\n",
    "\n",
    "1. **How does NumPy's vectorization make operations faster compared to Python loops?**\n",
    "\n",
    "   NumPy's vectorization performs operations on entire arrays at once using optimized C code, rather than iterating through elements one by one in Python. For example, in Problem 2, `arr ** 2` squares all elements simultaneously, which is much faster than using a for loop like `[x**2 for x in arr]`. NumPy operations are implemented in C and use CPU-level optimizations like SIMD (Single Instruction, Multiple Data), making them 10-100x faster for large datasets.\n",
    "\n",
    "2. **What is the purpose of the `axis` parameter in NumPy functions?**\n",
    "\n",
    "   The `axis` parameter specifies which dimension to perform an operation along. In Problem 1, `axis=0` calculates along rows (down columns) to get test averages, while `axis=1` calculates along columns (across rows) to get student averages. Think of it as: `axis=0` collapses rows (vertical), `axis=1` collapses columns (horizontal). Without an axis parameter, the operation applies to the entire flattened array.\n",
    "\n",
    "3. **Explain the difference between element-wise multiplication and matrix multiplication.**\n",
    "\n",
    "   Element-wise multiplication (`*`) multiplies corresponding elements: `[[1,2],[3,4]] * [[5,6],[7,8]] = [[5,12],[21,32]]`. Matrix multiplication (`@` or `np.matmul()`) performs the dot product of rows and columns: for 2x2 matrices, result[i,j] = sum of (row i of A * column j of B). Matrix multiplication requires compatible shapes (m×n @ n×p = m×p), while element-wise requires identical shapes. Matrix multiplication is fundamental to linear algebra and machine learning transformations.\n",
    "\n",
    "4. **Why is it important to pay attention to array shapes?**\n",
    "\n",
    "   Array shapes determine whether operations are valid and affect the results. Bug 1 showed that reshape requires compatible dimensions (8 elements can't become 3×3). Bug 2 demonstrated how indexing depends on understanding row vs column dimensions. Shape mismatches cause ValueError in operations like matrix multiplication or addition. In data science, shape errors often indicate conceptual mistakes - like trying to multiply features with the wrong number of samples. Always check shapes with `.shape` before operations.\n",
    "\n",
    "5. **How can boolean indexing be used for data filtering?**\n",
    "\n",
    "   Boolean indexing creates a mask of True/False values based on conditions, then uses it to select elements. In Problem 1, `grades >= 60` created a boolean array, which we used to count passing grades. The Bonus Challenge showed more complex filtering with conditions like `(data >= -threshold) & (data <= threshold)`. Real-world example: filtering customer data to find high-value purchases over $100: `expensive_items = prices[prices > 100]`. This is much cleaner and faster than loops, and it's essential for data cleaning, outlier detection, and feature engineering in data science workflows."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1cbee261",
   "metadata": {},
   "source": [
    "---\n",
    "## Additional Tips and Best Practices\n",
    "\n",
    "### Performance Tips:\n",
    "1. **Avoid loops** - Use vectorized operations whenever possible\n",
    "2. **Preallocate arrays** - Use `np.zeros()` or `np.empty()` instead of growing arrays\n",
    "3. **Use views, not copies** - Slicing creates views; use `.copy()` only when needed\n",
    "4. **Choose appropriate dtype** - Use the smallest dtype that fits your data\n",
    "\n",
    "### Common Pitfalls:\n",
    "1. **Modifying arrays in place** - Remember that some operations return new arrays\n",
    "2. **Integer division** - Be explicit about float types when doing division\n",
    "3. **Broadcasting confusion** - Understand how NumPy expands dimensions\n",
    "4. **Memory efficiency** - Large arrays can consume lots of memory; be mindful of copies\n",
    "\n",
    "### Debugging Strategies:\n",
    "1. **Print shapes** - Use `print(arr.shape)` frequently\n",
    "2. **Check dtypes** - Use `print(arr.dtype)` to verify data types\n",
    "3. **Test with small arrays** - Debug with 2-3 element arrays first\n",
    "4. **Use array_equal** - Compare arrays with `np.array_equal(a, b)`"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4838a8c1",
   "metadata": {},
   "source": [
    "---\n",
    "## Congratulations!\n",
    "\n",
    "You've completed the Lesson 09 NumPy Challenge solutions! Key takeaways:\n",
    "\n",
    "- **Array operations** are vectorized and much faster than Python loops\n",
    "- **Shape awareness** is critical for avoiding errors and understanding results\n",
    "- **Boolean indexing** provides powerful data filtering capabilities\n",
    "- **Statistical functions** with axis parameters enable sophisticated data analysis\n",
    "- **Matrix operations** form the foundation of linear algebra in data science\n",
    "\n",
    "Continue practicing these concepts as you move forward with pandas, scikit-learn, and other data science libraries. NumPy is the foundation of the entire scientific Python ecosystem!"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": ".venv",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
