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 Vega-Lite figure. export_all writes PNG + SVG + Vega JSON + 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='qwen2.5vl:7b')[source]

Write the full three-fold deliverable into outdir; returns the paths.

Parameters:
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='qwen2.5vl:7b')[source]

The written interpretation as Markdown.

Parameters:

model (str)

Return type:

str

to_vega()[source]

The Vega-Lite spec for the map.

Return type:

dict

to_yaml()[source]

All coordinates + coefficients as YAML.

Return type:

str

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.

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

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

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

Parameters:

spec (dict)

Return type:

bytes

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

  • reference (int | str)

  • top (str | None)

  • right (str | None)

  • lower_is_better (list[str] | None)

  • model (str)

  • lang (str | None)

Return type:

Positioning

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.

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='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.

Parameters:
Return type:

list[str]

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:
  • 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) – Ollama model to query.

  • 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:
  • 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.

Parameters:
Return type:

dict

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

Parameters:
Return type:

dict