Skip to content

Commit 62c73fa

Browse files
author
miranov25
committed
feat: Add flexible missing column handling to compress_columns
Add `on_missing` and `return_summary` parameters to compress_columns method for more flexible handling of missing columns during compression workflows. Changes: - Add `on_missing` parameter with three modes: * 'warn' (default): Skip missing columns with warning * 'error': Raise KeyError if any column missing (strict mode) * 'ignore': Skip missing columns silently - Add `return_summary` parameter to return compression results: * Returns dict with 'compressed' and 'skipped' column lists * Maintains backward compatibility (default returns self) - Implement smart column availability checking: * Checks DataFrame columns, aliases, and compression_info * Handles compressed columns that become aliases * Preserves state validation for already-tracked columns * Special handling for selective mode validation - Update docstring with new parameters, examples, and return types This enables progressive compression workflows where some columns may not exist yet, while maintaining strict validation when needed for production use cases. Fixes issues with: - Compressing columns across multiple stages - Handling optional calibration columns - Progressive workflow debugging with return_summary - Alias source compression All tests passing (61/61). ATO-628
1 parent 8874958 commit 62c73fa

1 file changed

Lines changed: 76 additions & 4 deletions

File tree

UTILS/dfextensions/AliasDataFrame/AliasDataFrame.py

Lines changed: 76 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -705,6 +705,8 @@ def _schemas_equal(self, schema1, schema2):
705705
return True
706706

707707
def compress_columns(self, compression_spec=None, columns=None, suffix='_c', drop_original=True,
708+
on_missing='warn', # NEW
709+
return_summary=False, # NEW
708710
measure_precision=False):
709711
"""
710712
Compress columns using bidirectional transforms with state management.
@@ -738,18 +740,28 @@ def compress_columns(self, compression_spec=None, columns=None, suffix='_c', dro
738740
Compressed column name suffix (default: '_c'). Ignored when reusing schema.
739741
drop_original : bool, optional
740742
Remove original column after compression (default: True)
743+
on_missing : {'warn', 'error', 'ignore'}, optional
744+
How to handle columns that don't exist in DataFrame (default: 'warn'):
745+
- 'warn': Skip missing columns with warning
746+
- 'error': Raise KeyError if any column missing
747+
- 'ignore': Skip missing columns silently
748+
return_summary : bool, optional
749+
Return summary dict with compressed/skipped columns (default: False)
741750
measure_precision : bool, optional
742751
Compute and store compression precision loss (default: False)
743752
744753
Returns
745754
-------
746-
self : AliasDataFrame
747-
For method chaining
755+
dict or self
756+
If return_summary=True: {'compressed': [...], 'skipped': [...]}
757+
Otherwise: self (for method chaining)
748758
749759
Raises
750760
------
751761
ValueError
752762
If invalid state transition, name collision, or missing schema
763+
KeyError
764+
If on_missing='error' and columns don't exist in DataFrame
753765
754766
Examples
755767
--------
@@ -765,6 +777,10 @@ def compress_columns(self, compression_spec=None, columns=None, suffix='_c', dro
765777
>>> # Direct compression (all columns in spec)
766778
>>> adf.compress_columns(spec) # Compress everything
767779
780+
>>> # Handle missing columns flexibly
781+
>>> adf.compress_columns(spec, on_missing='ignore') # Skip silently
782+
>>> result = adf.compress_columns(spec, return_summary=True) # Get summary
783+
768784
Notes
769785
-----
770786
- State transitions: SCHEMA_ONLY → COMPRESSED, DECOMPRESSED → COMPRESSED
@@ -779,7 +795,7 @@ def compress_columns(self, compression_spec=None, columns=None, suffix='_c', dro
779795
cols_to_process = [
780796
col for col in self.compression_info
781797
if col != "__meta__" and
782-
self.compression_info[col].get('state') in (CompressionState.SCHEMA_ONLY, CompressionState.DECOMPRESSED)
798+
self.compression_info[col].get('state') in (CompressionState.SCHEMA_ONLY, CompressionState.DECOMPRESSED)
783799
]
784800
if not cols_to_process:
785801
return self # Nothing to do
@@ -818,6 +834,51 @@ def compress_columns(self, compression_spec=None, columns=None, suffix='_c', dro
818834
"- compress_columns(spec, columns=[...]) for selective compression"
819835
)
820836

837+
# === NEW: Filter columns based on on_missing parameter ===
838+
# Special handling for selective mode to preserve original validation behavior
839+
if schema_mode != 'selective':
840+
import warnings
841+
842+
# Check which columns exist in DataFrame, aliases, OR are already tracked
843+
existing_in_df = set(self.df.columns)
844+
existing_in_aliases = set(self.aliases.keys())
845+
tracked_in_schema = set(self.compression_info.keys()) - {'__meta__'}
846+
847+
# A column is "available" if:
848+
# 1. It exists physically in the DataFrame, OR
849+
# 2. It exists as an alias (compressed columns become aliases), OR
850+
# 3. It's already tracked in compression_info (for state validation)
851+
available_cols = [col for col in cols_to_process
852+
if col in existing_in_df or
853+
col in existing_in_aliases or
854+
col in tracked_in_schema]
855+
856+
# A column is "missing" only if it's nowhere: not in df, not an alias, not tracked
857+
missing_cols = [col for col in cols_to_process
858+
if col not in existing_in_df and
859+
col not in existing_in_aliases and
860+
col not in tracked_in_schema]
861+
862+
# Handle missing columns according to on_missing mode
863+
if missing_cols:
864+
if on_missing == 'error':
865+
raise KeyError(
866+
f"Missing columns: {missing_cols}\n"
867+
f"Available in DataFrame: {list(existing_in_df)[:20]}...\n"
868+
f"Available as aliases: {list(existing_in_aliases)[:20]}..."
869+
)
870+
elif on_missing == 'warn':
871+
warnings.warn(
872+
f"Skipping missing columns: {missing_cols}\n"
873+
f"Hint: Use columns= to restrict, or on_missing='error' for strict mode."
874+
)
875+
# else: on_missing == 'ignore', do nothing
876+
877+
# Update cols_to_process to only include available columns
878+
cols_to_process = available_cols
879+
# else: In selective mode, don't filter - let existing validation handle it
880+
# === END NEW CODE ===
881+
821882
for orig_col in cols_to_process:
822883
# Get config (from spec or existing schema)
823884
if schema_mode == 'reuse':
@@ -995,8 +1056,19 @@ def compress_columns(self, compression_spec=None, columns=None, suffix='_c', dro
9951056
if precision_info is not None:
9961057
self.compression_info[orig_col]['precision'] = precision_info
9971058

998-
return self
1059+
# === NEW: Return summary if requested ===
1060+
if return_summary:
1061+
compressed_cols = [col for col in available_cols
1062+
if schema_mode != 'define' and
1063+
col in self.compression_info and
1064+
self.compression_info[col].get('state') == CompressionState.COMPRESSED]
1065+
return {
1066+
'compressed': compressed_cols,
1067+
'skipped': missing_cols
1068+
}
1069+
# === END NEW CODE ===
9991070

1071+
return self
10001072
def _validate_compressed_col_name(self, orig_col, compressed_col):
10011073
"""
10021074
Validate compressed column name doesn't conflict.

0 commit comments

Comments
 (0)