Skip to content

Processors

Every name on this page is importable from sci_etl_core.processors.

sci_etl_core.processors.base

Processor

Bases: ABC

Contract for one post-processing step over the exported table.

process abstractmethod

process(frame: DataFrame) -> DataFrame

Transform a dataframe and return the result.

ProcessorChain

ProcessorChain(steps: list[Processor])

Bases: Processor

Run processor steps in order, each on the previous step's output.

process

process(frame: DataFrame) -> DataFrame

Return frame after every step, or frame itself when there are no steps.

sci_etl_core.processors.normalization

KeyNormalizer

Bases: ABC

Contract for mapping an entity's name to the key its duplicates share.

normalize abstractmethod

normalize(raw_value: Any) -> str

Produce a canonical key used to match duplicate records.

Returns "" when no key can be formed; callers treat an empty key as "no identity" and never match on it.

DefaultKeyNormalizer

Bases: KeyNormalizer

Domain-agnostic key: case, width, spacing, and punctuation differences are ignored.

normalize

normalize(raw_value: Any) -> str

Casefold and NFKC-fold a value, keeping letters, marks, numbers and symbols.

Missing values and containers such as lists or dicts cannot form a key and normalize to "".

NormalizationStep

NormalizationStep(
    key_column: str,
    normalizer: KeyNormalizer,
    output_column: str = "_norm_key",
)

Bases: Processor

Add output_column holding normalizer's key for each row's key_column value.

process

process(frame: DataFrame) -> DataFrame

Return a copy of frame with the key column added.

Raises:

Type Description
KeyError

frame has no key_column.

sci_etl_core.processors.dedup

NeighborMatcher

Bases: ABC

Contract for finding near-duplicate rows that do not share a normalized key.

find_matches abstractmethod

find_matches(
    frame: DataFrame, threshold: float
) -> list[tuple[int, int]]

Return (keep_index, drop_index) pairs for rows considered duplicates.

DeduplicationStep

DeduplicationStep(
    norm_key_column: str,
    matcher: NeighborMatcher | None = None,
    match_threshold: float = 0.0,
    mergeable_columns: list[str] | None = None,
    *,
    source_column: str | None = None,
    sources_column: str = "sources",
)

Bases: Processor

Collapse rows sharing a normalized key, then merge matched neighbors.

Rows with the same key become one row holding the first non-missing value of each column, ordered by key. Rows whose key is missing or empty carry no identity to match on, so each passes through as its own row, after the keyed rows, instead of being merged with every other keyless row.

Matched pairs only fill the kept row's gaps. When a pair names a row that was already merged away as the one to keep, its values flow into the row that absorbed it, so a chain of matches never discards data.

With source_column, such as the record_id column an exporter writes, each output row gains sources_column: the distinct non-empty source_column values of every input row merged into it, sorted and joined with "; ". The column says which papers a merged row came from, though each value in the row is still the first one found.

process

process(frame: DataFrame) -> DataFrame

sci_etl_core.processors.clustering

FeatureExtractor

Bases: ABC

Contract for choosing and scaling the numeric features that :class:ClusteringStep clusters on.

extract abstractmethod

extract(frame: DataFrame) -> tuple[ndarray, Index]

Return a feature matrix and the row index it corresponds to.

ClusteringStep

ClusteringStep(
    feature_extractor: FeatureExtractor,
    output_column: str = "cluster_id",
    eps: float = 1.0,
    min_samples: int = 2,
)

Bases: Processor

Label rows with a DBSCAN cluster id. Needs the cluster extra.

process

process(frame: DataFrame) -> DataFrame

Return a copy of frame with output_column added.

Noise points, rows the feature extractor leaves out, and every row when fewer than min_samples rows have features are labeled -1.

sci_etl_core.processors.quality

CompletenessStep

CompletenessStep(
    tracked_fields: list[str],
    output_column: str = "completeness_pct",
)

Bases: Processor

Add output_column with the percentage of tracked_fields each row fills.

process

process(frame: DataFrame) -> DataFrame

Return a copy of frame with the percentage, rounded to one decimal place.

Tracked fields missing from frame are ignored. When none is present, every row scores 0.0.

QualityFlagStep

QualityFlagStep(
    completeness_column: str = "completeness_pct",
    output_column: str = "quality_flag",
    review_threshold: float = 50.0,
)

Bases: Processor

Label each row from its completeness, as computed by :class:CompletenessStep.

process

process(frame: DataFrame) -> DataFrame

Return a copy of frame with the label column added.

A row at 100% is "Confirmed", one at or above review_threshold is "Needs Review", and any other row, including one without a completeness value, is "Low Confidence".

Raises:

Type Description
KeyError

frame has no completeness_column.

sci_etl_core.processors.validation

Violation dataclass

Violation(
    *,
    code: str,
    field: str | None,
    severity: Literal["error", "warning"],
    message: str,
)

One reason a validator gives for rejecting, or warning about, an entity.

code is a short machine-readable rule name such as "out-of-range", field is the entity key it concerns, or None when it concerns the entity as a whole, and message is a sentence a reviewer can read.

code instance-attribute

code: str

field instance-attribute

field: str | None

severity instance-attribute

severity: Literal['error', 'warning']

message instance-attribute

message: str

ValidationResult dataclass

ValidationResult(*, violations: tuple[Violation, ...] = ())

Every violation a validator found in one entity.

violations class-attribute instance-attribute

violations: tuple[Violation, ...] = ()

ok property

ok: bool

Whether no violation has severity == "error".

RecordValidator

Bases: ABC

Contract for a domain rule that decides whether an extracted entity is kept.

:meth:is_valid answers yes or no, and :meth:validate also says why. :class:~sci_etl_core.llm.extraction_async.AsyncLLMEntityExtractor calls :meth:validate. A validator that overrides only :meth:is_valid keeps working, because the default :meth:validate turns its rejection into one violation.

is_valid abstractmethod

is_valid(record: dict[str, Any]) -> bool

Return whether a raw extracted record should be kept.

validate

validate(record: dict[str, Any]) -> ValidationResult

Return every reason record is rejected.

The default wraps :meth:is_valid: a rejection becomes one :class:Violation with code "rejected", no field, and the validator's class name in the message. Override it to name the field and the rule. An override must reject exactly the records :meth:is_valid rejects.

KeywordExclusionValidator

KeywordExclusionValidator(
    key_field: str, forbidden_keywords: list[str]
)

Bases: RecordValidator

Reject an entity whose key_field is null-like or contains a forbidden keyword as whole words.

is_valid

is_valid(record: dict[str, Any]) -> bool

Return False for a missing, empty, or null-like key, or one containing a forbidden phrase.

Null-like values are null, none, unknown, n/a, and nan, in any case. Keys and keywords are split into runs of letters and runs of numbers after NFKC case folding, and a keyword matches only a contiguous sequence of whole runs, so "star" never matches "starburst".

validate

validate(record: dict[str, Any]) -> ValidationResult

Name the rule a rejected key broke: "null-key" or "forbidden-keyword".

NumericRangeValidator

NumericRangeValidator(
    field_ranges: dict[str, tuple[float, float]],
)

Bases: RecordValidator

Reject an entity whose value for a field lies outside that field's inclusive range.

is_valid

is_valid(record: dict[str, Any]) -> bool

Return False when a present value is out of range or cannot be read as a number.

A missing or None value passes, so completeness is left to other steps.

validate

validate(record: dict[str, Any]) -> ValidationResult

Report every field whose value is "not-a-number" or "out-of-range".

CompositeValidator

CompositeValidator(validators: list[RecordValidator])

Bases: RecordValidator

Keep an entity only when every validator keeps it.

is_valid

is_valid(record: dict[str, Any]) -> bool

Return whether all validators accept record, stopping at the first rejection.

validate

validate(record: dict[str, Any]) -> ValidationResult

Collect every validator's violations, in order, without stopping at the first rejection.

sci_etl_core.processors.shaping

ValueClipStep

ValueClipStep(bounds: Mapping[str, tuple[float, float]])

Bases: Processor

Clamp numeric columns into closed ranges during post-processing.

Each column named in bounds is converted to numbers, with values that are not numeric becoming NaN, and then clipped to (low, high). This is the post-processing counterpart of the CSV exporter's numeric_clip. Columns missing from the frame are skipped, and the input frame is never modified.

Store the bounds to apply.

Raises:

Type Description
ValueError

A bound is NaN, or its low end exceeds its high end.

process

process(frame: DataFrame) -> DataFrame

TableLayoutStep

TableLayoutStep(
    sort_by: Sequence[tuple[str, bool]] = (),
    leading_columns: Sequence[str] = (),
    hidden_prefixes: Iterable[str] = (),
    reset_index: bool = False,
)

Bases: Processor

Sort rows and arrange columns for a published table.

Rows are sorted by the sort_by columns that exist in the frame, each ascending or descending as paired, with missing values last and ties kept in their input order. Columns whose name starts with any of hidden_prefixes are dropped, then the leading_columns that exist come first in the given order, followed by the rest in their input order. The input frame is never modified.

Store the layout.

sort_by pairs each column with True for ascending order. reset_index renumbers the rows from 0 after sorting.

Raises:

Type Description
ValueError

A column appears twice in sort_by or in leading_columns, or a hidden prefix is empty, which would hide every column.

process

process(frame: DataFrame) -> DataFrame

The table sinks need the processors extra, and SqlTableSink and Plotly3DSink also need sql and viz when constructed. ScatterPlotConfig and render_scatter_3d are imported from sci_etl_core.processors.sinks.

sci_etl_core.processors.sinks

Destinations for a post-processed table.

Importing this module needs the processors extra (pandas). Constructing :class:SqlTableSink also needs the sql extra, and :class:Plotly3DSink the viz extra; each imports its library when constructed, so importing this module never fails for a missing optional library.

TableSink

Bases: ABC

A blocking destination for the table a :class:~sci_etl_core.processors.base.ProcessorChain produced.

Call :meth:write once the table is final, for example through asyncio.to_thread from async code.

write abstractmethod

write(frame: DataFrame) -> None

Store frame.

SqlTableSink

SqlTableSink(
    url: str, table_name: str, if_exists: str = "append"
)

Bases: TableSink

Write a table to a SQL database through SQLAlchemy. Needs the sql extra.

url is a synchronous SQLAlchemy database URL, such as "sqlite:///catalogue.db", and if_exists is passed to DataFrame.to_sql. Each :meth:write creates an engine, writes the table without its index in one transaction, and disposes of the engine.

Raises:

Type Description
ImportError

SQLAlchemy is not installed.

write

write(frame: DataFrame) -> None

ScatterPlotConfig dataclass

ScatterPlotConfig(
    x_column: str,
    y_column: str,
    z_column: str,
    color_column: str,
    size_column: str | None = None,
    hover_name_column: str | None = None,
    title: str = "3D Scatter",
    template: str = "plotly_dark",
    hover_data_columns: Sequence[str] = (),
    hover_template: str | None = None,
    color_continuous_scale: str
    | Sequence[str]
    | None = None,
    color_range: tuple[float, float] | None = None,
    color_label: str | None = None,
    marker: Mapping[str, Any] = dict(),
    layout: Mapping[str, Any] = dict(),
)

What a 3D scatter plot shows and how it looks.

hover_data_columns are passed to the figure as custom data, so hover_template can show them as %{customdata[0]}, %{customdata[1]}, and so on, in the order listed; the hover_name_column value is %{hovertext}. A numeric color_column is drawn with color_continuous_scale, such as "Viridis", over color_range when one is given, so colors mean the same in every export regardless of the values present. color_label names the color bar or legend. marker is applied to every trace's marker, such as {"sizemode": "diameter", "sizemin": 3}, and layout is applied to the figure layout last, overriding template and the default margins.

Raises:

Type Description
ValueError

color_range does not hold two finite numbers in increasing order, or hover_data_columns repeats a column.

x_column instance-attribute

x_column: str

y_column instance-attribute

y_column: str

z_column instance-attribute

z_column: str

color_column instance-attribute

color_column: str

size_column class-attribute instance-attribute

size_column: str | None = None

hover_name_column class-attribute instance-attribute

hover_name_column: str | None = None

title class-attribute instance-attribute

title: str = '3D Scatter'

template class-attribute instance-attribute

template: str = 'plotly_dark'

hover_data_columns class-attribute instance-attribute

hover_data_columns: Sequence[str] = ()

hover_template class-attribute instance-attribute

hover_template: str | None = None

color_continuous_scale class-attribute instance-attribute

color_continuous_scale: str | Sequence[str] | None = None

color_range class-attribute instance-attribute

color_range: tuple[float, float] | None = None

color_label class-attribute instance-attribute

color_label: str | None = None

marker class-attribute instance-attribute

marker: Mapping[str, Any] = field(default_factory=dict)

layout class-attribute instance-attribute

layout: Mapping[str, Any] = field(default_factory=dict)

Plotly3DSink

Plotly3DSink(config: ScatterPlotConfig, path: str | Path)

Bases: TableSink

Write a table as an interactive 3D scatter plot in a standalone HTML file. Needs the viz extra.

Rows missing an x, y, or z value are left out, and nothing is written when no row remains. The file is replaced atomically.

Raises:

Type Description
ImportError

Plotly is not installed.

write

write(frame: DataFrame) -> None

render_scatter_3d

render_scatter_3d(
    frame: DataFrame, config: ScatterPlotConfig
) -> str | None

Render frame as a standalone HTML 3D scatter plot, or return None when no row has x, y, and z.

Needs the viz extra.