standpoint package
Submodules
Module contents
Standpoint: know where each option actually stands.
Explainable 2D PCA positioning map from any comparison table.
Turn a table of approaches x criteria (CSV or Markdown, numeric ratings on any scale) into a competitive positioning map, plus a written interpretation and a full dump of the coefficients: a three-fold deliverable from one input file.
Pipeline
- parseCSV or Markdown table -> numeric DataFrame (blanks -> minimum value
of the non-blank, non-NaN values in that column).
- preparenormalization (default = z-score standardization, i.e. correlation
PCA, because PCA is scale-sensitive and criteria carry different variances). Missing cells are imputed with the column minimum.
- pca_2dPCA onto 2 components, keeping the canonical axes (loadings) so
every axis stays a readable linear combination of the criteria.
- orientrigidly rotate the 2D scatter so the reference row (the first row by
default) leads in the TOP-RIGHT, and reposition an all-max reference to the best Pareto point; RECOMPUTE the canonical axes in the rotated frame (new_components = R(alpha) @ components).
Then: automatic roles by principled projection, distinct OKLCH colours by map position, local-LLM axis pole names from the loadings, and a de-cluttered, hand-authored, interactive SVG figure (no Vega, no external chart-rendering runtime). export_all writes PNG + SVG + a Markdown analysis + a YAML of coordinates and coefficients.
- class standpoint.PCAResult(names, features, scores, components, explained_variance_ratio, rotation_deg, reference, x_std, lower=frozenset({}))[source]
Bases:
objectThe oriented 2D PCA of one comparison table: the map’s raw geometry.
Produced by
analyze()and consumed by everything downstream (roles, pole naming, figure, YAML). It holds the two-component projection after the rotation that puts the reference option in the top-right, soscoresare ready to plot.- Parameters:
- scores
(n, 2)oriented coordinates, one row per option (axis-1, axis-2).- Type:
np.ndarray
- components
(2, p)oriented canonical axes, i.e. the per-criterion loadings.- Type:
np.ndarray
- explained_variance_ratio
Fraction of variance each of the two axes carries, from the PCA fit.
- Type:
np.ndarray
- rotation_deg
The rotation (degrees) applied to bring the reference onto the +45° diagonal.
- Type:
- x_std
(n, p)normalized feature matrix that was fed to the PCA.- Type:
np.ndarray
- components: ndarray
- explained_variance_ratio: ndarray
- loadings()[source]
Criterion weights per oriented axis, as a features x (axis-1, axis-2) frame.
- Return type:
DataFrame
- scores: ndarray
- x_std: ndarray
- class standpoint.Positioning(df, result, roles, poles, axis_names, colors, noun_singular='Approach', noun_plural='Approaches', title='Approaches in the Quadrant')[source]
Bases:
objectResult of positioning(): the map plus everything computed for it.
- Parameters:
- property coords: DataFrame
Oriented (axis-1, axis-2) coordinates per option.
- df: DataFrame
- export(outdir='.', stem=None, model=None)[source]
Write the full three-fold deliverable into outdir; returns the paths.
- property loadings: DataFrame
Axis loadings (criterion weights) per axis.
- standpoint.analysis_markdown(result, roles, poles, model=None, lang=None)[source]
A thoughtful, precise interpretation of the map as Markdown.
Combines data-derived facts (axis loadings, variance, roles, coordinates) with an LLM-written narrative in the table’s own language (auto-detected). Falls back to a templated narrative when the model is unavailable.
- standpoint.analyze(df, reference=0, soften_reference=1.0, lower_is_better=None)[source]
Run the full pipeline: prepare -> PCA(2) -> rotate reference to top-right.
The reference row is rotated onto the +45 deg diagonal (equal, positive coordinates = top-right corner). The canonical axes are then recomputed in the rotated frame so their loadings describe the displayed axes.
soften_reference repositions an all-max reference (a straight-5-stars first row otherwise lands as a far outlier) to the best Pareto point: max x and max y of the competitors, times this factor (default 1.0 = exactly best-in-class on each axis, so it weakly dominates everyone without being an outlier). Set to 0 or None to keep the raw PCA position.
lower_is_better names criteria where a lower value is better (price, latency). They are negated before the PCA so the whole space is uniformly higher-is-better; header markers like
Price (↓)are picked up automatically too.
- standpoint.assign_roles(result, top=None, right=None)[source]
Label four options by domain-agnostic map geometry; the rest are competitors.
Every pick is read straight off the oriented coordinates, so it means the same thing for any table (no per-domain keyword list):
best the reference, sitting at the top-right corner by construction; worst the weakest overall: the minimum projection onto the top-right hero
diagonal (equivalently the smallest axis-1 + axis-2);
- top the challenger reaching furthest up the vertical axis, the peer that
most defines the map’s top pole (the leader excluded);
- right the challenger reaching furthest along the horizontal axis, the peer
that most defines the right pole (leader and top champion excluded).
- Parameters:
result (PCAResult) – The oriented positioning (scores and reference).
top (str, optional) – Force a specific option into the top-pole / right-pole highlight by exact name, bypassing the geometric pick.
right (str, optional) – Force a specific option into the top-pole / right-pole highlight by exact name, bypassing the geometric pick.
- Returns:
One role per option, aligned with
result.names; collisions resolve byROLE_ORDER(best beats worst beats the two champions).- Return type:
- standpoint.axis_poles(result, model=None, lang=None)[source]
Four distinct pole labels [left, right, bottom, top] for the two axes.
Each PCA axis is a weighted mix of the criteria. The local LLM names each pole (1-3 words) for what the approaches at that end are collectively strongest at, from the signed loadings and the original column names, in the table’s own language (auto-detected from the column names; see i18n.yaml). Always uses the local model; loading-derived words only serve as the per-label robustness fallback when the model returns a bad label (see finalize_poles).
- standpoint.detect_language(texts)[source]
Detect the language (one of SUPPORTED_LANGS) from text; default English.
Used on the table’s column names so the pole labels and written analysis come out in the table’s own language.
- standpoint.engine()[source]
Return the resolved LLM/VLM engine descriptor for Standpoint.
Thin cache over
best_engine_ai_helper.ensure(): loadsstandpoint/llm.engine.yaml(backend + per-kind model for this machine), or resolves it from the committedstandpoint/llm.brief.yamlon first use and writes it. Raises loudly if the brief is missing. Nothing is computed at import time; the first LLM call triggers resolution.- Return type:
- standpoint.export_all(df, result, roles, poles, axis_names, colors, stem, model=None, noun_plural='Approaches', title=None)[source]
Write the full three-fold deliverable for one table: figures (PNG + SVG), a Markdown interpretation, and a YAML of coordinates + coefficients. Returns the list of paths written.
- standpoint.gradient_colors(result, roles)[source]
Distinct, clean per-approach colours.
Competitors are placed at EVENLY spaced hues around the OKLCH circle in order of their direction on the map: balanced hues, every colour vivid (fixed chroma, never a muddy centre), all distinct. Lightness gets a small per-name spread for extra separation. Named roles keep their fixed identity hue.
- standpoint.i18n(lang='en')[source]
Prompt templates for lang (falls back to English), loaded from i18n.yaml.
- standpoint.parse_table(source)[source]
Parse a markdown/CSV table (path or raw string) into a numeric DataFrame.
The first column becomes the row index (approach names); every other cell is parsed as a number (int or float); blanks become NaN.
- Parameters:
source (str)
- Return type:
DataFrame
- standpoint.png_on_white(svg)[source]
Render svg to PNG bytes on an opaque white background.
The exported figures are transparent, but the vision self-check sends the image to a model whose backend flattens transparency onto a dark canvas, which would hide the near-black labels and make the check misfire. White is the figure’s intended reading surface, so the check runs against a white-composited copy rather than the transparent file on disk.
- standpoint.positioning(data, reference=0, top=None, right=None, lower_is_better=None, model=None, lang=None)[source]
Position options from a table in one call.
data is a pandas DataFrame (options × numeric criteria) or a path / raw string of a CSV or Markdown table. lower_is_better names criteria where a lower value is better (also picked up from
(↓)header markers). top / right force a named option into the top-pole / right-pole highlight (see assign_roles). lang forces the output language (one of SUPPORTED_LANGS); left None it is detected from the column names. Returns a Positioning with .coords, .loadings, .axes, .to_svg(), .to_markdown(), .to_yaml(), .export().>>> pos = positioning("examples/programming_languages.csv") >>> pos.export("out")
- standpoint.render_figures(svg, stem)[source]
Write an SVG figure (and its raster companion) as transparent and white pairs.
svg is the transparent figure from
to_svg(). Writes four files: the transparent <stem>.png / <stem>.svg (the default, for dropping onto any coloured page) and a white-background <stem>.white.png / <stem>.white.svg (for dark surfaces (e.g. GitHub dark mode) where the map’s near-black labels would otherwise vanish on a transparent background). Returns the four paths in that order.
- standpoint.resolve_polarity(df, lower_is_better=None)[source]
Detect lower-is-better criteria and return a clean-named copy + their names.
A criterion is lower-is-better if its header carries a marker (
Price (↓),Latency (lower)) or is named in lower_is_better. Markers are stripped from the column name; the returned set uses the cleaned names.
- standpoint.results_yaml(df, result, roles, poles, axis_names, colors)[source]
Everything about the fit as YAML: metadata, axis loadings, and per-approach coordinates, roles, colours, and original attribute values.
- standpoint.run(table, reference='0', outdir='out', stem=None, top=None, right=None, lower='', model=None, check=False)[source]
Shared CLI core: build the positioning, print a summary, write the files.
Used by both the argparse (main) and click (main_click) entry points. top / right force a named option into the top-pole / right-pole highlight. Returns the list of written paths.
- standpoint.suggest_ratings(noun, options, criteria, model=None, lang=None)[source]
Ask the local model to fill a ratings matrix from the option / criterion names.
This backs the GUI’s “Flemme” (lazy) auto-fill: the user typed only the row (option) and column (criterion) names, and the model scores every option on every criterion on a 1 to 5 scale from its own knowledge. It is offline by design (the model’s training knowledge, not a live web search), so nothing leaves the machine.
- Parameters:
noun (str) – The first-column word (e.g. “Programming Language”), naming what a row is.
criteria (list[str]) – The criterion (column) names to score each option on.
model (str | None) – Force a specific model tag;
Noneuses the one resolved in the engine.lang (str | None) – Output language for the prompt; detected from the names when None.
- Returns:
{option: {criterion: rating}}with every rating clamped to 1..5. Missing pairs default to 3 (neutral) so the caller always gets a complete matrix.- Return type:
- Raises:
RuntimeError – The local model backend is unreachable, or the model errors.
- standpoint.to_svg(result, roles=None, poles=None, colors=None, noun_plural='Approaches', title=None, attributes=None)[source]
Build a complete, self-contained, interactive SVG document for the map.
Elements, bottom to top: a centred dashed cross through the origin (the neutral intersection), every approach coloured by its position (Apple-wheel OKLCH), the four pole words at the axis ends, and labels for the four corner extremes. No frame, spines, ticks, numeric scales, or arrows. Every dot carries a native
<title>tooltip (the criterion values, or role + coordinates as a fallback) and a pure-CSS:hover/:focuslift — no JavaScript, no external renderer. The background is transparent;render_figures()derives an opaque-white companion for dark surfaces.title is the fully-localized figure title (e.g. “Voitures dans le quadrant”); when omitted it defaults to the English “<plural> in the Quadrant” so direct callers still get a sensible heading.
- standpoint.validate_table(df)[source]
Raise a clear
ValueErrorif the table can’t be positioned.Needs at least 2 options (rows) and 2 numeric criteria (columns) with some variation, no fully-empty column, and no duplicate: two options with identical ratings would land on the same point, and two identical criteria would count the same evidence twice and skew the axes. Otherwise PCA is undefined, degenerate, or misleading.
- Parameters:
df (DataFrame)
- Return type:
None
- standpoint.vlm_assess(image, model=None)[source]
Ask the qwen vision-LLM to sanity-check a rendered positioning map.
image is a PNG path or raw PNG bytes (bytes let the caller assess a white-composited render without touching the transparent file on disk). Returns a verdict dict: whether the red leader dot sits top-right, whether the point labels are readable, and whether the four axis pole labels are visible, plus free-text notes. Empty dict if the model or a rendered image is unavailable.