Hanukkah of Data

Author

François Michonneau

Published

March 1, 2023

Intro

Let’s get familiar with the data.

import ibis
from ibis import _
print("Using ibis version: ", ibis.__version__)
ibis.options.interactive = True

con = ibis.sqlite.connect("noahs.sqlite")

customers = con.table('customers')
products = con.table('products')
orders = con.table('orders')
orders_items = con.table('orders_items')

customers.head()
Using ibis version:  4.1.0
┏━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━┓
┃ customerid ┃ name             ┃ address         ┃ citystatezip            ┃ birthdate  ┃ phone        ┃
┑━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━┩
β”‚ int32      β”‚ string           β”‚ string          β”‚ string                  β”‚ string     β”‚ string       β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚       1001 β”‚ Jack Quinn       β”‚ 201 E Park St   β”‚ Los Angeles, CA 91343   β”‚ 1960-05-14 β”‚ 805-287-8515 β”‚
β”‚       1002 β”‚ David Powell     β”‚ 224C Tysens Ln  β”‚ Staten Island, NY 10306 β”‚ 1978-04-04 β”‚ 516-768-1652 β”‚
β”‚       1003 β”‚ Carrie Green     β”‚ 1608 W 53rd Way β”‚ Tampa, FL 33614         β”‚ 1969-01-21 β”‚ 727-209-0470 β”‚
β”‚       1004 β”‚ Steven Miller    β”‚ 178Β½ E 7th St   β”‚ Manhattan, NY 10009     β”‚ 1953-08-17 β”‚ 607-941-9563 β”‚
β”‚       1005 β”‚ Christine Powers β”‚ 270 W 242nd St  β”‚ Bronx, NY 10463         β”‚ 1983-06-06 β”‚ 212-759-9043 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
products.head()
/home/francois/.local/lib/python3.10/site-packages/ibis/backends/base/sql/alchemy/__init__.py:125: SAWarning: Dialect sqlite+pysqlite does *not* support Decimal objects natively, and SQLAlchemy must convert from floating point - rounding errors and other issues may occur. Please consider storing Decimal numbers as strings or integers on this platform for lossless storage.
  yield con.execute(*args, **kwargs)
┏━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┓
┃ sku     ┃ desc                                    ┃ wholesale_cost ┃
┑━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━┩
β”‚ string  β”‚ string                                  β”‚ decimal(10, 2) β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ DLI0002 β”‚ Smoked Whitefish Sandwich               β”‚           9.33 β”‚
β”‚ PET0005 β”‚ Vegan Cat Food, Turkey & Chicken        β”‚           4.35 β”‚
β”‚ HOM0018 β”‚ Power Radio (red)                       β”‚          21.81 β”‚
β”‚ KIT0034 β”‚ Azure Ladle                             β”‚           2.81 β”‚
β”‚ PET0041 β”‚ Gluten-free Cat Food, Pumpkin & Pumpkin β”‚           4.60 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
orders.head()
/home/francois/.local/lib/python3.10/site-packages/ibis/backends/base/sql/alchemy/__init__.py:125: SAWarning: Dialect sqlite+pysqlite does *not* support Decimal objects natively, and SQLAlchemy must convert from floating point - rounding errors and other issues may occur. Please consider storing Decimal numbers as strings or integers on this platform for lossless storage.
  yield con.execute(*args, **kwargs)
TypeError: must be real number, not str

TypeError: must be real number, not str
orders_items.head()
/home/francois/.local/lib/python3.10/site-packages/ibis/backends/base/sql/alchemy/__init__.py:125: SAWarning: Dialect sqlite+pysqlite does *not* support Decimal objects natively, and SQLAlchemy must convert from floating point - rounding errors and other issues may occur. Please consider storing Decimal numbers as strings or integers on this platform for lossless storage.
  yield con.execute(*args, **kwargs)
┏━━━━━━━━━┳━━━━━━━━━┳━━━━━━━┳━━━━━━━━━━━━━━━━┓
┃ orderid ┃ sku     ┃ qty   ┃ unit_price     ┃
┑━━━━━━━━━╇━━━━━━━━━╇━━━━━━━╇━━━━━━━━━━━━━━━━┩
β”‚ int32   β”‚ string  β”‚ int32 β”‚ decimal(10, 2) β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚    1001 β”‚ COL0820 β”‚     1 β”‚          25.52 β”‚
β”‚    1002 β”‚ TOY8907 β”‚     1 β”‚          12.92 β”‚
β”‚    1002 β”‚ KIT5813 β”‚     1 β”‚           7.99 β”‚
β”‚    1002 β”‚ KIT3981 β”‚     2 β”‚           7.21 β”‚
β”‚    1003 β”‚ KIT7098 β”‚     1 β”‚          12.53 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

There is definitely something weird with the data types provided in the SQLite database, so let’s use pyarrow to create parquet files from the CSV and check that the data types are what is expected.

from pyarrow import csv
import pyarrow.parquet as pq

customers = csv.read_csv("noahs-customers.csv")
pq.write_table(customers, "noahs-customers.parquet")
orders = csv.read_csv("noahs-orders.csv")
pq.write_table(orders, "noahs-orders.parquet")
products = csv.read_csv("noahs-products.csv")
pq.write_table(products, "noahs-products.parquet")
items_orders = csv.read_csv("noahs-orders_items.csv")
pq.write_table(items_orders, "noahs-orders_items.parquet")
con =  ibis.connect("duckdb://")
customers = con.read_parquet('noahs-customers.parquet')
products = con.read_parquet('noahs-products.parquet')
orders_items = con.read_parquet('noahs-orders_items.parquet')
orders = con.read_parquet('noahs-orders.parquet')

Day 1

We need to convert the last names of the customers and find one that matches their phone number.

(
    customers
    .mutate(last_name = _.name.re_extract(r"(.+)\s(.+)", 2).lower())
    .filter(_.last_name.length() == 10)
    .mutate(converted = _.last_name.re_replace("[abc]", "2")
                        .re_replace("[def]", "3")
                        .re_replace("[ghi]", "4")
                        .re_replace("[jkl]", "5")
                        .re_replace("[mno]", "6")
                        .re_replace("[pqrs]","7")
                        .re_replace("[tuv]", "8")
                        .re_replace("[wxyz]","9")
           )
    .mutate(edit_phone = _.phone.re_replace("-", ""))
    .filter(_.edit_phone == _.converted)
    .select(_.name, _.phone, _.converted, _.edit_phone)
)
┏━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━┓
┃ name           ┃ phone        ┃ converted  ┃ edit_phone ┃
┑━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━┩
β”‚ string         β”‚ string       β”‚ string     β”‚ string     β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Sam Guttenberg β”‚ 488-836-2374 β”‚ 4888362374 β”‚ 4888362374 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Day 2

We are looking for customers who have bought coffee/bagels, in 2017, that have the initals JD.

(
    products.filter(_.desc.lower().contains("coffee, drip"))
)
┏━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┓
┃ sku     ┃ desc         ┃ wholesale_cost ┃
┑━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━┩
β”‚ string  β”‚ string       β”‚ float64        β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ DLI1464 β”‚ Coffee, Drip β”‚           6.21 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
(
    products.filter(_.desc.lower().contains("bagel"))
)
┏━━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┓
┃ sku     ┃ desc          ┃ wholesale_cost ┃
┑━━━━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━┩
β”‚ string  β”‚ string        β”‚ float64        β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ BKY4234 β”‚ Caraway Bagel β”‚           6.23 β”‚
β”‚ BKY5887 β”‚ Sesame Bagel  β”‚           6.38 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
orders_items.head()
┏━━━━━━━━━┳━━━━━━━━━┳━━━━━━━┳━━━━━━━━━━━━┓
┃ orderid ┃ sku     ┃ qty   ┃ unit_price ┃
┑━━━━━━━━━╇━━━━━━━━━╇━━━━━━━╇━━━━━━━━━━━━┩
β”‚ int64   β”‚ string  β”‚ int64 β”‚ float64    β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚    1001 β”‚ COL0820 β”‚     1 β”‚      25.52 β”‚
β”‚    1002 β”‚ TOY8907 β”‚     1 β”‚      12.92 β”‚
β”‚    1002 β”‚ KIT5813 β”‚     1 β”‚       7.99 β”‚
β”‚    1002 β”‚ KIT3981 β”‚     2 β”‚       7.21 β”‚
β”‚    1003 β”‚ KIT7098 β”‚     1 β”‚      12.53 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
(
    orders_items.filter(_.sku.isin(['DLI1464', 'BKY4234', 'BKY5887']))
    .select(_.orderid)
    .distinct()
    .left_join(orders, _.orderid == orders.orderid)
    .select(_.orderid_x, _.customerid, _.ordered, _.total)
    .mutate(year = _.ordered.strftime('%Y'))
    .filter(_.year == '2017')
    .left_join(customers, _.customerid == customers.customerid)
    .select(_.orderid_x, _.total, _.customerid_y, _.name, _.phone, _.address, _.citystatezip)
    .filter(_.name.re_search('J(.+)\sD(.+)'))
)
┏━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ orderid_x ┃ total   ┃ customerid_y ┃ name         ┃ phone        ┃ address          ┃ citystatezip              ┃
┑━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
β”‚ int64     β”‚ float64 β”‚ int64        β”‚ string       β”‚ string       β”‚ string           β”‚ string                    β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚      7409 β”‚   33.22 β”‚         4164 β”‚ Jeremy Davis β”‚ 212-771-8924 β”‚ 134-10 Foch Blvd β”‚ South Ozone Park, NY 114… β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
orders_items.filter(_.orderid == 7409)
┏━━━━━━━━━┳━━━━━━━━━┳━━━━━━━┳━━━━━━━━━━━━┓
┃ orderid ┃ sku     ┃ qty   ┃ unit_price ┃
┑━━━━━━━━━╇━━━━━━━━━╇━━━━━━━╇━━━━━━━━━━━━┩
β”‚ int64   β”‚ string  β”‚ int64 β”‚ float64    β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚    7409 β”‚ DLI1464 β”‚     1 β”‚       7.73 β”‚
β”‚    7409 β”‚ KIT5861 β”‚     1 β”‚      12.53 β”‚
β”‚    7409 β”‚ HOM8601 β”‚     1 β”‚       4.48 β”‚
β”‚    7409 β”‚ BKY5887 β”‚     1 β”‚       8.48 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Day 3

We are looking for someone born in the year of the dog (comes back every 12 years: 2030, 2018, 2006, 1994, 1982, 1970, 1958), is an Aries (born between March 21 and April 19), and lives in South Ozone Park (see previous answer).

# Aries born in the year of the Dog
# year of the dog comes back every 12 years: 2030, 2018, 2006, 1994, 1982, 1970, 1958
# Aries: March 21 to Apr 19


# range for the birth days
customers.filter((_.birthdate == _.birthdate.min()) | (_.birthdate == _.birthdate.max()))
┏━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━┓
┃ customerid ┃ name          ┃ address          ┃ citystatezip       ┃ birthdate  ┃ phone        ┃
┑━━━━━━━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━┩
β”‚ int64      β”‚ string        β”‚ string           β”‚ string             β”‚ date       β”‚ string       β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚       2658 β”‚ Timothy Evans β”‚ 335Β½ 20th St     β”‚ Brooklyn, NY 11215 β”‚ 1935-02-14 β”‚ 838-950-3946 β”‚
β”‚       7499 β”‚ Barry Barnes  β”‚ 2758 Mermaid Ave β”‚ Brooklyn, NY 11224 β”‚ 2000-12-30 β”‚ 516-305-8067 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
dog_years = ["1994", "1982", "1970", "1958", "1936"]

(
    customers
    .mutate(year = _.birthdate.year())
    .filter(_.year.isin(dog_years))
    .filter(
        ((_.birthdate.month() == 3) & (_.birthdate.day() >= 21)) |
        ((_.birthdate.month() == 4) & (_.birthdate.day() <= 19))
    )
    .filter(_.citystatezip.ilike("south ozone park%"))
)
┏━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━┓
┃ customerid ┃ name         ┃ address         ┃ citystatezip               ┃ birthdate  ┃ phone        ┃ year  ┃
┑━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━┩
β”‚ int64      β”‚ string       β”‚ string          β”‚ string                     β”‚ date       β”‚ string       β”‚ int32 β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€
β”‚       2274 β”‚ Brent Nguyen β”‚ 109-19 110th St β”‚ South Ozone Park, NY 11420 β”‚ 1958-03-25 β”‚ 516-636-7397 β”‚  1958 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”˜

Day 4

We are looking for someone who buys regularly baked good between 4 and 5am.

(
    products.filter(_.sku.startswith("BKY"))
    .left_join(orders_items, orders_items.sku == _.sku)
    .select(_.orderid)
    .left_join(orders, orders.orderid == _.orderid)
    .filter((_.ordered.hour() >= 4) & (_.ordered.hour() < 5))
    .group_by(_.customerid)
    .aggregate(n_visits = _.count())
    .filter(_.n_visits == _.n_visits.max())
    .join(customers, customers.customerid == _.customerid)
)
┏━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━┓
┃ customerid ┃ n_visits ┃ name             ┃ address          ┃ citystatezip    ┃ birthdate  ┃ phone        ┃
┑━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━┩
β”‚ int64      β”‚ int64    β”‚ string           β”‚ string           β”‚ string          β”‚ date       β”‚ string       β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚       5375 β”‚       10 β”‚ Christina Booker β”‚ 1127 Grinnell Pl β”‚ Bronx, NY 10474 β”‚ 1981-01-08 β”‚ 718-649-9036 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Day 5

We are looking for someone who buys a lot of cat food for their old cats and lives in Queens Village.

(
    products.filter(_.desc.lower().contains("senior cat"))
)
┏━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┓
┃ sku     ┃ desc                                          ┃ wholesale_cost ┃
┑━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━┩
β”‚ string  β”‚ string                                        β”‚ float64        β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ PET0096 β”‚ Wet Senior Cat Food, Turkey & Shrimp          β”‚           3.11 β”‚
β”‚ PET0304 β”‚ Gluten-free Senior Cat Food, Chicken & Turkey β”‚           4.24 β”‚
β”‚ PET0925 β”‚ Senior Cat Food                               β”‚           3.84 β”‚
β”‚ PET0977 β”‚ Vegan Senior Cat Food, Chicken & Beef         β”‚           4.36 β”‚
β”‚ PET1090 β”‚ Vegan Senior Cat Food, Tuna & Duck            β”‚           4.39 β”‚
β”‚ PET1220 β”‚ Dry Senior Cat Food, Salmon & Shrimp          β”‚           3.51 β”‚
β”‚ PET1536 β”‚ Dry Senior Cat Food, Chicken & Chicken        β”‚           2.26 β”‚
β”‚ PET1849 β”‚ Vegan Senior Cat Food, Duck & Pumpkin         β”‚           4.65 β”‚
β”‚ PET1909 β”‚ Vegan Senior Cat Food, Chicken & Salmon       β”‚           4.60 β”‚
β”‚ PET1919 β”‚ Gluten-free Senior Cat Food, Shrimp & Tuna    β”‚           4.23 β”‚
β”‚ …       β”‚ …                                             β”‚              … β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
## let's find out who buys senior cat food
(
    products.filter(_.desc.lower().contains("senior cat"))
    .left_join(orders_items, orders_items.sku == _.sku)
    .select(_.orderid)
    .left_join(orders, orders.orderid == _.orderid)
    .select(_.customerid)
    .distinct()
    .left_join(customers, customers.customerid == _.customerid)
    .filter(_.citystatezip.contains("Queens Village"))
)
┏━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━┓
┃ customerid_x ┃ customerid_y ┃ name       ┃ address         ┃ citystatezip           ┃ birthdate  ┃ phone        ┃
┑━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━┩
β”‚ int64        β”‚ int64        β”‚ string     β”‚ string          β”‚ string                 β”‚ date       β”‚ string       β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚         5486 β”‚         5486 β”‚ Sam House  β”‚ 220-8 111th Ave β”‚ Queens Village, NY 11… β”‚ 1955-11-10 β”‚ 607-836-2966 β”‚
β”‚         7675 β”‚         7675 β”‚ Anita Koch β”‚ 106-51 214th St β”‚ Queens Village, NY 11… β”‚ 1955-11-14 β”‚ 315-492-7411 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
## it's not quite enough, let's compare how much they buy
(
    products.filter(_.desc.lower().contains("senior cat"))
    .left_join(orders_items, orders_items.sku == _.sku)
    .select(_.orderid)
    .left_join(orders, orders.orderid == _.orderid)
    .group_by(_.customerid)
    .aggregate(_.total.sum().name("total_catfood"))
    .left_join(customers, customers.customerid == _.customerid)
    .filter(_.citystatezip.contains("Queens Village"))
    .select(_.customerid_x, _.name, _.total_catfood, _.phone)    
)
┏━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┓
┃ customerid_x ┃ name       ┃ total_catfood ┃ phone        ┃
┑━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━┩
β”‚ int64        β”‚ string     β”‚ float64       β”‚ string       β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚         5486 β”‚ Sam House  β”‚        231.88 β”‚ 607-836-2966 β”‚
β”‚         7675 β”‚ Anita Koch β”‚        699.98 β”‚ 315-492-7411 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Day 6

We are looking for someone who minimizes the profit on their purchases at Noah’s Market.

(
    orders_items
    .left_join(products, products.sku == _.sku)
    .mutate(profit = _.unit_price - _.wholesale_cost)
    .group_by(_.orderid)
    .aggregate(_.profit.sum().name("total_profit_per_order"))
    .left_join(orders, orders.orderid == _.orderid)
    .group_by(_.customerid)
    .aggregate(_.total_profit_per_order.sum().name("total_profit_per_customer"))
    .filter(_.total_profit_per_customer == _.total_profit_per_customer.min())
    .join(customers, customers.customerid == _.customerid)
    .select(_.customerid, _.name, _.phone, _.total_profit_per_customer)
)
┏━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ customerid ┃ name           ┃ phone        ┃ total_profit_per_customer ┃
┑━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
β”‚ int64      β”‚ string         β”‚ string       β”‚ float64                   β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚       8342 β”‚ Emily Randolph β”‚ 914-868-0316 β”‚                   -313.16 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Day 7

We are looking for people who are buying the same product but in different colors (colors are indicated in parentheses in the descriptions), and that have made purchases one after the other (they were following each other in line).

(
    products
    .filter(_.desc.re_search('.+\s\(.+\)$')) ## colors are noted in parenthesis
    .left_join(orders_items, orders_items.sku == _.sku)
    .select(_.orderid, _.desc)
    .left_join(orders, orders.orderid == _.orderid)
    .order_by(_.ordered)
    .mutate(
        lag_customerid = _.customerid.lag(),
        lag_ordered = _.ordered.lag()
    )
    .filter(_.lag_customerid == 8342)
    .select(_.orderid_x, _.desc, _.customerid, _.ordered, _.lag_customerid, _.lag_ordered)
)
┏━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━┓
┃ orderid_x ┃ desc                      ┃ customerid ┃ ordered             ┃ lag_customerid ┃ lag_ordered         ┃
┑━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━┩
β”‚ int64     β”‚ string                    β”‚ int64      β”‚ timestamp           β”‚ int64          β”‚ timestamp           β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚     12625 β”‚ Automatic Mixer (white)   β”‚       1865 β”‚ 2017-05-28 09:15:47 β”‚           8342 β”‚ 2017-05-28 09:15:06 β”‚
β”‚     86372 β”‚ Electric Machine (purple) β”‚       8835 β”‚ 2019-06-01 12:50:25 β”‚           8342 β”‚ 2019-06-01 12:50:16 β”‚
β”‚     94040 β”‚ Electric Toaster (blue)   β”‚       8342 β”‚ 2019-08-18 14:20:28 β”‚           8342 β”‚ 2019-08-18 14:20:28 β”‚
β”‚     94044 β”‚ Noah's Gift Box (green)   β”‚      10880 β”‚ 2019-08-18 14:57:19 β”‚           8342 β”‚ 2019-08-18 14:20:28 β”‚
β”‚     96223 β”‚ Super Widget (white)      β”‚      11942 β”‚ 2019-09-09 16:53:22 β”‚           8342 β”‚ 2019-09-09 16:30:05 β”‚
β”‚    154876 β”‚ Electric Crockpot (purpl… β”‚       8342 β”‚ 2021-04-17 19:52:28 β”‚           8342 β”‚ 2021-04-17 19:52:28 β”‚
β”‚    154879 β”‚ Jigsaw Puzzle (Moses and… β”‚       2476 β”‚ 2021-04-17 20:22:32 β”‚           8342 β”‚ 2021-04-17 19:52:28 β”‚
β”‚    200952 β”‚ Noah's Jersey (puce)      β”‚       7880 β”‚ 2022-07-26 09:48:32 β”‚           8342 β”‚ 2022-07-26 08:18:05 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
# I need to figure out how to do the time difference because here I'm doing it by eye
# and ideally we need to compare the content of their orders and remove lines from the same order
# It's probably not in 2017 because that was the beginning of the story
customers.filter(_.customerid == 8835)
┏━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━┓
┃ customerid ┃ name           ┃ address       ┃ citystatezip            ┃ birthdate  ┃ phone        ┃
┑━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━┩
β”‚ int64      β”‚ string         β”‚ string        β”‚ string                  β”‚ date       β”‚ string       β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚       8835 β”‚ Jonathan Adams β”‚ 644 Targee St β”‚ Staten Island, NY 10304 β”‚ 1975-08-26 β”‚ 315-618-5263 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Day 8

We are looking for someone who has purchased lots of collectible (prefix with COL in the products table).

(
    products.filter(_.sku.lower().contains("col"))
    .left_join(orders_items, orders_items.sku == _.sku)
    .select(_.orderid, _.sku_x)
    .left_join(orders, orders.orderid == _.orderid)
    .group_by(_.customerid)
    .aggregate(n_collect = _.count())
    .order_by(ibis.desc(_.n_collect))
    .left_join(customers, customers.customerid == _.customerid)
    .select(_.name, _.phone, _.n_collect)
)
┏━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━┓
┃ name              ┃ phone        ┃ n_collect ┃
┑━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━┩
β”‚ string            β”‚ string       β”‚ int64     β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Travis Bartlett   β”‚ 929-906-5980 β”‚       111 β”‚
β”‚ Justin Jimenez    β”‚ 516-722-4758 β”‚        31 β”‚
β”‚ Joshua Smith      β”‚ 914-243-3254 β”‚        30 β”‚
β”‚ Tyler Hickman     β”‚ 585-307-8945 β”‚        27 β”‚
β”‚ Rachel Richardson β”‚ 212-778-9867 β”‚        26 β”‚
β”‚ Kim Yang          β”‚ 516-399-6228 β”‚        26 β”‚
β”‚ Joshua Parks      β”‚ 680-594-9693 β”‚        26 β”‚
β”‚ Amber Porter      β”‚ 838-307-4383 β”‚        26 β”‚
β”‚ Chelsea Marsh     β”‚ 315-565-2714 β”‚        26 β”‚
β”‚ Jennifer Daniels  β”‚ 516-994-3023 β”‚        26 β”‚
β”‚ …                 β”‚ …            β”‚         … β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜