Skip to main content 홈 크리에이터 synthetic-sciences openscience flow-cytometry-analysis
flow-cytometry-analysis Complete flow cytometry analysis pipeline. FCS file handling, compensation, manual/automated gating, immunophenotyping, CFSE proliferation analysis, cell cycle analysis (Dean-Jett-Fox), and apoptosis assays. Extends flowio with analytical workflows. For raw FCS parsing only use flowio.
설치로 이동 Skills Marketplace 커뮤니티가 만든 AI 스킬을 발견하고 탐색하세요.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
직접 명령은 검토 Prompt를 거치지 않습니다. 실행하기 전에 소스를 확인하세요.
npx skills add https://github.com/synthetic-sciences/openscience --skill flow-cytometry-analysis명령은 한 줄로 유지됩니다. 복사하기 전에 가로로 스크롤해 전체 내용을 확인하세요.
로컬 사본을 원하시나요? SkillsMP에서 현재 제공할 수 있는 파일을 다운로드하세요.
Zip 다운로드 다운로드 중... name flow-cytometry-analysis description Complete flow cytometry analysis pipeline. FCS file handling, compensation, manual/automated gating, immunophenotyping, CFSE proliferation analysis, cell cycle analysis (Dean-Jett-Fox), and apoptosis assays. Extends flowio with analytical workflows. For raw FCS parsing only use flowio. category biology license MIT license metadata {"skill-author":"InkVell Inc."}
Flow Cytometry Analysis: Complete Analysis Pipeline
Overview
Flow Cytometry Analysis provides end-to-end computational workflows for analyzing flow cytometry data. Starting from FCS file parsing, through compensation matrix application, gating strategies (manual rectangular/polygon gates and automated Gaussian mixture model gating), to downstream analyses including immunophenotyping, CFSE proliferation tracking, cell cycle phase quantification (Dean-Jett-Fox model), and apoptosis assays (Annexin V/PI). This skill extends basic FCS file handling (flowio) with full analytical pipelines.
When to Use This Skill
Analyzing multi-color flow cytometry experiments
Applying compensation matrices to correct spectral overlap
Building sequential gating hierarchies (debris exclusion, singlet gating, live/dead, marker gating)
Immunophenotyping with multi-marker panels (CD3, CD4, CD8, etc.)
Quantifying cell proliferation from CFSE/CellTrace dilution
Determining cell cycle phase distribution from DNA content histograms
Analyzing apoptosis from Annexin V / propidium iodide staining
Creating density plots, histograms, and overlay visualizations
Automated gating for high-throughput cytometry experiments Related Skills: For raw FCS file parsing and creation use flowio. For single-cell RNA-seq analysis use scanpy.
Installation uv pip install flowio fcsparser scipy numpy pandas matplotlib scikit-learn
Quick Start from flowio import FlowData
import numpy as np
fcs = FlowData('sample.fcs' )
events = fcs.as_array()
channels = fcs.pnn_labels
print (f"Events: {events.shape[0 ]} , Channels: {len (channels)} " )
print (f"Channels: {channels} " )
fsc_idx = channels.index('FSC-A' )
ssc_idx = channels.index('SSC-A' )
mask = (events[:, fsc_idx] > 50000 ) & (events[:, ssc_idx] > 10000 )
gated = events[mask]
print (f"After gating: {gated.shape[0 ]} events ({100 *mask.sum ()/len (mask):.1 f} %)" )
Core Capabilities
1. FCS File Handling Read FCS files and extract channel information.
from flowio import FlowData
import fcsparser
import numpy as np
fcs = FlowData('sample.fcs' )
events = fcs.as_array()
channel_names = fcs.pnn_labels
stain_names = fcs.pns_labels
meta, data = fcsparser.parse('sample.fcs' , reformat_meta=True )
print (f"Channels: {list (data.columns)} " )
if '$SPILLOVER' in fcs.text or 'SPILL' in fcs.text:
spill_str = fcs.text.get('$SPILLOVER' , fcs.text.get('SPILL' , '' ))
print ("Compensation matrix found in metadata" )
2. Compensation Apply spectral overlap correction.
import numpy as np
def parse_spillover_matrix (spill_string ):
"""Parse $SPILLOVER or SPILL keyword from FCS metadata."""
parts = spill_string.split(',' )
n = int (parts[0 ])
channel_names = parts[1 :n+1 ]
values = [float (x) for x in parts[n+1 :]]
matrix = np.array(values).reshape(n, n)
return channel_names, matrix
def compensate (events, fluoro_indices, spillover_matrix ):
"""Apply compensation to fluorescence channels."""
inv_spill = np.linalg.inv(spillover_matrix)
compensated = events.copy()
fluoro_data = events[:, fluoro_indices]
compensated[:, fluoro_indices] = fluoro_data @ inv_spill.T
return compensated
spill_str = fcs.text.get('$SPILLOVER' , fcs.text.get('SPILL' , '' ))
if spill_str:
comp_channels, spill_matrix = parse_spillover_matrix(spill_str)
fluoro_idx = [channel_names.index(ch) for ch in comp_channels]
events_comp = compensate(events, fluoro_idx, spill_matrix)
print (f"Compensated {len (fluoro_idx)} fluorescence channels" )
3. Gating Strategies Apply sequential gates to identify cell populations.
Manual rectangular gates:
import numpy as np
def rect_gate (events, ch_idx, lo, hi ):
"""Apply rectangular gate on one channel."""
return (events[:, ch_idx] >= lo) & (events[:, ch_idx] <= hi)
def polygon_gate (events, x_idx, y_idx, vertices ):
"""Apply polygon gate using ray casting."""
from matplotlib.path import Path
points = np.column_stack([events[:, x_idx], events[:, y_idx]])
poly = Path(vertices)
return poly.contains_points(points)
scatter_gate = rect_gate(events, fsc_idx, 30000 , 250000 ) & \
rect_gate(events, ssc_idx, 5000 , 200000 )
fsch_idx = channel_names.index('FSC-H' )
singlet_gate = scatter_gate.copy()
ratio = events[:, fsc_idx] / (events[:, fsch_idx] + 1 )
singlet_gate &= (ratio > 0.8 ) & (ratio < 1.2 )
print (f"Debris exclusion: {scatter_gate.sum ()} events" )
print (f"Singlets: {singlet_gate.sum ()} events" )
Automated gating with Gaussian Mixture Models:
from sklearn.mixture import GaussianMixture
import numpy as np
def auto_gate_gmm (events, channels_idx, n_components=2 ):
"""Automated gating using Gaussian Mixture Model."""
data = events[:, channels_idx]
data_log = np.log1p(np.clip(data, 0 , None ))
gmm = GaussianMixture(n_components=n_components, random_state=42 )
labels = gmm.fit_predict(data_log)
means = gmm.means_
pop_order = np.argsort(means.sum (axis=1 ))
return labels, gmm, pop_order
labels, gmm, order = auto_gate_gmm(events, [fsc_idx, ssc_idx], n_components=3 )
lymphocyte_mask = labels == order[1 ]
print (f"Lymphocyte gate: {lymphocyte_mask.sum ()} events" )
4. Immunophenotyping Multi-marker panel analysis for population identification.
import numpy as np
import pandas as pd
def immunophenotype (events, channel_names, gates, parent_mask=None ):
"""Calculate population frequencies from marker gates."""
if parent_mask is None :
parent_mask = np.ones(len (events), dtype=bool )
results = {}
parent_count = parent_mask.sum ()
for pop_name, marker_gates in gates.items():
pop_mask = parent_mask.copy()
for ch_name, threshold, direction in marker_gates:
ch_idx = channel_names.index(ch_name)
if direction == '+' :
pop_mask &= events[:, ch_idx] > threshold
else :
pop_mask &= events[:, ch_idx] <= threshold
count = pop_mask.sum ()
freq = 100 * count / parent_count if parent_count > 0 else 0
results[pop_name] = {'count' : count, 'frequency' : freq, 'mask' : pop_mask}
return results
phenotype_gates = {
'CD3+ T cells' : [('CD3' , 500 , '+' )],
'CD4+ T helper' : [('CD3' , 500 , '+' ), ('CD4' , 300 , '+' ), ('CD8' , 300 , '-' )],
'CD8+ T cytotoxic' : [('CD3' , 500 , '+' ), ('CD4' , 300 , '-' ), ('CD8' , 300 , '+' )],
'B cells (CD19+)' : [('CD3' , 500 , '-' ), ('CD19' , 200 , '+' )],
'NK cells' : [('CD3' , 500 , '-' ), ('CD56' , 200 , '+' )],
}
results = immunophenotype(events, channel_names, phenotype_gates, parent_mask=singlet_gate)
for pop, data in results.items():
print (f"{pop} : {data['count' ]} cells ({data['frequency' ]:.1 f} %)" )
5. CFSE Proliferation Analysis Quantify cell division from dye dilution.
import numpy as np
from scipy.signal import find_peaks
from scipy.optimize import curve_fit
def analyze_cfse_proliferation (cfse_values, n_generations=8 ):
"""Analyze CFSE dilution to quantify proliferation."""
log_cfse = np.log2(cfse_values[cfse_values > 0 ])
hist, bin_edges = np.histogram(log_cfse, bins=200 )
bin_centers = (bin_edges[:-1 ] + bin_edges[1 :]) / 2
peaks, properties = find_peaks(hist, height=len (cfse_values)*0.005 ,
distance=10 , prominence=50 )
peak_positions = bin_centers[peaks]
peak_heights = hist[peaks]
total_cells = sum (peak_heights)
undivided_equiv = sum (h / (2 **i) for i, h in enumerate (peak_heights))
division_index = total_cells / undivided_equiv if undivided_equiv > 0 else 0
if len (peak_heights) > 1 :
divided_cells = sum (peak_heights[1 :])
divided_precursors = sum (h / (2 **i) for i, h in enumerate (peak_heights[1 :], 1 ))
prolif_index = divided_cells / divided_precursors if divided_precursors > 0 else 0
else :
prolif_index = 1.0
return {
'n_peaks' : len (peaks),
'division_index' : division_index,
'proliferation_index' : prolif_index,
'peak_positions' : peak_positions,
'peak_heights' : peak_heights
}
cfse_idx = channel_names.index('CFSE' ) if 'CFSE' in channel_names else 0
cfse_data = events[singlet_gate, cfse_idx]
prolif = analyze_cfse_proliferation(cfse_data)
print (f"Generations detected: {prolif['n_peaks' ]} " )
print (f"Division index: {prolif['division_index' ]:.2 f} " )
print (f"Proliferation index: {prolif['proliferation_index' ]:.2 f} " )
6. Cell Cycle Analysis Fit DNA content histograms to determine cell cycle phase distribution.
import numpy as np
from scipy.optimize import curve_fit
def dean_jett_fox (x, g1_mean, g1_std, g1_amp,
s_amp, s_slope,
g2_amp, g2_std ):
"""Dean-Jett-Fox cell cycle model.
G1 and G2/M as Gaussians, S-phase as polynomial."""
g2_mean = 2 * g1_mean
g1 = g1_amp * np.exp(-0.5 * ((x - g1_mean) / g1_std) ** 2 )
g2 = g2_amp * np.exp(-0.5 * ((x - g2_mean) / g2_std) ** 2 )
s = np.zeros_like(x)
s_mask = (x > g1_mean + g1_std) & (x < g2_mean - g2_std)
s[s_mask] = s_amp + s_slope * (x[s_mask] - g1_mean)
return g1 + g2 + np.clip(s, 0 , None )
def cell_cycle_analysis (dna_values ):
"""Determine G0/G1, S, G2/M percentages from PI staining."""
hist, bin_edges = np.histogram(dna_values, bins=256 , range =(0 , dna_values.max ()))
bin_centers = (bin_edges[:-1 ] + bin_edges[1 :]) / 2
g1_peak = bin_centers[np.argmax(hist)]
g1_amp = hist.max ()
p0 = [g1_peak, g1_peak*0.05 , g1_amp, g1_amp*0.1 , 0 , g1_amp*0.3 , g1_peak*0.07 ]
bounds = ([0 , 0 , 0 , 0 , -np.inf, 0 , 0 ],
[np.inf, np.inf, np.inf, np.inf, np.inf, np.inf, np.inf])
try :
popt, pcov = curve_fit(dean_jett_fox, bin_centers, hist, p0=p0,
bounds=bounds, maxfev=10000 )
fitted = dean_jett_fox(bin_centers, *popt)
g1_mean, g1_std = popt[0 ], popt[1 ]
g2_mean = 2 * g1_mean
g1_mask = bin_centers < (g1_mean + 2 *g1_std)
g2_mask = bin_centers > (g2_mean - 2 *popt[5 ])
s_mask = ~g1_mask & ~g2_mask
total = hist.sum ()
g1_pct = 100 * hist[g1_mask].sum () / total
s_pct = 100 * hist[s_mask].sum () / total
g2m_pct = 100 * hist[g2_mask].sum () / total
return {'G0/G1' : g1_pct, 'S' : s_pct, 'G2/M' : g2m_pct}
except RuntimeError:
return {'error' : 'Curve fitting failed — check DNA histogram quality' }
pi_idx = channel_names.index('PI' ) if 'PI' in channel_names else 0
dna_data = events[singlet_gate, pi_idx]
phases = cell_cycle_analysis(dna_data)
print (f"G0/G1: {phases.get('G0/G1' , 'N/A' ):.1 f} %" )
print (f"S: {phases.get('S' , 'N/A' ):.1 f} %" )
print (f"G2/M: {phases.get('G2/M' , 'N/A' ):.1 f} %" )
7. Apoptosis Assays Analyze Annexin V / Propidium Iodide staining.
import numpy as np
def annexin_v_pi_analysis (events, annexin_idx, pi_idx,
annexin_threshold, pi_threshold, parent_mask=None ):
"""Quadrant analysis for apoptosis assay."""
if parent_mask is None :
parent_mask = np.ones(len (events), dtype=bool )
gated = events[parent_mask]
total = len (gated)
annexin_pos = gated[:, annexin_idx] > annexin_threshold
pi_pos = gated[:, pi_idx] > pi_threshold
viable = (~annexin_pos) & (~pi_pos)
early_apoptotic = annexin_pos & (~pi_pos)
late_apoptotic = annexin_pos & pi_pos
necrotic = (~annexin_pos) & pi_pos
return {
'viable' : 100 * viable.sum () / total,
'early_apoptotic' : 100 * early_apoptotic.sum () / total,
'late_apoptotic' : 100 * late_apoptotic.sum () / total,
'necrotic' : 100 * necrotic.sum () / total
}
results = annexin_v_pi_analysis(events, annexin_idx=3 , pi_idx=4 ,
annexin_threshold=200 , pi_threshold=200 ,
parent_mask=singlet_gate)
for state, pct in results.items():
print (f"{state} : {pct:.1 f} %" )
8. Visualization Create publication-quality flow cytometry plots.
import matplotlib.pyplot as plt
import numpy as np
def density_plot (events, x_idx, y_idx, channel_names, ax=None , gate_mask=None ):
"""2D density (pseudo-color) plot."""
if ax is None :
fig, ax = plt.subplots(figsize=(6 , 6 ))
data = events if gate_mask is None else events[gate_mask]
ax.hist2d(data[:, x_idx], data[:, y_idx], bins=256 ,
cmap='jet' , norm=plt.matplotlib.colors.LogNorm())
ax.set_xlabel(channel_names[x_idx])
ax.set_ylabel(channel_names[y_idx])
return ax
def histogram_overlay (datasets, channel_idx, labels, channel_name, ax=None ):
"""Overlay histograms for comparing conditions."""
if ax is None :
fig, ax = plt.subplots(figsize=(8 , 5 ))
for data, label in zip (datasets, labels):
ax.hist(data[:, channel_idx], bins=256 , alpha=0.5 , label=label, density=True )
ax.set_xlabel(channel_name)
ax.set_ylabel('Density' )
ax.legend()
return ax
Typical Workflows
Workflow 1: Full Immunophenotyping from Multi-Color FCS Files from flowio import FlowData
import numpy as np
fcs = FlowData('pbmc_panel.fcs' )
events = fcs.as_array()
ch = fcs.pnn_labels
spill_str = fcs.text.get('$SPILLOVER' , fcs.text.get('SPILL' , '' ))
if spill_str:
comp_ch, spill = parse_spillover_matrix(spill_str)
fidx = [ch.index(c) for c in comp_ch]
events = compensate(events, fidx, spill)
fsc, ssc = ch.index('FSC-A' ), ch.index('SSC-A' )
gate = (events[:, fsc] > 30000 ) & (events[:, ssc] > 5000 ) & (events[:, ssc] < 200000 )
panels = {
'T cells (CD3+)' : [('CD3' , 500 , '+' )],
'Helper T (CD3+CD4+)' : [('CD3' , 500 , '+' ), ('CD4' , 300 , '+' )],
'Cytotoxic T (CD3+CD8+)' : [('CD3' , 500 , '+' ), ('CD8' , 300 , '+' )],
}
results = immunophenotype(events, ch, panels, parent_mask=gate)
for pop, data in results.items():
print (f"{pop} : {data['frequency' ]:.1 f} %" )
Workflow 2: CFSE Proliferation Analysis with Division Tracking from flowio import FlowData
fcs = FlowData('cfse_stimulated.fcs' )
events = fcs.as_array()
ch = fcs.pnn_labels
fsc = ch.index('FSC-A' )
live = events[:, fsc] > 20000
cfse_idx = ch.index('CFSE' )
prolif = analyze_cfse_proliferation(events[live, cfse_idx])
print (f"Divisions detected: {prolif['n_peaks' ]} " )
print (f"Division index: {prolif['division_index' ]:.2 f} " )
print (f"Proliferation index: {prolif['proliferation_index' ]:.2 f} " )
Workflow 3: Cell Cycle Distribution from PI-Stained Samples from flowio import FlowData
fcs = FlowData('pi_stained.fcs' )
events = fcs.as_array()
ch = fcs.pnn_labels
pi_a = ch.index('PI-A' )
pi_w = ch.index('PI-W' ) if 'PI-W' in ch else None
if pi_w:
singlets = (events[:, pi_w] > 50000 ) & (events[:, pi_w] < 150000 )
else :
singlets = np.ones(len (events), dtype=bool )
phases = cell_cycle_analysis(events[singlets, pi_a])
for phase, pct in phases.items():
print (f"{phase} : {pct:.1 f} %" )
Best Practices
Always compensate before gating on fluorescence channels — uncompensated data leads to incorrect population identification
Gate sequentially — debris exclusion first, then singlets, then viability, then markers
Use FMO controls (Fluorescence Minus One) to set accurate positive/negative thresholds
Log or biexponential transform fluorescence data for visualization and gating
Back-gate to verify gated populations appear in expected scatter positions
Include isotype controls or unstained controls for threshold determination
Report parent gate denominators — frequencies must reference the correct parent population
Quality check — verify event counts are sufficient for rare populations (>100 events minimum)
Troubleshooting Problem: Compensation produces negative values
Solution: This is normal for properly compensated data. Do not zero-clip — negative values carry information. Use biexponential display for visualization.
Problem: GMM auto-gating splits one population into two
Solution: Reduce n_components. Pre-gate to remove obvious debris first. Try log-transforming data before fitting.
Problem: Cell cycle fitting fails
Solution: Ensure DNA histogram has clear G1 peak. Filter for singlets using DNA-area vs DNA-width. Check that PI staining is optimal (no under/over-staining).
Problem: CFSE peaks not resolved
Solution: Increase histogram bins. Ensure cells were labeled at correct CFSE concentration. Later generations may be unresolvable — focus on first 4-5 divisions.
Problem: FCS file channels have unexpected names
Solution: Check both pnn_labels (short names) and pns_labels (descriptive stain names). Different instruments use different naming conventions.
Resources