Fundamentos de DataFrames
Crea DataFrames a partir de RDDs, archivos CSV y JSON; comprende la inferencia de esquema vs definición explícita de esquema
Fundamentos de DataFrames
Los DataFrames son la API de alto nivel principal en Spark moderno. Organizan datos en columnas con nombre, similar a una tabla en una base de datos relacional o un DataFrame en la biblioteca pandas de Python. Los DataFrames proporcionan mejor rendimiento que los RDDs gracias al optimizador Catalyst y al motor de ejecución Tungsten.
DataFrame vs RDD
| Aspecto | RDD | DataFrame |
|---|---|---|
| Nivel de API | Bajo nivel | Alto nivel |
| Esquema | Sin esquema (objetos brutos) | Columnas con nombre y tipos |
| Optimización | Optimización manual | Optimizador Catalyst |
| Serialización | Serialización Java/Python | Tungsten (formato binario) |
| Rendimiento | Más lento | 5-10x más rápido |
| Soporte SQL | No | Sí (Spark SQL) |
| Caso de uso | Datos no estructurados, lógica personalizada | Datos estructurados/semiestructurados |
En Spark 3.x, DataFrames son la API recomendada para la mayoría de los casos de uso. Ofrecen mejor rendimiento, optimizaciones más ricas y una interfaz más amigable.
Creando DataFrames
Desde una SparkSession
from pyspark.sql import SparkSession
from pyspark.sql.types import StructType, StructField, StringType, IntegerType
spark = SparkSession.builder \
.appName("DataFrameBasics") \
.master("local[*]") \
.getOrCreate()Desde una Lista de Rows
from pyspark.sql import Row
data = [
Row(name="Alice", age=34, dept="Engineering"),
Row(name="Bob", age=28, dept="Design"),
Row(name="Charlie", age=41, dept="Engineering"),
Row(name="Diana", age=25, dept="Marketing")
]
df = spark.createDataFrame(data)
df.show()
# +-------+---+-----------+
# | name|age| dept|
# +-------+---+-----------+
# | Alice| 34|Engineering|
# | Bob| 28| Design|
# |Charlie| 41|Engineering|
# | Diana| 25| Marketing|
# +-------+---+-----------+Desde una Lista de Diccionarios Python
data = [
{"name": "Alice", "age": 34, "dept": "Engineering"},
{"name": "Bob", "age": 28, "dept": "Design"}
]
df = spark.createDataFrame(data)
df.printSchema()
# root
# |-- name: string (nullable = true)
# |-- age: long (nullable = true)
# |-- dept: string (nullable = true)Desde un RDD
rdd = sc.parallelize([
("Alice", 34, "Engineering"),
("Bob", 28, "Design"),
("Charlie", 41, "Engineering")
])
# Inferir nombres y tipos de columnas
df = rdd.toDF(["name", "age", "dept"])
df.show()El método toDF() en RDDs es el puente más simple entre las APIs RDD y DataFrame. El esquema se infiere a partir de los tipos de la primera fila.
Desde Archivos CSV
# Inferencia de esquema (Spark lee las primeras filas para inferir tipos)
df = spark.read.csv("data/people.csv", header=True, inferSchema=True)
df.show()
df.printSchema()
# Sin encabezado — Spark asigna nombres _c0, _c1, ...
df_no_header = spark.read.csv("data/people.csv", inferSchema=True)
df_no_header.show()| Opción CSV | Predeterminado | Descripción |
|---|---|---|
header | false | Primera línea son nombres de columnas |
inferSchema | false | Inferir tipos de columna desde datos |
sep | , | Delimitador de campo |
quote | " | Carácter de cita |
escape | \ | Carácter de escape |
mode | PERMISSIVE | Modo de análisis (PERMISSIVE, DROPMALFORMED, FAILFAST) |
nullValue | "" | Qué string representa nulo |
dateFormat | yyyy-MM-dd | String de formato de fecha |
# Opciones CSV explícitas
df = spark.read \
.option("header", "true") \
.option("inferSchema", "true") \
.option("sep", "|") \
.option("nullValue", "NA") \
.csv("data/pipe_delimited.txt")Desde Archivos JSON
# JSON (cada línea es un objeto JSON)
df = spark.read.json("data/people.json")
df.show()
df.printSchema()
# JSON multi-línea (pretty-printed)
df = spark.read \
.option("multiLine", "true") \
.json("data/people_pretty.json")Spark espera archivos JSON en formato "JSON Lines" donde cada línea es un objeto JSON separado. Para JSON pretty-printed (múltiples líneas por objeto), use multiLine=true.
Inferencia de Esquema vs Esquema Explícito
Inferencia de Esquema
Spark lee una muestra de los datos para determinar nombres y tipos de columnas.
df_inferred = spark.read \
.option("inferSchema", "true") \
.option("samplingRatio", "0.1") \
.csv("data/large_file.csv")Ventajas: Rápido, sin mantenimiento de código, bueno para exploración Desventajas:
- Pasada extra sobre los datos (hasta 10% más tiempo)
- Puede inferir tipos incorrectos (string en lugar de int si las primeras 1000 filas son nulas)
- Sobrecarga de rendimiento
La inferencia de esquema lee una muestra de los datos para determinar tipos. Si la muestra no es representativa (por ejemplo, todos los valores nulos en las primeras filas), obtiene tipos incorrectos. Siempre verifique esquemas inferidos en datos de producción.
Esquema Explícito
Defina el esquema programáticamente para control total y mejor rendimiento.
from pyspark.sql.types import StructType, StructField, StringType, IntegerType, DoubleType
schema = StructType([
StructField("name", StringType(), nullable=False),
StructField("age", IntegerType(), nullable=True),
StructField("salary", DoubleType(), nullable=True),
StructField("dept", StringType(), nullable=False)
])
df = spark.read \
.schema(schema) \
.csv("data/people.csv")
print(df.printSchema())Ventajas:
- Sin pasada extra para muestreo
- Seguridad de tipos — sin cambios sorpresa de tipo
- Mejor rendimiento (sin sobrecarga de inferencia)
Desventajas:
- Más código para escribir
- El esquema debe coincidir exactamente con los datos
Sintaxis de String DDL
Spark soporta strings DDL para definiciones compactas de esquema.
ddl_schema = "name STRING, age INT, salary DOUBLE, dept STRING"
df = spark.read \
.schema(ddl_schema) \
.csv("data/people.csv")Explorando el Esquema del DataFrame
# Imprimir árbol del esquema
df.printSchema()
# Obtener esquema como objeto StructType
schema = df.schema
print(schema)
# Obtener nombres de columnas
print(df.columns) # ['name', 'age', 'dept']
# Obtener tipos de datos
for field in df.schema.fields:
print(f"{field.name}: {field.dataType}")Tipos de Datos en Spark
| DataType | Descripción | Ejemplo |
|---|---|---|
StringType | Strings de texto | "hello" |
IntegerType | Entero de 4 bytes | 42 |
LongType | Entero de 8 bytes | 10000000000L |
FloatType | Float de 4 bytes | 3.14f |
DoubleType | Float de 8 bytes | 3.14159265 |
BooleanType | Verdadero/Falso | True |
DateType | Solo fecha | 2024-01-15 |
TimestampType | Fecha y hora | 2024-01-15 14:30:00 |
ArrayType | Lista de elementos | [1, 2, 3] |
MapType | Pares clave-valor | {"a": 1} |
StructType | Estructura anidada | (a: 1, b: "x") |
Manejando Valores Nulos
# Crear DataFrame con nulos
data = [
(1, "Alice", None),
(2, None, 50000.0),
(3, "Bob", 60000.0)
]
df = spark.createDataFrame(data, ["id", "name", "salary"])
# Eliminar filas con cualquier nulo
df.dropna().show()
# Eliminar filas donde columnas específicas son nulas
df.dropna(subset=["name", "salary"]).show()
# Rellenar nulos con un valor
df.fillna({"name": "Unknown", "salary": 0.0}).show()Conclusiones Clave
- Los DataFrames son la API Spark recomendada para datos estructurados
- Cree DataFrames desde RDDs, CSV, JSON, colecciones Python y bases de datos
- La inferencia de esquema es conveniente pero peligrosa para producción
- Los esquemas explícitos proporcionan seguridad de tipos y mejor rendimiento
- Las strings DDL ofrecen definiciones compactas de esquema
- Las funciones de manejo de nulos (
dropna,fillna) gestionan datos faltantes
Preguntas de Práctica
- ¿Qué ventajas tienen los DataFrames sobre los RDDs?
- ¿Cómo mejora el optimizador Catalyst el rendimiento del DataFrame?
- ¿Cuál es la diferencia entre
toDF()ycreateDataFrame()? - ¿Por qué la inferencia de esquema es potencialmente peligrosa para pipelines de producción?
- ¿Cómo se define un esquema con una string DDL?
- ¿Qué opciones CSV controlan el encabezado y el manejo del delimitador?
- ¿Cuándo usaría
multiLine=truepara archivos JSON? - ¿Qué es el formato "JSON Lines" y por qué Spark lo prefiere?
- ¿Cómo se verifica el esquema de un DataFrame existente?
- ¿Qué sucede si establece
inferSchema=falsesin proporcionar un esquema?