Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Answer key for HW01: Writing key quantities from descriptive statistics in Python yourself

⚠️⚠️⚠️ TOTAL POINTS POSSIBLE: 18 ⚠️⚠️⚠️

Preliminaries

Import needed python packages

import numpy as np # for working with arrays of numerical values
import scipy  # for various scientific calculations
import xarray as xr  # for loading data and subsequent analyses

Load the Central Park data

Notebook Cell
!pip install pooch

# The above command installs the needed `pooch` 3rd-party package if it's not already installed.


import hashlib  # for verifying that the Central Park file is not corrupted
import pathlib  # for constructing paths to the dataset's location on disk
import sys  # for checking if this is a Google Colab session or not
import pooch  # for downloading the dataset from the web, if needed


# Replace "../data" as needed to point to the correct directory for you.
# This can be an *absolute path* or a *relative path*.  One dot, `.`, means
# "this directory", while two dots, `..`, means "go up one directory."
LOCAL_DATA_DIR = "../data"  # If you're in Colab: just ignore this.

# The URL where the dataset can be downloaded from.
DATA_URL = (
    "https://spencerahill.github.io/25f-stat-methods-course/_downloads/"
    "91803b82950d49961a65355c075439b3/central-park-station-data_1869-01-01_2023-09-30.nc"
)

# This HASH_HEX stores a "hash" which we use to verify that the data you end up
# with has not been altered or corrupted compared to the one at the above URL.
HASH_HEX = "85237a4bae1202030a36f330764fd5bd0c2c4fa484b3ae34a05db49fe7721eee"


def create_data_path(
    colab_dir="/content/data", 
    local_dir=LOCAL_DATA_DIR,
    filename="central-park-station-data_1869-01-01_2023-09-30.nc",
):
    """Set the path for the data, whether on colab or a local Jupyter session."""
    is_this_a_colab = "google.colab" in sys.modules
    if is_this_a_colab:
        data_dir = colab_dir 
    else: 
        data_dir = local_dir

    DATA_DIR = pathlib.Path(data_dir)
    DATA_DIR.mkdir(parents=True, exist_ok=True)
    return DATA_DIR / filename


def sha256sum(path: pathlib.Path) -> str:
    """Get the hash of the file at the specified path."""
    return hashlib.sha256(path.read_bytes()).hexdigest()


DATA_PATH = create_data_path()
# Determine if we'll need to download the data, which we'll do if either (a) 
# the data can't be found, or (b) it appears corrupted/modified from the
# "master" file at the above URL.
need_fetch = (not DATA_PATH.exists()) or (sha256sum(DATA_PATH) != HASH_HEX)

# Download the data if needed.
if need_fetch:
    fetched_data = pooch.retrieve(
        url=DATA_URL, 
        known_hash=f"sha256:{HASH_HEX}",
        path=DATA_PATH.parents[0], 
        fname=DATA_PATH.name,
    )
    print(f"\nDownloaded and verified: {fetched_data}")
else:
    print(f"\nVerified existing file at {DATA_PATH}")
Looking in links: https://pypi.python.org/pypi, https://testpypi.python.org/pypi
Requirement already satisfied: pooch in /Users/sah2249/miniconda3/envs/25f-stats/lib/python3.13/site-packages (1.8.2)
Requirement already satisfied: platformdirs>=2.5.0 in /Users/sah2249/miniconda3/envs/25f-stats/lib/python3.13/site-packages (from pooch) (4.5.0)
Requirement already satisfied: packaging>=20.0 in /Users/sah2249/miniconda3/envs/25f-stats/lib/python3.13/site-packages (from pooch) (25.0)
Requirement already satisfied: requests>=2.19.0 in /Users/sah2249/miniconda3/envs/25f-stats/lib/python3.13/site-packages (from pooch) (2.32.5)
Requirement already satisfied: charset_normalizer<4,>=2 in /Users/sah2249/miniconda3/envs/25f-stats/lib/python3.13/site-packages (from requests>=2.19.0->pooch) (3.4.4)
Requirement already satisfied: idna<4,>=2.5 in /Users/sah2249/miniconda3/envs/25f-stats/lib/python3.13/site-packages (from requests>=2.19.0->pooch) (3.11)
Requirement already satisfied: urllib3<3,>=1.21.1 in /Users/sah2249/miniconda3/envs/25f-stats/lib/python3.13/site-packages (from requests>=2.19.0->pooch) (2.5.0)
Requirement already satisfied: certifi>=2017.4.17 in /Users/sah2249/miniconda3/envs/25f-stats/lib/python3.13/site-packages (from requests>=2.19.0->pooch) (2025.10.5)

Verified existing file at ../data/central-park-station-data_1869-01-01_2023-09-30.nc
import xarray as xr

# `DATA_PATH` variable was created by the hidden cell just above. 
# Un-hide that cell if you want to see the details.
ds_cp = xr.open_dataset(DATA_PATH)
ds_cp
Loading...

Introduction

All of the quantities that we discussed in the lecture on descriptive statistics---mean, variance, skewness, etc.---are used ubiquitously. As such, they have already been written up in Python as functions in myriad different ways. implemented many times in Python. That’s how I was able to call precip_central_park.mean() for example: the variable is precip_central_park is an xarray.DataArray object; and DataArrays include mean as a method.

Those built-in implementations in the Python standard library and in reliable packages we’ll use like numpy, scipy, and xarray are very widely tested for bugs, optimized for performance, and include lots of nice additional features for more complex calculations. So other than in this assignment, they are what you should use when you need to compute these kinds of things.

But they’re also kind of a black box---when you call precip_central_park.mean(), it doesn’t tell you what’s happening “under the hood.” It just spits out the result, whether you actually understand the quantity conceptually or not.

In this assignment, to make sure we each do understand what these different quantities each entail, we’ll write each one ourselves as a Python function. For each one, we’ll test it against the built-in version to make sure that they give the right answers. And along the way we’ll learn more about how Python works.

The goal is to challenge you in two key ways: (1) understanding more deeply the key quantities in descriptive statistics, and (2) understanding more deeply the basics of how Python works.

A toy example: addition

By way of example, I’ll create a function right now that simply adds two numbers. Of course, there is no need to do this; you can just use Python’s standard addition operator via the plus sign +:

2 + 3
5

But we could also write our own function that does this:

def add(num1, num2):
    """Add the two numbers."""
    return num1 + num2

This defined (def) a function named add that accepts two arguments (num1 and num2) and spits out (returns) their sum.

Now we can call our function to perform the addition:

add(2, 3)
5

Finally, even though it’s plainly obvious that our function will, as desired, give you the sum of two numbers, let’s formally show that using Python’s == operator (that’s two equals signs), which returns True if two things are the same or False if they aren’t:

add(2, 3) == (2 + 3)
True

Your specific task

These are the six quantities that you will implement in Python as functions:

Measures of central tendency:

  • mean

  • median

Measures of dispersion

  • range

  • variance

  • standard deviation

  • coefficient of variation

For each one, the function call signature (meaning what arguments it accepts) should look like this:

def my_func(arr):
   """replace w/ brief informative description of your function"""
    # Insert line(s) of code that perform the desired calculation
    # on the given 1D array named `arr`.
    return answer  # Here, `answer` should be replaced with whatever variable you define in your function that stores the final result of your function.

So, for example, write another addition function, making it add up all the elements in a 1D array rather than just two numbers. That might look like this:

def add_array(arr):
    """Sum all elements of the array."""
    sum = 0
    for arr_element in arr:  # Loop over every item in the array `arr`
        sum += arr_element  # Add that item to the running sum
    return sum  # Once the loop has run over all elements, give us ("return") the total sum.

What each function must do

  • Only use things built in to Python’s standard library, and excluding those in the built-in math package. In other words, there should be no import statements anywhere in your code!

  • You can use a function you already wrote inside of another! So for example, let’s say you wanted to create a function that computed the square of the sum of all array elements. Given that add_array already exists, we could write this as follows:

def square_sum(arr):
    return add_array(arr) ** 2

Things you do not have to worry about

  • You can assume that the function will only be given one-dimensional arrays consisting entirely of decimal numbers (i.e. “floats”). You don’t have to worry about handling, for example, if it was passed an integer instead, or a 2-D array, etc.

  • You don’t have to worry at all about performance, i.e. how fast the code runs and how much memory it uses. Just write it in the way that is the most intuitive to you.

Things to look out for

Especially for those newer to Python, a couple potential “gotchas”:

  • Indentation matters in Python! After the def my_func(arr): line, the lines of code that are part of the function must be indented, meaning those lines start with one or more spaces. The standard is four spaces, and so please use that.

  • The function must end with a line starting with return, followed by whatever it is you want that function to spit out at the end. So for example, if in my add_array function above, I had forgotten to include the return sum line, the function would compute the sum but then just not do anything with it!

What specifically you will submit

For each function, you’ll print out the results of that function computed on the following simple toy array:

arr_test = [1.0, 4.0, 10.0, 4.0, 2.]

This is small enough that you can double check all of the quantities yourself against a calculator.

For example, for my toy add_array function above, this is what the file would look like:

def add_array(arr):
    """Sum all elements of the array."""
    sum = 0
    for arr_element in arr:  # Loop over every item in the array `arr`
        sum += arr_element  # Add that item to the running sum
    return sum  # Once the loop has run over all elements, give us ("return") the total sum.


# You don't have to include this `if` statement, especially if you don't 
# yet know what it's doing.  We'll return to why this is a good practice later on.
# If you do comment it out, don't forget to remove the indentation of the lines
# after, otherwise you'll get an `IndentationError`.
if __name__ == "__main__":
    # Define the toy array we'll test the functions against.
    arr_test = [1.0, 4.0, 10.0, 4.0, 2.]
    # Print out its values to make sure you copy/pasted it correctly from the HW.
    print(f"Toy dataset is: {arr_test}")

    # Start with the first measure to be calculated.
    # In this toy example, it's my array-sum metric.  In yours, it will be the mean.

    # Describe the measure conceptually in words, and report the answer
    # that it should produce when computed on the toy array, which you can 
    # compute by hand or at most using a simple calculator.
    print()  # blank line for easier readability
    print("Function #1 is the sum: add all elements of the array.")
    ans_expected_sum = 21
    print(f"We can add those up without a computer or calculator; the answer is {ans_expected_sum}.")

    # Now call your function on the toy array, and print out the result.
    ans_add_array_on_toy = add_array(arr_test)
    print(f"Calling `add_array` on the toy dataset.  Answer: {ans_add_array_on_toy}")

    # Finally, print out the difference between what you expect the answer to be
    # and what the actual function computes (this should be zero or *very* close to it).
    diff_add_array_expect_actual = ans_expected_sum - ans_add_array_on_toy
    print(f"Difference between expected and actual: {diff_add_array_expect_actual}")
    print()  # A blank line at the end of each function makes the output easier to read.

    # Then repeat the lines of code starting with the "# Describe ..." comment but
    # applied to each of the remaining 4 quantities.

⚠️ POINTS: 18

For each function, two points for correctly implementing it, and one for properly demonstrating that it is correct. So 3 pts per function x 6 functions.

Extra credit

There are a few ways to earn extra credit, each one up to 5% of the total:

Implement the interquartile range

Implement the interquartile range. Unlike the others, here you are permitted to use something from another package, specifically the percentile function from numpy.

Compute your functions on the Central Park dataset

Show that your functions also give the right answers for the Central Park daily average temperature dataset. You won’t be able to compute the answers by hand for this, but you can just copy-paste in the values I provide in the corresponding lecture notes / slides.

(Even more bonus points if, rather than copy-pasting them, you explicitly compute these “reference values” yourself using built-in versions of each function in numpy, scipy, xarray, or any other package of your choosing. If you do this, for efficiency’s sake when I go to run these, please place the dataset netCDF file itself in the same directory as the script, and load it in using just its filename rather than a complete path, which will be different on your machine than on mine.)

✅ ANSWERS

The 6 functions, plus the IQR extra credit

import numpy as np
import xarray as xr
import scipy


def sum(x):
    """Adds the elements in a 1d array"""
    sum = 0
    for y in x:
        sum += y
    return sum


def mean(x):
    """Computes the average of elements in a 1d array using the sum function"""
    return sum(x) / len(x)


def median(x):
    """Computes the median of a 1d array using indexing"""
    len_x = len(x)
    x_sorted = sorted(x)
    is_even = len_x % 2 == 0
    if is_even:
        val1 = x_sorted[len_x // 2 - 1]
        val2 = x_sorted[len_x // 2]
        return 0.5 * (val1 + val2)
    else:
        return x_sorted[len(x) // 2]


def stats_range(x):
    """Computes the range max-min using built in functions"""
    return max(x) - min(x)


def variance(x):
    """Computes the variance with a for loop"""
    x_mean = mean(x)
    total = []
    for x_val in x:
        total.append((x_val - x_mean)**2)
    return sum(total) / len(x)


def stdev(x):
    """Square root of variance"""
    return variance(x) ** (1/2)


def coef_var(x):
    """Ratio of standard deviation to mean"""
    return stdev(x) / mean(x)


def iqr(x):
    """Interquartile range 25 to 75"""
    return np.percentile(x, 75) - np.percentile(x, 25)
# Define the toy array we'll test the functions against.
arr_test = [4.0, 1.0, 10.0, 2.0, 4.]
# Print out its values to make sure you copy/pasted it correctly from the HW.
print(f"Toy dataset is: {arr_test}")

# Start with the first measure to be calculated.
# Describe the measure conceptually in words, and report the answer
# that it should produce when computed on the toy array, which you can
# compute by hand or at most using a simple calculator.
print()  # blank line for easier readability
print("Function #1 is the average: add all elements of the array.")
ans_average = 4.2
print(f"We can add those up without a computer or calculator; the answer is {ans_average}.")

# Now call your function on the toy array, and print out the result.
average_toy = mean(arr_test)
print(f"Calling `mean` on the toy dataset.  Answer: {ans_average}")

# Finally, print out the difference between what you expect the answer to be
# and what the actual function computes (this should be zero or *very* close to it).
diff_average = ans_average - average_toy
print(f"Difference between expected and actual: {diff_average}")
print()  # A blank line at the end of each function makes the output easier to read.

# Then repeat the lines of code starting with the "# Describe ..." comment but
# applied to each of the remaining 4 quantities.

########################################################
print()
print("Function #2 is the median: what is the middle value of the sorted array?.")
ans_median = 4
print(f" The answer is {ans_median}.")

median_toy = median(arr_test)
print(f"Calling `median` on the toy dataset.  Answer: {median_toy}")

diff_median = ans_median - median_toy
print(f"Difference between expected and actual: {diff_median}")
print()

########################################################
print()
print("Function #3 is the range: The largest possible difference between two values in the data set.")
ans_range = 9
print(f"The answer is {ans_range}.")

range_toy = stats_range(arr_test)
print(f"Calling `range` on the toy dataset.  Answer: {range_toy}")

diff_range = ans_range - range_toy
print(f"Difference between expected and actual: {diff_range}")
print()

########################################################
print()
print("Function #4 is the variance: a measure of the spread of the dataset.")
ans_var = 9.76

print(f"The answer is {ans_var :.2f}.")

variance_toy = variance(arr_test)
print(f"Calling `variance` on the toy dataset.  Answer: {variance_toy :.2f}")

diff_var = ans_var - variance_toy
print(f"Difference between expected and actual: {diff_var}")
print()

########################################################
print()
print("Function #5 is the standard deviation: the average square distance of a data point from the mean.")
ans_stdev = ans_var ** 0.5

print(f"The answer is {ans_stdev :.2f}.")

stdev_toy = stdev(arr_test)
print(f"Calling `standard deviation` on the toy dataset.  Answer: {stdev_toy :.2f}")

diff_stdev = ans_stdev - stdev_toy
print(f"Difference between expected and actual: {diff_var}")
print()

########################################################
print()
print("Function #6 is the coefficent of variation : the ratio of the standard deviation to the mean.")
ans_var = 0.7438333024673005
print(f"The answer is {ans_var :.2f}.")

variance_toy = coef_var(arr_test)
print(f"Calling `coef_var` on the toy dataset.  Answer: {variance_toy :.2f}")

diff_var = ans_var - variance_toy
print(f"Difference between expected and actual: {diff_var}")
print()

######################################################
print()
print('Extra Credit 1: IQR')
print()
print("Function #7 is the interquartile range : the difference between the 25th and 75th percentiles.")
ans_var = 2
print(f"The answer (scipy.stats.iqr) is {ans_var :.2f}.")

variance_toy = iqr(arr_test)
print(f"Calling `iqr` on the toy dataset.  Answer: {variance_toy :.2f}")

diff_var = ans_var - variance_toy
print(f"Difference between expected and actual: {diff_var}")
print()
Toy dataset is: [4.0, 1.0, 10.0, 2.0, 4.0]

Function #1 is the average: add all elements of the array.
We can add those up without a computer or calculator; the answer is 4.2.
Calling `mean` on the toy dataset.  Answer: 4.2
Difference between expected and actual: 0.0


Function #2 is the median: what is the middle value of the sorted array?.
 The answer is 4.
Calling `median` on the toy dataset.  Answer: 4.0
Difference between expected and actual: 0.0


Function #3 is the range: The largest possible difference between two values in the data set.
The answer is 9.
Calling `range` on the toy dataset.  Answer: 9.0
Difference between expected and actual: 0.0


Function #4 is the variance: a measure of the spread of the dataset.
The answer is 9.76.
Calling `variance` on the toy dataset.  Answer: 9.76
Difference between expected and actual: -1.7763568394002505e-15


Function #5 is the standard deviation: the average square distance of a data point from the mean.
The answer is 3.12.
Calling `standard deviation` on the toy dataset.  Answer: 3.12
Difference between expected and actual: -1.7763568394002505e-15


Function #6 is the coefficent of variation : the ratio of the standard deviation to the mean.
The answer is 0.74.
Calling `coef_var` on the toy dataset.  Answer: 0.74
Difference between expected and actual: 0.0


Extra Credit 1: IQR

Function #7 is the interquartile range : the difference between the 25th and 75th percentiles.
The answer (scipy.stats.iqr) is 2.00.
Calling `iqr` on the toy dataset.  Answer: 2.00
Difference between expected and actual: 0.0

Central Park data extra credit

######################################################
avg_t_cp = ds_cp["temp_avg"].values

park_mean = np.mean(avg_t_cp)
park_median = np.median(avg_t_cp)
park_range = np.ptp(avg_t_cp)
park_var = np.var(avg_t_cp)
park_stdev = np.std(avg_t_cp)
park_coef = np.std(avg_t_cp) / np.mean(avg_t_cp)
park_iqr = scipy.stats.iqr(avg_t_cp)

print()
print("Extra Credit 2")
print()
print('Testing functions on central park dataset')
print()
print('#############################################')
print('Function 1: Mean')
print(f"Expected {park_mean :.2f}")
print(f"Homemade function {mean(avg_t_cp) :.2f}")
print(f'Difference {park_mean - mean(avg_t_cp) :.2f}')
print('#############################################')
print('Function 2: Median')
print(f"Expected {park_median :.2f}")
print(f"Homemade function {median(avg_t_cp) :.2f}")
print(f'Difference {park_median - median(avg_t_cp) :.2f}')
print('#############################################')
print('Function 3: Range')
print(f"Expected {park_range :.2f}")
print(f"Homemade function {stats_range(avg_t_cp) :.2f}")
print(f'Difference {park_range - stats_range(avg_t_cp) :.2f}')
print('#############################################')
print('Function 4: Variance')
print(f"Expected {park_var :.2f}")
print(f"Homemade function {variance(avg_t_cp) :.2f}")
print(f'Difference {park_var - variance(avg_t_cp) :.2f}')
print('#############################################')
print('Function 5: Standard Deviation')
print(f"Expected {park_stdev :.2f}")
print(f"Homemade function {stdev(avg_t_cp) :.2f}")
print(f'Difference {park_stdev - stdev(avg_t_cp) :.2f}')
print('#############################################')
print('Function 6: Coefficent of Variation')
print(f"Expected {park_coef :.2f}")
print(f"Homemade function {coef_var(avg_t_cp) :.2f}")
print(f'Difference {park_coef - coef_var(avg_t_cp) :.2f}')
print('#############################################')
print('Function 7: interquartile Range')
print(f"Expected {park_iqr :.2f}")
print(f"Homemade function {iqr(avg_t_cp) :.2f}")
print(f'Difference {park_iqr - iqr(avg_t_cp) :.2f}')

Extra Credit 2

Testing functions on central park dataset

#############################################
Function 1: Mean
Expected 54.07
Homemade function 54.07
Difference 0.00
#############################################
Function 2: Median
Expected 55.00
Homemade function 55.00
Difference 0.00
#############################################
Function 3: Range
Expected 99.50
Homemade function 99.50
Difference 0.00
#############################################
Function 4: Variance
Expected 316.29
Homemade function 316.29
Difference 0.00
#############################################
Function 5: Standard Deviation
Expected 17.78
Homemade function 17.78
Difference 0.00
#############################################
Function 6: Coefficent of Variation
Expected 0.33
Homemade function 0.33
Difference 0.00
#############################################
Function 7: interquartile Range
Expected 29.50
Homemade function 29.50
Difference 0.00