Ya no tienes que buscar más por todo internet ya que has llegado al sitio exacto, tenemos la respuesta que buscas y sin liarte.
Solución:
Editar: Como se discutió en los comentarios, para solucionar el problema mencionado en su actualización, podemos convertir student_id en cada momento en un ID de secuencia generalizado usando dense_rank, pasar del Paso 1 al 3 (usando la columna del estudiante) y luego usar join para convertir estudiante en cada tiempo de vuelta a su original Identificación del Estudiante. vea abajo Paso 0 y Paso 4. en caso de que haya menos de 4 profesores en una timeUnit, la dimensión se redimensionará a 4 en Numpy-end (usando np_vstack () y np_zeros ()), vea la función actualizada find_assigned
.
Puede probar pandas_udf y scipy.optimize.linear_sum_assignment (nota: el método backend es el algoritmo húngaro mencionado por @cronoik en los comentarios principales), ver a continuación:
from pyspark.sql.functions import pandas_udf, PandasUDFType, first, expr, dense_rank
from pyspark.sql.types import StructType
from scipy.optimize import linear_sum_assignment
from pyspark.sql import Window
import numpy as np
df = spark.createDataFrame([
('1596048041', 'p1', 's1', 0.7), ('1596048041', 'p1', 's2', 0.5), ('1596048041', 'p1', 's3', 0.3),
('1596048041', 'p1', 's4', 0.2), ('1596048041', 'p2', 's1', 0.9), ('1596048041', 'p2', 's2', 0.1),
('1596048041', 'p2', 's3', 0.15), ('1596048041', 'p2', 's4', 0.2), ('1596048041', 'p3', 's1', 0.2),
('1596048041', 'p3', 's2', 0.3), ('1596048041', 'p3', 's3', 0.4), ('1596048041', 'p3', 's4', 0.8),
('1596048041', 'p4', 's1', 0.2), ('1596048041', 'p4', 's2', 0.3), ('1596048041', 'p4', 's3', 0.35),
('1596048041', 'p4', 's4', 0.4)
] , ['time', 'professor_id', 'student_id', 'score'])
N = 4
cols_student = [*range(1,N+1)]
Paso 0: agregar una columna adicional student
, y cree un nuevo marco de datos df3 con todos los combos únicos de time
+ student_id
+ student
.
w1 = Window.partitionBy('time').orderBy('student_id')
df = df.withColumn('student', dense_rank().over(w1))
+----------+------------+----------+-----+-------+
| time|professor_id|student_id|score|student|
+----------+------------+----------+-----+-------+
|1596048041| p1| s1| 0.7| 1|
|1596048041| p2| s1| 0.9| 1|
|1596048041| p3| s1| 0.2| 1|
|1596048041| p4| s1| 0.2| 1|
|1596048041| p1| s2| 0.5| 2|
|1596048041| p2| s2| 0.1| 2|
|1596048041| p3| s2| 0.3| 2|
|1596048041| p4| s2| 0.3| 2|
|1596048041| p1| s3| 0.3| 3|
|1596048041| p2| s3| 0.15| 3|
|1596048041| p3| s3| 0.4| 3|
|1596048041| p4| s3| 0.35| 3|
|1596048041| p1| s4| 0.2| 4|
|1596048041| p2| s4| 0.2| 4|
|1596048041| p3| s4| 0.8| 4|
|1596048041| p4| s4| 0.4| 4|
+----------+------------+----------+-----+-------+
df3 = df.select('time','student_id','student').dropDuplicates()
+----------+----------+-------+
| time|student_id|student|
+----------+----------+-------+
|1596048041| s1| 1|
|1596048041| s2| 2|
|1596048041| s3| 3|
|1596048041| s4| 4|
+----------+----------+-------+
Paso 1: use pivot para encontrar la matriz de profesores frente a estudiantes, observe que establecemos un puntaje negativo en los valores de pivot para que podamos usar scipy.optimize.linear_sum_assignment para encontrar el costo mínimo de un problema de asignación:
df1 = df.groupby('time','professor_id').pivot('student', cols_student).agg(-first('score'))
+----------+------------+----+----+-----+----+
| time|professor_id| 1| 2| 3| 4|
+----------+------------+----+----+-----+----+
|1596048041| p4|-0.2|-0.3|-0.35|-0.4|
|1596048041| p2|-0.9|-0.1|-0.15|-0.2|
|1596048041| p1|-0.7|-0.5| -0.3|-0.2|
|1596048041| p3|-0.2|-0.3| -0.4|-0.8|
+----------+------------+----+----+-----+----+
Paso 2: use pandas_udf y scipy.optimize.linear_sum_assignment para obtener índices de columna y luego asigne el nombre de columna correspondiente a una nueva columna assigned
:
# returnSchema contains one more StringType column `assigned` than schema from the input pdf:
schema = StructType.fromJson(df1.schema.jsonValue()).add('assigned', 'string')
# since the # of students are always N, we can use np.vstack to set the N*N matrix
# below `n` is the number of professors/rows in pdf
# sz is the size of input Matrix, sz=4 in this example
def __find_assigned(pdf, sz):
cols = pdf.columns[2:]
n = pdf.shape[0]
n1 = pdf.iloc[:,2:].fillna(0).values
_, idx = linear_sum_assignment(np.vstack((n1,np.zeros((sz-n,sz)))))
return pdf.assign(assigned=[cols[i] for i in idx][:n])
find_assigned = pandas_udf(lambda x: __find_assigned(x,N), schema, PandasUDFType.GROUPED_MAP)
df2 = df1.groupby('time').apply(find_assigned)
+----------+------------+----+----+-----+----+--------+
| time|professor_id| 1| 2| 3| 4|assigned|
+----------+------------+----+----+-----+----+--------+
|1596048041| p4|-0.2|-0.3|-0.35|-0.4| 3|
|1596048041| p2|-0.9|-0.1|-0.15|-0.2| 1|
|1596048041| p1|-0.7|-0.5| -0.3|-0.2| 2|
|1596048041| p3|-0.2|-0.3| -0.4|-0.8| 4|
+----------+------------+----+----+-----+----+--------+
Nota: por sugerencia de @OluwafemiSule, podemos usar el parámetro maximize
en lugar de negar los valores de puntuación. este parámetro está disponible Ciencia ficción 1.4.0+:
_, idx = linear_sum_assignment(np.vstack((n1,np.zeros((N-n,N)))), maximize=True)
Paso 3: use la función de pila SparkSQL para normalizar el df2 anterior, niegue los valores de puntuación y filtre las filas con la puntuación NULL. el deseado is_match
la columna debería tener assigned==student
:
df_new = df2.selectExpr(
'time',
'professor_id',
'assigned',
'stack(,) as (student, score)'.format(len(cols_student), ','.join("int('0'), -`0`".format(c) for c in cols_student))
)
.filter("score is not NULL")
.withColumn('is_match', expr("assigned=student"))
df_new.show()
+----------+------------+--------+-------+-----+--------+
| time|professor_id|assigned|student|score|is_match|
+----------+------------+--------+-------+-----+--------+
|1596048041| p4| 3| 1| 0.2| false|
|1596048041| p4| 3| 2| 0.3| false|
|1596048041| p4| 3| 3| 0.35| true|
|1596048041| p4| 3| 4| 0.4| false|
|1596048041| p2| 1| 1| 0.9| true|
|1596048041| p2| 1| 2| 0.1| false|
|1596048041| p2| 1| 3| 0.15| false|
|1596048041| p2| 1| 4| 0.2| false|
|1596048041| p1| 2| 1| 0.7| false|
|1596048041| p1| 2| 2| 0.5| true|
|1596048041| p1| 2| 3| 0.3| false|
|1596048041| p1| 2| 4| 0.2| false|
|1596048041| p3| 4| 1| 0.2| false|
|1596048041| p3| 4| 2| 0.3| false|
|1596048041| p3| 4| 3| 0.4| false|
|1596048041| p3| 4| 4| 0.8| true|
+----------+------------+--------+-------+-----+--------+
Paso 4: use join para convertir al estudiante de nuevo a student_id (use broadcast join si es posible):
df_new = df_new.join(df3, on=["time", "student"])
+----------+-------+------------+--------+-----+--------+----------+
| time|student|professor_id|assigned|score|is_match|student_id|
+----------+-------+------------+--------+-----+--------+----------+
|1596048041| 1| p1| 2| 0.7| false| s1|
|1596048041| 2| p1| 2| 0.5| true| s2|
|1596048041| 3| p1| 2| 0.3| false| s3|
|1596048041| 4| p1| 2| 0.2| false| s4|
|1596048041| 1| p2| 1| 0.9| true| s1|
|1596048041| 2| p2| 1| 0.1| false| s2|
|1596048041| 3| p2| 1| 0.15| false| s3|
|1596048041| 4| p2| 1| 0.2| false| s4|
|1596048041| 1| p3| 4| 0.2| false| s1|
|1596048041| 2| p3| 4| 0.3| false| s2|
|1596048041| 3| p3| 4| 0.4| false| s3|
|1596048041| 4| p3| 4| 0.8| true| s4|
|1596048041| 1| p4| 3| 0.2| false| s1|
|1596048041| 2| p4| 3| 0.3| false| s2|
|1596048041| 3| p4| 3| 0.35| true| s3|
|1596048041| 4| p4| 3| 0.4| false| s4|
+----------+-------+------------+--------+-----+--------+----------+
df_new = df_new.drop("student", "assigned")