1717import polars as pl
1818import polars .selectors as cs
1919import polars_config_meta
20+ import polars_permute
2021from enum import Enum , auto
2122import logging
2223import copy
@@ -136,6 +137,7 @@ def startlog(self, clone=False) -> pl.DataFrame:
136137 :return: The same DataFrame for piping.
137138 :rtype: pl.DataFrame
138139 """
140+
139141 self ._df .config_meta .set (initial_shape = self ._df .shape , start_time = time .perf_counter_ns ())
140142 if clone :
141143 self ._df .config_meta .set (initial_df = self ._df .clone ())
@@ -243,12 +245,29 @@ def crosstab(
243245
244246 return ct .with_columns (values / options [perc ] * 100 )
245247
246- def join (self , df2 , * args , keep_source : bool = False , log_col_changes = False , ** kwargs ):
248+ def join (self , df2 :pl .DataFrame , * args , keep_row_index : bool = False , log_col_changes = False , ** kwargs ):
249+ """
250+ Wrapper around `pl.DataFrame.join` to log join operations.
251+
252+ :param df2: The dataframe on the right of the join
253+ :type df2: pl.DataFrame
254+ :param *args: Additional position arguments passed to `pl.DataFrame.join`
255+ :type *args: Any
256+ :param keep_row_index: Whether to keep columns indicating the row index of the source table in the output table
257+ :type keep_row_index: bool
258+ :param log_col_changes: Whether to log column changes
259+ :type log_col_changes: bool
260+ :param *kwargs: Additional keyword arguments passed to `pl.DataFrame.join`
261+ :type *kwargs: Any
262+ :return: The joined DataFrame
263+ :rtype: pl.DataFrame
264+ """
265+
247266 left_col = "source_left"
248267 right_col = "source_right"
249268 # Get DataFrame to join, add source column
250- df1 = self ._df .with_columns ( pl . lit ( True ). alias ( left_col ) )
251- df2 = df2 .with_columns ( pl . lit ( True ). alias ( right_col ) )
269+ df1 = self ._df .with_row_index ( left_col )
270+ df2 = df2 .with_row_index ( right_col )
252271 # Join DataFrames
253272 joined = df1 .join (df2 , * args , ** kwargs )
254273 n_rows_joined = joined .shape [0 ]
@@ -269,21 +288,21 @@ def join(self, df2, *args, keep_source: bool = False, log_col_changes=False, **k
269288 joined = joined .drop ([left_col ])
270289 return joined
271290 # Detect how many rows in the output table are present in the input tables
272- joined = joined .with_columns (
273- pl . col ([ left_col , right_col ]). fill_null ( False )
274- )
275- n_both = joined . filter ( pl . col ( left_col ), pl . col ( right_col )). shape [ 0 ]
291+ joined_both = joined .filter ( ~ pl . col ( left_col ). is_null (), ~ pl . col ( right_col ). is_null ())
292+ n_both = joined_both . shape [ 0 ]
293+ n_left_dups = joined_both . get_column ( left_col ). is_duplicated (). sum ( )
294+ n_right_dups = joined_both . get_column ( right_col ). is_duplicated (). sum ()
276295 n_left_only = joined .filter (
277- pl .col (left_col ), ~ pl .col (right_col )
296+ ~ pl .col (left_col ). is_null (), pl .col (right_col ). is_null ( )
278297 ).shape [0 ]
279298 n_right_only = joined .filter (
280- ~ pl .col (left_col ), pl .col (right_col )
299+ pl .col (left_col ). is_null (), ~ pl .col (right_col ). is_null ( )
281300 ).shape [0 ]
282301 # Log rows information
283302 msg = f"Total rows in output table: { n_rows_joined :,d} \n "
284- msg += f"\t From left: { n_left_only :,d} ({ n_left_only / n_rows_joined :.2%} )\n "
285- msg += f"\t From right: { n_right_only :,d} ({ n_right_only / n_rows_joined :.2%} )\n "
286- msg += f"\t From both: { n_both :,d} ({ n_both / n_rows_joined :.2%} )\n "
303+ msg += f"From left only : { n_left_only :,d } / { n_rows_joined :,d} ({ n_left_only / n_rows_joined :.2%} )\n "
304+ msg += f"From right only : { n_right_only :,d } / { n_rows_joined :,d} ({ n_right_only / n_rows_joined :.2%} )\n "
305+ msg += f"From both: { n_both :,d} / { n_rows_joined :,d } ({ n_both / n_rows_joined :.2%} ) (left dups { n_left_dups } , right dups { n_right_dups } )\n "
287306 # Detect added and removed columns
288307 if log_col_changes :
289308 cols_out = set (joined .columns )
@@ -292,21 +311,11 @@ def join(self, df2, *args, keep_source: bool = False, log_col_changes=False, **k
292311 msg += f"Columns in output table not in left table: { cols_out - cols_left } )\n "
293312 msg += f"Columns in output table not in right table: { cols_out - cols_right } )"
294313 logger .info (msg )
295- # Add a column that indicate where that row comes from
296- if keep_source :
297- joined = joined .with_columns (
298- pl .when (pl .col .source_left & pl .col .source_right )
299- .then (pl .lit ("both" ))
300- .when (pl .col .source_left & ~ pl .col .source_right )
301- .then (pl .lit ("left_only" ))
302- .when (~ pl .col .source_left & pl .col .source_right )
303- .then (pl .lit ("right_only" ))
304- .otherwise (pl .lit ("error" ))
305- .alias ("source" )
306- )
307- joined = joined .permute .append (["source" ])
308- # Drop source columns and return
309- joined = joined .drop ([left_col , right_col ])
314+ # Drop row indices
315+ if not keep_row_index :
316+ joined = joined .drop ([left_col , right_col ])
317+ else :
318+ joined = joined .permute .append ([left_col , right_col ])
310319 return joined
311320
312321if __name__ == "__main__" :
0 commit comments