Interactive ASCAT-like Purity/Ploidy Fitting¶
Because a bulk tumor sample contains both tumor and normal cells, its observed LogR and B-allele frequency (BAF) depend on tumor purity and ploidy. ASCAT estimates these parameters using a grid search over candidate combinations, favoring those that produce allele-specific copy-number estimates close to nonnegative integers, as described in Allele-specific copy number analysis of tumors.
This interactive GenomeSpy example lets you explore how different purity/ploidy combinations affect the inferred allele-specific copy numbers and their fit to the observed data.
The top "sunrise" plot shows integer-fit distance over a grid of tumor purity
(rho) and tumor ploidy (psi). Color shows the mean squared distance on a
logarithmic scale. Blue cells have lower distances, meaning that the included
allele-specific estimates are closer to integer copy numbers.
Click or drag anywhere on the sunrise plot to choose a fit. The readout below the heatmap evaluates the exact chosen coordinates, and the other panels update the allele-specific copy numbers and fitted LogR and BAF overlays.
The sample dropdown below the visualization switches between several simulated tumors. Its selection reloads the segment and raw-probe data and recomputes the sunrise plot and fitted tracks.
{
"description": [
"Interactive ASCAT-like Purity/Ploidy Fitting",
"Visualizes ASCAT purity/ploidy fits for simulated samples and shows how rounded allele-specific copy-number estimates change when rho and psi are adjusted.",
"Loo P Van, Nordgard SH, Lingjærde OC, et al.",
"Allele-specific copy number analysis of tumors",
"Proc Natl Acad Sci. 2010;107(39):16910-16915. doi:10.1073/pnas.1009843107"
],
"assembly": "hg19",
"params": [
{ "name": "minLength", "value": 1 },
{ "name": "selectedFit" },
{
"name": "sample",
"value": "S96",
"bind": {
"input": "select",
"options": [
"S17",
"S36",
"S54",
"S64",
"S77",
"S84",
"S96",
"S97",
"S100"
],
"name": "Sample"
}
},
{
"name": "gamma",
"value": 0.55,
"bind": {
"input": "range",
"min": 0.2,
"max": 1.2,
"step": 0.05,
"name": "LogR decompaction (gamma)",
"debounce": 100,
"description": "Controls LogR decompaction in the ASCAT copy-number equations."
}
},
{
"name": "downweightBalanced",
"value": true,
"bind": {
"input": "checkbox",
"name": "Downweight balanced segments",
"description": "Multiply the heterozygous-probe-count weight of exactly balanced segments by 0.05, as in ASCAT R v3.2.0."
}
},
{
"name": "fitBothAlleles",
"value": false,
"bind": {
"input": "checkbox",
"name": "Include both alleles in fit",
"description": "Include major-allele rounding error alongside minor-allele rounding error in the sunrise and selected-fit score."
}
}
],
"data": {
"url": {
"expr": "'https://data.genomespy.app/sample-data/ASCAT/ascat_fit_segments_' + sample + '.tsv.gz'"
}
},
"transform": [
{
"type": "collect"
},
{
"type": "formula",
"expr": "(selectedFit.values.y - 1 + pow(2, datum.logRMean / gamma) * (1 - datum.bafMean) * (2 * (1 - selectedFit.values.y) + selectedFit.values.y * selectedFit.values.x)) / selectedFit.values.y",
"as": "aRaw",
"description": "Apply ASCAT Supporting Information Eq. S7 to estimate raw major-allele copy number at the selected purity and tumor ploidy."
},
{
"type": "formula",
"expr": "(selectedFit.values.y - 1 + pow(2, datum.logRMean / gamma) * datum.bafMean * (2 * (1 - selectedFit.values.y) + selectedFit.values.y * selectedFit.values.x)) / selectedFit.values.y",
"as": "bRaw",
"description": "Apply ASCAT Supporting Information Eq. S8 to estimate raw minor-allele copy number at the selected purity and tumor ploidy."
},
{
"type": "formula",
"expr": "max(0, round(datum.aRaw))",
"as": "nMajor",
"description": "Round the raw major-allele estimate to a nonnegative integer copy number as in Supporting Information Eq. S10."
},
{
"type": "formula",
"expr": "max(0, round(datum.bRaw))",
"as": "nMinor",
"description": "Round the raw minor-allele estimate to a nonnegative integer copy number as in Supporting Information Eq. S11."
},
{
"type": "formula",
"expr": "abs(datum.aRaw - datum.nMajor)",
"as": "aError"
},
{
"type": "formula",
"expr": "abs(datum.bRaw - datum.nMinor)",
"as": "bError"
},
{
"type": "formula",
"expr": "gamma * log((2 * (1 - selectedFit.values.y) + selectedFit.values.y * (datum.nMajor + datum.nMinor)) / (2 * (1 - selectedFit.values.y) + selectedFit.values.y * selectedFit.values.x)) / LN2",
"as": "logRMean_ASCAT",
"description": "Project the rounded copy numbers back to fitted LogR using Supporting Information Eq. S12."
},
{
"type": "formula",
"expr": "(1 - selectedFit.values.y + selectedFit.values.y * datum.nMinor) / (2 - 2 * selectedFit.values.y + selectedFit.values.y * (datum.nMajor + datum.nMinor))",
"as": "bafMean_ASCAT",
"description": "Project the rounded copy numbers back to fitted BAF using Supporting Information Eq. S13."
},
{
"type": "identifier"
}
],
"resolve": { "axis": { "x": "shared" } },
"encoding": {
"x": {
"chrom": "chr",
"pos": "startpos",
"type": "locus",
"scale": { "type": "locus" },
"axis": { "title": null }
},
"x2": {
"chrom": "chr",
"pos": "endpos",
"offset": 1
}
},
"vconcat": [
{
"name": "sunrise",
"title": {
"text": {
"expr": "'Purity/ploidy integer-fit distance - ' + sample"
},
"style": "overlay-title"
},
"height": 240,
"params": [
{
"name": "selectedFit",
"push": "outer",
"value": { "x": 2.75, "y": 0.56 },
"ruler": {
"encodings": ["x", "y"],
"on": "mousedown",
"clear": false,
"snap": false,
"mark": {
"strokeDash": [5, 3]
}
}
}
],
"data": {
"sequence": {
"start": 0.1,
"stop": 1.051,
"step": 0.01,
"as": "rhoCandidate"
}
},
"transform": [
{
"type": "cross",
"description": "Form the purity and tumor-ploidy grid used by the ASCAT parameter search.",
"from": {
"data": {
"sequence": {
"start": 1,
"stop": 6.001,
"step": 0.05,
"as": "psiCandidate"
}
}
}
},
{
"type": "cross",
"description": "Cross every purity/ploidy candidate with ASCAT's joint segmented LogR/BAF runs for distance evaluation.",
"from": {
"data": {
"url": {
"expr": "'https://data.genomespy.app/sample-data/ASCAT/ascat_fit_segments_' + sample + '.tsv.gz'"
}
},
"transform": [
{
"type": "filter",
"expr": "datum.chr !== 'X'"
},
{
"type": "project",
"fields": ["logRMean", "bafMean", "nProbes"]
},
{
"type": "formula",
"expr": "datum.nProbes * (downweightBalanced && datum.bafMean === 0.5 ? 0.05 : 1)",
"as": "fitWeight",
"description": "Weight each joint run by its heterozygous-probe count and, optionally, downweight exactly balanced runs 20-fold as in ASCAT R v3.2.0."
}
]
}
},
{
"type": "formula",
"expr": "(datum.rhoCandidate - 1 + pow(2, datum.logRMean / gamma) * datum.bafMean * (2 * (1 - datum.rhoCandidate) + datum.rhoCandidate * datum.psiCandidate)) / datum.rhoCandidate",
"as": "bRawCandidate",
"description": "Apply ASCAT Supporting Information Eq. S8 to estimate minor-allele copy number for each candidate and segment."
},
{
"type": "formula",
"expr": "(datum.rhoCandidate - 1 + pow(2, datum.logRMean / gamma) * (1 - datum.bafMean) * (2 * (1 - datum.rhoCandidate) + datum.rhoCandidate * datum.psiCandidate)) / datum.rhoCandidate",
"as": "aRawCandidate",
"description": "Apply ASCAT Supporting Information Eq. S7 to estimate major-allele copy number when both alleles are included in the fit."
},
{
"type": "formula",
"expr": "max(0, round(datum.bRawCandidate))",
"as": "nMinorCandidate",
"description": "Round each candidate's raw minor-allele estimate to a nonnegative integer copy number."
},
{
"type": "formula",
"expr": "max(0, round(datum.aRawCandidate))",
"as": "nMajorCandidate",
"description": "Round each candidate's raw major-allele estimate to a nonnegative integer copy number."
},
{
"type": "formula",
"expr": "(pow(datum.bRawCandidate - datum.nMinorCandidate, 2) + (fitBothAlleles ? pow(datum.aRawCandidate - datum.nMajorCandidate, 2) : 0)) * datum.fitWeight",
"as": "errorSquaredWeighted",
"description": "Evaluate uncapped squared distance to nonnegative integers using the minor allele alone or both alleles, weighted by ASCAT's probe-based run weight."
},
{
"type": "aggregate",
"groupby": ["rhoCandidate", "psiCandidate"],
"fields": ["fitWeight", "errorSquaredWeighted"],
"ops": ["sum", "sum"],
"as": ["totalWeight", "distanceSum"],
"description": "Aggregate the selected allele-specific integer-fit distance for each purity and tumor-ploidy candidate."
},
{
"type": "formula",
"expr": "datum.distanceSum / (datum.totalWeight * (fitBothAlleles ? 2 : 1))",
"as": "meanRoundingError",
"description": "Normalize the summed error over the included alleles into the mean integer-fit distance displayed by the sunrise plot."
},
{
"type": "formula",
"expr": "max(0.1, datum.rhoCandidate - 0.005)",
"as": "rhoStart"
},
{
"type": "formula",
"expr": "min(1.05, datum.rhoCandidate + 0.005)",
"as": "rhoEnd"
},
{
"type": "formula",
"expr": "max(1, datum.psiCandidate - 0.025)",
"as": "psiStart"
},
{
"type": "formula",
"expr": "min(6, datum.psiCandidate + 0.025)",
"as": "psiEnd"
}
],
"resolve": {
"scale": { "x": "excluded" },
"axis": { "x": "excluded" }
},
"mark": {
"type": "rect",
"tooltip": null
},
"encoding": {
"x": {
"field": "psiStart",
"type": "quantitative",
"scale": { "domain": [1, 6] },
"axis": { "title": "Tumor ploidy (psi)", "format": ".1f" }
},
"x2": { "field": "psiEnd" },
"y": {
"field": "rhoStart",
"type": "quantitative",
"scale": { "domain": [0.1, 1.05] },
"axis": { "title": "Tumor purity (rho)", "format": ".2f" }
},
"y2": { "field": "rhoEnd" },
"color": {
"field": "meanRoundingError",
"type": "quantitative",
"scale": {
"scheme": "redblue",
"type": "log",
"reverse": true
}
}
}
},
{
"resolve": { "axis": { "x": "shared" } },
"vconcat": [
{
"name": "selectedGoodnessOfFit",
"title": {
"text": "Selected fit",
"orient": "none"
},
"height": 24,
"resolve": {
"scale": { "x": "excluded" },
"axis": { "x": "excluded" }
},
"transform": [
{
"type": "filter",
"expr": "datum.chr !== 'X'"
},
{
"type": "project",
"fields": ["bafMean", "aError", "bError", "nProbes"]
},
{
"type": "formula",
"expr": "datum.nProbes * (downweightBalanced && datum.bafMean === 0.5 ? 0.05 : 1)",
"as": "fitWeight",
"description": "Apply the same ASCAT probe-based and optional balanced-run weighting as the sunrise."
},
{
"type": "formula",
"expr": "(datum.bError * datum.bError + (fitBothAlleles ? datum.aError * datum.aError : 0)) * datum.fitWeight",
"as": "errorSquaredWeighted",
"description": "Evaluate the selected fit with the same weighted allele-specific error as the sunrise."
},
{
"type": "aggregate",
"fields": ["fitWeight", "errorSquaredWeighted"],
"ops": ["sum", "sum"],
"as": ["totalWeight", "distanceSum"],
"description": "Aggregate the selected fit's allele-specific error over all autosomal joint runs."
},
{
"type": "formula",
"expr": "100 - datum.distanceSum / (datum.totalWeight * (fitBothAlleles ? 2 : 1) * 0.25) * 100",
"as": "goodnessOfFit",
"description": "Convert the selected mean distance to an ASCAT-like goodness score; the publication defines the original rescaling after Eq. 3."
}
],
"layer": [
{
"name": "bar",
"mark": {
"type": "rect",
"color": "#a0e7e5",
"tooltip": null
},
"encoding": {
"x": {
"field": "goodnessOfFit",
"type": "quantitative",
"scale": {
"domain": [0, 100],
"clamp": true
},
"axis": null
},
"x2": { "datum": 0 }
}
},
{
"name": "text",
"mark": {
"type": "text",
"size": 12,
"tooltip": null
},
"encoding": {
"x": { "value": 0.5 },
"x2": null,
"text": {
"expr": "'rho ' + (round(selectedFit.values.y * 1000) / 1000) + ' | psi ' + (round(selectedFit.values.x * 1000) / 1000) + ' | gamma ' + gamma + ' | score ' + (round(datum.goodnessOfFit * 100) / 100)"
}
}
}
]
},
{
"name": "roundedAndDifference",
"title": {
"text": "Copy numbers rounded to integers + difference to raw values",
"style": "overlay"
},
"height": { "grow": 2 },
"layer": [
{
"title": "mismatchMinor",
"mark": {
"type": "rect",
"minWidth": { "expr": "minLength" },
"fillOpacity": 0.15,
"strokeOpacity": 1,
"strokeWidth": 1,
"fill": "gray",
"stroke": "#88d27a"
},
"encoding": {
"y": {
"field": "bRaw",
"type": "quantitative"
},
"y2": {
"field": "nMinor"
},
"strokeOpacity": {
"field": "bError",
"type": "quantitative",
"scale": {
"type": "pow",
"base": 2,
"domain": [0, 0.5],
"range": [0, 0.8]
}
}
}
},
{
"name": "mismatchMajor",
"title": "mismatchMajor",
"mark": {
"type": "rect",
"minWidth": { "expr": "minLength" },
"fillOpacity": 0.15,
"strokeOpacity": 1,
"strokeWidth": 1,
"fill": "gray",
"stroke": "#f06850"
},
"encoding": {
"y": {
"field": "aRaw",
"type": "quantitative"
},
"y2": {
"field": "nMajor"
},
"strokeOpacity": {
"field": "aError",
"type": "quantitative",
"scale": {
"type": "pow",
"base": 2,
"domain": [0, 0.5],
"range": [0, 0.8]
}
}
}
},
{
"title": "nMinor",
"mark": {
"type": "rule",
"minLength": { "expr": "minLength" },
"yOffset": -3.0
},
"encoding": {
"y": {
"field": "nMinor",
"type": "quantitative",
"scale": {
"domain": [0, 6],
"padding": 0.04,
"clamp": true
},
"axis": {
"tickMinStep": 1.0
}
},
"size": { "value": 5 },
"color": { "value": "#88d27a" }
}
},
{
"title": "nMajor",
"mark": {
"type": "rule",
"minLength": { "expr": "minLength" },
"yOffset": 3.0
},
"encoding": {
"y": {
"field": "nMajor",
"type": "quantitative",
"scale": {
"domain": [0, 6]
}
},
"size": { "value": 5 },
"color": {
"field": "nMajor",
"type": "quantitative",
"scale": {
"domain": [0, 6, 16],
"range": ["#f06850", "#f06850", "#5F0F0F"]
}
}
}
}
]
}
]
},
{
"name": "logRTrack",
"title": {
"text": "Observed and fitted LogR",
"style": "overlay-title"
},
"layer": [
{
"data": {
"url": {
"expr": "'https://data.genomespy.app/sample-data/ASCAT/ascat_raw_' + sample + '.tsv.gz'"
}
},
"title": "Single probe",
"mark": {
"type": "point",
"size": {
"expr": "min(pow(zoomLevel, 2) * width * height / 10000, 100)"
},
"color": "#aab",
"opacity": 0.3
},
"encoding": {
"x": {
"chrom": "chr",
"pos": "pos",
"type": "locus"
},
"y": { "field": "logR", "type": "quantitative", "title": null }
}
},
{
"title": "Mean LogR",
"mark": {
"type": "rule",
"minLength": { "expr": "minLength" }
},
"encoding": {
"y": {
"field": "logRMean",
"type": "quantitative",
"title": "LogR"
},
"size": { "value": 3 },
"color": { "value": "black" }
}
},
{
"title": "ASCAT-like fitted LogR",
"mark": {
"type": "rule",
"minLength": { "expr": "minLength" }
},
"encoding": {
"y": {
"field": "logRMean_ASCAT",
"type": "quantitative",
"title": null
},
"size": { "value": 2 },
"color": { "value": "#f06850" }
}
}
]
},
{
"name": "bafTrack",
"title": {
"text": "Observed and fitted B-allele frequency",
"style": "overlay-title"
},
"layer": [
{
"data": {
"url": {
"expr": "'https://data.genomespy.app/sample-data/ASCAT/ascat_raw_' + sample + '.tsv.gz'"
}
},
"transform": [{ "type": "filter", "expr": "datum.baf !== null" }],
"title": "Single probe",
"mark": {
"type": "point",
"size": {
"expr": "min(pow(zoomLevel, 2) * width * height / 10000, 100)"
},
"color": "#aab",
"opacity": 0.3
},
"encoding": {
"x": {
"chrom": "chr",
"pos": "pos",
"type": "locus"
},
"y": { "field": "baf", "type": "quantitative", "title": null }
}
},
{
"title": "Mean BAF",
"mark": {
"type": "rule",
"minLength": { "expr": "minLength" }
},
"encoding": {
"y": {
"field": "bafMean",
"type": "quantitative",
"scale": { "domain": [0, 1] },
"title": "BAF"
},
"size": { "value": 3 },
"color": { "value": "black" }
}
},
{
"title": "Mean BAF",
"mark": {
"type": "rule",
"minLength": 3.0
},
"encoding": {
"y": {
"expr": "1 - datum.bafMean",
"type": "quantitative",
"title": null
},
"size": { "value": 3 },
"color": { "value": "black" }
}
},
{
"title": "ASCAT-like fitted minor-allele BAF",
"mark": {
"type": "rule",
"minLength": { "expr": "minLength" }
},
"encoding": {
"y": {
"field": "bafMean_ASCAT",
"type": "quantitative",
"title": null
},
"size": { "value": 2 },
"color": { "value": "#88d27a" }
}
},
{
"title": "ASCAT-like fitted major-allele BAF",
"mark": {
"type": "rule",
"minLength": { "expr": "minLength" }
},
"encoding": {
"y": {
"expr": "1 - datum.bafMean_ASCAT",
"type": "quantitative",
"title": null
},
"size": { "value": 2 },
"color": { "value": "#f06850" }
}
}
]
}
],
"background": "#fafafa",
"config": {
"axisX": {
"grid": false,
"chromGrid": true,
"orient": "bottom"
},
"axisY": {
"grid": true,
"gridColor": "#f8f8f8"
},
"legend": { "disable": true },
"view": {
"fill": "white",
"stroke": "#c8c8c8",
"shadowBlur": 8,
"shadowColor": "black",
"shadowOpacity": 0.1
}
}
}
The visualization uses simulated example data from Allele-specific copy number analysis of tumors by Loo et al. and follows the ASCAT method described there. It is an explanatory ASCAT-like fit implemented as a GenomeSpy specification. The input has already been segmented using ASPCF; this visualization does not perform segmentation. The raw LogR and BAF probe values are shown only as a reference for the segmented and fitted values.
What to notice¶
Look for multiple local minima in the sunrise plot. Different purity/ploidy combinations can make the inferred copy numbers similarly close to integers, but a low distance is not necessarily the correct biological solution. For example, a solution may retain at least one minor-allele copy in every segment, implying no complete loss of heterozygosity. Whether this is plausible depends on the tumor. Many low-purity profiles also present a low-distance minima at 100% purity.
As the ruler moves, the gray mismatch bands show where the inferred major- or minor-allele copy numbers are farthest from integers. The colored fitted overlays project the integer copy numbers back into LogR and BAF space. A mismatch between these overlays and the black observed segment means shows what the integer-fit distance does not capture.
Fitting model¶
The original paper denotes the aberrant cell fraction by rho and tumor
ploidy by psi_t. It uses psi = 2 * (1 - rho) + rho * psi_t for the average
ploidy of the mixed sample. For compactness, the GenomeSpy specification calls
the tumor-ploidy parameter psi; thus, its psi corresponds to the paper's
psi_t.
The global gamma slider controls how strongly compressed LogR values are
expanded before copy numbers are calculated. Changing it recomputes the entire
sunrise plot and shows how this choice affects the purity/ploidy grid.
For every segment and candidate pair (rho, psi), the spec converts the
observed logRMean and bafMean into raw major- and minor-allele copy-number
estimates. These are Eqs. S7 and S8 in the
ASCAT Supporting Information,
which invert the LogR and BAF mixture model in Eqs. 1 and 2 of the paper:
aRaw = (rho - 1 + 2^(logRMean / gamma) * (1 - bafMean) * (2 * (1 - rho) + rho * psi)) / rhobRaw = (rho - 1 + 2^(logRMean / gamma) * bafMean * (2 * (1 - rho) + rho * psi)) / rho
Each estimate is rounded to a nonnegative integer. The input BAF values are
mirrored into [0, 0.5], which guarantees that bRaw is the minor-allele
estimate. Each segment is weighted by its number of germline-heterozygous
probes. When balanced-segment downweighting is enabled, segments with
bafMean = 0.5 receive 5% of that weight, matching ASCAT R v3.2.0:
weight = nProbes * (bafMean == 0.5 ? 0.05 : 1)error = weight * (bRaw - max(round(bRaw), 0))^2meanRoundingError = sum(error) / sum(weight)score = 100 - 100 * meanRoundingError / 0.25
The balanced-segment checkbox exposes this weighting choice interactively. It is especially important for low-purity samples, where many BAF estimates are too close to 0.5 to distinguish from balanced segments.
Higher scores indicate a better integer fit. The error has no upper limit, so strongly negative raw copy-number estimates can produce scores below zero.
By default, the visualization follows ASCAT R v3.2.0 and scores only the
minor-allele estimate (bRaw). The "Include both alleles in fit" toggle adds
the major-allele error. With balanced-segment weighting enabled, this matches
paper Eq. 3 apart from a constant scaling that does not change the locations of
the minima.
The visualization does not select an optimum: rho and psi come directly from
the interactive ruler.
The colored overlays in the lower tracks show what the rounded copy-number
states would look like in LogR and BAF space. The rounding follows Eqs. S10 and
S11, while the fitted LogR and BAF values use Eqs. S12 and S13. gamma is
applied in both directions so that the calculations remain consistent.
Sampled grid and exact selection¶
The sunrise heatmap samples rho from 0.10 to 1.05 in steps of 0.01 and
psi from 1.00 to 6.00 in steps of 0.05, matching ASCAT R v3.2.0's
default calculated distance matrix. GenomeSpy builds the grid by combining
every purity/ploidy pair with every segment, calculating the errors, and
summarizing the result for each pair.
The ruler does not snap to the sampled grid. It can select fractional values between cells, and the score and fitted tracks are calculated directly from those exact values rather than from the nearest heatmap cell.
GenomeSpy features¶
GenomeSpy calculates the analysis in the browser from a declarative
specification rather than loading a precomputed grid. The
cross,
formula, and
aggregate transforms combine every
candidate pair with the segments, evaluate the ASCAT-like equations, and
calculate the probe-weighted distances.
Global parameters control gamma and the fitting
options. Changing them recomputes the affected data. A two-dimensional
ruler parameter supplies rho
and psi to the linked views. It calculates the score and fitted profile at
exact values between sampled grid cells.
vconcat and
layer combine the grid, score,
copy-number, LogR, and BAF views, while the
locus scale aligns the genomic tracks.
No application-specific JavaScript is needed.