Post-processing and visualization¶
The CSV exporter¶
The pipeline's exporter receives each record's entities as a list[dict], and
AsyncCsvUpsertExporter is the built-in exporter that accepts that shape. It
keeps one row per normalized key: later records only fill empty cells, value
columns are converted to floats (anything non-numeric becomes empty), and
optional numeric_clip bounds clamp them.
The exporter reads the existing file (which must be UTF-8) before its first write. If that read fails, for example because of a different encoding or a malformed row, the export raises and the file is left untouched instead of being overwritten. On Windows, a write is retried for about a second and a half while another program holds the file open.
Keys come straight from LLM output, so a key a spreadsheet would run as a
formula (starting with =, +, -, @, a tab, or a carriage return) is
written with a leading apostrophe. The exporter strips it again when it
reloads the file; other tools reading the CSV see it. Pass
escape_formulas=False to write keys unchanged.
Cleaning and plotting¶
Cleanup, scoring, and plots are a separate step over a DataFrame:
import asyncio
import pandas as pd
from sci_etl_core import AsyncPlotly3DExporter, ScatterPlotConfig
from sci_etl_core.processors import (
CompletenessStep,
DeduplicationStep,
DefaultKeyNormalizer,
NormalizationStep,
ProcessorChain,
QualityFlagStep,
)
frame = pd.read_csv("results.csv", dtype={"name": str})
clean = ProcessorChain(
[
NormalizationStep("name", DefaultKeyNormalizer()), # adds _norm_key
DeduplicationStep("_norm_key"), # one row per key
CompletenessStep(["value_a", "value_b"]), # adds completeness_pct
QualityFlagStep(), # adds quality_flag
]
).process(frame)
plot = AsyncPlotly3DExporter(
ScatterPlotConfig(
x_column="value_a",
y_column="value_b",
z_column="completeness_pct",
color_column="quality_flag",
hover_name_column="name",
title="Corpus overview",
)
)
asyncio.run(plot.export(clean, "overview.html"))
clean then looks like this:
| _norm_key | name | value_a | value_b | completeness_pct | quality_flag |
|---|---|---|---|---|---|
| objecta | Object A | 12.4 | 0.87 | 100.0 | Confirmed |
| objectb | Object B | 9.1 | 50.0 | Needs Review | |
| objectc | Object C | 0.0 | Low Confidence |
The Plotly exporter drops rows that are missing any axis value.
Styling the plot¶
ScatterPlotConfig also controls hover text and colors:
from sci_etl_core import ScatterPlotConfig
config = ScatterPlotConfig(
x_column="x",
y_column="y",
z_column="z",
color_column="dark_matter_fraction",
size_column="radius",
hover_name_column="name",
hover_data_columns=["constellation", "distance"],
hover_template=(
"<b>%{hovertext}</b><br>Constellation: %{customdata[0]}<br>"
"Distance: %{customdata[1]} Mpc<extra></extra>"
),
color_continuous_scale="Viridis",
color_range=(0.0, 1.0),
color_label="DM fraction",
marker={"sizemode": "diameter", "sizemin": 3},
layout={"paper_bgcolor": "#0b0f19", "scene": {"aspectmode": "cube"}},
)
hover_data_columnsbecome%{customdata[0]},%{customdata[1]}, and so on inhover_template, in the order listed; the hover name is%{hovertext}.color_continuous_scaleandcolor_rangefix the colors of a numeric color column, so a value has the same color in every export.color_labeltitles the color bar, or the legend for a categorical color column.markerupdates every trace's markers, andlayoutis applied to the figure layout last, so it overridestemplateand the default margins.
Other building blocks¶
ClusteringStep(feature_extractor)runs DBSCAN over features returned by your ownFeatureExtractor.ValueClipStep(bounds)clamps numeric columns into ranges, such as{"fraction": (0.0, 1.0)}, turning values that aren't numbers into empty cells. It does during post-processing what the CSV exporter'snumeric_clipdoes during export.TableLayoutStep(sort_by, leading_columns, hidden_prefixes)prepares a table for publishing: it sorts rows by(column, ascending)pairs with missing values last, drops helper columns such as_norm_keyby prefix, and moves theleading_columnsto the front.- Record validators check individual entity dicts:
NumericRangeValidator,KeywordExclusionValidator, andCompositeValidator. Pass one toAsyncLLMEntityExtractor(validator=...)to drop invalid entities before export; each rejection is logged through itslogger, labelled by thelabel_fieldvalue when you name one:
from sci_etl_core import AsyncLLMEntityExtractor
from sci_etl_core.processors import CompositeValidator, KeywordExclusionValidator, NumericRangeValidator
extractor = AsyncLLMEntityExtractor(
llm_client,
system_prompt,
validator=CompositeValidator(
[
KeywordExclusionValidator("name", ["simulation", "mock"]),
NumericRangeValidator({"ra": (0.0, 360.0)}),
]
),
logger=print,
label_field="name",
)
AsyncSqlTableExporter(table_name) writes a DataFrame to a SQLAlchemy
async URL, e.g.
await AsyncSqlTableExporter("entities").export(clean, "sqlite+aiosqlite:///results.db").
Like the Plotly exporter, it takes a DataFrame, so use it after
post-processing rather than as the pipeline's exporter.