Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions src/datasure/processing/prep.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,8 @@ def execute(
"action": PrepActions.remove_column.value,
"column_names": None,
"affected_count": len(existing_columns),
"remaining_count": results.width,
"remaining_rows": results.height,
"remaining_columns": results.width,
"value": None,
"method": None,
"source_columns": existing_columns,
Expand Down Expand Up @@ -240,7 +241,8 @@ def execute(
"action": PrepActions.remove_row.value,
"column_names": None,
"affected_count": data.height - results.height,
"remaining_count": results.height,
"remaining_rows": results.height,
"remaining_columns": results.width,
"value": value,
"method": method,
"source_columns": source_columns,
Expand Down Expand Up @@ -442,7 +444,8 @@ def execute(
"action": "transform column(s)",
"column_names": None,
"affected_count": affected_count,
"remaining_count": None,
"remaining_rows": result_data.height,
"remaining_columns": result_data.width,
"value": prep_args.value,
"method": prep_args.method,
"source_columns": source_columns,
Expand Down Expand Up @@ -738,7 +741,8 @@ def execute(self, data: pl.DataFrame, prep_args: PrepActionResult) -> pl.DataFra
"action": PrepActions.add_column.value,
"column_names": new_col_name,
"affected_count": 1,
"remaining_count": results.width,
"remaining_rows": results.height,
"remaining_columns": results.width,
"value": value_spec,
"method": method,
"source_columns": source_columns,
Expand Down
104 changes: 90 additions & 14 deletions src/datasure/utils/prep_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ class PrepActionResult:
column_names: str | list[str] | None = None
affected_count: int | None = None
remaining_count: int | None = None
remaining_rows: int | None = None
remaining_columns: int | None = None
value: str | list | None = None
method: str | None = None
source_columns: str | list[str] | None = None
Expand Down Expand Up @@ -215,6 +217,79 @@ def _pluralize(count: int | None, singular: str, plural: str | None = None) -> s
plural = f"{singular}s"
return singular if count == 1 else plural

@classmethod
def _format_remaining(
cls, remaining_rows: int | None, remaining_columns: int | None
) -> str:
"""Build the standard 'dataset now has N rows and M columns' clause."""
parts = []
if remaining_rows is not None:
parts.append(f"{remaining_rows} {cls._pluralize(remaining_rows, 'row')}")
if remaining_columns is not None:
parts.append(
f"{remaining_columns} {cls._pluralize(remaining_columns, 'column')}"
)
if not parts:
return ""
return f"Dataset now has {' and '.join(parts)}."

@classmethod
def _with_remaining(cls, message: str, result: PrepActionResult) -> str:
"""Append the standard remaining rows/columns clause to a message."""
remaining = cls._format_remaining(
result.remaining_rows, result.remaining_columns
)
return f"{message} {remaining}".rstrip() if remaining else message

@staticmethod
def _quote_scalar(value: object) -> str:
"""Quote string values for display; leave other types as-is."""
return f'"{value}"' if isinstance(value, str) else str(value)

@classmethod
def _format_row_condition_detail(
cls,
source_columns: str | list[str] | None,
condition: str | None,
value: object,
) -> str:
"""Build a human-readable 'where <column> <condition> <value>' clause."""
column_display = cls._format_column_names(source_columns)
if not condition:
return "matching the specified criteria"

if condition in (
PrepRowConditions.missing.value,
PrepRowConditions.not_missing.value,
):
value_text = ""
elif condition in (
PrepRowConditions.between.value,
PrepRowConditions.not_between.value,
):
values = value if isinstance(value, list) else [value, value]
value_text = (
f" {cls._quote_scalar(values[0])} and {cls._quote_scalar(values[1])}"
)
elif isinstance(value, list):
value_text = " " + ", ".join(cls._quote_scalar(v) for v in value)
elif value is not None:
value_text = f" {cls._quote_scalar(value)}"
else:
value_text = ""

if column_display:
return f"where {column_display} {condition}{value_text}"
return f"where {condition}{value_text}"

@staticmethod
def _format_row_index_detail(value: object) -> str:
"""Build a human-readable clause describing removed row indexes."""
if not value:
return "at the specified row index"
items = value if isinstance(value, list) else [value]
return f"at row index {', '.join(str(v) for v in items)}"

# Main Actions
@classmethod
def transform_columns(cls, result: PrepActionResult) -> str:
Expand All @@ -223,10 +298,11 @@ def transform_columns(cls, result: PrepActionResult) -> str:
row_text = cls._pluralize(result.affected_count, "row")
method = result.method or "unknown method"
affected_count = result.affected_count or 0
return (
message = (
f"βœ“ Column transformation applied. {column_display} updated using "
f"{method}. {affected_count} {row_text} affected."
)
return cls._with_remaining(message, result)

@classmethod
def add_new_column(cls, result: PrepActionResult) -> str:
Expand All @@ -236,12 +312,11 @@ def add_new_column(cls, result: PrepActionResult) -> str:
if result.source_columns
else "specified parameters"
)
column_text = cls._pluralize(result.remaining_count, "column")
return (
message = (
f"βœ“ New column {cls._format_column_names(result.column_names)} added. "
f"Created using {result.method} from {source_display}. "
f"Your dataset now has {result.remaining_count} {column_text}."
f"Created using {result.method} from {source_display}."
)
return cls._with_remaining(message, result)

@classmethod
def remove_columns(cls, result: PrepActionResult) -> str:
Expand All @@ -258,26 +333,27 @@ def remove_columns(cls, result: PrepActionResult) -> str:
else (len(removed_columns) if isinstance(removed_columns, list) else 1)
)
column_text = cls._pluralize(column_count, "column")
remaining_text = cls._pluralize(result.remaining_count, "column")
column_display = cls._format_column_names(removed_columns)
message = (
f"βœ“ {column_count} {column_text} removed. {column_display} deleted from "
f"your dataset. {result.remaining_count} {remaining_text} remaining."
"your dataset."
)
if result.failed_count:
message = f"{message} {result.additional_info}"
return message
return cls._with_remaining(message, result)

@classmethod
def remove_rows(cls, result: PrepActionResult) -> str:
"""Generate message for removing rows."""
row_text = cls._pluralize(result.affected_count, "row")
remaining_text = cls._pluralize(result.remaining_count, "row")
method_text = result.method if result.method else "specified criteria"
return (
f"βœ“ {result.affected_count} {row_text} removed. Deleted rows {method_text}. "
f"{result.remaining_count} {remaining_text} remaining in your dataset."
)
if result.method == PrepMethods.row_index.value:
detail = cls._format_row_index_detail(result.value)
else:
detail = cls._format_row_condition_detail(
result.source_columns, result.condition, result.value
)
message = f"βœ“ {result.affected_count} {row_text} removed {detail}."
return cls._with_remaining(message, result)

# Add Column Methods
@classmethod
Expand Down
84 changes: 68 additions & 16 deletions tests/utils/test_prep_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,8 @@ def test_transform_columns_message(self):
source_columns=["column1"],
affected_count=100,
method="trim",
remaining_rows=500,
remaining_columns=12,
)

message = PrepConfirmationMessages.transform_columns(result)
Expand All @@ -412,6 +414,7 @@ def test_transform_columns_message(self):
assert '"column1"' in message
assert "trim" in message
assert "100 rows affected" in message
assert "500 rows and 12 columns" in message

def test_transform_columns_message_no_method(self):
"""Test transform_columns message with no method."""
Expand All @@ -432,7 +435,8 @@ def test_add_new_column_message(self):
result = PrepActionResult(
action="add new column",
column_names="new_col",
remaining_count=10,
remaining_rows=200,
remaining_columns=10,
method="constant",
source_columns=["source_col"],
)
Expand All @@ -442,14 +446,15 @@ def test_add_new_column_message(self):
assert "βœ“ New column" in message
assert '"new_col"' in message
assert "constant" in message
assert "10 columns" in message
assert "200 rows and 10 columns" in message

def test_add_new_column_message_no_source(self):
"""Test add_new_column message with no source columns."""
result = PrepActionResult(
action="add new column",
column_names="new_col",
remaining_count=5,
remaining_rows=5,
remaining_columns=5,
method="index",
source_columns=None,
)
Expand All @@ -463,33 +468,38 @@ def test_remove_columns_message(self):
result = PrepActionResult(
action="remove column(s)",
source_columns=["col1", "col2"],
remaining_count=8,
remaining_rows=50,
remaining_columns=8,
)

message = PrepConfirmationMessages.remove_columns(result)

assert "βœ“ 2 columns removed" in message
assert '"col1", "col2"' in message
assert "8 columns remaining" in message
assert "50 rows and 8 columns" in message

def test_remove_columns_message_single_column(self):
"""Test remove_columns message with single column."""
result = PrepActionResult(
action="remove column(s)", source_columns="single_col", remaining_count=5
action="remove column(s)",
source_columns="single_col",
remaining_rows=50,
remaining_columns=5,
)

message = PrepConfirmationMessages.remove_columns(result)

assert "βœ“ 1 column removed" in message
assert "5 columns remaining" in message
assert "50 rows and 5 columns" in message

def test_remove_columns_message_partial_failure(self):
"""Test remove_columns message notes columns skipped as missing."""
result = PrepActionResult(
action="remove column(s)",
source_columns=["col1", "col2"],
affected_count=2,
remaining_count=8,
remaining_rows=50,
remaining_columns=8,
failed_count=2,
additional_info="Columns not found and skipped: ['col3', 'col4']",
)
Expand All @@ -500,25 +510,67 @@ def test_remove_columns_message_partial_failure(self):
assert '"col1", "col2"' in message
assert "Columns not found and skipped: ['col3', 'col4']" in message

def test_remove_rows_message(self):
"""Test remove_rows message generation."""
def test_remove_rows_message_by_condition(self):
"""Test remove_rows message states the column, condition, and value."""
result = PrepActionResult(
action="remove row(s)",
affected_count=25,
remaining_count=75,
remaining_rows=75,
remaining_columns=10,
method="by condition",
source_columns=["age"],
condition="value is greater than",
value=65,
)

message = PrepConfirmationMessages.remove_rows(result)

assert "βœ“ 25 rows removed" in message
assert "by condition" in message
assert "75 rows remaining" in message
assert '"age" value is greater than 65' in message
assert "75 rows and 10 columns" in message

def test_remove_rows_message_by_condition_between(self):
"""Test remove_rows message formats a between-condition value range."""
result = PrepActionResult(
action="remove row(s)",
affected_count=5,
remaining_rows=95,
remaining_columns=10,
method="by condition",
source_columns=["age"],
condition="value is between",
value=[18, 65],
)

message = PrepConfirmationMessages.remove_rows(result)

assert '"age" value is between 18 and 65' in message

def test_remove_rows_message_no_method(self):
"""Test remove_rows message with no method."""
def test_remove_rows_message_by_index(self):
"""Test remove_rows message for row-index removal states the indexes."""
result = PrepActionResult(
action="remove row(s)", affected_count=10, remaining_count=90, method=None
action="remove row(s)",
affected_count=3,
remaining_rows=97,
remaining_columns=10,
method="by row index",
value=["1", "5", "10"],
)

message = PrepConfirmationMessages.remove_rows(result)

assert "βœ“ 3 rows removed" in message
assert "at row index 1, 5, 10" in message
assert "97 rows and 10 columns" in message

def test_remove_rows_message_no_condition(self):
"""Test remove_rows message with no condition falls back gracefully."""
result = PrepActionResult(
action="remove row(s)",
affected_count=10,
remaining_rows=90,
remaining_columns=10,
method=None,
)

message = PrepConfirmationMessages.remove_rows(result)
Expand Down
Loading