HackerRank HackerRant: Finding the Mean, Median, and Mode in Python
Tags: python, beginners, tutorial, bestpractices
Published: 2019-10-06 04:38:27

![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/z0cs8myq4enz8cw44bej.png)

_[HackerRank](https://www.hackerrank.com/) is an excellent website to create code based on prompt challenges, prepare for coding interviews, search for jobs, and to see how the community has approached the solutions over time. The author wanted to dive into the Python focused solutions, and is in no way affiliated with HackerRank itself._

---

## The Challenge: Mean, Median, Mode

From [[10 Days of Statistics] Day 0: Mean, Median, and Mode](https://www.hackerrank.com/challenges/s10-basic-statistics/problem):

> **Output Format**
>
> Print  lines of output in the following order:
>
> - Print the mean on a new line, to a scale of  decimal place (i.e., , ).
> - Print the median on a new line, to a scale of  decimal place (i.e., , ).
> - Print the mode on a new line; if more than one such value exists, print the numerically smallest one.
>
> **Sample Input**
>```
>10
>64630 11735 14216 99233 14470 4978 73429 38120 51135 67060
>```
>
> **Sample Output**
>```
>43900.6
>44627.5
>4978
>```

[The top-voted Python 3 solution](https://www.hackerrank.com/challenges/s10-basic-statistics/forum/comments/173805) came out to be:

> _Python 3 - Dont reinvent the wheel ;)_
>```python
>import numpy as np
>from scipy import stats
>
>size = int(input())
>numbers = list(map(int, input().split()))
>print(np.mean(numbers))
>print(np.median(numbers))
>print(int(stats.mode(numbers)[0]))
>```

To those who have been introduced to Python via data science courses and tools, this may seem like the solution one is looking for. Though, this is _only_ the case if a project already includes the [SciPy](https://www.scipy.org/) package.

### Wait, Why Could This Be Bad Practice?

The **scipy** and **numpy** packages are third-party libraries, and they would have to be added to a `requirements.txt`, `setup.py`, or `Pipfile` in order to make use of them in a project. This adds complexity by piling onto the software supply chain.[^fn1]

Installing **scipy** (which includes installing **numpy** as a dependency) results in:

- Downloading ~45mb worth of files: >3000 files
- Introducing potential for vulnerabilities in a project
  - [scipy Vulnerabilities](https://snyk.io/vuln/pip:scipy)
  - [numpy Vulnerabilities](https://snyk.io/vuln/pip:numpy)

Just this year, **numpy** had an [Arbitrary Code Execution (ACE)](https://www.professormesser.com/security-plus/sy0-401/arbitrary-and-remote-code-execution/) vulnerability raised around how it was unpickling-by-default with `numpy.load`, which has since changed. The **pickle** module is known for this vulnerability risk, and has a big red warning about it in the Python docs.[^fn2]

Using these third-party packages is overkill for a project that doesn't already contain the libraries, unless you'd really like to be on the lookout for long [GitHub Issue conversations](https://github.com/numpy/numpy/issues/12759) and _Common Vulnerabilities and Exposures (CVE)_ database entries (such as [CVE-2019-6446](https://www.cvedetails.com/cve/CVE-2019-6446/) in this case) where you try to decipher how big a problem this is if it even is a problem at all.

## Using Standard Libraries

How can we solve this problem with standard libraries that come with Python?

```python
# With standard lib imports only
from statistics import mean, median

def basicstats(numbers):
    print(round(mean(numbers),1))
    print(median(numbers))
    print(max(sorted(numbers), key=numbers.count))

input() # Don't need array length, so ignore input
numbers = list(map(float, input().split()))
basicstats(numbers)
```

### UPDATE: Python 3.8 introduced `multimode()`

The [`multimode`](https://docs.python.org/3/library/statistics.html#statistics.multimode) function was added to the `statistics` standard library, in Python 3.8, less than a month after this blog post was made! This means that `statistics` has everything required, out-of-box.

```python
# With standard lib imports only
from statistics import mean, median, multimode

def basicstats(numbers):
    print(round(mean(numbers),1))
    print(median(numbers))
    print(min(multimode(numbers)))

input() # Don't need array length, so ignore input
numbers = list(map(float, input().split()))
basicstats(numbers)
```

The rest of this article is still valid when it comes to alternative paths, including why `mode` doesn't work for the solution, and what the code does.

### Detailed Code Breakdown

```python
from statistics import mean, median
```

- `statistics` has been included with Python 3 since Python 3.4 (released in 2014).
- We only want `mean` and `median` from this library, so we are explicitly importing each rather than importing the entire library.
- Why aren't we using `mode` from `statistics`? This is because `mode` will error-out in cases where: _"...if there is not exactly **one** most common value, `StatisticsError` is raised."[^fn3]_
  - This is a problem, due to the last requirement of the challenge for **mode** output: _"...if more than one such \[mode\] value exists, print the numerically smallest one."_

```python
input() # Don't need array length, so ignore input
numbers = list(map(float, input().split()))
```

- We do nothing with the first `input()`, which is meant to be a count of numbers being input in the second prompt. This is dropped because it is not needed in order to produce the mean, median, and mode output.
- For `numbers`, let's start from the inside-most parentheses and move outword:
  - `input().split()` breaks apart the single-string input into a list of strings, as `split()` defaults to whitespace as the _sep_ delimiter: _"If **sep** is not specified or is `None`, a different splitting algorithm is applied: runs of consecutive whitespace are regarded as a single separator, and the result will contain no empty strings at the start or end if the string has leading or trailing whitespace. Consequently, splitting an empty string or a string consisting of just whitespace with a `None` separator returns `[]`."[^fn4]_
  - `map(float, input().split())`: Here, `map()` is being used to convert the resulting list of strings into **float** type values.
  - `list(map(...))`: The reason we need to convert the _map_ back into a _list_ is because `map()` returns an _iterator_. This means we can only call the elements within it _once_. If all we wanted was the median, for example, we wouldn't need to convert the _map_ to a _list_ type because we may not care about the values anymore after the median is returned.

> ***NOTE:*** _Instead of `list(map(...))` , we could use a **list comprehension**[^fn5] like so:_
>```python
>numbers = [float(number) for number in input().split()]
>```
> _This is argued as a better approach on StackOverflow,[^fn6] and if you are up for an interesting side note of history, you can read about how `map()` was nearly removed from Python 3 at one point.[^fn7]_

After we have our list of floats, `basicstats(numbers)` is called, running the following:

```python
def basicstats(numbers):
    print(round(mean(numbers),1))
    print(median(numbers))
    print(max(sorted(numbers), key=numbers.count))
```

- `print(round(mean(numbers), 1))` from the inside-most parentheses and move outword to see what we are printing out:
  - `mean(numbers)`: Simply returns the **mean** without a third-party package!
  - `round(mean(numbers), 1)` rounds the resulting float to one number after the decimal point (per requirements).
- `print(median(numbers))`: Simply returns the **median** without a third-party package!
- `print(max(sorted(numbers), key=numbers.count))`: how is this providing the **mode**?
  - `sorted(numbers)`: First, we need the list sorted as we are only meant to return the lowest-value mode if their is more than one value. This is needed for `max(...)` to properly return the lowest value we want.
  - `max(sorted(numbers), key=numbers.count))`: Providing `key=numbers.count` as an arg is ensuring we get the value with the highest count within the list. `max()` only returns a single value, so it will return the first value, being the lowest in the event that there is a draw (due to use using `sorted(numbers)`).

#### Optional Approach to Retrieving Mode: Using `Counter()`

Instead of `max()`, we could alternately use `Counter()`[^fn8] from `collections`, which is argued to be a better approach to this problem.[^fn9] **Counter()** was added to the **collections** module way back with Python 2.7.0 (released in 2010):

```python
# With standard lib imports only
from statistics import mean, median
from collections import Counter

def basicstats(numbers):
    print(round(mean(numbers),1))
    print(median(numbers))
    # Optional approach to 'mode'
    print(Counter(sorted(numbers)).most_common(1)[0][0])

input() # Don't need array length, so ignore input
numbers = list(map(float, input().split()))
basicstats(numbers)
```

- `Counter(sorted(numbers)).most_common(1)[0][0]` working from the inside, out:
  - `sorted(numbers)` is needs for the later call of `most_common()` to return the _lowest_ mode.
  - `Counter(...)`: Creates a dictionary with count values of all elements in the list.
  - `Counter(...).most_common(1)`: Returns a _list_ of _tuples_. Using `1` as an arg means it returns only one _tuple_, being the _first_ value that appears the most often.
  - `Counter(...).most_common(1)[0][0]`: The first `[0]` means we are calling the _tuple_ in the `0` index position of the _list_, with the `[0]` calling the `0` index value of that _tuple_.

## Conclusion

There are many ways to come to a solution, and depending on the situation, some are better than others. If packages like **scipy** and/or **numpy** are already included within a project, it certainly makes sense to use them.

Though, it is a great idea to take a look at whether built-in or standard libraries can solve a problem before looking into third-party solutions. This helps you:

- Learn what Python is capable of out-of-the-box
- Make your code more portable for use in other projects without installing additional resources
- Reduce the security complexity of the software supply chain[^fn1] by avoiding unnecessary inclusion of third-party packages

Was this helpful? Have thoughts to add? Please add to the conversation below!

---

_Originally published at [https://icanteven.io](https://www.icanteven.io/blog/hackerrant-stats-1/) on October 5th, 2019_

[^fn1]: [Software Supply Chain: Fewer, Better Suppliers.](https://www.devsecops.org/blog/tag/Software+Supply+Chain) Written by Shannon Lietz @ [DevSecOps](https://twitter.com/devsecops), 2016
[^fn2]: [Python 3 `pickle` Docs](https://docs.python.org/3/library/pickle.html)
[^fn3]: [Python 3 `statistics` Docs](https://docs.python.org/3.4/library/statistics.html#statistics.mode)
[^fn4]: [Python 3 `str.split()` Docs](https://docs.python.org/2/library/stdtypes.html#str.split)
[^fn5]: [Comprehending Python's Comprehensions.](https://dbader.org/blog/list-dict-set-comprehensions-in-python) Written by [Dan Bader](https://twitter.com/dbader_org) @ dbader.org
[^fn6]: [StackOverflow: List Comprehension vs. Map](https://stackoverflow.com/questions/1247486/list-comprehension-vs-map)
[^fn7]: [The Fate of `reduce()` in Python 3.](https://www.artima.com/weblogs/viewpost.jsp?thread=98196) Written by [Guido van Rossum](https://twitter.com/gvanrossum), 2005. _NOTE: He's the creator, and previous [BDFL](https://www.linuxjournal.com/content/guido-van-rossum-stepping-down-role-pythons-benevolent-dictator-life), of Python. The article includes thoughts on **map()**, **filter()**, and **lambda**._
[^fn8]: [Python 3 `collections.Counter()` Docs](https://docs.python.org/3/library/collections.html#collections.Counter)
[^fn9]: [StackOverflow: Python - Find The Item with Maximum Occurrences in A List.](https://stackoverflow.com/questions/6987285/python-find-the-item-with-maximum-occurrences-in-a-list)