Skip to content

jmweb-org/framescan

Repository files navigation

framescan

CI Python License: MIT

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.

Install

$ uv tool install git+https://github.com/jmweb-org/framescan
$ pipx install git+https://github.com/jmweb-org/framescan   # if you use pipx

Not on PyPI. The only dependency is polars, which also gives Parquet support for free — pl.read_parquet doesn't need pyarrow installed.

Usage

$ framescan data.csv -o report.html
usage: 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.

Example

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.

Limitations

  • 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=None is passed to polars.read_csv so 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 nan in 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.

Where this fits

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.

License

MIT. See LICENSE.

About

Profile a CSV or Parquet file and write a static HTML report.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages