Descriptive Statistics

When data contain thousands, if not millions, of observations, we need methods to summarize the data.

As a working example, consider the list of values below.

x = [1, 38, 4, 3, 7, -10, 7, 2, 5]

Mean and Variance

The mean of the data is the average value in the data. Within the sample of observations included in the data, the mean is the expected value of the data. It tells us, on average, what we’d expect to observe if we picked an observation at random. For data \(x\), the mean is sometimes written \(E(x)\) (the \(E\) stands for expected value).

import numpy as np
np.mean(x)
6.333333333333333

Naturally, not all observations are exactly equal to the mean value. Thus, beyond summarizing what the average value of the data is, we want some sense of how much observations deviate from the expected value. For a list of observations, \(x\), we can calculate the deviation of each of these observations from the mean by taking \(x-E(x)\).

x - np.mean(x)
array([ -5.33333333,  31.66666667,  -2.33333333,  -3.33333333,
         0.66666667, -16.33333333,   0.66666667,  -4.33333333,
        -1.33333333])

Now, the average deviation from the average is zero. So simply taking the mean of these deviations isn’t useful.

np.mean( x - np.mean(x) )
1.0855514018557087e-15

A trick is the square each deviation first, and then take the average of those squared deviations.

np.mean( (x - np.mean(x))**2 )
148.44444444444446

This is what’s called the variance of the data. Putting the above line of code into math, the variance is computed as \(E\bigg( \big(x-E(x)\big)^2 \bigg)\).

np.var(x)
148.44444444444446

Now, the variance in the example above looks quite large. Often, squaring things makes them bigger, so it makes sense that the average of squared numbers looks large. To summare how much observations tend to deviate away from the mean, an alternative to calculating the variance is to calculate the standard deviation, which is just the square root of the variance. This doesn’t completely un-do the earlier squaring that is included in the variance computation, but it does tend to yield a smaller number. The formal statement of a standard deviation is \(\Bigg[E\bigg(\big( x - E(x)\big)^2 \bigg)\Bigg]^{1/2}\).

np.sqrt(np.var(x))
12.18377792166471
np.std(x)
12.18377792166471

Median and Skewness

When we have a list of numbers, the .sort() command will sort that list in place. Performing an operation in place is the alternative to returning a value. For example, if we type the command 'LSU'.lower(), Python will return a copy of the string 'LSU' in lower case characters. Thus if we have s = 'LSU' and do s.lower(), Python returns the value 'lsu’, but the string s still equals 'LSU'. In contrast, x.sort() will not return anything. Rather, the list x is modified in place so that its elements get reordered from smallest to largest. We’ve already seen at least one other example of an in place command: .append().

x.sort()
print(x)
[-10, 1, 2, 3, 4, 5, 7, 7, 38]

Sorting, at least for small lists of numbers, makes it easy to pick out the middle number. The middle of a set of a numbers is called the median.

np.median(x)
4.0

If the set has an even number of elements, there isn’t a precise middle element. The default is then to take the average of the two middle numbers.

np.median(x[:8])
3.5

The mean is a good bit higher than the median. Why?

There is skewness in the distribution of numbers. That means that the set of numbers has some outliers out to one side or the other. Here, we have positive skewness, which means that there are some big outliers out to the right side.

Formally, a calculation for skewness is given in the scipy module. We won’t worry too much about it here.

import scipy.stats
scipy.stats.skew(x)
1.6656502035844625

Outliers, Kernel Density, and Box Plots

Means and medians tell us a little bit about the data at around the middle of the distribution. Likewise variance and skewness tell us something about the spread of the distribution (how wide it is and how much it leans to one side or the other).

These numbers, while extraordinarily useful for a variety of applications, are still sometimes difficult to digest. Below, we’ll see the wisdom of the old adage ‘’a picture is worth a thousand words.’’

For this analysis, we’ll need more data:

y = [i for i in range(100)] # start by setting y equal to a list of numbers 1,..,99
y = y + [200] # add the number 1000 to the list, this number is an *outlier*
y = y + [30, 30, 30, 30, 30] # add a few copies of the number 30 to the list, so that it's the most frequently ocurring number

When operating in Jupyter notebooks, we gain access to a special module that isn’t useful outside of a notebook environment (e.g. if you ran Python from your terminal/command window, this module would not work). The module’s name is seaborn.

Like numpy (usually abbreviated as np) and pandas (usually abbreviated as pd) the seaborn module has the following cononical import statement.

import seaborn as sns

The first tool to explore is the kernel density estimate, which plots the distribution of numbers in our data.

sns.kdeplot(y)
<AxesSubplot:ylabel='Density'>
../_images/6_descriptives_29_1.png

Note that the kernel density estimate is not good at the end points. For mathematical reasons, a kernel density estimate is not good at end points. Other aspects of the plot are consistent with expectations. The density is fairly level over 0 to 99, with a slight peak at around 30. There is also a little bump out at 200 where we placed an outlier.

np.median(y)
47.5
np.mean(y)
50.0

We again have a case of positive skewness (200 is a big outlier), and thus the mean is bigger than the median.

Knowledge about how these descriptive statics characterize the data is important in financial analytics and modeling. For instance, consider a business looking at building a new grocery store in a county. The expected cash flows for that project are more closely related to the median of county income, rather than the mean of county income. Why? Hint: county income has positive skewness.

As an alternative the the kernel density estimate, consder the boxplot below.

sns.boxplot(y)
/mnt/software/anaconda3/lib/python3.8/site-packages/seaborn/_decorators.py:36: FutureWarning: Pass the following variable as a keyword arg: x. From version 0.12, the only valid positional argument will be `data`, and passing other arguments without an explicit keyword will result in an error or misinterpretation.
  warnings.warn(
<AxesSubplot:>
../_images/6_descriptives_34_2.png

The box (the shaded region between roughly 25 and 75) tells us where the middle 50% of the data is. Thus, about 50% of the data falls between 25 and 75, with another 25% of the data on either side of this box.

The horizontal lines extending from either side of the box and terminating with a vertical line are called the whiskers. These give a sense of where approximately the other 50% of the data falls (again, 25% on each side of the box), save for a few outliers. Above, we see that the whiskers extend out to zero on the left and one hundered on the right. Data that do not fall within these ranges are referred to as outliers, and are plotted as individual points (e.g. the number 200 on the far right).

Outlier identification is a good way to assess data quality.

import pandas as pd
data = pd.read_csv('https://raw.githubusercontent.com/learning-fintech/data/master/stocks/all_stocks_5yr.csv.gz')

data.head()
date open high low close volume Name ret Mkt SMB HML RMW CMA RF
0 2013-02-08 45.07 45.35 45.00 45.08 1824755 A NaN 0.58 0.03 -0.21 0.12 -0.45 0.0
1 2013-02-11 45.17 45.18 44.45 44.60 2915405 A -0.010648 -0.08 -0.03 0.39 -0.18 0.28 0.0
2 2013-02-12 44.81 44.95 44.50 44.62 2373731 A 0.000448 0.16 0.26 0.71 -0.54 0.45 0.0
3 2013-02-13 44.81 45.24 44.68 44.75 2052338 A 0.002913 0.14 0.08 -0.01 -0.01 -0.04 0.0
4 2013-02-14 44.72 44.78 44.36 44.58 3826245 A -0.003799 0.11 0.32 0.12 -0.30 -0.21 0.0

An example using real data.

data[['ret','close']].describe()
ret close
count 618535.000000 619040.000000
mean 0.000541 83.043763
std 0.016060 97.389748
min -0.626183 1.590000
25% -0.006793 40.245000
50% 0.000635 62.620000
75% 0.008105 94.410000
max 1.009513 2049.000000

Note that there is no information about skewness.

sns.displot(data['ret'], height=8)
<seaborn.axisgrid.FacetGrid at 0x7f44bfec2700>
../_images/6_descriptives_40_1.png
sns.displot(data['close'], height=8)
<seaborn.axisgrid.FacetGrid at 0x7f44bcf03cd0>
../_images/6_descriptives_41_1.png