Downloads · 30 days
5
24% of all-time downloads
Jainamshahhh/chartsage-4b
chartsage-4b is a text generation model from Jainamshahhh. Use it when you need the model to write or continue text. It is set up for peft. The card lists the license as apache-2.0.
ChartSage-4B is a LoRA adaptation of google/gemma-3-4b-it that turns a data table into matplotlib code that executes and plots the right numbers. Built for the Adaption AutoScientist Challenge, Part 2 (Data Visualizat…
Downloads · 30 days
5
24% of all-time downloads
All-time downloads
21
Public
Repo size
272 MB
Likes
0
Public
Click a slice to open those files.
.safetensors238 MB · 88%
From the Hugging Face model README
ChartSage-4B is a LoRA adaptation of google/gemma-3-4b-it that turns a data table into
matplotlib code that executes and plots the right numbers. Built for the Adaption AutoScientist
Challenge, Part 2 (Data Visualization); the training corpus was co-optimized with Adaptive
Data (Adaption Labs).
Adapters are usually scored by win rate, the share of rows where a language model judge preferred the tuned output. Every number on this card is earned by executing the generated code and comparing the values it actually plots. A chart that looks plausible and plots the wrong series scores zero here.
The two are not interchangeable, and this project has the receipt. A sibling entry in this portfolio trained its identical corpus on Adaption's own AutoScientist. The platform reported a 95.48% win rate. Our scorer, run on that same adapter, measured 62.0% exact-match accuracy. A win rate is a preference; an accuracy is a fact.
| what is measured | the generated code is run, and the plotted values compared |
| a language model judged something | never, not the labels, not the scoring |
| held-out rows scored | 400, plus a 50-row public-benchmark anchor |
| significance | p = 1.1e-76, exact McNemar |
| what this card volunteers | a disclosed regression, with its diagnosis, further down |
| cost to recompute every number | one laptop, no GPU |
Scored by part2/scripts/chartforge/score_chart.py, which extracts the code block, runs it in a
subprocess, captures the plotted y-data by monkeypatching plt.show, and compares it against the
reference series. Code that looks plausible but plots the wrong numbers scores zero. Base and tuned
were generated in the same process from the same loaded weights under identical greedy decoding,
toggling the adapter with disable_adapter(), so nothing here is a decoding artifact.
| Slice | Base | ChartSage-4B | |
|---|---|---|---|
| Held-out ChartForge, all shards (400 rows) | 26.2% | 93.5% | +257% |
| Held-out hard patterns (200 rows) | 12.0% | 96.0% | +700% |
| Held-out table QA (181 rows) | 42.0% | 85.6% | +104% |
| Chart code, public benchmark pass@1 (50 rows) | 4.0% | 20.0% | +400% |
Truncation was 0% on every held-out slice, and 0 of 400 held-out rows were unscoreable, so the 93.5% carries no asterisk.
Chart-code generation is what this model is built for, and the three rows above are the measurement that matters for it.
Note the base scores 26.2% on the held-out set, not near zero. We did not construct a distribution the base model cannot attempt; it is a capable model that we made substantially better at a specific task. That is what makes the gain meaningful.
Base and tuned answer the same held-out rows in one process under identical greedy decoding, so this is a paired design and the right test is an exact McNemar test on the rows where exactly one model was right. The null is that the adapter is no better than the base.
| slice | only tuned right | only base right | exact two-sided p |
|---|---|---|---|
| Held-out ChartForge (400) | 272 | 3 | 1.1e-76 |
Rows no execution-based scorer can recover are skipped here exactly as the released scorer
skips them, rather than being counted as failures for either side. Recompute with the
released significance.py from the released generation files.
The 96.0% row is the one worth explaining, because it is the output of a method rather than a lucky draw.
An earlier model was trained and evaluated, and its crashes were then read one at a time instead
of guessed at. Of 12 sampled failures, 10 were the same thing: a label/value length mismatch.
Real tables frequently carry a trailing aggregate column (Jan ... Dec, then Year) that must be
excluded from the plotted series, and the generator had never produced one, so the model had never
seen the pattern. Two neighbouring gaps showed up alongside it: requests that name a subset of rows,
and series containing leading zeros.
So 12,000 rows targeting exactly those three patterns were built and appended on a disjoint id range, with a matching 600-row hard holdout so the difficulty appears on both sides of the split. The base scores 12.0% on that holdout, against 26.2% on the regular one, which confirms the patterns are genuinely harder rather than a label we invented. ChartSage scores 96.0%.
Diagnose mechanically, build for the diagnosis, then re-measure the same thing.
| Behaviour on the public benchmark | Base | ChartSage-4B |
|---|---|---|
| Re-parses the table JSON instead of using the values | 43 / 50 | 0 / 50 |
| Emits an unclosed code block (hit the token budget) | 1 / 50 | 3 / 50 |
The first row is the entry in one line. The base model's dominant failure is writing
json.loads(...) or pd.read_json(...) against a table it was handed as text, which crashes or
mangles the data. ChartSage hardcodes the values it was given, every time.
The second row is reported because it is not in our favour. At the 2,048-token budget used here the tuned model leaves an unclosed block slightly more often than the base. An earlier day-0 measurement of the base showed 9 of 50 at a smaller 1,280-token budget; that figure is not comparable and is not claimed as a fix.
The corpus carries a 39,982-row table-QA shard, and it works: on 181 held-out QA rows the model goes 42.0% to 85.6%, a +104% relative gain on questions of the kind it was built for. Those are computed questions with one correct value: maximum, minimum, sum, mean, count above average, trend direction. Every answer in training was derived arithmetically from the underlying series, so there is no model anywhere in the label path.
On the public benchmark's mixed QA slice the model scores 13.1% against the base's 20.2% (99 rows), and the reason is a scoping decision we can state precisely rather than a defect we cannot explain.
acqui terme, 490.7), three entities at once (spain, france, united kingdom), and free
prose describing anomalies in a table. ChartForge deliberately targets single computed values,
so training made the model sharper at exactly one answer shape and correspondingly less inclined to
produce the others. Better at what we aimed for, worse at what we did not.thereunion against our
reunion is the same episode with the leading article dropped. Counting near-misses on both sides
gives 18.2% against 24.2%, roughly half the apparent gap.The honest summary is that this model is strong on computed table QA (85.6%) and not built for descriptive table QA, and we can tell you which is which because we measured both.
The same corpus and the same recipe were run on a second, much stronger base,
Qwen/Qwen3.5-4B, and evaluated identically. The point was to check whether these results are a
property of the data or a quirk of one model. They are a property of the data.
| gemma-3-4b-it base | gemma-3-4b-it tuned | Qwen3.5-4B base | Qwen3.5-4B tuned | |
|---|---|---|---|---|
| Held-out ChartForge | 26.2% | 93.5% | 71.2% | 92.8% |
| Public benchmark pass@1 | 4.0% | 20.0% | 34.0% | 20.0% |
Two bases that start 45 points apart on the held-out distribution (26.2% against 71.2%) finish within 0.7 points of each other (93.5% against 92.8%). On the public benchmark both land on exactly 20.0%. ChartForge takes a model to the same place from either direction.
Three things follow, and the second one is not flattering:
The Qwen run was stopped after epoch 1, at which point it had already reached 92.8% against gemma's 93.5% at epoch 3, so further epochs would not have changed the conclusion.
[0.8, 0.8, 0.8, 0.8]). A perfectly correct answer to those seven scores zero.
score_chart.py prints this above every score rather than leaving it to be discovered.google/gemma-3-4b-it, Gemma terms..*\.language_model\..*\.(q|k|v|o|gate|up|down)_proj), attaching 2,142 modules and 0 vision
modules, verified at launch. gemma-3-4b-it is a multimodal checkpoint, and loading it through a
text-only class silently random-initializes the decoder; a sanity assertion generates 40 tokens
and refuses to proceed on degenerate output.from transformers import AutoTokenizer, AutoModelForCausalLM
from peft import PeftModel
import torch
# AutoModelForCausalLM resolves gemma-3-4b-it to its multimodal wrapper, which is correct.
# Do NOT load it through a text-only class: the decoder is then randomly initialized.
tok = AutoTokenizer.from_pretrained("google/gemma-3-4b-it")
base = AutoModelForCausalLM.from_pretrained(
"google/gemma-3-4b-it", torch_dtype=torch.bfloat16, device_map="auto")
model = PeftModel.from_pretrained(base, "Jainamshahhh/chartsage-4b").eval()
table = {"columns": ["Quarter", "Revenue"],
"data": [["Q1", "120"], ["Q2", "150"], ["Q3", "90"], ["Q4", "170"]]}
prompt = ("Given this table (JSON):\n" + str(table).replace("'", '"')
+ "\n\nTask: Draw a bar chart of Revenue by Quarter."
+ "\nWrite Python matplotlib code that draws the requested chart from the table data. "
"Return only a single python code block.")
ids = tok.apply_chat_template([{"role": "user", "content": prompt}],
add_generation_prompt=True, return_tensors="pt").to(model.device)
print(tok.decode(model.generate(ids, max_new_tokens=512, do_sample=False)[0][ids.shape[1]:],
skip_special_tokens=True))
Live side by side demo: https://huggingface.co/spaces/Jainamshahhh/chartsage-demo Enter your own input and watch the base model and ChartSage-4B answer it under identical greedy decoding. The GPU backend scales to zero, so a cold first request takes about a minute.
Released on both platforms, with the scorer, every eval slice, the per row verdicts and
significance.py alongside, so every number on this page can be recomputed rather than
trusted.
Jainamshahhh/chartforge. The dataset verifier, the scoring harness and the raw
generation files ship alongside, so every number on this card can be recomputed without a GPU:
python part2/scripts/chartforge/verify_dataset.py --sample 3000
python part2/scripts/chartforge/score_chart.py gen_chart_v3_ep3__chart_held.jsonl
Author: Jainam Shah. Built with Adaptive Data (Adaption Labs) and the open Gemma model.