Question: The scenario is as follow:  At my store I sell items that can be replaced by other items (i.e. have supersession). For example, until a certain date I may had for sale the item 'A' which was eventually replaced by a new item 'B'.  These supersessions can happen successively. This means that 'A' can be replaced by 'B', which can be replaced by 'C', which can be replaced by 'D'.  More than one product can be replaced by a common new product. This means that 'B' can be replaced by 'C', but item 'K' can also be replaced by 'C'.   Starting from a DataFrame with the old and the new items code, my goal is to add another column to this DataFrame with the latest item code of each item. With the examples above, that means that starting from:  I need to end up with:  Note: this question is not duplicated with this one. That question I asked when I was trying to accomplish this in PowerBI. The question is now for Polars application. Althougnt I came up with a solution I'm really not satisfied with it, because:  I loop though all items on the DataFrame (number_of_items)² times.  As far as I'm aware it is not a good practice to loop through items in a DataFrame/Series using the for/while loop (Polars has tons of built-in methods to do this, so why can't I use one of them)?  Polars documentation says that Series.set_at_idx() (which I used in my solution) is frequently an anti-pattern, as it can block optimisation (predicate pushdown, etc).   So any idea of how can I perform such task with a cleaner and more performatic approach? Follows my solution: # Import Polars. import polars as pl # Create the sample DataFrame. df = pl.DataFrame( { 'Old_code': ['A', 'B', 'C', 'K'], 'New_code': ['B', 'C', 'D', 'C'] } ) # Retrieve the New_code column. new_PN_col = df.get_column('New_code') # Checks if the old code appears on the new code column. df = df.with_columns( pl.col('Old_code').is_in(new_PN_col).alias('Has_chained') ) # Breaks the DataFrame into Series. old_code_Series = df.get_column('Old_code') new_code_Series = df.get_column('New_code') has_chain = df.get_column('Has_chained') # Retrieve the DataFrame height. size = df.height # Loop through all items. for i in range(size): # Checks if the item has chained supersession. if has_chain[i]: # Retrieves the old and new item's code. old_1 = old_code_Series[i] new_1 = new_code_Series[i] # Loop through all items again. for k in range(size): # Finds where we need to replace the item code. if new_code_Series[k] == old_1: # Updates the item code. new_code_Series.scatter(k, new_1) # Concat the original DataFrame with the updated Series horizontally. df = pl.concat( [ df, new_code_Series.rename('Last_new_code').to_frame() ], how='horizontal' ).select(pl.col('*').exclude('Has_chained')) 

Answer: This is more of a directed acyclic graph problem than Polars natively. Your old_code and new_code lists effectively define the edges between the vertices of such a graph: old_codes = ['A', 'B', 'C', 'K'] new_codes = ['B', 'C', 'D', 'C'] edges = {e1 : e2 for e1,e2 in zip(old_codes, new_codes)}  From here, since every vertex will only have at most one edge, we can traverse this graph by iterating over every vertex without an in-edge. For each, we can mark all the vertices it's connected to until we find a vertex without an out-edge, this is the final mapping we want. The mapping of edges above lends itself to this quick algorithm: vertices with no in-edge is a symmetric set difference, and vertices with no out-edge simply doesn't have a key in this dictionary. There can be some short-circuiting to this algorithm by checking the dictionary being built as we go for already calculated pathing. Overall: final_mapping = {} for v in (set(edges.keys()) - set(edges.values())): vertices = {v} while v in edges.keys() and v not in final_mapping: vertices.add(edges[v]) v = edges[v] for v2 in vertices: final_mapping[v2] = final_mapping.get(v, v)  Now we can use replace as a simple Polars expression: df = pl.DataFrame({'Old_code' : old_codes, 'New_codes' : new_codes}) df.with_columns(Last_new_code = pl.col('Old_code').replace(final_mapping))  shape: (4, 3) ┌──────────┬───────────┬───────────────┐ │ Old_code ┆ New_codes ┆ Last_new_code │ │ --- ┆ --- ┆ --- │ │ str ┆ str ┆ str │ ╞══════════╪═══════════╪═══════════════╡ │ A ┆ B ┆ D │ │ B ┆ C ┆ D │ │ C ┆ D ┆ D │ │ K ┆ C ┆ D │ └──────────┴───────────┴───────────────┘