⇦ Back

A t-test can be used when continuous data has been measured - for example height in cm or weight in kg - as opposed to binary pass/fail results or qualitative judgements

For the one-sample t-test the data in question will be a single sample of measurements and the statistical question will, roughly, ask if the mean of this sample is the same as the mean of the population as a whole. In general, it can be used if the population of data from which the sample is drawn is normally distributed, or if it is not normally distributed but the sample size is large.

The code on this page uses the numpy, matplotlib and scipy packages. These can be installed from the terminal with:

$ python3.11 -m pip install numpy
$ python3.11 -m pip install matplotlib
$ python3.11 -m pip install scipy

where python3.11 corresponds to the version of Python you have installed and are using.

1 Example Data

Let’s start with some example data. Create a series of 20 numbers distributed normally about a mean value of 100 and with a standard deviation of 5:

import numpy as np

# Set a seed for the random number generator so we get the same random numbers each time
np.random.seed(20210725)

# Create fake data
mean = 100
standard_deviation = 5
sample_size = 20
x = np.random.normal(mean, standard_deviation, sample_size)

print([f'{x:.1f}' for x in sorted(x)])
## ['92.2', '93.0', '95.4', '95.5', '97.0', '97.4', '98.6', '100.5', '100.6', '101.3', '101.3', '102.4', '102.6', '103.4', '104.1', '104.1', '104.2', '104.2', '104.3', '107.4']

This is what they look like on a number line:

import matplotlib.pyplot as plt

# Formatting options for plots
A = 6  # Want figure to be A6
plt.rc('figure', figsize=[46.82 * .5**(.5 * A), 33.11 * .5**(.5 * A) * 0.3])
plt.rc('text', usetex=True)
plt.rc('font', family='serif')
plt.rc('text.latex', preamble=r'\usepackage{textgreek}')

#
# Plot
#
ax = plt.axes()
# Add jitter to separate the points out
y = np.ones(len(x)) + np.random.uniform(-0.2, 0.2, size=len(x))
# Create scatter plot
ax.scatter(x, y, s=10)
ax.set_title('Example Data: 20 Random Measurements')
# Remove axes
ax.spines['right'].set_visible(False)
ax.spines['top'].set_visible(False)
ax.spines['left'].set_visible(False)
# Add arrows on x-axis
ax.arrow(100, 0, 11.5, 0, head_width=0.2, color='k', clip_on=False)
ax.arrow(100, 0, -11.5, 0, head_width=0.2, color='k', clip_on=False)
# Axes' settings
ax.set_ylim(0, 2)
ax.set_xlim(88.5, 111.5)
ax.tick_params(axis='y', left=False, labelleft=False)
# Finish
plt.subplots_adjust(left=0.1, bottom=0.2, right=0.9, top=0.8)
plt.show()
plt.close()

2 Descriptive Statistics

In addition to creating a plot, we should calculate ‘descriptive statistics’ to help describe the data:

# Sample size
n = len(x)
# Mean
x_bar = np.mean(x)
# Sample standard deviation
s = np.std(x, ddof=1)
# Standard error of the mean
sem = s / np.sqrt(n)

print(f'Sample size = {n}, mean = {x_bar:.2f}, sample std deviation = {s:.2f}, std error of the mean = {sem:.2f}')
## Sample size = 20, mean = 100.47, sample std deviation = 4.19, std error of the mean = 0.94

3 One-Sample t-Tests

The Scipy package has a ‘stats’ sub-package which contains the ttest_1samp() function. This performs t-tests with one sample. It has two compulsory inputs:

  • The sample observation (ie your data; the numbers you have measured)
  • The population mean (ie the expected value you want to test the mean of the sample against)

There are two values returned by this function:

  • The t-statistic, which can be thought of as the ‘result’ of the test
  • The p-value, which can be thought of as the odds that that result happened

3.1 Two-Sided Tests

By default, the null hypothesis used by the ttest_1samp() function is that the expected value (mean) of the sample of independent observations (the numbers in the first input) is equal to the given population mean (the second input to the function). In other words, it is a two-sided test; the null hypothesis is that the population mean is neither greater than nor less than the sample mean.

Of course, because we have artificially created this example data, we know that the expected value of a sample is 100 (because we’ve used a distribution that has a mean of 100). However, for the sake of this tutorial, let’s test to see whether it’s equal to 98.5:

from scipy import stats

# The population mean
popmean = 98.5
# Perform a two-sided one-sample t-test
statistic, pvalue = stats.ttest_1samp(x, popmean)

print(f'p = {pvalue:5.3f}')
## p = 0.049

This low p-value tells us that there is a small (less than 5%) chance that this sample was drawn randomly from a population with a mean of 98.5. If we had decided to use a 95% confidence level as our acceptance criteria, we would have rejected the null hypothesis after seeing this result.

3.2 One-Sided Tests

The two-sided test we did above had the alternative hypothesis that the mean of the population from which our sample was drawn and the given population mean (98.5) were not equal. However, we can also do a one-sided test where the alternative hypothesis is that the true population mean is greater than the given population mean by using the alternative='greater' keyword argument:

statistic, pvalue = stats.ttest_1samp(x, 98.5, alternative='greater')

print(f'p = {pvalue:5.3f}')
## p = 0.024

We are very unsure (only 2.4% confident) that the null hypothesis is true and that the true mean of the population from which our sample was drawn is less than 98.5 (which is good - remember that the true mean is 100!).

Conversely, we could have done the test the other way around, testing the alternative hypothesis that the true mean is less than the given population mean:

statistic, pvalue = stats.ttest_1samp(x, 98.5, alternative='less')

print(f'p = {pvalue:5.3f}')
## p = 0.976

We are now very sure that the null hypothesis is correct and that the true population mean is greater than the given population mean of 98.5!

In summary:

  • Two-sided test:
    • Null hypothesis: true population mean is equal to the given population mean
    • Alternative hypothesis: true population mean is not equal to the given population mean
  • One-side, greater than test:
    • Null hypothesis: true population mean is not greater than the given population mean
    • Alternative hypothesis: true population mean is greater than the given population mean
  • One-side, less than test:
    • Null hypothesis: true population mean is not less than the given population mean
    • Alternative hypothesis: true population mean is less than the given population mean

4 Two-Sided vs Greater vs Less

You might not have noticed it, but the results of the three t-tests we did above are related:

  • The p-value of the two-sided test was double that of the one-sided, greater than test
  • The p-value of the one-sided, greater than test plus the p-value of the one-sided, less than test added to 1
  • The p-value of the one-sided, less than test added to half the p-value of the two-sided test gives a value of 1

These are not coincidences: the two-sided test asks if the population mean is neither less than nor greater than the sample mean, while the one-sided, greater than test only asks if the population mean is not less than the sample mean. It only asks half the questions, so we can be doubly sure of the result! Likewise, the one-sided, less than test asks if the population mean is not greater than the sample mean…and surely the chance of not being greater than something and the chance of not being less than something should add to 1 (ie you’re 100% sure that one of them is true!). But don’t take my word for it, let’s check:

from scipy import stats

# Two-sided test
statistic, pvalue_twosided = stats.ttest_1samp(x, 98.5)

# One-sided, greater than test
statistic, pvalue_greater = stats.ttest_1samp(x, 98.5, alternative='greater')

# One-sided, less than test
statistic, pvalue_less = stats.ttest_1samp(x, 98.5, alternative='less')

print(pvalue_twosided == pvalue_greater * 2)
## True
print(pvalue_greater + pvalue_less == 1)
## True
print(pvalue_less + pvalue_twosided / 2 == 1)
## True

As promised, all three statements were true.

5 Confidence Intervals vs the One-Sample t-Test

95% confidence intervals for the value of the mean of the sample can be calculated as follows:

# Confidence level
alpha = 0.05  # 95% confidence
# Degrees of freedom
dof = n - 1
# Percent-point function (aka quantile function) of the t-distribution
t = stats.t.ppf(1 - (alpha / 2), dof)
# Margin of error
d = t * sem
# Intervals
upper_ci = x_bar + d
lower_ci = x_bar - d

print(f'Sample mean = {x_bar:.2f}, 95% CI [{lower_ci:.2f}, {upper_ci:.2f}]')
## Sample mean = 100.47, 95% CI [98.51, 102.43]

Put another way, the conclusion is:

print(f'You can be 95% confident that the range {lower_ci:.2f} to {upper_ci:.2f} contains the true population mean')
## You can be 95% confident that the range 98.51 to 102.43 contains the true population mean

This implies that 98.5 is just outside the range of numbers we expect to contain the true population mean and so we can say, with 95% certainty, that the true population mean and 98.5 are not equal. But hang on, was that not the conclusion of the two-sided t-test we did? And given that we were just on the cusp of making the opposite conclusion there (our p-value for that test was 0.049), is 98.5 being on the cusp of the above confidence interval a fluke? It turns out…no. The question of whether a given number lies within the 95% confidence interval of a sample mean is mathematically the same as performing a two-sided one-sample t-test if all the assumptions are the same. To check this, let’s do a test with the number 98.52, which lies just within the confidence interval and so, if we’re right, the t-test should just not reach significance:

# The population mean
popmean = 98.52
# Perform a two-sided one-sample t-test
statistic, pvalue = stats.ttest_1samp(x, popmean)

print(f'p = {pvalue:5.3f}')
## p = 0.051

As suspected, p > 0.05.

⇦ Back