Apache Spark

PySpark

Tutorial PySpark: Fundamentos de PySpark

PySpark: Descubre los fundamentos de Apache Spark y conceptos clave de SparkContext y SparkSession. Aprende a manejar RDDs y DataFrames en aplicaciones de Big Data, y comprende como funcionan los ciclos de vida en PySpark

Aprende PySpark y certifícate

Introducción a SparkContext y SparkSession

En PySpark, el SparkContext y el SparkSession son los puntos de entrada fundamentales para interactuar con un clúster de Apache Spark y ejecutar operaciones de procesamiento distribuido. Comprender su funcionamiento es esencial para desarrollar aplicaciones eficientes y escalables en entornos de Big Data.

El SparkContext es el objeto principal que conecta una aplicación de PySpark con el clúster de Spark. Actúa como interfaz entre el programa y los recursos del clúster, permitiendo la gestión de tareas y la distribución de datos. A través de él, se pueden crear Resilient Distributed Datasets (RDDs) y realizar operaciones de bajo nivel.

from pyspark import SparkContext

sc = SparkContext(master="local[*]", appName="MiAplicación")

En este ejemplo, se inicia un SparkContext que utiliza todos los núcleos locales (local[*]) y asigna el nombre MiAplicación a la aplicación. Este contexto es esencial para ejecutar operaciones de RDD y manejar tareas de paralelización.

Sin embargo, a partir de Spark 2.0, se introdujo el SparkSession como una abstracción de nivel superior que unifica y simplifica el acceso a todas las funcionalidades de Spark. El SparkSession reemplaza a las anteriores SQLContext y HiveContext, proporcionando una interfaz cohesiva para trabajar con DataFrames, Datasets y consultas SQL.

from pyspark.sql import SparkSession

spark = SparkSession.builder \
    .appName("MiAplicación") \
    .master("local[*]") \
    .getOrCreate()

El objeto spark creado es una instancia de SparkSession que permite realizar operaciones de alto nivel. A diferencia del SparkContext, el SparkSession incorpora funcionalidades adicionales como el manejo de Catálogos, Configuraciones y la capacidad de leer y escribir en múltiples formatos de datos.

Una característica importante es que el SparkContext subyacente es accesible a través del atributo spark.sparkContext. Esto significa que, aunque se utilice el SparkSession para operaciones de alto nivel, todavía es posible acceder al SparkContext para tareas específicas que lo requieran.

El SparkSession facilita la carga y manipulación de datos estructurados. Por ejemplo, para leer un archivo CSV y crear un DataFrame:

df = spark.read.csv("ruta/datos.csv", header=True, inferSchema=True)

Este código lee el archivo datos.csv, interpreta la primera fila como encabezado y deduce automáticamente el esquema de datos. La capacidad de inferSchema es especialmente útil para trabajar con datos sin necesidad de definir manualmente los tipos.

Además, el SparkSession permite realizar consultas SQL directas sobre los DataFrames. Primero, se crea una vista temporal:

df.createOrReplaceTempView("tabla_datos")

Luego, se ejecuta una consulta SQL:

resultado = spark.sql("SELECT columna1, columna2 FROM tabla_datos WHERE columna3 > 100")

La integración de SQL dentro de PySpark a través del SparkSession ofrece una gran flexibilidad para los analistas que están familiarizados con el lenguaje SQL y desean aprovechar las capacidades de procesamiento distribuido de Spark.

Por otro lado, aunque el uso de SparkSession es recomendado para la mayoría de casos, el SparkContext sigue siendo relevante para operaciones de bajo nivel con RDDs. Por ejemplo, para crear un RDD a partir de una colección:

datos = [1, 2, 3, 4, 5]
rdd = spark.sparkContext.parallelize(datos)

Con el RDD creado, se pueden aplicar transformaciones funcionales como map, filter o reduce:

rdd_pares = rdd.filter(lambda x: x % 2 == 0)

En este contexto, la programación funcional es clave para aprovechar el modelo de procesamiento distribuido de Spark.

Es crucial entender que el SparkSession se construye sobre el SparkContext, y ambos funcionan en conjunto. Mientras que el SparkSession es ideal para trabajar con datos estructurados y proporciona una API de más alto nivel, el SparkContext ofrece un control más granular y es necesario para ciertas operaciones avanzadas.

Para configurar propiedades específicas del clúster o de la aplicación, se pueden establecer opciones adicionales durante la construcción del SparkSession:

spark = SparkSession.builder \
    .appName("MiAplicación") \
    .config("spark.executor.memory", "4g") \
    .config("spark.driver.memory", "2g") \
    .getOrCreate()

Aquí, se asigna 4 GB de memoria a los ejecutores y 2 GB al driver, ajustando los recursos según las necesidades de procesamiento.

En conclusión, el SparkSession es el punto de entrada estándar para las aplicaciones de PySpark en versiones modernas de Spark, brindando una interfaz unificada para diversas operaciones de datos. No obstante, el SparkContext sigue siendo una herramienta esencial para acceder a funcionalidades de bajo nivel y para tareas que requieren un control más detallado sobre el procesamiento distribuido.

Resilient Distributed Datasets (RDDs)

Los Resilient Distributed Datasets (RDDs) son la abstracción fundamental de datos en Apache Spark, representando una colección inmutable y distribuida de objetos que se pueden procesar en paralelo. Comprender los RDDs es esencial para desarrollar aplicaciones eficientes en PySpark que aprovechen el poder del procesamiento distribuido.

Un RDD es una estructura de datos que permite operar con grandes conjuntos de datos distribuidos en múltiples nodos de un clúster. Los RDDs son tolerantes a fallos (resilientes) y proporcionan una interfaz de programación funcional para la manipulación de datos. Cada RDD se crea a partir de una fuente de datos externa o transformando otros RDDs existentes.

Para crear un RDD a partir de una colección en PySpark, se utiliza el método parallelize del SparkContext:

datos = [1, 2, 3, 4, 5]
rdd = sc.parallelize(datos)

En este ejemplo, se distribuye la lista datos entre los nodos del clúster, creando un RDD que puede ser procesado en paralelo. Los RDDs soportan dos tipos de operaciones: transformaciones y acciones.

Las transformaciones son operaciones que devuelven un nuevo RDD, permitiendo construir flujos de procesamiento de datos de manera perezosa (lazy evaluation). Algunas transformaciones comunes incluyen map, filter y flatMap:

rdd_pares = rdd.filter(lambda x: x % 2 == 0)

Aquí, rdd_pares es un nuevo RDD que contiene únicamente los elementos pares del RDD original. Las transformaciones no se ejecutan inmediatamente; Spark las registra para optimizar su ejecución posterior.

Las acciones son operaciones que devuelven resultados concretos al conductor (driver), disparando así la ejecución de las transformaciones pendientes. Ejemplos de acciones son collect, count y first:

resultado = rdd_pares.collect()
print(resultado)  # Salida: [2, 4]

La llamada a collect provoca que Spark ejecute todas las transformaciones necesarias y recopile los resultados en el driver. Es importante usar las acciones con precaución, especialmente en conjuntos de datos grandes, para evitar sobrecargar la memoria del driver.

Una característica clave de los RDDs es su tolerancia a fallos. Spark mantiene un registro del linaje de cada RDD, lo que permite reconstruir los datos en caso de fallo de algún nodo. Esta propiedad es fundamental para garantizar la disponibilidad y fiabilidad en entornos distribuidos.

Los RDDs pueden ser persistidos en memoria o disco para optimizar las operaciones repetidas. Utilizando el método persist, se especifica el nivel de persistencia deseado:

from pyspark import StorageLevel

rdd_pares.persist(StorageLevel.MEMORY_ONLY)

Al persistir el RDD en memoria, se agilizan futuras operaciones que lo reutilicen, reduciendo así el tiempo de cálculo. Es imprescindible liberar los recursos cuando ya no sean necesarios usando unpersist:

rdd_pares.unpersist()

Los RDDs también soportan particionamiento personalizado, lo que mejora el rendimiento en operaciones que requieren intercambio de datos entre nodos (shuffle). Mediante el uso de la función partitionBy, se puede controlar cómo se distribuyen los datos:

rdd_kv = rdd.map(lambda x: (x % 2, x))
rdd_particionado = rdd_kv.partitionBy(2)

En este caso, los datos se particionan en dos particiones según la clave calculada, optimizando operaciones posteriores que agrupen por dicha clave.

La programación con RDDs sigue el paradigma de la programación funcional, facilitando la escritura de código conciso y expresivo. Las funciones de orden superior, como map y reduce, son fundamentales en este enfoque:

suma_total = rdd.reduce(lambda x, y: x + y)
print(suma_total)  # Salida: 15

El uso de reduce permite sumar todos los elementos del RDD de manera distribuida y eficiente.

Aunque los DataFrames y Datasets han ganado popularidad por su facilidad de uso y optimizaciones internas, los RDDs continúan siendo relevantes para ciertas tareas. En particular, cuando se trabaja con datos no estructurados o se requieren operaciones de bajo nivel, los RDDs ofrecen mayor flexibilidad.

Por ejemplo, para leer un archivo de texto y procesarlo línea por línea:

rdd_texto = sc.textFile("ruta/al/archivo.txt")
lineas_filtradas = rdd_texto.filter(lambda linea: "error" in linea.lower())

Aquí, textFile crea un RDD donde cada elemento es una línea del archivo, permitiendo aplicar transformaciones directamente sobre el texto.

Además, los RDDs son fundamentales para comprender el funcionamiento interno de Spark, ya que los DataFrames y Datasets se construyen sobre ellos. Conocer los RDDs proporciona una comprensión más profunda de cómo Spark ejecuta las tareas y optimiza el procesamiento.

Es importante tener en cuenta las limitaciones de los RDDs. Al carecer de un esquema estructurado, no pueden beneficiarse de algunas optimizaciones que sí están disponibles en los DataFrames. Por ello, se recomienda utilizar DataFrames o Datasets cuando se trabaje con datos estructurados, y recurrir a los RDDs en casos específicos.

Los RDDs proporcionan una API para manejar datos distribuidos de forma eficiente y tolerante a fallos. Dominar su uso es esencial para sacar el máximo partido a PySpark y desarrollar aplicaciones escalables en entornos de Big Data.

DataFrames y Datasets

En PySpark, los DataFrames y los Datasets son las abstracciones de alto nivel que permiten manipular y procesar datos estructurados de manera eficiente y escalable. Estas estructuras ofrecen una API más expresiva y optimizada en comparación con los RDDs, facilitando el manejo de grandes volúmenes de datos en entornos distribuidos.

El DataFrame es una colección distribuida de datos organizados en columnas nombradas, similar a una tabla en una base de datos relacional o a un DataFrame de pandas en Python. Cada columna tiene un tipo de dato específico, lo que permite a Spark aplicar optimizaciones internas basadas en el esquema de los datos. Esta estructura favorece la realización de operaciones como filtrado, agregación y proyecciones de manera más eficiente.

Por ejemplo, para crear un DataFrame a partir de una lista de Python, se puede utilizar el método createDataFrame del SparkSession:

from pyspark.sql import SparkSession

spark = SparkSession.builder.appName("EjemploDataFrame").getOrCreate()

datos = [("Ana", 28), ("Luis", 34), ("Pedro", 23)]
columnas = ["Nombre", "Edad"]

df = spark.createDataFrame(datos, schema=columnas)
df.show()

Este código genera un DataFrame con dos columnas: Nombre y Edad, y muestra su contenido en pantalla. La función show() es una acción que desencadena la ejecución de las operaciones pendientes y presenta los datos de forma tabular.

Los DataFrames ofrecen numerosas ventajas sobre los RDDs, entre ellas:

  • Optimización automática de consultas mediante el Catalyst Optimizer.
  • Soporte para una amplia gama de formatos de datos y fuentes de almacenamiento.
  • Mejora en el rendimiento gracias a la planificación física y ejecución optimizada.

El Catalyst Optimizer es el motor de optimización de consultas de Spark que analiza las operaciones solicitadas y determina el plan de ejecución más eficiente. Esto se logra gracias al conocimiento del esquema y los tipos de datos de las columnas, permitiendo optimizaciones que no son posibles con RDDs.

Para inspeccionar el esquema de un DataFrame, se utiliza el método printSchema():

df.printSchema()

La salida muestra la estructura del DataFrame, indicando los nombres de las columnas y sus tipos de datos. Esta información es crucial para entender cómo Spark interpretará y procesará los datos.

Los Datasets, por otro lado, son una extensión de los DataFrames que proporcionan tipado estático y seguridad de tipos en tiempo de compilación. Sin embargo, en PySpark, dado que Python es un lenguaje dinámico, los DataFrames y los Datasets se unifican bajo la misma API. Esto significa que en PySpark, al trabajar con DataFrames, se está utilizando en esencia la funcionalidad de los Datasets.

A pesar de esta unificación, es importante comprender que los Datasets en lenguajes de tipado estático como Scala o Java ofrecen beneficios adicionales, como la capacidad de detectar errores de tipo antes de la ejecución y optimizaciones aún más profundas. En PySpark, se mantiene la flexibilidad de Python, pero se pierde este tipado estático.

Las operaciones sobre DataFrames son similares a las de una base de datos SQL. Se pueden realizar transformaciones como select, filter, groupBy, y aplicar funciones de agregación:

from pyspark.sql.functions import col, avg

df_filtrado = df.filter(col("Edad") > 25)
df_filtrado.show()

df_edad_media = df.groupBy().agg(avg("Edad"))
df_edad_media.show()

En este ejemplo, se filtran las filas donde la Edad es mayor a 25 y se calcula la edad media de todos los registros. La función col se utiliza para hacer referencia a las columnas, y las funciones de agregación se encuentran en el módulo pyspark.sql.functions.

Una característica clave de los DataFrames es el lazy evaluation o evaluación perezosa. Las transformaciones aplicadas no se ejecutan inmediatamente; Spark las acumula y construye un plan de ejecución optimizado. Solo cuando se realiza una acción que requiere un resultado concreto, como show() o collect(), Spark ejecuta el plan completo.

Los DataFrames también soportan la integración con SQL. Se pueden crear vistas temporales y ejecutar consultas SQL directamente sobre ellas:

df.createOrReplaceTempView("personas")

resultado = spark.sql("SELECT Nombre FROM personas WHERE Edad >= 30")
resultado.show()

Esto es especialmente útil para aquellos familiarizados con consultas SQL, permitiendo combinar la potencia de Spark con la expresividad de SQL.

Otra ventaja de los DataFrames es su capacidad para manejar datos provenientes de diversas fuentes y formatos, como JSON, CSV, Parquet, entre otros. Aunque la lectura y escritura de datos se aborda en profundidad en secciones posteriores, es importante destacar que la unificación del manejo de datos estructurados simplifica enormemente el procesamiento en PySpark.

Es fundamental mencionar que, aunque los Datasets no aportan beneficios adicionales en PySpark debido al tipado dinámico de Python, los DataFrames siguen siendo la herramienta principal para trabajar con datos estructurados. Al aprovechar las optimizaciones internas y una API expresiva, los DataFrames permiten desarrollar aplicaciones más eficientes y fáciles de mantener.

Para convertir un RDD existente en un DataFrame, se utiliza el método toDF():

rdd = spark.sparkContext.parallelize([("María", 31), ("José", 22)])
df_desde_rdd = rdd.toDF(["Nombre", "Edad"])
df_desde_rdd.show()

Esta conversión facilita el uso de las capacidades avanzadas de los DataFrames en datos que originalmente estaban en un RDD.

Además, es posible definir un esquema explícito utilizando la clase StructType y StructField, lo que proporciona un mayor control sobre los tipos de datos y permite definir esquemas complejos:

from pyspark.sql.types import StructType, StructField, StringType, IntegerType

esquema = StructType([
    StructField("Nombre", StringType(), True),
    StructField("Edad", IntegerType(), True)
])

df_con_esquema = spark.createDataFrame(datos, schema=esquema)
df_con_esquema.printSchema()

Definir el esquema es especialmente útil cuando se trabaja con fuentes de datos externas donde los tipos deben ser precisos para evitar errores durante el procesamiento.

Los DataFrames en PySpark son una herramienta esencial para manejar y procesar datos estructurados de forma eficiente. Aprovechan las capacidades de optimización del motor de Spark y proporcionan una API rica y expresiva. Aunque los Datasets no presentan diferencias significativas en PySpark, entender su concepto es valioso, especialmente al colaborar con equipos que utilizan Scala o Java, donde el tipado estático es más relevante.

El dominio de los DataFrames permite abordar problemas complejos de análisis de datos y es una base sólida para avanzar hacia temas más avanzados en PySpark, como el aprendizaje automático con MLlib y el procesamiento de grandes volúmenes de datos en tiempo real.

Qué estructura usar:

DataFrames

  • Para la mayoría de tareas de ingeniería de datos y análisis, los Spark DataFrames son la herramienta ideal.
  • Permiten consultas expresivas, un esquema claro y optimizaciones automáticas.
  • Se integran fácil con SQL y tienen una API más declarativa.

RDDs

  • Útiles cuando necesitas un control muy granular o realizar transformaciones que no encajan en el modelo tabular de DataFrame.
  • En la práctica, se recomiendan sólo si realmente necesitas ese nivel de detalle.
  • Es una estructura de más bajo nivel.

Tipos de datos complejos dentro de DataFrames (arrays, maps, structs):

  • Fundamental para manejar datos anidados o semiestructurados.
  • Se sigue usando la misma API de DataFrame, pero con funciones específicas (como explode, getField, etc.).

Ciclo de vida de una aplicación Spark

El ciclo de vida de una aplicación Spark comprende todas las etapas desde su creación hasta la finalización de su ejecución. Entender este proceso es fundamental para desarrollar aplicaciones eficientes y optimizar el uso de recursos en un entorno distribuido como Apache Spark.

Una aplicación Spark comienza con la creación de una SparkSession, el punto de entrada para interactuar con el clúster de Spark. La SparkSession permite acceder a todas las funcionalidades de Spark, incluyendo la manipulación de DataFrames, ejecución de consultas SQL y configuración de parámetros de la aplicación.

from pyspark.sql import SparkSession

spark = SparkSession.builder \
    .appName("MiAplicacion") \
    .master("local[*]") \
    .getOrCreate()

En este ejemplo, se inicializa una SparkSession con el nombre MiAplicacion, utilizando todos los núcleos disponibles en la máquina local. La creación de la SparkSession establece la conexión con el clúster y configura el entorno de ejecución.

Tras iniciar la SparkSession, el siguiente paso es la ingestión de datos. Spark soporta múltiples fuentes de datos y formatos, lo que permite leer información desde archivos CSV, JSON, Parquet, bases de datos y más. Un ejemplo común es la lectura de un archivo CSV para crear un DataFrame:

df = spark.read.csv("ruta/datos.csv", header=True, inferSchema=True)

Este código carga los datos desde datos.csv, interpreta la primera fila como encabezados de columna y deduce automáticamente el tipo de datos de cada columna gracias a inferSchema.

Una vez cargados los datos, se aplican transformaciones para procesarlos. Las transformaciones en Spark son perezosas, lo que significa que no se ejecutan inmediatamente. En su lugar, se construye un plan de ejecución que Spark optimizará posteriormente. Las transformaciones más comunes incluyen select, filter, groupBy, agg, entre otras:

df_filtrado = df.filter(df["edad"] > 18).select("nombre", "edad")

En este caso, se filtran las filas donde la columna edad es mayor a 18 y se seleccionan únicamente las columnas nombre y edad. Estas operaciones añaden etapas al Directed Acyclic Graph (DAG) de ejecución, pero aún no se realizan cálculos.

Para obtener resultados, es necesario ejecutar una acción, que desencadena la evaluación del DAG y realiza las operaciones en los datos. Acciones comunes son show, collect, count y write:

df_filtrado.show()

La llamada a show() muestra las primeras filas del DataFrame resultante, iniciando el procesamiento de las transformaciones previas. Aquí, Spark optimiza el plan de ejecución mediante el Catalyst Optimizer, mejorando la eficiencia y reduciendo el tiempo de cálculo.

Durante la ejecución de una acción, Spark divide el trabajo en jobs, stages y tasks. Un job corresponde a una acción ejecutada, que a su vez se descompone en stages basados en las operaciones que requieren shuffle o intercambio de datos entre nodos. Cada stage se subdivide en tasks, que representan unidades de trabajo sobre particiones individuales de datos.

Es importante gestionar adecuadamente el alineamiento de particiones y minimizar los shuffles, ya que estos pueden impactar significativamente en el rendimiento. Las operaciones que desencadenan un shuffle, como join o groupBy, implican redistribuir datos a través de la red del clúster.

Para mejorar la eficiencia, es posible persistir o cachear los DataFrames que se reutilizan en múltiples operaciones:

df_filtrado.cache()

Al cachear un DataFrame, se almacenan los resultados intermedios en memoria, acelerando futuras acciones que lo utilicen. Es esencial liberar los recursos cuando ya no sean necesarios para optimizar el uso de memoria:

df_filtrado.unpersist()

El monitoreo y la comprensión de la ejecución son facilitados por la interfaz web de Spark, conocida como Spark UI. Esta herramienta proporciona información detallada sobre los jobs, stages, tasks, uso de memoria y otras métricas relevantes. Accediendo a http://localhost:4040 mientras la aplicación está en ejecución, se puede analizar el rendimiento y detectar posibles cuellos de botella.

Al finalizar las operaciones, es buena práctica detener la SparkSession para liberar los recursos asociados:

spark.stop()

Detener la sesión cierra la conexión con el clúster y limpia los recursos en el driver y los ejecutores. Esto es especialmente relevante en aplicaciones que se ejecutan en entornos compartidos o en producción.

Otra consideración en el ciclo de vida es el manejo de configuraciones y parámetros. Es posible ajustar configuraciones específicas al iniciar la SparkSession o en tiempo de ejecución. Por ejemplo, para establecer el nivel de registro a WARN y definir el número de particiones por defecto:

spark = SparkSession.builder \
    .config("spark.sql.shuffle.partitions", "10") \
    .config("spark.eventLog.enabled", "true") \
    .getOrCreate()

Ajustar adecuadamente estas configuraciones puede mejorar el rendimiento y facilitar la depuración de la aplicación.

En aplicaciones más complejas, es común encapsular el código en funciones y clases, siguiendo buenas prácticas de programación modular. Esto mejora la mantenibilidad y permite reutilizar componentes en diferentes partes de la aplicación.

Adicionalmente, es posible manejar excepciones y errores mediante bloques try-except, asegurando que la aplicación se comporte de forma robusta ante situaciones inesperadas:

try:
    df_resultado = proceso_datos(df)
    df_resultado.write.parquet("ruta/salida")
except Exception as e:
    print(f"Error durante el procesamiento: {e}")
finally:
    spark.stop()

En este fragmento, se intenta procesar los datos y escribir el resultado en formato Parquet. Si ocurre algún error, se captura la excepción, se muestra un mensaje y se asegura que la SparkSession se detenga en cualquier caso mediante el bloque finally.

El ciclo de vida de una aplicación Spark también incluye la implementación y ejecución en diferentes entornos. Las aplicaciones pueden ejecutarse en modo local, en un clúster de YARN, Mesos o Kubernetes, dependiendo de las necesidades y la infraestructura disponible. Al empaquetar la aplicación, es importante considerar las dependencias y configuraciones necesarias para su correcta ejecución en el entorno de destino.

Finalmente, comprender cómo Spark gestiona los recursos y ejecuta las tareas permite optimizar las aplicaciones y aprovechar al máximo las capacidades del clúster. Aspectos como el manejo de serialización, ajustes en la memoria de los ejecutores y el uso eficiente de broadcast variables pueden marcar la diferencia en el rendimiento de una aplicación.

El ciclo de vida de una aplicación Spark abarca desde la creación de la SparkSession, pasando por la ingestión y transformación de datos, hasta la ejecución de acciones y cierre de la sesión. Cada etapa es crucial y requiere atención para desarrollar aplicaciones eficientes, escalables y mantenibles en el ecosistema de Apache Spark.

CONSTRUYE TU CARRERA EN IA Y PROGRAMACIÓN SOFTWARE

Accede a +1000 lecciones y cursos con certificado. Mejora tu portfolio con certificados de superación para tu CV.

30 % DE DESCUENTO

Plan mensual

19.00 /mes

13.30 € /mes

Precio normal mensual: 19 €
63 % DE DESCUENTO

Plan anual

10.00 /mes

7.00 € /mes

Ahorras 144 € al año
Precio normal anual: 120 €
Aprende PySpark online

Todas las lecciones de PySpark

Accede a todas las lecciones de PySpark y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.

Accede GRATIS a PySpark y certifícate

En esta lección

Objetivos de aprendizaje de esta lección

  1. Comprender la función y estructura del SparkContext y SparkSession.
  2. Diferenciar entre RDDs, DataFrames y Datasets en PySpark.
  3. Aprender la creación y manipulación de DataFrames y RDDs.
  4. Optimizar aplicaciones Spark utilizando el Catalyst Optimizer.
  5. Implementar transformaciones y acciones para procesar datos.
  6. Utilizar las capacidades de SQL dentro de PySpark.
  7. Manejar el ciclo de vida completo de una aplicación Spark.