180 日間のさまざまなカテゴリ、ストア、SKU の閲覧データの計算を伴うプロジェクトでは、カテゴリ、ストア、SKU によってユーザー アクティビティに大きな差があるため、計算中に深刻なデータの偏りが発生しました。次のように:
データが偏っている理由と、データが偏っているかどうかを判断する方法に関する以前のブログについては、以下を参照してください。
Spark によるデータ スキュー問題の処理_Just Jump のブログ-CSDN ブログ_Spark データ スキュー問題
この問題を解決するために、いくつかの方法が検討され、実験的にテストされましたが、最終的には repartition + mapPartitions を使用して解決しました。
pyspark MapPartition の使用例:
PySpark mapPartitions() の例 - Spark By {Examples}
# 官方提供的代码示例
from pyspark.sql import SparkSession
spark = SparkSession.builder.appName('SparkByExamples.com').getOrCreate()
data = [('James','Smith','M',3000),
('Anna','Rose','F',4100),
('Robert','Williams','M',6200),
]
columns = ["firstname","lastname","gender","salary"]
df = spark.createDataFrame(data=data, schema = columns)
df.show()
#Example 1 mapPartitions()
def reformat(partitionData):
for row in partitionData:
yield [row.firstname+","+row.lastname,row.salary*10/100]
df2=df.rdd.mapPartitions(reformat).toDF(["name","bonus"])
df2.show()
#Example 2 mapPartitions()
def reformat2(partitionData):
updatedData = []
for row in partitionData:
name=row.firstname+","+row.lastname
bonus=row.salary*10/100
updatedData.append([name,bonus])
return iter(updatedData)
df2=df.rdd.mapPartitions(reformat).toDF(["name","bonus"])
df2.show())
個人的な実践例:
モール内のすべてのレベルのカテゴリを閲覧したユーザーの重複除外統計は、四半期および半年ごとに作成されます。カテゴリには、訪問者数が多く頻度が高い人気のカテゴリもあれば、需要が比較的低く、訪問者の数も比較的少ないカテゴリもあります。カテゴリ別に直接グループ化すると、上のスクリーンショットに示されているように、ユーザー重複排除統計によってデータの偏りが生じやすくなります。repartition + mapPartitions(+udf) を組み合わせて、統計におけるデータ スキュー手法を解決します。
(1) repartition() は、スキューが発生しやすい列と、後続の計算で使用されるグループ化列に従って再パーティション化します。パーティションの数を増やすことができるように、これら 2 つのパラメーター、並列度およびシャッフル パーティションの数を同時に増やすことに注意してください。
--conf spark.sql.shuffle.partitions=10000 \
--conf spark.default.parallelism=10000 \
(2) mapPartition を使用して、最初に各パーティションで計算を実行し、次に中間結果に対して最終的な集計計算を実行します。
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time: 2022/12/22 4:41 下午
# @Author: TobyGao
import numpy as np
from utils import getNdays, getNMonth, filterUdf, getTaskSavePath,\
keep_window, get_output_table
from pyspark.sql.functions import lit, col, expr, sum, count
from pyspark.sql.types import *
import math
class ViewUsersAnalysis:
def __init__(self, spark, task_name, calc_date):
self.spark = spark
self.task_name = task_name
self.calc_date = calc_date
self.keep_window = keep_window
self.__output_table = get_output_table(task_name)
self.__view_data_schema = StructType([
StructField("user_id", StringType(), False),
StructField("first_cate_cd", StringType(), False),
StructField("first_cate_name", StringType(), False),
StructField("second_cate_cd", StringType(), False),
StructField("second_cate_name", StringType(), False),
StructField("third_cate_cd", StringType(), False),
StructField("third_cate_name", StringType(), False),
StructField("shop_id", StringType(), False),
StructField("shop_name", StringType(), False),
StructField("main_sku_id", StringType(), False),
StructField("sku_name", StringType(), False),
StructField("request_date", StringType(), False)])
self.spark.udf.register("filterUdf", filterUdf)
def __load_view_data(self, window_type):
start_date = getNdays(self.calc_date, -window_type)
action_info = self.spark.createDataFrame(
self.spark.sparkContext.emptyRDD(), self.__view_data_schema)
data_path_list = []
for n in range(math.ceil(window_type / 30) + 1):
data_path_list.append(getTaskSavePath(self.task_name,getNMonth(self.calc_date,
-n) + "-*"))
for file_path in data_path_list:
print("input file paths:", file_path)
action_info = action_info.unionAll(self.spark.read.format("parquet").load(file_path))
return action_info
@staticmethod
def __partition_agg_func_cate1(row_list):
cate_code_dict = dict()
res_cate_dict = dict()
res_dict = dict()
result = []
for row in row_list:
user_id = row.user_id
first_cate_cd = row.first_cate_cd
first_cate_name = row.first_cate_name
if first_cate_cd not in cate_code_dict.keys():
cate_code_dict[first_cate_cd] = first_cate_name
cate_key = (user_id, first_cate_cd)
if cate_key not in res_cate_dict.keys():
res_cate_dict[cate_key] = 1
for k in res_cate_dict.keys():
agg_key = k[1]
if agg_key in res_dict.keys():
res_dict[agg_key] += 1
else:
res_dict[agg_key] = 1
res_cate_dict.clear()
for k in res_dict.keys():
result.append([k, cate_code_dict[k], res_dict[k]])
res_dict.clear()
cate_code_dict.clear()
return iter(result)
def get_count_results_in_long_window(self, action_info, window_type):
action_info.cache()
view_cate1 = action_info.repartition(10000, col("user_id"), col("first_cate_cd")) \
.rdd.mapPartitions(self.__partition_agg_func_cate1) \
.toDF(["first_cate_cd", "first_cate_name", "user_cnt_first_cate_view"]) \
.groupBy("first_cate_cd", "first_cate_name") \
.agg(sum("user_cnt_first_cate_view").alias("user_cnt_first_cate_view"))
view_cate1.cache()
view_cate1.show(20)
view_shop = action_info.repartition(10000, col("user_id"),
col("shop_id")) \
.dropDuplicates().groupBy("first_cate_cd",
"first_cate_name",
"second_cate_cd",
"second_cate_name",
"third_cate_cd",
"third_cate_name",
"shop_id",
"shop_name") \
.agg(count("user_id").alias("user_cnt_shop_view"))
view_shop.show()
def get_count_results_in_windows(self):
for window_type in [60, 90]:
# self.spark.sql(
# """alter table {output_table} drop partition (dt='{calc_date}', window_type='{window_type}')""".format(
# output_table=self.__output_table, calc_date=self.calc_date, window_type=window_type))
action_info = self.__load_view_data(window_type).drop("request_date").withColumn(
"window_type", lit(window_type))
self.get_count_results_in_long_window(action_info, window_type)
この方法の制限は、大量のメモリを消費することですが、各パーティションに一時的に保存して計算する必要があるキーが多すぎないことが最善です。しかし、データの偏りの問題は非常にうまく解決されます。