Now Code

The Rainfall Problem

Overview

  • Teaching: 0 min
  • Exercises: 90 min

Questions

  • What is the Rainfall problem?

Outcomes

  • Understand how to combine loops and conditionals to achieve a desired result
  • Write defensive code to help users to use your function correctly, provide help and informative error messages

Getting Started

This exercise requires you to clone the repository from: github.com/arc-bath/rainfall.git. Make sure that the repository is not cloned into a directory or sub-directory of an existing git repository. On notebooks.azure.com, the easiest way to ensure this is to create a new library with Upload GitHub Repo

Once your library has been created Run the library, navigate to the src folder and start a new notebook. You can the run the tests in test_rainfall.py from within a code cell with:

!pytest test_rainfall.py

Alternative start up

If you wish to run from the terminal then you can instead clone the repository directly with git:

git clone https://github.com/arc-bath/rainfall.git

Once you have the repository change into the directory and run the tests in test_rainfall.py

% cd rainfall/src
% pytest test_rainfall.py

You should see a lot of output from pytest since many of the tests failed. The final line should contain a summary:

======================================== 9 failed, 3 passed in 0.16 seconds =========================================

The aim of this exercise is to modify your function so that it passes all these tests. Let's begin by reducing the output produced by pytest so we can see more clearly what is happening:

% pytest --tb=short test_rainfall.py

You should now see output that looks like:

================================================ test session starts ================================================
platform linux -- Python 3.6.3, pytest-3.2.1, py-1.4.34, pluggy-0.4.0
rootdir: /home/rjg20/training/arc-training/now-code-repos/rainfall, inifile:
collected 12 items

src/test_rainfall.py ...FFFFFFFFF

===================================================== FAILURES ======================================================
________________________________________________ test_rf_negative_1 _________________________________________________
src/test_rainfall.py:48: in test_rf_negative_1
    assert_almost_equal( observ, expect )
E   AssertionError:
E   Arrays are not almost equal to 7 decimals
E    ACTUAL: 2.3333333333333335
E    DESIRED: 3
________________________________________________ test_rf_negative_2 _________________________________________________
src/test_rainfall.py:59: in test_rf_negative_2
    assert_almost_equal( observ, expect )
E   AssertionError:
E   Arrays are not almost equal to 7 decimals
E    ACTUAL: 2.3333333333333335
E    DESIRED: 3
________________________________________________ test_rf_negative_3 _________________________________________________
src/test_rainfall.py:70: in test_rf_negative_3
    assert_almost_equal( observ, expect )
E   AssertionError:
E   Arrays are not almost equal to 7 decimals
E    ACTUAL: 2.3333333333333335
E    DESIRED: 3
________________________________________________ test_rf_negative_4 _________________________________________________
src/test_rainfall.py:81: in test_rf_negative_4
    assert_almost_equal( observ, expect )
E   AssertionError:
E   Arrays are not almost equal to 7 decimals
E    ACTUAL: 1.2
E    DESIRED: 4
___________________________________________________ test_rf_99_1 ____________________________________________________
src/test_rainfall.py:92: in test_rf_99_1
    assert_almost_equal( observ, expect )
E   AssertionError:
E   Arrays are not almost equal to 7 decimals
E    ACTUAL: 27.0
E    DESIRED: 3
___________________________________________________ test_rf_99_2 ____________________________________________________
src/test_rainfall.py:103: in test_rf_99_2
    assert_almost_equal( observ, expect )
E   AssertionError:
E   Arrays are not almost equal to 7 decimals
E    ACTUAL: 23.0
E    DESIRED: 3
___________________________________________________ test_rf_both ____________________________________________________
src/test_rainfall.py:114: in test_rf_both
    assert_almost_equal( observ, expect )
E   AssertionError:
E   Arrays are not almost equal to 7 decimals
E    ACTUAL: 20.818181818181817
E    DESIRED: 3
___________________________________________________ test_rf_empty ___________________________________________________
src/test_rainfall.py:123: in test_rf_empty
    observ = rf.rfmean( test_list )
src/rainfall.py:26: in rfmean
    return sum/count
E   ZeroDivisionError: division by zero
_________________________________________________ test_rf_99_start __________________________________________________
src/test_rainfall.py:136: in test_rf_99_start
    assert_almost_equal( observ, expect )
E   TypeError: unsupported operand type(s) for -: 'str' and 'float'
======================================== 9 failed, 3 passed in 0.17 seconds =========================================

The Problem

The Rainfall problem has a long history, see this review for details. The problem involves combining structural components of programming, e.g. loops and conditionals to calculate the mean of a data set subject to:

  1. Ignoring negative values
  2. Exiting and calculating the mean early if a certain STOP value is met.

In this exercise you will modify a provided function to meet these criteria where the STOP value is 99. You will also handle exceptions such as empty lists and related problems to improve the robustness of you function and provide informative messages and help to potential users.

Later in the exercise you will introduce the possibility of a user defined STOP value.

You will be able to monitor your progress by comparing how your functions pass a series of tests provided in the exercise repository. This also includes a prototype function to help you get started.

1: Correct the calculator rfmean()

Modify the function rfmean in rainfall.py so that it correctly calculates the mean rainfall as described above. Remember that we want to ignore negative numbers and stop the calculation when we read 99.

You may find it useful to know that you can break out of a loop with the following structure:

for value in collection:      # initiate loop
    if condition == True:     # If some condition is satisfied
        break                 # Break out of, or exit the current loop (i.e. do not process any more values in collection)

Before modifying the function, consider writing a flowchart, or pseudocode to develop a clear idea of what your program will need to do to correctly calculate the mean rainfall. Once your function is performing correctly only 4 tests should now be failing.

2: Handle incorrect use

Documenting your programs and functions is vital to ensure that they are used correctly, however in case of error it is useful to provide feedback to help users identify what is wrong, for example take an error message and compare with documentation. The last three tests in test_rainfall.py require you to add this to your solution.

Look at test_rainfall.py to identify the different conditions that you need to identify to pass the required response to you user. As before draw a flow chart or write pseudocode to work out how to catch the different errors before implementing your solution.

3: Altered STOP condition

Your function now works correctly for the condition STOP=99. We now wish to generalise the function so that the user can define their own value. To do this you will modify your function so that it takes an optional parameter with a default value of 99. Remember than you can specify a default value for function variables in the definition:

def my_func(a, b, c, my_default_value = 100):
    '''Does function stuff'''
    print(a, b, c, my_default_values)

Once you have modified your function check that it still works with the earlier tests.

4: Test your new functionality

So far you have been running the set of test in test_rainfall.py. There is also another test file test_enhanced.py. Run this a few times to check whether your function passes. Do you think the problem is with your function or this new test? Carefully check the test output to workout what has happened.

You should see that there is a problem with the new test, since the test is code it is we must make sure that the test is accurate as well. Open test_enhanced.py and reconstruct what it is doing in a flow chart, or with code, the documentation and comments in the function should help you to understand how it is operating. See if you can identify what is wrong and propose a fix.

Once you have verified that your function and the corrected tests are behaving correctly it is now time to write a new test which checks that your user defined STOP works correctly. Duplicate the existing test in test_enhanced.py and modify the test and documentation so that it generates a random STOP value for each iteration.

Once you have written your new test, check that your function passes the test. If it doesn't, check the error messages carefully to determine whether the error lies in the rfmean function or the new test. Although the new tests are more complicated think about the pros and cons of writing test like this.

Key Points

  • In this exercise you have combined the two principal logical constructs in programming: loops and conditionals.
  • Numerous studies have found this to be a significant challenge for new programmers.
  • Using tests helps you to verify that your code works as intended.
  • Tests are just code and may contain errors, simple tests are therefore easier to validate.
  • Enhanced tests can make it easier to test a range of functionality and automate testing with different values.