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 Vega-Lite figure. export_all writes PNG + SVG + Vega JSON + 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='qwen2.5vl:7b')[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='qwen2.5vl:7b', 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='qwen2.5vl:7b', 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.export_all(df, result, roles, poles, axis_names, colors, stem, model='qwen2.5vl:7b', noun_plural='Approaches', title=None)[source]
Write the full three-fold deliverable for one table: figures (PNG + SVG + Vega JSON), 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(spec)[source]
Render spec 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='qwen2.5vl:7b', 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_vega(), .to_markdown(), .to_yaml(), .export().>>> pos = positioning("examples/programming_languages.csv") >>> pos.export("out")
- standpoint.render_figures(spec, stem)[source]
Rasterize/vectorize a Vega-Lite spec to transparent and white PNG + 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='qwen2.5vl:7b', 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='qwen2.5vl:7b', 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:
- 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:
ConnectionError – The Ollama server is unreachable.
ollama.ResponseError – The model is not installed or errors.
- standpoint.to_vega(result, roles=None, poles=None, colors=None, noun_plural='Approaches', title=None, attributes=None)[source]
Build a self-contained Vega-Lite v5 spec (inline data) for the map.
Layers, bottom to top: a centred cross of axes through the origin (the neutral intersection), every approach coloured by its position (Apple-wheel HSV), the four pole words at the axis ends, and labels for the four corner extremes. No frame, spines, ticks, numeric scales, or arrows.
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, and no fully-empty column; otherwise PCA is undefined or degenerate.
- Parameters:
df (DataFrame)
- Return type:
None
- standpoint.vlm_assess(image, model='qwen2.5vl:7b')[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.