A spreadsheet and a question go in. The answer, a chart and the steps that produced them come out.
Web demo on the workbook (MP4). The recording replays a result computed by Qwen2.5-Coder-1.5B-Instruct (Q4_K_M).
Imagine a sales workbook and a question: which region grew fastest last quarter? Someone opens the file, builds a pivot, and pastes a number into a slide. A different reading of “last quarter” gives a different number, and the steps that produced it are usually already gone.
The useful result is not a model that can “understand spreadsheets.” It is the number, the chart, and the exact steps — including the code — that produced them, so a person can rerun and check.
The pipeline is five steps.
- An Excel file or a CSV comes in. Sheets and the header row are detected automatically.
- The file is profiled: columns, types, a few sample values.
- A model writes pandas code for the question, using a single reading of phrases such as “last quarter” and “grew fastest.”
- That code runs in a sandbox. The original file is not modified.
- You get the computed value, a sentence, a chart when one helps, and the steps — shown as readable steps and as code you can rerun.
The same five steps, whether the backend is your API or open models on your own machine.
One of the samples, before and after:
sales.xlsx. Answer “East,” which matches the ground truth. scripts/evaluate.py.
On seven questions against one sales workbook (and the same sales rows as a CSV), the API backend with Qwen2.5-Coder-1.5B-Instruct (Q4_K_M) got 6 of 7 values correct, in 20.1–103.5 seconds per question.
The one miss is top_region: “Which region has the highest total revenue?” expected North and came back as East.
| Id | Question | Expected | Got | Correct | Time (s) |
|---|---|---|---|---|---|
| latest_order_date | What is the latest order date? | 2026-09-15 | 2026-09-15 | yes | 24.9 |
| total_revenue | What is the total revenue? | 43290 | 43290 | yes | 31.0 |
| top_region | Which region has the highest total revenue? | North | East | no | 60.3 |
| top_product | Which product sold the most units? | Ash | Ash | yes | 51.4 |
| fastest_growth | Which region grew fastest last quarter? | East | East | yes | 103.5 |
| most_returns | Which region has the most returned units? | South | South | yes | 38.4 |
| csv_total_revenue | What is the total revenue? (sales.csv) | 43290 | 43290 | yes | 20.1 |
| All | 6/7 | 20.1–103.5 |
Methodology. One workbook (samples/sales.xlsx, sheets sales and returns) and samples/sales.csv, seven questions in samples/questions.json. A question counts as correct when the computed value matches that ground truth (dates as ISO, numbers exact, strings case-insensitive). The value comes from running the generated pandas in the sandbox, not from the wording of the sentence. “Last quarter” is the calendar quarter of the latest date; “grew fastest” is the largest percentage change in revenue between that quarter and the previous one. python scripts/evaluate.py. API backend, Qwen2.5-Coder-1.5B-Instruct (Q4_K_M) behind a local llama.cpp server, 12 November 2026. The raw table is in docs/eval.md.
The Hugging Face backend with Qwen2.5-Coder-1.5B-Instruct (and Qwen2.5-1.5B-Instruct for the sentence, loaded one at a time) got 4 of 7. top_region was correct there; top_product, fastest_growth and most_returns failed (the generated code looked up a units column that is not on the sales sheet, or returned no value). Times were 34.5–137.9 seconds. The raw table is in docs/eval-hf.md.
A number without the steps is a rumour. A number with the code that produced it is a claim you can rerun — from “East” back to the percentage-growth calculation, and reject it if the code is wrong. That is the difference between an answer you recompute to be safe, and an answer you can actually use.
It fits anywhere a team already keeps a spreadsheet and answers the same kinds of question from it. The backend can be a hosted API or open models on your own machine; the sandbox and the rest of the pipeline stay the same.
Python 3.11 or newer.
git clone https://github.com/ModelShifts/table-qa
cd table-qa
python -m venv .venv && source .venv/bin/activate
pip install -e ".[hf,demo]" # open models; use ".[api,demo]" for the API backend
cp .env.example .env # choose a backend and modelsOn a CPU-only machine, install the CPU build of PyTorch first (pip install torch --index-url https://download.pytorch.org/whl/cpu) and set MS_SMALL=1 in .env. That selects the 1.5B models. Models download from Hugging Face on first use.
# sheets, the detected header row, and columns
table-qa profile samples/sales.xlsx
# answer, steps, and the code. Add --out DIR to also save chart.png and analysis.py
table-qa ask samples/sales.xlsx "Which region grew fastest last quarter?"
# the same question against the CSV
table-qa ask samples/sales.csv "What is the total revenue?"
# the web demo at http://127.0.0.1:7860 (python app.py does the same)
table-qa servesamples/ holds the workbook and CSV, with a ground-truth file for the seven questions. Tests use a mocked model and need no downloads: pip install -e ".[dev]" && pytest.
Two interchangeable backends — API key or open models. The rest of the pipeline does not change.
Pick one with MS_BACKEND in .env.
| (a) Bring your own API key | (b) Open-source on Hugging Face | |
|---|---|---|
| What | Any OpenAI-compatible endpoint | Open models, run locally with transformers |
| Set | MS_BACKEND=api, MS_API_BASE, MS_API_KEY, MS_MODEL |
MS_BACKEND=hf, optionally MS_HF_CODE_MODEL and MS_HF_ANSWER_MODEL |
| Sentence | MS_ANSWER_MODEL if set, otherwise MS_MODEL |
MS_HF_ANSWER_MODEL |
| Good for | Fastest start, hosted models | Data stays on your machines, no per-call cost |
Default open-source models
| Model | Used for | License |
|---|---|---|
Qwen/Qwen2.5-Coder-7B-Instruct |
planning and generating the analysis code | Apache-2.0 |
Qwen/Qwen2.5-7B-Instruct |
writing the answer in plain language | Apache-2.0 |
On a machine without a GPU, set MS_SMALL=1. That selects Qwen/Qwen2.5-Coder-1.5B-Instruct for the code and Qwen/Qwen2.5-1.5B-Instruct for the sentence. On CPU the two models are loaded one at a time. The 7B defaults need a GPU.
To try the API backend without a paid key, run an open model behind a local OpenAI-compatible server, for example llama-cpp-python:
pip install "llama-cpp-python[server]" huggingface_hub \
--extra-index-url https://abetlen.github.io/llama-cpp-python/whl/cpu
hf download Qwen/Qwen2.5-Coder-1.5B-Instruct-GGUF qwen2.5-coder-1.5b-instruct-q4_k_m.gguf --local-dir models
python -m llama_cpp.server --model models/qwen2.5-coder-1.5b-instruct-q4_k_m.gguf --n_ctx 8192 --port 8001
# then: MS_BACKEND=api MS_API_BASE=http://127.0.0.1:8001/v1 MS_API_KEY=local MS_MODEL=qwen2.5-coder-1.5b-instruct-q4_k_mGenerated code runs in a subprocess with a timeout, no new sockets, and a read-only copy of the file. The original file is not modified. One header row per sheet is supported; merged cells, pivot layouts and .xls files are not. Macros are not run.
- Hosted demo
- Merged cells and pivot layouts
MIT © Modelshifts Technology Pte. Ltd.
Part of the Modelshifts showcase. November 2026, v1.

