Comment gérer des données catégoriques avec spark-ml
et non spark-mllib
?
Bien que la documentation ne soit pas très claire, il semble que les classificateurs, par exemple RandomForestClassifier
, LogisticRegression
, ont un argument featuresCol
, qui spécifie le nom de la colonne de fonctions dans DataFrame
, et un argument labelCol
, qui spécifie le nom de la colonne de classes étiquetées dans DataFrame
.
Évidemment, je veux utiliser plus d'une fonctionnalité dans ma prédiction, alors j'ai essayé d'utiliser VectorAssembler
pour mettre toutes mes fonctionnalités dans un seul vecteur sous featuresCol
.
Cependant, la VectorAssembler
accepte uniquement les types numériques, le type booléen et le type de vecteur (selon le site Web Spark), je ne peux donc pas insérer de chaînes dans mon vecteur de fonctionnalités.
Comment dois-je procéder?
Je voulais juste compléter la réponse de Holden.
Depuis Spark 2.3.0, OneHotEncoder
est obsolète et sera supprimé dans 3.0.0
. Veuillez utiliser OneHotEncoderEstimator
à la place.
En Scala:
import org.Apache.spark.ml.Pipeline
import org.Apache.spark.ml.feature.{OneHotEncoderEstimator, StringIndexer}
val df = Seq((0, "a", 1), (1, "b", 2), (2, "c", 3), (3, "a", 4), (4, "a", 4), (5, "c", 3)).toDF("id", "category1", "category2")
val indexer = new StringIndexer().setInputCol("category1").setOutputCol("category1Index")
val encoder = new OneHotEncoderEstimator()
.setInputCols(Array(indexer.getOutputCol, "category2"))
.setOutputCols(Array("category1Vec", "category2Vec"))
val pipeline = new Pipeline().setStages(Array(indexer, encoder))
pipeline.fit(df).transform(df).show
// +---+---------+---------+--------------+-------------+-------------+
// | id|category1|category2|category1Index| category1Vec| category2Vec|
// +---+---------+---------+--------------+-------------+-------------+
// | 0| a| 1| 0.0|(2,[0],[1.0])|(4,[1],[1.0])|
// | 1| b| 2| 2.0| (2,[],[])|(4,[2],[1.0])|
// | 2| c| 3| 1.0|(2,[1],[1.0])|(4,[3],[1.0])|
// | 3| a| 4| 0.0|(2,[0],[1.0])| (4,[],[])|
// | 4| a| 4| 0.0|(2,[0],[1.0])| (4,[],[])|
// | 5| c| 3| 1.0|(2,[1],[1.0])|(4,[3],[1.0])|
// +---+---------+---------+--------------+-------------+-------------+
En Python:
from pyspark.ml import Pipeline
from pyspark.ml.feature import StringIndexer, OneHotEncoderEstimator
df = spark.createDataFrame([(0, "a", 1), (1, "b", 2), (2, "c", 3), (3, "a", 4), (4, "a", 4), (5, "c", 3)], ["id", "category1", "category2"])
indexer = StringIndexer(inputCol="category1", outputCol="category1Index")
inputs = [indexer.getOutputCol(), "category2"]
encoder = OneHotEncoderEstimator(inputCols=inputs, outputCols=["categoryVec1", "categoryVec2"])
pipeline = Pipeline(stages=[indexer, encoder])
pipeline.fit(df).transform(df).show()
# +---+---------+---------+--------------+-------------+-------------+
# | id|category1|category2|category1Index| categoryVec1| categoryVec2|
# +---+---------+---------+--------------+-------------+-------------+
# | 0| a| 1| 0.0|(2,[0],[1.0])|(4,[1],[1.0])|
# | 1| b| 2| 2.0| (2,[],[])|(4,[2],[1.0])|
# | 2| c| 3| 1.0|(2,[1],[1.0])|(4,[3],[1.0])|
# | 3| a| 4| 0.0|(2,[0],[1.0])| (4,[],[])|
# | 4| a| 4| 0.0|(2,[0],[1.0])| (4,[],[])|
# | 5| c| 3| 1.0|(2,[1],[1.0])|(4,[3],[1.0])|
# +---+---------+---------+--------------+-------------+-------------+
Depuis Spark 1.4.0, MLLib fournit également la fonction OneHotEncoder , qui mappe une colonne d’index d’étiquette sur une colonne de vecteurs binaires, avec au plus une valeur unique.
Cet encodage permet aux algorithmes qui attendent des fonctionnalités continues, telles que la régression logistique, d’utiliser des fonctionnalités catégorielles.
Considérons la DataFrame
suivante:
val df = Seq((0, "a"),(1, "b"),(2, "c"),(3, "a"),(4, "a"),(5, "c"))
.toDF("id", "category")
La première étape serait de créer la DataFrame
indexée avec la StringIndexer
:
import org.Apache.spark.ml.feature.StringIndexer
val indexer = new StringIndexer()
.setInputCol("category")
.setOutputCol("categoryIndex")
.fit(df)
val indexed = indexer.transform(df)
indexed.show
// +---+--------+-------------+
// | id|category|categoryIndex|
// +---+--------+-------------+
// | 0| a| 0.0|
// | 1| b| 2.0|
// | 2| c| 1.0|
// | 3| a| 0.0|
// | 4| a| 0.0|
// | 5| c| 1.0|
// +---+--------+-------------+
Vous pouvez ensuite encoder la categoryIndex
avec OneHotEncoder
:
import org.Apache.spark.ml.feature.OneHotEncoder
val encoder = new OneHotEncoder()
.setInputCol("categoryIndex")
.setOutputCol("categoryVec")
val encoded = encoder.transform(indexed)
encoded.select("id", "categoryVec").show
// +---+-------------+
// | id| categoryVec|
// +---+-------------+
// | 0|(2,[0],[1.0])|
// | 1| (2,[],[])|
// | 2|(2,[1],[1.0])|
// | 3|(2,[0],[1.0])|
// | 4|(2,[0],[1.0])|
// | 5|(2,[1],[1.0])|
// +---+-------------+
Je vais vous donner une réponse d'un autre point de vue, car je m'interrogeais également sur les caractéristiques catégoriques relatives aux modèles arborescents dans Spark ML (et non sur MLlib), et la documentation n'est pas claire.
Lorsque vous transformez une colonne de votre cadre de données à l'aide de pyspark.ml.feature.StringIndexer
, des métadonnées supplémentaires sont stockées dans le cadre de données qui marque spécifiquement l'entité transformée en tant que caractéristique catégorielle.
Lorsque vous imprimez le cadre de données, vous verrez une valeur numérique (qui correspond à un index correspondant à l'une de vos valeurs catégoriques) et si vous examinez le schéma, vous constaterez que votre nouvelle colonne transformée est de type double
. Cependant, cette nouvelle colonne que vous avez créée avec pyspark.ml.feature.StringIndexer.transform
n’est pas simplement une colonne double normale, elle est associée à des métadonnées supplémentaires qui sont très importantes. Vous pouvez inspecter ces méta-données en consultant la propriété metadata
du champ approprié dans le schéma de votre cadre de données (vous pouvez accéder aux objets de schéma de votre cadre de données en examinant le fichier yourdataframe.schema).
Ces métadonnées supplémentaires ont deux implications importantes:
Lorsque vous appelez .fit()
lorsque vous utilisez un modèle basé sur une arborescence, il analyse les métadonnées de votre image de données et reconnaît les champs que vous avez codés de manière catégorique avec des transformateurs tels que pyspark.ml.feature.StringIndexer
(comme indiqué ci-dessus, d'autres transformateurs auront également cet effet, par exemple: pyspark.ml.feature.VectorIndexer
). De ce fait, vous n'avez PAS à coder vos fonctions après une transformation à l'aide de StringIndxer lors de l'utilisation de modèles basés sur des arbres dans spark ML (toutefois, vous devez toujours effectuer un codage à une utilisation lorsque vous utilisez d'autres modèles qui ne le sont pas. gérer naturellement des catégories comme la régression linéaire, etc.).
Étant donné que ces métadonnées sont stockées dans le bloc de données, vous pouvez utiliser pyspark.ml.feature.IndexToString
pour inverser les index numériques aux valeurs catégorielles d'origine (qui sont souvent des chaînes) à tout moment.
Il existe un composant du pipeline ML appelé StringIndexer
que vous pouvez utiliser pour convertir vos chaînes en doubles de manière raisonnable. http://spark.Apache.org/docs/latest/api/scala/index.html#org.Apache.spark.ml.feature.StringIndexer a plus de documentation et http: // spark Apache.org/docs/latest/ml-guide.html montre comment construire des pipelines.
J'utilise la méthode suivante pour oneHotEncoding une seule colonne dans un Spark dataFrame:
def ohcOneColumn(df, colName, debug=False):
colsToFillNa = []
if debug: print("Entering method ohcOneColumn")
countUnique = df.groupBy(colName).count().count()
if debug: print(countUnique)
collectOnce = df.select(colName).distinct().collect()
for uniqueValIndex in range(countUnique):
uniqueVal = collectOnce[uniqueValIndex][0]
if debug: print(uniqueVal)
newColName = str(colName) + '_' + str(uniqueVal) + '_TF'
df = df.withColumn(newColName, df[colName]==uniqueVal)
colsToFillNa.append(newColName)
df = df.drop(colName)
df = df.na.fill(False, subset=colsToFillNa)
return df
J'utilise la méthode suivante pour oneHotEncoding Spark dataFrames:
from pyspark.sql.functions import col, countDistinct, approxCountDistinct
from pyspark.ml.feature import StringIndexer
from pyspark.ml.feature import OneHotEncoderEstimator
def detectAndLabelCat(sparkDf, minValCount=5, debug=False, excludeCols=['Target']):
if debug: print("Entering method detectAndLabelCat")
newDf = sparkDf
colList = sparkDf.columns
for colName in sparkDf.columns:
uniqueVals = sparkDf.groupBy(colName).count()
if debug: print(uniqueVals)
countUnique = uniqueVals.count()
dtype = str(sparkDf.schema[colName].dataType)
#dtype = str(df.schema[nc].dataType)
if (colName in excludeCols):
if debug: print(str(colName) + ' is in the excluded columns list.')
Elif countUnique == 1:
newDf = newDf.drop(colName)
if debug:
print('dropping column ' + str(colName) + ' because it only contains one unique value.')
#end if debug
#Elif (1==2):
Elif ((countUnique < minValCount) | (dtype=="String") | (dtype=="StringType")):
if debug:
print(len(newDf.columns))
oldColumns = newDf.columns
newDf = ohcOneColumn(newDf, colName, debug=debug)
if debug:
print(len(newDf.columns))
newColumns = set(newDf.columns) - set(oldColumns)
print('Adding:')
print(newColumns)
for newColumn in newColumns:
if newColumn in newDf.columns:
try:
newUniqueValCount = newDf.groupBy(newColumn).count().count()
print("There are " + str(newUniqueValCount) + " unique values in " + str(newColumn))
except:
print('Uncaught error discussing ' + str(newColumn))
#else:
# newColumns.remove(newColumn)
print('Dropping:')
print(set(oldColumns) - set(newDf.columns))
else:
if debug: print('Nothing done for column ' + str(colName))
#end if countUnique == 1, Elif countUnique other condition
#end outer for
return newDf
Vous pouvez convertir un type de colonne string dans un cadre de données spark en un type de données numerical à l'aide de la fonction de conversion.
from pyspark.sql import SQLContext
from pyspark.sql.types import DoubleType, IntegerType
sqlContext = SQLContext(sc)
dataset = sqlContext.read.format('com.databricks.spark.csv').options(header='true').load('./data/titanic.csv')
dataset = dataset.withColumn("Age", dataset["Age"].cast(DoubleType()))
dataset = dataset.withColumn("Survived", dataset["Survived"].cast(IntegerType()))
Dans l'exemple ci-dessus, nous lisons dans un fichier csv un cadre de données, convertissons les types de données de chaîne par défaut en nombres entier et double et remplaçons le cadre de données d'origine. Nous pouvons ensuite utiliser VectorAssembler pour fusionner les entités dans un seul vecteur et appliquer votre algorithme Spark ML préféré.