Python For Sport Scientists: Descriptive Statistics | by John Cothran | Towards Data Science

Python has been gaining a tremendous amount of popularity over the past few years, and is the language of choice for many data scientists across the world. It is no accident that it is also gaining popularity for sport scientists, who have to work with a lot of data on a day-to-day basis.
While this article may benefit sport scientists who are beginners in Python, it is not an introduction to the language itself. Rather it is an introduction to how to reason about solving problems and analyzing data in a more functional way.
Furthermore, this article will use basic descriptive statistics to illustrate how to build composable functional Python code to solve problems.
In the first section, I’ll introduce two basic calculations of central tendency: the mean and the median. The second part applies the same principles to calculating standard deviation and variance. The last part introduces bivariate analysis by calculating the relationship between two variables.
Sport scientists are consistently working with datasets about athletes, and it is useful to be able to call mean on a list of data points to gain a general insight about it. In the case of datasets with outliers (we are working with athletes after all), we may need to use median to find out the 50th percentile.
While there are statistical libraries for Python to import these functions, I believe it can be extremely helpful to work through them to build the foundation to solve more complex problems later. When we get to composable functions like Sum of Squares, or Mean Squared Error, this will make more sense.
Let’s begin by creating a .py file and define the function mean. The following is a pure function that returns the mean:
# Python 3def mean (x):  return sum(x) / len(x)
Notice that sum() and len() are functions native to Python that return the sum and the length of a python data structure. The mean function here is a simple pure function, meaning that it has no side-effects (it always returns the same result given the same input.)
Calculating the median is slightly more complex than the mean, but simple enough to think through. Finding the median of a list of numbers hinges on finding half the length of a sorted list: half = lambda arr: len(arr) / 2. However, this function has two problems assuming we are going to use it to find the index (which starts at 0.) The first problem is that it returns a float, and we need an integer to find the value at an index. The second problem is that we want it to return the first of the middle two indexes if the length of numbers is even. We can solve this by using the int() function. Let’s add this to our file:
The half() function now returns the middle index (or in the case of an even list, the first of two middle indexes):
evenList = [11, 12, 13, 14]oddList = [21, 22, 23]half(evenList)# 1half(oddList)# 1
The next step in defining the median is to return the value at that index. If the length is even, we need to return the average of the arr[half(arr)] and arr[half(arr) + 1] values to get the median. So let’s finally write the median function and add it to our file:
def median (arr):    sortedList = sorted(arr)    index = half(sortedList)    if isEven(len(sortedList)):        return mean([ sortedList[index], sortedList[index + 1] ])    else:        return sortedList[index]
Voila! Calling median(evenList) from above will return 12.5, and calling median(oddList) will return 22.
This is pretty cool because we got to use that mean() function that we just defined! This is the most fun part about programming with functions — they serve as simple building blocks that can be build to tackle any problem. Furthermore, notice we had to use the native sorted() function to return a sorted copy of the dataset before operating on it, which is vital to getting the correct median value.
Let’s use these two functions in a somewhat practical example. I’ll use a basic dataset that contains some GPS data from a training session:
data = [  {"name": "John",  "distance": 5602,  "high-speed-running": 504},  {"name": "Mike",  "distance": 5242,  "high-speed-running": 622},  {"name": "Chad",  "distance": 4825,  "high-speed-running": 453},  {"name": "Phil",  "distance": 611,   "high-speed-running": 500},  {"name": "Tyler", "distance": 5436,  "high-speed-running": 409}]
This is a list of dictionaries, so we can’t just call mean(data[“distance”]) as it stands. Let’s import the handypandas library at the top of the file to transform the dataset into a Pandas DataFrame, so we can simply call mean on elements of the dataset:
Now we can call mean(df['distance']) to return 4343.2. Calling median(df[‘distance’]) returns 5242.
Something to think about: in this specific dataset, why might we want to use the median function rather than the mean in our analysis? Or vice-versa.
Standard deviation and variance are closely related to each other. For example, variance is defined as the square of the standard deviation (or standard deviation is the square root of the variance). With composable functions, this is almost too easy!
We can start with variance, since all we will have to do is to square it to get the standard deviation. The mean function we defined in Part 1 will also come in handy, as well as Python’s native len function.
Looking at the equation for sample variance and considering that we will be squaring it to get the standard deviation, it seems like a good idea to go ahead and define a function that squares a value:
def square(x):    return x * x
The next bit is slightly trickier, but we can evaluate the numerator with a function called, say sumOfSquaredDifferences:
def sumOfSquaredDifferences (arr):    xBar = mean(arr)    differences = map(lambda x: x - xBar, arr)    squaredDifferences = map(square, differences)    return sum(squaredDifferences)sprintEfforts = [88, 56, 51, 34, 50, 22, 61, 79, 90, 49]sumOfSquaredDifferences(sprintEfforts)# 4444
This function essentially evaluates the differences of each of the values and the mean (using the higher-order map function), then squares the differences and finds the sum of the squared differences. Simple enough!
Now finding the sample variance is easy:
def variance (arr):    n = len(arr)    return sumOfSquaredDifferences(arr) / (n-1)variance(sprintEfforts)# 493.7
The variance is great an all, but we would like a measure that is a little more familiar to us. Luckily, calculating Standard Deviation from Variance is a breeze, since it is simply the square root of Variance:
def sqrt (x):    return x**(1/2)def stDev (arr):    return sqrt(variance(arr))stDev(sprintEfforts)# 22.2
Other than Python’s wacky syntax for the power operator (**), this is super easy. Let’s apply it to the dataset of GPS scores from Part 1:
stDev(df['high-speed-running'])# 79.62
The updated file from Part 1 should now look like this:
Combining small, pure functions that have a single purpose is proving to be useful in bringing basic statistical concepts to life. These statistical calculations are perfect for practicing good programming principles.
We’ve used map quite a bit, along with lambda functions. These are foundational for working with lists of data. It is a good idea to try to master and understand how to use higher-order-functions like map, filter, and reduce in any language in order to most effectively build composable, functional code.
So far we have been defining functions of univariate analysis, which have been useful for demonstrating working with single arrays of data.
Now we can apply the same logic to undertake bivariate analysis by exploring correlation.
The number one rule of working with a dataset is to plot the data to see what we’re working with. For simplicity’s sake, let’s use import seaborn as sns for creating the plot:
The dataset is based off of the fictitious dataset used in Parts 1 and 2. However, if those were representative of one training session, this dataset can be weekly totals of the same metrics. This code will produce the following plot:
Cool! Right off the bat we can tell that there is a strong positive linear relationship between ‘distance’ and ‘high-speed-running.’
The next step is to put a number to it by finding the Pearson Correlation Coefficient, which can be represented by the following formula:
Let’s break this up into its parts. We’re going to need count for n, sum for Σ, sqrt, and square. Furthermore, we will need to map through each x and y index and multiply them, as well as square them. Lets start with by adding n to our file using count:
# Python3def n (arr):  return arr.count()
The next step is to map through each ‘x’ and ‘y’ pair and return a sum of their products:
def product (x, y):  return x * ydef sumXTimesY (x, y):  return sum(map(product, x, y))
Now we can easily define the numerator:
def numerator (x, y):  return n(x) * sumXTimesY(x, y) - sum(x) * sum(y)
The denominator looks a little trickier, and will need to be broken up into its smallest components. Let’s define sumOfSquares:
def sumOfSquares (val):  return sum(map(square, val))
Now we have all the tools to define the denominator with the help of our sqrt function from Part 2:
def denominator (x, y):  return sqrt((n(x) * sumOfSquares(x) - square(sum(x))) * (n(y) * sumOfSquares(y) - square(sum(y))))
We can now put it all together in our final version of the file:
Not too shabby! Not only do we see a very strong relationship between ‘distance’ and ‘high-speed-running’ at an r of 0.88, but we have put it all together using functional and composable code that is easy to reason about. Furthermore, breaking apart this Pearson r formula in this way creates a deeper understanding of what exactly a relationship between two variables really looks like.
It is worth reiterating that using libraries that do the statistical heavy lifting is the way to go, however the point of this article is to work on using simple functional concepts to solve complex problems.
This article introduced two libraries that are very powerful for data visualization and analysis: Seaborn and Pandas. Seaborn is a statistical visualization library that is based on Matplotlib. It is my go-to library for high-level statistical plotting due to its simplicity and flexibility. Pandas is a wonderful data analysis toolkit that will be very attractive to Sport Scientists who come from a background of working with spreadsheets.
My goal with this article was to provide Sport Scientists with the tools to build a functional code-base, and to understand how to approach analyzing datasets in a modular and functional way.