Back to List

CRISPR Guide RNA On/Off-Target Scoring — Ranking 100 Candidates in 30 Seconds Before a BRCA1 KO Experiment

Using a BRCA1 exon KO scenario as an example, rank 100–500 gRNA candidates targeting the K562 cell line in 30 seconds via a deep learning scorer ensemble (CRISPRon · DeepHF · CRISPRon-BE). On-target efficiency + off-target risk (CFD · CRISPOR DB) dual filter, crisprScore Bioconductor package rpy2 bridge, and automatic generation of lab-ready order reports.

Intermediate
|
35min
|
Verified (2026-07)
Progress0/15 (0%)

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 include NG (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

ToolRoleLicense
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 PythonGPL v2+
BiopythonFASTA · GenBank parsing, PAM scan, reverse complementBiopython License
CRISPOR remote API or local crispritzWhole-genome off-target scanAcademic free · GPL
Bioconductor BSgenome.Hsapiens.UCSC.hg38Human genome referenceArtistic-2.0
pandas · matplotlibResult tables · visualizationBSD

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:

mermaid

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.

python
from dataclasses import dataclass
from pathlib import Path
from typing import Iterator
import requests
from Bio import SeqIO, Entrez
from Bio.Seq import Seq
Entrez.email = "your@email.example" # NCBI required
@dataclass
class 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).

python
@dataclass
class 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_cands

Step 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.
python
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 filtered

Step 4. Calling R crisprScore from Python (rpy2 Bridge)

The crisprScore package integrates most standard scorers — CRISPRon · DeepHF · CRISPRon-BE · CFD · Hsu-Zhang · MIT.

python
import numpy as np
import rpy2.robjects as ro
from rpy2.robjects.packages import importr
from 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.0

R environment setup (reference commands for the article body):

bash
# In the R console
if (!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.

python
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.

python
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.

python
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.

python
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:

text
[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.md

Performance · Cost · Known Failure Cases

Performance Reference (Public Benchmark Citations)

ScorerBenchmark DatasetSpearman ρNotesSource
Rule 4 (Doench 2014)Own benchmark0.42Hand-engineered baselineDoench et al., Nat Biotechnol 2014
DeepCRISPRKim 2017 dataset0.651D CNNChuai et al., Genome Biol 2018 [1]
CRISPRon (Xiang 2021)Kim 20190.72Context-aware CNNXiang et al., Nat Commun 2021 [2]
DeepHF (WT SpCas9)Wang 20190.73RNN + attentionWang et al., Nat Commun 2019 [3]
DeepHF (SpCas9-HF1)Wang 20190.75HF1 variant specializedWang et al. 2019 [3]
CRISPRon-BE (ABE8e)ABEmax dataset0.68Base editingKim et al. 2022 [4]
BE-HiveBE4/ABE dataset0.75Strong at editing window predictionArbab et al. 2020
Ensemble (approximate to this article)Benchmark reproduction0.77–0.82 (estimated)Average of multiple scorersCommunity 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)

  1. rpy2 environment conflict (R 4.3 + Bioconductor 3.18 + rpy2 3.5)
    Symptom: R library load failures such as libR.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 set R_HOME · LD_LIBRARY_PATH environment 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.

  2. 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].

  3. 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.

  4. 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].

  5. 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 getCrispraiScores in 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

  1. 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
  2. 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
  3. 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
  4. 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
  5. 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
  6. CRISPOR web tool and documentation: http://crispor.tefor.net/ / https://github.com/maximilianh/crisporWebsite
  7. crisprScore Bioconductor package: https://bioconductor.org/packages/release/bioc/html/crisprScore.html
  8. rpy2 GitHub Issues: https://github.com/rpy2/rpy2/issues
  9. 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
  10. 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
  11. GENCODE human genome annotation: https://www.gencodegenes.org/human/
  12. Kim HK et al. "Predicting the efficiency of prime editing guide RNAs in human cells (PRIDICT)." Nat Biotechnol 2023.
  13. Broad Institute CRISPResso2 (editing result analysis): https://github.com/pinellolab/CRISPResso2
  14. Bioconductor BSgenome.Hsapiens.UCSC.hg38: https://bioconductor.org/packages/release/data/annotation/html/BSgenome.Hsapiens.UCSC.hg38.html
  15. RTH-tools CRISPRon-BE GitHub: https://github.com/RTH-tools/crispron-BE
  16. 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
  17. Addgene sgRNA order standard protocols: https://www.addgene.org/guides/crispr/
  18. crispritz (local off-target scan): https://github.com/pinellolab/CRISPRitz
  19. NCBI RefSeq NM_007294 (BRCA1 canonical mRNA): https://www.ncbi.nlm.nih.gov/nuccore/NM_007294
  20. MyGene.info API: https://mygene.info/v3/api

💬 Questions & Comments

0 comments

You can post without signing in. Guest comments cannot be edited or deleted by their author.

0/2000

Loading...