Ling 202: session 09: similarity-based prediction (key)

Author
Affiliations

UC Santa Barbara

JLU Giessen

Published

21 May 2025 12-34-56

1 Application to a linear model

In session 03, we fit a linear model to answer the question whether the logged reaction time to a word (in ms) varies as a function of

  • the Zipf frequency of that word (ZIPFFREQ);
  • the language that word was presented in (LANGUAGE: english vs. spanish);
  • the speaker group that words was presented to (GROUP: english vs. heritage);
  • any pairwise interaction of these predictors;
  • the three-way interaction of these predictors.

Let’s reload those data and apply the same transformation(s) etc. there as we did earlier:

rm(list=ls(all.names=TRUE)); library(cluster)
str(d <- read.delim(       # summarize d, the result of loading
   file="_input/rts.csv",  # this file
   stringsAsFactors=TRUE)) # change categorical variables into factors
'data.frame':   8000 obs. of  9 variables:
 $ CASE    : int  1 2 3 4 5 6 7 8 9 10 ...
 $ RT      : int  748 408 483 1266 636 395 765 1297 849 315 ...
 $ LENGTH  : int  5 5 6 4 6 6 8 4 5 6 ...
 $ LANGUAGE: Factor w/ 2 levels "english","spanish": 2 2 2 2 2 2 2 2 2 2 ...
 $ GROUP   : Factor w/ 2 levels "english","heritage": 2 2 2 2 2 2 2 2 2 2 ...
 $ CORRECT : Factor w/ 2 levels "correct","incorrect": 1 1 1 1 1 1 1 1 1 1 ...
 $ FREQPMW : int  19 78 233 133 14 67 34 114 2 20 ...
 $ ZIPFFREQ: num  4.28 4.89 5.37 5.12 4.15 ...
 $ SITE    : Factor w/ 3 levels "a","b","c": 2 2 2 2 2 2 3 2 2 1 ...
d <- d[complete.cases(d),]
d$LANGUAGE <- factor(d$LANGUAGE,   # for didactic reasons only, I am
   levels=levels(d$LANGUAGE)[2:1]) # changing the order of the levels
d$RT_log <- log2(d$RT)

Let’s refit our initial model from there …

summary(m_01 <- lm(          # make/summarize the linear model m_01:
   RT ~ 1 + ZIPFFREQ*LANGUAGE*GROUP, # RT ~ these predictors & their interactions
   data=d, na.action=na.exclude))    # those vars are in d, skip NAs

Call:
lm(formula = RT ~ 1 + ZIPFFREQ * LANGUAGE * GROUP, data = d,
    na.action = na.exclude)

Residuals:
   Min     1Q Median     3Q    Max
-489.7 -141.2  -54.5   67.4 3388.8

Coefficients:
                                       Estimate Std. Error t value Pr(>|t|)
(Intercept)                             1145.59      46.97  24.392  < 2e-16 ***
ZIPFFREQ                                 -82.48      10.15  -8.126 5.12e-16 ***
LANGUAGEenglish                         -432.15      69.57  -6.212 5.50e-10 ***
GROUPheritage                           -177.07      64.64  -2.739 0.006171 **
ZIPFFREQ:LANGUAGEenglish                  51.65      14.97   3.450 0.000563 ***
ZIPFFREQ:GROUPheritage                    23.49      14.00   1.677 0.093489 .
LANGUAGEenglish:GROUPheritage            306.90      97.24   3.156 0.001604 **
ZIPFFREQ:LANGUAGEenglish:GROUPheritage   -40.88      20.94  -1.953 0.050891 .
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 250.4 on 7744 degrees of freedom
Multiple R-squared:  0.09514,   Adjusted R-squared:  0.09432
F-statistic: 116.3 on 7 and 7744 DF,  p-value: < 2.2e-16

… and let’s add the predictions of this model to the data frame d:

d$PRED_LM <- predict(m_01)

Let’s create a distance matrix that compares each case to each other case while only considering the main effects we also put into our regression model:

distances <- daisy( # compute a distance matrix
   x=d[,c("ZIPFFREQ", "LANGUAGE", "GROUP")], # of this
   metric="gower",                           # use the Gower metric
   type=list(                                # specify the
      "numeric"="ZIPFFREQ",                  # variable
         "symm"=c("LANGUAGE", "GROUP"))) %>% # types, then
as.matrix(ncol=nrow(d)) # make that a symmetric matrix
str(distances)                 # check the ...
 num [1:7752, 1:7752] 0 0.0668 0.1185 0.092 0.0144 ...
 - attr(*, "dimnames")=List of 2
  ..$ : chr [1:7752] "1" "2" "3" "4" ...
  ..$ : chr [1:7752] "1" "2" "3" "4" ...
summary(as.numeric(distances)) # ... results
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max.
 0.0000  0.2615  0.3899  0.4004  0.6246  1.0000 

Let’s make predictions for each case that are based on a weighted mean of the observed reaction times of all those cases whose distance from a current case are among the 10 smallest:

d$PRED_NNB <- rep(NA, nrow(d)) # establish the collector column
for (i in seq(nrow(d))) { # for each case
   curr <- distances[i,]  # get all distance values
   curr[i] <- NA          # insert NA for this case to itself
   curr <- exp(-curr)     # reverse polarity to get similarity values
   tenth_highest_sim <- curr %>% unique %>% sort(decreasing=TRUE) %>% "["(10)
   picky <- which(curr >= tenth_highest_sim) # establish the cut-off point
   d$PRED_NNB[i] <-     # compute & store the prediction
      weighted.mean(    # with a weighted mean
         x=d$RT[picky], # of all reaction times
         w=curr[picky], # weighted by their sim to the current case
         na.rm=TRUE)    # remove the 1 NA point
}

Let’s compare this to the predictions of our linear model:

plot(pch=16, col="#00000020",
   xlab="Predictions from lm", x=d$PRED_LM,
   xlim=c(min(c(d$PRED_LM, d$PRED_NNB)), max(c(d$PRED_LM, d$PRED_NNB))),
   ylab="Predictions from nb", y=d$PRED_NNB,
   ylim=c(min(c(d$PRED_LM, d$PRED_NNB)), max(c(d$PRED_LM, d$PRED_NNB))))
abline(v=quantile(d$PRED_LM , probs=seq(0, 1, 0.1)), col="#00000020", lty=3)
abline(h=quantile(d$PRED_NNB, probs=seq(0, 1, 0.1)), col="#00000020", lty=3)
abline(lm(d$PRED_NNB ~ d$PRED_LM), col="#0000FF40", lwd=5)

The predictions seem very similar – how similar?

cor(d$PRED_LM, d$PRED_NNB)^2 %>% round(4)
[1] 0.9433

Let’s compare the residuals of the two kinds of predictions side by side:

par(mfrow=c(1,2))
hist(main=paste0("Deviance=",
   prettyNum(round(sum((d$RT - d$PRED_LM)^2), 1), big.mark=","),
   "\nR-squared=", round(cor(d$RT, d$PRED_LM), 4)),
   d$RT - d$PRED_LM, breaks="FD")
hist(main=paste0("Deviance=",
   prettyNum(round(sum((d$RT - d$PRED_NNB)^2), 1), big.mark=","),
   "\nR-squared=", round(cor(d$RT, d$PRED_NNB), 4)),
   d$RT - d$PRED_NNB, breaks="FD")
par(mfrow=c(1,1))

Again, the results are really similar, which is kinda amazing given how low-tech the nearest-neighbor approach actually is …

2 Application to a binary classification task

In session 05, we fit a binary logistic regression model to answer the question whether the choice of a genitive construction (of vs. s) varies as a function of

  • a length-based short-before-long effect, but, this time, if we hypothesize a short-before-long effect, maybe we should not just be looking at the length of the possessor, but how the length of the possessor compares to the length of the possessum;
  • the language that word was presented in (POR_ANIMACY: animate vs. collective vs. inanimate vs. locative vs. temporal);
  • whether the speakers are non-native speakers or native speakers of English (SPEAKER: nns vs. ns);
  • any pairwise interaction of these predictors;
  • the three-way interaction of these predictors.

Let’s reload those data and apply the same transformation(s) etc. there as we did earlier:

rm(list=ls(all.names=TRUE))
str(d <- read.delim(            # summarize d, the result of loading
   file="_input/genitives.csv", # this file
   stringsAsFactors=TRUE))      # change categorical variables into factors
'data.frame':   3600 obs. of  9 variables:
 $ CASE         : int  2 3 4 5 6 7 8 9 10 11 ...
 $ GENITIVE     : Factor w/ 2 levels "of","s": 1 1 1 1 2 2 2 2 2 2 ...
 $ SPEAKER      : Factor w/ 2 levels "nns","ns": 1 1 1 1 1 1 1 1 1 1 ...
 $ MODALITY     : Factor w/ 2 levels "spoken","written": 1 1 1 1 1 1 1 1 1 1 ...
 $ POR_LENGTH   : int  13 22 11 26 8 7 6 5 5 5 ...
 $ PUM_LENGTH   : int  7 7 8 4 4 3 36 3 5 5 ...
 $ POR_ANIMACY  : Factor w/ 5 levels "animate","collective",..: 2 1 1 2 1 1 1 1 1 1 ...
 $ POR_FINAL_SIB: Factor w/ 2 levels "absent","present": 1 1 2 1 1 1 1 1 1 1 ...
 $ POR_DEF      : Factor w/ 2 levels "definite","indefinite": 1 1 1 1 1 1 1 1 1 1 ...
d$LEN_PORmPUM_LOG <- log2(d$POR_LENGTH)-log2(d$PUM_LENGTH)

Let’s recompute the baseline and null deviance from there, …

(baselines <- c(
   "baseline 1"=max(          # make baselines[1] the highest
      prop.table(             # proportion in the
         table(d$GENITIVE))), # frequency table of the response
   "baseline 2"=sum(             # make baselines[2] the sum of the
      prop.table(                # proportions in the
         table(d$GENITIVE))^2))) # frequency table of the response squared
baseline 1 baseline 2
 0.7555556  0.6306173 
(deviance_null <- deviance(m_00 <- glm(GENITIVE ~ 1, family=binomial, data=d, na.action=na.exclude)))
[1] 4004.273

… let’s refit our initial model from there, …

summary(m_01 <- glm( # make/summarize the gen. linear model m_01:
   GENITIVE ~ 1 + LEN_PORmPUM_LOG*POR_ANIMACY*SPEAKER, # GENITIVE ~ these predictors
   data=d, family=binomial, na.action=na.exclude)) # vars are in d, resp = binary, skip NAs

Call:
glm(formula = GENITIVE ~ 1 + LEN_PORmPUM_LOG * POR_ANIMACY *
    SPEAKER, family = binomial, data = d, na.action = na.exclude)

Coefficients:
                                                Estimate Std. Error z value
(Intercept)                                      0.65632    0.09135   7.184
LEN_PORmPUM_LOG                                 -0.70601    0.08154  -8.659
POR_ANIMACYcollective                           -1.77888    0.16150 -11.015
POR_ANIMACYinanimate                            -4.32698    0.21686 -19.953
POR_ANIMACYlocative                             -2.30616    0.27227  -8.470
POR_ANIMACYtemporal                             -1.32138    0.22536  -5.863
SPEAKERns                                       -0.08278    0.17204  -0.481
LEN_PORmPUM_LOG:POR_ANIMACYcollective           -0.46680    0.15054  -3.101
LEN_PORmPUM_LOG:POR_ANIMACYinanimate             0.32099    0.17729   1.811
LEN_PORmPUM_LOG:POR_ANIMACYlocative             -0.29249    0.25885  -1.130
LEN_PORmPUM_LOG:POR_ANIMACYtemporal              0.44522    0.18375   2.423
LEN_PORmPUM_LOG:SPEAKERns                        0.14245    0.16001   0.890
POR_ANIMACYcollective:SPEAKERns                  0.79493    0.29353   2.708
POR_ANIMACYinanimate:SPEAKERns                  -0.28854    0.51213  -0.563
POR_ANIMACYlocative:SPEAKERns                   -0.50011    0.49785  -1.005
POR_ANIMACYtemporal:SPEAKERns                    0.16161    0.41275   0.392
LEN_PORmPUM_LOG:POR_ANIMACYcollective:SPEAKERns -0.14283    0.28943  -0.493
LEN_PORmPUM_LOG:POR_ANIMACYinanimate:SPEAKERns  -0.65372    0.43153  -1.515
LEN_PORmPUM_LOG:POR_ANIMACYlocative:SPEAKERns   -0.19485    0.50679  -0.384
LEN_PORmPUM_LOG:POR_ANIMACYtemporal:SPEAKERns   -0.53061    0.38091  -1.393
                                                Pr(>|z|)
(Intercept)                                     6.75e-13 ***
LEN_PORmPUM_LOG                                  < 2e-16 ***
POR_ANIMACYcollective                            < 2e-16 ***
POR_ANIMACYinanimate                             < 2e-16 ***
POR_ANIMACYlocative                              < 2e-16 ***
POR_ANIMACYtemporal                             4.54e-09 ***
SPEAKERns                                        0.63039
LEN_PORmPUM_LOG:POR_ANIMACYcollective            0.00193 **
LEN_PORmPUM_LOG:POR_ANIMACYinanimate             0.07022 .
LEN_PORmPUM_LOG:POR_ANIMACYlocative              0.25849
LEN_PORmPUM_LOG:POR_ANIMACYtemporal              0.01539 *
LEN_PORmPUM_LOG:SPEAKERns                        0.37331
POR_ANIMACYcollective:SPEAKERns                  0.00677 **
POR_ANIMACYinanimate:SPEAKERns                   0.57315
POR_ANIMACYlocative:SPEAKERns                    0.31511
POR_ANIMACYtemporal:SPEAKERns                    0.69538
LEN_PORmPUM_LOG:POR_ANIMACYcollective:SPEAKERns  0.62166
LEN_PORmPUM_LOG:POR_ANIMACYinanimate:SPEAKERns   0.12980
LEN_PORmPUM_LOG:POR_ANIMACYlocative:SPEAKERns    0.70062
LEN_PORmPUM_LOG:POR_ANIMACYtemporal:SPEAKERns    0.16362
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

(Dispersion parameter for binomial family taken to be 1)

    Null deviance: 4004.3  on 3599  degrees of freedom
Residual deviance: 2404.3  on 3580  degrees of freedom
AIC: 2444.3

Number of Fisher Scoring iterations: 7

… and let’s add the predictions of this model to the data frame d:

d$PRED_GLM_PP_S <- predict(m_01, type="response")
d$PRED_GLM_CAT <- factor(ifelse(d$PRED_GLM_PP_S>=0.5, levels(d$GENITIVE)[2], levels(d$GENITIVE)[1]))

Let’s create a distance matrix that compares each case to each other case while only considering the main effects we also put into our regression model:

distances <- daisy( # compute a distance matrix
   x=d[,c("LEN_PORmPUM_LOG", "POR_ANIMACY", "SPEAKER")], # of this
   metric="gower",                               # use the Gower metric
   type=list(                                    # specify
      "numeric"="LEN_PORmPUM_LOG",               # variable
      "factor"=c("POR_ANIMACY", "SPEAKER"))) %>% # types, then
as.matrix(ncol=nrow(d)) # make that a symmetric matrix
str(distances)                 # check the ...
 num [1:3600, 1:3600] 0 0.3607 0.349 0.0652 0.3372 ...
 - attr(*, "dimnames")=List of 2
  ..$ : chr [1:3600] "1" "2" "3" "4" ...
  ..$ : chr [1:3600] "1" "2" "3" "4" ...
summary(as.numeric(distances)) # ... results
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max.
 0.0000  0.3408  0.3820  0.4058  0.6717  0.9914 

Let’s make predictions for each case that are based on a weighted mean of the observed reaction times of all those cases whose distance from a current case are among the 10 smallest:

d$PRED_NNB_PP_S <- rep(NA, nrow(d)) # establish the collector column
for (i in seq(nrow(d))) { # for each case
   curr <- distances[i,]  # get all distance values
   curr[i] <- NA          # insert NA for this case to itself
   curr <- exp(-curr)     # reverse polarity to get similarity values
   tenth_highest_sim <- curr %>% unique %>% sort(decreasing=TRUE) %>% "["(10)
   picky <- which(curr >= tenth_highest_sim) # establish the cut-off point
   d$PRED_NNB_PP_S[i] <- # compute & store the prediction
      weighted.mean(     # with a weighted mean
         x=d$GENITIVE[picky]=="s", # of s-genitives
         w=curr[picky],  # weighted by their sim to the current case
         na.rm=TRUE)     # remove any missing data
}
d$PRED_NNB_CAT <- factor(ifelse(d$PRED_NNB_PP_S>=0.5, levels(d$GENITIVE)[2], levels(d$GENITIVE)[1]))

Let’s compare this to the predictions of our generalized linear model:

plot(pch=16, col="#00000020",
   xlab="Predictions from glm", xlim=c(0, 1), x=d$PRED_GLM_PP_S,
   ylab="Predictions from nnb", ylim=c(0, 1), y=d$PRED_NNB_PP_S)
abline(v=quantile(d$PRED_GLM_PP_S   , probs=seq(0, 1, 0.1)), col="#00000020", lty=3)
abline(v=0.5, col="#FF000040", lwd=3)
abline(h=quantile(d$PRED_NNB_PP_S, probs=seq(0, 1, 0.1)), col="#00000020", lty=3)
abline(h=0.5, col="#FF000040", lwd=3)
abline(lm(d$PRED_NNB_PP_S ~ d$PRED_GLM_PP_S), col="#0000FF40", lwd=5)

The predictions seem very similar – how similar?

cor(d$PRED_GLM_PP_S, d$PRED_NNB_PP_S)^2 %>% round(4)
[1] 0.9302
(preds_comparer <- table(GLM=d$PRED_GLM_CAT, NNB=d$PRED_NNB_CAT))
    NNB
GLM    of    s
  of 2649  107
  s    57  787
list("Cohen's kappa"=cohens.kappa(preds_comparer),          # 0.875645783
     "Matthew's cc "= MCC(preds_comparer),                  # 0.8762758
     "'Accuracy'"   = mean(d$PRED_GLM_CAT==d$PRED_NNB_CAT)) # 0.954444444
$`Cohen's kappa`
Cohen's kappa            se         lower         upper  p (2-tailed)
  0.875645783   0.009486673   0.857052246   0.894239320   0.000000000

$`Matthew's cc `
  MCC/phi
0.8762758

$`'Accuracy'`
[1] 0.9544444

Let’s compare the deviances of the two kinds of predictions side by side. For that, we first need to compute PRED_PP_obs and its nearest-neighbor equivalent:

d$PRED_GLM_PP_obs <-       # d$PRED_GLM_PP_obs is determined by ifelse
   ifelse(d$GENITIVE=="s", # if the obs order is the 2nd level of the response
      d$PRED_GLM_PP_S,     # take its predicted prob.
    1-d$PRED_GLM_PP_S)     # otherwise take 1 minus its predicted prob.
d$PRED_NNB_PP_obs <-        # d$PRED_NNB_PP_obs is determined by ifelse
   ifelse(d$GENITIVE=="s",  # if the obs order is the 2nd level of the response
      d$PRED_NNB_PP_S,      # take its 'predicted prob.'
    1-d$PRED_NNB_PP_S)      # otherwise take 1 minus its 'predicted prob.'

Then, we compute the contributions to logloss from those columns:

d$CONTR2LL_GLM <- -log(d$PRED_GLM_PP_obs)
d$CONTR2LL_NNB <- -log(d$PRED_NNB_PP_obs)

And then we can compare the deviance of the glm approach to that of the nearest-neighbor one:

sum(d$CONTR2LL_GLM)*2 # same as deviance(m_01)
[1] 2404.267
sum(d$CONTR2LL_NNB)*2 # a deviance equivalent
[1] Inf

Why does that happen?

summary(d$PRED_NNB_PP_obs)
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max.
 0.0000  0.6248  0.9230  0.7875  1.0000  1.0000 

How do we address that? We change the 0-values to half the otherwise smallest ‘predicted probability’:

d$PRED_NNB_PP_obs[d$PRED_NNB_PP_obs==0] <- # change the 0s to
   d$PRED_NNB_PP_obs %>% # of all the 'predicted probabilities',
   unique            %>% # the unique ones
   sort              %>% # when sorted in ascending order
   "["(2)            %>% # the 2nd value (i.e. the next one after 0)
   "/"(2)                # and divide that by 2
d$CONTR2LL_NNB  <- -log(d$PRED_NNB_PP_obs) # recompute contribs to logloss
sum(d$CONTR2LL_NNB)*2  # a deviance equivalent
[1] 2459.323

What R-squareds (McFadden) result from this?

list(
   "McFadden for glm" = (deviance_null - deviance(m_01))        / deviance_null,
   "McFadden for nnb" = (deviance_null - sum(d$CONTR2LL_NNB)*2) / deviance_null)
$`McFadden for glm`
[1] 0.3995747

$`McFadden for nnb`
[1] 0.3858254

Very similar. Let’s look at the real confusion matrices (observed vs. predicted):

list(
   "Confusion matrix for the glm"=c_m_GLM <- table( # confusion matrix: cross-tabulate
      "OBS"  =d$GENITIVE,      # observed genitives in the rows
      "PREDS"=d$PRED_GLM_CAT), # predicted genitives in the columns
   "Its evaluation"= c( # evaluate the confusion matrix
      "Prec. for s"     =c_m_GLM[ "s", "s"] / sum(c_m_GLM[    , "s"]),
      "Acc./rec. for s" =c_m_GLM[ "s", "s"] / sum(c_m_GLM[ "s",    ]),
      "Prec. for of"    =c_m_GLM["of","of"] / sum(c_m_GLM[    ,"of"]),
      "Acc./rec. for of"=c_m_GLM["of","of"] / sum(c_m_GLM["of",    ]),
      "Acc. (overall)"  =mean(d$GENITIVE==d$PRED_GLM_CAT),
      "p over baseline" =sum(dbinom(3024:3600, 3600, baselines[1]))),
   "Confusion matrix for the nnb"=c_m_NNB <- table( # confusion matrix: cross-tabulate
      "OBS"  =d$GENITIVE,      # observed genitives in the rows
      "PREDS"=d$PRED_NNB_CAT), # predicted genitives in the columns
   "Its evaluation"= c( # evaluate the confusion matrix
      "Prec. for s"     =c_m_NNB[ "s", "s"] / sum(c_m_NNB[    , "s"]),
      "Acc./rec. for s" =c_m_NNB[ "s", "s"] / sum(c_m_NNB[ "s",    ]),
      "Prec. for of"    =c_m_NNB["of","of"] / sum(c_m_NNB[    ,"of"]),
      "Acc./rec. for of"=c_m_NNB["of","of"] / sum(c_m_NNB["of",    ]),
      "Acc. (overall)"  =mean(d$GENITIVE==d$PRED_NNB_CAT),
      "p over baseline" =sum(dbinom(3012:3600, 3600, baselines[1]))))
$`Confusion matrix for the glm`
    PREDS
OBS    of    s
  of 2450  270
  s   306  574

$`Its evaluation`
     Prec. for s  Acc./rec. for s     Prec. for of Acc./rec. for of
    6.800948e-01     6.522727e-01     8.889695e-01     9.007353e-01
  Acc. (overall)  p over baseline
    8.400000e-01     3.296731e-35

$`Confusion matrix for the nnb`
    PREDS
OBS    of    s
  of 2419  301
  s   287  593

$`Its evaluation`
     Prec. for s  Acc./rec. for s     Prec. for of Acc./rec. for of
    6.633110e-01     6.738636e-01     8.939394e-01     8.893382e-01
  Acc. (overall)  p over baseline
    8.366667e-01     1.685128e-32 

Again, the results are really similar, which is kinda amazing given how low-tech the nearest-neighbor approach actually is …

3 Session info

sessionInfo()
R version 4.5.0 (2025-04-11)
Platform: x86_64-pc-linux-gnu
Running under: Pop!_OS 22.04 LTS

Matrix products: default
BLAS:   /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3
LAPACK: /usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.20.so;  LAPACK version 3.10.0

locale:
 [1] LC_CTYPE=en_US.UTF-8       LC_NUMERIC=C
 [3] LC_TIME=en_US.UTF-8        LC_COLLATE=en_US.UTF-8
 [5] LC_MONETARY=en_US.UTF-8    LC_MESSAGES=en_US.UTF-8
 [7] LC_PAPER=en_US.UTF-8       LC_NAME=C
 [9] LC_ADDRESS=C               LC_TELEPHONE=C
[11] LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C

time zone: America/Los_Angeles
tzcode source: system (glibc)

attached base packages:
[1] stats     graphics  grDevices utils     datasets  compiler  methods
[8] base

other attached packages:
[1] cluster_2.1.8.1 STGmisc_1.02    Rcpp_1.0.14     magrittr_2.0.3

loaded via a namespace (and not attached):
 [1] digest_0.6.37     fastmap_1.2.0     xfun_0.52         knitr_1.50
 [5] htmltools_0.5.8.1 rmarkdown_2.29    cli_3.6.5         rstudioapi_0.17.1
 [9] tools_4.5.0       evaluate_1.0.3    yaml_2.3.10       rlang_1.1.6
[13] jsonlite_2.0.0    htmlwidgets_1.6.4 MASS_7.3-65