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

  1. parseCSV or Markdown table -> numeric DataFrame (blanks -> minimum value

    of the non-blank, non-NaN values in that column).

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

  3. pca_2dPCA onto 2 components, keeping the canonical axes (loadings) so

    every axis stays a readable linear combination of the criteria.

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

Author

Warith Harchaoui, https://www.linkedin.com/in/warith-harchaoui

class standpoint.PCAResult(names, features, scores, components, explained_variance_ratio, rotation_deg, reference, x_std, lower=frozenset({}))[source]

Bases: object

The 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, so scores are ready to plot.

Parameters:
  • names (list[str])

  • features (list[str])

  • scores (ndarray)

  • components (ndarray)

  • explained_variance_ratio (ndarray)

  • rotation_deg (float)

  • reference (str)

  • x_std (ndarray)

  • lower (frozenset[str])

names

Option (row) labels, in input order.

Type:

list[str]

features

Criterion (column) names, in input order.

Type:

list[str]

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:

float

reference

Name of the option placed top-right.

Type:

str

x_std

(n, p) normalized feature matrix that was fed to the PCA.

Type:

np.ndarray

lower

Criteria where lower is better (their sign was flipped before the PCA).

Type:

frozenset[str]

components: ndarray
coords()[source]

Oriented (axis-1, axis-2) coordinates, one row per option.

Return type:

DataFrame

explained_variance_ratio: ndarray
features: list[str]
loadings()[source]

Criterion weights per oriented axis, as a features x (axis-1, axis-2) frame.

Return type:

DataFrame

lower: frozenset[str] = frozenset({})
names: list[str]
reference: str
rotation_deg: float
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: object

Result of positioning(): the map plus everything computed for it.

Parameters:
property axes: dict[str, str]

‘Cost ↔ Innovation’, ‘y’: …}.

Type:

The two axis names, e.g. {‘x’

axis_names: list[str]
colors: list[str]
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.

Parameters:
  • outdir (str)

  • stem (str | None)

  • model (str | None)

Return type:

list[str]

figure(stem)[source]

Render the map to <stem>.png and <stem>.svg; returns the paths.

Parameters:

stem (str)

Return type:

list[str]

property loadings: DataFrame

Axis loadings (criterion weights) per axis.

noun_plural: str = 'Approaches'
noun_singular: str = 'Approach'
poles: list[str]
result: PCAResult
property role_of: dict[str, str]

Map each option name to its role (best / worst / … / competitor).

roles: list[str]
title: str = 'Approaches in the Quadrant'
to_markdown(model=None)[source]

The written interpretation as Markdown.

Parameters:

model (str | None)

Return type:

str

to_svg()[source]

The self-contained, interactive SVG document for the map.

Return type:

str

to_yaml()[source]

All coordinates + coefficients as YAML.

Return type:

str

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.

Parameters:
Return type:

str

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.

Parameters:
  • df (DataFrame)

  • reference (int | str)

  • soften_reference (float)

  • lower_is_better (list[str] | None)

Return type:

PCAResult

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 by ROLE_ORDER (best beats worst beats the two champions).

Return type:

list[str]

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

Parameters:
Return type:

list[str]

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.

Parameters:

texts (list[str])

Return type:

str

standpoint.engine()[source]

Return the resolved LLM/VLM engine descriptor for Standpoint.

Thin cache over best_engine_ai_helper.ensure(): loads standpoint/llm.engine.yaml (backend + per-kind model for this machine), or resolves it from the committed standpoint/llm.brief.yaml on 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:

dict

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.

Parameters:
Return type:

list[str]

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.

Parameters:
Return type:

list[str]

standpoint.i18n(lang='en')[source]

Prompt templates for lang (falls back to English), loaded from i18n.yaml.

Parameters:

lang (str)

Return type:

dict

standpoint.main(argv=None)[source]

argparse entry point (console command standpoint).

Parameters:

argv (list[str] | None)

Return type:

None

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.

Parameters:

svg (str)

Return type:

bytes

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")
Parameters:
  • data (DataFrame | str)

  • reference (int | str)

  • top (str | None)

  • right (str | None)

  • lower_is_better (list[str] | None)

  • model (str | None)

  • lang (str | None)

Return type:

Positioning

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.

Parameters:
Return type:

list[str]

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.

Parameters:
  • df (DataFrame)

  • lower_is_better (list[str] | None)

Return type:

tuple[DataFrame, frozenset[str]]

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.

Parameters:
Return type:

str

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.

Parameters:
  • table (str)

  • reference (str)

  • outdir (str)

  • stem (str | None)

  • top (str | None)

  • right (str | None)

  • lower (str)

  • model (str | None)

  • check (bool)

Return type:

list[str]

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.

  • options (list[str]) – The option (row) names to score.

  • criteria (list[str]) – The criterion (column) names to score each option on.

  • model (str | None) – Force a specific model tag; None uses 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:

dict[str, dict[str, int]]

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/:focus lift — 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.

Parameters:
Return type:

str

standpoint.validate_table(df)[source]

Raise a clear ValueError if 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.

Parameters:
Return type:

dict