web-dev-qa-db-fra.com

Comment effectuer l'union sur deux DataFrames avec différentes quantités de colonnes en spark?

J'ai 2 DataFrames comme suit:

Source data

J'ai besoin d'union comme celle-ci:

enter image description here

La fonction unionAll ne fonctionne pas car le nombre et le nom des colonnes sont différents.

Comment puis-je faire ceci?

37
Allan Feliph

Dans Scala, il vous suffit d'ajouter toutes les colonnes manquantes sous la forme nulls.

import org.Apache.spark.sql.functions._

// let df1 and df2 the Dataframes to merge
val df1 = sc.parallelize(List(
  (50, 2),
  (34, 4)
)).toDF("age", "children")

val df2 = sc.parallelize(List(
  (26, true, 60000.00),
  (32, false, 35000.00)
)).toDF("age", "education", "income")

val cols1 = df1.columns.toSet
val cols2 = df2.columns.toSet
val total = cols1 ++ cols2 // union

def expr(myCols: Set[String], allCols: Set[String]) = {
  allCols.toList.map(x => x match {
    case x if myCols.contains(x) => col(x)
    case _ => lit(null).as(x)
  })
}

df1.select(expr(cols1, total):_*).unionAll(df2.select(expr(cols2, total):_*)).show()

+---+--------+---------+-------+
|age|children|education| income|
+---+--------+---------+-------+
| 50|       2|     null|   null|
| 34|       4|     null|   null|
| 26|    null|     true|60000.0|
| 32|    null|    false|35000.0|
+---+--------+---------+-------+

Mise à jour

Les deux DataFrames temporels auront le même ordre de colonnes, car nous effectuons un mappage à travers total dans les deux cas.

df1.select(expr(cols1, total):_*).show()
df2.select(expr(cols2, total):_*).show()

+---+--------+---------+------+
|age|children|education|income|
+---+--------+---------+------+
| 50|       2|     null|  null|
| 34|       4|     null|  null|
+---+--------+---------+------+

+---+--------+---------+-------+
|age|children|education| income|
+---+--------+---------+-------+
| 26|    null|     true|60000.0|
| 32|    null|    false|35000.0|
+---+--------+---------+-------+
38
Alberto Bonsanto

Un moyen très simple de faire cela - select les colonnes dans le même ordre à partir des images et utiliser unionAll

df1.select('code', 'date', 'A', 'B', 'C', lit(None).alias('D'), lit(None).alias('E'))\
   .unionAll(df2.select('code', 'date', lit(None).alias('A'), 'B', 'C', 'D', 'E'))
9
Rags

Voici une solution pyspark.

Il suppose que si un champ dans df1 est absent de df2, alors vous ajoutez ce champ manquant à df2 avec des valeurs nulles. Cependant, il suppose également que si le champ existe dans les deux images, mais que le type ou la possibilité de nullité du champ est différent, les deux images sont en conflit et ne peuvent pas être combinées. Dans ce cas, je lève un TypeError.

from pyspark.sql.functions import lit

def harmonize_schemas_and_combine(df_left, df_right):
    left_types = {f.name: f.dataType for f in df_left.schema}
    right_types = {f.name: f.dataType for f in df_right.schema}
    left_fields = set((f.name, f.dataType, f.nullable) for f in df_left.schema)
    right_fields = set((f.name, f.dataType, f.nullable) for f in df_right.schema)

    # First go over left-unique fields
    for l_name, l_type, l_nullable in left_fields.difference(right_fields):
        if l_name in right_types:
            r_type = right_types[l_name]
            if l_type != r_type:
                raise TypeError, "Union failed. Type conflict on field %s. left type %s, right type %s" % (l_name, l_type, r_type)
            else:
                raise TypeError, "Union failed. Nullability conflict on field %s. left nullable %s, right nullable %s"  % (l_name, l_nullable, not(l_nullable))
        df_right = df_right.withColumn(l_name, lit(None).cast(l_type))

    # Now go over right-unique fields
    for r_name, r_type, r_nullable in right_fields.difference(left_fields):
        if r_name in left_types:
            l_type = left_types[r_name]
            if r_type != l_type:
                raise TypeError, "Union failed. Type conflict on field %s. right type %s, left type %s" % (r_name, r_type, l_type)
            else:
                raise TypeError, "Union failed. Nullability conflict on field %s. right nullable %s, left nullable %s" % (r_name, r_nullable, not(r_nullable))
        df_left = df_left.withColumn(r_name, lit(None).cast(r_type))    

    # Make sure columns are in the same order
    df_left = df_left.select(df_right.columns)

    return df_left.union(df_right)
7
conradlee

Version modifiée d'Alberto Bonsanto pour conserver l'ordre des colonnes d'origine (OP supposait que l'ordre devait correspondre aux tables d'origine). De plus, la partie match a généré un avertissement Intellij.

Voici ma version:

def unionDifferentTables(df1: DataFrame, df2: DataFrame): DataFrame = {

  val cols1 = df1.columns.toSet
  val cols2 = df2.columns.toSet
  val total = cols1 ++ cols2 // union

  val order = df1.columns ++  df2.columns
  val sorted = total.toList.sortWith((a,b)=> order.indexOf(a) < order.indexOf(b))

  def expr(myCols: Set[String], allCols: List[String]) = {
      allCols.map( {
        case x if myCols.contains(x) => col(x)
        case y => lit(null).as(y)
      })
  }

  df1.select(expr(cols1, sorted): _*).unionAll(df2.select(expr(cols2, sorted): _*))
}
4
swdev

Voici le code pour Python 3.0 utilisant pyspark:

from pyspark.sql import SQLContext
import pyspark
from pyspark.sql.functions import lit

def __orderDFAndAddMissingCols(df, columnsOrderList, dfMissingFields):
    ''' return ordered dataFrame by the columns order list with null in missing columns '''
    if not dfMissingFields:  #no missing fields for the df
        return df.select(columnsOrderList)
    else:
        columns = []
        for colName in columnsOrderList:
            if colName not in dfMissingFields:
                columns.append(colName)
            else:
                columns.append(lit(None).alias(colName))
        return df.select(columns)

def __addMissingColumns(df, missingColumnNames):
    ''' Add missing columns as null in the end of the columns list '''
    listMissingColumns = []
    for col in missingColumnNames:
        listMissingColumns.append(lit(None).alias(col))

    return df.select(df.schema.names + listMissingColumns)

def __orderAndUnionDFs( leftDF, rightDF, leftListMissCols, rightListMissCols):
    ''' return union of data frames with ordered columns by leftDF. '''
    leftDfAllCols = __addMissingColumns(leftDF, leftListMissCols)
    rightDfAllCols = __orderDFAndAddMissingCols(rightDF, leftDfAllCols.schema.names, rightListMissCols)
    return leftDfAllCols.union(rightDfAllCols)

def unionDFs(leftDF,rightDF):
    ''' Union between two dataFrames, if there is a gap of column fields,
     it will append all missing columns as nulls '''
    # Check for None input
    if leftDF == None:
        raise ValueError('leftDF parameter should not be None')
    if rightDF == None:
        raise ValueError('rightDF parameter should not be None')
        #For data frames with equal columns and order- regular union
    if leftDF.schema.names == rightDF.schema.names:
        return leftDF.union(rightDF)
    else: # Different columns
        #Save dataFrame columns name list as set
        leftDFColList = set(leftDF.schema.names)
        rightDFColList = set(rightDF.schema.names)
        # Diff columns between leftDF and rightDF
        rightListMissCols = list(leftDFColList - rightDFColList)
        leftListMissCols = list(rightDFColList - leftDFColList)
        return __orderAndUnionDFs(leftDF, rightDF, leftListMissCols, rightListMissCols)


if __== '__main__':
    sc = pyspark.SparkContext()
    sqlContext = SQLContext(sc)
    leftDF = sqlContext.createDataFrame( [(1, 2, 11), (3, 4, 12)] , ('a','b','d'))
    rightDF = sqlContext.createDataFrame( [(5, 6 , 9), (7, 8, 10)] , ('b','a','c'))

    unionDF = unionDFs(leftDF,rightDF)
    print(unionDF.select(unionDF.schema.names).show())
3
Eli B

J'ai eu le même problème et en utilisant rejoindre au lieu d'union résolu mon problème. Ainsi, par exemple avec python, au lieu de cette ligne de code: result = left.union(right), qui ne pourra pas s'exécuter pour un nombre différent de colonnes, vous devriez utiliser celle-ci:

result = left.join(right, left.columns if (len(left.columns) < len(right.columns)) else right.columns, "outer")

Notez que le deuxième argument contient les colonnes communes entre les deux DataFrames. Si vous ne l'utilisez pas, le résultat aura des colonnes dupliquées, l'une d'elles étant null et l'autre pas. J'espère que ça aide.

2
drkostas

Il y a beaucoup de manière concise de traiter cette question avec un sacrifice modéré de performance.

def unionWithDifferentSchema(a: DataFrame, b: DataFrame): DataFrame = {
    sparkSession.read.json(a.toJSON.union(b.toJSON).rdd)
}

C'est la fonction qui fait l'affaire. L'utilisation de toJSON avec chaque structure de données crée une union json. Cela préserve le classement et le type de données.

La seule prise est toJSON est relativement cher (cependant, vous obtenez probablement un ralentissement de 10-15%). Cependant, cela garde le code propre.

2

Voici ma Python:

from pyspark.sql import SparkSession, HiveContext
from pyspark.sql.functions import lit
from pyspark.sql import Row

def customUnion(df1, df2):
    cols1 = df1.columns
    cols2 = df2.columns
    total_cols = sorted(cols1 + list(set(cols2) - set(cols1)))
    def expr(mycols, allcols):
        def processCols(colname):
            if colname in mycols:
                return colname
            else:
                return lit(None).alias(colname)
        cols = map(processCols, allcols)
        return list(cols)
    appended = df1.select(expr(cols1, total_cols)).union(df2.select(expr(cols2, total_cols)))
    return appended

Voici un exemple d'utilisation:

data = [
    Row(Zip_code=58542, dma='MIN'),
    Row(Zip_code=58701, dma='MIN'),
    Row(Zip_code=57632, dma='MIN'),
    Row(Zip_code=58734, dma='MIN')
]

firstDF = spark.createDataFrame(data)

data = [
    Row(Zip_code='534', name='MIN'),
    Row(Zip_code='353', name='MIN'),
    Row(Zip_code='134', name='MIN'),
    Row(Zip_code='245', name='MIN')
]

secondDF = spark.createDataFrame(data)

customUnion(firstDF,secondDF).show()
1
user2102359

en voici un autre:

def unite(df1: DataFrame, df2: DataFrame): DataFrame = {
    val cols1 = df1.columns.toSet
    val cols2 = df2.columns.toSet
    val total = (cols1 ++ cols2).toSeq.sorted
    val expr1 = total.map(c => {
      if (cols1.contains(c)) c else "NULL as " + c
    })
    val expr2 = total.map(c => {
      if (cols2.contains(c)) c else "NULL as " + c
    })
    df1.selectExpr(expr1:_*).union(
      df2.selectExpr(expr2:_*)
    )
}
0
Yosi Hammer

Union et union externe pour la concaténation Pyspark DataFrame. Cela fonctionne pour plusieurs trames de données avec différentes colonnes.

def union_all(*dfs):
    return reduce(ps.sql.DataFrame.unionAll, dfs)

def outer_union_all(*dfs):

    all_cols = set([])
    for df in dfs:
        all_cols |= set(df.columns) 
    all_cols = list(all_cols)
    print(all_cols)

    def expr(cols, all_cols):

        def append_cols(col):
            if col in cols:
                return col
            else:
                return sqlfunc.lit(None).alias(col)

        cols_ = map(append_cols, all_cols)
        return list(cols_)

    union_df = union_all(*[df.select(expr(df.columns, all_cols)) for df in dfs])
    return union_df
0
aysa