Profile a CSV or Parquet file and write the result as one static HTML page: inferred type, null count, unique count, min/max/mean/std and a histogram for every numeric column, the top five values for every categorical column, a correlation matrix for the numeric columns, plus duplicate row count and an estimated memory size for the whole file.
The report is a single .html file with the CSS inline and the histograms
drawn as inline SVG. No JavaScript charting library, no CDN, no server to
keep running. Open it in a browser, attach it to an email, or commit it next
to the dataset it describes.
$ uv tool install git+https://github.com/jmweb-org/framescan
$ pipx install git+https://github.com/jmweb-org/framescan # if you use pipxNot on PyPI. The only dependency is polars, which also gives Parquet
support for free — pl.read_parquet doesn't need pyarrow installed.
$ framescan data.csv -o report.htmlusage: framescan [-h] [-o OUTPUT] [--version] input
Profile a CSV or Parquet file and write a static HTML report.
positional arguments:
input path to a .csv or .parquet file
options:
-h, --help show this help message and exit
-o OUTPUT, --output OUTPUT
where to write the HTML report (default: report.html)
--version show program's version number and exit
If input doesn't exist, is a directory, or has an extension framescan
doesn't understand, it prints a one-line error to stderr and exits 1
instead of writing a half-finished report or a Rust traceback.
examples/inventory.csv is a small, made-up product inventory: 26 rows, 7
columns, one row duplicated on purpose, a few missing ratings and one row
missing its warehouse.
$ framescan examples/inventory.csv -o /tmp/inventory-report.html
framescan: wrote /tmp/inventory-report.html (10,132 bytes)That command's real output, not a mockup. Two fragments pulled straight out
of the generated file: the row for the price column —
<tr><td class="col-name">price</td><td><code>Float64</code></td>
<td>0 <span class="muted">(0.0%)</span></td><td>23</td>
<td><div class="stats"><span>min 7.5</span><span>max 249</span>
<span>mean 56.8</span><span>std 62.7</span></div>
<svg viewBox="0 0 260 60" width="260" height="60" role="img"
aria-label="histogram">...</svg></td></tr>— and the correlation matrix, three numeric columns wide:
<table class="correlation">
<thead><tr><th></th><th>price</th><th>quantity</th><th>rating</th></tr></thead>
<tbody>
<tr><th>price</th>
<td style="background:rgb(92, 138, 240)">1.00</td>
<td style="background:rgb(240, 159, 159)">-0.59</td>
<td style="background:rgb(207, 221, 251)">0.29</td></tr>
<tr><th>quantity</th>
<td style="background:rgb(240, 159, 159)">-0.59</td>
<td style="background:rgb(92, 138, 240)">1.00</td>
<td style="background:rgb(249, 218, 218)">-0.23</td></tr>
<tr><th>rating</th>
<td style="background:rgb(207, 221, 251)">0.29</td>
<td style="background:rgb(249, 218, 218)">-0.23</td>
<td style="background:rgb(92, 138, 240)">1.00</td></tr>
</tbody></table>Reading that table: price and quantity move against each other in this data (-0.59, expensive items are stocked in smaller numbers), price and rating barely relate (0.29), and every column correlates perfectly with itself, as it should. The cell background is computed in Python from the correlation value — negative shades toward red, positive toward blue — there is no JavaScript on the page drawing that gradient.
The rest of the report — the sku and category columns rendered as chips
of their top five values, warehouse's two missing entries showing up as
2 (7.7%) nulls, the duplicate SKU-0006 row counted in the summary bar — is
exactly what running the command above produces.
- Not for out-of-core data. framescan loads the whole file into memory
with
polars, then pulls full columns out to Python lists to bin histograms and count top values. If the file fits in memory as a DataFrame, it fits; there is no streaming mode. - CSV schema inference reads the whole file.
infer_schema_length=Noneis passed topolars.read_csvso a numeric column with one stray value near the end of a large file doesn't get typed as text by mistake. That costs time on very large CSVs — correctness over speed. - Correlation is Pearson, nothing else. It catches linear relationships between numeric columns and nothing more; two columns tied together by a strong nonlinear relationship can still show a correlation near zero.
- NaN vs. null. A CSV read through the default parser never produces a
floating-point NaN — a literal
nanin a numeric column just gets typed as text. Parquet can carry a real NaN that is not a null, and framescan drops those from the numeric stats the same way it drops nulls, so they don't turn a column's mean into NaN. They are not counted separately from non-null values anywhere in the report. - No datetime-aware binning. Date and datetime columns are profiled as categorical — top five values by frequency — not bucketed into a time-based histogram.
- One file at a time. There's no way to point framescan at two files and ask what changed between them; see "Where this fits" below.
dsdiff and framescan both look at
tabular data and neither does the other's job. framescan describes one
file — what's in it, column by column. dsdiff compares two — the schema
changes and the column-level distribution drift between an old version of a
dataset and a new one. Profile a file with framescan when you're getting to
know it for the first time; reach for dsdiff once you have a second
version of it and want to know what moved.
MIT. See LICENSE.