PySpark resuelve el problema del gráfico conectado

Revisión anterior:

Instalación y uso de PySpark y GraphFrameshttps:
//xxmdmst.blog.csdn.net/article/details/123009617

networkx resuelve rápidamente el problema del gráfico conectado
https://xxmdmst.blog.csdn.net/article/details/123012333

Anteriormente expliqué el uso de la biblioteca de computación de gráficos PySpark y dos ejemplos de python puro para resolver el problema del gráfico conectado. En este artículo, continuamos usando PySpark para el último problema de grafos conectados.

Requisito 1: encontrar una comunidad

Liu Bei y Guan Yu están relacionados, lo que indica que son una comunidad, y Liu Bei y Zhang Fei también están relacionados, por lo que Liu Bei, Guan Yu, Zhang Fei pertenecen a una comunidad, y así sucesivamente.

imagen-20220218200637173

¿Cómo resolver este problema de gráfico conectado usando Pyspark?

Primero, creamos el objeto chispa:

from pyspark.sql import SparkSession, Row
from graphframes import GraphFrame

spark = SparkSession \
    .builder \
    .appName("PySpark") \
    .master("local[*]") \
    .getOrCreate()
sc = spark.sparkContext
# 设置检查点目录
sc.setCheckpointDir("checkpoint")

Luego construye los datos:

data = [
    ['刘备', '关羽'],
    ['刘备', '张飞'],
    ['张飞', '诸葛亮'],
    ['曹操', '司马懿'],
    ['司马懿', '张辽'],
    ['曹操', '曹丕']
]
data = spark.createDataFrame(data, ["人员", "相关人员"])
data.show()
+------+--------+
|  人员|相关人员|
+------+--------+
|  刘备|    关羽|
|  刘备|    张飞|
|  张飞|  诸葛亮|
|  曹操|  司马懿|
|司马懿|    张辽|
|  曹操|    曹丕|
+------+--------+

Obviamente, los datos originales son los datos de borde requeridos por el cálculo del gráfico, solo modifique uno de los nombres de columna:

edges = data.toDF("src", "dst")
edges.printSchema()
root
 |-- src: string (nullable = true)
 |-- dst: string (nullable = true)

Comencemos a construir los datos del vértice:

vertices = (
    edges.rdd.flatMap(lambda x: x)
            .distinct()
            .map(lambda x: Row(x))
            .toDF(["id"])
)
vertices.show()
+------+
|    id|
+------+
|诸葛亮|
|  刘备|
|  曹操|
|司马懿|
|  曹丕|
|  关羽|
|  张飞|
|  张辽|
+------+

Lo siguiente usa el cálculo gráfico de Spark para calcular el gráfico conectado:

g = GraphFrame(vertices, edges)
result = g.connectedComponents().orderBy("component")
result.show()
+------+------------+
|    id|   component|
+------+------------+
|司马懿|           0|
|  张辽|           0|
|  曹丕|           0|
|  曹操|           0|
|  关羽|635655159808|
|  刘备|635655159808|
|  张飞|635655159808|
|诸葛亮|635655159808|
+------+------------+

Se puede observar que los miembros de una comunidad han sido identificados exitosamente a través del mismo componente, y las necesidades han sido resueltas exitosamente.

Requisito 2: Identificación de usuario unificada

Los cinco campos de abcde representan identificadores únicos como la dirección mac, la dirección ip, device_id, imei, etc. Las etiquetas representan la etiqueta del usuario. Por alguna razón, siempre faltan varios campos en el campo de identificación única del mismo usuario, ahora se requiere identificar los datos del mismo usuario y fusionar las etiquetas de cada usuario. Ejemplos de datos sin procesar y modelos resultantes son los siguientes:

imagen

Primero, construimos los datos:

df = spark.createDataFrame([
    ['a1', None, 'c1', None, None, 'tag1'],
    [None, None, 'c1', 'd1', None, 'tag2'],
    [None, 'b1', None, 'd1', None, 'tag3'],
    [None, 'b1', 'c1', 'd1', 'e1', 'tag4'],
    ['a2', 'b2', None, None, None, 'tag1'],
    [None, 'b4', 'c4', None, 'e4', 'tag1'],
    ['a2', None, None, 'd2', None, 'tag2'],
    [None, None, 'c2', 'd2', None, 'tag3'],
    [None, 'b3', None, None, 'e3', 'tag1'],
    [None, None, 'c3', None, 'e3', 'tag2'],
], list("abcde")+["tags"])
df.show()

resultado:

+----+----+----+----+----+----+
|   a|   b|   c|   d|   e|tags|
+----+----+----+----+----+----+
|  a1|null|  c1|null|null|tag1|
|null|null|  c1|  d1|null|tag2|
|null|  b1|null|  d1|null|tag3|
|null|  b1|  c1|  d1|  e1|tag4|
|  a2|  b2|null|null|null|tag1|
|null|  b4|  c4|null|  e4|tag1|
|  a2|null|null|  d2|null|tag2|
|null|null|  c2|  d2|null|tag3|
|null|  b3|null|null|  e3|tag1|
|null|null|  c3|null|  e3|tag2|
+----+----+----+----+----+----+

La siguiente idea sigue siendo la misma que la última vez. Primero, asigne una identificación única a cada fila de datos y luego, para cada columna identificada de manera única, cree la relación de conexión entre las filas según sean iguales o no. Todas identificadas de forma única. las columnas generan conexiones Las relaciones funcionan juntas como bordes para el cálculo de gráficos.

Lo siguiente usa el método zipWithUniqueId de RDD para generar una identificación única para cada fila y mover esta identificación al frente (debido a que estos datos pueden usarse con frecuencia muchas veces más tarde, por lo que se almacenan en caché):

tmp = df.rdd.zipWithUniqueId().map(lambda x: (x[1], x[0]))
tmp.cache()
tmp.first()
(0, Row(a='a1', b=None, c='c1', d=None, e=None, tags='tag1'))

Cree datos de vértice basados ​​en una identificación única:

vertices = tmp.map(lambda x: Row(x[0])).toDF(["id"])
vertices.show()
+---+
| id|
+---+
|  0|
|  1|
|  7|
|  2|
|  8|
|  3|
|  4|
| 10|
|  5|
| 11|
+---+

A continuación, cree los datos de borde:

def func(p):
    for k, ids in p:
        ids = list(ids)
        n = len(ids)
        if n <= 1:
            continue
        for i in range(n-1):
            for j in range(i+1, n):
                yield (ids[i], ids[j])


edges = []
keylist = list("abcde")
for key in keylist:
    data = tmp.mapPartitions(lambda area: [(row[key], i) for i, row in area if row[key]])
    edgeRDD = data.groupByKey().mapPartitions(func)
    edges.append(edgeRDD)
edgesDF = sc.union(edges).toDF(["src", "dst"])
edgesDF.cache()
edgesDF.show()
+---+---+
|src|dst|
+---+---+
|  8|  4|
|  7|  2|
|  0|  1|
|  0|  2|
|  1|  2|
|  4| 10|
|  1|  7|
|  1|  2|
|  7|  2|
|  5| 11|
+---+---+

Puede ver que todas las relaciones de número de línea se han adquirido correctamente.

Lo siguiente usa el cálculo gráfico para calcular las filas que pertenecen al mismo usuario:

gdf = GraphFrame(vertices, edgesDF)
components = gdf.connectedComponents()
components.show()
+---+---------+
| id|component|
+---+---------+
|  0|        0|
|  1|        0|
|  7|        0|
|  2|        0|
|  8|        4|
|  3|        3|
|  4|        4|
| 10|        4|
|  5|        5|
| 11|        5|
+---+---------+

Con el número de fila y el identificador único del grupo al que pertenece, podemos obtener el componente al que pertenece cada fila de los datos originales mediante el table join:

result = tmp.cogroup(components.rdd) \
    .map(lambda pair: pair[1][0].data[0] + Row(pair[1][1].data[0])) \
    .toDF(df.schema.names+["component"])
result.cache()
result.show()
+----+----+----+----+----+----+---------+
|   a|   b|   c|   d|   e|tags|component|
+----+----+----+----+----+----+---------+
|  a1|null|  c1|null|null|tag1|        0|
|null|null|  c1|  d1|null|tag2|        0|
|null|  b1|  c1|  d1|  e1|tag4|        0|
|null|  b4|  c4|null|  e4|tag1|        3|
|  a2|null|null|  d2|null|tag2|        4|
|null|  b3|null|null|  e3|tag1|        5|
|null|  b1|null|  d1|null|tag3|        0|
|  a2|  b2|null|null|null|tag1|        4|
|null|null|  c2|  d2|null|tag3|        4|
|null|null|  c3|null|  e3|tag2|        5|
+----+----+----+----+----+----+---------+

Puede ver que hemos identificado con éxito al mismo usuario, y el resto solo necesita agrupar y fusionar los datos usando la lógica de pandas:

def func(pdf):
    row = pdf[keylist].bfill().head(1)
    row["tags"] = pdf.tags.str.cat(sep=",")
    return row


result.groupBy("component").applyInPandas(
    func, schema="a string, b string, c string, d string, e string, tags string"
).show()
+----+---+---+----+----+-------------------+
|   a|  b|  c|   d|   e|               tags|
+----+---+---+----+----+-------------------+
|  a1| b1| c1|  d1|  e1|tag1,tag2,tag4,tag3|
|null| b4| c4|null|  e4|               tag1|
|  a2| b2| c2|  d2|null|     tag2,tag1,tag3|
|null| b3| c3|null|  e3|          tag1,tag2|
+----+---+---+----+----+-------------------+

Se puede ver que el resultado deseado se ha obtenido con éxito.

Nota: el resultado devuelto por applyInPandas debe ser el objeto datafream de pandas, por lo que la lógica anterior se cambió de .iloc[0] a .head(1)

Si su chispa no es la versión 3.X y no hay un método applyInPandas, será un gran problema usar el método rdd nativo:

def func(pair):
    component, rows = pair
    keyList = list("abcde")
    ids = {
    
    }
    for row in rows:
        for key in keylist:
            v = getattr(row, key)
            if v:
                ids[key] = v
        ids.setdefault("tags", []).append(row.tags)
    result = []
    for key in keylist:
        result.append(ids.get(key))
    result.append(",".join(ids["tags"]))
    return result


result2 = result.rdd.groupBy(
    lambda row: row.component).map(func).toDF(df.schema)
result2.cache()
result2.show()

Mismo resultado:

+----+---+---+----+----+-------------------+
|   a|  b|  c|   d|   e|               tags|
+----+---+---+----+----+-------------------+
|  a1| b1| c1|  d1|  e1|tag1,tag2,tag4,tag3|
|null| b4| c4|null|  e4|               tag1|
|  a2| b2| c2|  d2|null|     tag2,tag1,tag3|
|null| b3| c3|null|  e3|          tag1,tag2|
+----+---+---+----+----+-------------------+

Supongo que te gusta

Origin blog.csdn.net/as604049322/article/details/123036398
Recomendado
Clasificación