spacr.qt.widgets.pca_view¶
The PCA surface: a scree plot, a scores plot, and a loadings biplot.
The statistics are in spacr.qt.widgets.pca_model and there is none in
here. This module is the three pictures and the controls, and its one real
design decision is that the scores plot is a Graph Builder.
Why the scores plot is a GraphCanvas¶
A PCA scores plot is a scatter of two continuous columns that a user wants to
colour by gene, facet by plateID, and brush to pull a cluster into
Annotate. That is the Graph Builder, exactly, with the two columns computed
rather than measured. So PCAResult.scores_frame appends PC1…PCk to the
source frame and PCAScoresCanvas is a
GraphCanvas subclass that draws arrows
on top.
Everything that took work in the Graph Builder therefore already works here and
cannot drift out of step with it: the shared-scale facet grid, the fixed
categorical colour order, the large-data policy and its notice, the
selection-dims-never-hides rule, and — the point of the exercise — the brush.
Because the scores frame carries the object key columns, a rectangle dragged
around a cluster in PC space publishes a real
Selection, and the UMAP, the plate map and the crop
grid highlight the same cells. Reimplementing a scatter here would have meant
reimplementing all of that and getting the brush subtly wrong.
The biplot, and what its arrows do and do not mean¶
An arrow is the feature’s Pearson correlation with the two plotted
components — PCAResult.correlations, a quantity with a meaning
of its own, rather than a unit-norm loading whose scale is an artefact of the
normalisation. So:
direction is where more of that feature lies, and is exact;
relative length is how much of the feature this plane shows: a feature perfectly captured by the plane reaches the dashed unit circle, one pointing out of the plane is short. Short means “not visible here”, never “unimportant”;
absolute length in data units is meaningless. Every arrow is multiplied by one shared constant chosen to fill the panel, because scores and correlations have no common unit. The circle is scaled by the same constant, which is what keeps the comparison honest — it is the ruler.
Only the DEFAULT_ARROWS best-represented features are drawn. Four
hundred arrows is an ink blot, and the ones left out are the ones pointing
somewhere the reader is not looking.
Classes¶
Which columns go into the decomposition. |
|
Feature picker, options, scree, scores + biplot, and the report. |
|
A |
|
Explained variance per component, with the cumulative line over it. |
Functions¶
|
The one constant every arrow and the unit circle are multiplied by. |
Module Contents¶
- class spacr.qt.widgets.pca_view.FeaturePicker(parent=None)[source]¶
Bases:
PySide6.QtWidgets.QWidgetWhich columns go into the decomposition.
A tick list rather than drop zones: PCA takes tens to hundreds of features at once, and dragging four hundred columns one at a time is not a gesture anybody makes twice. The default ticks every continuous column, which is what
candidate_features()offers and what a user exploring a new table wants first.- Parameters:
parent – parent widget.
Build the feature list with its search box.
- Parameters:
parent – parent widget.
- available() Tuple[str, ...][source]¶
Every feature the picker is offering.
- Returns:
the feature names, in list order.
- selected() Tuple[str, ...][source]¶
The ticked features, in the offered order (not the click order).
- set_frame(frame: pandas.DataFrame | None) None[source]¶
Offer
frame’s continuous columns, all ticked.- Parameters:
frame – the table whose continuous columns are offered, or None to offer none.
- class spacr.qt.widgets.pca_view.PCAPanel(parent=None, *, link=None, source: str = 'pca', threaded: bool = False)[source]¶
Bases:
PySide6.QtWidgets.QWidgetFeature picker, options, scree, scores + biplot, and the report.
- Parameters:
link – a private
LinkedSelectionfor tests;Nonejoins the process-wide one, which is what makes brushing a cluster here highlight it everywhere else.threaded – run the decomposition on a worker thread. Defaults to False, and the default is the interesting part – see
recompute().PCAScreenpasses its ownthreadedthrough, so the application gets the threaded panel and a panel built directly keeps returning its result from the call.parent – parent widget; ownership only.
source – this view’s name on the link, stamped onto everything it publishes so a selection can be attributed and a view does not react to its own brushing. Two panels sharing a link MUST NOT share this.
Build the picker, the scree plot and the scores canvas.
- Parameters:
parent – parent widget.
link – the shared selection to join, if any.
source – the table to decompose.
threaded – whether the fit runs on a worker.
- closeEvent(event)[source]¶
Abandon an in-flight fit rather than let it outlive the panel.
The worker holds the table and delivers into widgets that are being destroyed, so the runner is shut down before the canvas it would have drawn into is closed.
- Parameters:
event – the close event, passed to the base class after the fit runner is shut down and the canvas closed.
- recompute() spacr.qt.widgets.pca_model.PCAResult | None[source]¶
Decompose and redraw. Refusals become a message, never a traceback.
THE CONTRACT, and how it was resolved. This method returned its
PCAResultsynchronously, and the sklearn fit behind it is 1.63 s on a 200 000-row x 48-column table – measured, and recorded asPCA_STALL_BUDGET_Sintests/qt/test_gui_responsiveness.pyrather than hidden, because that is a 1.63 s frozen window on every option change and every filter change.The fit now runs on a worker, and this returns
Nonewhile it does. Rather than pretend that is the same thing, the panel takes athreadedflag:threaded=False(the default, and what a directly-constructed panel gets) runs the fit inline throughJobRunner’s unthreaded path and returns thePCAResultexactly as before.threaded=True– whatPCAScreenpasses, so the application gets it – dispatches and returnsNone. The result arrives at_on_computed(), which is where the drawing was already done, andcomputedis emitted from there as it always was.
A host that wants the result asynchronously has always had
computed; nothing that listens to it changed. A caller that reads the return value gets the old behaviour by not asking for threading, which is honest about the fact that a value cannot be returned before it has been computed.- Returns:
the decomposition, or
Nonewhen it was refused or is still running.
- set_frame(frame: pandas.DataFrame | None, *, compute: bool = True) None[source]¶
Point the panel at a table and (by default) decompose it.
- Parameters:
frame – the table to decompose, or None; its columns also fill the colour-by list.
- spec() spacr.qt.widgets.pca_model.PCASpec[source]¶
The spec the controls currently describe.
- property result: spacr.qt.widgets.pca_model.PCAResult | None[source]¶
The decomposition this panel is showing, if any.
- Returns:
the PCA result, or None before one has been computed.
- property scores_frame: pandas.DataFrame | None[source]¶
The frame the scores plot is drawn from — every original column plus
PC1…PCk. What “export the scores” writes.
- class spacr.qt.widgets.pca_view.PCAScoresCanvas(parent=None, *, link=None, source: str = 'pca')[source]¶
Bases:
spacr.qt.widgets.graph_builder.GraphCanvasA
GraphCanvasthat also draws the loadings.Everything the base class does is untouched — the facet grid, the shared scales, the large-data policy, the brush and the linked selection. The subclass adds one thing after each render: the feature arrows for whatever pair of components happens to be on x and y, read from the spec rather than configured separately, so the arrows cannot end up describing a different plane from the points.
- Parameters:
parent – parent widget.
link – the
LinkedSelectionthis view joins, so selecting here selects in every other view on it.Nonejoins the shared one; pass a private one in a test so the selection does not reach the rest of the application.source – this view’s name on that link, stamped onto everything it publishes – which is how a view knows not to answer its own selection.
Build the scores plot and link it to the shared selection.
- Parameters:
parent – parent widget.
link – the shared selection to join, if any.
source – the table being decomposed.
- plane() Tuple[int, int] | None[source]¶
(kx, ky)when both axes carry a component, elseNone.Read from the spec, so dragging
PC3onto Y moves the arrows with the points and draggingareaonto Y removes them — a biplot of a component against a raw measurement is not a biplot.
- set_biplot(on: bool, *, count: int | None = None, render: bool = True) None[source]¶
Turn the arrows on or off, and optionally set how many.
render=Falseis for a caller about to change the spec anyway: the spec change redraws, and doing it twice for one user action is a visible flicker on a large scatter.- Parameters:
on – whether to draw the loading arrows; converted to bool.
- set_result(result: spacr.qt.widgets.pca_model.PCAResult | None, frame: pandas.DataFrame | None) None[source]¶
Point the canvas at a decomposition and its scores frame.
Both together, always: a result and a frame that do not match would put arrows from one PCA over the points of another, which is a picture that looks entirely reasonable and is wrong.
- Parameters:
result – the decomposition whose loadings are drawn, or None.
frame – the scores frame from the same decomposition, or None.
- property arrow_scale: float[source]¶
The display constant the last render used; 0.0 when no arrows were drawn. Public so a test — or a caption — can state the ruler.
- property result: spacr.qt.widgets.pca_model.PCAResult | None[source]¶
The decomposition being plotted, if any.
- Returns:
the PCA result, or None before one has been computed.
- class spacr.qt.widgets.pca_view.ScreePlot(parent=None)[source]¶
Bases:
PySide6.QtWidgets.QWidgetExplained variance per component, with the cumulative line over it.
Both, always. The bars are what people look at for the elbow; the cumulative line is what stops “PC1 and PC2 look big” from being mistaken for “PC1 and PC2 are most of it” when they are 9% and 7%.
Clicking a bar emits
component_picked, so the scree plot is the control that chooses what the scores plot draws rather than a decoration beside it.- Parameters:
parent – parent widget.
Build the variance-explained plot.
- Parameters:
parent – parent widget.
- spacr.qt.widgets.pca_view.arrow_scale(result: spacr.qt.widgets.pca_model.PCAResult, kx: int, ky: int, x_limits: Tuple[float, float], y_limits: Tuple[float, float], *, count: int = DEFAULT_ARROWS, fill: float = ARROW_FILL) float[source]¶
The one constant every arrow and the unit circle are multiplied by.
Scores are in standardised-feature units and correlations are in
[-1, 1]; there is no conversion between them, so a biplot has to pick a display scale. It is picked once, from the drawn axes, so that the longest arrow spansfillof the shorter half-range — and applied to the circle too, so the reader has a ruler on screen rather than an assurance.Returns 0.0 when there is nothing to scale (no finite limits, or every correlation zero), which callers read as “draw no arrows”.
- Parameters:
result – the decomposition whose feature correlations are drawn.
kx – zero-based index of the component on the x axis.
ky – zero-based index of the component on the y axis.
x_limits –
(low, high)of the drawn x axis.y_limits –
(low, high)of the drawn y axis.