Appendix L — Final Exam Questions


Notes:

About Numpy Representation

When Numpy computes something for us, and the result is an ndarray it will look like this when displayed in the interactive terminal: array([1, 2, 3]). When the result is a scalar, however, it will look something like this: np.int64(99). This is the same as saying 99, but Numpy has overwritten the __repr__ dunder method, which controls the representation of the value in the terminal. If you do print(np.int64(99)) it will give 99 which looks like a normal int. The point is that 99 is the crucial part of the result and np.int64 is just there to remind us about the type. So in the examples below, when it says a function should return 99, this means both 99 and np.int64(99) are equivalent.

L.1 Question 1 (3 marks)

Write a function which accepts a string then finds the product of all the numerical characters in the string. The function should have an option to ignore 0’s, and if the string has no numerical values it should return None.

def Q1(___):  # Replace the ___ with your own argument(s)
    r"""
    Computes the product of all numerical characters in a string. 

    Parameters
    ----------
    string : str
        A string containing assorted characters.
    ignore_zeros : bool
        If this flag is True (the default) then 0's will not be included
        when computing the product. If False, they are included, meaning 
        the product will be 0.
    
    Returns
    -------
    prod : int or None
        The product of multiplying all numerical characters. If the 
        string contains no numerical values then None is returned.  
    """
    # Your code here
    return prod

Some examples:

Q1('a1b2c3')  # Should return 6
Q1('abcde')  # Should return None
Q1('01234')  # Should return 24
Q1('0a1b2', False)  # Should return 0
Q1('0a1b2', True)  # Should return 2

L.2 Question 2 (2 marks)

Write a function which merges two dictionaries. In the event that both dicts have keys in common, the function should prevent overwriting of duplicates by appending '_dupe' to the key.

def Q2(___):  # Replace the ___ with your own argument(s)
    r"""
    Merges two dictionaries into one, without overwriting duplicates.

    Parameters
    ----------
    a, b : dict
        The two dicts to be merged. 

    Returns
    -------
    d : dict
        A new dict containing all the key-value pairs from both a and b. 
        Note that '_dupe' will be appended to any keys in b which were 
        already present in a. 
    """
    # Your code here
    return d

Here are some examples:

a = {'bob': 2, 'dave': [1]}
b = {'dave': [2], 'fred': 'abc'}
Q2(a, b)  # Should return {'bob': 2, 'dave': [1], 'dave_dupe': [2], 'fred': 'abc'}

L.3 Question 3 (2 marks)

Python does not include any built-in keyword or function for determining the “exclusive or” condition. Write a function which accepts 2 boolean values, computes the “exclusive or” and returns True or False accordingly. (Hint: You may need to lookup what “exclusive or” means.)

def Q3(___):  # Replace the ___ with your own argument(s)
    r"""
    Computes the "exclusive or" between a pair of Boolean values.

    Parameters
    ----------
    a, b : bool
        The two boolean values which are to be compared.

    Returns
    -------
    xor : bool
        The "exclusive or" condition for the given values.
    """
    # Your code here
    return xor

Creating example cases to test your logic is part of this question, so no examples are provided.

L.4 Question 4 (3 marks)

Write a function which flattens a list containing sub-lists into a single 1D list. The sub-lists can be of any length, and they may contain arbitrary data values (i.e. numbers, characters, etc.), but not any additional sub-lists.

def Q4(___):  # Replace the ___ with your own argument(s)
    r"""
    Converts a list containing sub-lists into a single flattened, 1D list.

    Parameters
    ----------
    lil : list of lists
        The list containing other lists. 

    Returns
    -------
    flattened : list
        The flattened list.
    """
    # Your code here
    return flattened

Here are some examples:

Q4([[1, 2, 3]])  # Should return [1, 2, 3]
Q4([[1, 2, 3], [1, 2], [1]])  # Should return [1, 2, 3, 1, 2, 1]

L.5 Question 5 (2 marks)

Write a function that converts a list of numerical values into a Numpy ndarray. Assume that all the values in the list are of the same type, and ensure that the ndarray is of that dtype.

def Q5(___):  # Replace the ___ with your own argument(s)
    r"""
    Converts a list to a Numpy ndarray of a corresponding shape and 
    suitable dtype.

    Parameters
    ----------
    vals : list 
        A list to be converted to an ndarray. 

    Returns
    -------
    arr : ndarray
        A Numpy ndarray containing the values in vals, with a dtype that 
        is compatible.
    """
    # Your code here
    return arr

Here are some examples:

vals = [1+1j, 2+2j, 3+3j]
Q5(vals)  # Returns array([1.+1.j, 2.+2.j, 3.+3.j])

vals = [1, 2, 3, 4, 5, 6]
Q5(vals)  # Returns array([1, 2, 3, 4, 5, 6])

L.6 Question 6 (3 marks)

A chemical storage tank with a height of 7.5 m has a level sensor which outputs the height of the fluid in the tank in 1 hour intervals. The data is stored in a text file with one value on each line and no other information (not even a header row). Write a function which accepts the name of the file, then returns the number of times the level exceeded 7.0 m, which is considered dangerously close to overflowing.

def Q6(___):  # Replace the ___ with your own argument(s)
    r"""
    Counts the number of times the values in a text file exceed a given 
    threshold.

    Parameters
    ----------
    filename : str
        The name of the file containing the values to be checked.
    threshold : float
        The value above which should be counted. The default is 7.0

    Returns
    -------
    count : int
        The number of times the values in the specified text file exceeded 
        the given threshold limit.
    """
    # Your code here
    return count

The text file containing level data would look like this:

6.85
7.33
7.12
7.00
6.06
5.67

And here is an example, assuming the above data is stored in a file called data.txt:

Q6('data.txt', 7.0)  # Should return 2

L.7 Question 7 (2 marks)

Write a function which accepts a Numpy ndarray filled with numerical values, and an upper and lower limit, then counts the number of values that lie on or between the limits. (Hint: True + True == 2)

def Q7(___):  # Replace the ___ with your own argument(s)
    r"""
    Finds the number of occurrences in arr which lie between hi and lo.

    Parameters
    ----------
    arr : ndarray
        A Numpy ndarray containing values to be checked.  This array can be 
        any shape and have any number of dimensions.
    lo, hi : float
        The lower and upper limits between which values in arr are counted.  
        Values equal to lo and hi are considered within the limits. 

    Results
    -------
    count : int
        The number of values in arr that lie between lo and hi.
    """
    # Your code here
    return count

Some examples:

arr = np.array([[2, 3, 2], 
                [4, 3, 2], 
                [6, 4, 5]])
Q7(arr, 3, 5)  # Should return 5

L.8 Question 8 (3 marks)

Write a function which accepts an arbitrary number of Numpy ndarrays, and plots their histograms in an “N-by-N” grid of subplots, where “N” is determined to give a square grid. It is possible that the last row is not filled. (Hint: The *args notation collects all positional arguments received by a function and packages them into a tuple called args.)

def Q8(___):  # Replace the ___ with your own argument(s)
    r"""
    Generates a Matplotlib figure containing histograms in a square 
    grid arrangement.

    Parameters
    ----------
    *args : ndarrays
        An arbitrary of number of Numpy arrays passed as positional 
        arguments that are to be plotted as histograms.

    Returns
    -------
    fig, ax
        Matplotlib figure and axis handles are returned as created by 
        plt.subplots(). 
    """
    # Your code here
    return fig, ax

L.9 Question 9 (2 marks)

Write a function which accepts 2 Numpy ndarrays of equal size and shape, performs matrix multiplication on them, then computes the trace of the result. Use Numpy’s linear algebra functions as needed.

def Q9(___):  # Replace the ___ with your own argument(s)
    r"""
    Computes the trace of the matrix product of the given arrays.

    Parameters
    ----------
    a, b : ndarray
        Numpy ndarrays of equal size and shape
    
    Returns
    -------
    c : float
        The trace of the matrix product of a and b.  
    """
    # Your code here
    return c

Some examples:

a = np.array([[1, 2, 3], 
              [4, 5, 6], 
              [7, 8, 9]])
b = np.ones_like(a)*2
Q9(a, b)  # Should return 90

L.10 Question 10 (2 marks)

Write a function which computes the value of a polynomial for a set of x values given the coefficients in a tuple arranged from low to high order terms. In other words, coeffs = (a0, a1, a2, a3) would correspond to the following polynomial:

\[ y = a_0 + a_1x + a_2x^2 + a_3x^3 \]

def Q10(___):  # Replace the ___ with your own argument(s)
    r"""
    Computes the y-values of a polynomial given x-values and a set of 
    coefficients.

    Parameters
    ----------
    x : scalar or array_like
        The x-value(s) at which the polynomial should be evaluated. 
    coeffs : tuple of floats
        A tuple containing the coefficients to use, ordered from lowest to 
        highest power terms. I.e., if "y = a0 + a1*x", then 
        "coeffs = (a0, a1)".

    Returns
    -------
    y : scalar or array_like
        The y-value(s) corresponding to the given x after applying an N-th 
        order polynomial.
    """
    # Your code here
    return y

Some examples:

x = [0, 1, 2]
coeffs = [1, 2]
Q10(x, coeffs)  # Should return array([1, 3, 5])

L.11 Question 11 (2 marks)

Write a wrapper or helper function for Numpy’s polyfit function which accepts a Pandas DataFrame containing columns with 'x' and 'y' data and the desired order of the fit, and returns a tuple containing the fitted coefficients stored in increasing order of the term to which they belong, as defined by the following:

\[ y = a_0 + a_1x + a_2x^2 + a_3x^3 + ... \]

def Q11(___):  # Replace the ___ with your own argument(s)
    r"""
    A wrapper function for Numpy's polyfit function that accepts data
    in the form of a Pandas DataFrame

    Parameters
    ----------
    df : Pandas DataFrame
        A DataFrame containing columns labelled 'x' and 'y' containing
        the data to be fit.  All other columns will be ignored.
    order : int
        The desired order of the fit. If not provided a value of 1 is 
        assumed.

    Returns
    -------
    coeffs : tuple
        A tuple containing each of the coefficients sorted by increasing
        order of the term they belong to.
    """
    # Your code
    return coeffs

Some examples:

x = [0, 1, 2]
y = [1, 3, 5]
df = ...  # You need to figure out how to put x and y into a DataFrame
Q11(df, 1)  # Should return array([1., 2.])

L.12 Question 12 (4 marks)

You have a 2D Numpy array where each row is the temperature taken hourly, starting at midnight and ending at 11pm, and each column is data from a new day. Write a function that finds the largest jump in hourly temperature for the entire time period. You can use Numpy’s diff function, but be sure that you include the change between 11pm and 12am the following day. (Hint: It might be helpful to reshape the data array, but you need to pay attention to how Numpy does the reshaping. This might require reading the documentation and experimenting in the Python terminal for a few minutes.)

data = [[ 22.50, 18.62, 22.04 ],
        [ 20.27, 19.86, 20.03 ],
        [ 20.26, 17.33, 18.60 ],
        [ 21.75, 15.02, 21.26 ],
        [ 21.05, 13.86, 23.08 ],
        [ 23.47, 11.43, 23.98 ],
        [ 22.84,  9.98, 25.45 ],
        [ 21.38,  9.19, 25.90 ],
        [ 20.03, 11.92, 27.57 ],
        [ 19.71, 10.89, 29.30 ],
        [ 19.14, 11.56, 26.94 ],
        [ 19.83, 11.62, 26.78 ],
        [ 18.24, 12.27, 24.45 ],
        [ 20.35, 14.32, 25.18 ],
        [ 18.86, 16.99, 23.81 ],
        [ 17.06, 19.86, 24.62 ],
        [ 16.02, 19.77, 27.13 ],
        [ 14.68, 19.27, 28.45 ],
        [ 16.71, 17.77, 28.51 ],
        [ 16.98, 20.60, 26.17 ],
        [ 15.93, 19.96, 24.31 ],
        [ 14.14, 19.94, 24.95 ],
        [ 14.33, 21.40, 23.07 ],
        [ 16.60, 21.27, 23.35 ]]
def Q12(___):  # Replace the ___ with your own argument(s)
    r"""
    Finds the absolute maximum temperature difference between adjacent 
    hours.

    Parameters
    ----------
    temperatures : ndarray
        A 2D ndarray with rows containing hourly temperature measurements, 
        and columns containing measurements for a given day.

    Returns
    -------
    maxdiff : float
        The absolute maximum difference in temperatures experienced 
        between two adjacent hours. 
    """
    # Your code
    return maxdiff

An example:

data = np.array([[1, 10], 
                 [2, 11],
                 [3, 12],
                 [6, 13]])
Q12(data)  # Should return 4