standpoint package

Submodules

Module contents

Standpoint: where do you stand?

Explainable 2D Principal Component Analysis (PCA) positioning map from any comparison table: PCA finds the few directions along which the rows differ the most, so a table of many criteria collapses into a readable two-axis map.

Turn a table of approaches x criteria (CSV or Markdown, numeric ratings on any scale) into a competitive positioning map, plus 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', lang='en')[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)[source]

Write the full deliverable into outdir; returns the paths.

Parameters:
  • outdir (str)

  • stem (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]

lang: str = 'en'
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()[source]

The written analysis: the computed facts, then the model’s reading.

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(df, result, poles, axis_names, *, lang='en', title=None, model=None)[source]

The written analysis as a Markdown document: computed facts, then prose.

The third face of the deliverable, next to the figure and the YAML. The module docstring has promised it for a long time and export_all() stopped writing it, which left every consumer parsing a file that was never there.

The split is the point. Everything numeric here – the variance each axis preserves, the cumulative total, which cells were imputed – is computed in Python and written by this function. The model contributes interpretation only, under a prompt that forbids it from quoting a number. That keeps the document auditable, which is the one property a positioning map has to have.

The ## Axes section is deliberately machine-readable: each line reads **Horizontal: A <-> B** -- ~NN% of the variance, so a downstream generator can lift the pole names without a model and without this function’s prose.

Parameters:
  • df (pandas.DataFrame) – The rated table, as parsed.

  • result (PCAResult) – The oriented fit.

  • poles (list of str) – [left, right, bottom, top].

  • axis_names (list of str) – The two "A <-> B" axis names.

  • lang (str, optional) – Output language; falls back to English.

  • title (str, optional) – Document title. Defaults to the reference option’s name.

  • model (str, optional) – Override the resolved model tag.

Returns:

A Markdown document, always complete: when the model is unreachable the analysis section says so rather than being silently absent.

Return type:

str

standpoint.analysis_prose(df, result, poles, axis_names, *, lang='en', model=None)[source]

The local model’s interpretation of the map’s shape, as plain prose.

Returns an empty string when the model is unreachable, so a caller can degrade to the computed facts rather than failing: the figure and the YAML are the deliverable’s load-bearing halves, and neither needs a model.

The prompt is given the axis names, the loadings and every option’s position, and is told not to state a percentage, a coordinate or a rating. Those are computed in analysis_markdown() and printed around this prose. A model asked to narrate its own numbers will occasionally round one differently and contradict the file it sits in, and a positioning map that disagrees with itself is worse than one that says less.

Parameters:
  • df (pandas.DataFrame) – The rated table, as parsed.

  • result (PCAResult) – The oriented fit, for loadings and coordinates.

  • poles (list of str) – [left, right, bottom, top].

  • axis_names (list of str) – The two "A <-> B" axis names.

  • lang (str, optional) – Prompt language; falls back to English.

  • model (str, optional) – Override the resolved model tag.

Returns:

Two or three paragraphs, or "" when no model answered.

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.assess_layout(pos, *, crowding=0.045)[source]

Sanity-check a rendered positioning map from its own geometry, no model.

Returns the same verdict shape vlm_assess does – leader_top_right, readable, axis_labels_visible, notes – computed directly from the coordinates and pole labels instead of asking a vision model to look at a PNG. This is what the CLI’s --check runs.

Measuring the teacher settled the question: across the 1480 recorded vlm_assess examples in this project’s distillation corpus, readable came back true 1480 times and axis_labels_visible came back true 1480 times, while leader_top_right is a fact about a coordinate’s sign. A model was being asked to squint at a picture to recover numbers the caller already holds exactly.

Parameters:
  • pos (Positioning) – The computed map, as returned by positioning().

  • crowding (float, optional) – Two points closer than this fraction of the plot diagonal are reported as a label-overlap risk (default 0.045, i.e. under ~5% of the diagonal).

Returns:

{"leader_top_right": bool, "readable": bool, "axis_labels_visible": bool, "notes": str}, the notes localized to pos.lang.

Return type:

dict

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.check_analysis(prose, result, *, lang='en')[source]

Find claims in the prose that the computed positions contradict.

No model and no reasoning about meaning: find the direction words, find the option names, and compare each claim against the sign of that option’s coordinate. A sentence saying an option sits on the right when its first coordinate is negative is a mismatch, full stop.

This exists because the model does over-group even when handed every position in words. In one run it filed two left-hand options under “lower right” while placing three others correctly, and the only way to notice was to read the coordinates alongside the prose. A package whose promise is that a placement is auditable rather than hand-drawn has to be able to audit its own sentences.

Attribution is by position, not by sentence. One sentence routinely carries two claims about two different groups – “LiveKit shares the upper region, while Amazon Lex and Vapi are grouped in the lower right” – so applying every direction word in a sentence to every name in it reported nine problems where two were real. A direction word therefore claims only the names since the previous direction word, and a direction word with no names of its own inherits the previous one’s, which is what makes “lower-right” a single claim about a single group.

A span that names nobody is ignored: “those on the right prioritise modularity” is a statement about the axis, not about anybody’s position.

Claims stop at a contrastive conjunction, not only at a full stop. “A clusters upper left, while B and C sit lower right” is two claims, and reading the sentence whole let the first one’s forward window reach past while and claim B and C – eight false reports on French prose whose every claim was correct. Splitting there also bounds the inheritance rule above to the compound directions it was written for, so a later aside (“but those in the left-top corner differ”) no longer inherits the sentence’s subject. The break words are translated like every other word list here, since the shape occurs in each language’s prose.

Parameters:
  • prose (str) – The written analysis, as returned by analysis_prose().

  • result (PCAResult) – The oriented fit the prose is describing.

  • lang (str, optional) – Which language’s direction vocabulary to read.

Returns:

One message per contradicted claim, de-duplicated and sorted. Empty means every spatial statement in the prose agrees with the map.

Return type:

list of 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, noun_plural='Approaches', title=None, lang='en', model=None)[source]

Write the full deliverable for one table. Returns the paths written.

Three faces, not two: the figures (PNG + SVG), the written analysis (Markdown), and the YAML of coordinates + coefficients. The Markdown face was promised by this module’s docstring and had stopped being written, so anything downstream looking for it found nothing and had no way to tell a missing analysis from a failed run.

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.

Delegates to os_helper.Prompts, the loader shared across the suite. This used to transpose the file by hand — key-then-locale on disk, locale-then-key for callers — behind a module-level cache, and resolve the language with a plain .get(lang, …[“en”]). All three now live in os-helper, which also means fr-CA reaches the French prompts instead of silently getting English, and that a prompt whose translation dropped a {placeholder} is reported by problems() rather than raising at render time.

Parameters:

lang (str) – Language code, regional tags included. Anything the file does not carry falls back to English.

Returns:

Every template for lang, keyed by semantic name.

Return type:

dict

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

Called by the standpoint command line (standpoint.cli), and usable directly by any caller that wants the same printed summary. 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, lang=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). lang picks the prompt’s language from i18n.yaml (falls back to English when None, like i18n() itself) – there is no image text to auto-detect a language from, unlike axis_poles/noun_forms. 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 (in lang). Empty dict if the model or a rendered image is unavailable.

Parameters:
Return type:

dict