Cytometry in R:

Compensation in flowCore

Published

October 22, 2024

Getting Set Up

If you haven’t already, please go ahead and install the following packages:

Code
# If not yet done so, install required packages
install.packages("BiocManager")
BiocManager::install("BiocStyle")
BiocManager::install("flowCore")
BiocManager::install("ggcyto")
BiocManager::install("FlowSOM")

Once installed, load the required packages into R by calling library.

Code
library(flowCore)
library(ggcyto)
library(BiocStyle)
library(FlowSOM)

For this example, I am using an .fcs file contained within the FlowSOM package’s extdata folder. I find it using system.file, and then list.files to show the full path to it’s location. Once it’s located, I will load it into a flowframe via flowCore similar to what we can see in the Christopher Hall video.

Code
File_Location <- system.file("extdata", package = "FlowSOM")
FCSFile_Example <- list.files(File_Location, pattern="fcs", full.names=TRUE)
flowframe <- read.FCS(FCSFile_Example, transformation = FALSE, truncate_max_range = FALSE)

If you have your own .fcs file, instead of system.file I usually will use file.path to indicate the location. For example:

Code
Location <- file.path("C:", "Users", "StepUpCytometry", "Desktop", "TodaysExperiment")
ExperimentFCSFiles <- list.files(Location, pattern=".fcs", full.names=TRUE)
flowframe <- read.FCS(FCSFile_Example[1], transformation = FALSE, truncate_max_range = FALSE)

flowFrame objects

Once you have loaded the .fcs file into a flowframe, let’s quickly poke around to see what this kind of object looks like in R.

Code
flowframe
flowFrame object '698c2f7d-15be-452c-b0c4-4bbb2d55db06'
with 19225 cells and 18 observables:
               name   desc     range  minRange  maxRange
$P1            Time     NA    262144         0    262144
$P2           FSC-A     NA    262144         0    262144
$P3           FSC-H     NA    262144         0    262144
$P4           FSC-W     NA    262144         0    262144
$P5           SSC-A     NA    262144         0    262144
...             ...    ...       ...       ...       ...
$P14      APC-Cy7-A   TCRb    262144    -111.0    262144
$P15           PE-A  NK1/1    262144    -111.0    262144
$P16 PE-Texas Red-A    CD4    262144    -111.0    262144
$P17       PE-Cy5-A   CD19    262144    -111.0    262144
$P18       PE-Cy7-A    CD3    262144     -93.6    262144
248 keywords are stored in the 'description' slot

Within flowCore flowframe and flowset objects, the information for an individual .fcs file is contained within exprs (data), parameters and description (keywords). For this example I will abbreviate them to the first 10 entries, if you want to see the full entries, run the code-chunks below on your own computer.

Code
View(flowframe@description)
$`$BEGINANALYSIS`
[1] "0"

$`$BEGINDATA`
[1] "5481"

$`$BEGINSTEXT`
[1] "0"

$`$BTIM`
[1] "00:29:01"

$`$BYTEORD`
[1] "4,3,2,1"

$`$DATATYPE`
[1] "F"

$`$DATE`
[1] "02-JUL-2013"

$`$ENDANALYSIS`
[1] "0"

$`$ENDDATA`
[1] "1389680"

$`$ENDSTEXT`
[1] "0"

Description (keywords) is a named list containing everything from voltage settings, compensation matrix, metadata, software configurations, etc.

Code
View(flowframe@parameters@data)
                  name  desc  range minRange maxRange
$P1               Time  <NA> 262144     0.00   262144
$P2              FSC-A  <NA> 262144     0.00   262144
$P3              FSC-H  <NA> 262144     0.00   262144
$P4              FSC-W  <NA> 262144     0.00   262144
$P5              SSC-A  <NA> 262144     0.00   262144
$P6              SSC-H  <NA> 262144     0.00   262144
$P7              SSC-W  <NA> 262144     0.00   262144
$P8             FITC-A   GFP 262144   -44.25   262144
$P9     Pacific Blue-A   CD8 262144  -111.00   262144
$P10          AmCyan-A   l/d 262144  -111.00   262144
$P11        Qdot 605-A  <NA> 262144  -111.00   262144
$P12             APC-A TCRyd 262144  -111.00   262144
$P13 Alexa Fluor 700-A  CD45 262144   -58.80   262144
$P14         APC-Cy7-A  TCRb 262144  -111.00   262144
$P15              PE-A NK1/1 262144  -111.00   262144
$P16    PE-Texas Red-A   CD4 262144  -111.00   262144
$P17          PE-Cy5-A  CD19 262144  -111.00   262144
$P18          PE-Cy7-A   CD3 262144   -93.60   262144

Parameters is one of the many locations containing the name of the fluorophore, and the ligand name.

Code
View(flowframe@exprs)
      Time     FSC-A    FSC-H  FSC-W    SSC-A    SSC-H SSC-W FITC-A
 [1,]  0.0 110519.01 69460.30 104275 42186.75 70610.42 39155  22.50
 [2,]  0.2  77340.06 67542.95  75042 54567.75 80071.48 44662  54.75
 [3,]  0.5  89551.35 67677.27  86718 46320.00 75690.11 40106  43.50
 [4,]  0.9  78047.55 68193.54  75006 44152.50 68023.37 42538  63.75
 [5,]  1.0  77033.25 67900.25  74351 33922.50 68753.52 32335  25.50
 [6,]  1.3  97850.34 69162.95  92719 39126.00 68525.66 37419  46.50
 [7,]  1.5  90782.37 67412.77  88255 33993.75 68022.80 32751  47.25
 [8,]  1.5  56237.58 66731.59  55230 27647.25 66983.00 27050   9.75
 [9,]  2.1  85523.76 68674.70  81615 36888.75 68588.56 35247  60.75
[10,]  2.2  84030.03 66443.77  82882 19114.50 68897.15 18182  51.00
      Pacific Blue-A AmCyan-A Qdot 605-A      APC-A Alexa Fluor 700-A
 [1,]         124.62   353.40    2484.96  670.32001          6499.920
 [2,]         152.52   582.18    1831.17 2626.67993          2898.000
 [3,]          38.13   348.75    6149.16 8242.08008          4729.200
 [4,]          46.50   526.38     679.83  225.95999          6376.440
 [5,]          50.22   171.12      60.45   54.60000          4185.720
 [6,]        5514.90  1886.04     342.24  811.44000          6111.000
 [7,]       15879.75  4655.58     121.83  367.07999          8005.200
 [8,]          93.93   331.08     166.47   97.43999           -37.800
 [9,]        7871.52  2282.22     242.73  549.35999          9285.359
[10,]          19.53  -106.02     919.77  420.84000          4166.400
       APC-Cy7-A     PE-A PE-Texas Red-A PE-Cy5-A PE-Cy7-A
 [1,] 7297.07959  6518.85       23279.10  9873.50 11799.45
 [2,] 1798.43994  1076.40         484.25  1264.25 10302.50
 [3,] 2652.71997  1014.00         587.60 29994.90  2083.25
 [4,] 3202.07983 44454.15       12822.55  3282.50   588.90
 [5,] 1896.71997   107.90          65.00  -165.75   295.10
 [6,] 5848.07959   467.35          66.95   744.90  5811.65
 [7,] 6662.03955   353.60          52.00   148.85  5317.00
 [8,]  -90.71999   -64.35         -21.45   152.10    48.75
 [9,] 6241.19971   228.15          74.75   148.85  4195.75
[10,] 5498.63965  2813.20        7030.40  3245.45 11470.55

And exprs contains the actual raw data, with each row representing the measurements of an individual cell.

Applying Compensation

For the question of compensation, the compensation matrix is stored under keyword “SPILL” within the description (keyword) list and is a matrix object.

Code
flowframe@description[["SPILL"]]
            FITC-A Pacific Blue-A     AmCyan-A  Qdot 605-A       APC-A
 [1,] 1.000000e+00   0.0473734538 0.7135625558 0.053295127 0.017115568
 [2,] 4.843844e-04   1.0000000000 0.2714277161 0.006727130 0.001736034
 [3,] 8.109116e-04   0.0776772290 1.0000000000 0.048265462 0.001816442
 [4,] 8.625151e-05   0.0002495544 0.0005347592 1.000000000 0.066816169
 [5,] 4.354693e-04   0.0000000000 0.0025199164 0.230572282 1.000000000
 [6,] 1.246572e-03   0.0046372490 0.0100473729 0.019708302 0.026527051
 [7,] 0.000000e+00   0.0022519623 0.0028149513 0.024208587 0.085429249
 [8,] 1.166385e-03   0.0017175023 0.0059660591 0.019886864 0.001551292
 [9,] 5.446396e-04   0.0005402827 0.0016208480 0.099715889 0.001768989
[10,] 3.372389e-04   0.0005018117 0.0016727050 0.224393397 0.252233134
[11,] 8.522607e-04   0.0031704107 0.0068692232 0.004755614 0.003579495
      Alexa Fluor 700-A    APC-Cy7-A        PE-A PE-Texas Red-A    PE-Cy5-A
 [1,]      0.0010697231 8.659662e-04 0.009933141   0.0024832865 0.004966571
 [2,]      0.0001085021 0.000000e+00 0.000000000   0.0000000000 0.000000000
 [3,]      0.0011352762 1.773193e-03 0.000000000   0.0001756974 0.003689648
 [4,]      0.0002576045 2.606712e-05 0.000000000   0.0039119851 0.121894463
 [5,]      0.1391643652 7.020152e-02 0.000000000   0.0018870336 0.268965180
 [6,]      1.0000000000 4.934763e-01 0.000000000   0.0000000000 0.009723261
 [7,]      0.0572918382 1.000000e+00 0.000000000   0.0000000000 0.023871598
 [8,]      0.0002449408 0.000000e+00 1.000000000   0.3045236332 0.075751838
 [9,]      0.0001829989 0.000000e+00 0.266786250   1.0000000000 0.426046132
[10,]      0.0405280277 2.108778e-02 0.032047816   0.0108725826 1.000000000
[11,]      0.0033408617 5.555603e-02 0.069246179   0.0221587763 0.013479923
          PE-Cy7-A
 [1,] 0.000000e+00
 [2,] 3.875075e-05
 [3,] 9.595787e-04
 [4,] 3.641730e-05
 [5,] 1.276409e-02
 [6,] 5.765395e-02
 [7,] 1.766660e-01
 [8,] 3.567193e-03
 [9,] 2.574693e-02
[10,] 6.295464e-02
[11,] 1.000000e+00

In the case of Christopher Hall’s video #2’s example, if we look at the initial data (from flowframe@exprs) we can see the initial values for the first ten cells

Example <- flowframe@exprs
head(Example, 10)
      Time     FSC-A    FSC-H  FSC-W    SSC-A    SSC-H SSC-W FITC-A
 [1,]  0.0 110519.01 69460.30 104275 42186.75 70610.42 39155  22.50
 [2,]  0.2  77340.06 67542.95  75042 54567.75 80071.48 44662  54.75
 [3,]  0.5  89551.35 67677.27  86718 46320.00 75690.11 40106  43.50
 [4,]  0.9  78047.55 68193.54  75006 44152.50 68023.37 42538  63.75
 [5,]  1.0  77033.25 67900.25  74351 33922.50 68753.52 32335  25.50
 [6,]  1.3  97850.34 69162.95  92719 39126.00 68525.66 37419  46.50
 [7,]  1.5  90782.37 67412.77  88255 33993.75 68022.80 32751  47.25
 [8,]  1.5  56237.58 66731.59  55230 27647.25 66983.00 27050   9.75
 [9,]  2.1  85523.76 68674.70  81615 36888.75 68588.56 35247  60.75
[10,]  2.2  84030.03 66443.77  82882 19114.50 68897.15 18182  51.00
      Pacific Blue-A AmCyan-A Qdot 605-A      APC-A Alexa Fluor 700-A
 [1,]         124.62   353.40    2484.96  670.32001          6499.920
 [2,]         152.52   582.18    1831.17 2626.67993          2898.000
 [3,]          38.13   348.75    6149.16 8242.08008          4729.200
 [4,]          46.50   526.38     679.83  225.95999          6376.440
 [5,]          50.22   171.12      60.45   54.60000          4185.720
 [6,]        5514.90  1886.04     342.24  811.44000          6111.000
 [7,]       15879.75  4655.58     121.83  367.07999          8005.200
 [8,]          93.93   331.08     166.47   97.43999           -37.800
 [9,]        7871.52  2282.22     242.73  549.35999          9285.359
[10,]          19.53  -106.02     919.77  420.84000          4166.400
       APC-Cy7-A     PE-A PE-Texas Red-A PE-Cy5-A PE-Cy7-A
 [1,] 7297.07959  6518.85       23279.10  9873.50 11799.45
 [2,] 1798.43994  1076.40         484.25  1264.25 10302.50
 [3,] 2652.71997  1014.00         587.60 29994.90  2083.25
 [4,] 3202.07983 44454.15       12822.55  3282.50   588.90
 [5,] 1896.71997   107.90          65.00  -165.75   295.10
 [6,] 5848.07959   467.35          66.95   744.90  5811.65
 [7,] 6662.03955   353.60          52.00   148.85  5317.00
 [8,]  -90.71999   -64.35         -21.45   152.10    48.75
 [9,] 6241.19971   228.15          74.75   148.85  4195.75
[10,] 5498.63965  2813.20        7030.40  3245.45 11470.55

And after compensation, we can compare the values to see how they change once compensation is applied:

TheComps <- spillover(flowframe)
flowframe_comped <- compensate(flowframe, TheComps[[1]])
head(flowframe_comped@exprs, 10)
      Time     FSC-A    FSC-H  FSC-W    SSC-A    SSC-H SSC-W    FITC-A
 [1,]  0.0 110519.01 69460.30 104275 42186.75 70610.42 39155 -6.313441
 [2,]  0.2  77340.06 67542.95  75042 54567.75 80071.48 44662 40.889262
 [3,]  0.5  89551.35 67677.27  86718 46320.00 75690.11 40106 28.703288
 [4,]  0.9  78047.55 68193.54  75006 44152.50 68023.37 42538  3.952831
 [5,]  1.0  77033.25 67900.25  74351 33922.50 68753.52 32335 20.015388
 [6,]  1.3  97850.34 69162.95  92719 39126.00 68525.66 37419 31.672404
 [7,]  1.5  90782.37 67412.77  88255 33993.75 68022.80 32751 25.865051
 [8,]  1.5  56237.58 66731.59  55230 27647.25 66983.00 27050  9.484324
 [9,]  2.1  85523.76 68674.70  81615 36888.75 68588.56 35247 42.592859
[10,]  2.2  84030.03 66443.77  82882 19114.50 68897.15 18182 33.278025
      Pacific Blue-A   AmCyan-A Qdot 605-A      APC-A Alexa Fluor 700-A
 [1,]       30.36604  171.18751  -63.67789  196.18180        6237.55480
 [2,]       72.73710  429.49950 1085.01643 2404.24067        2529.70261
 [3,]      -13.50348  245.75616 -797.27920  672.76698        3412.65144
 [4,]      -76.47428  214.64085 -304.80468  -50.59753        6360.16110
 [5,]       21.38416  108.33454   18.42346   14.75374        4202.85112
 [6,]     5441.98121  283.50222  -87.46906  276.15010        5882.77782
 [7,]    15804.39212  230.99478 -213.06889  -95.08870        7854.81455
 [8,]       69.99279  305.62480  111.34597   64.67410         -48.09541
 [9,]     7811.96835   11.47178  -72.01849  169.90971        9164.87054
[10,]      -24.75892 -256.30967   34.39165  -18.69712        3958.50180
       APC-Cy7-A         PE-A PE-Texas Red-A    PE-Cy5-A    PE-Cy7-A
 [1,] 3643.61990  -359.137162   23165.188223  -299.69303 10217.17443
 [2,] -187.81341   326.643836     148.564628   238.72639 10138.67126
 [3,]  296.54263    -7.998177     269.458448 29756.36525   -54.89582
 [4,]   58.79272 44650.326470    -776.964679   217.08501    59.34040
 [5,] -179.04186    99.061786      35.034765  -232.79967    97.52624
 [6,] 2637.97557   129.750163     -88.694117   520.85534  4971.46997
 [7,] 2546.33437    64.102696     -64.709637    25.46337  4414.51005
 [8,]  -77.91719   -71.686136      -2.898771   128.19014    56.42285
 [9,] 1518.92617    -6.299055       1.826056   -59.83295  3400.27881
[10,] 2958.99121   289.975885    6707.161943   116.35005 10539.00981

As you might be able to tell, FSC SSC parameters remain the same, while the values for the Fluorophore columns have been adjusted. This is due to no columns being present for FSC SSC Time etc. in the Spillover matrix.

Visualizing (ggcyto)

We can visualize this with the with the ggcyto package to see the effects for the before vs. after:

Code
autoplot(flowframe, x = "PE-Cy7-A", y = "PE-Texas Red-A", bins = 270) +
  scale_x_flowjo_biexp() + scale_y_flowjo_biexp() + theme_bw()

autoplot(flowframe_comped, x = "PE-Cy7-A", y = "PE-Texas Red-A", bins = 270) +
  scale_x_flowjo_biexp() + scale_y_flowjo_biexp() + theme_bw()

Applying an External Compensation Matrix.

The above works great when the acquisition software already stored the compensation matrix in the SPILL keyword. But what happens when it is not there, or we want to apply an external compensation matrix? It can be done, but there is a bit more data-tidying that needs to be done first to make the .csv output from FlowJo compatible with the matrix format flowCore stores SPILL as. To save someone who is just getting started a headache, here is a worked out example below:

If we look closer at what type of object is returned by spillover, we can see that the initial command is returning three list:

Code
TheComps <- spillover(flowframe)
TheComps
$SPILL
            FITC-A Pacific Blue-A     AmCyan-A  Qdot 605-A       APC-A
 [1,] 1.000000e+00   0.0473734538 0.7135625558 0.053295127 0.017115568
 [2,] 4.843844e-04   1.0000000000 0.2714277161 0.006727130 0.001736034
 [3,] 8.109116e-04   0.0776772290 1.0000000000 0.048265462 0.001816442
 [4,] 8.625151e-05   0.0002495544 0.0005347592 1.000000000 0.066816169
 [5,] 4.354693e-04   0.0000000000 0.0025199164 0.230572282 1.000000000
 [6,] 1.246572e-03   0.0046372490 0.0100473729 0.019708302 0.026527051
 [7,] 0.000000e+00   0.0022519623 0.0028149513 0.024208587 0.085429249
 [8,] 1.166385e-03   0.0017175023 0.0059660591 0.019886864 0.001551292
 [9,] 5.446396e-04   0.0005402827 0.0016208480 0.099715889 0.001768989
[10,] 3.372389e-04   0.0005018117 0.0016727050 0.224393397 0.252233134
[11,] 8.522607e-04   0.0031704107 0.0068692232 0.004755614 0.003579495
      Alexa Fluor 700-A    APC-Cy7-A        PE-A PE-Texas Red-A    PE-Cy5-A
 [1,]      0.0010697231 8.659662e-04 0.009933141   0.0024832865 0.004966571
 [2,]      0.0001085021 0.000000e+00 0.000000000   0.0000000000 0.000000000
 [3,]      0.0011352762 1.773193e-03 0.000000000   0.0001756974 0.003689648
 [4,]      0.0002576045 2.606712e-05 0.000000000   0.0039119851 0.121894463
 [5,]      0.1391643652 7.020152e-02 0.000000000   0.0018870336 0.268965180
 [6,]      1.0000000000 4.934763e-01 0.000000000   0.0000000000 0.009723261
 [7,]      0.0572918382 1.000000e+00 0.000000000   0.0000000000 0.023871598
 [8,]      0.0002449408 0.000000e+00 1.000000000   0.3045236332 0.075751838
 [9,]      0.0001829989 0.000000e+00 0.266786250   1.0000000000 0.426046132
[10,]      0.0405280277 2.108778e-02 0.032047816   0.0108725826 1.000000000
[11,]      0.0033408617 5.555603e-02 0.069246179   0.0221587763 0.013479923
          PE-Cy7-A
 [1,] 0.000000e+00
 [2,] 3.875075e-05
 [3,] 9.595787e-04
 [4,] 3.641730e-05
 [5,] 1.276409e-02
 [6,] 5.765395e-02
 [7,] 1.766660e-01
 [8,] 3.567193e-03
 [9,] 2.574693e-02
[10,] 6.295464e-02
[11,] 1.000000e+00

$spillover
NULL

$`$SPILLOVER`
NULL

And that the first item that we passed to compensate is the matrix array object containing the spillover/compensation matrix. If we didn’t have it in the fcs file to begin with (either due to a difference in acquisition software, or desire to apply a separate compensation matrix you generated elsewhere), you could format it similarly and swap it in.

Code
TheComps[[1]]
            FITC-A Pacific Blue-A     AmCyan-A  Qdot 605-A       APC-A
 [1,] 1.000000e+00   0.0473734538 0.7135625558 0.053295127 0.017115568
 [2,] 4.843844e-04   1.0000000000 0.2714277161 0.006727130 0.001736034
 [3,] 8.109116e-04   0.0776772290 1.0000000000 0.048265462 0.001816442
 [4,] 8.625151e-05   0.0002495544 0.0005347592 1.000000000 0.066816169
 [5,] 4.354693e-04   0.0000000000 0.0025199164 0.230572282 1.000000000
 [6,] 1.246572e-03   0.0046372490 0.0100473729 0.019708302 0.026527051
 [7,] 0.000000e+00   0.0022519623 0.0028149513 0.024208587 0.085429249
 [8,] 1.166385e-03   0.0017175023 0.0059660591 0.019886864 0.001551292
 [9,] 5.446396e-04   0.0005402827 0.0016208480 0.099715889 0.001768989
[10,] 3.372389e-04   0.0005018117 0.0016727050 0.224393397 0.252233134
[11,] 8.522607e-04   0.0031704107 0.0068692232 0.004755614 0.003579495
      Alexa Fluor 700-A    APC-Cy7-A        PE-A PE-Texas Red-A    PE-Cy5-A
 [1,]      0.0010697231 8.659662e-04 0.009933141   0.0024832865 0.004966571
 [2,]      0.0001085021 0.000000e+00 0.000000000   0.0000000000 0.000000000
 [3,]      0.0011352762 1.773193e-03 0.000000000   0.0001756974 0.003689648
 [4,]      0.0002576045 2.606712e-05 0.000000000   0.0039119851 0.121894463
 [5,]      0.1391643652 7.020152e-02 0.000000000   0.0018870336 0.268965180
 [6,]      1.0000000000 4.934763e-01 0.000000000   0.0000000000 0.009723261
 [7,]      0.0572918382 1.000000e+00 0.000000000   0.0000000000 0.023871598
 [8,]      0.0002449408 0.000000e+00 1.000000000   0.3045236332 0.075751838
 [9,]      0.0001829989 0.000000e+00 0.266786250   1.0000000000 0.426046132
[10,]      0.0405280277 2.108778e-02 0.032047816   0.0108725826 1.000000000
[11,]      0.0033408617 5.555603e-02 0.069246179   0.0221587763 0.013479923
          PE-Cy7-A
 [1,] 0.000000e+00
 [2,] 3.875075e-05
 [3,] 9.595787e-04
 [4,] 3.641730e-05
 [5,] 1.276409e-02
 [6,] 5.765395e-02
 [7,] 1.766660e-01
 [8,] 3.567193e-03
 [9,] 2.574693e-02
[10,] 6.295464e-02
[11,] 1.000000e+00
Code
class(TheComps[[1]])
[1] "matrix" "array" 

In the case of the FlowSOM example file, the compensation applied appears to be mostly correct. The closest example to an overcompensation was the following:

Code
autoplot(flowframe_comped, x = "Qdot 605-A", y = "Alexa Fluor 700-A", bins = 270) +
  scale_x_flowjo_biexp() + scale_y_flowjo_biexp() + theme_bw()

In FlowJo, I edited the compensation matrix and then hit Save Matrix and saved as a .csv. This can then be imported into R on an individual computer something similar to as follows:

# Set Location for your own folder
Location <- file.path("C:", "Users", "StepUpCytometry", "Desktop", "MyCompensations")
TheMatrix <- list.files(Location, pattern = ".csv", full.names = TRUE)
MyMatrixOfInterest <- TheMatrix[4] # Ie, the 4th .csv file in the Matrix list above
MyCompensation <- read.csv("MyComp.csv", check.names=FALSE)
MyCompensation
                             FITC-A :: GFP Pacific Blue-A :: CD8
1              FITC-A :: GFP  1.0000000000          0.0473734550
2      Pacific Blue-A :: CD8  0.0004843844          1.0000000000
3            AmCyan-A :: l_d  0.0008109116          0.0776772276
4                 Qdot 605-A  0.0000862515          0.0002495544
5             APC-A :: TCRyd  0.0004354693          0.0000000000
6  Alexa Fluor 700-A :: CD45  0.0012465719          0.0046372488
7          APC-Cy7-A :: TCRb  0.0000000000          0.0022519622
8              PE-A :: NK1_1  0.0011663849          0.0017175023
9      PE-Texas Red-A :: CD4  0.0005446396          0.0005402827
10          PE-Cy5-A :: CD19  0.0003372389          0.0005018117
11           PE-Cy7-A :: CD3  0.0008522607          0.0031704106
   AmCyan-A :: l_d   Qdot 605-A APC-A :: TCRyd Alexa Fluor 700-A :: CD45
1     0.7135625482  0.053295128    0.017115569              0.0010697230
2     0.2714277208  0.006727130    0.001736034              0.0001085021
3     1.0000000000  0.048265461    0.001816442              0.0011352762
4     0.0005347593  1.000000000    0.066816166              0.0000000000
5     0.0025199165  0.230572283    1.000000000              0.1391643584
6     0.0100473734 -0.010000000    0.026527051              1.0000000000
7     0.0028149514  0.024208587    0.085429251              0.0572918393
8     0.0059660589  0.019886864    0.001551292              0.0002449408
9     0.0016208480  0.099715889    0.001768990              0.0001829989
10    0.0016727051  0.224393398    0.252233148              0.0405280292
11    0.0068692234  0.004755614    0.003579495              0.0033408618
   APC-Cy7-A :: TCRb PE-A :: NK1_1 PE-Texas Red-A :: CD4 PE-Cy5-A :: CD19
1       0.0008659662   0.009933141          0.0024832867      0.004966570
2       0.0000000000   0.000000000          0.0000000000      0.000000000
3       0.0017731935   0.000000000          0.0001756974      0.003689648
4       0.0000260671   0.000000000          0.0039119851      0.121894464
5       0.0702015162   0.000000000          0.0018870336      0.268965185
6       0.4934762716   0.000000000          0.0000000000      0.009723261
7       1.0000000000   0.000000000          0.0000000000      0.023871597
8       0.0000000000   1.000000000          0.3045236468      0.075751841
9       0.0000000000   0.266786248          1.0000000000      0.426046133
10      0.0210877769   0.032047816          0.0108725829      1.000000000
11      0.0555560328   0.069246180          0.0221587755      0.013479923
   PE-Cy7-A :: CD3
1     0.0000000000
2     0.0000387507
3     0.0009595787
4     0.0000364173
5     0.0127640879
6     0.0576539524
7     0.1766659617
8     0.0035671934
9     0.0257469285
10    0.0629546419
11    1.0000000000

If we compare the above to the matrix currently stored in spillover, we can see a couple potential issues, namely, the FlowJo exported .csv has the names of the ligands still included; and the row.names column is still present.

TheComps[1]
$SPILL
            FITC-A Pacific Blue-A     AmCyan-A  Qdot 605-A       APC-A
 [1,] 1.000000e+00   0.0473734538 0.7135625558 0.053295127 0.017115568
 [2,] 4.843844e-04   1.0000000000 0.2714277161 0.006727130 0.001736034
 [3,] 8.109116e-04   0.0776772290 1.0000000000 0.048265462 0.001816442
 [4,] 8.625151e-05   0.0002495544 0.0005347592 1.000000000 0.066816169
 [5,] 4.354693e-04   0.0000000000 0.0025199164 0.230572282 1.000000000
 [6,] 1.246572e-03   0.0046372490 0.0100473729 0.019708302 0.026527051
 [7,] 0.000000e+00   0.0022519623 0.0028149513 0.024208587 0.085429249
 [8,] 1.166385e-03   0.0017175023 0.0059660591 0.019886864 0.001551292
 [9,] 5.446396e-04   0.0005402827 0.0016208480 0.099715889 0.001768989
[10,] 3.372389e-04   0.0005018117 0.0016727050 0.224393397 0.252233134
[11,] 8.522607e-04   0.0031704107 0.0068692232 0.004755614 0.003579495
      Alexa Fluor 700-A    APC-Cy7-A        PE-A PE-Texas Red-A    PE-Cy5-A
 [1,]      0.0010697231 8.659662e-04 0.009933141   0.0024832865 0.004966571
 [2,]      0.0001085021 0.000000e+00 0.000000000   0.0000000000 0.000000000
 [3,]      0.0011352762 1.773193e-03 0.000000000   0.0001756974 0.003689648
 [4,]      0.0002576045 2.606712e-05 0.000000000   0.0039119851 0.121894463
 [5,]      0.1391643652 7.020152e-02 0.000000000   0.0018870336 0.268965180
 [6,]      1.0000000000 4.934763e-01 0.000000000   0.0000000000 0.009723261
 [7,]      0.0572918382 1.000000e+00 0.000000000   0.0000000000 0.023871598
 [8,]      0.0002449408 0.000000e+00 1.000000000   0.3045236332 0.075751838
 [9,]      0.0001829989 0.000000e+00 0.266786250   1.0000000000 0.426046132
[10,]      0.0405280277 2.108778e-02 0.032047816   0.0108725826 1.000000000
[11,]      0.0033408617 5.555603e-02 0.069246179   0.0221587763 0.013479923
          PE-Cy7-A
 [1,] 0.000000e+00
 [2,] 3.875075e-05
 [3,] 9.595787e-04
 [4,] 3.641730e-05
 [5,] 1.276409e-02
 [6,] 5.765395e-02
 [7,] 1.766660e-01
 [8,] 3.567193e-03
 [9,] 2.574693e-02
[10,] 6.295464e-02
[11,] 1.000000e+00

We could of course edit this manually by hand, or we can have R do it. First we remove the row name column:

MyCompensation <- MyCompensation[-1] #Removed the first rowname column
colnames(MyCompensation)
 [1] "FITC-A :: GFP"             "Pacific Blue-A :: CD8"    
 [3] "AmCyan-A :: l_d"           "Qdot 605-A"               
 [5] "APC-A :: TCRyd"            "Alexa Fluor 700-A :: CD45"
 [7] "APC-Cy7-A :: TCRb"         "PE-A :: NK1_1"            
 [9] "PE-Texas Red-A :: CD4"     "PE-Cy5-A :: CD19"         
[11] "PE-Cy7-A :: CD3"          

With that done, we remove everything within the column names that is present after -A:

colnames(MyCompensation) <- sub("-A.*", "-A", colnames(MyCompensation))
colnames(MyCompensation)
 [1] "FITC-A"            "Pacific Blue-A"    "AmCyan-A"         
 [4] "Qdot 605-A"        "APC-A"             "Alexa Fluor 700-A"
 [7] "APC-Cy7-A"         "PE-A"              "PE-Texas Red-A"   
[10] "PE-Cy5-A"          "PE-Cy7-A"         

And finally, we convert it from a data.frame to a matrix

MyCompensation <- as.matrix(MyCompensation)
MyCompensation
            FITC-A Pacific Blue-A     AmCyan-A   Qdot 605-A       APC-A
 [1,] 1.0000000000   0.0473734550 0.7135625482  0.053295128 0.017115569
 [2,] 0.0004843844   1.0000000000 0.2714277208  0.006727130 0.001736034
 [3,] 0.0008109116   0.0776772276 1.0000000000  0.048265461 0.001816442
 [4,] 0.0000862515   0.0002495544 0.0005347593  1.000000000 0.066816166
 [5,] 0.0004354693   0.0000000000 0.0025199165  0.230572283 1.000000000
 [6,] 0.0012465719   0.0046372488 0.0100473734 -0.010000000 0.026527051
 [7,] 0.0000000000   0.0022519622 0.0028149514  0.024208587 0.085429251
 [8,] 0.0011663849   0.0017175023 0.0059660589  0.019886864 0.001551292
 [9,] 0.0005446396   0.0005402827 0.0016208480  0.099715889 0.001768990
[10,] 0.0003372389   0.0005018117 0.0016727051  0.224393398 0.252233148
[11,] 0.0008522607   0.0031704106 0.0068692234  0.004755614 0.003579495
      Alexa Fluor 700-A    APC-Cy7-A        PE-A PE-Texas Red-A    PE-Cy5-A
 [1,]      0.0010697230 0.0008659662 0.009933141   0.0024832867 0.004966570
 [2,]      0.0001085021 0.0000000000 0.000000000   0.0000000000 0.000000000
 [3,]      0.0011352762 0.0017731935 0.000000000   0.0001756974 0.003689648
 [4,]      0.0000000000 0.0000260671 0.000000000   0.0039119851 0.121894464
 [5,]      0.1391643584 0.0702015162 0.000000000   0.0018870336 0.268965185
 [6,]      1.0000000000 0.4934762716 0.000000000   0.0000000000 0.009723261
 [7,]      0.0572918393 1.0000000000 0.000000000   0.0000000000 0.023871597
 [8,]      0.0002449408 0.0000000000 1.000000000   0.3045236468 0.075751841
 [9,]      0.0001829989 0.0000000000 0.266786248   1.0000000000 0.426046133
[10,]      0.0405280292 0.0210877769 0.032047816   0.0108725829 1.000000000
[11,]      0.0033408618 0.0555560328 0.069246180   0.0221587755 0.013479923
          PE-Cy7-A
 [1,] 0.0000000000
 [2,] 0.0000387507
 [3,] 0.0009595787
 [4,] 0.0000364173
 [5,] 0.0127640879
 [6,] 0.0576539524
 [7,] 0.1766659617
 [8,] 0.0035671934
 [9,] 0.0257469285
[10,] 0.0629546419
[11,] 1.0000000000

Now, returning to the original example of applying the spillover, we provide the external:

flowframe_ExternalComp <- compensate(flowframe, MyCompensation)

And visualize the effect before and after:

Code
autoplot(flowframe_comped, x = "Qdot 605-A", y = "Alexa Fluor 700-A", bins = 270) +
  scale_x_flowjo_biexp() + scale_y_flowjo_biexp() + theme_bw()

Code
autoplot(flowframe_ExternalComp, x = "Qdot 605-A", y = "Alexa Fluor 700-A", bins = 270) +
  scale_x_flowjo_biexp() + scale_y_flowjo_biexp() + theme_bw()

Notice it did move (still ugly-ish but I only barely adjusted the compensation matrix due to lack of time).

Hope this helps clarify a little!!! Keep it up!

System Information

Code
sessionInfo()
R version 4.4.1 (2024-06-14 ucrt)
Platform: x86_64-w64-mingw32/x64
Running under: Windows 11 x64 (build 22631)

Matrix products: default


locale:
[1] LC_COLLATE=English_United States.utf8 
[2] LC_CTYPE=English_United States.utf8   
[3] LC_MONETARY=English_United States.utf8
[4] LC_NUMERIC=C                          
[5] LC_TIME=English_United States.utf8    

time zone: America/New_York
tzcode source: internal

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

other attached packages:
[1] FlowSOM_2.12.0       igraph_2.1.1         BiocStyle_2.32.1    
[4] ggcyto_1.32.0        flowWorkspace_4.16.0 ncdfFlow_2.50.0     
[7] BH_1.84.0-0          ggplot2_3.5.1        flowCore_2.16.0     

loaded via a namespace (and not attached):
 [1] gtable_0.3.5                xfun_0.48                  
 [3] htmlwidgets_1.6.4           ConsensusClusterPlus_1.68.0
 [5] rstatix_0.7.2               Biobase_2.64.0             
 [7] lattice_0.22-6              vctrs_0.6.5                
 [9] tools_4.4.1                 generics_0.1.3             
[11] stats4_4.4.1                tibble_3.2.1               
[13] fansi_1.0.6                 cluster_2.1.6              
[15] pkgconfig_2.0.3             data.table_1.16.2          
[17] ggnewscale_0.5.0            RColorBrewer_1.1-3         
[19] S4Vectors_0.42.1            graph_1.82.0               
[21] lifecycle_1.0.4             compiler_4.4.1             
[23] farver_2.1.2                munsell_0.5.1              
[25] ggforce_0.4.2               carData_3.0-5              
[27] htmltools_0.5.8.1           yaml_2.3.10                
[29] Formula_1.2-5               car_3.1-3                  
[31] tidyr_1.3.1                 hexbin_1.28.4              
[33] pillar_1.9.0                ggpubr_0.6.0               
[35] MASS_7.3-60.2               abind_1.4-8                
[37] RProtoBufLib_2.16.0         tidyselect_1.2.1           
[39] digest_0.6.37               Rtsne_0.17                 
[41] purrr_1.0.2                 dplyr_1.1.4                
[43] labeling_0.4.3              polyclip_1.10-7            
[45] fastmap_1.2.0               grid_4.4.1                 
[47] colorspace_2.1-1            cli_3.6.3                  
[49] magrittr_2.0.3              XML_3.99-0.17              
[51] utf8_1.2.4                  broom_1.0.7                
[53] withr_3.0.1                 backports_1.5.0            
[55] scales_1.3.0                rmarkdown_2.28             
[57] matrixStats_1.4.1           gridExtra_2.3              
[59] ggsignif_0.6.4              cytolib_2.16.0             
[61] evaluate_1.0.1              knitr_1.48                 
[63] rlang_1.1.4                 Rcpp_1.0.13                
[65] glue_1.8.0                  Rgraphviz_2.48.0           
[67] tweenr_2.0.3                BiocManager_1.30.25        
[69] BiocGenerics_0.50.0         rstudioapi_0.17.0          
[71] jsonlite_1.8.9              R6_2.5.1                   
[73] plyr_1.8.9                  zlibbioc_1.50.0            
[75] colorRamps_2.3.4