R-Basic Part VI: Loop Functions and Debugging in R

 Highlights

  • Loop Functions - lapply, sapply, mapply, tapply and split in greater detail
  • Debugging in R

Introduction

Loop functions are some of the most powerful functions in R language. They make it very easy to work in R, especially in an interactive setting. Writing for and/or while loops is useful when programming but not easy when working interactively on the command line. There are some functions use the ‘looping’ characteristics to ease coding.

  • lapply: Loops over a list and evaluate a function on each element. It’s a very general concept but can be efficiently used to conduct a lot of computations.
  • sapply: Same as lapply but it provides a nicer, cleaner outputs
  • apply: Applies a function over the margins of an array. It is very useful if we want to summarize matrices, or other higher dimensional arrays.
  • tapply: Applies a function over subsets of a vector. A short form of the table apply.
  • mapply: Multivariate version of 
  • split: An auxiliary function, can be used in conjunction with 


lapply
function (X, FUN, ...) 
{
    FUN <- match.fun(FUN)
    if (!is.vector(X) || is.object(X)) 
        X <- as.list(X)
    .Internal(lapply(X, FUN))
}
<bytecode: 0x000000001453c2e8>
<environment: namespace:base>

It takes the three arguments:

  1. X: a list ‘X’
  2. FUN: a function (or the name of the function), and
  3. Other arguments as represented by …

Caveat: If X is not a list, it gets coerced to a list using the as.list function, as long as possible. If not we get an error.

Example:

  1. Creating an object named my_list having two variables.
my_list <- list(number = 5:14, random_number = rnorm(10))
# checking the list I just created
my_list
$number
 [1]  5  6  7  8  9 10 11 12 13 14

$random_number
 [1] -0.2909048 -0.4078042  2.4512386  1.6469673 -1.7675869  1.1715307
 [7]  1.2076329  0.1854738  0.5550943  0.8995863

The first one is an array of numbers between 5 and 14, while the other is a list of 10 random numbers. I am going to calculate mean of both of the variables using the  function followed by their standard deviations.

# Applying lapply by calculating mean and standard deviations of the variables
lapply(my_list, mean)
$number
[1] 9.5

$random_number
[1] 0.5651228
lapply(my_list, sd)
$number
[1] 3.02765

$random_number
[1] 1.19654

We received the mean and the standard deviation of both of the variables in my_list. The number and random_number are the names of the columns, R assigned to the variables.

Here’s the another way we can use the lapply function. I am going to create a uniform random variables based on a given set of list:

# Setting the limit for num1
list1 <- 1:6
# Generating a random uniform variables
lapply(list1, runif)
[[1]]
[1] 0.4777198

[[2]]
[1] 0.4195502 0.8510725

[[3]]
[1] 0.9559967 0.6260833 0.4466767

[[4]]
[1] 0.1708987 0.3679903 0.9392495 0.6885258

[[5]]
[1] 0.9160025 0.5101619 0.8902106 0.9504878 0.8899351

[[6]]
[1] 0.7069945 0.7875418 0.6586249 0.3977399 0.3351455 0.4057888

I designated the limit to an object list1,meaning, it has the values between 1 through 6. Then, I passed the  function and requested random uniform numbers. As expected, R populated 6 lists that included one item in the first one, all the way up to 6 items in the 6th one.

I want to repeat the same object but want to set a limit for the range between 2 and 5.

lapply(list1, runif, min = 2, max = 5) # generate random uniform numbers having elements between 2 and 5
[[1]]
[1] 2.121966

[[2]]
[1] 3.013352 2.659712

[[3]]
[1] 3.328347 2.846241 3.974327

[[4]]
[1] 3.250641 2.600905 4.601561 2.012102

[[5]]
[1] 3.467141 2.933492 3.763539 4.876187 4.860873

[[6]]
[1] 3.114903 3.595497 4.886456 2.263064 4.727873 3.333714

The  and similar functions make use of anonymous functions. Anonymous functions are functions not bound to any identifier, i.e., they don’t have names. They are created and used but not assigned to a specific variable.

# Creating a composit matrix having two different matrices within it
composite_matrix <- list(matrix_1 = matrix(3:6, 2, 2), matrix_2 = matrix(4:9, 3, 2))
composite_matrix
$matrix_1
     [,1] [,2]
[1,]    3    5
[2,]    4    6

$matrix_2
     [,1] [,2]
[1,]    4    7
[2,]    5    8
[3,]    6    9
# Extracting first column of both of the matrices
lapply(composite_matrix, function(c1m) c1m[, 2]) # created a function c1m and used it to get values of second column in both of the matrices. It is an anonymous function
$matrix_1
[1] 5 6

$matrix_2
[1] 7 8 9
# Extracting first row of both of the matrices
lapply(composite_matrix, function(r1w) r1w[2, ]) # Here, r1w, and it is used to extract values in the second rows of both matrices
$matrix_1
[1] 4 6

$matrix_2
[1] 5 8

The lapply always returns a list, one of the most boring aspect of this function. So, sometimes it may not be as convenient. It is always good to have a nice clean output. If we are all about a cleaner output with all the goodies of lapply function, we use sapply function.


It tries to simplify the results of the  functions as possible:

  1. if the result is a list where every element is length 1, then a vector is returned
  2. if the result is a list where every element is a vector of the same length (>1), a matrix is returned
  3. if it can’t figure things out, a list is returned

First of all, I am creating a list and using lapply function to calculate mean. Afterward, I will use sapply, and compare the differences.

test_1 <- list(a = 11:14, b = rnorm(8), c = rnorm(11, 1), d = rnorm(80, 7))
lapply(test_1, mean)
$a
[1] 12.5

$b
[1] 0.4978634

$c
[1] 1.261984

$d
[1] 7.062322
# just for fun!
library(lattice)
densityplot(test_1$d)

Received a vector containing the column-wise means of all 4 variables. Now I am going to use sapply and compare the differences:

sapply(test_1, mean)
         a          b          c          d 
12.5000000  0.4978634  1.2619837  7.0623221 

As we can see the results appear in much nicer tabular form.

apply Function

This function is used to evaluate a function (often an anonymous function)over the margins of an array.

  • It is most often used to apply a function to the rows or column of a matrix.
  • It can be used with general arrays, e.g., taking an average of an array of matrices.
  • It is not really faster than writing a loop, but it works in one line.

Let’s check the arguments required to execute the apply function:

str(apply)
function (X, MARGIN, FUN, ..., simplify = TRUE)  
  • X is an array (name of an array or a matrix, or a data frame)
  • MARGIN is an integer vector indicating margins that we want to retain, 1 for rows and 2 for columns
  • FUN is a function to be applied
  •  is used for optional arguments, if any.

Let’s create a matrix with 15 rows and 12 columns and pass apply function.

a <- matrix(rnorm(180), 15, 12)
# Let's calculate the mean of all the columns
apply(a, 2, mean)
 [1] -0.153200019 -0.075872931  0.002956461 -0.507005579 -0.313867771
 [6] -0.081446408 -0.175040796  0.359766967 -0.175581366  0.086174246
[11] -0.003869750  0.008839847

In the above syntax: a is the name of the matrix, 2 tells R to do something by columns, and mean tells to calculate mean. All in all we got 12 values and they are the means of 12 columns in matrix a.

Now, let’s add the values in all 15 rows:

apply(a, 1, sum)
 [1] -0.08684487 -2.14716736 -8.92255127 -2.56494317 -3.19202907  0.67495589
 [7]  0.79953179 -0.79809065  1.37828910  2.73759452 -0.73893141  2.59880531
[13] -4.19098774 -0.48338688 -0.48645067

Here we go. We have 15 values and they are the sums of each 15 rows in the matrix a.

There are optimized functions in R that would simplify the tasks of calculating row or column sums or means. They are:

Col/row sums and means

  • rowSums = apply(a, 1, sum)
  • rowMeans = apply(a, 1, mean)
  • colSums = apply(a, 2, sum)
  • colMeans = apply(a, 2, mean)

These functions are much faster and easy to run. Let’s check one of them in action:

rowSums(a)
 [1] -0.08684487 -2.14716736 -8.92255127 -2.56494317 -3.19202907  0.67495589
 [7]  0.79953179 -0.79809065  1.37828910  2.73759452 -0.73893141  2.59880531
[13] -4.19098774 -0.48338688 -0.48645067

If we compare the values, we have exact same sums. However, the function of apply function doesn’t end there. We can calculate the quantile value of the matrix a and even provide some criteria, like calculating 25th and 75th percentile values by columns and rows. For example:

# Calculating 25th and 75th percentile values by the column
apply(a, 2, quantile, probs = c(0.25, 0.75))
          [,1]       [,2]       [,3]       [,4]       [,5]       [,6]
25% -0.8423442 -0.6907166 -0.7951699 -1.1551727 -1.0933942 -0.7135899
75%  0.3651124  0.3158423  0.5303270 -0.1662272  0.5507102  1.0754898
          [,7]       [,8]       [,9]      [,10]      [,11]      [,12]
25% -0.7997972 -0.3922864 -1.2192782 -0.4900615 -0.5460706 -0.7462452
75%  0.2414441  0.9069541  0.6958692  0.7759199  0.5697043  0.7117724

Here’s our values. Now, lets calculate 10th and 90th percentile values in each rows of a.

apply(a, 1, quantile, probs = c(0.10, 0.90))
          [,1]      [,2]       [,3]      [,4]       [,5]       [,6]       [,7]
10% -1.0694537 -1.144273 -2.0398378 -1.696547 -1.9961706 -0.7917604 -0.9581427
90%  0.6182671  1.441989  0.7044851  1.042399  0.7822355  1.1493056  1.4242421
          [,8]       [,9]     [,10]      [,11]     [,12]      [,13]     [,14]
10% -0.8727061 -0.8452812 -1.076491 -0.9527951 -1.159961 -0.9202480 -1.089469
90%  0.6788490  1.1021605  1.043711  1.2101656  1.409082  0.4615516  1.204281
         [,15]
10% -1.1549929
90%  0.9598868

Looks like we got the values we were looking for. The function went through each row of the matrix and calculated the 10th and 90th percentile values for us. And the results were provided in a matrix that had 2-rows and 15-columns.

We can use this function not just with a matrix but also with an array. for example:

# rnorm(2*2*10) tells R to create 10 2X2 matrices using random normal values
# c(2,2,10) tells R what the names of those 2X2 matrices are: they are 1 through 10
b <- array(rnorm(2 * 2 * 10), c(2, 2, 10))

# Let's check how the 1st and 7th matrices look like
b[, , 1:2] # Returns the first and second matrices
, , 1

           [,1]       [,2]
[1,]  0.3799450  1.0245053
[2,] -0.1882674 -0.8532041

, , 2

         [,1]       [,2]
[1,] 1.463727  0.1250609
[2,] 0.297211 -0.8219237
# b[,,7]#7th matrix

# If we want a range of matrices we can use the syntax like this
# b[,,2:8]#print all matrices from 2 through 8

# Calculating the means in array b
apply(b, c(1, 2), mean)
            [,1]       [,2]
[1,]  0.31224407 -0.2381830
[2,] -0.04370969  0.1739053
# Or
rowMeans(b, dims = 2)
            [,1]       [,2]
[1,]  0.31224407 -0.2381830
[2,] -0.04370969  0.1739053

We got the exact same outcomes.

Function

It is a multivariate version of lapply function, which works parallel over a set of arguments. Here’s the structure of this function, which is followed by the arguments:

str(mapply)
function (FUN, ..., MoreArgs = NULL, SIMPLIFY = TRUE, USE.NAMES = TRUE)  

Where,

  • FUN is a function to apply, e.g., mean, sd etc.
  •  is an optional arguments, e.g., prob.
  • MoreArgs is a list of other arguments to FUN.
  • SIMPLIFY is to tell R whether we want to simplify the results.
  • USE.NAMES is to tell R where we want the results by the names of the multiple variables, arrays etc.

Let’s take an example: in which I want to create a list containing the values between 1 and 6. I want 1 to repeat 6 times followed by 2 five times, etc. I can use either list or mapply function.

# Using list function
list(rep(1, 6), rep(2, 5), rep(3, 4), rep(4, 3), rep(5, 2), rep(6, 1))
[[1]]
[1] 1 1 1 1 1 1

[[2]]
[1] 2 2 2 2 2

[[3]]
[1] 3 3 3 3

[[4]]
[1] 4 4 4

[[5]]
[1] 5 5

[[6]]
[1] 6
# Using mapply function
mapply(rep, 1:6, 6:1) # rep is the 'repeat' function
[[1]]
[1] 1 1 1 1 1 1

[[2]]
[1] 2 2 2 2 2

[[3]]
[1] 3 3 3 3

[[4]]
[1] 4 4 4

[[5]]
[1] 5 5

[[6]]
[1] 6

We got the exact same lists, but the mapply code is much shorter and easier to comprehend. Let’s take one more example: I want to create an increasing set of data points with increasing mean simultaneously, while having a static standard deviation. For this, I can write a function, but it doesn’t work the way I want it. Let me try it a couple different ways ways:

# 1. Creating a Function
function_increasing_mean <- function(n, mean, sd) { # create a function with 3 arguments
  rnorm(n, mean, sd) # use random n numbers to create a list having the mean of 'mean' and the standard deviation of 'sd'
}
# Let's pass the function
function_increasing_mean(4, 1, 2)
[1] -1.5193146  3.4179477  1.2874957  0.5228852

I got a list of 4 numbers which have the mean of 1 and sd of 2, but this is not what I was looking for. Now, let’s try one more attempt;

# Range Method
function_increasing_mean(1:4, 1:4, 2)
[1] 2.129922 2.235477 2.529156 1.564761

Nope. I got a list of four numbers like before and this time the mean is not even 1. Now, let’s try mapply

# mapply Method
mapply(function_increasing_mean, 1:4, 1:4, 2)
[[1]]
[1] 3.804388

[[2]]
[1] 3.2546670 0.6776769

[[3]]
[1]  5.157456  2.220927 -1.719136

[[4]]
[1] 1.405115 3.169284 3.136489 1.114021

Here, we go. I have four lists of numbers. The first one has the mean of 1 (kind of) and fourth 4, while the sd remained same. It’s the result I was looking for.

I can access the thing using the list function but I have to manually type all the following codes:

list(function_increasing_mean(1, 1, 2), function_increasing_mean(2, 2, 2), function_increasing_mean(3, 3, 2), function_increasing_mean(4, 4, 2))
[[1]]
[1] 3.004481

[[2]]
[1] 3.507127 2.819970

[[3]]
[1]  2.161425 -1.404574  4.935112

[[4]]
[1] 3.344601 5.560084 7.324707 6.654232

tapply function

A very useful function in subsets of a vector. Here’s the arguments that it takes:

str(tapply)
function (X, INDEX, FUN = NULL, ..., default = NA, simplify = TRUE)  

Where,

  • X is a vector : 100 participants
  • INDEX is a factor or a list of factors(or else they are coerced to factors)- The vectors should be of the same length: 50 males and 50 females
  • FUN is s function that we want to apply
  •  optional arguments
  • simplify, simplify the results?
# Creating a vector e that has 30 elements
e <- c(rnorm(20), runif(20), rnorm(20, 1))
# Creating a factor variable f (which will have 3 categories) using the gl function and each of the variables will be repeated 20 times
f <- gl(3, 20)
# Now, tapply on e, pass the factor variable and calculate the means of the three groups
tapply(e, f, mean)
        1         2         3 
0.4235368 0.4528599 1.0091479 

The means of all of the three categories are populated. We got the results in a nice clean table. This is the default function. If we choose simplify=FALSE, we get little bit messy (as single vectors) outcome. For example:

tapply(e, f, mean, simplify = FALSE)
$`1`
[1] 0.4235368

$`2`
[1] 0.4528599

$`3`
[1] 1.009148

This is how the simplify function work. We can also use the tapply function in little bit complex function as well:

tapply(e, f, range)
$`1`
[1] -1.549748  1.593652

$`2`
[1] 0.1292044 0.9631355

$`3`
[1] -0.9592114  3.2768423
# tapply(e, f, median)#Gives Median
# tapply(e, f, mode)#Gives Mode
# tapply(e, f, sd)#Gives Standard Deviation

We got the largest and the smallest values in all of the three categories.

split function

This function takes a vector or other objects and splits it into the groups determined by a factor or list of factors. It is not a loop function but it is very handy to use in conjunction with other apply functions. Here’s the required arguments:

str(split)
function (x, f, drop = FALSE, ...)  

Where,

  • x is a vector (or list) or data frame
  • f is a factor (or coerced to one) or a list of factors
  • drop indicates whether empty factors levels should be dropped

Let’s check a simple example by using the same e and f objects from above:

split(e, f)
$`1`
 [1]  1.4618127  0.4799389 -1.3472795 -0.2473212 -0.8226667  0.8259930
 [7]  0.2362713 -0.3391439  1.0249322  0.8832730  1.5936519 -1.5497480
[13]  0.3956828 -1.0576296  1.2871582  1.3303311  0.9890388  0.3949247
[19]  1.5594840  1.3720320

$`2`
 [1] 0.3996126 0.2008936 0.2083913 0.5477170 0.2131851 0.5507455 0.5044728
 [8] 0.4093820 0.5985622 0.1813289 0.1691159 0.3402328 0.4921366 0.5325530
[15] 0.5390553 0.8783741 0.5826376 0.9631355 0.1292044 0.6164618

$`3`
 [1]  0.8033411  0.1701114  0.9569272  0.9942633  2.1616198 -0.9592114
 [7] -0.1841470  1.9297430  0.1556894  1.0039132  3.2768423  2.0701952
[13]  0.2773997  1.1883556  0.0273861  1.4982400  0.8396264  1.2838947
[19]  0.8666556  1.8221130

As we can see, the split function took the factors we created in object f and applied them to our data set e. We have equal number of elements in all three categories.

Once the data splitted we can use other functions like lapply or sapply. Here’s the most common way of using split function.

lapply(split(e, f), mean)
$`1`
[1] 0.4235368

$`2`
[1] 0.4528599

$`3`
[1] 1.009148
sapply(split(e, f), sd)
        1         2         3 
1.0028438 0.2294096 0.9672566 

We got the mean and standard deviation of all three categories. And we also know why the second outcome looks little better than the first one. It’s because we used sapply. These functions though, are not necessary because as we saw in the previous example, the tapply function does the same thing.

However, the nice thing about the split function is that we can use it in much more complex type of objects. For example let’s use it on ‘airquality’ data set.

head(airquality) # checking the structure of the data set
 
 
Ozone
<int>
Solar.R
<int>
Wind
<dbl>
Temp
<int>
Month
<int>
Day
<int>
1411907.46751
2361188.07252
31214912.67453
41831311.56254
5NANA14.35655
628NA14.96656

I want to take the mean of the Ozone, Solar Radiation, Wind, and Temperature by month. How do I do this? I will first split the data frame by months and calculate means.

split_airquality <- split(airquality, airquality$Month)
lapply(split_airquality, function(f) colMeans(f[, c("Ozone", "Solar.R", "Wind", "Temp")])) # function (f) is an anonymous variable
$`5`
   Ozone  Solar.R     Wind     Temp 
      NA       NA 11.62258 65.54839 

$`6`
    Ozone   Solar.R      Wind      Temp 
       NA 190.16667  10.26667  79.10000 

$`7`
     Ozone    Solar.R       Wind       Temp 
        NA 216.483871   8.941935  83.903226 

$`8`
    Ozone   Solar.R      Wind      Temp 
       NA        NA  8.793548 83.967742 

$`9`
   Ozone  Solar.R     Wind     Temp 
      NA 167.4333  10.1800  76.9000 

The results gave the means of Ozone, Solar.R, Wind, and Temp for the months 5 (i.e., May) through 9 (i.e., September). The Mean of the the Ozone and Solar.R are not calculated because there are some missing data. We can see the temperature rising through August and dropping in the month of September. The results are fine but let’s see if we get better outcome (in terms of how they look) when we apply sapply function.

sapply(split_airquality, function(f) colMeans(f[, c("Ozone", "Solar.R", "Wind", "Temp")]))
               5         6          7         8        9
Ozone         NA        NA         NA        NA       NA
Solar.R       NA 190.16667 216.483871        NA 167.4333
Wind    11.62258  10.26667   8.941935  8.793548  10.1800
Temp    65.54839  79.10000  83.903226 83.967742  76.9000

The results are properly managed in a tabular format. They are easy to read and understand. Much easier to compare the findings among the variables.

However, when we have the missing values, R doesn’t give us the mean for that variable. We can get rid of missing values and calculate mean for all of the variables. Here’s how we do it:

sapply(split_airquality, function(f) colMeans(f[, c("Ozone", "Solar.R", "Wind", "Temp")], na.rm = TRUE)) # gets rid of NAs
                5         6          7          8         9
Ozone    23.61538  29.44444  59.115385  59.961538  31.44828
Solar.R 181.29630 190.16667 216.483871 171.857143 167.43333
Wind     11.62258  10.26667   8.941935   8.793548  10.18000
Temp     65.54839  79.10000  83.903226  83.967742  76.90000

In the example above we talked about having factors of a single object or dataframe. Sometimes, we can have multiple objects with various factors. We can still use split function to compute the values of our interest. Let’s take an example:

# Creating a data set with 30 random numbers
g <- rnorm(30)

# First categorical variable
grade <- gl(3, 10) # 3 different grades of 10 each

# Second categorical variable
ethnicity <- gl(6, 5) # 6-ethnic groups of 5 each

# Calculating Interaction between grade and ethnicity
interaction(grade, ethnicity)
 [1] 1.1 1.1 1.1 1.1 1.1 1.2 1.2 1.2 1.2 1.2 2.3 2.3 2.3 2.3 2.3 2.4 2.4 2.4 2.4
[20] 2.4 3.5 3.5 3.5 3.5 3.5 3.6 3.6 3.6 3.6 3.6
18 Levels: 1.1 2.1 3.1 1.2 2.2 3.2 1.3 2.3 3.3 1.4 2.4 3.4 1.5 2.5 3.5 ... 3.6

There are total of 18 levels after interaction. Now, I want to split the vector g according to the categorical variables grade, and ethnicity.

# splitting the variable g and asking for the lists of grade and ethnicity and finally asking for the structure of all of the 18 distinct categories.
str(split(g, list(grade, ethnicity)))
List of 18
 $ 1.1: num [1:5] 0.82 -1.391 -0.618 -0.226 -1.448
 $ 2.1: num(0) 
 $ 3.1: num(0) 
 $ 1.2: num [1:5] 0.3894 1.5388 -0.1598 0.0697 -0.1458
 $ 2.2: num(0) 
 $ 3.2: num(0) 
 $ 1.3: num(0) 
 $ 2.3: num [1:5] 0.428 0.262 -0.459 1.383 0.216
 $ 3.3: num(0) 
 $ 1.4: num(0) 
 $ 2.4: num [1:5] -2.442 0.983 1.019 -0.626 -0.945
 $ 3.4: num(0) 
 $ 1.5: num(0) 
 $ 2.5: num(0) 
 $ 3.5: num [1:5] -0.922 1.179 -0.678 -0.609 2.146
 $ 1.6: num(0) 
 $ 2.6: num(0) 
 $ 3.6: num [1:5] -0.631 -1.293 1.934 -1.22 -1.412

If I want to use the split function, I don’t have to call for the interaction. It automatically does so for us. And obviously not all interaction have values. If we want to get rid of the empty levels, we have the option called drop, which gives back the interaction units that have values in them.

str(split(g, list(grade, ethnicity), drop = TRUE))
List of 6
 $ 1.1: num [1:5] 0.82 -1.391 -0.618 -0.226 -1.448
 $ 1.2: num [1:5] 0.3894 1.5388 -0.1598 0.0697 -0.1458
 $ 2.3: num [1:5] 0.428 0.262 -0.459 1.383 0.216
 $ 2.4: num [1:5] -2.442 0.983 1.019 -0.626 -0.945
 $ 3.5: num [1:5] -0.922 1.179 -0.678 -0.609 2.146
 $ 3.6: num [1:5] -0.631 -1.293 1.934 -1.22 -1.412

We got only the six interactions that have values in them.

Let’s wrap up with a fun figure (using lattice package and airquality data):

library(lattice)
densityplot(~Temp, groups = Month, data = airquality, plot.points = FALSE, auto.key = TRUE)

Lesson: 1. These are the basic aspects of using R. Without the knowledge of these functions, we always feel lost. So, better learn them!

  1. Most of these outcomes can also be achieved using purrr package, which uses an entirely different grammar.

References:

- Field, A., Miles, J., & Field, Z. (2012). Discovering statistics using R. Sage.

- Irizarry, R.(2021). Data Science: R Basics. https://learning.edx.org/course/course-v1:HarvardX+PH125.1x+2T2020/progress

- Peng, R.,D.(2020). R Programming for Data Science. Leanpub.

Comments

Popular posts from this blog

Education Matters: Understanding Nepal’s Education (Publication Date: June 19, 2023, Ratopati-English, Link at the End)

Multiple Correspondence Analysis (MCA) in Educational Data

charting Concept and Computation: Maps for the Deep Learning Frontier