14  Bridging the Gap: The Point Biserial Correlation

The point biserial correlation is a statistical measure that quantifies the relationship between a continuous variable and a dichotomous variable. It is a special case of the Pearson product moment correlation coefficient, which is used to measure the strength and direction of the linear relationship between two continuous variables. Why is this so important? The point biserial correlation (\(r_{pb}\)) is the key to bridging the gap between continuous and dichotomous variables - a gap caused by the shift from one GLM model (MRC with continuous) to another (ANOVA with discrete or categorical predictors).

14.1 Learning Objectives

  1. Understand the concept of the point biserial correlation.
  2. Learn how to calculate the point biserial correlation.
  3. Understand how to interpret the point biserial correlation.
  4. Learn how MRC and ANOVA are fundamentally the same model.

14.2 The Trick: A Dichotomy Is Just 0 and 1

Here is the idea that makes this simple. A dichotomous variable (passed/failed, treatment/control, yes/no) looks categorical, but if we simply code it 0 and 1, we can drop it straight into a Pearson correlation as if it were a number. That is all the point-biserial correlation is: an ordinary Pearson \(r\) where one of the two variables happens to be a 0/1 indicator.

NoteWorking in SPSS, Julia, or Python?

The code tabs below assume this chapter’s data is already loaded. Grab the one-file setup for your language from Getting the Book’s Data, run it once, then load what you need by name - this chapter uses ch13-study. For example, book_data("ch13-study") in R, Julia, or Python, or !bookdata name = "ch13-study". in SPSS. Every language reads the same shipped files, so your numbers will match the ones printed here exactly.

library(tidyverse)    # dplyr, ggplot2, purrr, tibble, readr, stringr, forcats
library(broom)        # tidy(), glance(), augment() for model output
source("_common.R")   # book-wide helpers: round2(), fmt_p(), tidy2()

set.seed(14)
d <- tibble(studied = rbinom(50, 1, 0.5)) |>          # 0 = no study, 1 = studied
  mutate(score = 70 + 8 * studied + rnorm(50, 0, 6))  # studiers score higher

d |>
  summarise(
    point_biserial = cor(score, studied),   # point-biserial: just Pearson r!
    # and the same number even if the dichotomy is stored as a factor
    same_thing     = cor(score, as.numeric(factor(studied)) - 1)
  ) |>
  round2()
point_biserial same_thing
0.54 0.54
INSERT FILE='data/sim/ch13-study.sps'.
* point-biserial correlation: just Pearson r with a 0/1 variable.
CORRELATIONS /VARIABLES=score studied.
                Correlations
+---------------------------+-----+-------+
|                           |score|studied|
+---------------------------+-----+-------+
|score   Pearson Correlation|1.000|   .537|
|        Sig. (2-tailed)    |     |   .000|
|        N                  |   50|     50|
+---------------------------+-----+-------+
|studied Pearson Correlation| .537|  1.000|
|        Sig. (2-tailed)    | .000|       |
|        N                  |   50|     50|
+---------------------------+-----+-------+
using CSV, DataFrames
df = CSV.read("data/sim/ch13-study.csv", DataFrame)

using Statistics
cor(df.score, df.studied)   # point-biserial: just Pearson r!
0.537014507332376
import pandas as pd
df = pd.read_csv("data/sim/ch13-study.csv")

df.score.corr(df.studied)      # point-biserial: just Pearson r!
np.float64(0.5370145073323761)

No new formula, no new function; cor() did not even notice one variable was a dichotomy. The point-biserial correlation is just Pearson’s \(r\) computed with a 0/1 variable.

14.3 The Same Thing, Wearing Three Hats

Now the payoff. The relationship between a continuous outcome and a two-group predictor can be expressed as (a) a point-biserial correlation, (b) a two-sample t-test, or (c) a simple regression, and they are the identical analysis. Every one gives the same t statistic and p-value:

# (a) test the point-biserial correlation
ct <- cor.test(d$score, d$studied) |> tidy()

# (b) the classic two-sample t-test (pooled variance)
tt <- t.test(score ~ studied, data = d, var.equal = TRUE) |> tidy()

# (c) regression with the 0/1 predictor
rg <- lm(score ~ studied, data = d) |> tidy()

tibble(
  cor_test_t   = ct$statistic,
  t_test_t     = abs(tt$statistic),
  regression_t = abs(rg$statistic[2])
) |>
  round2()
cor_test_t t_test_t regression_t
4.41 4.41 4.41
INSERT FILE='data/sim/ch13-study.sps'.
* (a) test the point-biserial correlation.
CORRELATIONS /VARIABLES=score studied.
* (b) the classic two-sample t-test (pooled variance).
T-TEST GROUPS=studied(0 1) /VARIABLES=score.
* (c) regression with the 0/1 predictor.
REGRESSION /STATISTICS COEFF R ANOVA /DEPENDENT score /METHOD=ENTER studied.
                Correlations
+---------------------------+-----+-------+
|                           |score|studied|
+---------------------------+-----+-------+
|score   Pearson Correlation|1.000|   .537|
|        Sig. (2-tailed)    |     |   .000|
|        N                  |   50|     50|
+---------------------------+-----+-------+
|studied Pearson Correlation| .537|  1.000|
|        Sig. (2-tailed)    | .000|       |
|        N                  |   50|     50|
+---------------------------+-----+-------+

                 Group Statistics
+--------------+--+-----+--------------+---------+
|      Group   | N| Mean|Std. Deviation|S.E. Mean|
+--------------+--+-----+--------------+---------+
|score .000000 |20|70.06|          5.59|     1.25|
|      1.000000|30|76.82|          5.13|      .94|
+--------------+--+-----+--------------+---------+

                          Independent Samples Test
+---------------+----------+------------------------------------------------
|               | Levene's |
|               | Test for |
|               | Equality |
|               |    of    |
|               | Variances|             T-Test for Equality of Means
|               +----+-----+-----+-----+-------+----------+----------+------
|               |    |     |     |     |       |          |          |    95
|               |    |     |     |     |       |          |          | Confi
|               |    |     |     |     |       |          |          |Interv
|               |    |     |     |     |       |          |          |    th
|               |    |     |     |     |  Sig. |          |          | Diffe
|               |    |     |     |     |  (2-  |   Mean   |Std. Error+-----+
|               |  F | Sig.|  t  |  df |tailed)|Difference|Difference|Lower|
+---------------+----+-----+-----+-----+-------+----------+----------+-----+
|score Equal    | .01| .920|-4.41|48.00|   .000|     -6.77|      1.53|-9.85|
|      variances|    |     |     |     |       |          |          |     |
|      assumed  |    |     |     |     |       |          |          |     |
|      Equal    |    |     |-4.33|38.39|   .000|     -6.77|      1.56|-9.93|
|      variances|    |     |     |     |       |          |          |     |
|      not      |    |     |     |     |       |          |          |     |
|      assumed  |    |     |     |     |       |          |          |     |
+---------------+----+-----+-----+-----+-------+----------+----------+-----+

+---------------+-----+
|               |     |
|               |     |
|               |     |
|               |     |
|               |     |
|               +-----+
|               |%    |
|               |dence|
|               |al of|
|               |e    |
|               |rence|
|               +-----+
|               |Upper|
+---------------+-----+
|score Equal    |-3.68|
|      variances|     |
|      assumed  |     |
|      Equal    |-3.61|
|      variances|     |
|      not      |     |
|      assumed  |     |
+---------------+-----+

                   Model Summary (score)
+---+--------+-----------------+--------------------------+
| R |R Square|Adjusted R Square|Std. Error of the Estimate|
+---+--------+-----------------+--------------------------+
|.54|     .29|              .27|                      5.32|
+---+--------+-----------------+--------------------------+

                    ANOVA (score)
+----------+--------------+--+-----------+-----+----+
|          |Sum of Squares|df|Mean Square|  F  |Sig.|
+----------+--------------+--+-----------+-----+----+
|Regression|        549.75| 1|     549.75|19.45|.000|
|Residual  |       1356.55|48|      28.26|     |    |
|Total     |       1906.30|49|           |     |    |
+----------+--------------+--+-----------+-----+----+

                             Coefficients (score)
+----------+----------------------------+-------------------------+-----+----+
|          | Unstandardized Coefficients|Standardized Coefficients|     |    |
|          +-----------+----------------+-------------------------+     |    |
|          |     B     |   Std. Error   |           Beta          |  t  |Sig.|
+----------+-----------+----------------+-------------------------+-----+----+
|(Constant)|      70.06|            1.19|                      .00|58.93|.000|
|studied   |       6.77|            1.53|                      .54| 4.41|.000|
+----------+-----------+----------------+-------------------------+-----+----+
using CSV, DataFrames
df = CSV.read("data/sim/ch13-study.csv", DataFrame)

using HypothesisTests, GLM
# (a) test the point-biserial correlation
CorrelationTest(df.score, df.studied)
# (b) the classic two-sample t-test (pooled variance)
EqualVarianceTTest(df.score[df.studied .== 0], df.score[df.studied .== 1])
# (c) regression with the 0/1 predictor
coeftable(lm(@formula(score ~ studied), df))
Test for nonzero correlation
----------------------------
Population details:
    parameter of interest:   Correlation
    value under h_0:         0.0
    point estimate:          0.537015
    95% confidence interval: (0.3041, 0.7093)

Test summary:
    outcome with 95% confidence: reject h_0
    two-sided p-value:           <1e-04

Details:
    number of observations:          50
    number of conditional variables: 0
    t-statistic:                     4.41046
    degrees of freedom:              48

Two sample t-test (equal variance)
----------------------------------
Population details:
    parameter of interest:   Mean difference
    value under h_0:         0
    point estimate:          -6.76848
    95% confidence interval: (-9.854, -3.683)

Test summary:
    outcome with 95% confidence: reject h_0
    two-sided p-value:           <1e-04

Details:
    number of observations:   [20,30]
    t-statistic:              -4.4104607562171445
    degrees of freedom:       48
    empirical standard error: 1.5346431746816767

────────────────────────────────────────────────────────────────────────
                Coef.  Std. Error      t  Pr(>|t|)  Lower 95%  Upper 95%
────────────────────────────────────────────────────────────────────────
(Intercept)  70.0558      1.18873  58.93    <1e-45   67.6657    72.4459
studied       6.76848     1.53464   4.41    <1e-04    3.68288    9.85409
────────────────────────────────────────────────────────────────────────
import pandas as pd
df = pd.read_csv("data/sim/ch13-study.csv")

from scipy import stats
import statsmodels.formula.api as smf

# (a) test the point-biserial correlation
stats.pearsonr(df.score, df.studied)
# (b) the classic two-sample t-test (pooled variance)
stats.ttest_ind(df.score[df.studied == 0], df.score[df.studied == 1])
# (c) regression with the 0/1 predictor
smf.ols("score ~ studied", data=df).fit().summary()
PearsonRResult(statistic=np.float64(0.537014507332376), pvalue=np.float64(5.802483428097993e-05))
TtestResult(statistic=np.float64(-4.410460756217154), pvalue=np.float64(5.802483428097736e-05), df=np.float64(48.0))
<class 'statsmodels.iolib.summary.Summary'>
"""
                            OLS Regression Results                            
==============================================================================
Dep. Variable:                  score   R-squared:                       0.288
Model:                            OLS   Adj. R-squared:                  0.274
Method:                 Least Squares   F-statistic:                     19.45
Date:                Tue, 08 Sep 2026   Prob (F-statistic):           5.80e-05
Time:                        21:29:01   Log-Likelihood:                -153.46
No. Observations:                  50   AIC:                             310.9
Df Residuals:                      48   BIC:                             314.8
Df Model:                           1                                         
Covariance Type:            nonrobust                                         
==============================================================================
                 coef    std err          t      P>|t|      [0.025      0.975]
------------------------------------------------------------------------------
Intercept     70.0558      1.189     58.933      0.000      67.666      72.446
studied        6.7685      1.535      4.410      0.000       3.683       9.854
==============================================================================
Omnibus:                        0.020   Durbin-Watson:                   1.827
Prob(Omnibus):                  0.990   Jarque-Bera (JB):                0.074
Skew:                          -0.030   Prob(JB):                        0.964
Kurtosis:                       2.821   Cond. No.                         2.92
==============================================================================

Notes:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.
"""

Three “different” techniques, one t statistic. The correlation, the group comparison, and the regression are the same GLM. And the regression coefficient tells you something the correlation does not show directly: the raw difference between the two group means:

means <- d |>
  summarise(mean = mean(score), .by = studied) |>
  arrange(studied)

tibble(
  regression_slope = rg$estimate[2],
  mean_difference  = means$mean[2] - means$mean[1]
) |>
  round2()
regression_slope mean_difference
6.77 6.77

The slope on a 0/1 predictor is the difference in group means. (This is the same dummy-coding logic from the ANOVA chapter, previewed here with the simplest possible category.)

14.4 From \(r\) to \(t\) and Back

Because they are the same analysis, the correlation and the t statistic are tied by a simple formula, handy for converting between an effect size (\(r_{pb}\)) and a significance test (\(t\)):

\[r_{pb} = \frac{t}{\sqrt{t^2 + df}}\]

t_val <- abs(tt$statistic)
df    <- tt$parameter

tibble(
  r_pb     = cor(d$score, d$studied),
  r_from_t = t_val / sqrt(t_val^2 + df)
) |>
  round2()
r_pb r_from_t
0.54 0.54

They match. This little equation is why you can always recover an effect size from a reported t (and its df), and vice versa, which is useful when reading other people’s papers.

ImportantWhy This Bridge Matters

MRC (regression with continuous predictors) and ANOVA (with categorical predictors) are often taught as if they were unrelated. The point-biserial correlation connects them: once you code a category as numbers, a group difference is a correlation and a t-test is a regression. Every categorical predictor is just a set of 0/1 indicators you can correlate. That idea is the key to the entire General Linear Model.

14.5 Challenge

TipDo One Yourself
  1. Simulate a continuous outcome and a 0/1 predictor. Compute the point-biserial correlation with cor(), then confirm the t from cor.test(), t.test(var.equal = TRUE), and lm() are identical.
  2. Show that the regression slope equals the difference in the two group means.
  3. Recode the predictor as 1/2 instead of 0/1 and refit. What changes about the intercept, what stays the same about the slope and the p-value, and why?

14.6 Where We Go Next

We have the bridge and the engine. Now we build the machine in earnest, starting with the simplest continuous-predictor case: bivariate regression.