Praneshrajan15's picture
Deploy DataForge playground API
13fe504 verified
Raw
History Blame Contribute Delete
4.51 kB
"""Repairer for missing values via functional-dependency-derivable fills only.
A missing value cannot be invented. This repairer fills a missing cell only
when its value is *derivable* from a declared functional dependency: if some FD
``determinant -> this_column`` holds and another row shares the same determinant
values with a known (non-missing) value in this column, that value is proposed.
Otherwise it abstains (returns ``None``) - the missing value stays
detection-only. Every proposal still passes the SMT verifier and constitution.
"""
from __future__ import annotations
from typing import Any
from dataforge.detectors.base import Issue, Schema
from dataforge.detectors.missing_value import is_missing
from dataforge.repairers.base import ProposedFix, RetryContext
from dataforge.table import TableLike, cell_value, column_names, row_count
from dataforge.transactions.txn import CellFix
class MissingValueRepairer:
"""Fills missing values only when a functional dependency makes them derivable."""
def propose(
self,
issue: Issue,
df: TableLike,
schema: Schema | None,
retry_context: RetryContext | None = None,
) -> ProposedFix | None:
"""Propose an FD-derived fill, or abstain when the value is not derivable."""
del retry_context
if issue.issue_type != "missing_value" or schema is None:
return None
fds = getattr(schema, "functional_dependencies", None)
if not fds:
return None
old_value = cell_value(df, issue.row, issue.column)
columns = set(column_names(df))
derived = self._derive_from_fds(df, issue.row, issue.column, fds, columns)
if derived is None or is_missing(derived) or derived == old_value:
return None
return ProposedFix(
fix=CellFix(
row=issue.row,
column=issue.column,
old_value=old_value,
new_value=derived,
detector_id="missing_value",
operation="update",
),
reason=(
f"Filled missing value in '{issue.column}' from a functional "
f"dependency match -> '{derived}'."
),
confidence=issue.confidence,
provenance="deterministic",
)
def _derive_from_fds(
self,
df: TableLike,
row: int,
column: str,
fds: Any,
columns: set[str],
) -> str | None:
"""Return a uniquely FD-derived value for the cell, or None."""
candidates: set[str] = set()
for fd in fds:
dependent = str(getattr(fd, "dependent", ""))
determinant = [str(c) for c in getattr(fd, "determinant", [])]
if dependent != column or not determinant:
continue
if any(col not in columns for col in determinant):
continue
key = self._row_key(df, row, determinant)
if key is None:
continue # determinant itself has a missing value; cannot match
match = self._lookup(df, determinant, key, column)
if match is not None:
candidates.add(match)
# Only fill when every matching FD agrees on a single value.
return next(iter(candidates)) if len(candidates) == 1 else None
@staticmethod
def _row_key(df: TableLike, row: int, determinant: list[str]) -> tuple[str, ...] | None:
"""Return the determinant key for a row, or None if any part is missing."""
values: list[str] = []
for col in determinant:
value = cell_value(df, row, col)
if is_missing(value):
return None
values.append(value)
return tuple(values)
@staticmethod
def _lookup(
df: TableLike, determinant: list[str], key: tuple[str, ...], column: str
) -> str | None:
"""Find a non-missing dependent value for another row with the same key."""
found: set[str] = set()
for other in range(row_count(df)):
if tuple(cell_value(df, other, col) for col in determinant) != key:
continue
value = cell_value(df, other, column)
if not is_missing(value):
found.add(value)
return next(iter(found)) if len(found) == 1 else None