好问题,理想情况下我会使用 udf 以使事情变得简单,但由于此任务是使用 Spark SQL 高阶函数的一个很好的例子......可能有点冗长,所以我把它分成4个步骤。让我知道它是否有效,欢迎提出任何问题:
Step-1:将字符串转换为字符串数组
用连续的模式(?:(?!/)\p{Punct}|\s)+'))分割字符串
标点符号(/ 除外)或空格,然后过滤掉 EMPTY(前导/尾随)的项目。临时列temp1 用于保存中间列。
from pyspark.sql.functions import split, expr
df1 = df.withColumn('temp1', split('Assembly_name', r'(?:(?!/)\p{Punct}|\s)+')) \
.withColumn('temp1', expr("filter(temp1, x -> x <> '')"))
df1.select('temp1').show(truncate=False)
+-------------------------------------------------------------------------------------+
|temp1 |
+-------------------------------------------------------------------------------------+
|[OIL, PUMP, ASSEMBLY, A01EA09CA, 4999202399920239A06] |
|[OIL, PUMP, ASSEMBLY, A02EA09CA/CB/CC, 4999202399920239A06] |
|[OIL, PUMP, ASSEMBLY, A01EA05CA, 4999202399920239A06] |
|[DRIVE, TRAIN, TRANSMISSION, 6, SPEED, V08AB26/GB26/LB26, ALL, OPTIONS, 49VICTRANS08]|
|[SUSPENSION, 7043244, S09PR6HSL/PS6HSL/HEL, 49SNOWSHOCKFRONT7043244SB] |
+-------------------------------------------------------------------------------------+
Step-2:将 temp1 转换为数组数组
使用/再次拆分数组项,以便所有part-id都在自己的数组项上
df2 = df1.withColumn('temp1', expr("transform(temp1, x -> split(x, '/'))"))
df2.select('temp1').show(truncate=False)
+----------------------------------------------------------------------------------------------------------+
|temp1 |
+----------------------------------------------------------------------------------------------------------+
|[[OIL], [PUMP], [ASSEMBLY], [A01EA09CA], [4999202399920239A06]] |
|[[OIL], [PUMP], [ASSEMBLY], [A02EA09CA, CB, CC], [4999202399920239A06]] |
|[[OIL], [PUMP], [ASSEMBLY], [A01EA05CA], [4999202399920239A06]] |
|[[DRIVE], [TRAIN], [TRANSMISSION], [6], [SPEED], [V08AB26, GB26, LB26], [ALL], [OPTIONS], [49VICTRANS08]] |
|[[SUSPENSION], [7043244], [S09PR6HSL, PS6HSL, HEL], [49SNOWSHOCKFRONT7043244SB]] |
+----------------------------------------------------------------------------------------------------------+
第 3 步:使用聚合重置部分 ID
聚合函数将对内部数组进行操作:
df3 = df2.withColumn('temp1', expr("""
flatten(
transform(temp1, x ->
transform(sequence(1, size(x)), i ->
aggregate(
sequence(1, i)
, x[0]
, (acc,j) -> concat(substr(acc, 1, length(x[0])-length(x[j-1])), x[j-1])
)
)
)
)
"""))
df3.select('temp1').show(truncate=False)
+----------------------------------------------------------------------------------------------+
|temp1 |
+----------------------------------------------------------------------------------------------+
|[OIL, PUMP, ASSEMBLY, A01EA09CA, 4999202399920239A06] |
|[OIL, PUMP, ASSEMBLY, A02EA09CA, A02EA09CB, A02EA09CC, 4999202399920239A06] |
|[OIL, PUMP, ASSEMBLY, A01EA05CA, 4999202399920239A06] |
|[DRIVE, TRAIN, TRANSMISSION, 6, SPEED, V08AB26, V08GB26, V08LB26, ALL, OPTIONS, 49VICTRANS08] |
|[SUSPENSION, 7043244, S09PR6HSL, S09PS6HSL, S09PS6HEL, 49SNOWSHOCKFRONT7043244SB] |
+----------------------------------------------------------------------------------------------+
地点:
-
transform(temp1, x -> func1(x)) : 遍历数组temp1中的每一项运行func1(x),x是内部数组(字符串数组)
-
上面提到的
func1(x) 是另一个变换函数,它遍历 sequence(1, size(x)) 并在每个 上运行 func2(i)我:
transform(sequence(1, size(x)), i -> func2(i))
-
上面提到的
func2(i)是一个聚合函数,它遍历sequence(1,i),初始值为x[0],并累加/减少使用函数:
(acc,j) -> concat(substr(acc, 1, length(acc)-length(x[j-1])), x[j-1])
注意: substr() 的位置是从 1 开始的,array-indexing 是从 0 开始的,因此我们需要 x[j-1] 来引用上面 reduce/ 中的当前数组项聚合函数
-
最后,运行flatten() 合并内部数组
这一步是在做类似下面的 pysudo-loop:
for x in temp1:
for i in range(1, size(x)+1):
acc = x[0]
for j in range(1,i+1):
acc = concat(substr(acc, 1, length(acc)-length(x[j-1])), x[j-1])
第 4 步:合并和删除重复项
df4 = df3.groupby('Itemno').agg(
expr("concat_ws(' ', array_distinct(flatten(collect_list(temp1)))) AS Assembly_names")
, expr("concat_ws(' ', collect_set(Assembly_id)) as Assembly_ids")
)
地点:
- 使用collect_list()获取数组数组(temp1是字符串数组)
- 使用flatten()将上面的转换成字符串数组
- 使用 array_distinct() 删除重复项
-
使用concat_ws()将上面的数组转换成字符串
df4.select('Assembly_names').show(truncate=False)
+---------------------------------------------------------------------------------------+
|Assembly_names |
+---------------------------------------------------------------------------------------+
|OIL PUMP ASSEMBLY A01EA09CA 4999202399920239A06 A02EA09CA A02EA09CB A02EA09CC A01EA05CA|
|SUSPENSION 7043244 S09PR6HSL S09PS6HSL S09PS6HEL 49SNOWSHOCKFRONT7043244SB |
|DRIVE TRAIN TRANSMISSION 6 SPEED V08AB26 V08GB26 V08LB26 ALL OPTIONS 49VICTRANS08 |
+---------------------------------------------------------------------------------------+
更新:
第一个很容易修复,它比现有的要容易得多(不需要聚合)。对于第二个,以下解决方案需要逐个字符地遍历字符串,这可能会很慢。如果是这样,我们可能必须使用 udf 进行检查。
以下是变化:
-
第 1 步:只需添加下划线即可从标点符号中排除:(请注意,如果在文本的其他位置显示任何下划线,可能需要先运行 regexp_replace 以清理它们)
df1 = df.withColumn('temp1', split('Assembly_name', r'(?:(?![/_])\p{Punct}|\s)+')) \
.withColumn('temp1', expr("filter(temp1, x -> x <> '')"))
-
Step-2: 将数组数组进一步拆分为数组数组数组,最里面的数组将字符串拆分为字符。反转最里面的数组,以便于比较。
df2 = df1.withColumn('temp1', expr("transform(temp1, x -> split(x, '/'))")) \
.withColumn('temp1', expr("transform(temp1, x -> transform(x, y -> reverse(split(y, ''))) )"))
-
第 3 步: 使用 transform() 而不是 aggregate() 来重置零件 ID。我们检查 y[i](最内层数组的项)是否为 NULL 或下划线,然后将其替换为 x[0][i]中的相应项>。然后我们反转数组并使用 concat_ws(''..) 将其转换回字符串。
df3 = df2.withColumn('temp1', expr("""
flatten(
transform(temp1, x ->
transform(x, y ->
concat_ws('',
reverse(
transform(sequence(0, size(x[0])-1), i -> IF(y[i] is NULL or y[i] == '_', x[0][i], y[i]))
)
)
)
)
)
"""))
下面是上面的结果
df3.select('temp1').show(truncate=False)
+---------------------------------------------------------------------------------------------+
|temp1 |
+---------------------------------------------------------------------------------------------+
|[OIL, PUMP, ASSEMBLY, A01EA09CA, 4999202399920239A06] |
|[OIL, PUMP, ASSEMBLY, A02EA09CA, A02EA09CB, A02EA09CC, 4999202399920239A06] |
|[OIL, PUMP, ASSEMBLY, A01EA05CA, 4999202399920239A06] |
|[DRIVE, TRAIN, TRANSMISSION, 6, SPEED, V08AB26, V08GB26, V08LB26, ALL, OPTIONS, 49VICTRANS08]|
|[SUSPENSION, 7043244, S09PR6HSL, S09PS6HSL, S09PR6HEL, 49SNOWSHOCKFRONT7043244SB] |
|[DRIVE, TRAIN, CLUTCH, PRIMARY, S09PR6HSL, S09PS6HSL, S09PR6HSL, 49SNOWDRIVECLUTCH09600TRG] |
|[DRIVE, TRAIN, CLUTCH, PRIMARY, S09PR6HSL, S09PS6HSL, S09PR6HSL, 49SNOWDRIVECLUTCH09600TRG] |
+---------------------------------------------------------------------------------------------+
处理前的字段:
df.select('Assembly_name').show(truncate=False)
+----------------------------------------------------------------------------------+
|Assembly_name |
+----------------------------------------------------------------------------------+
|OIL PUMP ASSEMBLY - A01EA09CA (4999202399920239A06) |
|OIL PUMP ASSEMBLY - A02EA09CA/CB/CC (4999202399920239A06) |
|OIL PUMP ASSEMBLY - A01EA05CA (4999202399920239A06) |
|DRIVE TRAIN, TRANSMISSION (6 SPEED) - V08AB26/GB26/LB26 ALL OPTIONS (49VICTRANS08)|
|SUSPENSION (7043244) - S09PR6HSL/PS6HSL/HEL (49SNOWSHOCKFRONT7043244SB) |
|DRIVE TRAIN, CLUTCH, PRIMARY - S09PR6HSL/PS_HSL/H_L (49SNOWDRIVECLUTCH09600TRG) |
|DRIVE TRAIN, CLUTCH, PRIMARY - S09PR6HSL/_S__SL/H_L (49SNOWDRIVECLUTCH09600TRG) |
+----------------------------------------------------------------------------------+
UPDATE-2 添加了 Step-0:
Step-0: 预处理列Assembly_name,使用regexp_replace + split 将模型# 分离成一个新列并从原列Assembly_name中删除:
from pyspark.sql.functions import regexp_replace, split
df0 = df.withColumn('new_col', split(regexp_replace('Assembly_name', r'^(.*)-\s*(\S+)(.*)$', '$1$3\0$2'),'\0')) \
.selectExpr(
'Itemno'
, 'Assembly_id'
, "coalesce(new_col[0], Assembly_name) as Assembly_name"
, "coalesce(new_col[1], '') as models"
)
df0.show(truncate=False)
+-------+-----------+---------------------------------------------------------------+--------------------+
|Itemno |Assembly_id|Assembly_name |models |
+-------+-----------+---------------------------------------------------------------+--------------------+
|0450056|44011 |OIL PUMP ASSEMBLY (4999202399920239A06) |A01EA09CA |
|0450056|135502 |OIL PUMP ASSEMBLY (4999202399920239A06) |A02EA09CA/CB/CC |
|0450056|37884 |OIL PUMP ASSEMBLY (4999202399920239A06) |A01EA05CA |
|0450067|12345 |DRIVE TRAIN, TRANSMISSION (6 SPEED) ALL OPTIONS (49VICTRANS08)|V08AB26/GB26/LB26 |
|0450068|1000 |SUSPENSION (7043244) (49SNOWSHOCKFRONT7043244SB) |S09PR6HSL/PS6HSL/HEL|
|0450066|12345 |DRIVE TRAIN, CLUTCH, PRIMARY (49SNOWDRIVECLUTCH09600TRG) |S09PR6HSL/PS_HSL/H_L|
|0450069|12346 |DRIVE TRAIN, CLUTCH, PRIMARY (49SNOWDRIVECLUTCH09600TRG) | |
+-------+-----------+---------------------------------------------------------------+--------------------+
然后您可以使用 RegexTokenier 和 StopwordsRemover 处理 Assembly_name,models 是当前帖子的简化版本,您可以跳过第 1 步,但请注意数组的深度。
(注意:从最后一条记录中删除S09PR6HSL/_S__SL/H_L进行测试)