Programmatically Replicate Gating from a FlowJo Workspace

[1]:
import os
import bokeh
from bokeh.plotting import show
import numpy as np

import flowkit as fk

bokeh.io.output_notebook()
Loading BokehJS ...

Load FlowJo Workspace

We load the workspace just to show the gating strategy that we want to replicate programmatically.

[2]:
base_dir = "../../../data/8_color_data_set"

sample_path = os.path.join(base_dir, "fcs_files")
wsp_path = os.path.join(base_dir, "8_color_ICS.wsp")
[3]:
workspace = fk.Workspace(wsp_path, fcs_samples=sample_path)
[4]:
workspace.get_sample_ids()
[4]:
['101_DEN084Y5_15_E01_008_clean.fcs',
 '101_DEN084Y5_15_E03_009_clean.fcs',
 '101_DEN084Y5_15_E05_010_clean.fcs']
[5]:
workspace.get_sample_groups()
[5]:
['All Samples', 'DEN', 'GEN', 'G69', 'Lyo Cells']

Record a sample ID to use to replicate analysis

[6]:
sample_id = '101_DEN084Y5_15_E03_009_clean.fcs'
[7]:
sample = workspace.get_sample(sample_id)
[8]:
sample.channels
[8]:
channel_number pnn pns png pne pnr
0 1 FSC-A 1.0 (0.0, 0.0) 262144.0
1 2 FSC-H 1.0 (0.0, 0.0) 262144.0
2 3 FSC-W 1.0 (0.0, 0.0) 262144.0
3 4 SSC-A 1.0 (0.0, 0.0) 262144.0
4 5 SSC-H 1.0 (0.0, 0.0) 262144.0
5 6 SSC-W 1.0 (0.0, 0.0) 262144.0
6 7 TNFa FITC FLR-A 1.0 (0.0, 0.0) 262144.0
7 8 CD8 PerCP-Cy55 FLR-A 1.0 (0.0, 0.0) 262144.0
8 9 IL2 BV421 FLR-A 1.0 (0.0, 0.0) 262144.0
9 10 Aqua Amine FLR-A 1.0 (0.0, 0.0) 262144.0
10 11 IFNg APC FLR-A 1.0 (0.0, 0.0) 262144.0
11 12 CD3 APC-H7 FLR-A 1.0 (0.0, 0.0) 262144.0
12 13 CD107a PE FLR-A 1.0 (0.0, 0.0) 262144.0
13 14 CD4 PE-Cy7 FLR-A 1.0 (0.0, 0.0) 262144.0
14 15 Time 1.0 (0.0, 0.0) 262144.0

Here’s the gate hierachy we aim to replicate

[9]:
print(workspace.get_gate_hierarchy(sample_id, 'ascii'))
root
╰── Time
    ╰── Singlets
        ╰── aAmine-
            ╰── CD3+
                ├── CD4+
                │   ├── CD107a+
                │   ├── IFNg+
                │   ├── IL2+
                │   ╰── TNFa+
                ╰── CD8+
                    ├── CD107a+
                    ├── IFNg+
                    ├── IL2+
                    ╰── TNFa+
[10]:
workspace.analyze_samples(sample_id=sample_id)
[11]:
results = workspace.get_gating_results(sample_id)
[12]:
results.report.head()
[12]:
sample gate_path gate_name gate_type quadrant_parent parent count absolute_percent relative_percent level
0 101_DEN084Y5_15_E03_009_clean.fcs (root,) Time RectangleGate None root 283968 99.999648 99.999648 1
1 101_DEN084Y5_15_E03_009_clean.fcs (root, Time) Singlets PolygonGate None Time 236780 83.382341 83.382635 2
2 101_DEN084Y5_15_E03_009_clean.fcs (root, Time, Singlets) aAmine- PolygonGate None Singlets 161823 56.986150 68.343188 3
3 101_DEN084Y5_15_E03_009_clean.fcs (root, Time, Singlets, aAmine-) CD3+ PolygonGate None aAmine- 132200 46.554377 81.694197 4
4 101_DEN084Y5_15_E03_009_clean.fcs (root, Time, Singlets, aAmine-, CD3+) CD4+ PolygonGate None CD3+ 81855 28.825330 61.917549 5

Create a new Session

We’ll create a new Session and load this sample.

[13]:
full_sample_path = os.path.join(sample_path, sample_id)

session = fk.Session(fcs_samples=full_sample_path)
sample = session.get_sample(sample_id)

Load the compensation matrix from a CSV file and add it to the session

[14]:
# setup comp matrix
detectors = [sample.pnn_labels[i] for i in sample.fluoro_indices]
den_comp_mat = fk.Matrix('den_comp', '../../../data/8_color_data_set/den_comp.csv', detectors)
[15]:
den_comp_mat.detectors
[15]:
['TNFa FITC FLR-A',
 'CD8 PerCP-Cy55 FLR-A',
 'IL2 BV421 FLR-A',
 'Aqua Amine FLR-A',
 'IFNg APC FLR-A',
 'CD3 APC-H7 FLR-A',
 'CD107a PE FLR-A',
 'CD4 PE-Cy7 FLR-A']
[16]:
session.add_comp_matrix(den_comp_mat)

Define our transformations and add them. For the time transform, we’ll check our sample’s time values

[17]:
time_idx = sample.get_channel_index('Time')
time_events = sample.get_channel_events(time_idx, source='raw')
time_events.min(), time_events.max()
[17]:
(0.8440000152587891, 72.235)
[18]:
# Setup transforms
time_xform = fk.transforms.LinearTransform('time_xform', param_t=72.235, param_a=0)
scatter_xform = fk.transforms.LinearTransform('lin_xform', param_t=262144, param_a=0)
flr_xform = fk.transforms.LogicleTransform(
    'logicle_xform',
    param_t=262144.0,
    param_w=1.0,
    param_m=4.418539922,
    param_a=0.0
)

session.add_transform(time_xform)
session.add_transform(scatter_xform)
session.add_transform(flr_xform)

Begin defining gates, starting with the Time gate

[19]:
# NOTE: The ranges we use for this time gate are not the same as those in the .wsp file,
#       so event counts will be different
time_dim = fk.Dimension(
    'Time',
    compensation_ref='uncompensated',
    transformation_ref=time_xform.id,
    range_min=0.1,
    range_max=0.96
)
[20]:
# create time gate using Time dimension
time_gate = fk.gates.RectangleGate('Time', [time_dim])
[21]:
session.add_gate(time_gate, gate_path=('root',))
[22]:
print(session.get_gate_hierarchy())
root
╰── Time
[23]:
session.analyze_samples(sample_id=sample_id)
[24]:
p = session.plot_gate(sample_id, "Time")
show(p)

Define Singlet gate

[25]:
# create singlet gate, starting with dimensions
dim_fsc_w = fk.Dimension('FSC-W', 'uncompensated', transformation_ref=scatter_xform.id)
dim_fsc_h = fk.Dimension('FSC-H', 'uncompensated', transformation_ref=scatter_xform.id)
[26]:
# look at dimensions gated on Time
p = session.plot_scatter(
    sample_id,
    dim_fsc_w,
    dim_fsc_h,
    gate_name="Time",
    subsample_count=10000
)
show(p)
[27]:
# get our vertices from analyst, algo, etc.
singlet_vertices = [
    [0.328125, 0.1640625],
    [0.296875, 0.1484375],
    [0.27734375, 0.25390625],
    [0.27734375, 0.52734375],
    [0.28125, 0.78515625],
    [0.30859375, 0.8515625],
    [0.34765625, 0.3984375],
    [0.3359375, 0.1875]
]
[28]:
singlet_gate = fk.gates.PolygonGate(
    'Singlets',
    [dim_fsc_w, dim_fsc_h],
    singlet_vertices
)
[29]:
session.add_gate(singlet_gate, ('root', 'Time'))
[30]:
print(session.get_gate_hierarchy())
root
╰── Time
    ╰── Singlets
[31]:
session.analyze_samples()

Define Live gate

[32]:
# create live cell gate, creating dims then plotting the previous gate
dim_amine_a = fk.Dimension('Aqua Amine FLR-A', den_comp_mat.id, transformation_ref=flr_xform.id)
dim_ssc_a = fk.Dimension('SSC-A', 'uncompensated', transformation_ref=scatter_xform.id)
[33]:
# look at dimensions gated on Singlets
p = session.plot_scatter(
    sample_id,
    dim_amine_a,
    dim_ssc_a,
    gate_name="Singlets",
    x_min=0,
    x_max=1.0,
    y_min=0,
    y_max=1.0
)
show(p)
[34]:
# get our vertices from analyst, algo, etc.
live_cell_vertices = [
    [0.2629268137285685, 0.0625],
    [0.24318837264468562, 0.03515625],
    [0.21573453285608676, 0.0390625],
    [0.20396768438347745, 0.0546875],
    [0.20396768438347745, 0.140625],
    [0.20460078058895426, 0.3117570495605469],
    [0.2355517136894538, 0.328125],
    [0.26856506770333155, 0.3125],
    [0.29042797365869377, 0.24609375],
    [0.29042797365869377, 0.1484375]
]
[35]:
live_cell_gate = fk.gates.PolygonGate(
    'aAmine-',
    [dim_amine_a, dim_ssc_a],
    live_cell_vertices
)
[36]:
session.add_gate(live_cell_gate, ('root', 'Time', 'Singlets'))
[37]:
print(session.get_gate_hierarchy())
root
╰── Time
    ╰── Singlets
        ╰── aAmine-
[38]:
session.analyze_samples()

Define CD3+ gate

[39]:
# create CD3+ gate
dim_cd3_a = fk.Dimension(
    'CD3 APC-H7 FLR-A',
    den_comp_mat.id,
    transformation_ref=flr_xform.id
)
[40]:
f = session.plot_scatter(
    sample_id,
    dim_cd3_a,
    dim_ssc_a,
    gate_name='aAmine-',
    x_min=0,
    x_max=1,
    y_min=0,
    y_max=1
)
show(f)
[41]:
# get our vertices from analyst, algo, etc.
cd3_vertices = [
    [0.28415161867527605, 0.11328125],
    [0.3132637699981912, 0.203125],
    [0.42207818508379846, 0.3046875],
    [0.5067109372185516, 0.359375],
    [0.6853991917182599, 0.35546875],
    [0.6896802981119161, 0.05078125],
    [0.5692952580886116, 0.01953125],
    [0.3192472844795108, 0.01953125]
]
[42]:
cd3_gate = fk.gates.PolygonGate('CD3+', [dim_cd3_a, dim_ssc_a], cd3_vertices)
[43]:
session.add_gate(cd3_gate, ('root', 'Time', 'Singlets', 'aAmine-'))
[44]:
session.analyze_samples()

Define CD4+ and CD8+ branch gates

[45]:
# create CD4+ & CD8+ gates
dim_cd4_a = fk.Dimension('CD4 PE-Cy7 FLR-A', den_comp_mat.id, transformation_ref=flr_xform.id)
dim_cd8_a = fk.Dimension('CD8 PerCP-Cy55 FLR-A', den_comp_mat.id, transformation_ref=flr_xform.id)
[46]:
# look at dimensions gated on CD3+
p = session.plot_scatter(
    sample_id,
    dim_cd4_a,
    dim_cd8_a,
    gate_name="CD3+",
    x_min=0,
    x_max=1.0,
    y_min=0,
    y_max=1.0
)
show(p)
[47]:
cd4_vertices = [
    [0.33228361583463906, 0.20521609423858533],
    [0.32558526100158003, 0.22402959677045098],
    [0.3288891623611386, 0.2534446627500065],
    [0.35453955634069056, 0.3162117257472119],
    [0.38884335063325615, 0.37109236044857546],
    [0.49681261945848476, 0.39344265440087484],
    [0.6241875538107384, 0.4172035843426509],
    [0.6811074648618941, 0.32237152038011546],
    [0.6939511366527197, 0.23138424146124928],
    [0.6982119969313532, 0.20396768438347745],
    [0.5311666680646416, 0.20396768438347745],
    [0.33576875548246565, 0.20396768438347745]
]
[48]:
cd8_vertices = [
    [0.19654236830112726, 0.8063681300583732],
    [0.7981838566398077, 0.8186148712026381],
    [0.8145363952765393, 0.45683210068669505],
    [0.36683425557526916, 0.37109236044857546],
    [0.28415161867527605, 0.2949602838822682],
    [0.19654236830112726, 0.28826829762740075]
]
[49]:
cd4_gate = fk.gates.PolygonGate('CD4+', [dim_cd4_a, dim_cd8_a], cd4_vertices)
cd8_gate = fk.gates.PolygonGate('CD8+', [dim_cd4_a, dim_cd8_a], cd8_vertices)
[50]:
session.add_gate(cd4_gate, ('root', 'Time', 'Singlets', 'aAmine-', 'CD3+'))
session.add_gate(cd8_gate, ('root', 'Time', 'Singlets', 'aAmine-', 'CD3+'))
[51]:
print(session.get_gate_hierarchy())
root
╰── Time
    ╰── Singlets
        ╰── aAmine-
            ╰── CD3+
                ├── CD4+
                ╰── CD8+
[52]:
session.analyze_samples()

Create CD107a+, IFNg, IL2, & TNFa gates, then add to CD4 & CD8 branches

[53]:
dim_cd107a_a = fk.Dimension('CD107a PE FLR-A', den_comp_mat.id, transformation_ref=flr_xform.id)
dim_ifng_a = fk.Dimension('IFNg APC FLR-A', den_comp_mat.id, transformation_ref=flr_xform.id)
dim_il2_a = fk.Dimension('IL2 BV421 FLR-A', den_comp_mat.id, transformation_ref=flr_xform.id)
dim_tnfa_a = fk.Dimension('TNFa FITC FLR-A', den_comp_mat.id, transformation_ref=flr_xform.id)
[54]:
# start with CD107a gated on CD4+ branch
p = session.plot_scatter(
    sample_id,
    dim_cd3_a,
    dim_cd107a_a,
    gate_name="CD4+",
    x_min=0,
    x_max=1.0,
    y_min=0,
    y_max=1.0
)
show(p)
[55]:
# these next gates are all rectangle gates, so bounds will be defined in the Dimension instances
cd4_cd3_dim = fk.Dimension(
    'CD3 APC-H7 FLR-A',
    compensation_ref=den_comp_mat.id,
    transformation_ref=flr_xform.id,
    range_min=0.2,
    range_max=1.0
)
cd4_cd107_dim = fk.Dimension(
    'CD107a PE FLR-A',
    compensation_ref=den_comp_mat.id,
    transformation_ref=flr_xform.id,
    range_min=0.5645990565096747,
    range_max=1.1647085183977386
)
[56]:
cd4_cd107_gate = fk.gates.RectangleGate('CD107a+', [cd4_cd3_dim, cd4_cd107_dim])
[57]:
cd4_pos_gate_path = ('root', 'Time', 'Singlets', 'aAmine-', 'CD3+', 'CD4+')
cd8_pos_gate_path = ('root', 'Time', 'Singlets', 'aAmine-', 'CD3+', 'CD8+')
[58]:
session.add_gate(cd4_cd107_gate, cd4_pos_gate_path)
[59]:
# the IFNg, IL2, & TNFa gates will share the same CD3 bounds, so only define their dims
cd4_ifng_dim = fk.Dimension(
    'IFNg APC FLR-A',
    compensation_ref=den_comp_mat.id,
    transformation_ref=flr_xform.id,
    range_min=0.30232120911824945,
    range_max=1.1874194728667935
)
cd4_il2_dim = fk.Dimension(
    'IL2 BV421 FLR-A',
    compensation_ref=den_comp_mat.id,
    transformation_ref=flr_xform.id,
    range_min=0.45683210351871706,
    range_max=1.136312449258137
)
cd4_tnfa_dim = fk.Dimension(
    'TNFa FITC FLR-A',
    compensation_ref=den_comp_mat.id,
    transformation_ref=flr_xform.id,
    range_min=0.2904279740369533,
    range_max=1.0965394616932618
)
[60]:
# and likewise, create & add their rectangle gates
cd4_ifng_gate = fk.gates.RectangleGate('IFNg+', [cd4_cd3_dim, cd4_ifng_dim])
cd4_il2_gate = fk.gates.RectangleGate('IL2+', [cd4_cd3_dim, cd4_il2_dim])
cd4_tnfa_gate = fk.gates.RectangleGate('TNFa+', [cd4_cd3_dim, cd4_tnfa_dim])

session.add_gate(cd4_ifng_gate, cd4_pos_gate_path)
session.add_gate(cd4_il2_gate, cd4_pos_gate_path)
session.add_gate(cd4_tnfa_gate, cd4_pos_gate_path)
[61]:
# the gates in the CD8+ branch are the same except for the parent ID
cd8_cd107_gate = fk.gates.RectangleGate('CD107a+', [cd4_cd3_dim, cd4_cd107_dim])
cd8_ifng_gate = fk.gates.RectangleGate('IFNg+', [cd4_cd3_dim, cd4_ifng_dim])
cd8_il2_gate = fk.gates.RectangleGate('IL2+', [cd4_cd3_dim, cd4_il2_dim])
cd8_tnfa_gate = fk.gates.RectangleGate('TNFa+', [cd4_cd3_dim, cd4_tnfa_dim])
[62]:
session.add_gate(cd8_cd107_gate, cd8_pos_gate_path)
session.add_gate(cd8_ifng_gate, cd8_pos_gate_path)
session.add_gate(cd8_il2_gate, cd8_pos_gate_path)
session.add_gate(cd8_tnfa_gate, cd8_pos_gate_path)
[63]:
print(session.get_gate_hierarchy())
root
╰── Time
    ╰── Singlets
        ╰── aAmine-
            ╰── CD3+
                ├── CD4+
                │   ├── CD107a+
                │   ├── IFNg+
                │   ├── IL2+
                │   ╰── TNFa+
                ╰── CD8+
                    ├── CD107a+
                    ├── IFNg+
                    ├── IL2+
                    ╰── TNFa+
[64]:
session.analyze_samples()

Review all our gates

[65]:
for i, row in results.report.iterrows():
    p = session.plot_gate(
        row['sample'], # 'sample' is a Pandas, so lookup explicitly
        gate_name=row.gate_name,
        gate_path=row.gate_path,
        x_min=0,
        x_max=1.2,
        y_min=0,
        y_max=1.2
    )
    show(p)

Get gating results (currently with only 1 sample assigned & analyzed)

[66]:
results = session.get_gating_results(sample_id)
[67]:
results.report.head()
[67]:
sample gate_path gate_name gate_type quadrant_parent parent count absolute_percent relative_percent level
0 101_DEN084Y5_15_E03_009_clean.fcs (root,) Time RectangleGate None root 245545 86.468946 86.468946 1
1 101_DEN084Y5_15_E03_009_clean.fcs (root, Time) Singlets PolygonGate None Time 204886 72.150833 83.441324 2
2 101_DEN084Y5_15_E03_009_clean.fcs (root, Time, Singlets) aAmine- PolygonGate None Singlets 139970 49.290592 68.316039 3
3 101_DEN084Y5_15_E03_009_clean.fcs (root, Time, Singlets, aAmine-) CD3+ PolygonGate None aAmine- 114426 40.295243 81.750375 4
4 101_DEN084Y5_15_E03_009_clean.fcs (root, Time, Singlets, aAmine-, CD3+) CD4+ PolygonGate None CD3+ 70881 24.960823 61.944838 5

Now add more samples and simply re-run the analysis

[68]:
session.add_samples(
    [
        os.path.join(sample_path, '101_DEN084Y5_15_E01_008_clean.fcs'),
        os.path.join(sample_path, '101_DEN084Y5_15_E05_010_clean.fcs')
    ]
)
[69]:
# running with verbose=True to see progress as it can take some time to run
session.analyze_samples(verbose=True)
#### Processing gates for 3 samples (multiprocessing is enabled - 3 cpus) ####
101_DEN084Y5_15_E03_009_clean.fcs: processing gate Time
101_DEN084Y5_15_E03_009_clean.fcs: processing gate Singlets
101_DEN084Y5_15_E03_009_clean.fcs: processing gate aAmine-
101_DEN084Y5_15_E03_009_clean.fcs: processing gate CD3+
101_DEN084Y5_15_E01_008_clean.fcs: processing gate Time
101_DEN084Y5_15_E01_008_clean.fcs: processing gate Singlets
101_DEN084Y5_15_E03_009_clean.fcs: processing gate CD4+
101_DEN084Y5_15_E01_008_clean.fcs: processing gate aAmine-
101_DEN084Y5_15_E05_010_clean.fcs: processing gate Time
101_DEN084Y5_15_E03_009_clean.fcs: processing gate CD8+101_DEN084Y5_15_E05_010_clean.fcs: processing gate Singlets

101_DEN084Y5_15_E01_008_clean.fcs: processing gate CD3+
101_DEN084Y5_15_E05_010_clean.fcs: processing gate aAmine-
101_DEN084Y5_15_E01_008_clean.fcs: processing gate CD4+
101_DEN084Y5_15_E03_009_clean.fcs: processing gate CD107a+
101_DEN084Y5_15_E05_010_clean.fcs: processing gate CD3+
101_DEN084Y5_15_E01_008_clean.fcs: processing gate CD8+
101_DEN084Y5_15_E03_009_clean.fcs: processing gate IFNg+
101_DEN084Y5_15_E05_010_clean.fcs: processing gate CD4+
101_DEN084Y5_15_E01_008_clean.fcs: processing gate CD107a+
101_DEN084Y5_15_E03_009_clean.fcs: processing gate IL2+
101_DEN084Y5_15_E05_010_clean.fcs: processing gate CD8+
101_DEN084Y5_15_E01_008_clean.fcs: processing gate IFNg+
101_DEN084Y5_15_E03_009_clean.fcs: processing gate TNFa+
101_DEN084Y5_15_E05_010_clean.fcs: processing gate CD107a+
101_DEN084Y5_15_E01_008_clean.fcs: processing gate IL2+
101_DEN084Y5_15_E03_009_clean.fcs: processing gate CD107a+
101_DEN084Y5_15_E05_010_clean.fcs: processing gate IFNg+
101_DEN084Y5_15_E01_008_clean.fcs: processing gate TNFa+
101_DEN084Y5_15_E03_009_clean.fcs: processing gate IFNg+
101_DEN084Y5_15_E05_010_clean.fcs: processing gate IL2+
101_DEN084Y5_15_E03_009_clean.fcs: processing gate IL2+
101_DEN084Y5_15_E05_010_clean.fcs: processing gate TNFa+
101_DEN084Y5_15_E01_008_clean.fcs: processing gate CD107a+
101_DEN084Y5_15_E03_009_clean.fcs: processing gate TNFa+
101_DEN084Y5_15_E05_010_clean.fcs: processing gate CD107a+
101_DEN084Y5_15_E01_008_clean.fcs: processing gate IFNg+
101_DEN084Y5_15_E05_010_clean.fcs: processing gate IFNg+
101_DEN084Y5_15_E01_008_clean.fcs: processing gate IL2+
101_DEN084Y5_15_E05_010_clean.fcs: processing gate IL2+
101_DEN084Y5_15_E01_008_clean.fcs: processing gate TNFa+
101_DEN084Y5_15_E05_010_clean.fcs: processing gate TNFa+

Retrieve and review the final report

[70]:
grp_report = session.get_analysis_report()
[71]:
grp_report.head()
[71]:
sample gate_path gate_name gate_type quadrant_parent parent count absolute_percent relative_percent level
0 101_DEN084Y5_15_E03_009_clean.fcs (root,) Time RectangleGate None root 245545 86.468946 86.468946 1
1 101_DEN084Y5_15_E03_009_clean.fcs (root, Time) Singlets PolygonGate None Time 204886 72.150833 83.441324 2
2 101_DEN084Y5_15_E03_009_clean.fcs (root, Time, Singlets) aAmine- PolygonGate None Singlets 139970 49.290592 68.316039 3
3 101_DEN084Y5_15_E03_009_clean.fcs (root, Time, Singlets, aAmine-) CD3+ PolygonGate None aAmine- 114426 40.295243 81.750375 4
4 101_DEN084Y5_15_E03_009_clean.fcs (root, Time, Singlets, aAmine-, CD3+) CD4+ PolygonGate None CD3+ 70881 24.960823 61.944838 5
[72]:
grp_report[grp_report.gate_name == 'CD8+']
[72]:
sample gate_path gate_name gate_type quadrant_parent parent count absolute_percent relative_percent level
5 101_DEN084Y5_15_E03_009_clean.fcs (root, Time, Singlets, aAmine-, CD3+) CD8+ PolygonGate None CD3+ 40640 14.311421 35.516404 5
19 101_DEN084Y5_15_E01_008_clean.fcs (root, Time, Singlets, aAmine-, CD3+) CD8+ PolygonGate None CD3+ 42426 14.620983 35.226716 5
33 101_DEN084Y5_15_E05_010_clean.fcs (root, Time, Singlets, aAmine-, CD3+) CD8+ PolygonGate None CD3+ 41368 14.500333 35.387208 5
[ ]: