44
55import dataclasses
66import time
7- from collections .abc import Mapping
7+ from collections .abc import Callable , Mapping
88
99import pandas as pd
1010
2323from .steps .prune import drop_constant_columns , drop_empty_columns , drop_empty_rows
2424from .steps .strings import clean_strings
2525
26+ ProgressCallback = Callable [[dict [str , object ]], None ]
27+
2628
2729def _validate_input (df : object , config : CleanConfig ) -> pd .DataFrame :
2830 if isinstance (df , pd .Series ):
@@ -42,9 +44,27 @@ def _validate_input(df: object, config: CleanConfig) -> pd.DataFrame:
4244 return frame
4345
4446
47+ def _emit_progress (
48+ callback : ProgressCallback | None ,
49+ step : str ,
50+ status : str ,
51+ frame : pd .DataFrame ,
52+ ) -> None :
53+ if callback is None :
54+ return
55+ callback (
56+ {
57+ "step" : step ,
58+ "status" : status ,
59+ "rows" : len (frame ),
60+ "columns" : frame .shape [1 ],
61+ }
62+ )
63+
64+
4565def run_pipeline (
4666 df : pd .DataFrame ,
47- config : CleanConfig ,
67+ def run_pipeline ( # noqa: PLR0915 - fixed-order pipeline orchestration
4868 * ,
4969 memory : object | None = None ,
5070 profile : object | None = None ,
@@ -69,6 +89,8 @@ def run_pipeline(
6989 forwarded so the profile backend can replay learned value maps.
7090 """
7191 df = _validate_input (df , config )
92+ progress_callback = config .progress_callback
93+ _emit_progress (progress_callback , "input" , "after" , df )
7294 report = CleanReport (
7395 rows_before = len (df ),
7496 cols_before = df .shape [1 ],
@@ -84,10 +106,12 @@ def run_pipeline(
84106 from .context import apply_policy_to_config # noqa: PLC0415
85107
86108 config = apply_policy_to_config (config , df = df , report = report )
109+ _emit_progress (progress_callback , "context" , "after" , df )
87110
88111 out = df .copy (deep = False ) if config .preserve_original else df
89112 if config .column_names :
90113 out = normalize_column_names (out , report )
114+ _emit_progress (progress_callback , "column_names" , "after" , out )
91115
92116 # Hard protected-column guard (context policy / mutable=False): fold the
93117 # protected set into preserve_columns so drop/impute logic honors it, and
@@ -110,44 +134,60 @@ def run_pipeline(
110134 else :
111135 guard_snapshot = {}
112136 out = clean_strings (out , config , report )
137+ _emit_progress (progress_callback , "strings" , "after" , out )
113138 if config .drop_empty_columns :
114139 out = drop_empty_columns (out , report , config )
140+ _emit_progress (progress_callback , "empty_columns" , "after" , out )
115141 if config .drop_empty_rows :
116142 out = drop_empty_rows (out , report )
143+ _emit_progress (progress_callback , "empty_rows" , "after" , out )
117144 if config .fix_dtypes :
118145 out = fix_dtypes (out , config , report )
146+ _emit_progress (progress_callback , "dtypes" , "after" , out )
119147 if config .drop_constant_columns :
120148 out = drop_constant_columns (out , config , report )
149+ _emit_progress (progress_callback , "constant_columns" , "after" , out )
121150 if config .drop_duplicates :
122151 out = drop_duplicate_rows (out , config , report )
152+ _emit_progress (progress_callback , "duplicates" , "after" , out )
123153 if config .semantic_enabled :
124154 # Semantic cleaning runs after representation repair and before the
125155 # statistical engine, so missing/outlier logic sees repaired values.
126156 # Lazily imported to keep ``import freshdata`` light.
127157 from .semantic .apply import run_semantic # noqa: PLC0415
128158
129159 out = run_semantic (out , config , report , memory = memory , profile = profile )
160+ _emit_progress (progress_callback , "semantic" , "after" , out )
130161 if config .engine_mode is not None :
131162 cache = build_engine_cache (out , config )
163+ _emit_progress (progress_callback , "engine_cache" , "after" , out )
132164 out = auto_missing (
133165 out , config , report , contexts = cache .contexts , numeric_corr = cache .numeric_corr
134166 )
167+ _emit_progress (progress_callback , "engine_missing" , "after" , out )
135168 out = auto_outliers (out , config , report , contexts = cache .contexts )
169+ _emit_progress (progress_callback , "engine_outliers" , "after" , out )
136170 out = impute_missing (out , config , report )
171+ _emit_progress (progress_callback , "missing" , "after" , out )
137172 out = handle_outliers (out , config , report )
173+ _emit_progress (progress_callback , "outliers" , "after" , out )
138174 out = optimize_memory (out , config , report )
175+ _emit_progress (progress_callback , "memory" , "after" , out )
139176 if guard_snapshot :
140177 # Physical byte-identity check, before reset_index so row survivors
141178 # can still be aligned by their original index labels.
142179 verify_protected (out , guard_snapshot , report )
180+ _emit_progress (progress_callback , "protected_columns" , "after" , out )
143181 if config .reset_index :
144182 out = out .reset_index (drop = True )
183+ _emit_progress (progress_callback , "index" , "after" , out )
145184
146185 report .rows_after = len (out )
147186 report .cols_after = out .shape [1 ]
148187 report .memory_after = memory_bytes (out )
149188 report .missing_after = int (out .isna ().sum ().sum ())
150189 report .duration_seconds = time .perf_counter () - started
190+ _emit_progress (progress_callback , "complete" , "after" , out )
151191 return out , report
152192
153193
0 commit comments