Share an EDA / data-profiling report
The first honest thing anyone does with a new dataset is profile it —
ydata-profiling, Sweetviz, D-Tale, skimpy. One line of Python, and out
comes a genuinely excellent document: every column's distribution,
missingness, cardinality, correlations, the rows that look wrong.
Then comes the part nobody solved. The file is 15–40 MB of self-contained HTML. Email bounces it. Slack mangles it. Committing it to the repo makes the reviewer download a blob to open locally, and putting it on a static host means publishing sample rows of production data to the internet. So the profile — the artifact most likely to catch a data problem before it reaches a model or a board deck — gets looked at by exactly one person.
Publish it instead
from ydata_profiling import ProfileReport
ProfileReport(df, title="Orders — 2026-08").to_file("profile.html")
Then drag profile.html into Comma, or push
it from a pipeline:
curl -fsS -X POST https://commareports.com/api/v1/reports \
-H "Authorization: Bearer $COMMA_API_TOKEN" \
-H "Content-Type: application/json" \
-d "$(jq -n --rawfile html profile.html \
'{title: "Orders profile — 2026-08", html: $html}')"
You get a URL that opens like a document — no Python, no notebook server, no repo access — behind the access level you pick. Reports are private by default and can be opened to invited people, to any signed-in colleague, or to the public. For an artifact containing real sample rows, that choice is the whole point; see the sharing model.
The conversation belongs on the column
A profiling report is a list of things that might be wrong. Every one of them deserves a sentence from someone who knows the domain, and every one of those sentences currently gets typed into Slack, detached from the column it's about.
With an anchored comment layer on the rendered report,
the analyst highlights discount_pct — 12.4% missing and pins a thread:
"missing means no promo applied, not unknown — don't impute." Someone
else marks the bimodal session_length histogram: "that's the bot
traffic we filtered out in July, it's back." The next person to open the
profile reads the domain knowledge next to the statistic instead of
rediscovering the same anomaly a month later.
That's the difference between a profile you generate and a profile your team actually reviews.
Re-profile without breaking the link
PATCH the same report id when the data refreshes:
curl -fsS -X PATCH "https://commareports.com/api/v1/reports/$REPORT_ID" \
-H "Authorization: Bearer $COMMA_API_TOKEN" \
-H "Content-Type: application/json" \
-d "$(jq -n --rawfile html profile.html '{html: $html}')"
Same URL, one revision per refresh, and any two revisions can be diffed — which is how a drifting distribution or a newly-null column shows up as a change rather than as a fact you have to notice twice. Open threads carry across revisions.
To take the human out of the loop entirely, a
routine regenerates and republishes on a schedule,
and a webhook on revision.created announces each refresh in
Slack.
What renders, and what doesn't
- Renders faithfully — per-column statistics tables, histograms, correlation heatmaps, missing-value matrices, sample rows. These are HTML, CSS, and embedded images.
- Inert — the collapsible tab navigation and any interactive widget,
because Comma strips
<script>on write as defense-in-depth on an endpoint that accepts arbitrary HTML. Reports generated in the flat or long-form layout read best; Sweetviz and skimpy output land the same way. - Big files are fine — a self-contained profile with inlined images is a single HTML document. If your generator writes a folder instead, drop the whole folder or a zip of it and the relative references are rewritten automatically.
Working in notebooks? Share the notebook itself the same
way — export with nbconvert, publish, and comment on the cells.
Try it
Comma is free — unlimited reports, unlimited commenters, unlimited revision history. Profile your next dataset, publish it, and let the people who know the data write on it.