Соедините заказы Spark с данными товаров и клиентов

Биг датаBeginner
Практиковаться сейчас

Введение

Заказ содержит идентификаторы клиента и товара, а отдельные справочники описывают, где живёт клиент и к какой категории относится товар. Соединение (join) связывает записи по этим ключам, чтобы отчёт отвечал на вопросы о выручке по регионам и категориям товаров.

Вы дополните оплаченные заказы по одному справочнику за раз, выберете однозначные выходные столбцы и сверите итоги. Также вы сравните INNER и LEFT JOIN при отсутствии клиента. Предоставленные ключи клиентов и товаров уникальны; каждый совпавший заказ должен найти по одной записи в каждом справочнике. Дубликаты ключей справочника могли бы размножить соединённые строки и завысить выручку, поэтому предполагаемая связь важна.

Независимая работа подготавливает чистые заказы, двух клиентов, три товара и отдельный пример отсутствующего совпадения. Вам следует знать типизированное чтение, фильтры, десятичные суммы, агрегацию и минимальную запись Parquet. Hive, SQL, Hadoop, pandas и настройка кластера не требуются. Используйте терминал.

Добавьте регионы клиентов через явный ключ

На этом шаге вы соедините четыре оплаченных заказа с уникальными записями клиентов. Изучите исходную связь:

cd /home/labex/project
cat /home/labex/project/data/orders.csv
cat /home/labex/project/data/customers.csv

Несколько заказов могут иметь один customer_id, а справочник содержит одну строку для c1 и одну для c2. Равные идентификаторы связывают записи; совпадение позиций строк было бы неверным правилом. c1 даёт East, c2 — West.

Метод DataFrame join принимает другой DataFrame, условие соответствия и тип соединения. inner оставляет только совпавшие записи. Псевдонимы DataFrame alias("o") и alias("c") дают входам короткие имена ссылок. col("o.customer_id") обозначает столбец стороны заказов, col("c.customer_id") — стороны справочника.

Оба входа содержат customer_id. Явный select выбирает ключ стороны заказов и каждое нужное поле, избегая неоднозначных одинаковых имён. Псевдоним DataFrame называет входную ссылку, а псевдоним столбца — поле результата. Сохраните и запустите полную версию, в которой чтение и расчёт суммы повторяют прежние навыки:

cat > /home/labex/project/enrich_orders.py <<'PYTHON'
from pyspark.sql import SparkSession
from pyspark.sql.functions import col, sum as spark_sum
from pyspark.sql.types import StructType, StructField, StringType, IntegerType, DecimalType, DateType

spark = SparkSession.builder.appName("EnrichOrders").master("local[2]").getOrCreate()
spark.sparkContext.setLogLevel("ERROR")
order_schema = StructType([
    StructField("order_id", StringType(), True),
    StructField("customer_id", StringType(), True),
    StructField("product_id", StringType(), True),
    StructField("quantity", IntegerType(), True),
    StructField("unit_price", DecimalType(10, 2), True),
    StructField("order_status", StringType(), True),
    StructField("order_date", DateType(), True),
])
orders = spark.read.option("header", True).schema(order_schema).csv("/home/labex/project/data/orders.csv")
paid = orders.filter(col("order_status") == "paid").withColumn("order_amount", col("quantity") * col("unit_price"))
customers = spark.read.option("header", True).schema("customer_id string, region string").csv("/home/labex/project/data/customers.csv")
products = spark.read.option("header", True).schema("product_id string, category string").csv("/home/labex/project/data/products.csv")

with_customers = (
    paid.alias("o")
    .join(customers.alias("c"), col("o.customer_id") == col("c.customer_id"), "inner")
    .select(
        col("o.order_id").alias("order_id"), col("o.customer_id").alias("customer_id"),
        col("o.product_id").alias("product_id"), col("o.quantity").alias("quantity"),
        col("o.unit_price").alias("unit_price"), col("o.order_status").alias("order_status"),
        col("o.order_date").alias("order_date"), col("o.order_amount").alias("order_amount"),
        col("c.region").alias("region"),
    )
)
with_customers.orderBy("order_id").show(truncate=False)
with_customers.write.mode("overwrite").parquet("/home/labex/project/output/orders-with-regions")
spark.stop()
PYTHON
bigdata-python /home/labex/project/enrich_orders.py > /home/labex/project/output/customer-join.txt
cat /home/labex/project/output/customer-join.txt

Ожидаются четыре обогащённые оплаченные строки. o1 и o4 должны относиться к East, o2 и o6 — к West; суммы остаются 20.00, 25.00, 50.00 и 30.00. Четыре оплаченных входа с полными уникальными совпадениями дают четыре соединённые строки. Результат Parquet сохраняет типизированные поля и добавляет region без изменения исходного CSV.

Добавьте категории товаров и сверьте выручку

На этом шаге вы добавите второй справочник через product_id. Изучите сопоставление товаров:

cat /home/labex/project/data/products.csv

p1 и p2 относятся к Office, p3 — к Home. Соединение по customer_id было бы неверной связью для этой таблицы. Сохраните промежуточные обогащённые поля заказов и выберите только категорию из справочника товаров.

Полная версия сохраняет первое соединение и добавляет второе. Явная проекция оставляет один product_id, а не два одинаково названных ключа. Затем знакомые groupBy и агрегатная сумма рассчитывают выручку категорий:

cat > /home/labex/project/enrich_orders.py <<'PYTHON'
from pyspark.sql import SparkSession
from pyspark.sql.functions import col, sum as spark_sum
from pyspark.sql.types import StructType, StructField, StringType, IntegerType, DecimalType, DateType

spark = SparkSession.builder.appName("EnrichOrders").master("local[2]").getOrCreate()
spark.sparkContext.setLogLevel("ERROR")
order_schema = StructType([
    StructField("order_id", StringType(), True),
    StructField("customer_id", StringType(), True),
    StructField("product_id", StringType(), True),
    StructField("quantity", IntegerType(), True),
    StructField("unit_price", DecimalType(10, 2), True),
    StructField("order_status", StringType(), True),
    StructField("order_date", DateType(), True),
])
orders = spark.read.option("header", True).schema(order_schema).csv("/home/labex/project/data/orders.csv")
paid = orders.filter(col("order_status") == "paid").withColumn("order_amount", col("quantity") * col("unit_price"))
customers = spark.read.option("header", True).schema("customer_id string, region string").csv("/home/labex/project/data/customers.csv")
products = spark.read.option("header", True).schema("product_id string, category string").csv("/home/labex/project/data/products.csv")

with_customers = (
    paid.alias("o")
    .join(customers.alias("c"), col("o.customer_id") == col("c.customer_id"), "inner")
    .select(
        col("o.order_id").alias("order_id"), col("o.customer_id").alias("customer_id"),
        col("o.product_id").alias("product_id"), col("o.quantity").alias("quantity"),
        col("o.unit_price").alias("unit_price"), col("o.order_status").alias("order_status"),
        col("o.order_date").alias("order_date"), col("o.order_amount").alias("order_amount"),
        col("c.region").alias("region"),
    )
)
enriched = (
    with_customers.alias("e")
    .join(products.alias("p"), col("e.product_id") == col("p.product_id"), "inner")
    .select(
        col("e.order_id").alias("order_id"), col("e.customer_id").alias("customer_id"),
        col("e.product_id").alias("product_id"), col("e.quantity").alias("quantity"),
        col("e.unit_price").alias("unit_price"), col("e.order_status").alias("order_status"),
        col("e.order_date").alias("order_date"), col("e.order_amount").alias("order_amount"),
        col("e.region").alias("region"), col("p.category").alias("category"),
    )
)
category_sales = enriched.groupBy("category").agg(spark_sum("order_amount").alias("revenue"))
enriched.write.mode("overwrite").parquet("/home/labex/project/output/enriched-orders")
category_sales.write.mode("overwrite").parquet("/home/labex/project/output/category-sales")
enriched.orderBy("order_id").show(truncate=False)
category_sales.orderBy("category").show(truncate=False)
spark.stop()
PYTHON
bigdata-python /home/labex/project/enrich_orders.py > /home/labex/project/output/product-join.txt
cat /home/labex/project/output/product-join.txt

Обогащённый результат должен по-прежнему содержать четыре оплаченных заказа, теперь с регионом и категорией. Отчёт категорий должен показать Home 30.00 и Office 95.00. Их сумма — те же 125.00 оплаченной выручки.

Сохранение четырёх строк и сверенная выручка дают две полезные проверки. Ни одно значение не должно расти только из-за добавления описательных столбцов. Ключи примера уникальны и полны; следующий шаг изучает отсутствие совпадения отдельно, не меняя основные итоги продаж.

Сравните INNER и LEFT JOIN при отсутствующем клиенте

На этом шаге вы увидите поведение каждого соединения, когда справочник не находит совпадение. Изучите независимый пример:

cat /home/labex/project/data/example-orders.csv

demo1 ссылается на c1, demo2 — на c_missing, отсутствующего в customers. Сначала предположите INNER JOIN: должен остаться только demo1. LEFT JOIN сохраняет каждую запись примера слева и даёт NULL для полей, которые поступили бы из отсутствующей правой записи.

Сохраните небольшую отдельную программу, чтобы основной скрипт обогащённых продаж и его данные остались неизменными:

cat > /home/labex/project/compare_joins.py <<'PYTHON'
from pyspark.sql import SparkSession
from pyspark.sql.functions import col

spark = SparkSession.builder.appName("CompareCustomerMatches").master("local[2]").getOrCreate()
spark.sparkContext.setLogLevel("ERROR")
examples = spark.read.option("header", True).schema("order_id string, customer_id string").csv("/home/labex/project/data/example-orders.csv")
customers = spark.read.option("header", True).schema("customer_id string, region string").csv("/home/labex/project/data/customers.csv")
inner = examples.alias("o").join(customers.alias("c"), col("o.customer_id") == col("c.customer_id"), "inner").select(col("o.order_id").alias("order_id"), col("o.customer_id").alias("customer_id"), col("c.region").alias("region"))
left = examples.alias("o").join(customers.alias("c"), col("o.customer_id") == col("c.customer_id"), "left").select(col("o.order_id").alias("order_id"), col("o.customer_id").alias("customer_id"), col("c.region").alias("region"))
print("Inner matches:")
inner.orderBy("order_id").show(truncate=False)
print("Left matches:")
left.orderBy("order_id").show(truncate=False)
inner.write.mode("overwrite").parquet("/home/labex/project/output/example-inner")
left.write.mode("overwrite").parquet("/home/labex/project/output/example-left")
spark.stop()
PYTHON
bigdata-python /home/labex/project/compare_joins.py > /home/labex/project/output/join-comparison.txt
cat /home/labex/project/output/join-comparison.txt

Inner matches должен содержать demo1/c1/East. Left matches — эту строку и demo2/c_missing/NULL. Левый customer_id остаётся доступен, хотя правый region отсутствует. NULL — отсутствие совпавшего региона, а не регион с буквальным именем NULL.

INNER JOIN подходит отчёту, полнота совпадений которого уже установлена. LEFT JOIN может сохранять неразрешённые записи для контроля качества или осознанной бизнес-политики. Выбор типа соединения меняет сохраняемые записи, а не просто оформление вывода.

Используйте обогащённые поля для регионального сравнения

На этом шаге вы примените изученные фильтрацию и группировку к основному обогащённому набору. Региональная выручка всех категорий должна быть East 70.00 и West 55.00. Предположите результат только Office: East останется 70.00, а West станет 25.00, поскольку заказ Home на 30.00 исключён.

Запустите полный основной скрипт с двумя дополнительными производными отчётами. Соединения остаются прежними; условие категории меняет только то, какие обогащённые строки заказов поступают во вторую агрегацию:

cat > /home/labex/project/enrich_orders.py <<'PYTHON'
from pyspark.sql import SparkSession
from pyspark.sql.functions import col, sum as spark_sum
from pyspark.sql.types import StructType, StructField, StringType, IntegerType, DecimalType, DateType

spark = SparkSession.builder.appName("EnrichOrders").master("local[2]").getOrCreate()
spark.sparkContext.setLogLevel("ERROR")
order_schema = StructType([
    StructField("order_id", StringType(), True),
    StructField("customer_id", StringType(), True),
    StructField("product_id", StringType(), True),
    StructField("quantity", IntegerType(), True),
    StructField("unit_price", DecimalType(10, 2), True),
    StructField("order_status", StringType(), True),
    StructField("order_date", DateType(), True),
])
orders = spark.read.option("header", True).schema(order_schema).csv("/home/labex/project/data/orders.csv")
paid = orders.filter(col("order_status") == "paid").withColumn("order_amount", col("quantity") * col("unit_price"))
customers = spark.read.option("header", True).schema("customer_id string, region string").csv("/home/labex/project/data/customers.csv")
products = spark.read.option("header", True).schema("product_id string, category string").csv("/home/labex/project/data/products.csv")

with_customers = (
    paid.alias("o")
    .join(customers.alias("c"), col("o.customer_id") == col("c.customer_id"), "inner")
    .select(
        col("o.order_id").alias("order_id"), col("o.customer_id").alias("customer_id"),
        col("o.product_id").alias("product_id"), col("o.quantity").alias("quantity"),
        col("o.unit_price").alias("unit_price"), col("o.order_status").alias("order_status"),
        col("o.order_date").alias("order_date"), col("o.order_amount").alias("order_amount"),
        col("c.region").alias("region"),
    )
)
enriched = (
    with_customers.alias("e")
    .join(products.alias("p"), col("e.product_id") == col("p.product_id"), "inner")
    .select(
        col("e.order_id").alias("order_id"), col("e.customer_id").alias("customer_id"),
        col("e.product_id").alias("product_id"), col("e.quantity").alias("quantity"),
        col("e.unit_price").alias("unit_price"), col("e.order_status").alias("order_status"),
        col("e.order_date").alias("order_date"), col("e.order_amount").alias("order_amount"),
        col("e.region").alias("region"), col("p.category").alias("category"),
    )
)
category_sales = enriched.groupBy("category").agg(spark_sum("order_amount").alias("revenue"))
enriched.write.mode("overwrite").parquet("/home/labex/project/output/enriched-orders")
category_sales.write.mode("overwrite").parquet("/home/labex/project/output/category-sales")
enriched.orderBy("order_id").show(truncate=False)
category_sales.orderBy("category").show(truncate=False)
regional = enriched.groupBy("region").agg(spark_sum("order_amount").alias("revenue"))
office = enriched.filter(col("category") == "Office").groupBy("region").agg(spark_sum("order_amount").alias("revenue"))
regional.write.mode("overwrite").parquet("/home/labex/project/output/regional-sales")
office.write.mode("overwrite").parquet("/home/labex/project/output/office-regional-sales")
print("All categories by region:")
regional.orderBy("region").show(truncate=False)
print("Office category by region:")
office.orderBy("region").show(truncate=False)
spark.stop()
PYTHON
bigdata-python /home/labex/project/enrich_orders.py > /home/labex/project/output/regional-comparison.txt
cat /home/labex/project/output/regional-comparison.txt

Ожидаются предсказанные региональные результаты всех категорий и только Office. Сумма регионов Office снова даёт 95.00, совпадая с подытогом Office предыдущего шага. Общие регионы дают 125.00.

Эти сверки объясняют и связь соединения, и выбранное бизнес-подмножество. Основной обогащённый набор Parquet по-прежнему содержит четыре правильно типизированных оплаченных заказа, а демонстрация отсутствия совпадения остаётся отдельной. Далее вы выразите сопоставимые операции через Spark SQL над представлением DataFrame.

Итоги

Вы дополнили заказы по явным уникальным ключам клиентов и товаров, выбрали однозначные выходные столбцы и сверили итоги категорий и регионов. INNER и LEFT JOIN намеренно дали разное поведение при отсутствии совпадения. Далее вы сравните Spark SQL и выражения DataFrame над теми же структурированными данными.