{"slug": "nushell-carts-combo", "title": "Nushell Carts Combo", "summary": "A developer used Nushell to analyze grocery store transactions and identify the top five product pairs purchased together most often. The analysis revealed that Salsa and Tortilla Chips are the most common pair, followed by Bananas and Milk, Bananas and Bread, Bananas and Salsa, and Bananas and Tortilla Chips. The developer also benchmarked different approaches, with a Polars-based Nushell script completing the task in 0.5 seconds.", "body_md": "| #!/usr/bin/env nu | |\n| # PR 2026-07-15 11:23:32 | |\n| # https://mavenanalytics.io/data-drills/cart-combos | |\n| #Your dataset contains one year of point-of-sale transactions | |\n| # from a grocery store, including approximately 88,000 line items | |\n| # across 20,000 customer orders. | |\n| # | |\n| #Your task is to identify the five product pairs that are purchased | |\n| # together most often. | |\n| # | |\n| #Two products are \"purchased together\" if they appear in the same | |\n| # transaction | |\n| # | |\n| #A pair must consist of two different products | |\n| # | |\n| #Product pairs are unordered, meaning (A, B) and (B, A) should | |\n| # be treated as the same pair | |\n| # | |\n| #Count each pair once per transaction, regardless of the quantity | |\n| # purchased | |\n| # ********************************************************************** | |\n| # code from gemini (36 seconds) | |\n| # Load the transactions and select only the relevant columns | |\n| open grocery_transactions.csv | |\n| | select transaction_id product_name | |\n| # Remove duplicate products within the same transaction to count them once per transaction | |\n| | uniq | |\n| # Group the dataset by transaction ID | |\n| | group-by transaction_id | |\n| | values | |\n| # For each transaction, generate all unique product pairs | |\n| | each { |tx| | |\n| let products = ($tx.product_name | sort) | |\n| $products | enumerate | each { |item| | |\n| let idx = $item.index | |\n| let p1 = $item.item | |\n| $products | skip ($idx + 1) | each { |p2| | |\n| { product_a: $p1, product_b: $p2 } | |\n| } | |\n| } | flatten | |\n| } | |\n| | flatten | |\n| # Count the occurrences of each unique pair | |\n| | uniq --count | |\n| # Sort by count descending and take the top 5 | |\n| | sort-by --reverse count | |\n| | first 5 | |\n| ╭───┬────────────────────────────────┬───────╮ | |\n| │ # │ value │ count │ | |\n| ├───┼────────────────────────────────┼───────┤ | |\n| │ 0 │ ╭───────────┬────────────────╮ │ 5358 │ | |\n| │ │ │ product_a │ Salsa │ │ │ | |\n| │ │ │ product_b │ Tortilla Chips │ │ │ | |\n| │ │ ╰───────────┴────────────────╯ │ │ | |\n| │ 1 │ ╭───────────┬─────────╮ │ 4483 │ | |\n| │ │ │ product_a │ Bananas │ │ │ | |\n| │ │ │ product_b │ Milk │ │ │ | |\n| │ │ ╰───────────┴─────────╯ │ │ | |\n| │ 2 │ ╭───────────┬─────────╮ │ 2970 │ | |\n| │ │ │ product_a │ Bananas │ │ │ | |\n| │ │ │ product_b │ Bread │ │ │ | |\n| │ │ ╰───────────┴─────────╯ │ │ | |\n| │ 3 │ ╭───────────┬─────────╮ │ 2874 │ | |\n| │ │ │ product_a │ Bananas │ │ │ | |\n| │ │ │ product_b │ Salsa │ │ │ | |\n| │ │ ╰───────────┴─────────╯ │ │ | |\n| │ 4 │ ╭───────────┬────────────────╮ │ 2857 │ | |\n| │ │ │ product_a │ Bananas │ │ │ | |\n| │ │ │ product_b │ Tortilla Chips │ │ │ | |\n| │ │ ╰───────────┴────────────────╯ │ │ | |\n| ╰───┴────────────────────────────────┴───────╯ | |\n| #********************************************************************** | |\n| # code from lumo.ai (30 seconds) | |\n| # Top 5 product pairs purchased together most often | |\n| open grocery_transactions.csv | |\n| | select transaction_id product_name | |\n| | group-by transaction_id | |\n| | items { |tid, rows| | |\n| let products = ($rows.product_name | uniq | sort) | |\n| let n = ($products | length) | |\n| if $n < 2 { [] } else { | |\n| seq 0 ($n - 2) | |\n| | each { |i| | |\n| seq ($i + 1) ($n - 1) | |\n| | each { |j| | |\n| $\"($products | get $i) × ($products | get $j)\" | |\n| } | |\n| } | |\n| | flatten | |\n| } | |\n| } | |\n| | flatten | |\n| | uniq --count | |\n| | sort-by count --reverse | |\n| | first 5 | |\n| ╭───┬──────────────────────────┬───────╮ | |\n| │ # │ value │ count │ | |\n| ├───┼──────────────────────────┼───────┤ | |\n| │ 0 │ Salsa × Tortilla Chips │ 5358 │ | |\n| │ 1 │ Bananas × Milk │ 4483 │ | |\n| │ 2 │ Bananas × Bread │ 2970 │ | |\n| │ 3 │ Bananas × Salsa │ 2874 │ | |\n| │ 4 │ Bananas × Tortilla Chips │ 2857 │ | |\n| ╰───┴──────────────────────────┴───────╯ | |\n| #********************************************************************** | |\n| # nushell with polars (0.5 seconds) | |\n| # from gemini | |\n| # 1. Load the CSV file as a Polars LazyFrame and select relevant columns | |\n| let df = (polars open grocery_transactions.csv | |\n| | polars select [transaction_id product_name] | |\n| # Remove duplicate products within the same transaction | |\n| | polars unique --subset [transaction_id product_name] | |\n| ) | |\n| # 2. Prepare a clone of the DataFrame for the self-join | |\n| let df_right = ($df | |\n| | polars rename product_name product_b | |\n| ) | |\n| # Rename the original DataFrame's product column for matching | |\n| let df_left = ($df | |\n| | polars rename product_name product_a | |\n| ) | |\n| # 3. Perform the self-join, filter, group, count, sort, and fetch top 5 | |\n| $df_left | |\n| | polars join $df_right [transaction_id] [transaction_id] | |\n| # Keep only pairs where product_a comes alphabetically before product_b | |\n| | polars filter-with ((polars col product_a) < (polars col product_b)) | |\n| # Group by the distinct product pairs | |\n| | polars group-by [product_a product_b] | |\n| # Count occurrences of each pair | |\n| | polars agg [ | |\n| ((polars col transaction_id | polars count) | polars as count) | |\n| ] | |\n| # Sort by count descending (Passing '[true]' to '--reverse') | |\n| | polars sort-by count --reverse [true] | |\n| # Fetch the top 5 pairs | |\n| | polars first 5 | |\n| # Convert back to a standard Nushell table to view | |\n| | polars collect | |\n| | polars into-nu | |\n| ╭───┬───────────┬────────────────┬───────╮ | |\n| │ # │ product_a │ product_b │ count │ | |\n| ├───┼───────────┼────────────────┼───────┤ | |\n| │ 0 │ Salsa │ Tortilla Chips │ 5358 │ | |\n| │ 1 │ Bananas │ Milk │ 4483 │ | |\n| │ 2 │ Bananas │ Bread │ 2970 │ | |\n| │ 3 │ Bananas │ Salsa │ 2874 │ | |\n| │ 4 │ Bananas │ Tortilla Chips │ 2857 │ | |\n| ╰───┴───────────┴────────────────┴───────╯ | |\n| #********************************************************************** | |\n| # nushell with polars (0.5 seconds) | |\n| # from luno.ai | |\n| # Load, select relevant columns, deduplicate (transaction_id, product_name) pairs | |\n| let df = ( | |\n| polars open grocery_transactions.csv | |\n| | polars select transaction_id product_name | |\n| | polars drop-duplicates | |\n| ) | |\n| # Create left/right copies with renamed product columns for self-join | |\n| let df_left = ($df | polars rename product_name prod_a) | |\n| let df_right = ($df | polars rename product_name prod_b) | |\n| # Self-join on transaction_id to generate all product combinations per transaction | |\n| $df_left | |\n| | polars join $df_right transaction_id transaction_id | |\n| # Keep only unique unordered pairs (prod_a < prod_b eliminates self-pairs and dupes) | |\n| | polars filter ((polars col prod_a) < (polars col prod_b)) | |\n| # Count how many transactions contain each pair | |\n| | polars group-by prod_a prod_b | |\n| | polars agg [(polars len | polars as \"count\")] | |\n| | polars collect | |\n| # Sort by count descending and take top 5 | |\n| | polars sort-by count -r [true] | |\n| | polars first 5 | |\n| ╭───┬─────────┬────────────────┬───────╮ | |\n| │ # │ prod_a │ prod_b │ count │ | |\n| ├───┼─────────┼────────────────┼───────┤ | |\n| │ 0 │ Salsa │ Tortilla Chips │ 5358 │ | |\n| │ 1 │ Bananas │ Milk │ 4483 │ | |\n| │ 2 │ Bananas │ Bread │ 2970 │ | |\n| │ 3 │ Bananas │ Salsa │ 2874 │ | |\n| │ 4 │ Bananas │ Tortilla Chips │ 2857 │ | |\n| ╰───┴─────────┴────────────────┴───────╯ | |\n| #********************************************************************** | |\n| # xan code (with nushell) (0.5 seconds) | |\n| # from lumo.ai | |\n| # Top 5 product pairs using Xan (final corrected version) | |\n| try {rm temp.csv} | |\n| # Step 1: Select and deduplicate | |\n| xan select transaction_id,product_name grocery_transactions.csv | |\n| | xan dedup -s transaction_id,product_name | |\n| | save temp.csv | |\n| # Step 2: Self-join, filter to unique unordered pairs, create pair column | |\n| xan join -r right_ -D left transaction_id temp.csv temp.csv | |\n| | xan filter \"lt(product_name, right_product_name)\" | |\n| | xan map \"concat(product_name, ' & ', right_product_name) as pair\" | |\n| # Step 3: Frequency already returns results sorted by count descending | |\n| # frequency -s pair -l 5 gives us the top 5 pairs by count | |\n| | xan frequency -s pair -l 5 | |\n| # Step 4: Convert to Nushell table | |\n| | from csv | |\n| ## Cleanup | |\n| #rm temp.csv | |\n| ╭───┬───────┬──────────────────────────┬────────╮ | |\n| │ # │ field │ value │ count │ | |\n| ├───┼───────┼──────────────────────────┼────────┤ | |\n| │ 0 │ pair │ Salsa & Tortilla Chips │ 5358 │ | |\n| │ 1 │ pair │ Bananas & Milk │ 4483 │ | |\n| │ 2 │ pair │ Bananas & Bread │ 2970 │ | |\n| │ 3 │ pair │ Bananas & Salsa │ 2874 │ | |\n| │ 4 │ pair │ Bananas & Tortilla Chips │ 2857 │ | |\n| │ 5 │ pair │ <rest> │ 156773 │ | |\n| ╰───┴───────┴──────────────────────────┴────────╯ | |", "url": "https://wpnews.pro/news/nushell-carts-combo", "canonical_source": "https://gist.github.com/paul-d-ray/c8f85c7f55afa987a34247f6354574d1", "published_at": "2026-07-15 17:28:09+00:00", "updated_at": "2026-07-15 17:34:52.061662+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["Nushell", "Polars", "Gemini", "Lumo"], "alternates": {"html": "https://wpnews.pro/news/nushell-carts-combo", "markdown": "https://wpnews.pro/news/nushell-carts-combo.md", "text": "https://wpnews.pro/news/nushell-carts-combo.txt", "jsonld": "https://wpnews.pro/news/nushell-carts-combo.jsonld"}}