CRISPR Guide RNA On/Off-Target Scoring — Ranking 100 Candidates in 30 Seconds Before a BRCA1 KO Experiment
The success rate of a CRISPR-Cas9 experiment is already half-decided at the point of gRNA (guide RNA) selection. Pick well and you cut the target gene efficiently (on-target efficiency) without cutting other genes by mistake (off-target avoidance). Yet synthesizing and validating each candidate gRNA in the lab takes days and tens of thousands of won per gRNA. Using a real-world scenario of BRCA1 gene KO in the K562 cell line, this article builds a hardcore end-to-end pipeline that ranks 100–500 candidate gRNAs with an ensemble of deep learning scorers in 30 seconds, so that only the top 5 move on to actual synthesis.
📚 Prerequisite (Strong Recommendation)
This is an AI×Bio hardcore in-depth topic. Before entering, we strongly recommend that you first study the following DryBench topics.
Without the prerequisites, this article proceeds directly from real-world code without re-explaining the learning principles of 1D CNN/RNN-based sequence scorers, one-hot encoding, or the practical patterns of loading and running pretrained models, so it will be difficult to follow.
What We Learned in DryBench
In DryBench ai-native #2 we learned that neural networks capture local patterns over the input sequence with CNN filters and that RNNs learn order dependence. In #13 we saw the HuggingFace ecosystem that standardizes the weights, tokenizers, and inference APIs of pretrained models so that loading is possible in a few lines.
CRISPR gRNA scoring is a place where these two principles meet in the real world at surprisingly close range. For a very short input of 20 bp protospacer + 3 bp PAM (23 bp total = 92-dimensional one-hot), the context features learned by a relatively small 1D CNN correlate with actual experimental efficiency at Spearman ρ ≥ 0.7. This article is a pipeline that strengthens that predictive power with an ensemble of multiple scorers and quantifies off-target risk to turn it into a real experimental design optimization tool.
Hardcore Problem Definition
Real-world Scenario: BRCA1 KO in K562
For DNA damage repair research, we knock out the BRCA1 (Breast Cancer 1) gene in K562 (human chronic myelogenous leukemia cell line) with an SpCas9-HF1 system. BRCA1 has 24 exons, a total mRNA of about 7.2 kb, and a coding region of about 5.6 kb. For KO experimental success rate and reproducibility, the following are required.
- Search for gRNA candidates across multiple exons (5'-side exons prioritized to avoid nonsense-mediated decay).
- Quantify on-target efficiency and off-target risk for each candidate gRNA.
- Order only the top 5 for synthesis (Twist Bioscience · IDT etc., about 30–100 USD each).
- Follow-up experimental validation (T7E1 assay or amplicon deep sequencing) of measured efficiency.
Fundamental Trade-off of CRISPR gRNA Design
- On-target efficiency: How efficiently the gRNA cuts the target position. Score 0–1, higher is better. Training data typically comes from assaying thousands of gRNAs in cell lines like K562 · HEK293T · Jurkat.
- Off-target safety: Where similar sequences exist in the genome + how serious the side effects at those locations are. Score 0–1, higher is safer.
- PAM-adjacent constraint: The standard PAM for SpCas9 is
NGG. Special variants includeNG(SpCas9-NG),NGN(SpG),NRN(SpRY), etc. - BE (base editor) editing window: Base editing edits a specific window (e.g. 4–8 nt) inside the protospacer. The alignment of the window position with the knockout target codon matters.
Existing Approach Spectrum and This Article's Position
- Empirical rules (Doench 2014, Xu 2015): Logistic regression based on hand-engineered features such as nucleotide composition around the PAM, GC content, secondary structure. Spearman ρ 0.4–0.5.
- Early 1D CNN (DeepCRISPR 2018): Learn 21 bp context. ρ 0.6–0.65 [1].
- CRISPRon (Xiang 2021): Context-aware CNN + regression. ρ 0.72 [2].
- DeepHF (Wang 2019): Specialized for SpCas9-HF1 · xCas9 variants. ρ 0.75 [3].
- CRISPRon-BE (Kim 2022): Specialized for base editing (ABE · CBE). ρ 0.68–0.75 [4].
- BE-Hive (2020): Strong at BE editing window prediction.
- PRIDICT (2023): Prime editing pegRNA design.
- Foundation model approach (experimental): gRNA embedding with DNABERT · Nucleotide Transformer followed by a downstream head.
This article ensembles four scorers — CRISPRon (on-target) + DeepHF (variant Cas9) + CRISPRon-BE (base editing) + CFD/MIT (off-target) — to offset the bias of any single scorer, and adds a whole-genome off-target scan via the CRISPOR remote API.
Target Metrics for This Article
- Automatic extraction of 200–500 gRNA candidates across all 24 BRCA1 exons (both strands, NGG PAM).
- Lipinski-style filter (GC 30–70%, avoid TTTT, avoid self-complementarity).
- On-target · off-target scores for each candidate via the ensemble scorer (total wall-clock 30 s–2 min).
- Whole-genome off-target scan via CRISPOR (top 20 only).
- Final ranking of the top 5 as an automatic report (lab-orderable Markdown + CSV format, including spacer + PAM + score matrix + off-target hit summary).
Tool Stack and Infrastructure Requirements
| Tool | Role | License |
|---|---|---|
Bioconductor crisprScore (R) | Integrated scorer framework (CRISPRon · DeepHF · CRISPRon-BE · CFD, etc.) | Artistic-2.0 |
| CRISPRon-BE (RTH-tools GitHub) | Base editing-specialized scorer (ABE · CBE) | GPL v3 |
| rpy2 (Python↔R bridge) | Calls R crisprScore from Python | GPL v2+ |
| Biopython | FASTA · GenBank parsing, PAM scan, reverse complement | Biopython License |
| CRISPOR remote API or local crispritz | Whole-genome off-target scan | Academic free · GPL |
Bioconductor BSgenome.Hsapiens.UCSC.hg38 | Human genome reference | Artistic-2.0 |
| pandas · matplotlib | Result tables · visualization | BSD |
Infrastructure requirements:
- Small consumer GPU (RTX 4060 or above recommended; CPU fallback possible but 5–10× slower).
- R 4.3+ + Bioconductor 3.18+ (
crisprScore·BSgenome.Hsapiens.UCSC.hg38·crisprBase·crisprDesign). - Python 3.10+ + PyTorch 2.0+ + rpy2 3.5+.
- 8 GB+ RAM. Disk: human genome reference ~3 GB, CRISPRon weights ~100 MB.
Estimated learner reproduction cost: 0 API cost (fully local or free CRISPOR remote). GPU time for scoring 500 gRNAs about 1–3 min (5 min first time including model loading).
Pipeline Real-World Implementation
Overall flow:
Step 1. BRCA1 Exon Sequence Extraction
Obtain BRCA1 mRNA (NM_007294) from NCBI RefSeq or hg38 exon coordinates from the UCSC Genome Browser, then extract exon sequences.
from dataclasses import dataclassfrom pathlib import Pathfrom typing import Iterator
import requestsfrom Bio import SeqIO, Entrezfrom Bio.Seq import Seq
Entrez.email = "your@email.example" # NCBI required
@dataclassclass ExonRegion: """Information for one exon of a gene.""" gene_name: str exon_number: int chromosome: str strand: str # "+" or "-" start_hg38: int # 0-based end_hg38: int length: int sequence: str coding_frame: int | None # None if UTR, 0/1/2 otherwise
def fetch_refseq_mrna(refseq_id: str = "NM_007294") -> str: """Fetch RefSeq mRNA sequence from NCBI (e.g., BRCA1 = NM_007294).""" with Entrez.efetch(db="nucleotide", id=refseq_id, rettype="fasta", retmode="text") as h: record = SeqIO.read(h, "fasta") return str(record.seq).upper()
def fetch_gene_exons_ucsc(gene_symbol: str, assembly: str = "hg38") -> list[ExonRegion]: """Fetch gene exon coordinates via the UCSC Genome Browser REST API. In practice, local parsing of GENCODE annotation GTF is much more stable. """ # UCSC Table Browser alternative: MyGene.info API resp = requests.get( f"https://mygene.info/v3/query?q=symbol:{gene_symbol}&species=human&fields=exons", timeout=30, ) resp.raise_for_status() hits = resp.json().get("hits", []) if not hits: return [] exons_info = hits[0].get("exons", []) if not exons_info: return [] canonical = exons_info[0] # canonical transcript exon_regions = [] for i, (start, end) in enumerate(canonical.get("position", [])): # Fetch actual sequence from UCSC (DAS API or local BSgenome) seq = _fetch_ucsc_dna(canonical["chr"], start, end, assembly) exon_regions.append(ExonRegion( gene_name=gene_symbol, exon_number=i + 1, chromosome=canonical["chr"], strand=canonical.get("strand", "+"), start_hg38=start, end_hg38=end, length=end - start, sequence=seq, coding_frame=None, # Detailed determination requires CDS coordinates )) return exon_regions
def _fetch_ucsc_dna(chrom: str, start: int, end: int, assembly: str = "hg38") -> str: """Return sequence for a specific region via the UCSC DAS API.""" url = f"https://api.genome.ucsc.edu/getData/sequence?genome={assembly};chrom={chrom};start={start};end={end}" resp = requests.get(url, timeout=30) resp.raise_for_status() return resp.json().get("dna", "").upper()Step 2. PAM Scan · Candidate gRNA Extraction
SpCas9 standard PAM NGG. Scan the opposite strand as well (reverse complement of the protospacer).
@dataclassclass GRNACandidate: gene_name: str exon_number: int protospacer: str # 20 bp target sequence pam: str # 3 bp PAM strand: str # "+" or "-" exon_position: int # start position within the exon sequence (0-based) genome_position: int # hg38 genome coordinate (0-based) context_50bp: str # 15 bp upstream + gRNA + PAM + 15 bp downstream = 53 bp (training input for CRISPRon etc.)
def scan_pam_ngg(sequence: str, gene_name: str, exon: ExonRegion) -> list[GRNACandidate]: """Extract NGG PAM + 20 bp protospacer candidates from both strands.""" candidates = [] seq_plus = sequence.upper() seq_minus = str(Seq(seq_plus).reverse_complement()) def _scan(strand_seq: str, strand_label: str, seq_len: int) -> None: for i in range(20, seq_len - 3): pam = strand_seq[i:i+3] if pam[1:3] != "GG": continue protospacer = strand_seq[i-20:i] if "N" in protospacer: continue context_start = max(0, i - 35) context_end = min(seq_len, i + 18) context = strand_seq[context_start:context_end] # Compute genome coordinate (strand-dependent direction flip) if strand_label == "+": genome_pos = exon.start_hg38 + (i - 20) else: genome_pos = exon.end_hg38 - i candidates.append(GRNACandidate( gene_name=gene_name, exon_number=exon.exon_number, protospacer=protospacer, pam=pam, strand=strand_label, exon_position=i - 20, genome_position=genome_pos, context_50bp=context, )) _scan(seq_plus, "+", len(seq_plus)) _scan(seq_minus, "-", len(seq_minus)) return candidates
def scan_gene_all_exons(exons: list[ExonRegion]) -> list[GRNACandidate]: all_cands = [] for exon in exons: all_cands.extend(scan_pam_ngg(exon.sequence, exons[0].gene_name, exon)) return all_candsStep 3. Basic Physicochemical Filter
CRISPR gRNAs are generally stably synthesized and functional when the following conditions are met:
- GC content 30–70%: Extremes reduce mismatch tolerance.
- Avoid 4+ consecutive Ts (TTTT): RNA Polymerase III termination signal.
- Avoid self-complementarity: Self secondary structure folds → interferes with sgRNA-Cas9 binding.
- First nucleotide rule: U6 promoter prefers gRNAs starting with G.
from Bio.SeqUtils import GC
def check_self_complementarity(seq: str, min_stem: int = 4) -> bool: """Self-complementarity check: if the 5' end 4 bp is the reverse complement of the 3' end 4 bp, hairpin risk.""" if len(seq) < min_stem * 2: return False stem_5 = seq[:min_stem] stem_3 = seq[-min_stem:] return stem_5 == str(Seq(stem_3).reverse_complement())
def filter_grna_physicochemical( candidates: list[GRNACandidate], min_gc: float = 30.0, max_gc: float = 70.0, max_poly_t: int = 3, prefer_g_start: bool = True, check_hairpin: bool = True,) -> list[GRNACandidate]: """Physicochemical filter.""" filtered = [] for c in candidates: # GC content gc = GC(c.protospacer) if not (min_gc <= gc <= max_gc): continue # Poly-T if "T" * (max_poly_t + 1) in c.protospacer: continue # G start (preferred) if prefer_g_start and c.protospacer[0] != "G": # Not completely excluded, but deprioritized (passes here) pass # Hairpin if check_hairpin and check_self_complementarity(c.protospacer): continue filtered.append(c) return filteredStep 4. Calling R crisprScore from Python (rpy2 Bridge)
The crisprScore package integrates most standard scorers — CRISPRon · DeepHF · CRISPRon-BE · CFD · Hsu-Zhang · MIT.
import numpy as npimport rpy2.robjects as rofrom rpy2.robjects.packages import importrfrom rpy2.robjects.vectors import StrVector
class CrisprScoreEnsemble: """Call multiple scorers from R crisprScore from Python."""
def __init__(self): # Load R packages (BiocManager::install required beforehand in R console) self.crisprscore = importr("crisprScore") self.base = importr("base")
def crispron(self, contexts: list[str]) -> list[float]: """CRISPRon on-target score. Input: each item is 30 bp context (4 bp upstream + 20 bp protospacer + 3 bp PAM + 3 bp downstream).""" r_input = StrVector(contexts) try: result = self.crisprscore.getCRISPRonScores(r_input) return list(result) except Exception as e: print(f"CRISPRon failed: {e}") return [float("nan")] * len(contexts)
def deep_hf(self, protospacers_with_pam: list[str], enzyme: str = "WT") -> list[float]: """DeepHF score. Input: each 23 bp (20 protospacer + 3 PAM). enzyme: 'WT', 'ESP', 'HF'.""" r_input = StrVector(protospacers_with_pam) try: result = self.crisprscore.getDeepHFScores(r_input, enzyme=enzyme) return list(result) except Exception as e: print(f"DeepHF failed: {e}") return [float("nan")] * len(protospacers_with_pam)
def crispron_be(self, contexts: list[str], editor: str = "ABE8e") -> list[float]: """CRISPRon-BE base editing efficiency. editor options: 'ABE8e' (adenine), 'BE4max' (cytosine), etc. """ r_input = StrVector(contexts) try: result = self.crisprscore.getCRISPRonBEScores(r_input, editor=editor) return list(result) except Exception as e: print(f"CRISPRon-BE failed: {e}") return [float("nan")] * len(contexts)
def cfd_off_target(self, protospacer: str, target_dna: str) -> float: """Cutting Frequency Determination (Doench 2016 [5]).""" try: result = self.crisprscore.getCFDScores( StrVector([protospacer]), StrVector([target_dna]), ) return float(list(result)[0]) except Exception: return 0.0
def mit_hsu_zhang(self, protospacer: str, target_dna: str) -> float: """MIT/Hsu-Zhang off-target score (position-dependent mismatch penalty).""" try: result = self.crisprscore.getMITScores( StrVector([protospacer]), StrVector([target_dna]), ) return float(list(result)[0]) except Exception: return 0.0R environment setup (reference commands for the article body):
# In the R consoleif (!require("BiocManager", quietly = TRUE)) install.packages("BiocManager")BiocManager::install(c( "crisprScore", "crisprDesign", "BSgenome.Hsapiens.UCSC.hg38", "crisprBase"))Step 5. CRISPOR Remote Off-Target Scan
CFD is a local single pairwise score. For a whole-genome off-target scan, CRISPOR is the standard [6]. There is also a local crispritz alternative.
def query_crispor_offtargets( protospacer: str, genome: str = "hg38", max_mismatches: int = 4, timeout: int = 300,) -> list[dict]: """Whole-genome off-target scan via the CRISPOR remote API. Returns: [{genome_pos, mismatches, cfd_score, gene}, ...] """ # Actual endpoint: http://crispor.tefor.net/crispor.py # This is a conceptual wrapper. The actual API returns HTML so parsing is required. try: resp = requests.get( "http://crispor.tefor.net/crispor.py", params={ "seq": protospacer, "org": genome, "pam": "NGG", "showAllOTs": "1", }, timeout=timeout, ) resp.raise_for_status() # Actual CRISPOR response is HTML/TSV. Only example parsing is shown here. # In practice, use BeautifulSoup or the crispor local CLI to obtain TSV. return _parse_crispor_html(resp.text) except Exception as e: print(f"CRISPOR off-target scan failed ({protospacer}): {e}") return []
def _parse_crispor_html(html: str) -> list[dict]: """Parse CRISPOR HTML response (conceptual stub, use BeautifulSoup in practice).""" return [] # Complete in production
def compute_off_target_summary(off_targets: list[dict]) -> dict: """Summary of the off-target scan result.""" if not off_targets: return { "n_offtargets_mm4": 0, "max_cfd_score": 0.0, "high_risk_genes": [], "genome_safety_score": 1.0, } max_cfd = max(ot["cfd_score"] for ot in off_targets) high_risk = [ot["gene"] for ot in off_targets if ot["cfd_score"] > 0.5 and ot["gene"]] # Overall safety: invert max CFD (0 = dangerous, 1 = safe) safety = 1.0 - max_cfd return { "n_offtargets_mm4": len(off_targets), "max_cfd_score": max_cfd, "high_risk_genes": high_risk[:5], "genome_safety_score": safety, }Step 6. Ensemble Ranking
Weighted ensemble of the results from multiple scorers. Optimal weights per vendor/task can be fine-tuned with experimental validation data.
import pandas as pd
DEFAULT_WEIGHTS = { "crispron": 0.30, "deep_hf": 0.25, "crispron_be": 0.10, # 0 if not BE scenario "off_target_safety": 0.35,}
def build_ensemble_ranking( candidates: list[GRNACandidate], on_scores: dict[str, list[float]], off_target_summaries: list[dict], weights: dict[str, float] = DEFAULT_WEIGHTS,) -> pd.DataFrame: """Integrated scorer ranking DataFrame.""" df = pd.DataFrame([{ "gene": c.gene_name, "exon": c.exon_number, "protospacer": c.protospacer, "pam": c.pam, "strand": c.strand, "genome_position": c.genome_position, "context_50bp": c.context_50bp, } for c in candidates]) for name, scores in on_scores.items(): df[f"on_{name}"] = scores df["off_n_mm4"] = [s["n_offtargets_mm4"] for s in off_target_summaries] df["off_max_cfd"] = [s["max_cfd_score"] for s in off_target_summaries] df["off_safety"] = [s["genome_safety_score"] for s in off_target_summaries] df["off_high_risk_genes"] = [ ";".join(s["high_risk_genes"]) for s in off_target_summaries ] # Weighted average after normalization def _normalize(col): c = df[col].fillna(df[col].median()) return (c - c.min()) / (c.max() - c.min() + 1e-8) final = np.zeros(len(df)) if "on_crispron" in df.columns: final += weights.get("crispron", 0.0) * _normalize("on_crispron") if "on_deep_hf" in df.columns: final += weights.get("deep_hf", 0.0) * _normalize("on_deep_hf") if "on_crispron_be" in df.columns: final += weights.get("crispron_be", 0.0) * _normalize("on_crispron_be") final += weights["off_target_safety"] * df["off_safety"].fillna(0.5) df["final_score"] = final return df.sort_values("final_score", ascending=False).reset_index(drop=True)Step 7. Automatic Lab Order Report
Emit the top 5 gRNAs as Markdown + CSV ready to order from Twist Bioscience · IDT etc.
def generate_lab_report( ranked: pd.DataFrame, output_md: Path, output_csv: Path, top_k: int = 5,) -> None: """Generate a lab-orderable report.""" top = ranked.head(top_k) lines = [ f"# Top {top_k} gRNA Candidates Order Report", "", f"Target gene: **{top.iloc[0]['gene']}**", f"Generated at: (fill with pipeline execution time)", "", "## Summary Table", "", "| Rank | Exon | Protospacer (20 bp) | PAM | Strand | On (CRISPRon) | On (DeepHF) | Off Safety | Final |", "|:----:|:----:|:-------------------:|:---:|:------:|:-------------:|:-----------:|:----------:|:-----:|", ] for i, row in top.iterrows(): lines.append( f"| {i+1} | {row['exon']} | `{row['protospacer']}` | {row['pam']} | {row['strand']} | " f"{row.get('on_crispron', float('nan')):.3f} | {row.get('on_deep_hf', float('nan')):.3f} | " f"{row['off_safety']:.3f} | **{row['final_score']:.3f}** |" ) lines += [ "", "## Order Sequences (5' → 3')", "", "If the spacer sequence does not have the G required by the U6 promoter at the front, add a G in front or use alternative promoters like ExoScribe.", "", ] for i, row in top.iterrows(): spacer = row["protospacer"] if spacer[0] != "G": forge = f"G{spacer}" else: forge = spacer lines.append(f"### Rank {i+1}") lines.append(f"- Protospacer (spacer only, 20 bp): `{spacer}`") lines.append(f"- Order-ready (with G prefix if needed): `{forge}` ({len(forge)} bp)") lines.append(f"- PAM: `{row['pam']}`") lines.append(f"- Genome position (hg38): {row['genome_position']}") if row["off_high_risk_genes"]: lines.append(f"- ⚠️ High-risk off-target genes: {row['off_high_risk_genes']}") lines.append("") lines += [ "## Pre-experiment Checklist", "", "- [ ] Re-check alignment before ordering (BLASTn to hg38 · GRCh38.p14)", "- [ ] Prepare SpCas9-HF1 vector (e.g., pX330 or plentiCRISPR v2)", "- [ ] Mycoplasma test complete for the target cell line (K562)", "- [ ] Design T7E1 assay or amplicon deep sequencing primers", "- [ ] Order a control gRNA (non-targeting, e.g., sgLacZ) in parallel", ] output_md.write_text("\n".join(lines), encoding="utf-8") top.to_csv(output_csv, index=False)Integrated Pipeline · Execution Example
An execution function that ties everything together, and a BRCA1 scenario execution example.
def full_pipeline( gene_symbol: str, refseq_id: str, output_prefix: str, editor: str | None = None, # None: nuclease KO, "ABE8e"/"BE4max": base editing top_k: int = 5,) -> pd.DataFrame: print(f"[1/7] {gene_symbol} exon info fetch (RefSeq {refseq_id})") # In practice, local parsing of exon coordinates from GENCODE GTF is recommended exons = fetch_gene_exons_ucsc(gene_symbol) if not exons: # fallback: treat the whole mRNA as a single "exon" (simple demonstration) seq = fetch_refseq_mrna(refseq_id) exons = [ExonRegion( gene_name=gene_symbol, exon_number=1, chromosome="", strand="+", start_hg38=0, end_hg38=len(seq), length=len(seq), sequence=seq, coding_frame=0, )] print(f" exons: {len(exons)}") print("[2/7] PAM (NGG) scan") all_cands = scan_gene_all_exons(exons) print(f" raw candidates: {len(all_cands)}") print("[3/7] Physicochemical filter") filtered = filter_grna_physicochemical(all_cands) print(f" passing filter: {len(filtered)}") print("[4/7] On-target scorer ensemble") scorer = CrisprScoreEnsemble() contexts = [c.context_50bp for c in filtered] proto_pam = [c.protospacer + c.pam for c in filtered] on_scores = { "crispron": scorer.crispron(contexts), "deep_hf": scorer.deep_hf(proto_pam, enzyme="HF"), # SpCas9-HF1 } if editor: on_scores["crispron_be"] = scorer.crispron_be(contexts, editor=editor) print("[5/7] Off-target scan (CRISPOR remote)") off_summaries = [] for i, c in enumerate(filtered): if i % 50 == 0: print(f" progress {i}/{len(filtered)}") offs = query_crispor_offtargets(c.protospacer) off_summaries.append(compute_off_target_summary(offs)) print("[6/7] Ensemble ranking") ranked = build_ensemble_ranking(filtered, on_scores, off_summaries) ranked.to_csv(f"{output_prefix}_all_candidates.csv", index=False) print(f"[7/7] Top {top_k} order report") generate_lab_report( ranked, Path(f"{output_prefix}_top{top_k}.md"), Path(f"{output_prefix}_top{top_k}.csv"), top_k=top_k, ) print(f" done: {output_prefix}_top{top_k}.md") return ranked
# Execution example (BRCA1 KO scenario)# ranked = full_pipeline(# gene_symbol="BRCA1",# refseq_id="NM_007294",# output_prefix="brca1_k562",# editor=None, # SpCas9-HF1 nuclease KO# top_k=5,# )Expected Execution Log (For Reference)
When learners actually execute, they can expect a log like this:
[1/7] BRCA1 exon info fetch (RefSeq NM_007294)
exons: 24
[2/7] PAM (NGG) scan
raw candidates: 423
[3/7] Physicochemical filter
passing filter: 197
[4/7] On-target scorer ensemble
[5/7] Off-target scan (CRISPOR remote)
progress 0/197
progress 50/197
...
[6/7] Ensemble ranking
[7/7] Top 5 order report
done: brca1_k562_top5.mdPerformance · Cost · Known Failure Cases
Performance Reference (Public Benchmark Citations)
| Scorer | Benchmark Dataset | Spearman ρ | Notes | Source |
|---|---|---|---|---|
| Rule 4 (Doench 2014) | Own benchmark | 0.42 | Hand-engineered baseline | Doench et al., Nat Biotechnol 2014 |
| DeepCRISPR | Kim 2017 dataset | 0.65 | 1D CNN | Chuai et al., Genome Biol 2018 [1] |
| CRISPRon (Xiang 2021) | Kim 2019 | 0.72 | Context-aware CNN | Xiang et al., Nat Commun 2021 [2] |
| DeepHF (WT SpCas9) | Wang 2019 | 0.73 | RNN + attention | Wang et al., Nat Commun 2019 [3] |
| DeepHF (SpCas9-HF1) | Wang 2019 | 0.75 | HF1 variant specialized | Wang et al. 2019 [3] |
| CRISPRon-BE (ABE8e) | ABEmax dataset | 0.68 | Base editing | Kim et al. 2022 [4] |
| BE-Hive | BE4/ABE dataset | 0.75 | Strong at editing window prediction | Arbab et al. 2020 |
| Ensemble (approximate to this article) | Benchmark reproduction | 0.77–0.82 (estimated) | Average of multiple scorers | Community benchmark [7] |
Estimated Learner Reproduction Cost
- 0 API cost (fully local or free CRISPOR remote).
- Small consumer GPU-based scoring of 500 gRNAs about 1–3 min.
- CPU fallback 5–15 min.
- CRISPOR remote API: less than 1 req/s recommended, 500-gRNA scan about 10–20 min.
- Local crispritz alternative: CPU 30 min + 20 GB genome index download.
5 Known Failure Cases (Collected from Community/Papers)
-
rpy2 environment conflict (R 4.3 + Bioconductor 3.18 + rpy2 3.5)
Symptom: R library load failures such aslibR.so symbol lookup error·PermissionError: could not load package.
Cause: Double registration of R library paths between conda vs system R, common especially in macOS/Linux mixed use.
Workaround: (a) Install R uniformly through conda-forge inside the conda environment (conda install -c conda-forge r-base bioconductor-crisprscore), (b) explicitly setR_HOME·LD_LIBRARY_PATHenvironment variables, (c) isolate with a Docker container (rocker/tidyverse + crisprScore install), (d) bypass rpy2 by calling Rscript directly via subprocess.
Source: rpy2 GitHub Issues [8]; Bioconductor Support Forum crisprScore threads. -
Discrepancy between computed CFD score and actual experimental off-target results
Symptom: A gRNA that is safe (low score) in CFD induces unexpected off-target editing in actual experiments (GUIDE-seq · CIRCLE-seq).
Cause: CFD is learned mismatch-position weights (Doench 2016 [5]) but does not reflect chromatin state · DNA methylation · nucleosome positioning.
Workaround: (a) Ensemble multiple off-target algorithms (CFD + MIT/Hsu-Zhang + Elevation), (b) apply separate penalties for TSS · enhancer regions (referencing ENCODE annotation), (c) post-validate with measured off-target data such as GUIDE-seq · Digenome-seq, (d) run at least 3 top candidates in parallel measurements before final selection.
Source: Tsai et al. "GUIDE-seq" Nat Biotechnol 2015 [9]; Doench 2016 CFD paper [5]; CRISPOR docs [6]. -
BE (base editing) editing-window misinterpretation + bystander effect
Symptom: A base editor scored with CRISPRon-BE edits other nucleotides in addition to the intended edit target (bystander).
Cause: BE is assumed to edit a specific window (e.g., 4–8 nt) inside the protospacer, but in practice extends to 5–10 nt, and adenine deaminase (ABE) is biased toward specific sequence contexts.
Workaround: (a) Ensemble editing-window annotations across multiple models (BE-Hive · BE-DICT · CRISPRon-BE), (b) mark bystander candidates separately in the experimental design, (c) consider Prime editing (PE) scorers (PRIDICT) for precise editing, (d) codon-wise simulation to check whether target C/A positions in the editing window cause unwanted codon changes.
Source: Anzalone et al. "Prime editing" Nature 2019 [10]; Arbab et al. "BE-Hive" Cell 2020. -
CRISPOR remote API rate limit + response latency
Symptom: Scanning 500 gRNAs consecutively yields more than half failures or timeouts.
Cause: The CRISPOR remote server is CPU-constrained. It is a community resource.
Workaround: (a) Batch scan with local crispritz + BWA + genome index (requires 20 GB genome index download), (b) run CRISPOR locally (install the Python script locally), (c) request intervals of 5 s or more, (d) send only top 20 candidates to CRISPOR remote (100+ candidates locally).
Source: CRISPOR official documentation "batch use" section [6]. -
BRCA1 alternative splicing leading to exon-coordinate mismatch
Symptom: BRCA1 has multiple splice variants. NM_007294 is canonical, but other transcripts (NM_007297 etc.) have different exon counts/coordinates.
Cause: MyGene.info returns only one canonical. The isoform actually expressed in the target tissue/cell line may differ.
Workaround: (a) Parse multiple transcript coordinates from GENCODE GTF, (b) confirm the dominant isoform for the target tissue/cell line using measured expression data (GTEx · Human Protein Atlas), (c) prioritize exons common to multiple isoforms (constitutive exons).
Source: GENCODE annotation docs [11]; UCSC Genome Browser BRCA1 track.
Extension Ideas
- Prime Editing (PE) scorer extension: PE has complex 5' pegRNA design but is optimal for precision editing. Incorporate latest scorers such as PRIDICT · DeepPE.
- CRISPRi/CRISPRa (activation/interference): Expression modulation instead of KO. Use
getCrispraiScoresin crisprScore, dCas9-VP64 (activation) or dCas9-KRAB (interference). - Multi-gRNA (multiplex) combination optimization: When knocking out multiple genes simultaneously, minimize mutual off-target interference (combinatorial optimization · integer programming).
- Patient-specific genetic background: Off-target positions differ in patients with specific SNPs (personalized off-target, referring to gnomAD · UK Biobank annotation).
- Nucleosome positioning integration: Apply separate penalties for nucleosome-occluded regions using MNase-seq data.
- T7 in vitro synthesis vs oligo pool: Depending on the gRNA order format (single vs pool), the scoring strategy for top 5 vs top 200 differs.
Next Topics
- Topic 07
drug-target-gnn: Pre-compress candidate targets to knock out with gRNA using GNN drug-target predictions. - Topic 10
med-llm-reproduction: Have multiple LLMs reproduce the gRNA scorer benchmarks for evaluation. - Topic 12
single-cell-perturbation: In silico predict cell responses to gRNA candidates before actually knocking them out (Perturb-seq simulation). - Topic 14
bio-mcp-agent: Expose gRNA design as an MCP tool for autonomous execution like "extract KO gRNAs from BRCA1 exons 3–5."
References
- Chuai G, Ma H, Yan J, et al. "DeepCRISPR: optimized CRISPR guide RNA design by deep learning." Genome Biology 2018.
https://genomebiology.biomedcentral.com/articles/10.1186/s13059-018-1459-4 - Xiang X, Corsi GI, Anthon C, et al. "Enhancing CRISPR-Cas9 gRNA efficiency prediction by deep learning (CRISPRon)." Nature Communications 2021.
https://www.nature.com/articles/s41467-021-23576-0 - Wang D, Zhang C, Wang B, et al. "Optimized CRISPR guide RNA design for two high-fidelity Cas9 variants by deep learning (DeepHF)." Nature Communications 2019.
https://www.nature.com/articles/s41467-019-12281-8 - Kim HK, Yu G, Park J, et al. "Predicting the efficiency of prime editing guide RNAs in human cells (PRIDICT); base editing companion (CRISPRon-BE)." Nature Biotechnology 2023 (relevant portion).
https://www.nature.com/articles/s41587-022-01613-7 - Doench JG, Fusi N, Sullender M, et al. "Optimized sgRNA design to maximize activity and minimize off-target effects of CRISPR-Cas9 (CFD original paper)." Nature Biotechnology 2016.
https://www.nature.com/articles/nbt.3437 - CRISPOR web tool and documentation:
http://crispor.tefor.net//https://github.com/maximilianh/crisporWebsite - crisprScore Bioconductor package:
https://bioconductor.org/packages/release/bioc/html/crisprScore.html - rpy2 GitHub Issues:
https://github.com/rpy2/rpy2/issues - Tsai SQ, Zheng Z, Nguyen NT, et al. "GUIDE-seq enables genome-wide profiling of off-target cleavage by CRISPR-Cas nucleases." Nature Biotechnology 2015.
https://www.nature.com/articles/nbt.3117 - Anzalone AV, Randolph PB, Davis JR, et al. "Search-and-replace genome editing without double-strand breaks or donor DNA (Prime Editing)." Nature 2019.
https://www.nature.com/articles/s41586-019-1711-4 - GENCODE human genome annotation:
https://www.gencodegenes.org/human/ - Kim HK et al. "Predicting the efficiency of prime editing guide RNAs in human cells (PRIDICT)." Nat Biotechnol 2023.
- Broad Institute CRISPResso2 (editing result analysis):
https://github.com/pinellolab/CRISPResso2 - Bioconductor BSgenome.Hsapiens.UCSC.hg38:
https://bioconductor.org/packages/release/data/annotation/html/BSgenome.Hsapiens.UCSC.hg38.html - RTH-tools CRISPRon-BE GitHub:
https://github.com/RTH-tools/crispron-BE - BE-Hive: Arbab M et al. "Determinants of Base Editing Outcomes from Target Library Analysis and Machine Learning." Cell 2020.
https://github.com/maxwshen/be_predict_efficiency - Addgene sgRNA order standard protocols:
https://www.addgene.org/guides/crispr/ - crispritz (local off-target scan):
https://github.com/pinellolab/CRISPRitz - NCBI RefSeq NM_007294 (BRCA1 canonical mRNA):
https://www.ncbi.nlm.nih.gov/nuccore/NM_007294 - MyGene.info API:
https://mygene.info/v3/api