web-dev-qa-db-fra.com

Transposer une colonne en ligne avec Spark

J'essaie de transposer certaines colonnes de mon tableau en ligne. J'utilise Python et Spark 1.5.0. Voici mon tableau initial:

+-----+-----+-----+-------+
|  A  |col_1|col_2|col_...|
+-----+-------------------+
|  1  |  0.0|  0.6|  ...  |
|  2  |  0.6|  0.7|  ...  |
|  3  |  0.5|  0.9|  ...  |
|  ...|  ...|  ...|  ...  |

Je voudrais avoir quelque chose comme ça:

+-----+--------+-----------+
|  A  | col_id | col_value |
+-----+--------+-----------+
|  1  |   col_1|        0.0|
|  1  |   col_2|        0.6|   
|  ...|     ...|        ...|    
|  2  |   col_1|        0.6|
|  2  |   col_2|        0.7| 
|  ...|     ...|        ...|  
|  3  |   col_1|        0.5|
|  3  |   col_2|        0.9|
|  ...|     ...|        ...|

Est-ce que quelqu'un sait que je peux le faire? Merci de votre aide.

19
Raouf

C'est relativement simple à faire avec les fonctions de base de Spark SQL.

Python

from pyspark.sql.functions import array, col, explode, struct, lit

df = sc.parallelize([(1, 0.0, 0.6), (1, 0.6, 0.7)]).toDF(["A", "col_1", "col_2"])

def to_long(df, by):

    # Filter dtypes and split into column names and type description
    cols, dtypes = Zip(*((c, t) for (c, t) in df.dtypes if c not in by))
    # Spark SQL supports only homogeneous columns
    assert len(set(dtypes)) == 1, "All columns have to be of the same type"

    # Create and explode an array of (column_name, column_value) structs
    kvs = explode(array([
      struct(lit(c).alias("key"), col(c).alias("val")) for c in cols
    ])).alias("kvs")

    return df.select(by + [kvs]).select(by + ["kvs.key", "kvs.val"])

to_long(df, ["A"])

Scala :

import org.Apache.spark.sql.DataFrame
import org.Apache.spark.sql.functions.{array, col, explode, lit, struct}

val df = Seq((1, 0.0, 0.6), (1, 0.6, 0.7)).toDF("A", "col_1", "col_2")

def toLong(df: DataFrame, by: Seq[String]): DataFrame = {
  val (cols, types) = df.dtypes.filter{ case (c, _) => !by.contains(c)}.unzip
  require(types.distinct.size == 1, s"${types.distinct.toString}.length != 1")      

  val kvs = explode(array(
    cols.map(c => struct(lit(c).alias("key"), col(c).alias("val"))): _*
  ))

  val byExprs = by.map(col(_))

  df
    .select(byExprs :+ kvs.alias("_kvs"): _*)
    .select(byExprs ++ Seq($"_kvs.key", $"_kvs.val"): _*)
}

toLong(df, Seq("A"))
27
zero323

Les bibliothèques d'algèbre linéaire locale de Spark sont actuellement très faibles: elles n'incluent pas d'opérations de base comme ci-dessus.

Il existe une JIRA pour résoudre ce problème pour Spark 2.1 - mais cela ne vous aidera pas aujourd'hui .

Quelque chose à considérer: effectuer une transposition nécessitera probablement un remaniement complet des données.

Pour le moment, vous devrez écrire directement le code RDD. J'ai écrit transpose en scala - mais pas en python. Voici la version scala:

 def transpose(mat: DMatrix) = {
    val nCols = mat(0).length
    val matT = mat
      .flatten
      .zipWithIndex
      .groupBy {
      _._2 % nCols
    }
      .toSeq.sortBy {
      _._1
    }
      .map(_._2)
      .map(_.map(_._1))
      .toArray
    matT
  }

Vous pouvez donc convertir cela en python pour votre usage. Je n'ai pas de bande passante pour écrire/tester cela à ce moment particulier: faites le moi savoir si vous étiez incapable de faire cette conversion.

Au minimum, les éléments suivants sont facilement convertis en python

  • zipWithIndex -> enumerate() (équivalent en python - crédit de @ zero323)
  • map -> [someOperation(x) for x in ..] 
  • groupBy -> itertools.groupBy()

Voici l'implémentation de flatten qui n'a pas d'équivalent python: 

  def flatten(L):
        for item in L:
            try:
                for i in flatten(item):
                    yield i
            except TypeError:
                yield item

Vous devriez donc pouvoir les mettre ensemble pour trouver une solution.

4
javadba

Utilisez flatmap. Quelque chose comme ci-dessous devrait fonctionner

from pyspark.sql import Row

def rowExpander(row):
    rowDict = row.asDict()
    valA = rowDict.pop('A')
    for k in rowDict:
        yield Row(**{'A': valA , 'colID': k, 'colValue': row[k]})

newDf = sqlContext.createDataFrame(df.rdd.flatMap(rowExpander))
2
David

J'ai pris la réponse Scala que @javadba avait écrite et créé une version Python pour transposer toutes les colonnes dans une DataFrame. Cela pourrait être un peu différent de ce que l'OP demandait ...

from itertools import chain
from pyspark.sql import DataFrame


def _sort_transpose_Tuple(tup):
    x, y = tup
    return x, Tuple(Zip(*sorted(y, key=lambda v_k: v_k[1], reverse=False)))[0]


def transpose(X):
    """Transpose a PySpark DataFrame.

    Parameters
    ----------
    X : PySpark ``DataFrame``
        The ``DataFrame`` that should be tranposed.
    """
    # validate
    if not isinstance(X, DataFrame):
        raise TypeError('X should be a DataFrame, not a %s' 
                        % type(X))

    cols = X.columns
    n_features = len(cols)

    # Sorry for this unreadability...
    return X.rdd.flatMap( # make into an RDD
        lambda xs: chain(xs)).zipWithIndex().groupBy( # Zip index
        lambda val_idx: val_idx[1] % n_features).sortBy( # group by index % n_features as key
        lambda grp_res: grp_res[0]).map( # sort by index % n_features key
        lambda grp_res: _sort_transpose_Tuple(grp_res)).map( # maintain order
        lambda key_col: key_col[1]).toDF() # return to DF

Par exemple:

>>> X = sc.parallelize([(1,2,3), (4,5,6), (7,8,9)]).toDF()
>>> X.show()
+---+---+---+
| _1| _2| _3|
+---+---+---+
|  1|  2|  3|
|  4|  5|  6|
|  7|  8|  9|
+---+---+---+

>>> transpose(X).show()
+---+---+---+
| _1| _2| _3|
+---+---+---+
|  1|  4|  7|
|  2|  5|  8|
|  3|  6|  9|
+---+---+---+
1
Tgsmith61591

Un moyen très pratique d'implémenter:

from pyspark.sql import Row

def rowExpander(row):
    rowDict = row.asDict()
    valA = rowDict.pop('A')
    for k in rowDict:
        yield Row(**{'A': valA , 'colID' : k, 'colValue' : row[k]})

    newDf = sqlContext.createDataFrame(df.rdd.flatMap(rowExpander)
1
Parul Singh

Une façon de résoudre avec pyspark sql en utilisant les fonctions create_map et explode.

from pyspark.sql import functions as func
#Use `create_map` to create the map of columns with constant 
df = df.withColumn('mapCol', \
                    func.create_map(func.lit('col_1'),df.col_1,
                                    func.lit('col_2'),df.col_2,
                                    func.lit('col_3'),df.col_3
                                   ) 
                  )
#Use explode function to explode the map 
res = df.select('*',func.explode(df.mapCol).alias('col_id','col_value'))
res.show()
0
Vamsi Prabhala