The rule to remember: every column in the SELECT list is either in the GROUP BY or inside an aggregate. PostgreSQL and most other databases refuse a query that breaks it. SQLite and older MySQL settings let it through and quietly pick a value from one of the rows, which is a bug waiting to happen.
WHERE filters rows before they're grouped. HAVING filters the groups after, so it can use aggregates. Shipped orders only, then only customers with two or more of them:
SELECT customer_id, COUNT(*) AS orders
FROM orders
WHERE status = 'shipped'
GROUP BY customer_id
HAVING COUNT(*) >= 2;
COUNT(*) counts rows. COUNT(city) counts the rows where city isn't NULL, and COUNT(DISTINCT city) counts the different values. The other aggregates skip NULLs too, so AVG is the average of the values that are there.
SELECT COUNT(*), COUNT(city), COUNT(DISTINCT city)
FROM customers;