Data Analyse
Take a dataset — a clean(ish) .xlsx/CSV, a pasted table, or the output of another
toolkit skill — and deliver an insight brief: the headline findings, the key metrics
for that type of data, and honest caveats. The design is intent-first (ask what
question the data should answer before computing anything) and compute-then-interpret
(every quoted number comes from the deterministic local engine; the narrative interprets,
it never generates figures).
Self-sufficient & local engine. All computation runs on your machine via
scripts/analyse.py (+ the shared toolkit engine) — no network calls. Note the AI agent
driving the skill does send whatever it reads into its context to your AI provider;
"never leaves" is not claimed. See ../../PRINCIPLES.md#data-handling--pii-policy.
Workflow
0 — Intent (ask up front)
Before computing anything, ask (a short AskUserQuestion):
- What question should this data answer? ("are sales growing?", "where's the risk
in receivables?", "which segment matters?") — or is it an open "tell me what you see"?
- Who's it for and what decision does it feed? — a partner one-pager reads
differently from a working analysis.
- Anything known already? — targets, prior-period figures, or a hypothesis to test.
An open-ended "just analyse it" is fine — then the playbook (step 3) drives the metric
selection, and the brief says so.
Done when: you have the question (or "open-ended"), the audience/decision, and any prior figures/hypotheses — or the user has explicitly said "just analyse it". A one-line restatement to the user confirms alignment.
1 — Ingest
Same shared engine as the other skills (../../scripts/):
import sys, pathlib
sys.path.insert(0, str(pathlib.Path("../../scripts").resolve()))
import ingest
header_rows, note = ingest.read_any(path) # xlsx/csv/pdf/docx/msg; read_paste for pasted tables
Run from the skill directory (skills/data-analyse/). The ../../scripts path resolves
to the toolkit-root scripts/ where ingest.py and dataclean.py live. The analysis
engine (analyse.py) is in this skill's own scripts/ subdirectory.
Multi-tab workbooks: read_any raises SheetSelectionRequired if several tabs hold data —
list the sheets and ask, don't guess.
For files with 10k+ rows, use ingest.read_large instead of read_any to avoid OOM. See
references/large-file-patterns.md for vectorised operation guidance.
Done when: you have (header_rows, note) from read_any (or read_large for 10k+ rows). If SheetSelectionRequired was raised, the user has chosen a sheet. note has been read for source-quality flags.
2 — Profile & quality gate
dataclean.profile_table(header, rows) / score_quality(...) — column types, missing %,
duplicates. If the data is too messy to analyse honestly (columns that won't parse,
heavy duplication, mixed junk), stop and route to data-tidy first — analysing dirty
data produces confident-looking nonsense. A little noise is fine: the engine counts every
skipped/unparseable cell and the brief must disclose those counts.
Currency gate: analyse.currency_mix(col) — pass the currency-bearing column: the
amount column when codes ride inside the amounts (S$100, £1,000), or the separate
ISO-code column when the layout is two columns (Amount + Currency of GBP/USD) — the
common export shape. If it reports more than one code, never sum the amounts as-is
(100 USD ≠ 100 SGD). Split by currency (breakdown(by=<currency col>)) or ask.
3 — Propose the analysis plan (confirm before computing)
analyse.suggest_playbook(header, rows) maps columns to roles (dates, amounts,
categories, ids) and suggests analyses. Combine that with the intent and the data-type
playbook (references/playbooks.md — transactions, receivables/ageing, pipeline,
survey/categorical, task/ops list, general ledger, inventory, spend/AP, time series,
cross-domain, generic) into a short plan:
"I'll look at: monthly trend of Amount, breakdown by Customer with concentration,
outliers, and ageing from Due date as of 12 Jul 2026 — sound right?" Show it; confirm.
Two datasets provided? If the user hands over two files that share a dimension
(sales vs competitor prices, actual vs budget), that's the cross-domain playbook —
join_on to relate them on the shared key (report coverage), then compare_series to read
position and co-movement. It relates, it does not match line-by-line — reconciling two
sets to find breaks is data-reconcile. Nail the join key and grain before computing.
4 — Compute (deterministic, engine only)
All metrics come from scripts/analyse.py — exact Decimal, dayfirst dates, reusing the
shared parsers:
| Function |
What it computes |
numeric_summary(col) |
n, missing, skipped, total, mean, median, quartiles, min/max, negatives |
outliers_iqr(col) |
Tukey fences + the outlying values (exact counts) |
breakdown(header, rows, by, value=) |
groups sorted largest-first with shares, top-1/top-3 share, groups-to-80% |
period_series(header, rows, date_col, value=, grain=) |
month/quarter/year totals with gap periods filled as zero, deltas, % change, YoY |
ageing(header, rows, date_col, as_of=, value=) |
0–30/31–60/61–90/90+ buckets (configurable); future & unparsed bucketed visibly |
join_on(l_header, l_rows, r_header, r_rows, on=) |
two-dataset join on a shared key (case/space-folded); returns the joined table + a matched/left-only/right-only coverage report |
compare_series(a, b, a_label=, b_label=) |
relate two ordered series (gap / ratio / % diff per key, Pearson correlation, ±1 lead/lag) — for cross-domain & actual-vs-budget |
concentration(values, top_n=4) |
HHI (0–10000 antitrust scale), top-N share, groups-to-80%, classification (fragmented/moderate/concentrated/highly concentrated) — for revenue/customer concentration |
pivot(header, rows, rows_col, cols_col, value=, aggfunc=) |
2D cross-tab matrix (rows × columns) with sum/count/mean aggregation + row/column grand totals |
distribution(values) |
skewness + excess kurtosis (Fisher-Pearson, Excel-compatible) + classification (symmetric/moderately skewed/highly skewed/heavy-tailed) |
trend(series) |
linear regression slope + R² + direction (rising/falling/weakly/flat) on an ordered (key, value) series — descriptive, not a forecast |
percentile(values, q) |
arbitrary quantile(s) with linear interpolation (Excel PERCENTILE.INC-compatible); single float → {value, n, skipped}, list → {q: value} — p90/p95/p99 for VaR/latency |
cohort(header, rows, id_col, date_col, value=, grain=) |
retention matrix: group by first-active period, track over subsequent periods. Count mode: retention = active/size (0–1). Value mode: matrix = value sums, retention still entity-count-based, value_matrix separate. All rows padded rectangular. |
correlation_matrix(header, rows, columns) |
pairwise Pearson across N numeric columns (symmetric matrix). Row-wise alignment — only rows where BOTH cells parse are paired (junk in one column doesn't shift others). Association, not cause. |
rolling(series, window, func=) |
trailing-window aggregate (mean/sum/median) on an ordered series — smoothing, pairs with period_series. None values in a window are skipped. |
gini(values) |
Gini coefficient (0=equal, 1=concentrated) + classification — inequality of distribution, complements HHI |
seasonality(header, rows, date_col, value=, grain=) |
average by month-of-year (1–12) or quarter (1–4) + seasonal index. Overall average = mean of seasons WITH data (not grand/12). |
filter_rows(header, rows, filters) |
declarative row filter ({"col", "op", "value"/"values"/"lo"+"hi"}) — == != > >= < <= in not_in between not_between contains is_empty not_empty, ANDed. Numbers via parse_number, dates via parse_date, strings case-insensitive; between is inclusive of both ends. A text cell against a numeric threshold is incomparable — dropped and counted, never string-compared. Returns (rows, report) with n_in/n_out/n_dropped and per-filter removed counts |
currency_mix(col) / numbers(col) |
currency codes present / parsed Decimals + skipped count |
fmt / pct / render_md(...) |
house-style formatting + markdown tables for the brief |
write_metrics_xlsx(sections, path) |
optional metrics workbook (one sheet per analysis) |
Run computations in a script (not mental arithmetic) so the analysis is reproducible;
keep the script in the working folder as the audit trail. The engine ships tested — don't
re-run its --self-test or envcheck.py as part of a normal analysis (that's setup
overhead the user pays every run); reach for them only when an import fails or the
environment is genuinely uncertain.
5 — Write the insight brief
ALWAYS this structure (markdown; British English, dates DD MMM YYYY, currency with code):
# [Dataset] — insight brief (as of [date], n = [rows])
## Headline 3–5 findings, most consequential first, each with its number
## Key metrics the tables from the engine (render_md)
## Notable outliers, concentration, anomalies — with the specific rows/values
## Caveats & quality skipped cells, missing %, gap periods, currency notes, what this
data CANNOT answer
[footer: draft for review — descriptive analysis, not advice]
Deliver the brief as a .md (plus the optional metrics .xlsx). Keep it to one page of
reading unless asked for depth.
Done when: the brief follows the exact structure (Headline / Key metrics / Notable / Caveats & quality / footer), every number in it came from the engine, and the .md file is written. If a metrics .xlsx was requested, that too is written.
6 — Offer the next step
- Visual one-pager wanted? → hand the computed series/breakdowns to data-visualise
(its
rows_from_xlsx reads the metrics workbook directly).
- Numbers look wrong vs another source? → that's data-reconcile, not this skill.
Done when: the next step is offered to the user (visualise, reconcile, or "done") and the brief has been delivered. The skill does not start the next step without the user's go-ahead.
Insight discipline (what makes the brief trustworthy)
- Every number is computed. If a figure appears in the brief, it came out of the
engine or is an arithmetic step shown in the brief. No free-form estimates.
- Observation ≠ interpretation. "March revenue fell 40% MoM" is an observation;
"likely the contract gap" is an interpretation — label it as a possible explanation and
say what would confirm it. Never present correlation as cause.
- Calibrated language. "Verified" vs "likely" vs "couldn't confirm" (PRINCIPLES.md #4).
A trend over 3 periods is "early"; over 2 it is not a trend.
- Disclose the denominators. Shares, averages and trends state n and what was
excluded (skipped cells, unparsed dates, filled gap periods).
- Ratios and multiples are computed too. "20× the typical ticket" is only meaningful
with the comparator named — compute it from engine figures and say what the base is
(median, mean, next-largest). An eyeballed multiple is a free-form estimate in disguise.
- Never invent a conversion. No exchange rates, deflators or per-unit factors the
user didn't supply — a blended figure built on an assumed rate is an invented number,
however clearly footnoted. Report per-currency (per-unit) and, if a combined view is
wanted, ask for the rate to use.
- Descriptive, not advisory. The brief describes what the data shows. It never
recommends buying/selling/pricing/provisioning — flag the decision to the qualified
owner (drafts, not advice).
Files
scripts/analyse.py — the metric engine (see table above); reuses the shared parsers
in ../../scripts/dataclean.py; python analyse.py --self-test.
references/playbooks.md — per-data-type metric menus: which analyses matter for
transactions, receivables, pipeline, survey, ops-list, time-series and generic tables.
examples/sample_sales.csv — a synthetic sales export (no real data) to demo against.
Principles
Behavioural charter: ../../PRINCIPLES.md — drafts not advice, never invent, honesty and
calibration, plain speech, action boundary.
Data handling
Metrics are computed on your machine — the data, the metrics and the brief stay on your synced
or shared file store, and the engine uploads nothing. (The AI agent driving the skill does send
whatever it reads into its context to your AI provider.) A brief that names individuals or quotes
confidential figures is gated on any egress. Full rule: ../../PRINCIPLES.md#data-handling--pii-policy.
Feedback
Improvement or bug? Use the toolkit's shared format — ../../CONTRIBUTING.md#skill-feedback-format — save as
feedback_data-analyse_[date].txt and hand it to the user to file; fix in scope if asked.
Requirements & mode
Portable: Python + openpyxl for .xlsx in/out; PDF/.docx/.msg inputs use the same
optional libraries as data-tidy (degrade per-source). No network, no Office, no
credentials. If an import fails or the environment is uncertain, pre-screen with
python ../../scripts/envcheck.py and see ../../README.md#mode--environment-compatibility — otherwise just
start analysing.
1---2name: data-analyse3description: Analyse a dataset and deliver the insights and key metrics that matter for it — an insight brief with headline findings, trends, breakdowns, concentration, outliers and ageing, tailored to the type of data (transactions, receivables, pipeline, survey, task list, any table). Use when the user says "analyse this data", "what are the key metrics", "any insights from this spreadsheet", "summarise this export", "what's driving the numbers", "who are the top customers", or hands over a table and asks what it says. NOT data cleaning (data-tidy), NOT matching two datasets (data-reconcile), NOT a dashboard (data-visualise — natural next step); descriptive analysis only, never financial or investment advice.4---56# Data Analyse78Take a dataset — a clean(ish) `.xlsx`/CSV, a pasted table, or the output of another9toolkit skill — and deliver an **insight brief**: the headline findings, the key metrics10for that *type* of data, and honest caveats. The design is **intent-first** (ask what11question the data should answer before computing anything) and **compute-then-interpret**12(every quoted number comes from the deterministic local engine; the narrative interprets,13it never generates figures).1415> **Self-sufficient & local engine.** All computation runs on your machine via16> `scripts/analyse.py` (+ the shared toolkit engine) — no network calls. Note the AI agent17> driving the skill does send whatever it reads into its context to your AI provider;18> "never leaves" is not claimed. See `../../PRINCIPLES.md#data-handling--pii-policy`.1920## Workflow2122### 0 — Intent (ask up front)23Before computing anything, ask (a short `AskUserQuestion`):241. **What question should this data answer?** ("are sales growing?", "where's the risk25 in receivables?", "which segment matters?") — or is it an open "tell me what you see"?262. **Who's it for and what decision does it feed?** — a partner one-pager reads27 differently from a working analysis.283. **Anything known already?** — targets, prior-period figures, or a hypothesis to test.2930An open-ended "just analyse it" is fine — then the playbook (step 3) drives the metric31selection, and the brief says so.3233**Done when:** you have the question (or "open-ended"), the audience/decision, and any prior figures/hypotheses — or the user has explicitly said "just analyse it". A one-line restatement to the user confirms alignment.3435### 1 — Ingest36Same shared engine as the other skills (`../../scripts/`):37```python38import sys, pathlib39sys.path.insert(0, str(pathlib.Path("../../scripts").resolve()))40import ingest41header_rows, note = ingest.read_any(path) # xlsx/csv/pdf/docx/msg; read_paste for pasted tables42```43> **Run from the skill directory** (`skills/data-analyse/`). The `../../scripts` path resolves44> to the toolkit-root `scripts/` where `ingest.py` and `dataclean.py` live. The analysis45> engine (`analyse.py`) is in this skill's own `scripts/` subdirectory.46Multi-tab workbooks: `read_any` raises `SheetSelectionRequired` if several tabs hold data —47list the sheets and ask, don't guess.4849For files with 10k+ rows, use `ingest.read_large` instead of `read_any` to avoid OOM. See50`references/large-file-patterns.md` for vectorised operation guidance.5152**Done when:** you have `(header_rows, note)` from `read_any` (or `read_large` for 10k+ rows). If `SheetSelectionRequired` was raised, the user has chosen a sheet. `note` has been read for source-quality flags.5354### 2 — Profile & quality gate55`dataclean.profile_table(header, rows)` / `score_quality(...)` — column types, missing %,56duplicates. **If the data is too messy to analyse honestly** (columns that won't parse,57heavy duplication, mixed junk), stop and route to **data-tidy** first — analysing dirty58data produces confident-looking nonsense. A little noise is fine: the engine counts every59skipped/unparseable cell and the brief must disclose those counts.6061**Currency gate:** `analyse.currency_mix(col)` — pass the **currency-bearing column**: the62amount column when codes ride inside the amounts (`S$100`, `£1,000`), **or** the separate63ISO-code column when the layout is two columns (`Amount` + `Currency` of `GBP`/`USD`) — the64common export shape. If it reports more than one code, never sum the amounts as-is65(100 USD ≠ 100 SGD). Split by currency (`breakdown(by=<currency col>)`) or ask.6667### 3 — Propose the analysis plan (confirm before computing)68`analyse.suggest_playbook(header, rows)` maps columns to roles (dates, amounts,69categories, ids) and suggests analyses. Combine that with the intent and the data-type70playbook (`references/playbooks.md` — transactions, receivables/ageing, pipeline,71survey/categorical, task/ops list, general ledger, inventory, spend/AP, time series,72cross-domain, generic) into a short plan:73*"I'll look at: monthly trend of Amount, breakdown by Customer with concentration,74outliers, and ageing from Due date as of 12 Jul 2026 — sound right?"* Show it; confirm.7576**Two datasets provided?** If the user hands over *two* files that share a dimension77(sales vs competitor prices, actual vs budget), that's the **cross-domain** playbook —78`join_on` to relate them on the shared key (report coverage), then `compare_series` to read79position and co-movement. It **relates**, it does not match line-by-line — reconciling two80sets to find breaks is **data-reconcile**. Nail the join key and grain before computing.8182### 4 — Compute (deterministic, engine only)83All metrics come from `scripts/analyse.py` — exact `Decimal`, dayfirst dates, reusing the84shared parsers:8586| Function | What it computes |87|---|---|88| `numeric_summary(col)` | n, missing, skipped, total, mean, median, quartiles, min/max, negatives |89| `outliers_iqr(col)` | Tukey fences + the outlying values (exact counts) |90| `breakdown(header, rows, by, value=)` | groups sorted largest-first with shares, top-1/top-3 share, groups-to-80% |91| `period_series(header, rows, date_col, value=, grain=)` | month/quarter/year totals with gap periods **filled as zero**, deltas, % change, YoY |92| `ageing(header, rows, date_col, as_of=, value=)` | 0–30/31–60/61–90/90+ buckets (configurable); future & unparsed bucketed visibly |93| `join_on(l_header, l_rows, r_header, r_rows, on=)` | **two-dataset** join on a shared key (case/space-folded); returns the joined table + a matched/left-only/right-only coverage report |94| `compare_series(a, b, a_label=, b_label=)` | relate two ordered series (gap / ratio / % diff per key, Pearson correlation, ±1 lead/lag) — for cross-domain & actual-vs-budget |95| `concentration(values, top_n=4)` | HHI (0–10000 antitrust scale), top-N share, groups-to-80%, classification (fragmented/moderate/concentrated/highly concentrated) — for revenue/customer concentration |96| `pivot(header, rows, rows_col, cols_col, value=, aggfunc=)` | 2D cross-tab matrix (rows × columns) with sum/count/mean aggregation + row/column grand totals |97| `distribution(values)` | skewness + excess kurtosis (Fisher-Pearson, Excel-compatible) + classification (symmetric/moderately skewed/highly skewed/heavy-tailed) |98| `trend(series)` | linear regression slope + R² + direction (rising/falling/weakly/flat) on an ordered (key, value) series — descriptive, not a forecast |99| `percentile(values, q)` | arbitrary quantile(s) with linear interpolation (Excel PERCENTILE.INC-compatible); single float → `{value, n, skipped}`, list → `{q: value}` — p90/p95/p99 for VaR/latency |100| `cohort(header, rows, id_col, date_col, value=, grain=)` | retention matrix: group by first-active period, track over subsequent periods. Count mode: retention = active/size (0–1). Value mode: matrix = value sums, retention still entity-count-based, `value_matrix` separate. All rows padded rectangular. |101| `correlation_matrix(header, rows, columns)` | pairwise Pearson across N numeric columns (symmetric matrix). Row-wise alignment — only rows where BOTH cells parse are paired (junk in one column doesn't shift others). Association, not cause. |102| `rolling(series, window, func=)` | trailing-window aggregate (mean/sum/median) on an ordered series — smoothing, pairs with `period_series`. `None` values in a window are skipped. |103| `gini(values)` | Gini coefficient (0=equal, 1=concentrated) + classification — inequality of distribution, complements HHI |104| `seasonality(header, rows, date_col, value=, grain=)` | average by month-of-year (1–12) or quarter (1–4) + seasonal index. Overall average = mean of seasons WITH data (not grand/12). |105| `filter_rows(header, rows, filters)` | declarative row filter (`{"col", "op", "value"/"values"/"lo"+"hi"}`) — `==` `!=` `>` `>=` `<` `<=` `in` `not_in` `between` `not_between` `contains` `is_empty` `not_empty`, ANDed. Numbers via `parse_number`, dates via `parse_date`, strings case-insensitive; `between` is **inclusive of both ends**. A text cell against a numeric threshold is *incomparable* — dropped and counted, never string-compared. Returns `(rows, report)` with `n_in`/`n_out`/`n_dropped` and per-filter removed counts |106| `currency_mix(col)` / `numbers(col)` | currency codes present / parsed Decimals + skipped count |107| `fmt` / `pct` / `render_md(...)` | house-style formatting + markdown tables for the brief |108| `write_metrics_xlsx(sections, path)` | optional metrics workbook (one sheet per analysis) |109110Run computations in a script (not mental arithmetic) so the analysis is reproducible;111keep the script in the working folder as the audit trail. The engine ships tested — don't112re-run its `--self-test` or `envcheck.py` as part of a normal analysis (that's setup113overhead the user pays every run); reach for them only when an import fails or the114environment is genuinely uncertain.115116### 5 — Write the insight brief117ALWAYS this structure (markdown; British English, dates DD MMM YYYY, currency with code):118119```120# [Dataset] — insight brief (as of [date], n = [rows])121## Headline 3–5 findings, most consequential first, each with its number122## Key metrics the tables from the engine (render_md)123## Notable outliers, concentration, anomalies — with the specific rows/values124## Caveats & quality skipped cells, missing %, gap periods, currency notes, what this125 data CANNOT answer126[footer: draft for review — descriptive analysis, not advice]127```128129Deliver the brief as a `.md` (plus the optional metrics `.xlsx`). Keep it to one page of130reading unless asked for depth.131132**Done when:** the brief follows the exact structure (Headline / Key metrics / Notable / Caveats & quality / footer), every number in it came from the engine, and the `.md` file is written. If a metrics `.xlsx` was requested, that too is written.133134### 6 — Offer the next step135- **Visual one-pager wanted?** → hand the computed series/breakdowns to **data-visualise**136 (its `rows_from_xlsx` reads the metrics workbook directly).137- **Numbers look wrong vs another source?** → that's **data-reconcile**, not this skill.138139**Done when:** the next step is offered to the user (visualise, reconcile, or "done") and the brief has been delivered. The skill does not start the next step without the user's go-ahead.140141## Insight discipline (what makes the brief trustworthy)142143- **Every number is computed.** If a figure appears in the brief, it came out of the144 engine or is an arithmetic step shown in the brief. No free-form estimates.145- **Observation ≠ interpretation.** "March revenue fell 40% MoM" is an observation;146 "likely the contract gap" is an interpretation — label it as a possible explanation and147 say what would confirm it. Never present correlation as cause.148- **Calibrated language.** "Verified" vs "likely" vs "couldn't confirm" (PRINCIPLES.md #4).149 A trend over 3 periods is "early"; over 2 it is not a trend.150- **Disclose the denominators.** Shares, averages and trends state n and what was151 excluded (skipped cells, unparsed dates, filled gap periods).152- **Ratios and multiples are computed too.** "20× the typical ticket" is only meaningful153 with the comparator named — compute it from engine figures and say what the base is154 (median, mean, next-largest). An eyeballed multiple is a free-form estimate in disguise.155- **Never invent a conversion.** No exchange rates, deflators or per-unit factors the156 user didn't supply — a blended figure built on an assumed rate is an invented number,157 however clearly footnoted. Report per-currency (per-unit) and, if a combined view is158 wanted, ask for the rate to use.159- **Descriptive, not advisory.** The brief describes what the data shows. It never160 recommends buying/selling/pricing/provisioning — flag the decision to the qualified161 owner (drafts, not advice).162163## Files164165- `scripts/analyse.py` — the metric engine (see table above); reuses the shared parsers166 in `../../scripts/dataclean.py`; `python analyse.py --self-test`.167- `references/playbooks.md` — per-data-type metric menus: which analyses matter for168 transactions, receivables, pipeline, survey, ops-list, time-series and generic tables.169- `examples/sample_sales.csv` — a synthetic sales export (no real data) to demo against.170171## Principles172173Behavioural charter: `../../PRINCIPLES.md` — drafts not advice, never invent, honesty and174calibration, plain speech, action boundary.175176## Data handling177178Metrics are computed **on your machine** — the data, the metrics and the brief stay on your synced179or shared file store, and the engine uploads nothing. (The AI agent driving the skill does send180whatever it reads into its context to your AI provider.) A brief that names individuals or quotes181confidential figures is gated on any egress. Full rule: `../../PRINCIPLES.md#data-handling--pii-policy`.182183## Feedback184185Improvement or bug? Use the toolkit's shared format — `../../CONTRIBUTING.md#skill-feedback-format` — save as186`feedback_data-analyse_[date].txt` and hand it to the user to file; fix in scope if asked.187188## Requirements & mode189190Portable: Python + `openpyxl` for `.xlsx` in/out; PDF/.docx/.msg inputs use the same191optional libraries as data-tidy (degrade per-source). No network, no Office, no192credentials. If an import fails or the environment is uncertain, pre-screen with193`python ../../scripts/envcheck.py` and see `../../README.md#mode--environment-compatibility` — otherwise just194start analysing.