Variance & Bridge
v2.0.0Explain performance against budget, prior year and forecast. Start here for a focused variance review.
Budget vs actualCompare monthly and year-to-date performance with budget, then identify the movements that matter.
- Inputs
- P&L CSV: period, line item, Revenue/COGS/Opex category, actual and budget.
- Outputs
bva.jsonbva-note.md
Folder structure
budget-vs-actual/
├── scripts/
│ └── bva.py
└── SKILL.mdRead SKILL.md
packs/variance-bridge/skills/budget-vs-actual/SKILL.md
---
name: budget-vs-actual
description: Compares actual to budget by P&L line for a month and year to date, with favourable and unfavourable variances, and writes the variance commentary. Use when asked for budget vs actual, BvA, actual versus plan, a P&L variance analysis, why we missed or beat budget, or commentary on the month or the year to date. Needs a P&L CSV with period, line item, category, actual and budget.
---
# Budget vs actual
Produces two files: the script's figures (`bva.json`) and a variance
commentary note (`bva-note.md`). Unless the user names other paths, write
both to an `output/` folder in the current working directory.
## Steps
1. Find the P&L CSV the user named. Open only its header row. It needs
`period` (YYYY-MM), `line_item`, `category` (Revenue, COGS or Opex),
`actual` and `budget`. If any are missing, ask.
2. Run the script, `skills/budget-vs-actual/scripts/bva.py`. As in every
file in this pack, paths are relative to the pack root, the top folder of
the pack, which holds its main SKILL.md:
python3 <pack root>/skills/budget-vs-actual/scripts/bva.py \
--input <the CSV> --output output/bva.json
The month defaults to the last period in the file; the year to date runs
from the first month of that year to the month. If the user names another
month, add `--period YYYY-MM`.
3. If the script exits with an error, here or in step 4, show it and stop.
Exit code 3 means the variances do not reconcile; never report its figures.
4. Run it with `--note`; it writes the note, Drivers left as a marked line:
python3 <pack root>/skills/budget-vs-actual/scripts/bva.py --note \
--input output/bva.json --output output/bva-note.md
5. Replace the marked line with the Drivers bullets, to the contract below,
using only `display` in `output/bva.json`. Never edit another section.
6. Check the note before you finish. Every figure must be a display string,
character for character, with parentheses exactly where `display` has
them: a negative keeps them in prose too ("a variance of (0.9) pts") and
a positive figure never gets them. Join a percentage to its amount with
"or": "favourable by $38K, or 28.5%".
## Reading the display block
- A positive variance is favourable: revenue or profit above budget, or a
cost below it. An unfavourable variance prints in parentheses. Use only
the words favourable and unfavourable for the sign.
- `display.ytd` and `display.month` hold every line and the subtotals.
- `display.movers` ranks the cost lines (COGS and Opex) by the size of their
year-to-date variance: up to three favourable and three unfavourable.
Revenue is not ranked; `display.monthly_revenue` shows its variance month
by month.
- `display.peaks` gives each line's largest year-to-date variance and the
month it was reached. A line whose year-to-date variance is now much
smaller than its peak reversed during the year.
- Every amount is in $K, rounded half up from its exact figure on its own:
$11,238K, ($362K). One that is not zero but would show as zero keeps the
fewest decimals that show it: $0.4K. `display.rounding` names the rows
whose rounded figures do not add up, and is empty when they all do.
## Output contract
The note has exactly these five sections, in this order:
## Headline
One sentence: year-to-date revenue against budget and operating income
against budget, each with its variance and variance percentage, from
`display.ytd.subtotals`.
## Results
A table with columns Measure, Actual, Budget and Variance, and these
rows in this order, from `display.ytd.subtotals`: Revenue, COGS, Gross
profit, Gross margin, Operating expenses, Operating income. The Gross
margin row shows percentages, and its Variance is the points string,
which ends in pts.
## Month
One to three sentences on the month's revenue and operating income
against budget, from `display.month.subtotals`. The first sentence
starts with `display.labels.period_name`, month and year.
## Drivers
Two to four bullets. Each names one line from `display.movers` with its
year-to-date variance; or the months in `display.monthly_revenue` where
the revenue miss concentrated, with their variances; or a line whose
`display.peaks` entry shows a variance that reversed, with the peak and
its month.
## Data notes
One line in this form, with the backticks, where <file>, <rows> and
<sha12> are the strings in display.input:
<file>, <rows> rows, sha256 `<sha12>`
Then display.rounding, after a blank line, when it is not empty.
- Copy every figure exactly as it appears in `display`, including parentheses.
- Display figures are rounded. Never add them up, subtract them or derive
new figures from them, and never add up months.
- You choose which movers matter, their order, and what to ask each line
owner. Call a variance timing only when `display.monthly_revenue` or
`display.peaks` shows the reversal. Do not name a cause the data does not
show.
- The Results table's COGS row is the COGS category: Cost of goods sold
plus Freight out. In prose, name a line by its `line_item` in full
("Cost of goods sold"), and keep "COGS" for the category row.
Revenue bridgeExplain the revenue change between two years through customer movements.
- Inputs
- Customer CSV: ID, name, prior-year and current-year revenue; timing amount is optional.
- Outputs
bridge.jsonbridge-note.md
Folder structure
revenue-bridge/
├── references/
│ └── definitions.md
├── scripts/
│ └── bridge.py
└── SKILL.mdRead SKILL.md
packs/variance-bridge/skills/revenue-bridge/SKILL.md
---
name: revenue-bridge
description: Computes a prior-year to current-year revenue bridge by customer (timing, loss, decline, upsell, new) and writes a one-page commentary note. Use when asked for a revenue bridge, a PY-to-CY revenue walk, a customer bridge, why revenue changed year over year, or which customers drove the change. Needs a customer CSV with prior-year and current-year revenue columns.
---
# Revenue bridge
Produces two files: the script's figures (`bridge.json`) and a commentary
note (`bridge-note.md`). Unless the user names other paths, write both to an
`output/` folder in the current working directory.
## Steps
1. Find the input CSV the user named. Open only its header row. It needs
`customer_id`, `customer_name`, a prior-year and a current-year revenue
column, and optionally `timing_amount`. If any are missing, ask.
2. Run the script, `skills/revenue-bridge/scripts/bridge.py`. As in every
file in this pack, paths are relative to the pack root, the top folder of
the pack, which holds its main SKILL.md:
python3 <pack root>/skills/revenue-bridge/scripts/bridge.py \
--input <the CSV> --output output/bridge.json
If the revenue columns are not named `py_revenue`/`cy_revenue` or
`fyNNNN_revenue`, add `--py-col <name> --cy-col <name>`.
3. If the script exits with an error, show the error and stop. Exit code 3
means the bridge does not reconcile; never report its figures. Before
exit 0 the script checks that PY + Timing + Loss + Decline + Upsell + New
= CY to the cent.
4. Read `output/bridge.json`. Use only the strings under `display`.
5. Write `output/bridge-note.md` to the contract below.
6. Check the note before you finish. Every figure must be a display string,
character for character, with parentheses exactly where `display` has
them: a negative keeps them and a positive figure never gets them. Join a
figure to a name with a verb: "Ember Analytics added $136K".
## Output contract
The note has exactly these four sections, in this order:
## Headline
One sentence: current-year revenue, prior-year revenue, and the change
with its percentage (the script prints n/a when prior-year revenue is 0).
## Bridge
A table with columns Step and Amount, and these rows in this order:
<PY label>, Timing, Loss, Decline, Upsell, New, <CY label>.
## Drivers
Two to four bullets. Each names one customer from `display.movers` and
joins its amount to the name with a verb ("Ember Analytics added
$136K"), or gives one movement total with its customer count.
## Data notes
One line in this form, with the backticks, where <file>, <rows> and
<sha12> are the strings in display.input:
<file>, <rows> rows, sha256 `<sha12>`
If display.rounding is not empty, copy it whole on the next line.
- Copy every figure exactly as it appears in `display`, including parentheses.
- Display figures are rounded. Never add them up or derive new figures from them.
- Use the labels from `labels` (for example FY2024) for the first and last rows.
- You choose which customers and movements to name and in what order. Do not
name a cause the data does not show.
- See `skills/revenue-bridge/references/definitions.md` for what each movement
means and how the display rounds.
Price, volume & mixSeparate a revenue change into price, volume and mix effects by product family and segment.
- Inputs
- Product CSV: product family, segment, units and revenue for both years.
- Outputs
pvm.jsonpvm-note.md
Folder structure
price-volume-mix/
├── references/
│ └── method.md
├── scripts/
│ └── pvm.py
└── SKILL.mdRead SKILL.md
packs/variance-bridge/skills/price-volume-mix/SKILL.md
---
name: price-volume-mix
description: Splits a revenue change between two years into price, volume and mix effects by product family and segment, reconciled to the cent, and writes the commentary. Use when asked for a price-volume-mix analysis, PVM, rate-volume-mix, how much of the revenue change was price versus volume, or a mix shift explanation. Needs a product CSV with units and revenue for both years.
---
# Price-volume-mix
Produces two files: the script's figures (`pvm.json`) and a commentary note
(`pvm-note.md`). Unless the user names other paths, write both to an
`output/` folder in the current working directory.
## Steps
1. Find the product CSV the user named. Open only its header row. It needs
`product_family`, `segment`, and units and revenue for both years. If
any are missing, ask.
2. Run the script, `skills/price-volume-mix/scripts/pvm.py`. As in every
file in this pack, paths are relative to the pack root, the top folder of
the pack, which holds its main SKILL.md:
python3 <pack root>/skills/price-volume-mix/scripts/pvm.py \
--input <the CSV> --output output/pvm.json
By default the script uses the two `fyNNNN_units` and `fyNNNN_revenue`
column pairs, the earlier year as prior year. If the columns are named
otherwise, add `--py-units`, `--py-rev`, `--cy-units` and `--cy-rev`,
each with a column name.
3. If the script exits with an error, show the error and stop. Exit code 3
means the effects do not reconcile; never report its figures. Before
exit 0 the script checks that PY + Volume + Mix + Price = CY to the
cent, in total and for every family and segment, and that the families
and the segments add up to the totals.
4. Read `output/pvm.json`. Use only the strings under `display`.
5. Write `output/pvm-note.md` to the contract below.
6. Check the note before you finish. Every figure must be a display string,
character for character, with parentheses exactly where `display` has
them: a negative keeps them and a positive figure never gets them. Join a
figure to a name with a verb: "Paper Mid-Market added $75K".
See `skills/price-volume-mix/references/method.md` for what each effect
means, how it is rounded and how the display rounds.
## Output contract
The note has exactly these five sections, in this order:
## Headline
One sentence: current-year revenue against prior-year revenue, and the
change with its percentage, from `display.totals`.
## Bridge
A table with columns Step and Amount, and these rows in this order,
from `display.totals`: <PY label>, Volume, Mix, Price, <CY label>.
## By family
A table with columns Family, <PY label>, <CY label>, Change, Price, Mix
and Volume: one row per family, in the order of `display.by_family`.
## Drivers
Two to four bullets. Each bullet starts with the name of a family or a
segment, from `display.by_family`, `display.by_segment` or
`display.movers`, and gives its effect. The totals are in the Headline
and Bridge; no bullet restates them.
## Data notes
One line in this form, with the backticks, where <file>, <rows> and
<sha12> are the strings in display.input:
<file>, <rows> rows, sha256 `<sha12>`
If display.rounding is not empty, copy it whole on the next line.
- Copy every figure exactly as it appears in `display`, including parentheses.
- Display figures are rounded. Never add them up or derive new figures from them.
- Use the labels from `display.labels` (for example FY2024) for the first
and last bridge rows and the year columns.
- You decide whether the story is price or volume, which family carries it,
and what a pricing or sales reader would ask next. Do not compute a
price, an average price or a share of the total. Call a change a mix
shift only when `display.by_segment` or `display.movers.mix` shows it.
Forecast accuracyCompare forecast versions with actual results and identify where the forecasting process missed.
- Inputs
- Forecast-versions CSV plus the P&L actuals and budget. Forecast rows identify closed months.
- Outputs
accuracy.jsonaccuracy-note.md
Folder structure
forecast-accuracy/
├── references/
│ └── metrics.md
├── scripts/
│ └── accuracy.py
└── SKILL.mdRead SKILL.md
packs/variance-bridge/skills/forecast-accuracy/SKILL.md
---
name: forecast-accuracy
description: Measures how accurate each forecast version was against actuals (full-year variance, mean absolute percentage error, bias, worst month) and writes the accuracy review. Use when asked how good the forecasts or reforecasts were, forecast accuracy, forecast versus actual by version, MAPE, bias, or which reforecast to trust. Needs a forecast-versions CSV and the P&L actuals.
---
# Forecast accuracy
Produces two files: the script's figures (`accuracy.json`) and an accuracy
review (`accuracy-note.md`). Unless the user names other paths, write both
to an `output/` folder in the current working directory.
## Steps
1. Find the forecast-versions CSV and the P&L CSV the user named. Open only
their header rows. The forecasts need `version`, `period`, `line_item`,
`amount` and `is_actual` (1 for a closed month that carries the actual,
0 for a forecast month); the P&L needs `period`, `line_item`, `category`,
`actual` and `budget`. If a file or a column is missing, ask.
2. Run the script, `skills/forecast-accuracy/scripts/accuracy.py`. As in
every file in this pack, paths are relative to the pack root, the top
folder of the pack, which holds its main SKILL.md:
python3 <pack root>/skills/forecast-accuracy/scripts/accuracy.py \
--forecasts <the forecasts CSV> --actuals <the P&L CSV> \
--output output/accuracy.json
A forecast line takes its actuals from the P&L line of the same name;
Total operating expenses is the sum of the P&L's Opex lines.
3. If the script exits with an error, show the error and stop. Exit code 3
means a version does not tie to the P&L; never report its figures.
Before exit 0 the script checks that every closed month carries the P&L
actual and that the Budget version equals the P&L budget.
4. Read `output/accuracy.json`. Use only the strings under `display`.
5. Write `output/accuracy-note.md` to the contract below.
6. Check the note before you finish. Every figure must be a display string,
character for character, with parentheses exactly where `display` has
them: a negative keeps them and a positive figure never gets them. Join a
percentage to its amount with "or": "a variance of $3K, or 0.02%".
See `skills/forecast-accuracy/references/metrics.md` for what each metric
means, what a reader may conclude from it and how the display rounds.
## Output contract
The note has exactly these five sections, in this order:
## Headline
One sentence: the most accurate revenue version by name, from
`display.best`, with its full-year variance and MAPE.
## Revenue
A table with columns Version, Closed months, FY forecast, FY actual,
Variance, %, MAPE and Bias: one row per version for Revenue, in the
order of `display.labels.versions`. The cells are the version's
`closed_months`, `fy_forecast`, `fy_actual`, `fy_variance`,
`fy_variance_pct`, `mape` and `bias` from `display.versions`.
## Operating expenses
The same table for Total operating expenses.
## Findings
Two to four bullets. Each bullet starts with a version name from
`display.labels.versions` and gives a figure for that version from
`display.versions`, such as its worst month or its MAPE.
## Data notes
One bullet per input, in this form, with the backticks, where <file>,
<rows> and <sha12> are the strings in display.inputs.forecasts, then
display.inputs.actuals:
- <file>, <rows> rows, sha256 `<sha12>`
If display.rounding is not empty, copy it whole as one more bullet.
- Copy every figure exactly as it appears in `display`, including parentheses.
- Display figures are rounded. Never add them up or derive new figures from them.
- You judge whether accuracy improved because fewer months were left to
forecast or because the forecasting improved, what a single large miss
means for the process, and which version a planner should anchor on. Do
not compute a difference between two versions' figures, average MAPEs, or
call a miss a one-off without naming its month.
Pack routing instructions
packs/variance-bridge/SKILL.md
---
name: variance-bridge
description: Builds revenue bridges, budget-vs-actual commentary, price-volume-mix analyses and forecast-accuracy reviews for finance reviews. Use when asked for a revenue bridge or PY-to-CY walk, why revenue changed, budget vs actual or variance commentary, price versus volume versus mix, or how accurate the forecasts were. Not for monthly reporting packs, account reconciliations or forward forecasts.
license: MIT
compatibility: Requires Python 3.9 or later (standard library only) and permission to run scripts. If scripts cannot run, the pack stops and says so; it never computes figures itself.
metadata:
pack-version: "2.0.0"
pack-standard: "skills-pack/1.0"
house: "interstationary/2.0.0"
---
# Variance and Bridge pack
This pack holds several skills. Pick the row that matches the request, read
every file listed for it, then follow the skill. Read the files before
answering; do not answer from memory.
All paths in this pack, in every file, are relative to this file's folder
(the pack root).
| Request | Read first | Then read |
| --- | --- | --- |
| Revenue bridge, PY-to-CY walk, customer movement, why revenue changed year over year | `skills/revenue-bridge/SKILL.md` | `_shared/source-trace/SKILL.md`, `_shared/executive-voice/SKILL.md` |
| Budget vs actual, BvA, actual versus plan, P&L variance commentary for a month or the year | `skills/budget-vs-actual/SKILL.md` | `_shared/source-trace/SKILL.md`, `_shared/executive-voice/SKILL.md` |
| Price-volume-mix, PVM, how much of the change was price versus volume, mix shift | `skills/price-volume-mix/SKILL.md` | `_shared/source-trace/SKILL.md`, `_shared/executive-voice/SKILL.md` |
| Forecast accuracy, how good were the reforecasts, forecast versus actual by version, MAPE, bias | `skills/forecast-accuracy/SKILL.md` | `_shared/source-trace/SKILL.md`, `_shared/executive-voice/SKILL.md` |
| Anything else (monthly review, close, cash, scenarios) | Not in this pack | Say so and stop |
## Rules for every skill in this pack
1. Numbers come from scripts. Never calculate, round or restate a figure yourself.
2. If a script fails or cannot run, report the error message and stop.
3. If the request is missing an input the skill needs, ask for it. Do not guess.
