0% encontró este documento útil (0 votos)
228 vistas

03 Spark The Definitive Guide ESP

Spark the Definitive Guide ESP

Cargado por

Nacho Villarraza
Derechos de autor
© © All Rights Reserved
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como PDF, TXT o lee en línea desde Scribd
0% encontró este documento útil (0 votos)
228 vistas

03 Spark The Definitive Guide ESP

Spark the Definitive Guide ESP

Cargado por

Nacho Villarraza
Derechos de autor
© © All Rights Reserved
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como PDF, TXT o lee en línea desde Scribd
Está en la página 1/ 601

Machine Translated by Google

Machine Translated by Google

Spark: la guía definitiva


Procesamiento de Big Data simplificado

Bill Chambers y Matei Zaharia


Machine Translated by Google

Spark: la guía definitiva

por Bill Chambers y Matei Zaharia

Copyright © 2018 Databricks. Reservados todos los derechos.

Impreso en los Estados Unidos de América.

Publicado por O'Reilly Media, Inc., 1005 Gravenstein Highway North, Sebastopol, CA 95472.

Los libros de O'Reilly se pueden comprar con fines educativos, comerciales o de promoción de ventas. Las ediciones en
línea también están disponibles para la mayoría de los títulos (http://oreilly.com/safari). Para obtener más información,
comuníquese con nuestro departamento de ventas corporativo/institucional: 800­998­9938 o [email protected].

Montaje: Nicole Tache

Editor de producción: Justin Billing

Corrector de estilo: Octal Publishing, Inc., Chris Edwards y Amanda Kersey

Correctora: Jasmine Kwityn

Indexador: Judith McConville

Diseñador de interiores: David Futato

Diseño de portada: Karen Montgomery

Ilustrador: Rebecca Demarest

Febrero 2018: Primera Edición

Historial de revisiones de la primera edición

2018­02­08: Primer lanzamiento

Consulte http://oreilly.com/catalog/errata.csp?isbn=9781491912218 para conocer los detalles de la versión.

El logotipo de O'Reilly es una marca comercial registrada de O'Reilly Media, Inc. Spark: The Definitive Guide, la imagen de
portada y la imagen comercial relacionada son marcas comerciales de O'Reilly Media, Inc. Apache, Spark y Apache Spark
son marcas comerciales de la Fundación de Software Apache.

Si bien el editor y los autores se han esforzado de buena fe para garantizar que la información y las instrucciones contenidas
en este trabajo sean precisas, el editor y los autores renuncian a toda responsabilidad por errores u omisiones, incluida,
entre otras, la responsabilidad por daños resultantes del uso o confianza en este trabajo. El uso de la información e
instrucciones contenidas en este trabajo es bajo su propio riesgo. Si alguna muestra de código u otra tecnología que
este trabajo contiene o describe está sujeta a licencias de código abierto o a los derechos de propiedad intelectual de
Machine Translated by Google

otros, es su responsabilidad asegurarse de que su uso cumpla con dichas licencias y/o derechos.

978­1­491­91221­8

[METRO]
Machine Translated by Google

Prefacio

¡Bienvenidos a esta primera edición de Spark: La Guía Definitiva! Nos complace presentarle el recurso más
completo sobre Apache Spark hoy, centrándonos especialmente en la nueva generación de API de Spark
presentada en Spark 2.0.

Apache Spark es actualmente uno de los sistemas más populares para el procesamiento de datos a gran escala,
con API en múltiples lenguajes de programación y una gran cantidad de bibliotecas integradas y de terceros.
Aunque el proyecto ha existido durante varios años, primero como un proyecto de investigación iniciado en
UC Berkeley en 2009, luego en Apache Software Foundation desde 2013, la comunidad de código abierto
continúa creando API más potentes y bibliotecas de alto nivel sobre Spark, por lo que Aún queda mucho por
escribir sobre el proyecto. Decidimos escribir este libro por dos razones. Primero, queríamos presentar el libro más
completo sobre Apache Spark, cubriendo todos los casos de uso fundamentales con ejemplos fáciles de
ejecutar. En segundo lugar, queríamos explorar especialmente las API "estructuradas" de nivel superior que se
finalizaron en Apache Spark 2.0, a saber, DataFrames, Datasets, Spark SQL y Structured Streaming, que los
libros antiguos sobre Spark no siempre incluyen. Esperamos que este libro le brinde una base sólida para
escribir aplicaciones Apache Spark modernas utilizando todas las herramientas disponibles en el proyecto.

En este prefacio, le contaremos un poco sobre nuestros antecedentes y le explicaremos para quién es este libro
y cómo hemos organizado el material. También queremos agradecer a las numerosas personas que
ayudaron a editar y revisar este libro, sin las cuales no hubiera sido posible.

Sobre los autores


Ambos autores del libro han estado involucrados en Apache Spark durante mucho tiempo, por lo que estamos
muy emocionados de poder traerles este libro.

Bill Chambers comenzó a usar Spark en 2014 en varios proyectos de investigación. Actualmente, Bill es
gerente de productos en Databricks, donde se enfoca en permitir que los usuarios escriban varios tipos de
aplicaciones Apache Spark. Bill también bloguea regularmente sobre Spark y presenta en conferencias y reuniones
sobre el tema. Bill tiene una Maestría en Gestión y Sistemas de Información de la Escuela de Información de
UC Berkeley.

Matei Zaharia comenzó el proyecto Spark en 2009, durante su tiempo como estudiante de doctorado en
UC Berkeley. Matei trabajó con otros investigadores de Berkeley y colaboradores externos para diseñar las API
principales de Spark y hacer crecer la comunidad de Spark, y ha seguido participando en nuevas iniciativas,
como las API estructuradas y la transmisión estructurada. En 2013, Matei y otros miembros del equipo de
Berkeley Spark cofundaron Databricks para hacer crecer aún más el proyecto de código abierto y ofrecer
ofertas comerciales a su alrededor. En la actualidad, Matei continúa trabajando como tecnólogo jefe en
Databricks y también ocupa un puesto como profesor asistente de informática en la Universidad de Stanford,
donde investiga sobre sistemas a gran escala e IA. Matei recibió su doctorado en Ciencias de la Computación
de UC Berkeley en 2013.
Machine Translated by Google

Para quien es este libro


Diseñamos este libro principalmente para científicos de datos e ingenieros de datos que buscan usar Apache Spark.
Los dos roles tienen necesidades ligeramente diferentes, pero en realidad, la mayoría del desarrollo de aplicaciones cubre
un poco de ambos, por lo que creemos que el material será útil en ambos casos. Específicamente, en nuestra opinión,
la carga de trabajo del científico de datos se enfoca más en consultar datos de forma interactiva para responder
preguntas y crear modelos estadísticos, mientras que el trabajo del ingeniero de datos se enfoca en escribir
aplicaciones de producción repetibles y mantenibles, ya sea para usar los modelos del científico de datos en la práctica,
o simplemente para preparar los datos para un análisis posterior (p. ej., crear una canalización de ingesta de datos).
Sin embargo, a menudo vemos con Spark que estos roles se desdibujan. Por ejemplo, los científicos de datos pueden
empaquetar aplicaciones de producción sin demasiados problemas y los ingenieros de datos usan análisis interactivos
para comprender e inspeccionar sus datos para construir y mantener canalizaciones.

Si bien tratamos de proporcionar todo lo que los científicos e ingenieros de datos necesitan para comenzar, hay algunas
cosas en las que no teníamos espacio para enfocarnos en este libro. En primer lugar, este libro no incluye introducciones
detalladas a algunas de las técnicas de análisis que puede usar en Apache Spark, como el aprendizaje automático. En
cambio, le mostramos cómo invocar estas técnicas usando bibliotecas en Spark, asumiendo que ya tiene una formación
básica en aprendizaje automático. Existen muchos libros completos e independientes para cubrir estas técnicas en detalle
formal, por lo que recomendamos comenzar con ellos si desea aprender sobre estas áreas. En segundo lugar, este libro se
enfoca más en el desarrollo de aplicaciones que en las operaciones y la administración (p. ej., cómo administrar un clúster
de Apache Spark con docenas de usuarios). No obstante, hemos tratado de incluir material completo sobre monitoreo,
depuración y configuración en las Partes V y VI del libro para ayudar a los ingenieros a ejecutar su aplicación de manera
eficiente y abordar el mantenimiento diario. Finalmente, este libro pone menos énfasis en las API más antiguas de nivel
inferior en Spark, específicamente RDD y DStreams, para presentar la mayoría de los conceptos utilizando las API
estructuradas de nivel superior más nuevas. Por lo tanto, es posible que el libro no sea la mejor opción si necesita
mantener una aplicación RDD o DStream antigua, pero debería ser una excelente introducción para escribir nuevas
aplicaciones.

Las convenciones usadas en este libro


En este libro se utilizan las siguientes convenciones tipográficas:

Itálico

Indica nuevos términos, URL, direcciones de correo electrónico, nombres de archivo y extensiones de archivo.

Ancho constante

Se utiliza para listas de programas, así como dentro de párrafos para hacer referencia a elementos de programas
como nombres de variables o funciones, bases de datos, tipos de datos, variables de entorno, declaraciones y
palabras clave.

Negrita de ancho constante


Machine Translated by Google

Muestra comandos u otro texto que el usuario debe escribir literalmente.

Cursiva de ancho constante

Muestra texto que debe reemplazarse con valores proporcionados por el usuario o por valores determinados por
contexto.

CONSEJO

Este elemento significa un consejo o sugerencia.

NOTA

Este elemento significa una nota general.

ADVERTENCIA

Este elemento indica una advertencia o precaución.

Uso de ejemplos de código


Estamos muy emocionados de haber diseñado este libro para que todo el contenido del código se pueda ejecutar en datos reales.
Escribimos todo el libro con cuadernos de Databricks y publicamos los datos y el material relacionado en GitHub. Esto significa que
puede ejecutar y editar todo el código a medida que avanza, o copiarlo en código de trabajo en sus propias aplicaciones.

Intentamos usar datos reales siempre que fue posible para ilustrar los desafíos que enfrentará al crear aplicaciones de datos a
gran escala. Finalmente, también incluimos varias aplicaciones independientes más grandes en el repositorio de GitHub
del libro para ejemplos que no tiene sentido mostrar en línea en el texto.

El repositorio de GitHub seguirá siendo un documento vivo a medida que actualicemos según el progreso de Spark.
Asegúrese de seguir las actualizaciones allí.

Este libro está aquí para ayudarle a hacer su trabajo. En general, si se ofrece un código de ejemplo con este libro, puede usarlo en
sus programas y documentación. No es necesario que se comunique con nosotros para obtener permiso a menos que esté
reproduciendo una parte significativa del código. Por ejemplo, escribir un programa que use varios fragmentos de código de este
libro no requiere permiso. Vender o distribuir un CD­ROM de ejemplos de libros de O'Reilly requiere permiso. Responder una pregunta
citando este libro y citando código de ejemplo no requiere permiso.

La incorporación de una cantidad significativa de código de ejemplo de este libro en la documentación de su producto
requiere permiso.
Machine Translated by Google

Apreciamos, pero no requerimos, atribución. Una atribución suele incluir el título, el autor, el editor y el ISBN.
Por ejemplo: “Spark: La guía definitiva de Bill Chambers y Matei Zaharia (O'Reilly). Copyright 2018 Databricks,
Inc., 978­1­491­91221­8.”

Si cree que su uso de los ejemplos de código está fuera del uso justo o del permiso otorgado anteriormente, no
dude en contactarnos en [email protected].

Safari O´Reilly
Safari (anteriormente Safari Books Online) es una plataforma de referencia y capacitación basada en membresía
para empresas, gobiernos, educadores e individuos.

Los miembros tienen acceso a miles de libros, videos de capacitación, rutas de aprendizaje, tutoriales
interactivos y listas de reproducción seleccionadas de más de 250 editores, incluidos O'Reilly Media, Harvard
Business Review, Prentice Hall Professional, Addison­Wesley Professional, Microsoft Press, Sams, Que ,
Peachpit Press, Adobe, Focal Press, Cisco Press, John Wiley & Sons, Syngress, Morgan Kaufmann, IBM
Redbooks, Packt, Adobe Press, FT Press, Apress, Manning, New Riders, McGraw­Hill, Jones & Bartlett y
Course Tecnología, entre otros.

Para obtener más información, visite http://oreilly.com/safari.

Cómo contactarnos
Dirija sus comentarios y preguntas sobre este libro a la editorial:

O'Reilly Media, Inc.

1005 Carretera Gravenstein Norte

Sebastopol, CA 95472

800­998­9938 (en los Estados Unidos o Canadá)

707­829­0515 (internacional o local)

707­829­0104 (fax)

Para comentar o hacer preguntas técnicas sobre este libro, envíe un correo
electrónico a [email protected].

Para obtener más información sobre nuestros libros, cursos, conferencias y noticias, visite nuestro sitio web
en http://www.oreilly.com.

Encuéntranos en Facebook: http://facebook.com/oreilly

Síganos en Twitter: http://twitter.com/oreillymedia


Machine Translated by Google

Míranos en YouTube: http://www.youtube.com/oreillymedia

Expresiones de gratitud
Hubo un gran número de personas que hicieron posible este libro.

En primer lugar, nos gustaría agradecer a nuestro empleador, Databricks, por dedicarnos tiempo para
trabajar en este libro. Sin el apoyo de la empresa, este libro no hubiera sido posible. En particular, nos
gustaría agradecer a Ali Ghodsi, Ion Stoica y Patrick Wendell por su apoyo.

Además, hay numerosas personas que leen borradores del libro y capítulos individuales. Nuestros
revisores fueron los mejores en su clase y brindaron comentarios invaluables.

Estos revisores, en orden alfabético por apellido, son:

lynn amstrong

mikio braun

Jules Damji

denny lee

Alex Tomas

Además de los revisores de libros formales, hubo muchos otros usuarios, colaboradores y autores de
Spark que leyeron capítulos específicos o ayudaron a formular cómo se deben discutir los temas.
En orden alfabético por apellido, las personas que ayudaron son:

Samier Agarwal

Bagrat Amirbekian

Miguel Armbrust

José Bradley

Tathagata Das

hossein falaki

Ventilador Wenchen

sue ana hong

Yin Huai

tim cazador

xiao li

cheng lian
Machine Translated by Google

Xiangrui Meng

kris mok

jose rosen

Srinath Shankar

Takuya Ueshin

Herman van Hovell

reynold xin

felipe yang

Burak Yavuz

Shixiong Zhu

Por último, nos gustaría agradecer a amigos, familiares y seres queridos. Sin su apoyo, paciencia y aliento, no
hubiéramos podido escribir la guía definitiva de Spark.
Machine Translated by Google

P arte I. Visión general suave de Big D ata y


S park
Machine Translated by Google

Capítulo 1. ¿Qué es Apache Spark?

Apache Spark es un motor de computación unificado y un conjunto de bibliotecas para el procesamiento de datos en paralelo en
clústeres de computadoras. Al momento de escribir este artículo, Spark es el motor de código abierto más desarrollado para
esta tarea, lo que lo convierte en una herramienta estándar para cualquier desarrollador o científico de datos interesado en big data.
Spark admite múltiples lenguajes de programación ampliamente utilizados (Python, Java, Scala y R), incluye bibliotecas
para diversas tareas que van desde SQL hasta transmisión y aprendizaje automático, y se ejecuta en cualquier lugar, desde
una computadora portátil hasta un clúster de miles de servidores. Esto lo convierte en un sistema fácil de usar para comenzar y
escalar hacia el procesamiento de big data o una escala increíblemente grande.

La Figura 1­1 ilustra todos los componentes y bibliotecas que Spark ofrece a los usuarios finales.

Figura 1­1. Caja de herramientas de Spark

Notará que las categorías corresponden aproximadamente a las diferentes partes de este libro. Eso realmente no debería
sorprendernos; nuestro objetivo aquí es educarlo sobre todos los aspectos de Spark, y Spark se compone de varios componentes
diferentes.

Dado que está leyendo este libro, es posible que ya sepa un poco sobre Apache Spark y lo que puede hacer. No obstante, en este
capítulo, queremos cubrir brevemente la filosofía primordial detrás de Spark, así como el contexto en el que se desarrolló (¿por
qué todo el mundo se entusiasma repentinamente con el procesamiento de datos en paralelo?) y su historia. También describiremos
los primeros pasos para ejecutar
Machine Translated by Google

Chispa ­ chispear.

Filosofía de Apache Spark


Analicemos nuestra descripción de Apache Spark, un motor de computación unificado y un conjunto de bibliotecas
para big data, en sus componentes clave:

unificado

El objetivo principal de Spark es ofrecer una plataforma unificada para escribir aplicaciones de big data. ¿Qué
entendemos por unificado? Spark está diseñado para admitir una amplia gama de tareas de análisis de datos,
que van desde la simple carga de datos y consultas SQL hasta el aprendizaje automático y el cómputo de
transmisión, sobre el mismo motor informático y con un conjunto uniforme de API. La idea principal detrás de este
objetivo es que las tareas de análisis de datos del mundo real, ya sean análisis interactivos en una herramienta
como un cuaderno Jupyter o desarrollo de software tradicional para aplicaciones de producción, tienden a
combinar muchos tipos de procesamiento y bibliotecas diferentes.

La naturaleza unificada de Spark hace que estas tareas sean más fáciles y eficientes de escribir. En primer lugar,
Spark proporciona API compatibles y coherentes que puede usar para crear una aplicación a partir de piezas más
pequeñas o de bibliotecas existentes. También le facilita escribir sus propias bibliotecas de análisis en la parte
superior. Sin embargo, las API componibles no son suficientes: las API de Spark también están diseñadas para
permitir un alto rendimiento al optimizar las diferentes bibliotecas y funciones compuestas juntas en un
programa de usuario. Por ejemplo, si carga datos con una consulta SQL y luego evalúa un modelo de aprendizaje
automático con la biblioteca ML de Spark, el motor puede combinar estos pasos en un solo escaneo de los
datos. La combinación de API generales y ejecución de alto rendimiento, sin importar cómo las combine, hace de
Spark una plataforma poderosa para aplicaciones interactivas y de producción.

El enfoque de Spark en la definición de una plataforma unificada es la misma idea detrás de las plataformas
unificadas en otras áreas de software. Por ejemplo, los científicos de datos se benefician de un conjunto unificado
de bibliotecas (p. ej., Python o R) al realizar el modelado, y los desarrolladores web se benefician de
marcos unificados como Node.js o Django. Antes de Spark, ningún sistema de código abierto intentaba
proporcionar este tipo de motor unificado para el procesamiento de datos en paralelo, lo que significaba que los
usuarios tenían que unir una aplicación a partir de varias API y sistemas. Así, Spark se convirtió rápidamente en el
estándar para este tipo de desarrollo. Con el tiempo, Spark continuó expandiendo sus API integradas para cubrir más
cargas de trabajo. Al mismo tiempo, los desarrolladores del proyecto han seguido refinando su tema de un motor
unificado. En particular, un enfoque principal de este libro serán las "API estructuradas" (tramas de datos, conjuntos
de datos y SQL) que se finalizaron en Spark 2.0 para permitir una optimización más potente en las aplicaciones
de los usuarios.

motor de computación

Al mismo tiempo que Spark se esfuerza por unificar, limita cuidadosamente su alcance a un motor informático. Con
esto, queremos decir que Spark maneja la carga de datos de los sistemas de almacenamiento y realiza
cálculos en ellos, no el almacenamiento permanente como el fin en sí mismo. Puede usar Spark con una amplia
variedad de sistemas de almacenamiento persistente, incluidos los sistemas de almacenamiento en la nube como
Machine Translated by Google

Azure Storage y Amazon S3, sistemas de archivos distribuidos como Apache Hadoop, almacenes de clave­valor
como Apache Cassandra y buses de mensajes como Apache Kafka. Sin embargo, Spark no almacena datos a largo
plazo ni favorece a uno sobre otro. La motivación clave aquí es que la mayoría de los datos ya residen en una
combinación de sistemas de almacenamiento. Los datos son costosos de mover, por lo que Spark se enfoca en
realizar cálculos sobre los datos, sin importar dónde residan. En las API orientadas al usuario, Spark trabaja
arduamente para hacer que estos sistemas de almacenamiento se vean muy similares para que las aplicaciones
no tengan que preocuparse por dónde están sus datos.

El enfoque de Spark en la computación lo hace diferente de las plataformas de software de big data anteriores,
como Apache Hadoop. Hadoop incluía tanto un sistema de almacenamiento (el sistema de archivos Hadoop,
diseñado para almacenamiento de bajo costo en grupos de servidores básicos) como un sistema informático
(MapReduce), que estaban estrechamente integrados. Sin embargo, esta elección dificulta la ejecución de
uno de los sistemas sin el otro. Más importante aún, esta elección también hace que sea un desafío escribir aplicaciones
que accedan a datos almacenados en cualquier otro lugar. Aunque Spark funciona bien en el almacenamiento de
Hadoop, hoy en día también se usa ampliamente en entornos para los que la arquitectura de Hadoop no tiene
sentido, como la nube pública (donde el almacenamiento se puede comprar por separado de la informática) o las
aplicaciones de transmisión.

bibliotecas

El componente final de Spark son sus bibliotecas, que se basan en su diseño como un motor unificado para
proporcionar una API unificada para tareas comunes de análisis de datos. Spark es compatible tanto con las bibliotecas
estándar que se envían con el motor como con una amplia gama de bibliotecas externas publicadas como paquetes de
terceros por las comunidades de código abierto. Hoy en día, las bibliotecas estándar de Spark son en realidad la mayor
parte del proyecto de código abierto: el motor central de Spark ha cambiado poco desde que se lanzó por primera
vez, pero las bibliotecas han crecido para proporcionar más y más tipos de funcionalidad.
Spark incluye bibliotecas para SQL y datos estructurados (Spark SQL), aprendizaje automático (MLlib), procesamiento
de secuencias (Spark Streaming y la transmisión estructurada más reciente) y análisis de gráficos (GraphX).
Más allá de estas bibliotecas, hay cientos de bibliotecas externas de código abierto que van desde conectores
para varios sistemas de almacenamiento hasta algoritmos de aprendizaje automático.
Un índice de bibliotecas externas está disponible en spark­packages.org.

Contexto: el problema de los grandes datos


¿Por qué necesitamos un nuevo motor y modelo de programación para el análisis de datos en primer lugar? Como ocurre
con muchas tendencias en la informática, esto se debe a cambios en los factores económicos que subyacen en las
aplicaciones y el hardware informáticos.

Durante la mayor parte de su historia, las computadoras se volvieron más rápidas cada año a través de aumentos en la
velocidad del procesador: los nuevos procesadores cada año podían ejecutar más instrucciones por segundo que el año
anterior. Como resultado, las aplicaciones también se volvieron más rápidas automáticamente cada año, sin necesidad de
realizar cambios en su código. Esta tendencia condujo a la creación de un ecosistema grande y establecido de aplicaciones
con el tiempo, la mayoría de las cuales fueron diseñadas para ejecutarse solo en un solo procesador. Estas aplicaciones
siguieron la tendencia de velocidades de procesador mejoradas para escalar a cálculos más grandes y volúmenes de datos
más grandes con el tiempo.
Machine Translated by Google

Desafortunadamente, esta tendencia en el hardware se detuvo alrededor de 2005: debido a los límites estrictos en la
disipación de calor, los desarrolladores de hardware dejaron de fabricar procesadores individuales más rápidos y
cambiaron a agregar más núcleos de CPU paralelos, todos funcionando a la misma velocidad. Este cambio
significó que de repente las aplicaciones debían modificarse para agregar paralelismo a fin de ejecutarse más rápido, lo
que sentó las bases para nuevos modelos de programación como Apache Spark.

Además de eso, las tecnologías para almacenar y recopilar datos no se ralentizaron apreciablemente en 2005, cuando lo
hicieron las velocidades de los procesadores. El costo de almacenar 1 TB de datos continúa disminuyendo
aproximadamente dos veces cada 14 meses, lo que significa que es muy económico para las organizaciones de todos
los tamaños almacenar grandes cantidades de datos. Además, muchas de las tecnologías para recopilar datos
(sensores, cámaras, conjuntos de datos públicos, etc.) continúan reduciendo su costo y mejorando su resolución. Por
ejemplo, la tecnología de las cámaras continúa mejorando en resolución y disminuyendo el costo por píxel cada año,
hasta el punto en que una cámara web de 12 megapíxeles cuesta solo $ 3 a $ 4; esto ha hecho que sea económico
recopilar una amplia gama de datos visuales, ya sea de personas que filman videos o de sensores automatizados en un
entorno industrial. Además, las cámaras son en sí mismas los sensores clave en otros dispositivos de recopilación de
datos, como telescopios e incluso máquinas de secuenciación de genes, lo que también reduce el costo de estas
tecnologías.

El resultado final es un mundo en el que recopilar datos es extremadamente económico (muchas organizaciones hoy en día
incluso consideran negligente no registrar datos de posible relevancia para el negocio), pero procesarlos requiere
grandes cálculos paralelos, a menudo en grupos de máquinas. Además, en este nuevo mundo, el software desarrollado
en los últimos 50 años no puede escalarse automáticamente, ni tampoco los modelos de programación tradicionales
para aplicaciones de procesamiento de datos, lo que crea la necesidad de nuevos modelos de programación. Es este
mundo para el que se creó Apache Spark.

Historia de la chispa
Apache Spark comenzó en UC Berkeley en 2009 como el proyecto de investigación Spark, que se publicó por
primera vez al año siguiente en un artículo titulado "Spark: Cluster Computing with Working Sets" de Matei Zaharia,
Mosharaf Chowdhury, Michael Franklin, Scott Shenker e Ion Stoica. del AMPlab de la Universidad de California en Berkeley.
En ese momento, Hadoop MapReduce era el motor de programación paralelo dominante para clústeres, siendo el primer
sistema de código abierto en abordar el procesamiento paralelo de datos en clústeres de miles de nodos. El AMPlab
había trabajado con varios de los primeros usuarios de MapReduce para comprender las ventajas y desventajas de este
nuevo modelo de programación y, por lo tanto, pudo sintetizar una lista de problemas en varios casos de uso y comenzar
a diseñar plataformas informáticas más generales. Además, Zaharia también había trabajado con usuarios de
Hadoop en UC Berkeley para comprender sus necesidades de la plataforma, específicamente, equipos que estaban
realizando aprendizaje automático a gran escala utilizando algoritmos iterativos que necesitan realizar múltiples pases sobre
los datos.

A través de estas conversaciones, dos cosas quedaron claras. En primer lugar, la computación en clúster tenía un
enorme potencial: en cada organización que usaba MapReduce, se podían crear aplicaciones completamente nuevas
usando los datos existentes, y muchos grupos nuevos comenzaron a usar el sistema después de sus casos de uso iniciales.
En segundo lugar, sin embargo, el motor MapReduce hizo que la creación de aplicaciones de gran tamaño fuera desafiante
e ineficiente. Por ejemplo, el algoritmo típico de aprendizaje automático podría necesitar hacer 10 o 20
Machine Translated by Google

pasa sobre los datos, y en MapReduce, cada paso tenía que escribirse como un trabajo de MapReduce separado,
que tenía que lanzarse por separado en el clúster y cargar los datos desde cero.

Para abordar este problema, el equipo de Spark primero diseñó una API basada en programación funcional que
podría expresar de manera sucinta aplicaciones de varios pasos. Luego, el equipo implementó esta API en un nuevo
motor que podría realizar un intercambio eficiente de datos en memoria a través de los pasos de cómputo. El
equipo también comenzó a probar este sistema tanto con Berkeley como con usuarios externos.

La primera versión de Spark solo admitía aplicaciones por lotes, pero pronto quedó claro otro caso de
uso convincente: la ciencia de datos interactiva y las consultas ad hoc. Simplemente conectando el intérprete
de Scala a Spark, el proyecto podría proporcionar un sistema interactivo muy útil para ejecutar consultas en
cientos de máquinas. El AMPlab también se basó rápidamente en esta idea para desarrollar Shark, un motor que
podría ejecutar consultas SQL sobre Spark y permitir el uso interactivo por parte de analistas y científicos de datos.
Shark se lanzó por primera vez en 2011.

Después de estos lanzamientos iniciales, rápidamente quedó claro que las adiciones más poderosas a Spark
serían las nuevas bibliotecas, por lo que el proyecto comenzó a seguir el enfoque de "biblioteca estándar" que tiene
hoy. En particular, diferentes grupos de AMPlab iniciaron MLlib, Spark Streaming y GraphX.
También se aseguraron de que estas API serían altamente interoperables, lo que permitiría escribir aplicaciones
de big data de extremo a extremo en el mismo motor por primera vez.

En 2013, el proyecto se había generalizado, con más de 100 colaboradores de más de 30 organizaciones fuera
de UC Berkeley. El AMPlab contribuyó con Spark a Apache Software Foundation como un hogar a largo
plazo e independiente del proveedor para el proyecto. El primer equipo de AMPlab también lanzó una
empresa, Databricks, para fortalecer el proyecto, uniéndose a la comunidad de otras empresas y
organizaciones que contribuyen a Spark. Desde entonces, la comunidad de Apache Spark lanzó Spark 1.0 en
2014 y Spark 2.0 en 2016, y continúa realizando lanzamientos regulares, incorporando nuevas funciones al
proyecto.

Finalmente, la idea central de Spark de las API componibles también se ha perfeccionado con el tiempo. Las
primeras versiones de Spark (antes de la 1.0) definían en gran medida esta API en términos de operaciones
funcionales: operaciones paralelas como mapas y reducciones sobre colecciones de objetos Java. A partir de 1.0, el
proyecto agregó Spark SQL, una nueva API para trabajar con datos estructurados : tablas con un formato de datos
fijo que no está vinculado a la representación en memoria de Java. Spark SQL permitió optimizaciones nuevas
y potentes en bibliotecas y API al comprender con más detalle tanto el formato de datos como el código de usuario
que se ejecuta en ellos. Con el tiempo, el proyecto agregó una gran cantidad de nuevas API que se basan
en esta base estructurada más poderosa, incluidos DataFrames, canalizaciones de aprendizaje automático
y transmisión estructurada, una API de transmisión optimizada automáticamente de alto nivel. En este libro,
dedicaremos una cantidad significativa de tiempo a explicar estas API de próxima generación, la mayoría de
las cuales están marcadas como listas para producción.

El presente y el futuro de Spark


Spark existe desde hace varios años, pero continúa ganando popularidad y casos de uso.
Muchos proyectos nuevos dentro del ecosistema Spark continúan empujando los límites de lo que está
Machine Translated by Google

posible con el sistema. Por ejemplo, en 2016 se introdujo un nuevo motor de transmisión de alto nivel, Structured
Streaming. Esta tecnología es una gran parte de las empresas que resuelven desafíos de datos a gran escala,
desde compañías de tecnología como Uber y Netflix que usan las herramientas de transmisión y aprendizaje
automático de Spark, hasta instituciones como la NASA, el CERN y el Instituto Broad del MIT y Harvard aplican
Spark al análisis de datos científicos.

Spark seguirá siendo la piedra angular de las empresas que realizan análisis de big data en el futuro previsible,
especialmente dado que el proyecto aún se está desarrollando rápidamente. Cualquier científico o ingeniero de
datos que necesite resolver grandes problemas de datos probablemente necesite una copia de Spark en su
máquina y, con suerte, ¡una copia de este libro en su biblioteca!

Chispa corriente
Este libro contiene una gran cantidad de código relacionado con Spark y es esencial que esté preparado para
ejecutarlo a medida que aprende. En su mayor parte, querrá ejecutar el código de forma interactiva para poder
experimentar con él. Repasemos algunas de sus opciones antes de comenzar a trabajar con las partes de
codificación del libro.

Puede usar Spark desde Python, Java, Scala, R o SQL. Spark está escrito en Scala y se ejecuta en Java Virtual
Machine (JVM), por lo tanto, para ejecutar Spark en su computadora portátil o en un clúster, todo lo que necesita
es una instalación de Java. Si desea utilizar la API de Python, también necesitará un intérprete de Python
(versión 2.7 o posterior). Si desea utilizar R, necesitará una versión de R en su máquina.

Hay dos opciones que recomendamos para comenzar con Spark: descargar e instalar Apache Spark en su
computadora portátil o ejecutar una versión basada en web en Databricks Community Edition, un entorno de nube
gratuito para aprender Spark que incluye el código de este libro. A continuación explicamos ambas opciones.

Descargar Spark localmente


Si desea descargar y ejecutar Spark localmente, el primer paso es asegurarse de tener Java instalado en su
máquina (disponible como Java), así como una versión de Python si desea usar Python. A continuación, visite
la página de descarga oficial del proyecto, seleccione el tipo de paquete "Prediseñado para Hadoop 2.7 y versiones
posteriores" y haga clic en "Descarga directa". Esto descarga un archivo TAR comprimido, o tarball, que luego
deberá extraer. La mayor parte de este libro se escribió con Spark 2.2, por lo que descargar la versión 2.2 o
posterior debería ser un buen punto de partida.

Descarga de Spark para un clúster de Hadoop

Spark puede ejecutarse localmente sin ningún sistema de almacenamiento distribuido, como Apache Hadoop. Sin
embargo, si desea conectar la versión de Spark en su computadora portátil a un clúster de Hadoop, asegúrese de
descargar la versión de Spark correcta para esa versión de Hadoop, que se puede elegir en http://
spark.apache.org/downloads.html seleccionando un tipo de paquete diferente. Discutimos cómo se ejecuta
Spark en clústeres y el sistema de archivos Hadoop en capítulos posteriores, pero en este punto
Machine Translated by Google

recomendamos simplemente ejecutar Spark en su computadora portátil para comenzar.

NOTA

En Spark 2.2, los desarrolladores también agregaron la capacidad de instalar Spark para Python a través de
pip install pyspark. Esta funcionalidad surgió cuando se estaba escribiendo este libro, por lo que no pudimos incluir
todas las instrucciones relevantes.

Construyendo Spark desde la fuente

No cubriremos esto en el libro, pero también puede compilar y configurar Spark desde la fuente. Puede
seleccionar un paquete fuente en la página de descarga de Apache para obtener solo la fuente y seguir las
instrucciones en el archivo README para compilar.

Una vez que haya descargado Spark, querrá abrir un indicador de línea de comandos y extraer el paquete.
En nuestro caso, estamos instalando Spark 2.2. El siguiente es un fragmento de código que puede ejecutar en
cualquier línea de comando de estilo Unix para descomprimir el archivo que descargó de Spark y moverlo al
directorio:

cd ~/Descargas
tar ­xf spark­2.2.0­bin­hadoop2.7.tgz cd
spark­2.2.0­bin­hadoop2.7.tgz

Tenga en cuenta que Spark tiene una gran cantidad de directorios y archivos dentro del proyecto.
¡No se deje intimidar! La mayoría de estos directorios son relevantes solo si está leyendo el código fuente.
La siguiente sección cubrirá los directorios más importantes, los que nos permiten lanzar las diferentes
consolas de Spark para uso interactivo.

Lanzamiento de las consolas interactivas de Spark


Puede iniciar un shell interactivo en Spark para varios lenguajes de programación diferentes. La mayor
parte de este libro está escrito con Python, Scala y SQL en mente; por lo tanto, esos son nuestros
puntos de partida recomendados.

Lanzamiento de la consola de Python

Necesitará Python 2 o 3 instalado para iniciar la consola de Python. Desde el directorio de inicio de Spark,
ejecute el siguiente código:

./bin/pyspark

Después de hacer eso, escriba "chispa" y presione Entrar. Verá impreso el objeto SparkSession, que trataremos
en el Capítulo 2.

Inicio de la consola Scala


Machine Translated by Google

Para iniciar la consola Scala, deberá ejecutar el siguiente comando:

./bin/spark­shell

Después de hacer eso, escriba "chispa" y presione Entrar. Al igual que en Python, verá el objeto SparkSession,
que trataremos en el Capítulo 2.

Lanzamiento de la consola SQL

Partes de este libro cubrirán una gran cantidad de Spark SQL. Para esos, es posible que desee iniciar la
consola SQL. Revisaremos algunos de los detalles más relevantes después de que realmente cubramos
estos temas en el libro.

./bin/spark­sql

Ejecutar Spark en la nube


Si desea tener una experiencia de cuaderno interactiva simple para aprender Spark, es posible que
prefiera usar Databricks Community Edition. Databricks, como mencionamos anteriormente, es una
empresa fundada por el equipo de Berkeley que inició Spark y ofrece una edición comunitaria gratuita de su
servicio en la nube como entorno de aprendizaje. Databricks Community Edition incluye una copia de
todos los datos y ejemplos de código de este libro, lo que facilita la ejecución rápida de cualquiera de ellos.
Para usar Databricks Community Edition, siga las instrucciones en https://
github.com/databricks/Spark­The­Definitive­Guide. Podrá utilizar Scala, Python, SQL o R desde una
interfaz basada en navegador web para ejecutar y visualizar resultados.

Datos utilizados en este libro


Usaremos varias fuentes de datos en este libro para nuestros ejemplos. Si desea ejecutar el código
localmente, puede descargarlo del repositorio de código oficial de este libro, como se describe en https://
github.com/databricks/Spark­The­Definitive­Guide. En resumen, descargará los datos, los colocará en una
carpeta y luego ejecutará los fragmentos de código en este libro.
Machine Translated by Google

Capítulo 2. Una suave introducción a


Spark

Ahora que nuestra lección de historia sobre Apache Spark está completa, ¡es hora de comenzar a usarlo y aplicarlo!
Este capítulo presenta una breve introducción a Spark, en la que recorreremos la arquitectura central de un clúster,
la aplicación Spark y las API estructuradas de Spark mediante DataFrames y SQL. En el camino, abordaremos la
terminología y los conceptos básicos de Spark para que pueda comenzar a usar Spark de inmediato.
Comencemos con algunos antecedentes básicos.

Arquitectura básica de Spark


Por lo general, cuando piensa en una "computadora", piensa en una máquina que se encuentra en su escritorio en
casa o en el trabajo. Esta máquina funciona perfectamente bien para ver películas o trabajar con software de
hoja de cálculo. Sin embargo, como es probable que muchos usuarios experimenten en algún momento, hay
algunas cosas que su computadora no es lo suficientemente poderosa para realizar. Un área particularmente
desafiante es el procesamiento de datos. Las máquinas individuales no tienen suficiente potencia y
recursos para realizar cálculos en grandes cantidades de información (o el usuario probablemente no tiene
tiempo para esperar a que finalice el cálculo). Un clúster o grupo de computadoras reúne los recursos de
muchas máquinas, lo que nos brinda la capacidad de utilizar todos los recursos acumulados como si fueran una
sola computadora. Ahora, un grupo de máquinas por sí solo no es poderoso, necesita un marco para coordinar
el trabajo entre ellas. Spark hace exactamente eso, administrar y coordinar la ejecución de tareas en datos en un
grupo de computadoras.

El clúster de máquinas que usará Spark para ejecutar tareas es administrado por un administrador de clústeres como
el administrador de clústeres independiente de Spark, YARN o Mesos. Luego, enviamos aplicaciones Spark a
estos administradores de clústeres, que otorgarán recursos a nuestra aplicación para que podamos completar nuestro
trabajo.

Aplicaciones de chispa
Las aplicaciones Spark constan de un proceso controlador y un conjunto de procesos ejecutores . El proceso del
controlador ejecuta su función main(), se ubica en un nodo en el clúster y es responsable de tres cosas:
mantener la información sobre la aplicación Spark; responder al programa o entrada de un usuario; y analizar,
distribuir y programar el trabajo entre los ejecutores (discutido momentáneamente).
El proceso del controlador es absolutamente esencial: es el corazón de una aplicación Spark y mantiene toda la
información relevante durante la vida útil de la aplicación.

Los ejecutores son responsables de realizar efectivamente el trabajo que el conductor les asigna.
Esto significa que cada ejecutor es responsable de solo dos cosas: ejecutar el código que le asigna el controlador
e informar el estado de la computación en ese ejecutor al nodo del controlador.
Machine Translated by Google

La figura 2­1 demuestra cómo el administrador de clústeres controla las máquinas físicas y asigna recursos a las
aplicaciones Spark. Puede ser uno de los tres administradores de clúster principales: el administrador de clúster
independiente de Spark, YARN o Mesos. Esto significa que puede haber varias aplicaciones Spark ejecutándose en un
clúster al mismo tiempo. Hablaremos más sobre los administradores de clústeres en la Parte IV.

Figura 2­1. La arquitectura de una aplicación Spark

En la Figura 2­1, podemos ver el conductor a la izquierda y cuatro ejecutores a la derecha. En este diagrama, eliminamos el
concepto de nodos de clúster. El usuario puede especificar cuántos ejecutores deben caer en cada nodo a través de
configuraciones.

NOTA

Spark, además de su modo clúster, también tiene un modo local. El controlador y los ejecutores son
simplemente procesos, lo que significa que pueden vivir en la misma máquina o en diferentes máquinas. En modo
local, el controlador y los ejecutores se ejecutan (como subprocesos) en su computadora individual en lugar de en
un clúster. Escribimos este libro pensando en el modo local, por lo que debería poder ejecutar todo en una sola máquina.

Estos son los puntos clave que debe comprender acerca de las aplicaciones Spark en este momento:

Spark emplea un administrador de clústeres que realiza un seguimiento de los recursos disponibles.
Machine Translated by Google

El proceso del controlador es responsable de ejecutar los comandos del programa del controlador en los
ejecutores para completar una tarea determinada.

Los ejecutores, en su mayor parte, siempre ejecutarán código Spark. Sin embargo, el controlador puede ser "controlado"
desde varios idiomas diferentes a través de las API de idioma de Spark. Echemos un vistazo a los de la siguiente sección.

API de lenguaje de Spark


Las API de lenguaje de Spark le permiten ejecutar código Spark usando varios lenguajes de programación. En su
mayor parte, Spark presenta algunos "conceptos" básicos en todos los idiomas; estos conceptos luego se traducen
en código Spark que se ejecuta en el grupo de máquinas. Si usa solo las API estructuradas, puede esperar que todos
los idiomas tengan características de rendimiento similares.
He aquí un breve resumen:

Scala

Spark está escrito principalmente en Scala, lo que lo convierte en el lenguaje "predeterminado" de Spark. Este
libro incluirá ejemplos de código Scala donde sea relevante.

Java

Aunque Spark está escrito en Scala, los autores de Spark han tenido cuidado de asegurarse de que pueda escribir
código Spark en Java. Este libro se centrará principalmente en Scala, pero proporcionará ejemplos de Java
cuando sea pertinente.

Pitón

Python admite casi todas las construcciones que admite Scala. Este libro incluirá ejemplos de código de Python
siempre que incluyamos ejemplos de código de Scala y exista una API de Python.

sql

Spark admite un subconjunto del estándar ANSI SQL 2003. Esto facilita que los analistas y los no programadores
aprovechen los poderes de big data de Spark. Este libro incluye ejemplos de código SQL siempre que sea relevante.

Spark tiene dos bibliotecas de R de uso común: una como parte del núcleo de Spark (SparkR) y otra como un
paquete impulsado por la comunidad de R (sparklyr). Cubrimos ambas integraciones en el Capítulo 32.

La Figura 2­2 presenta una ilustración simple de esta relación.


Machine Translated by Google

Figura 2­2. La relación entre SparkSession y la API de lenguaje de Spark

Cada API de idioma mantiene los mismos conceptos básicos que describimos anteriormente. Hay un
objeto SparkSession disponible para el usuario, que es el punto de entrada para ejecutar código Spark.
Cuando usa Spark desde Python o R, no escribe instrucciones JVM explícitas; en su lugar, escribe
código Python y R que Spark traduce en código que luego puede ejecutar en las JVM del ejecutor.

API de Spark
Aunque puede manejar Spark desde una variedad de idiomas, vale la pena mencionar lo que pone a
disposición en esos idiomas. Spark tiene dos conjuntos fundamentales de API: las API "no
estructuradas" de bajo nivel y las API estructuradas de nivel superior. Discutimos ambos en este libro,
pero estos capítulos introductorios se centrarán principalmente en las API estructuradas de nivel superior.

Chispa de arranque
Hasta ahora, cubrimos los conceptos básicos de las aplicaciones Spark. Todo esto ha sido de naturaleza
conceptual. Cuando realmente comencemos a escribir nuestra aplicación Spark, necesitaremos una forma
de enviarle comandos y datos de usuario. Lo hacemos creando primero una SparkSession.

NOTA
Para hacer esto, iniciaremos el modo local de Spark, tal como lo hicimos en el Capítulo 1. Esto significa
ejecutar ./bin/spark­shell para acceder a la consola de Scala e iniciar una sesión interactiva. También puede
iniciar la consola de Python utilizando ./bin/pyspark. Esto inicia una aplicación Spark interactiva. También existe
un proceso para enviar aplicaciones independientes a Spark llamado spark­submit, mediante el cual
puede enviar una aplicación precompilada a Spark. Le mostraremos cómo hacerlo en el Capítulo 3.

Cuando inicia Spark en este modo interactivo, crea implícitamente una SparkSession que administra la
aplicación Spark. Cuando lo inicia a través de una aplicación independiente, debe crear el objeto
SparkSession usted mismo en el código de su aplicación.
Machine Translated by Google

La SparkSession
Como se discutió al comienzo de este capítulo, usted controla su aplicación Spark a través de un
proceso de controlador llamado SparkSession. La instancia de SparkSession es la forma en que Spark
ejecuta manipulaciones definidas por el usuario en todo el clúster. Existe una correspondencia uno a uno
entre una SparkSession y una aplicación Spark. En Scala y Python, la variable está disponible como
chispa cuando inicia la consola. Avancemos y veamos SparkSession tanto en Scala como en Python:

Chispa ­ chispear

En Scala, debería ver algo como lo siguiente:

res0: org.apache.spark.sql.SparkSession = org.apache.spark.sql.SparkSession@...

En Python verás algo como esto:

<pyspark.sql.session.SparkSession en 0x7efda4c1ccd0>

Realicemos ahora la tarea simple de crear un rango de números. Este rango de números es como una
columna con nombre en una hoja de cálculo:

// en Scala
val myRange = chispa.rango(1000).toDF("numero")

# en Python
myRange = chispa.rango(1000).toDF("número")

¡Acabas de ejecutar tu primer código Spark! Creamos un DataFrame con una columna que contiene 1000
filas con valores de 0 a 999. Este rango de números representa una colección distribuida. Cuando se ejecuta
en un clúster, cada parte de este rango de números existe en un ejecutor diferente. Este es un marco de
datos Spark.

marcos de datos
Un DataFrame es la API estructurada más común y simplemente representa una tabla de datos con
filas y columnas. La lista que define las columnas y los tipos dentro de esas columnas se denomina
esquema. Puede pensar en un DataFrame como una hoja de cálculo con columnas nombradas. La
figura 2­3 ilustra la diferencia fundamental: una hoja de cálculo se encuentra en una computadora
en una ubicación específica, mientras que un Spark DataFrame puede abarcar miles de computadoras.
La razón para poner los datos en más de una computadora debe ser intuitiva: o los datos son demasiado
grandes para caber en una máquina o simplemente llevaría demasiado tiempo realizar ese cálculo en una máquina.
Machine Translated by Google

Figura 2­3. Análisis distribuido versus análisis de una sola máquina

El concepto DataFrame no es exclusivo de Spark. R y Python tienen conceptos similares.


Sin embargo, los marcos de datos de Python/R (con algunas excepciones) existen en una máquina
en lugar de en varias máquinas. Esto limita lo que puede hacer con un DataFrame dado a los recursos
que existen en esa máquina específica. Sin embargo, debido a que Spark tiene interfaces de lenguaje tanto
para Python como para R, es bastante fácil convertir Pandas (Python) DataFrames en Spark
DataFrames y R DataFrames en Spark DataFrames.

NOTA

Spark tiene varias abstracciones básicas: conjuntos de datos, marcos de datos, tablas SQL y conjuntos de datos
distribuidos resistentes (RDD). Todas estas diferentes abstracciones representan colecciones distribuidas de datos. Los
más fáciles y eficientes son los DataFrames, que están disponibles en todos los idiomas. Cubrimos los conjuntos de datos
al final de la Parte II y los RDD en la Parte III.

Particiones
Para permitir que cada ejecutor realice el trabajo en paralelo, Spark divide los datos en fragmentos llamados
particiones. Una partición es una colección de filas que se encuentran en una máquina física en su clúster.
Las particiones de un DataFrame representan cómo los datos se distribuyen físicamente en el grupo
de máquinas durante la ejecución. Si tiene una partición, Spark tendrá un paralelismo de solo una, incluso si
tiene miles de ejecutores. Si tiene muchas particiones pero solo un ejecutor, Spark seguirá teniendo un
paralelismo de solo uno porque solo hay un recurso de cómputo.

Una cosa importante a tener en cuenta es que con DataFrames (en su mayor parte) no manipula las
particiones de forma manual o individual. Simplemente especifica transformaciones de datos de alto nivel en
las particiones físicas y Spark determina cómo se ejecutará realmente este trabajo en el clúster.
Las API de nivel inferior existen (a través de la interfaz RDD), y las cubrimos en la Parte III.

Transformaciones
Machine Translated by Google

En Spark, las estructuras de datos principales son inmutables, lo que significa que no se pueden cambiar una vez
creadas. Esto puede parecer un concepto extraño al principio: si no puede cambiarlo, ¿cómo se supone que debe
usarlo? Para "cambiar" un DataFrame, debe indicarle a Spark cómo le gustaría modificarlo para que haga lo que desea.
Estas instrucciones se llaman transformaciones. Realicemos una transformación simple para encontrar todos los
números pares en nuestro DataFrame actual:

// en Scala val
divisBy2 = myRange.where("numero % 2 = 0")

# en Python
divisBy2 = myRange.where("número % 2 = 0")

Tenga en cuenta que estos no devuelven ningún resultado. Esto se debe a que especificamos solo una transformación
abstracta, y Spark no actuará sobre las transformaciones hasta que llamemos una acción (lo discutiremos en breve).
Las transformaciones son el núcleo de cómo expresa su lógica de negocios usando Spark. Hay dos tipos de
transformaciones: las que especifican dependencias estrechas y las que especifican dependencias amplias.

Las transformaciones que consisten en dependencias estrechas (las llamaremos transformaciones estrechas) son aquellas
en las que cada partición de entrada contribuirá a una sola partición de salida. En el fragmento de código anterior, la
declaración where especifica una dependencia estrecha, donde solo una partición contribuye a una partición de
salida como máximo, como puede ver en la Figura 2­4.

Figura 2­4. Una estrecha dependencia


Machine Translated by Google

Una transformación de estilo de dependencia amplia (o transformación amplia) tendrá particiones de


entrada que contribuirán a muchas particiones de salida. A menudo escuchará que esto se conoce como una
mezcla en la que Spark intercambiará particiones en todo el clúster. Con transformaciones limitadas,
Spark realizará automáticamente una operación llamada canalización, lo que significa que si especificamos
varios filtros en DataFrames, todos se realizarán en la memoria. No se puede decir lo mismo de las barajas.
Cuando realizamos una reproducción aleatoria, Spark escribe los resultados en el disco. Las transformaciones
amplias se ilustran en la figura 2­5.

Figura 2­5. Una amplia dependencia

Verá mucha discusión sobre la optimización aleatoria en la web porque es un tema importante, pero por ahora,
todo lo que necesita entender es que hay dos tipos de transformaciones. Ahora puede ver cómo las
transformaciones son simplemente formas de especificar diferentes series de manipulación de datos.
Esto nos lleva a un tema llamado evaluación perezosa.

Evaluación perezosa
La evaluación perezosa significa que Spark esperará hasta el último momento para ejecutar el gráfico de
instrucciones de cálculo. En Spark, en lugar de modificar los datos inmediatamente cuando expresa alguna
operación, crea un plan de transformaciones que le gustaría aplicar a sus datos de origen. Al esperar hasta
el último minuto para ejecutar el código, Spark compila este plan a partir de sus transformaciones de DataFrame
sin procesar en un plan físico optimizado que se ejecutará de la manera más eficiente posible en todo el clúster.
Esto proporciona inmensos beneficios porque Spark puede optimizar el
Machine Translated by Google

todo el flujo de datos de extremo a extremo. Un ejemplo de esto es algo llamado pushdown de predicado en DataFrames. Si
construimos un gran trabajo de Spark pero especificamos un filtro al final que solo requiere que obtengamos una fila de nuestros
datos de origen, la forma más eficiente de ejecutar esto es acceder al único registro que necesitamos. Spark realmente optimizará
esto para nosotros empujando el filtro hacia abajo automáticamente.

Comportamiento

Las transformaciones nos permiten construir nuestro plan lógico de transformación. Para activar el cálculo, ejecutamos una acción.
Una acción le indica a Spark que calcule un resultado de una serie de transformaciones.

La acción más simple es contar, que nos da el número total de registros en el DataFrame:

divisBy2.contar()

La salida del código anterior debe ser 500. Por supuesto, contar no es la única acción. Hay tres tipos de acciones:

Acciones para ver datos en la consola

Acciones para recopilar datos a objetos nativos en el idioma respectivo

Acciones para escribir en orígenes de datos de salida

Al especificar esta acción, comenzamos un trabajo de Spark que ejecuta nuestra transformación de filtro (una transformación
limitada), luego una agregación (una transformación amplia) que realiza los recuentos por partición y luego una recopilación, lo que
lleva nuestro resultado a un objeto nativo en el idioma respectivo. Puede ver todo esto al inspeccionar la interfaz de usuario de
Spark, una herramienta incluida en Spark con la que puede monitorear los trabajos de Spark que se ejecutan en un clúster.

Interfaz de usuario de Spark

Puede monitorear el progreso de un trabajo a través de la interfaz de usuario web de Spark. La interfaz de usuario de Spark está
disponible en el puerto 4040 del nodo del controlador. Si está ejecutando en modo local, será http://localhost:4040.
La interfaz de usuario de Spark muestra información sobre el estado de sus trabajos de Spark, su entorno y el estado del clúster. Es
muy útil, especialmente para ajustar y depurar. La figura 2­6 muestra una IU de ejemplo para un trabajo de Spark donde se ejecutaron
dos etapas que contenían nueve tareas.
Machine Translated by Google

Figura 2­6. La interfaz de usuario de Spark

Este capítulo no entrará en detalles sobre la ejecución de trabajos de Spark y la interfaz de usuario de Spark. Lo cubriremos
en el Capítulo 18. En este punto, todo lo que necesita comprender es que un trabajo de Spark representa un conjunto de
transformaciones desencadenadas por una acción individual, y puede monitorear ese trabajo desde la interfaz de usuario
de Spark.

Un ejemplo de extremo a extremo


En el ejemplo anterior, creamos un DataFrame de un rango de números; no es exactamente un big data
innovador. En esta sección, reforzaremos todo lo que aprendimos anteriormente en este capítulo con un ejemplo más
realista y explicaremos paso a paso lo que sucede debajo del capó. Usaremos Spark para analizar algunos datos de vuelo
de las estadísticas de la Oficina de Transporte de los Estados Unidos.

Dentro de la carpeta CSV, verá que tenemos varios archivos. También hay una serie de otras carpetas con diferentes
formatos de archivo, que analizamos en el Capítulo 9. Por ahora, concentrémonos en los archivos CSV.

Cada archivo tiene un número de filas dentro de él. Estos archivos son archivos CSV, lo que significa que tienen un formato
de datos semiestructurados, en el que cada fila del archivo representa una fila en nuestro DataFrame futuro:

$ head /data/flight­data/csv/2015­summary.csv

DEST_COUNTRY_NAME,ORIGIN_COUNTRY_NAME,recuento
Estados Unidos, Rumania, 15
Estados Unidos, Croacia, 1
Estados Unidos, Irlanda, 344

Spark incluye la capacidad de leer y escribir desde una gran cantidad de fuentes de datos. Para leer estos datos,
utilizaremos un DataFrameReader asociado a nuestra SparkSession. Al hacerlo, nosotros
Machine Translated by Google

especificará el formato del archivo, así como cualquier opción que queramos especificar. En nuestro caso,
queremos hacer algo llamado inferencia de esquema, lo que significa que queremos que Spark adivine cuál debería
ser el esquema de nuestro DataFrame. También queremos especificar que la primera fila es el encabezado del
archivo, por lo que también lo especificaremos como una opción.

Para obtener la información del esquema, Spark lee un poco de los datos y luego intenta analizar los tipos en
esas filas según los tipos disponibles en Spark. También tiene la opción de especificar estrictamente un
esquema cuando lee datos (lo que recomendamos en escenarios de producción):

// en Scala
val flightData2015 =

chispa .read .option("inferSchema",


"true") .option("header",
"true") .csv("/data/flight­data/csv/2015­summary.csv ")

# en Python
flightData2015 =

chispa\ .read\ .option("inferSchema",


"true")\ .option("header", "true")
\ .csv("/data/flight­data/csv/2015­summary .csv")

Cada uno de estos DataFrames (en Scala y Python) tiene un conjunto de columnas con un número de filas
no especificado. La razón por la que no se especifica el número de filas es porque la lectura de datos es
una transformación y, por lo tanto, es una operación diferida. Spark echó un vistazo a solo un par de filas de
datos para tratar de adivinar de qué tipo debería ser cada columna. La Figura 2­7 proporciona una ilustración del
archivo CSV que se lee en un DataFrame y luego se convierte en una matriz local o lista de filas.

Figura 2­7. Leer un archivo CSV en un DataFrame y convertirlo en una matriz local o una lista de filas

Si realizamos la acción de tomar en el DataFrame, podremos ver los mismos resultados que vimos antes cuando
usamos la línea de comando:

datos de vuelo2015.take(3)

Array([Estados Unidos,Rumania,15], [Estados Unidos,Croacia...

¡Especifiquemos algunas transformaciones más! Ahora, ordenemos nuestros datos de acuerdo con la
columna de recuento, que es de tipo entero. La figura 2­8 ilustra este proceso.
Machine Translated by Google

NOTA

Recuerde, sort no modifica el DataFrame. Usamos sort como una transformación que devuelve un nuevo
DataFrame al transformar el DataFrame anterior. Ilustremos lo que sucede cuando llamamos a tomar ese
DataFrame resultante (Figura 2­8).

Figura 2­8. Lectura, clasificación y recopilación de un DataFrame

No sucede nada con los datos cuando llamamos a ordenar porque es solo una transformación. Sin embargo, podemos
ver que Spark está creando un plan sobre cómo ejecutará esto en todo el clúster mirando el plan de explicación. Podemos
llamar a Explain en cualquier objeto DataFrame para ver el linaje del DataFrame (o cómo Spark ejecutará esta
consulta):

datos de vuelo2015.sort("recuento").explain()

== Plan físico == *Ordenar


[recuento n.º 195 ASC NULLS PRIMERO], verdadero, 0
+­ Intercambio de partición de rango (recuento n.º 195 ASC NULLS PRIMERO, 200)
+­ *FileScan csv [DEST_COUNTRY_NAME#193,ORIGIN_COUNTRY_NAME#194,count#195] ...

¡Felicidades, acabas de leer tu primer plan de explicación! Explicar los planes es un poco misterioso, pero con un poco de
práctica se convierte en una segunda naturaleza. Puede leer los planes de explicación de arriba a abajo, siendo la parte
superior el resultado final y la parte inferior la(s) fuente(s) de datos. En este caso, eche un vistazo a las primeras palabras
clave. Verá ordenar, intercambiar y FileScan. Esto se debe a que el tipo de nuestros datos es en realidad una transformación
amplia porque las filas deberán compararse entre sí. No se preocupe demasiado por comprender todo acerca de los planes
de explicación en este punto, pueden ser herramientas útiles para depurar y mejorar su conocimiento a medida que
avanza con Spark.

Ahora, tal como lo hicimos antes, podemos especificar una acción para iniciar este plan. Sin embargo, antes de hacer
eso, vamos a establecer una configuración. De forma predeterminada, cuando realizamos una reproducción aleatoria,
Spark genera 200 particiones aleatorias. Establezcamos este valor en 5 para reducir el número de particiones de
salida de la reproducción aleatoria:

chispa.conf.set("chispa.sql.shuffle.particiones", "5")

datosvuelo2015.sort("recuento").tomar(2)

... Array([Estados Unidos,Singapur,1], [Moldavia,Estados Unidos,1])


Machine Translated by Google

La Figura 2­9 ilustra esta operación. Tenga en cuenta que, además de las transformaciones lógicas, también
incluimos el recuento de particiones físicas.

Figura 2­9. El proceso de manipulación lógica y física de DataFrame

El plan lógico de transformaciones que construimos define un linaje para DataFrame de modo que, en cualquier
momento, Spark sepa cómo volver a calcular cualquier partición realizando todas las operaciones que tenía antes
en los mismos datos de entrada. Esto se encuentra en el corazón del modelo de programación de Spark:
programación funcional donde las mismas entradas siempre dan como resultado las mismas salidas cuando las
transformaciones en esos datos se mantienen constantes.

No manipulamos los datos físicos; en cambio, configuramos las características de ejecución física a través de
cosas como el parámetro de particiones aleatorias que configuramos hace unos momentos. Terminamos con
cinco particiones de salida porque ese es el valor que especificamos en la partición aleatoria. Puede cambiar esto
para ayudar a controlar las características de ejecución física de sus trabajos de Spark. Continúe y experimente
con diferentes valores y vea la cantidad de particiones usted mismo. Al experimentar con diferentes valores,
debería ver tiempos de ejecución drásticamente diferentes. Recuerde que puede monitorear el progreso del
trabajo navegando a la interfaz de usuario de Spark en el puerto 4040 para ver las características de
ejecución física y lógica de sus trabajos.

marcos de datos y SQL


Trabajamos a través de una transformación simple en el ejemplo anterior, ahora trabajemos a través de una
más compleja y sigamos tanto en DataFrames como en SQL. Spark puede ejecutar las mismas
transformaciones, independientemente del idioma, exactamente de la misma manera. Puede expresar su
lógica comercial en SQL o DataFrames (ya sea en R, Python, Scala o Java) y Spark compilará esa lógica
en un plan subyacente (que puede ver en el plan de explicación) antes de ejecutar su código. Con Spark
SQL, puede registrar cualquier DataFrame como una tabla o vista (una tabla temporal) y consultarlo usando
SQL puro. No hay diferencia de rendimiento entre escribir consultas SQL o escribir código DataFrame,
ambos se "compilan" en el mismo plan subyacente que especificamos en el código DataFrame.
Machine Translated by Google

Puede convertir cualquier DataFrame en una tabla o vista con una simple llamada de método:

datos de vuelo2015.createOrReplaceTempView("datos_de_vuelo_2015")

Ahora podemos consultar nuestros datos en SQL. Para hacerlo, usaremos la función spark.sql (recuerde,
spark es nuestra variable SparkSession) que devuelve convenientemente un nuevo DataFrame. Aunque esto
puede parecer un poco circular en lógica, que una consulta SQL contra un DataFrame devuelva otro
DataFrame, en realidad es bastante poderoso. ¡Esto le permite especificar transformaciones de
la manera más conveniente para usted en cualquier momento dado y no sacrificar ninguna eficiencia para hacerlo!
Para entender que esto está sucediendo, echemos un vistazo a dos planes de explicación:

// en Scala val
sqlWay = chispa.sql("""
SELECCIONE DEST_COUNTRY_NAME, cuenta(1)
DESDE flight_data_2015
GRUPO POR DEST_COUNTRY_NAME
""")

val dataFrameWay =

flightData2015 .groupBy('DEST_COUNTRY_NAME) .count()

sqlWay.explain
dataFrameWay.explain

# en Python
sqlWay = chispa.sql("""
SELECCIONE DEST_COUNTRY_NAME, cuenta(1)
DESDE flight_data_2015
GRUPO POR DEST_COUNTRY_NAME
""")

dataFrameWay = datos de vuelo2015\


.groupBy("DEST_COUNTRY_NAME")
\ .count()

sqlWay.explain()
dataFrameWay.explain()

== Plano físico ==
*HashAgregate(teclas=[DEST_COUNTRY_NAME#182], funciones=[recuento(1)])
+­ Intercambio de partición hash (DEST_COUNTRY_NAME#182, 5)
+­ *HashAgregate(keys=[DEST_COUNTRY_NAME#182], functions=[parcial_count(1)])
+­ *FileScan csv [DEST_COUNTRY_NAME#182] ...
== Plano físico ==
*HashAgregate(teclas=[DEST_COUNTRY_NAME#182], funciones=[recuento(1)])
+­ Intercambio de partición hash (DEST_COUNTRY_NAME#182, 5)
+­ *HashAgregate(keys=[DEST_COUNTRY_NAME#182], functions=[parcial_count(1)])
+­ *FileScan csv [DEST_COUNTRY_NAME#182] ...
Machine Translated by Google

¡Observe que estos planes compilan exactamente el mismo plan subyacente!

Extraigamos algunas estadísticas interesantes de nuestros datos. Una cosa que debe entender es
que DataFrames (y SQL) en Spark ya tienen una gran cantidad de manipulaciones disponibles. Hay cientos de
funciones que puede usar e importar para ayudarlo a resolver sus problemas de big data más rápido. Usaremos
la función max, para establecer el número máximo de vuelos hacia y desde un lugar determinado. Esto
simplemente escanea cada valor en la columna relevante en el DataFrame y verifica si es mayor que los
valores anteriores que se han visto. Esta es una transformación, porque estamos filtrando efectivamente
a una fila. Veamos cómo se ve eso:

spark.sql("SELECCIONE max(count) from flight_data_2015").take(1)

// en Scala
importar org.apache.spark.sql.functions.max

flightData2015.select(max("recuento")).tomar(1)

# en Python
desde pyspark.sql.functions import max

flightData2015.select(max("recuento")).tomar(1)

Genial, ese es un ejemplo simple que da un resultado de 370,002. Realicemos algo un poco más complicado y
encontremos los cinco principales países de destino en los datos. Esta es nuestra primera consulta de
transformación múltiple, así que la veremos paso a paso. Comencemos con una agregación de SQL bastante
sencilla:

// en Scala val
maxSql = chispa.sql("""
SELECCIONE DEST_COUNTRY_NAME, sum(count) as destination_total
FROM flight_data_2015
GROUP BY DEST_COUNTRY_NAME
ORDER BY sum(count) DESC
LIMIT 5
""")

maxSql.show()

# en Python
maxSql = chispa.sql("""
SELECCIONE DEST_COUNTRY_NAME, sum(count) as destination_total
FROM flight_data_2015
GROUP BY DEST_COUNTRY_NAME
ORDER BY sum(count) DESC
LIMIT 5
""")

maxSql.show()
Machine Translated by Google

+­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
|DEST_COUNTRY_NAME|total_destino|
+­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
| Estados Unidos| | 411352|
Canadá| 8399|
| México| 7140|
| Reino Unido| | 2025|
Japón| 1548|
+­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+

Ahora, pasemos a la sintaxis de DataFrame que es semánticamente similar pero ligeramente diferente en
implementación y ordenamiento. Pero, como mencionamos, los planes subyacentes para ambos son
lo mismo. Ejecutemos las consultas y veamos sus resultados como una verificación de cordura:

// en escala
importar org.apache.spark.sql.functions.desc

datos de vuelo2015
.groupBy("DEST_COUNTRY_NAME")
.sum("contar")
.withColumnRenamed("suma(recuento)", "destino_total")
.sort(desc("destino_total"))
.límite(5)
.espectáculo()

# en pitón
desde pyspark.sql.functions import desc

datos de vuelo2015\
.groupBy("DEST_COUNTRY_NAME")\
.sum("contar")\
.withColumnRenamed("suma(recuento)", "destino_total")\
.sort(desc("destino_total"))\
.límite(5)\
.espectáculo()

+­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
|DEST_COUNTRY_NAME|total_destino|
+­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
| Estados Unidos| | 411352|
Canadá| 8399|
| México| 7140|
| Reino Unido| | 2025|
Japón| 1548|
+­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+

Ahora hay siete pasos que nos llevan de vuelta a los datos de origen. Puedes ver esto en el
explique el plan en esos DataFrames. La figura 2­10 muestra el conjunto de pasos que realizamos en "código".
El verdadero plan de ejecución (el visible en la explicación) diferirá del que se muestra en la Figura 2­10
por optimizaciones en la ejecución física; sin embargo, la ilustración es tan buena como una
Machine Translated by Google

punto de partida como cualquiera. Este plan de ejecución es un gráfico acíclico dirigido (DAG) de transformaciones, cada una
de las cuales da como resultado un nuevo DataFrame inmutable, en el que llamamos a una acción para generar un resultado.

Figura 2­10. Todo el flujo de transformación de DataFrame

El primer paso es leer los datos. Definimos el DataFrame anteriormente pero, como recordatorio, Spark no lo lee hasta que
se invoca una acción en ese DataFrame o una derivada del DataFrame original.

El segundo paso es nuestra agrupación; técnicamente, cuando llamamos a groupBy, terminamos con un
RelationalGroupedDataset, que es un nombre elegante para un DataFrame que tiene una agrupación especificada pero
necesita que el usuario especifique una agregación antes de que se pueda consultar más. Básicamente especificamos
que vamos a agrupar por clave (o conjunto de claves) y que ahora vamos a realizar una agregación sobre cada una de esas
claves.

Por lo tanto, el tercer paso es especificar la agregación. Usemos el método de agregación de suma.
Esto toma como entrada una expresión de columna o, simplemente, un nombre de columna. El resultado de la llamada
al método sum es un nuevo DataFrame. Verás que tiene un nuevo esquema pero que sí conoce el tipo de cada columna. Es

importante reforzar (¡otra vez!) que no se ha realizado ningún cálculo. Esta es simplemente otra transformación
que hemos expresado, y Spark simplemente puede rastrear nuestra información de tipo a través de ella.

El cuarto paso es un simple cambio de nombre. Usamos el método withColumnRenamed que toma dos argumentos, el
nombre de la columna original y el nuevo nombre de la columna. Por supuesto, esto no realiza cálculos: ¡es solo otra
transformación!

El quinto paso ordena los datos de tal manera que si tuviéramos que sacar los resultados de la parte superior del DataFrame,
tendrían los valores más grandes en la columna destination_total.

Probablemente notó que tuvimos que importar una función para hacer esto, la función desc. También podrías
Machine Translated by Google

He notado que desc no devuelve una cadena sino una Columna. En general, muchos métodos de
DataFrame aceptarán cadenas (como nombres de columna) o tipos o expresiones de columna. Las columnas
y las expresiones son en realidad exactamente lo mismo.

Por último, especificaremos un límite. Esto solo especifica que solo queremos devolver los primeros cinco
valores en nuestro DataFrame final en lugar de todos los datos.

¡El último paso es nuestra acción! Ahora, de hecho, comenzamos el proceso de recopilación de los resultados
de nuestro DataFrame, y Spark nos devolverá una lista o matriz en el idioma que estamos ejecutando. Para
reforzar todo esto, veamos el plan de explicación de la consulta anterior:

// en Scala

flightData2015 .groupBy("DEST_COUNTRY_NAME") .sum("count") .withColumnRenamed("sum(count)", "destination_total") .sort(desc("destina

# en Python

flightData2015\ .groupBy("DEST_COUNTRY_NAME")
\ .sum("count")
\ .withColumnRenamed("sum(count)", "destination_total")
\ .sort(desc("destination_total"))

\ .limit( 5)\ .explicar()

== Plan físico ==
TakeOrderedAndProject(limit=5, orderBy=[destination_total#16194L DESC], salida...
+­ *HashAgregate(keys=[DEST_COUNTRY_NAME#7323], functions=[sum(count#7325L)])
+­ Partición de hash de intercambio (DEST_COUNTRY_NAME#7323, 5)
+­ *HashAgregate(keys=[DEST_COUNTRY_NAME#7323], functions=[parcial_sum...
+­ InMemoryTableScan [DEST_COUNTRY_NAME#7323, cuenta#7325L]
+­ InMemoryRelation [DEST_COUNTRY_NAME#7323, ORIGIN_COUNTRY_NA...
+­ *Escanear csv [DEST_COUNTRY_NAME#7578,ORIGIN_COUNTRY_NAME...

Aunque este plan de explicación no coincide con nuestro “plan conceptual” exacto, todas las piezas están ahí.
Puede ver la declaración de límite así como orderBy (en la primera línea). También puede ver cómo ocurre nuestra
agregación en dos fases, en las llamadas de suma_parcial. Esto se debe a que sumar una lista de números es
conmutativo y Spark puede realizar la suma, partición por partición. Por supuesto, también podemos ver cómo
leemos en el DataFrame.

Naturalmente, no siempre necesitamos recopilar los datos. También podemos escribirlo en cualquier fuente de
datos compatible con Spark. Por ejemplo, supongamos que queremos almacenar la información en una base
de datos como PostgreSQL o escribirla en otro archivo.
Machine Translated by Google

Conclusión
Este capítulo introdujo los conceptos básicos de Apache Spark. Hablamos sobre transformaciones y
acciones, y cómo Spark ejecuta perezosamente un DAG de transformaciones para optimizar el plan de
ejecución en DataFrames. También discutimos cómo se organizan los datos en particiones y preparamos el
escenario para trabajar con transformaciones más complejas. En el Capítulo 3 , lo llevamos a un recorrido por el
vasto ecosistema de Spark y observamos algunos conceptos y herramientas más avanzados que están
disponibles en Spark, desde la transmisión hasta el aprendizaje automático.
Machine Translated by Google

Capítulo 3. Un recorrido por el conjunto de herramientas de Spark

En el Capítulo 2, presentamos los conceptos básicos de Spark, como transformaciones y acciones, en el


contexto de las API estructuradas de Spark. Estos bloques de construcción conceptuales simples son la base del
vasto ecosistema de herramientas y bibliotecas de Apache Spark (Figura 3­1). Spark se compone de estas primitivas,
las API de nivel inferior y las API estructuradas, y luego una serie de bibliotecas estándar para funcionalidad
adicional.

Figura 3­1. Conjunto de herramientas de Spark

Las bibliotecas de Spark admiten una variedad de tareas diferentes, desde análisis de gráficos y aprendizaje
automático hasta transmisión e integraciones con una gran cantidad de sistemas informáticos y de almacenamiento.
Este capítulo presenta un recorrido rápido por gran parte de lo que Spark tiene para ofrecer, incluidas algunas de
las API que aún no hemos cubierto y algunas de las bibliotecas principales. Para cada sección,
encontrará información más detallada en otras partes de este libro; nuestro propósito aquí es brindarle una
descripción general de lo que es posible.
Machine Translated by Google

Este capítulo cubre lo siguiente:

Ejecución de aplicaciones de producción con spark­submit

Conjuntos de datos: API de tipo seguro para datos estructurados

Transmisión estructurada

Aprendizaje automático y análisis avanzado

Conjuntos de datos distribuidos resistentes (RDD): las API de bajo nivel de Spark

SparkR

El ecosistema de paquetes de terceros

Una vez que haya realizado el recorrido, podrá saltar a las partes correspondientes del libro para encontrar respuestas a sus
preguntas sobre temas particulares.

Ejecución de aplicaciones de producción


Spark facilita el desarrollo y la creación de programas de big data. Spark también facilita convertir su exploración interactiva
en aplicaciones de producción con spark­submit, una herramienta de línea de comandos integrada. spark­submit
hace una cosa: le permite enviar el código de su aplicación a un clúster y ejecutarlo allí. Tras el envío, la aplicación se
ejecutará hasta que salga (completa la tarea) o encuentre un error. Puede hacer esto con todos los administradores de
clústeres de soporte de Spark, incluidos Standalone, Mesos y YARN.

spark­submit ofrece varios controles con los que puede especificar los recursos que necesita su aplicación, así como también
cómo debe ejecutarse y sus argumentos de línea de comandos.

Puede escribir aplicaciones en cualquiera de los idiomas compatibles con Spark y luego enviarlas para su ejecución. El
ejemplo más simple es ejecutar una aplicación en su máquina local. Mostraremos esto ejecutando una aplicación Scala de
muestra que viene con Spark, usando el siguiente comando en el directorio donde descargó Spark:

./bin/spark­enviar \
­­class org.apache.spark.examples.SparkPi \ ­­
master local \ ./
examples/jars/spark­examples_2.11­2.2.0.jar 10

Esta aplicación de muestra calcula los dígitos de pi a un cierto nivel de estimación. Aquí, le hemos dicho a spark­submit
que queremos ejecutar en nuestra máquina local, qué clase y qué JAR nos gustaría ejecutar, y algunos argumentos de
línea de comandos para esa clase.

También podemos ejecutar una versión Python de la aplicación usando el siguiente comando:

./bin/spark­enviar \
Machine Translated by Google

­­maestro local \ ./
ejemplos/src/main/python/pi.py 10

Al cambiar el argumento maestro de spark­submit, también podemos enviar la misma aplicación a un clúster que ejecuta
el administrador de clústeres independiente de Spark, Mesos o YARN.

spark­submit será útil para ejecutar muchos de los ejemplos que hemos incluido en este libro.
En el resto de este capítulo, veremos ejemplos de algunas API que aún no hemos visto en nuestra introducción a
Spark.

Conjuntos de datos: API estructuradas con seguridad de tipos

La primera API que describiremos es una versión de tipo seguro de la API estructurada de Spark llamada Conjuntos de
datos, para escribir código tipificado estáticamente en Java y Scala. La API de conjunto de datos no está disponible en
Python y R, porque esos lenguajes se escriben dinámicamente.

Recuerde que los DataFrames, que vimos en el capítulo anterior, son una colección distribuida de objetos de tipo
Row que pueden contener varios tipos de datos tabulares. La API de conjunto de datos brinda a los usuarios la
capacidad de asignar una clase de Java/Scala a los registros dentro de un marco de datos y manipularlos como
una colección de objetos escritos, similar a Java ArrayList o Scala Seq. Las API disponibles en los conjuntos de
datos son de tipo seguro, lo que significa que no puede ver accidentalmente los objetos en un conjunto de datos
como si fueran de otra clase que la clase que colocó inicialmente. Esto hace que los conjuntos de datos sean especialmente
atractivos para escribir aplicaciones grandes, con las que varios ingenieros de software deben interactuar a través
de interfaces bien definidas.

La clase Dataset se parametriza con el tipo de objeto que contiene: Dataset<T> en Java y Dataset[T] en Scala. Por
ejemplo, se garantizará que un Dataset[Person] contenga objetos de la clase Person. A partir de Spark 2.0, los
tipos admitidos son clases que siguen el patrón JavaBean en Java y clases de casos en Scala. Estos tipos están
restringidos porque Spark necesita poder analizar automáticamente el tipo T y crear un esquema apropiado para los
datos tabulares dentro de su conjunto de datos.

Una gran cosa acerca de los conjuntos de datos es que puede usarlos solo cuando lo necesite o quiera. Por
ejemplo, en el siguiente ejemplo, definiremos nuestro propio tipo de datos y lo manipularemos a través de funciones
arbitrarias de mapa y filtro. Una vez que hayamos realizado nuestras manipulaciones, Spark puede volver a convertirlo
automáticamente en un DataFrame, y podemos manipularlo más usando los cientos de funciones que incluye Spark.
Esto hace que sea más fácil descender a un nivel inferior, realizar una codificación segura cuando sea necesario y
pasar a SQL para un análisis más rápido. Aquí hay un pequeño ejemplo que muestra cómo puede usar funciones de
seguridad de tipos y expresiones SQL similares a DataFrame para escribir rápidamente la lógica comercial:

// en el caso
de Scala, clase Vuelo (DEST_COUNTRY_NAME:
String, ORIGIN_COUNTRY_NAME:
String, count:
BigInt) val vuelosDF = chispa.leer
Machine Translated by Google

.parquet("/data/flight­data/parquet/2010­summary.parquet/") val vuelos =


vuelosDF.as[Vuelo]

Una ventaja final es que cuando llama a recopilar o tomar un conjunto de datos, recopilará objetos del tipo adecuado en su conjunto de

datos, no filas de tramas de datos. Esto facilita la obtención de seguridad de tipo y la manipulación segura de forma distribuida y
local sin cambios de código:

// en vuelos

Scala .filter(flight_row => flight_row.ORIGIN_COUNTRY_NAME != "Canada") .map(flight_row =>


flight_row) .take(5)

vuelos .take(5) .filter(flight_row => flight_row.ORIGIN_COUNTRY_NAME != "Canada") .map(fr


=> Flight(fr.DEST_COUNTRY_NAME, fr.ORIGIN_COUNTRY_NAME, fr.count + 5))

Cubrimos los conjuntos de datos en profundidad en el Capítulo 11.

Transmisión estructurada
La transmisión estructurada es una API de alto nivel para el procesamiento de transmisión que pasó a estar lista para la producción
en Spark 2.2. Con la transmisión estructurada, puede realizar las mismas operaciones que realiza en modo por lotes con las API
estructuradas de Spark y ejecutarlas en forma de transmisión. Esto puede reducir la latencia y permitir el procesamiento incremental.
Lo mejor de la transmisión estructurada es que le permite extraer valor de forma rápida y rápida de los sistemas de transmisión
prácticamente sin cambios en el código. También facilita la conceptualización porque puede escribir su trabajo por lotes como una
forma de prototipo y luego puede convertirlo en un trabajo de transmisión. La forma en que todo esto funciona es mediante el
procesamiento incremental de esos datos.

Veamos un ejemplo simple de lo fácil que es comenzar con la transmisión estructurada.


Para esto, usaremos un conjunto de datos minoristas, uno que tiene fechas y horas específicas para que podamos usar.
Usaremos el conjunto de archivos "por día", en el que un archivo representa un día de datos.

Lo ponemos en este formato para simular que los datos se producen de manera consistente y regular mediante un proceso diferente.
Estos son datos minoristas, así que imagine que los producen las tiendas minoristas y se envían a una ubicación donde serán leídos
por nuestro trabajo de transmisión estructurada.

También vale la pena compartir una muestra de los datos para que pueda hacer referencia a cómo se ven los datos:

Número de factura, código de stock, descripción, cantidad, fecha de factura, precio unitario, ID de cliente, país
536365,85123A, SOPORTE DE LUZ EN T CON CORAZÓN COLGANTE BLANCO, 6,2010­12­01 08:26:00,2.55,17...
536365,71053,LINTERNA DE METAL BLANCO,6,2010­12­01 08:26:00,3.39,17850.0,Reino Unido...
536365,84406B,PERCHA CORAZONES CUPIDO CREMA,8,2010­12­01 08:26:00,2.75,17850...

Para fundamentar esto, primero analicemos los datos como un conjunto de datos estáticos y creemos un DataFrame para hacerlo.
Machine Translated by Google

También crearemos un esquema a partir de este conjunto de datos estáticos (hay formas de usar la inferencia
de esquemas con transmisión que trataremos en la Parte V):

// en Scala
val staticDataFrame = spark.read.format("csv")
.option("header",
"true") .option("inferSchema",
"true") .load("/data/retail­data/by­day/*.csv")

staticDataFrame.createOrReplaceTempView("retail_data") val
staticSchema = staticDataFrame.schema

# en Python
staticDataFrame = spark.read.format("csv")
\ .option("header", "true")
\ .option("inferSchema", "true")\ .load("/
data/retail­data /por­día/*.csv")

staticDataFrame.createOrReplaceTempView("retail_data")
staticSchema = staticDataFrame.schema

Debido a que estamos trabajando con datos de series de tiempo, vale la pena mencionar cómo podemos agrupar
y agregar nuestros datos. En este ejemplo, veremos las horas de venta durante las cuales un cliente determinado
(identificado por CustomerId) realiza una compra importante. Por ejemplo, agreguemos una columna de costo total
y veamos en qué días gastó más un cliente.

La función de ventana incluirá todos los datos de cada día en la agregación. Es simplemente una ventana
sobre la columna de series de tiempo en nuestros datos. Esta es una herramienta útil para manipular las marcas de
fecha y hora porque podemos especificar nuestros requisitos en una forma más humana (a través de intervalos), y
Spark los agrupará todos juntos para nosotros:

// en Scala
import org.apache.spark.sql.functions.{window, column, desc, col}

staticDataFrame .selectExpr( "CustomerId",


"(UnitPrice *
Cantidad)
as total_cost", "InvoiceDate") .groupBy( col ("IdCliente"), ventana(col("FechaFactura"), "1 día"))

.sum("coste_total") .show(5)

# en Python
desde pyspark.sql.functions import window, column, desc, col

staticDataFrame\ .selectExpr( "CustomerId",


"(UnitPrice * Cantidad) as total_cost", "InvoiceDate")\
Machine Translated by Google

.groupBy( col("IdCliente"), ventana(col("FechaFactura"), "1 día"))\


.sum("coste_total")\ .show(5)

Vale la pena mencionar que también puede ejecutar esto como código SQL, tal como vimos en el capítulo
anterior.

Aquí hay una muestra de la salida que verá:

+­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­ ­+
|IdCliente| ventana| suma(coste_total)|
+­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­ ­+
| 17450.0|[2011­09­20 00:00...| 71601.44|
...
| nulo|[2011­12­08 00:00...|31975.590000000007|
+­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­ ­+

Los valores nulos representan el hecho de que no tenemos un ID de cliente para algunas transacciones.

Esa es la versión estática de DataFrame; no debería haber grandes sorpresas allí si está familiarizado
con la sintaxis.

Debido a que probablemente esté ejecutando esto en modo local, es una buena práctica establecer la cantidad
de particiones aleatorias en algo que se ajuste mejor al modo local. Esta configuración especifica el número de
particiones que deben crearse después de una reproducción aleatoria. Por defecto, el valor es 200, pero
debido a que no hay muchos ejecutores en esta máquina, vale la pena reducirlo a 5. Hicimos esta misma
operación en el Capítulo 2, así que si no recuerdas por qué esto es importante, siéntete libre para voltear
hacia atrás para revisar.

chispa.conf.set("chispa.sql.shuffle.particiones", "5")

Ahora que hemos visto cómo funciona, ¡echemos un vistazo al código de transmisión! Notarás que muy poco
cambia realmente en el código. El mayor cambio es que usamos readStream en lugar de read, además,
notará la opción maxFilesPerTrigger, que simplemente especifica la cantidad de archivos que debemos leer a
la vez. Esto es para hacer nuestra demostración más
"transmisión", y en un escenario de producción esto probablemente se omitiría.

val streamingDataFrame =

spark.readStream .schema(staticSchema) .option("maxFilesPerTrigger", 1) .format("csv") .option("header", "true") .load("/data/retail­data/by ­día/*.csv")

# en Python
streamingDataFrame =

chispa.readStream\ .schema(staticSchema)\ .option("maxFilesPerTrigger", 1)\


Machine Translated by Google

.format("csv")
\ .option("header", "true")\ .load("/
data/retail­data/by­day/*.csv")

Ahora podemos ver si nuestro DataFrame está transmitiendo:

streamingDataFrame.isStreaming // devuelve verdadero

Configuremos la misma lógica comercial que la manipulación anterior de DataFrame. Realizaremos una suma en el proceso:

// en Scala
val compraPorClientePorHora = streamingDataFrame

.selectExpr( "CustomerId", "(UnitPrice * Cantidad)


as total_cost",

"InvoiceDate") .groupBy( $"CustomerId", window($"InvoiceDate",


"1 day")) .sum("total_cost")

# en Python
buyByCustomerPerHour =

streamingDataFrame\ .selectExpr( "CustomerId",


"(UnitPrice *
Cantidad)
as total_cost", "InvoiceDate")\ .groupBy( col("CustomerId"), window(col("InvoiceDate"), "1 día"))\
.sum("coste_total")

Esta sigue siendo una operación perezosa, por lo que necesitaremos llamar a una acción de transmisión para iniciar la ejecución
de este flujo de datos.

Las acciones de transmisión son un poco diferentes de nuestra acción estática convencional porque vamos a completar datos en
algún lugar en lugar de simplemente llamar a algo como contar (que de todos modos no tiene ningún sentido en una transmisión). La
acción que usaremos se enviará a una tabla en memoria que actualizaremos después de cada activación. En este caso, cada
activador se basa en un archivo individual (la opción de lectura que configuramos). Spark mutará los datos en la tabla en memoria de
modo que siempre tengamos el valor más alto como se especifica en nuestra agregación anterior:

// en Scala
compraPorClientePorHora.writeStream
.format("memoria") // memoria = almacenar tabla en
memoria .queryName("customer_purchases") // el nombre de la tabla en
memoria .outputMode("completo") // completo = todos los conteos deben estar en la tabla .start()

# en pitón
Machine Translated by Google

buyByCustomerPerHour.writeStream\ .format("memory")

\ .queryName("customer_purchases")
\ .outputMode("complete")
\ .start()

Cuando comenzamos la transmisión, podemos ejecutar consultas en su contra para depurar cómo se verá nuestro resultado
si tuviéramos que escribir esto en un receptor de producción:

// en Scala
chispa.sql("""
SELECCIONAR *

FROM compras_clientes
ORDEN POR `sum(total_cost)` DESC

""") .show(5)

# en Python
chispa.sql("""
SELECCIONAR *

FROM compras_clientes
ORDEN POR `sum(total_cost)` DESC
""")
\ .show(5)

Notará que la composición de nuestra tabla cambia a medida que leemos más datos. Con cada archivo, los resultados
pueden cambiar o no según los datos. Naturalmente, debido a que estamos agrupando a los clientes, esperamos ver un
aumento en las cantidades principales de compra de los clientes a lo largo del tiempo (¡y así será por un período de
tiempo!). Otra opción que puede usar es escribir los resultados en la consola:

buyByCustomerPerHour.writeStream .format("console") .queryName("customer_purchases_2") .outputMode("complete") .start()

No debe usar ninguno de estos métodos de transmisión en producción, pero son una demostración conveniente del
poder de la transmisión estructurada. Observe cómo esta ventana también se basa en la hora del evento, no en la hora en
que Spark procesa los datos. Esta fue una de las deficiencias de Spark Streaming que Structured Streaming ha
resuelto. Cubrimos la transmisión estructurada en profundidad en la Parte V.

Aprendizaje automático y análisis avanzado


Otro aspecto popular de Spark es su capacidad para realizar aprendizaje automático a gran escala con una biblioteca
integrada de algoritmos de aprendizaje automático llamada MLlib. MLlib permite el preprocesamiento, la manipulación, el
entrenamiento de modelos y la realización de predicciones a escala sobre los datos. Incluso puedes usar modelos
Machine Translated by Google

entrenado en MLlib para hacer predicciones en Strucutred Streaming. Spark proporciona una API de aprendizaje
automático sofisticada para realizar una variedad de tareas de aprendizaje automático, desde la clasificación hasta la
regresión y la agrupación en clústeres hasta el aprendizaje profundo. Para demostrar esta funcionalidad, realizaremos
un agrupamiento básico de nuestros datos usando un algoritmo estándar llamado ­means.

¿QUÉ ES K­MEANS?

­means es un algoritmo de agrupación en el que los "" centros se asignan aleatoriamente dentro de los datos.
Los puntos más cercanos a ese punto se "asignan" a una clase y se calcula el centro de los puntos
asignados. Este punto central se llama baricentro. Luego, etiquetamos los puntos más cercanos a ese centroide, a
la clase del centroide, y cambiamos el centroide al nuevo centro de ese grupo de puntos. Repetimos este
proceso para un conjunto finito de iteraciones o hasta la convergencia (nuestros puntos centrales dejan de cambiar).

Spark incluye una serie de métodos de preprocesamiento listos para usar. Para demostrar estos métodos,
comenzaremos con algunos datos sin procesar, construiremos transformaciones antes de obtener los datos en el formato
correcto, momento en el que podemos entrenar nuestro modelo y luego ofrecer predicciones:

staticDataFrame.printSchema()

raíz
|­­ NºFactura: cadena (anulable = verdadero)
|­­ StockCode: cadena (anulable = verdadero)
|­­ Descripción: cadena (anulable = verdadero)
|­­ Cantidad: entero (anulable = verdadero)
|­­ FacturaFecha: marca de tiempo (anulable = verdadero)
|­­ PrecioUnidad: doble (anulable = verdadero)
|­­ CustomerID: doble (anulable = verdadero)
|­­ País: cadena (anulable = verdadero)

Los algoritmos de aprendizaje automático en MLlib requieren que los datos se representen como valores numéricos.
Nuestros datos actuales están representados por una variedad de tipos diferentes, que incluyen marcas de tiempo,
números enteros y cadenas. Por lo tanto, necesitamos transformar estos datos en alguna representación numérica.
En este caso, usaremos varias transformaciones de DataFrame para manipular nuestros datos de fecha:

// en Scala
import org.apache.spark.sql.functions.date_format val
preppedDataFrame =

staticDataFrame .na.fill(0) .withColumn("day_of_week", date_format($"InvoiceDate",


"EEEE")) .coalesce(5 )

# en Python
desde pyspark.sql.functions import date_format, col preppedDataFrame
= staticDataFrame\ .na.fill(0)\ .withColumn("day_of_week",

date_format(col("InvoiceDate"), "EEEE"))\ .coalesce( 5)


Machine Translated by Google

También necesitaremos dividir los datos en conjuntos de entrenamiento y prueba. En este caso, vamos a hacer esto
manualmente por la fecha en que ocurrió una determinada compra; sin embargo, también podríamos usar las API de
transformación de MLlib para crear un conjunto de entrenamiento y prueba a través de divisiones de validación de trenes
o validación cruzada (estos temas se tratan detalladamente en la Parte VI):

// en Scala val
trainDataFrame =
preppedDataFrame .where("FacturaDate < '2011­07­01'")
val testDataFrame = preppedDataFrame
.where("FechaFactura >= '2011­07­01'")

# en Python
trainDataFrame = preppedDataFrame\
.where("FechaFactura < '2011­07­01'")
testDataFrame = preppedDataFrame\
.where("FechaFactura >= '2011­07­01'")

Ahora que hemos preparado los datos, dividámoslos en un conjunto de entrenamiento y prueba. Debido a que este es un
conjunto de datos de series de tiempo, lo dividiremos por una fecha arbitraria en el conjunto de datos. Aunque esta podría
no ser la división óptima para nuestro entrenamiento y prueba, para los propósitos de este ejemplo funcionará bien.
Veremos que esto divide nuestro conjunto de datos aproximadamente a la mitad:

trenDataFrame.count()
pruebaDataFrame.count()

Tenga en cuenta que estas transformaciones son transformaciones de DataFrame, que cubrimos ampliamente en la
Parte II. MLlib de Spark también proporciona una serie de transformaciones con las que podemos automatizar algunas
de nuestras transformaciones generales. Uno de esos transformadores es un StringIndexer:

// en Scala
import org.apache.spark.ml.feature.StringIndexer val indexer =
new

StringIndexer() .setInputCol("day_of_week") .setOutputCol("day_of_week_index")

# en Python
desde pyspark.ml.feature import StringIndexer indexer =
StringIndexer()\
.setInputCol("día_de_la_semana")
\ .setOutputCol("índice_de_día_de_la_semana")

Esto convertirá nuestros días de semanas en valores numéricos correspondientes. Por ejemplo, Spark podría representar
el sábado como 6 y el lunes como 1. Sin embargo, con este esquema de numeración, implícitamente afirmamos
que el sábado es mayor que el lunes (por valores numéricos puros). Esto es obviamente incorrecto. Para
solucionar esto, necesitamos usar un OneHotEncoder para codificar cada uno de estos valores como su propia
columna. Estas banderas booleanas indican si ese día de la semana es el día de la semana relevante:
Machine Translated by Google

// en Scala
import org.apache.spark.ml.feature.OneHotEncoder val
codificador = new OneHotEncoder()

.setInputCol("índice_día_de_la_semana") .setOutputCol("codificado_día_de_la_semana")

# en Python
desde pyspark.ml.feature import OneHotEncoder
codificador = OneHotEncoder()
\ .setInputCol("day_of_week_index")
\ .setOutputCol("day_of_week_encoded")

Cada uno de estos dará como resultado un conjunto de columnas que "ensamblaremos" en un vector. Todos los algoritmos de aprendizaje

automático en Spark toman como entrada un tipo Vector, que debe ser un conjunto de valores numéricos:

// en Scala
importar org.apache.spark.ml.feature.VectorAssembler

val vectorAssembler = new VectorAssembler()


.setInputCols(Array("PrecioUnitario", "Cantidad",
"día_de_la_semana_codificado")) .setOutputCol("características")

# en Python
desde pyspark.ml.feature importar VectorAssembler

vectorAssembler = VectorAssembler()
\ .setInputCols(["PrecioUnitario", "Cantidad", "día_de_la_semana_codificado"])
\ .setOutputCol("características")

Aquí tenemos tres características clave: el precio, la cantidad y el día de la semana. A continuación, configuraremos esto en una canalización para

que cualquier dato futuro que necesitemos transformar pueda pasar exactamente por el mismo

proceso:

// en Scala
importar org.apache.spark.ml.Pipeline

val transformaciónPipeline = nueva tubería ()


.setStages(Array(indexador, codificador, vectorAssembler))

# en Python
desde pyspark.ml import Pipeline

transformPipeline = Pipeline()
\ .setStages([indexador, codificador, vectorAssembler])

La preparación para la capacitación es un proceso de dos pasos. Primero debemos adaptar nuestros transformadores a este conjunto de datos.

Cubrimos esto en profundidad en la Parte VI, pero básicamente nuestro StringIndexer necesita saber cuántos valores únicos hay que indexar.

Después de que existan, la codificación es fácil pero Spark debe mirar


Machine Translated by Google

todos los valores distintos en la columna que se indexarán para almacenar esos valores más adelante:

// en Scala
val .

# en Python
canalizaciónequipada = canalizacióntransformación.fit(trainDataFrame)

Después de ajustar los datos de entrenamiento, estamos listos para tomar esa canalización ajustada y usarla para
transformar todos nuestros datos de manera consistente y repetible:

// en Scala
val transformedTraining = addedPipeline.transform(trainDataFrame)

# en Python
transformedTraining = addedPipeline.transform(trainDataFrame)

En este punto, vale la pena mencionar que podríamos haber incluido nuestro modelo de capacitación en nuestra
canalización. Elegimos no hacerlo para demostrar un caso de uso para el almacenamiento en caché de los datos. En
cambio, vamos a realizar algunos ajustes de hiperparámetros en el modelo porque no queremos repetir exactamente las
mismas transformaciones una y otra vez; específicamente, usaremos el almacenamiento en caché, una optimización que
analizamos con más detalle en la Parte IV. Esto colocará una copia del conjunto de datos transformado intermediamente
en la memoria, lo que nos permitirá acceder a él repetidamente a un costo mucho menor que ejecutar toda la
canalización nuevamente. Si tiene curiosidad por ver la diferencia que esto hace, omita esta línea y ejecute el entrenamiento
sin almacenar en caché los datos. Luego pruébalo después del almacenamiento en caché; Verás que los resultados son
significativos:

transformedTraining.cache()

Ahora tenemos un conjunto de entrenamiento; es hora de entrenar al modelo. Primero importaremos el modelo relevante
que nos gustaría usar y lo instanciaremos:

// en Scala
importar org.apache.spark.ml.clustering.KMeans val
kmeans = new

KMeans() .setK(20) .setSeed(1L)

# en Python
desde pyspark.ml.clustering import KMeans
kmeans = KMeans()

\ .setK(20)\ .setSeed(1L)

En Spark, entrenar modelos de aprendizaje automático es un proceso de dos fases. Primero, inicializamos un
modelo no entrenado y luego lo entrenamos. Siempre hay dos tipos para cada algoritmo en MLlib
Machine Translated by Google

API de marco de datos. Siguen el patrón de nomenclatura de Algorithm, para la versión no entrenada, y AlgorithmModel
para la versión entrenada. En nuestro ejemplo, esto es KMeans y luego KMeansModel.

Los estimadores en la API DataFrame de MLlib comparten aproximadamente la misma interfaz que vimos anteriormente con
nuestros transformadores de preprocesamiento como StringIndexer. Esto no debería sorprender porque simplifica el
entrenamiento de una canalización completa (que incluye el modelo). Para nuestros propósitos aquí, queremos hacer las
cosas un poco más paso a paso, por lo que decidimos no hacer esto en este ejemplo:

// en Scala val
kmModel = kmeans.fit(entrenamientotransformado)

# en Python
kmModel = kmeans.fit(entrenamiento transformado)

Después de entrenar este modelo, podemos calcular el costo según algunos méritos de éxito en nuestro conjunto de
entrenamiento. El costo resultante en este conjunto de datos es en realidad bastante alto, lo que probablemente se deba
al hecho de que no preprocesamos ni escalamos correctamente nuestros datos de entrada, que cubrimos en profundidad
en el Capítulo 25 :

kmModel.computeCost(entrenamiento transformado)

// en Scala val
transformedTest = addedPipeline.transform(testDataFrame)

# en Python
transformedTest = addedPipeline.transform(testDataFrame)

kmModel.computeCost(transformedTest)

Naturalmente, podríamos continuar mejorando este modelo, superponiendo más preprocesamiento y realizando ajustes
de hiperparámetros para asegurarnos de obtener un buen modelo. Dejamos esa discusión para la Parte VI.

API de nivel inferior


Spark incluye una serie de primitivas de nivel inferior para permitir la manipulación arbitraria de objetos de Java y Python a
través de conjuntos de datos distribuidos resistentes (RDD). Prácticamente todo en Spark está construido sobre RDD. Como
veremos en el Capítulo 4, las operaciones de DataFrame se construyen sobre RDD y se compilan hasta estas herramientas
de nivel inferior para una ejecución distribuida conveniente y extremadamente eficiente. Hay algunas cosas para las que
podría usar RDD, especialmente cuando está leyendo o manipulando datos sin procesar, pero en su mayor parte debe
ceñirse a las API estructuradas. Los RDD tienen un nivel más bajo que los DataFrames porque revelan características de
ejecución física (como particiones) a los usuarios finales.

Una cosa para la que podría usar RDD es paralelizar datos sin procesar que ha almacenado en la memoria
Machine Translated by Google

en la máquina del controlador. Por ejemplo, paralelicemos algunos números simples y creemos un DataFrame
después de hacerlo. Luego podemos convertir eso en un DataFrame para usarlo con otros
marcos de datos:

// en Scala
spark.sparkContext.parallelize(Seq(1, 2, 3)).toDF()

# en Python
desde pyspark.sql fila de importación

chispa.sparkContext.parallelize([Fila(1), Fila(2), Fila(3)]).toDF()

Los RDD están disponibles tanto en Scala como en Python. Sin embargo, no son equivalentes. Esto difiere de la API de
DataFrame (donde las características de ejecución son las mismas) debido a algunos detalles de implementación
subyacentes. Cubrimos las API de nivel inferior, incluidos los RDD en la Parte IV. Como usuarios finales, no debería necesitar
usar mucho los RDD para realizar muchas tareas, a menos que esté manteniendo un código Spark antiguo. Básicamente, no
hay instancias en Spark moderno, para las cuales debería usar RDD en lugar de las API estructuradas más allá de manipular
algunos datos sin procesar y sin estructurar muy crudos.

SparkR
SparkR es una herramienta para ejecutar R en Spark. Sigue los mismos principios que todos los demás enlaces de idioma de
Spark. Para usar SparkR, simplemente impórtelo en su entorno y ejecute su código. Todo es muy similar a la API de Python,
excepto que sigue la sintaxis de R en lugar de Python.
En su mayor parte, casi todo lo disponible en Python está disponible en SparkR:

# en la
biblioteca R
(SparkR) sparkDF <­ read.df("/data/flight­data/csv/2015­summary.csv", source
= "csv", header="true", inferSchema = "true") tomar (chispaDF, 5)

# en R
recopilar (ordenar por (sparkDF, "contar"), 20)

Los usuarios de R también pueden usar otras bibliotecas de R como el operador de tubería en magrittr para hacer
que las transformaciones de Spark sean un poco más parecidas a R. Esto puede facilitar su uso con otras bibliotecas como ggplot
para un trazado más sofisticado:

# en la
biblioteca R(magrittr)
sparkDF %>%
orderBy(desc(sparkDF$count)) %>%
groupBy("ORIGIN_COUNTRY_NAME")
%>% count() %>%
Machine Translated by Google

límite(10) %>%
cobrar()

No incluiremos ejemplos de código R como lo hacemos en Python, porque casi todos los conceptos de este
libro que se aplican a Python también se aplican a SparkR. La única diferencia será por la sintaxis. Cubrimos SparkR y
sparklyr en la Parte VII.

Ecosistema y paquetes de Spark


Una de las mejores partes de Spark es el ecosistema de paquetes y herramientas que ha creado la comunidad. Algunas de
estas herramientas incluso pasan al proyecto central Spark a medida que maduran y se vuelven ampliamente utilizadas.
Al momento de escribir este artículo, la lista de paquetes es bastante larga, con más de 300, y se agregan más con
frecuencia. Puede encontrar el índice más grande de Spark Packages en spark packages.org, donde cualquier
usuario puede publicar en este repositorio de paquetes. También hay varios otros proyectos y paquetes que puede encontrar
en la web; por ejemplo, en GitHub.

Conclusión
Esperamos que este capítulo le haya mostrado la gran variedad de formas en que puede aplicar Spark a sus propios
desafíos comerciales y técnicos. El modelo de programación simple y robusto de Spark hace que sea fácil de aplicar a una
gran cantidad de problemas, y la gran variedad de paquetes que se han creado a su alrededor, creados por cientos de
personas diferentes, son un verdadero testimonio de la capacidad de Spark para abordar una serie de problemas.
de los problemas y desafíos empresariales. A medida que crece el ecosistema y la comunidad, es probable que sigan
apareciendo más y más paquetes. ¡Esperamos ver lo que la comunidad tiene reservado!

El resto de este libro proporcionará inmersiones más profundas en las áreas de productos de la Figura 3­1.

Puede leer el resto del libro de la forma que prefiera, descubrimos que la mayoría de las personas saltan de un área a
otra a medida que escuchan la terminología o quieren aplicar Spark a ciertos problemas que enfrentan.
Machine Translated by Google

Parte II. API estructuradas: marcos de datos,


SQL y conjuntos de datos
Machine Translated by Google

Capítulo 4. Descripción general de la API estructurada

Esta parte del libro será una inmersión profunda en las API estructuradas de Spark. Las API estructuradas son una herramienta para
manipular todo tipo de datos, desde archivos de registro no estructurados hasta archivos CSV semiestructurados y archivos
Parquet altamente estructurados. Estas API se refieren a tres tipos principales de API de recopilación distribuida:

conjuntos de datos

marcos de datos

tablas y vistas SQL

Aunque son partes distintas del libro, la mayoría de las API estructuradas se aplican tanto a la computación por lotes como a la
transmisión . Esto significa que cuando trabaja con las API estructuradas, debería ser sencillo migrar de lote a transmisión (o
viceversa) con poco o ningún esfuerzo. Cubriremos la transmisión en detalle en la Parte V.

Las API estructuradas son la abstracción fundamental que utilizará para escribir la mayoría de sus flujos de datos. Hasta ahora,
en este libro, hemos adoptado un enfoque basado en tutoriales, recorriendo gran parte de lo que Spark tiene para ofrecer. Esta parte
ofrece una exploración más profunda. En este capítulo, presentaremos los conceptos fundamentales que debe comprender: las
API con tipo y sin tipo (y sus diferencias); cuál es la terminología central; y, por último, cómo Spark realmente toma los flujos de datos
de la API estructurada y los ejecuta en el clúster. Luego proporcionaremos información más específica basada en tareas para
trabajar con ciertos tipos de datos o datos

fuentes.

NOTA

Antes de continuar, repasemos los conceptos y definiciones fundamentales que cubrimos en la Parte I.
Spark es un modelo de programación distribuida en el que el usuario especifica las transformaciones.
Las transformaciones múltiples construyen un gráfico acíclico dirigido de instrucciones. Una acción inicia
el proceso de ejecución de ese gráfico de instrucciones, como un solo trabajo, al dividirlo en etapas y
tareas para ejecutar en todo el clúster. Las estructuras lógicas que manipulamos con transformaciones y
acciones son DataFrames y Datasets. Para crear un nuevo DataFrame o Dataset, llame a una
transformación. Para iniciar el cálculo o convertir a tipos de idioma nativo, llame a una acción.

Marcos de datos y conjuntos de datos

La parte I discutió DataFrames. Spark tiene dos nociones de colecciones estructuradas: DataFrames y Datasets. Abordaremos
las diferencias (matices) en breve, pero primero definamos qué representan ambas.
Machine Translated by Google

DataFrames y Datasets son colecciones (distribuidas) similares a tablas con filas y columnas bien definidas. Cada
columna debe tener el mismo número de filas que todas las demás columnas (aunque puede usar nulo para especificar
la ausencia de un valor) y cada columna tiene información de tipo que debe ser coherente para cada fila de la
colección. Para Spark, los marcos de datos y los conjuntos de datos representan planes inmutables y perezosamente
evaluados que especifican qué operaciones aplicar a los datos que residen en una ubicación para generar algún
resultado. Cuando realizamos una acción en un DataFrame, le indicamos a Spark que realice las transformaciones reales
y devuelva el resultado. Estos representan planes de cómo manipular filas y columnas para calcular el resultado
deseado por el usuario.

NOTA

Las tablas y las vistas son básicamente lo mismo que los marcos de datos. Simplemente ejecutamos SQL contra
ellos en lugar del código DataFrame. Cubrimos todo esto en el Capítulo 10, que se enfoca específicamente en Spark
SQL.

Para agregar un poco más de especificidad a estas definiciones, necesitamos hablar sobre los esquemas, que son la
forma en que define los tipos de datos que está almacenando en esta colección distribuida.

esquemas

Un esquema define los nombres de las columnas y los tipos de un DataFrame. Puede definir esquemas
manualmente o leer un esquema de una fuente de datos (a menudo llamado esquema en lectura). Los esquemas constan
de tipos, lo que significa que necesita una forma de especificar qué se encuentra dónde.

Descripción general de los tipos de chispas estructuradas

Spark es efectivamente un lenguaje de programación propio. Internamente, Spark usa un motor llamado Catalyst que
mantiene su propia información de tipo a través de la planificación y el procesamiento del trabajo. Al hacerlo, esto abre
una amplia variedad de optimizaciones de ejecución que marcan diferencias significativas. Los tipos de
Spark se asignan directamente a las diferentes API de lenguaje que Spark mantiene y existe una tabla de búsqueda
para cada uno de ellos en Scala, Java, Python, SQL y R. Incluso si usamos las API estructuradas de Spark de Python
o R, la mayoría de nuestros las manipulaciones operarán estrictamente en los tipos Spark, no en los tipos Python. Por
ejemplo, el siguiente código no realiza sumas en Scala o Python; en realidad realiza sumas puramente en Spark:

// en Scala
val df = chispa.rango(500).toDF("numero")
df.select(df.col("numero") + 10)

# en Python
df = chispa.rango(500).toDF("número")
df.select(df["número"] + 10)
Machine Translated by Google

Esta operación de suma ocurre porque Spark convertirá una expresión escrita en un lenguaje de entrada a la
representación interna de Catalyst de Spark de ese mismo tipo de información. Entonces operará sobre esa
representación interna. Abordamos por qué este es el caso momentáneamente, pero antes de que podamos, debemos
analizar los conjuntos de datos.

Marcos de datos frente a conjuntos de datos

En esencia, dentro de las API estructuradas, hay dos API más, los marcos de datos "sin tipo" y los conjuntos de datos
"con tipo". Decir que los DataFrames no están tipificados es un poco inexacto; tienen tipos, pero Spark los
mantiene por completo y solo verifica si esos tipos se alinean con los especificados en el esquema en tiempo de
ejecución. Los conjuntos de datos, por otro lado, verifican si los tipos se ajustan a la especificación en el momento de
la compilación. Los conjuntos de datos solo están disponibles para lenguajes basados en Java Virtual Machine (JVM)
(Scala y Java) y especificamos tipos con clases de casos o beans de Java.

En su mayor parte, es probable que trabaje con DataFrames. Para Spark (en Scala), los marcos de datos son
simplemente conjuntos de datos de tipo fila. El tipo "Fila" es la representación interna de Spark de su formato en
memoria optimizado para computación. Este formato permite un cómputo altamente especializado y eficiente
porque, en lugar de usar tipos de JVM, que pueden generar altos costos de recolección de elementos no utilizados y
creación de instancias de objetos, Spark puede operar en su propio formato interno sin incurrir en ninguno de esos
costos. Para Spark (en Python o R), no existe un Dataset: todo es un DataFrame y, por lo tanto, siempre
operamos en ese formato optimizado.

NOTA

El formato interno de Catalyst está bien cubierto en numerosas presentaciones de Spark. Dado que este
libro está destinado a un público más general, nos abstendremos de entrar en la implementación. Si
tiene curiosidad, hay algunas charlas excelentes de Josh Rosen y Herman van Hovell, ambos de
Databricks, sobre su trabajo en el desarrollo del motor Catalyst de Spark.

La comprensión de DataFrames, Spark Types y Schemas toma algún tiempo para digerir. Lo que necesita saber
es que cuando usa DataFrames, está aprovechando el formato interno optimizado de Spark. Este formato
aplica las mismas ganancias de eficiencia a todas las API de lenguaje de Spark. Si necesita una verificación
estricta en tiempo de compilación, lea el Capítulo 11 para obtener más información al respecto.

Pasemos a algunos conceptos más amigables y accesibles: columnas y filas.

columnas

Las columnas representan un tipo simple como un número entero o una cadena, un tipo complejo como una matriz o un
mapa, o un valor nulo. Spark realiza un seguimiento de todo este tipo de información por usted y ofrece una variedad
de formas con las que puede transformar columnas. Las columnas se analizan ampliamente en el Capítulo 5, pero en
su mayor parte puede pensar en los tipos de columnas Spark como columnas en una tabla.
Machine Translated by Google

Filas
Una fila no es más que un registro de datos. Cada registro en un DataFrame debe ser de tipo Fila, como podemos ver
cuando recolectamos los siguientes DataFrames. Podemos crear estas filas manualmente desde SQL, desde conjuntos
de datos distribuidos resistentes (RDD), desde fuentes de datos o manualmente desde cero.
Aquí, creamos uno usando un rango:

// en Scala
spark.range(2).toDF().collect()

# en Python
spark.range(2).collect()

Ambos dan como resultado una matriz de objetos Row.

Tipos de chispa
Mencionamos anteriormente que Spark tiene una gran cantidad de representaciones de tipos internos. Incluimos una
práctica tabla de referencia en las próximas páginas para que pueda consultar más fácilmente qué tipo, en su idioma
específico, se alinea con el tipo en Spark.

Antes de llegar a esas tablas, hablemos de cómo instanciamos o declaramos que una columna es de cierto tipo.

Para trabajar con los tipos correctos de Scala, utilice lo siguiente:

importar org.apache.spark.sql.types._ val b


= ByteType

Para trabajar con los tipos de Java correctos, debe usar los métodos de fábrica en el siguiente paquete:

importar org.apache.spark.sql.types.DataTypes; Tipo


de byte x = Tipos de datos. Tipo de byte;

Los tipos de Python a veces tienen ciertos requisitos, que puede ver enumerados en la Tabla 4­1, al igual que Scala
y Java, que puede ver enumerados en las Tablas 4­2 y 4­3, respectivamente. Para trabajar con los tipos de Python
correctos, utilice lo siguiente:

desde pyspark.sql.types import * b =


ByteType()

Las siguientes tablas proporcionan la información de tipo detallada para cada uno de los enlaces de idioma de
Spark.

Tabla 4­1. referencia de tipo Python


Machine Translated by Google

API para acceder o


Tipo de datos Tipo de valor en Python
crear un tipo de datos

int o largo. Nota: los números se convertirán a 1 byte con signo


tipo de byte números enteros en tiempo de ejecución. Asegúrese de que los números estén dentro Tipo de byte ()
el rango de –128 a 127.

int o largo. Nota: Los números se convertirán a 2 bytes con signo


Tipo corto números enteros en tiempo de ejecución. Asegúrese de que los números estén dentro tipocorto()
el rango de –32768 a 32767.

int o largo. Nota: Python tiene una definición indulgente de "entero".


TipoEntero Spark SQL rechazará los números demasiado grandes si TipoEntero()
utiliza el IntegerType(). Es una buena práctica usar LongType.

largo. Nota: los números se convertirán a 8 bytes con signo


números enteros en tiempo de ejecución. Asegúrese de que los números estén dentro
Tipo largo el rango de –9223372036854775808 a LongType()
9223372036854775807. De lo contrario, convierta los datos a
decimal.Decimal y use DecimalType.

flotar. Nota: Los números se convertirán en números de punto flotante de


tipo de flotador Tipo flotante ()
precisión simple de 4 bytes en tiempo de ejecución.

tipo doble flotar TipoDoble()

DecimalType decimal.Decimal tipo decimal()

Tipo de cadena cadena Tipo de cadena ()

BinaryType bytearray tipobinario()

Tipo booleano bool tipo booleano()

TimestampType datetime.datetime Tipo de marca de tiempo ()

Tipo de fecha fechahora.fecha Tipo de fecha ()

ArrayType(elementType,
[contiene Nulo]). Nota la
tipo de matriz lista, tupla o matriz
valor predeterminado de

contiene Nulo es Verdadero.

Tipo de mapa (tipo de clave,


tipo de valor,
[valorContieneNulo]).
Tipo de mapa dictar
Nota: El valor predeterminado de
valueContainsNull es
Verdadero.

Tipo de estructura (campos). Nota:


campos es una lista de

Tipo de estructura lista o tupla Campos de estructura. También, campos


Machine Translated by Google

Campos de estructura. También, campos

con el mismo nombre son


No permitido.

StructField(nombre,
El tipo de valor en Python del tipo de datos de este campo (por tipo de datos, [anulable])
campo de estructura
ejemplo, Int para un StructField con el tipo de datos IntegerType) Nota: El valor predeterminado de
anulable es True.

Tabla 4­2. Referencia de tipo Scala

Tipo de datos Tipo de valor en Scala API para acceder o crear un tipo de datos

tipo de byte Byte tipo de byte

Tipo corto Corto Tipo corto

TipoEntero En t TipoEntero

Tipo largo Largo Tipo largo

tipo de flotador Flotar tipo de flotador

tipo doble Doble tipo doble

DecimalType java.math.BigDecimal tipo decimal

Tipo de cadena Cadena Tipo de cadena

BinaryType Matriz[Byte] BinaryType

Tipo booleano Booleano tipo booleano

TimestampType java.sql.Timestamp Tipo de marca de tiempo

Tipo de fecha java.sql.Fecha Tipo de fecha

ArrayType(elementType, [containsNull]).
tipo de matriz scala.colección.Seq Nota: El valor predeterminado de containsNull es
verdadero.

MapType(keyType, valueType,
Tipo de mapa scala.colección.Mapa [valorContieneNulo]). Nota: El valor predeterminado
el valor de valueContainsNull es verdadero.

Tipo de estructura (campos). Nota: los campos son un

Tipo de estructura org.apache.spark.sql.Row Matriz de StructFields. Además, los campos con


el mismo nombre no están permitidos.

El tipo de valor en Scala del tipo de datos de


StructField(nombre, tipo de datos, [anulable]).
campo de estructura este campo (por ejemplo, Int para un StructField
Nota: El valor predeterminado de anulable es verdadero.
con el tipo de datos IntegerType)
Machine Translated by Google

Tabla 4­3. Referencia de tipo Java

Tipo de datos Tipo de valor en Java API para acceder o crear un tipo de datos

tipo de byte byte o byte Tipos de datos.ByteType

Tipo corto corto o corto Tipos de datos. Tipo corto

TipoEntero int o entero DataTypes.IntegerType

Tipo largo largo o largo Tipos de datos. Tipo largo

tipo de flotador flotar o flotar Tipos de datos.Tipo flotante

tipo doble doble o doble Tipos de datos.DoubleType

Tipos de datos.createDecimalType()
DecimalType java.math.BigDecimal
DataTypes.createDecimalType(precisión, escala).

Tipo de cadena Cadena Tipos de datos.StringType

BinaryType byte[] Tipos de datos.BinaryType

BooleanType booleano o booleano Tipos de datos.BooleanType

TimestampType java.sql.Timestamp Tipos de datos.TimestampType

Tipo de fecha java.sql.Fecha Tipos de datos. Tipo de fecha

DataTypes.createArrayType(elementType). Nota:
El valor de containsNull será verdadero
tipo de matriz java.util.Lista
TiposDeDatos.createArrayType(elementType,
contiene Nulo).

DataTypes.createMapType(keyType, valueType).
Nota: El valor de valueContainsNull será verdadero.
Tipo de mapa java.util.mapa
DataTypes.createMapType(keyType, valueType,
valueContainsNull)

DataTypes.createStructType(campos). Nota: los campos son un


Tipo de estructura org.apache.spark.sql.Row Lista o matriz de StructFields. Además, dos campos
con el mismo nombre no están permitidos.

El tipo de valor en Java de los datos.


tipo de este campo (por ejemplo, int DataTypes.createStructField(nombre, tipo de datos,
campo de estructura
para un StructField con el tipo de datos anulable)
TipoEntero)

Vale la pena tener en cuenta que los tipos pueden cambiar con el tiempo a medida que Spark SQL continúa
grow, por lo que es posible que desee consultar la documentación de Spark para futuras actualizaciones. Por supuesto, todos
estos tipos son geniales, pero casi nunca trabajas con DataFrames puramente estáticos. Tú siempre
Machine Translated by Google

manipularlos y transformarlos. Por lo tanto, es importante que le demos una descripción general del proceso de
ejecución en las API estructuradas.

Descripción general de la ejecución de API estructurada

Esta sección demostrará cómo se ejecuta realmente este código en un clúster. Esto lo ayudará a comprender (y
potencialmente depurar) el proceso de escribir y ejecutar código en clústeres, así que analicemos la ejecución de
una única consulta de API estructurada desde el código de usuario hasta el código ejecutado. Aquí hay una
descripción general de los pasos:

1. Escriba DataFrame/Dataset/SQL Code.

2. Si el código es válido, Spark lo convierte en un plan lógico.

3. Spark transforma este plan lógico en un plan físico, verificando las optimizaciones a lo largo
el camino.

4. Luego, Spark ejecuta este plan físico (manipulaciones de RDD) en el clúster.

Para ejecutar código, debemos escribir código. Luego, este código se envía a Spark a través de la consola o
mediante un trabajo enviado. Luego, este código pasa a través de Catalyst Optimizer, que decide cómo se
debe ejecutar el código y establece un plan para hacerlo antes de que, finalmente, se ejecute el código y se
devuelva el resultado al usuario. La Figura 4­1 muestra el proceso.

Figura 4­1. El optimizador de catalizador

Planificación lógica
La primera fase de ejecución está destinada a tomar el código de usuario y convertirlo en un plan lógico.
La Figura 4­2 ilustra este proceso.
Machine Translated by Google

Figura 4­2. El proceso de planificación lógica de la API estructurada

Este plan lógico solo representa un conjunto de transformaciones abstractas que no se refieren a ejecutores o controladores, es
puramente para convertir el conjunto de expresiones del usuario en la versión más optimizada. Lo hace convirtiendo el
código de usuario en un plan lógico no resuelto. Este plan no está resuelto porque, aunque su código puede ser válido,
las tablas o columnas a las que hace referencia pueden existir o no. Spark usa el catálogo, un repositorio de toda la información
de tablas y tramas de datos, para resolver columnas y tablas en el analizador. El analizador puede rechazar el plan lógico no
resuelto si el nombre de la tabla o columna requerida no existe en el catálogo. Si el analizador puede resolverlo, el resultado
se pasa a través de Catalyst Optimizer, un conjunto de reglas que intentan optimizar el plan lógico empujando predicados
o selecciones hacia abajo. Los paquetes pueden extender Catalyst para incluir sus propias reglas para optimizaciones
específicas de dominio.

Planificación Física
Después de crear con éxito un plan lógico optimizado, Spark comienza el proceso de planificación física. El plan físico, a
menudo llamado plan Spark, especifica cómo se ejecutará el plan lógico en el clúster generando diferentes estrategias de
ejecución física y comparándolas a través de un modelo de costos, como se muestra en la Figura 4­3. Un ejemplo de la
comparación de costos podría ser elegir cómo realizar una combinación determinada observando los atributos físicos de una
tabla determinada (qué tan grande es la tabla o qué tan grandes son sus particiones).

Figura 4­3. El proceso de planificación física.

La planificación física da como resultado una serie de RDD y transformaciones. Este resultado es el motivo por el que es
posible que haya escuchado que se hace referencia a Spark como un compilador: toma consultas en DataFrames, Datasets y SQL.
Machine Translated by Google

y los compila en transformaciones RDD para usted.

Ejecución
Al seleccionar un plan físico, Spark ejecuta todo este código sobre RDD, la interfaz de programación
de nivel inferior de Spark (que cubrimos en la Parte III). Spark realiza más optimizaciones en
tiempo de ejecución, generando un código de bytes nativo de Java que puede eliminar tareas o etapas
completas durante la ejecución. Finalmente el resultado es devuelto al usuario.

Conclusión
En este capítulo, cubrimos las API estructuradas de Spark y cómo Spark transforma su código en lo que
se ejecutará físicamente en el clúster. En los capítulos que siguen, cubrimos conceptos básicos y cómo usar
la funcionalidad clave de las API estructuradas.
Machine Translated by Google

Capítulo 5. Operaciones Estructuradas Básicas

En el Capítulo 4, presentamos las abstracciones centrales de la API estructurada. Este capítulo se aleja de los
conceptos arquitectónicos y se acerca a las herramientas tácticas que usará para manipular DataFrames y los datos
dentro de ellos. Este capítulo se centra exclusivamente en las operaciones fundamentales de DataFrame y
evita agregaciones, funciones de ventana y uniones. Estos se discuten en capítulos posteriores.

Por definición, un DataFrame consta de una serie de registros (como filas en una tabla), que son de tipo Fila, y un número
de columnas (como columnas en una hoja de cálculo) que representan una expresión de cálculo que se puede
realizar en cada registro individual en el conjunto de datos. Los esquemas definen el nombre y el tipo de datos en cada
columna. La partición del DataFrame define el diseño del DataFrame o la distribución física del conjunto de datos
en todo el clúster. El esquema de partición define cómo se asigna. Puede configurar esto para que se base en los
valores de una determinada columna o de forma no determinista.

Vamos a crear un DataFrame con el que podamos trabajar:

// en Scala val
df = spark.read.format("json") .load("/data/
flight­data/json/2015­summary.json")

# en Python df
= spark.read.format("json").load("/data/flight­data/json/2015­summary.json")

Discutimos que un DataFame tendrá columnas y usamos un esquema para definirlas. Echemos un vistazo al esquema
en nuestro DataFrame actual:

df.imprimirEsquema()

Los esquemas unen todo, por lo que vale la pena insistir.

esquemas
Un esquema define los nombres de las columnas y los tipos de un DataFrame. Podemos dejar que una fuente de datos
defina el esquema (llamado esquema en lectura) o podemos definirlo explícitamente nosotros mismos.

ADVERTENCIA

Decidir si necesita definir un esquema antes de leer sus datos depende de su caso de uso.
Para el análisis ad hoc, el esquema en lectura generalmente funciona bien (aunque a veces puede ser un poco lento con formatos
de archivo de texto sin formato como CSV o JSON). Sin embargo, esto también puede generar problemas de precisión, como
un tipo largo configurado incorrectamente como un número entero al leer un archivo. Al usar Spark para producir Extracto,
Machine Translated by Google

Transformar y cargar (ETL), a menudo es una buena idea definir sus esquemas manualmente, especialmente
cuando se trabaja con fuentes de datos sin tipo como CSV y JSON porque la inferencia del esquema puede
variar según el tipo de datos que lea.

Comencemos con un archivo simple, que vimos en el Capítulo 4, y dejemos que la naturaleza semiestructurada de
JSON delimitado por líneas defina la estructura. Estos son datos de vuelo de las estadísticas de la Oficina de
Transporte de los Estados Unidos:

// en Scala
spark.read.format("json").load("/data/flight­data/json/2015­summary.json").schema

Scala devuelve lo siguiente:

org.apache.spark.sql.types.StructType = ...
StructType(StructField(DEST_COUNTRY_NAME,StringType,true),
StructField(ORIGIN_COUNTRY_NAME,StringType,true),
StructField(count,LongType,true))

# en Python
spark.read.format("json").load("/data/flight­data/json/2015­summary.json").schema

Python devuelve lo siguiente:

StructType(Lista(StructField(DEST_COUNTRY_NAME,StringType,true),
StructField(ORIGIN_COUNTRY_NAME,StringType,true),
StructField(recuento,TipoLargo,verdadero)))

Un esquema es un StructType compuesto por una serie de campos, StructFields, que tienen un nombre, un tipo,
un indicador booleano que especifica si esa columna puede contener valores faltantes o nulos y, finalmente, los
usuarios pueden especificar opcionalmente metadatos asociados con esa columna. . Los metadatos son una forma de
almacenar información sobre esta columna (Spark los usa en su biblioteca de aprendizaje automático).

Los esquemas pueden contener otros StructTypes (tipos complejos de Spark). Veremos esto en el Capítulo 6 cuando
discutamos el trabajo con tipos complejos. Si los tipos en los datos (en tiempo de ejecución) no coinciden con el
esquema, Spark generará un error. El siguiente ejemplo muestra cómo crear y aplicar un esquema específico
en un DataFrame.

// en Scala
importar org.apache.spark.sql.types.{StructField, StructType, StringType, LongType} importar
org.apache.spark.sql.types.Metadata

val myManualSchema = StructType(Array(


StructField("DEST_COUNTRY_NAME", StringType, verdadero),
StructField("ORIGIN_COUNTRY_NAME", StringType, verdadero),
StructField("recuento", LongType, falso,
Metadata.fromJson("{\"hola\":\"mundo\"}"))
))
Machine Translated by Google

val df = chispa.read.format("json").schema(myManualSchema) .load("/


data/flight­data/json/2015­summary.json")

He aquí cómo hacer lo mismo en Python:

# en Python
desde pyspark.sql.types import StructField, StructType, StringType, LongType

myManualSchema =
StructType([ StructField("DEST_COUNTRY_NAME", StringType(),
True), StructField("ORIGIN_COUNTRY_NAME", StringType(),
True), StructField("count", LongType(), False, metadata={"hola": "mundo"}) ]) df =

chispa.read.format("json").schema(myManualSchema)\ .load("/
data/flight­data/json/2015­summary.json")

Como se discutió en el Capítulo 4, no podemos simplemente establecer tipos a través de los tipos por idioma porque Spark
mantiene su propia información de tipo. Analicemos ahora lo que definen los esquemas: columnas.

Columnas y Expresiones
Las columnas en Spark son similares a las columnas en una hoja de cálculo, marco de datos R o marco de datos pandas.
Puede seleccionar, manipular y eliminar columnas de DataFrames y estas operaciones se representan como
expresiones.

Para Spark, las columnas son construcciones lógicas que simplemente representan un valor calculado por registro por
medio de una expresión. Esto significa que para tener un valor real para una columna, necesitamos tener una fila; y para
tener una fila, necesitamos tener un DataFrame. No puede manipular una columna individual fuera del contexto de un
DataFrame; debe usar transformaciones Spark dentro de un DataFrame para modificar el contenido de una columna.

columnas
Hay muchas formas diferentes de construir y hacer referencia a las columnas, pero las dos formas más sencillas son
mediante el uso de las funciones col o column. Para usar cualquiera de estas funciones, pasa en una columna
nombre:

// en Scala
import org.apache.spark.sql.functions.{col, column}
col("someColumnName")
column("someColumnName")

# en Python
desde pyspark.sql.functions import col, column
col("someColumnName")
column("someColumnName")
Machine Translated by Google

Nos limitaremos a usar col a lo largo de este libro. Como se mencionó, esta columna puede o no existir en nuestros
DataFrames. Las columnas no se resuelven hasta que comparamos los nombres de las columnas con los que
mantenemos en el catálogo. La resolución de columnas y tablas ocurre en la fase del analizador , como se
discutió en el Capítulo 4.

NOTA

Acabamos de mencionar dos formas diferentes de referirnos a las columnas. Scala tiene algunas características
de lenguaje únicas que permiten formas más abreviadas de referirse a las columnas. Los siguientes bits de azúcar
sintáctico realizan exactamente lo mismo, es decir, crear una columna, pero no proporcionan una mejora
en el rendimiento:

// en Scala
$"miColumna"
'miColumna

El $ nos permite designar una cadena como una cadena especial que debe hacer referencia a una expresión. La
marca de verificación (') es algo especial llamado símbolo; esta es una construcción específica de Scala para
referirse a algún identificador. Ambos realizan lo mismo y son formas abreviadas de referirse a las columnas por su nombre.
Es probable que vea todas las referencias antes mencionadas cuando lea el código Spark de diferentes personas.
Dejamos que usted use lo que sea más cómodo y fácil de mantener para usted y para aquellos con quienes trabaja.

Referencias de columna explícitas

Si necesita consultar la columna de un DataFrame específico, puede usar el método col en el DataFrame
específico. Esto puede ser útil cuando realiza una unión y necesita hacer referencia a una columna específica
en un marco de datos que podría compartir un nombre con otra columna en el marco de datos unido. Veremos
esto en el Capítulo 8. Como beneficio adicional, Spark no necesita resolver esta columna por sí mismo (durante
la fase de análisis ) porque lo hicimos para Spark:

df.col("contar")

Expresiones
Mencionamos anteriormente que las columnas son expresiones, pero ¿qué es una expresión? Una expresión es
un conjunto de transformaciones en uno o más valores en un registro en un DataFrame. Piense en ello como un
función que toma como entrada uno o más nombres de columna, los resuelve y luego aplica potencialmente
más expresiones para crear un valor único para cada registro en el conjunto de datos. Es importante destacar que
este "valor único" en realidad puede ser un tipo complejo como un mapa o una matriz. Veremos más de
los tipos complejos en el Capítulo 6.

En el caso más simple, una expresión, creada a través de la función expr, es solo una referencia de columna de
DataFrame. En el caso más simple, expr("someCol") es equivalente a col("someCol").
Machine Translated by Google

Columnas como expresiones

Las columnas proporcionan un subconjunto de funcionalidad de expresión. Si usa col() y desea realizar transformaciones
en esa columna, debe realizarlas en esa referencia de columna. Cuando se usa una expresión, la función expr en realidad
puede analizar transformaciones y referencias de columna de una cadena y, posteriormente, puede pasarse a otras
transformaciones. Veamos algunos ejemplos.

expr("someCol ­ 5") es la misma transformación que realizar col("someCol") ­ 5, o incluso expr("someCol") ­ 5. Esto se debe a
que Spark las compila en un árbol lógico que especifica el orden de las operaciones. Esto puede ser un poco confuso al
principio, pero recuerda un par de puntos clave:

Las columnas son solo expresiones.

Las columnas y las transformaciones de esas columnas se compilan en el mismo plan lógico que las
expresiones analizadas.

Vamos a fundamentar esto con un ejemplo:

(((col("algunaCol") + 5) * 200) ­ 6) < col("otraCol")

La figura 5­1 muestra una descripción general de ese árbol lógico.

Figura 5­1. un árbol lógico


Machine Translated by Google

Esto puede parecer familiar porque es un gráfico acíclico dirigido. Este gráfico está representado de
manera equivalente por el siguiente código:

// en Scala
import org.apache.spark.sql.functions.expr expr("(((algunCol
+ 5) * 200) ­ 6) <otroCol")

# en Python
desde pyspark.sql.functions import expr
expr("(((algunCol + 5) * 200) ­ 6) <otroCol")

Este es un punto muy importante para reforzar. Observe cómo la expresión anterior también es un código SQL
válido, al igual que podría poner en una instrucción SELECT. Esto se debe a que esta expresión SQL y el código
DataFrame anterior se compilan en el mismo árbol lógico subyacente antes de la ejecución. Esto significa que
puede escribir sus expresiones como código DataFrame o como expresiones SQL y obtener exactamente
las mismas características de rendimiento. Esto se discute en el Capítulo 4.

Accediendo a las columnas de un DataFrame

A veces, necesitará ver las columnas de un DataFrame, lo que puede hacer usando algo como printSchema;
sin embargo, si desea acceder a las columnas mediante programación, puede usar la propiedad de las
columnas para ver todas las columnas en un DataFrame:

chispa.read.format("json").load("/data/flight­data/json/2015­summary.json")
.columnas

Registros y Filas
En Spark, cada fila en un DataFrame es un solo registro. Spark representa este registro como un objeto de tipo
Fila. Spark manipula objetos Row usando expresiones de columna para producir valores utilizables. Los
objetos de fila representan internamente matrices de bytes. La interfaz de matriz de bytes nunca se muestra a
los usuarios porque solo usamos expresiones de columna para manipularlas.

Notará que los comandos que devuelven filas individuales al controlador siempre devolverán uno o más tipos de
Fila cuando trabajemos con DataFrames.

NOTA

Usamos "fila" y "registro" en minúsculas indistintamente en este capítulo, con un enfoque en este último.
Una Fila en mayúscula se refiere al objeto Fila.

Veamos una fila llamando primero a nuestro DataFrame:

df.primero()
Machine Translated by Google

Creación de filas
Puede crear filas instanciando manualmente un objeto Fila con los valores que pertenecen a cada columna. Es importante
tener en cuenta que solo los marcos de datos tienen esquemas. Las filas en sí mismas no tienen esquemas. Esto significa
que si crea una Fila manualmente, debe especificar los valores en el mismo orden que el esquema del Marco de datos al
que se pueden agregar (veremos esto cuando hablemos sobre la creación de Marcos de datos):

// en Scala
import org.apache.spark.sql.Row val
myRow = Row("Hello", null, 1, false)

# en Python
desde pyspark.sql import Row
myRow = Row("Hello", None, 1, False)

Acceder a los datos en filas es igual de fácil: simplemente especifique la posición que le gustaría. En Scala o Java,
debe usar los métodos auxiliares o forzar explícitamente los valores. Sin embargo, en Python o R, el valor se convertirá
automáticamente en el tipo correcto:

// en Scala
myRow(0) // escriba
Any myRow(0).asInstanceOf[String] // String
myRow.getString(0) // String
myRow.getInt(2) // Int

# en Python
miFila[0]
miFila[2]

También puede devolver explícitamente un conjunto de datos en los objetos correspondientes de la máquina virtual de
Java (JVM) mediante las API de conjunto de datos. Esto se trata en el Capítulo 11.

Transformaciones de tramas de datos


Ahora que definimos brevemente las partes centrales de un DataFrame, pasaremos a manipular DataFrames. Cuando
se trabaja con DataFrames individuales, existen algunos objetivos fundamentales.
Estos se dividen en varias operaciones centrales, como se muestra en la Figura 5­2:

Podemos agregar filas o columnas

Podemos eliminar filas o columnas

Podemos transformar una fila en una columna (o viceversa)

Podemos cambiar el orden de las filas en función de los valores de las columnas.
Machine Translated by Google

Figura 5­2. Diferentes tipos de transformaciones.

Afortunadamente, podemos traducir todo esto en transformaciones simples, siendo las más comunes aquellas que toman
una columna, la cambian fila por fila y luego devuelven nuestros resultados.

Creación de tramas de datos


Como vimos anteriormente, podemos crear DataFrames a partir de fuentes de datos sin procesar. Esto se cubre
extensamente en el Capítulo 9; sin embargo, los usaremos ahora para crear un DataFrame de ejemplo (con fines
ilustrativos más adelante en este capítulo, también registraremos esto como una vista temporal para que podamos
consultarlo con SQL y mostrar también las transformaciones básicas en SQL):

// en Scala val
df = chispa.read.format("json") .load("/data/
flight­data/json/2015­summary.json")
df.createOrReplaceTempView("dfTable")

# en Python
df = spark.read.format("json").load("/data/flight­data/json/2015­summary.json")
df.createOrReplaceTempView("dfTable")

También podemos crear tramas de datos sobre la marcha tomando un conjunto de filas y convirtiéndolas en una
trama de datos.

// en Scala
importar org.apache.spark.sql.Row
importar org.apache.spark.sql.types.{StructField, StructType, StringType, LongType}

val myManualSchema = new StructType(Array(


nuevo StructField("algunos", StringType, verdadero),
nuevo StructField("col", StringType, verdadero),
nuevo StructField("nombres", LongType, falso)))
val myRows = Seq(Row("Hola", nulo, 1L))
Machine Translated by Google

val myRDD = spark.sparkContext.parallelize(myRows) val


myDf = spark.createDataFrame(myRDD, myManualSchema)
myDf.show()

NOTA

En Scala, también podemos aprovechar los implícitos de Spark en la consola (y si los importa en su
código JAR) ejecutando toDF en un tipo Seq. Esto no funciona bien con tipos nulos, por lo que no se
recomienda necesariamente para casos de uso de producción.

// en Scala
val myDF = Seq(("Hola", 2, 1L)).toDF("col1", "col2", "col3")

# en Python
desde pyspark.sql import Row
from pyspark.sql.types import StructField, StructType, StringType, LongType
myManualSchema =
StructType([ StructField("some", StringType(),
True), StructField("col", StringType() , True),
StructField("names", LongType(), False) ])

myRow = Row("Hello", None, 1)


myDf = spark.createDataFrame([myRow], myManualSchema)
myDf.show()

Dando una salida de:

+­­­­­+­­­­+­­­­­+
| algunos| col|nombres|
+­­­­­+­­­­+­­­­­+
|Hola|null| 1|
+­­­­­+­­­­+­­­­­+

Ahora que sabe cómo crear DataFrames, echemos un vistazo a los métodos más útiles que usará: el
método select cuando trabaja con columnas o expresiones, y el método selectExpr cuando trabaja con
expresiones en cadenas. Naturalmente, algunas transformaciones no se especifican como métodos en
columnas; por lo tanto, existe un grupo de funciones que se encuentran en el paquete
org.apache.spark.sql.functions.

Con estas tres herramientas, debería poder resolver la gran mayoría de los desafíos de transformación
que podría encontrar en DataFrames.

seleccione y seleccioneExpr
select y selectExpr le permiten hacer el equivalente de DataFrame de las consultas SQL en una tabla de
datos:
Machine Translated by Google

­­ en SQL
SELECCIONE * DESDE la tabla de tramas de datos
SELECCIONE el nombre de la columna DESDE dataFrameTable

SELECCIONE columnName * 10, otherColumn, someOtherCol as c FROM dataFrameTable

En los términos más simples posibles, puede usarlos para manipular columnas en sus DataFrames.
Veamos algunos ejemplos de DataFrames para hablar sobre algunas de las diferentes formas de abordar este
problema. La forma más fácil es usar el método de selección y pasar los nombres de las columnas como cadenas
con las que le gustaría trabajar:

// en Scala
df.select("DEST_COUNTRY_NAME").show(2)

# en Python
df.select("DEST_COUNTRY_NAME").show(2)

­­ en SQL
SELECCIONE DEST_COUNTRY_NAME DE dfTable LÍMITE 2

Dando una salida de:

+­­­­­­­­­­­­­­­­­+
|DEST_COUNTRY_NAME|
+­­­­­­­­­­­­­­­­­+
Estados Unidos|
|| Estados Unidos|
+­­­­­­­­­­­­­­­­­+

Puede seleccionar varias columnas usando el mismo estilo de consulta, solo agregue más cadenas de nombre de
columna a su llamada de método de selección:

// en Scala
df.select("DEST_COUNTRY_NAME", "ORIGIN_COUNTRY_NAME").show(2)

# en Python
df.select("DEST_COUNTRY_NAME", "ORIGIN_COUNTRY_NAME").show(2)

­­ en SQL
SELECCIONE DEST_COUNTRY_NAME, ORIGIN_COUNTRY_NAME DE dfTable LÍMITE 2

Dando una salida de:

+­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
|DEST_COUNTRY_NAME|ORIGIN_COUNTRY_NAME|
+­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
Estados Unidos| Rumania|
|| Estados Unidos| Croacia|
+­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
Machine Translated by Google

Como se discutió en "Columnas y expresiones", puede hacer referencia a las columnas de varias maneras
diferentes; todo lo que necesita tener en cuenta es que puede usarlos indistintamente:

// en Scala
import org.apache.spark.sql.functions.{expr, col, column}

df.select( df.col("DEST_COUNTRY_NAME"),
col("DEST_COUNTRY_NAME"),
column("DEST_COUNTRY_NAME"), '
DEST_COUNTRY_NAME,
$"DEST_COUNTRY_NAME",
expr("DEST_COUNTRY_NAME")) .show(2)

# en Python
desde pyspark.sql.functions import expr, col, column

df.select( expr("DEST_COUNTRY_NAME"),
col("DEST_COUNTRY_NAME"),
column("DEST_COUNTRY_NAME"))
\ .show(2)

Un error común es intentar mezclar cadenas y objetos Column. Por ejemplo, el siguiente código generará un
error de compilación:

df.select(col("DEST_COUNTRY_NAME"), "DEST_COUNTRY_NAME")

Como hemos visto hasta ahora, expr es la referencia más flexible que podemos usar. Puede referirse a una
columna simple o una manipulación de cadena de una columna. Para ilustrar, cambiemos el nombre de la
columna y luego volvamos a cambiarlo usando la palabra clave AS y luego el método de alias en la columna:

// en Scala
df.select(expr("DEST_COUNTRY_NAME COMO destino")).show(2)

# en Python
df.select(expr("DEST_COUNTRY_NAME COMO destino")).show(2)

­­ en SQL
SELECCIONE DEST_COUNTRY_NAME como destino DESDE dfTable LIMIT 2

Esto cambia el nombre de la columna a "destino". Puede manipular aún más el resultado de su expresión
como otra expresión:

// en Scala
df.select(expr("DEST_COUNTRY_NAME como destino").alias("DEST_COUNTRY_NAME"))
.mostrar(2)

# en pitón
Machine Translated by Google

df.select(expr("DEST_COUNTRY_NAME como destino").alias("DEST_COUNTRY_NAME"))\


.mostrar(2)

La operación anterior cambia el nombre de la columna a su nombre original.

Debido a que select seguido de una serie de expr es un patrón tan común, Spark tiene una abreviatura para
hacer esto de manera eficiente: selectExpr. Esta es probablemente la interfaz más conveniente para
el uso diario:

// en Scala
df.selectExpr("DEST_COUNTRY_NAME as newColumnName", "DEST_COUNTRY_NAME").show(2)

# en Python
df.selectExpr("DEST_COUNTRY_NAME as newColumnName", "DEST_COUNTRY_NAME").show(2)

Esto abre el verdadero poder de Spark. Podemos tratar selectExpr como una forma sencilla de construir
expresiones complejas que crean nuevos DataFrames. De hecho, podemos agregar cualquier instrucción
SQL no agregada válida, y mientras las columnas se resuelvan, ¡será válida! Aquí hay un ejemplo simple que
agrega una nueva columna dentro de Country a nuestro DataFrame que especifica si el destino y el origen
son iguales:

// en Scala
df.selectExpr( "*", //
incluye todas las columnas originales
"(DEST_COUNTRY_NAME = ORIGIN_COUNTRY_NAME) como dentro del país") .show(2)

# en Python
df.selectExpr( "*", #
todas las columnas originales
"(DEST_COUNTRY_NAME = ORIGIN_COUNTRY_NAME) como dentro del país")\ .show(2)

­­ en SQL
SELECCIONE *, (DEST_COUNTRY_NAME = ORIGIN_COUNTRY_NAME) como dentro del país
DESDE dfTable
LÍMITE 2

Dando una salida de:

+­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­+­­­­­+­­­­­ ­­­­­­­­+
|DEST_COUNTRY_NAME|ORIGIN_COUNTRY_NAME|recuento|dentro del país|
+­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­+­­­­­+­­­­­ ­­­­­­­­+
Estados Unidos| Rumania| 15| falso|
|| Estados Unidos| Croacia| 1| falso|
+­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­+­­­­­+­­­­­ ­­­­­­­­+

Con la expresión de selección, también podemos especificar agregaciones en todo el DataFrame tomando
Machine Translated by Google

aprovechar las funciones que tenemos. Estos se parecen a lo que hemos estado mostrando hasta ahora:

// en Scala
df.selectExpr("avg(count)", "count(distinct(DEST_COUNTRY_NAME))").show(2)

# en Python
df.selectExpr("avg(count)", "count(distinct(DEST_COUNTRY_NAME))").show(2)

­­ en SQL
SELECCIONE promedio (recuento), recuento (distinto (DEST_COUNTRY_NAME)) DESDE dfTable LIMIT 2

Dando una salida de:

+­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
| promedio(recuento)|recuento(DISTINTO DEST_COUNTRY_NAME)|
+­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
|1770.765625| 132|
+­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+

Conversión a tipos de chispa (literales)


A veces, necesitamos pasar valores explícitos a Spark que son solo un valor (en lugar de una nueva
columna). Esto podría ser un valor constante o algo con lo que tendremos que comparar más adelante. La
forma en que hacemos esto es a través de literales. Esto es básicamente una traducción del valor literal
de un lenguaje de programación dado a uno que Spark entienda. Los literales son expresiones y puedes
usarlos de la misma manera:

// en Scala
import org.apache.spark.sql.functions.lit
df.select(expr("*"), lit(1).as("One")).show(2)

# en Python
desde pyspark.sql.functions import lit
df.select(expr("*"), lit(1).alias("One")).show(2)

En SQL, los literales son solo el valor específico:

­­ en SQL
SELECCIONE *, 1 como uno DESDE dfTable LIMIT 2

Dando una salida de:

+­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­+­­­­­+­­­+
|DEST_COUNTRY_NAME|ORIGIN_COUNTRY_NAME|recuento|Uno|
+­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­+­­­­­+­­­+
Estados Unidos| Rumania| 15| 1|
|| Estados Unidos| Croacia| 1| 1|
+­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­+­­­­­+­­­+
Machine Translated by Google

Esto aparecerá cuando necesite verificar si un valor es mayor que alguna constante u otra variable creada mediante
programación.

Agregar columnas
También hay una forma más formal de agregar una nueva columna a un DataFrame, y es usando el método
withColumn en nuestro DataFrame. Por ejemplo, agreguemos una columna que solo agregue el número uno
como columna:

// en Scala
df.withColumn("numberOne", lit(1)).show(2)

# en Python
df.withColumn("numberOne", lit(1)).show(2)

­­ en SQL
SELECCIONE *, 1 como número uno DESDE dfTable LIMIT 2

Dando una salida de:

+­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­+­­­­­+­­­­­ ­­­­+
|DEST_COUNTRY_NAME|ORIGIN_COUNTRY_NAME|count|numberOne|
+­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­+­­­­­+­­­­­ ­­­­+
Estados Unidos| Rumania| 15| 1|
|| Estados Unidos| Croacia| 1| 1|
+­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­+­­­­­+­­­­­ ­­­­+

Hagamos algo un poco más interesante y convirtámoslo en una expresión real. En el siguiente ejemplo,
estableceremos una bandera booleana para cuando el país de origen sea el mismo que el país de destino:

// en Scala
df.withColumn("dentro del País", expr("ORIGIN_COUNTRY_NAME == DEST_COUNTRY_NAME"))
.mostrar(2)

# en Python
df.withColumn("dentro del país", expr("ORIGIN_COUNTRY_NAME == DEST_COUNTRY_NAME"))\
.mostrar(2)

Tenga en cuenta que la función withColumn toma dos argumentos: el nombre de la columna y la expresión que
creará el valor para esa fila dada en el DataFrame. Curiosamente, también podemos cambiar el nombre de una
columna de esta manera. La sintaxis de SQL es la misma que teníamos anteriormente, por lo que podemos
omitirla en este ejemplo:

df.withColumn("Destino", expr("DEST_COUNTRY_NAME")).columnas

Resultando en:
Machine Translated by Google

... DEST_COUNTRY_NAME, ORIGIN_COUNTRY_NAME, recuento, Destino

Cambio de nombre de columnas


Aunque podemos cambiar el nombre de una columna de la manera que acabamos de describir, otra alternativa es
usar el método withColumnRenamed. Esto cambiará el nombre de la columna con el nombre de la cadena en el primer
argumento a la cadena en el segundo argumento:

// en Scala
df.withColumnRenamed("DEST_COUNTRY_NAME", "dest").columns

# en Python
df.withColumnRenamed("DEST_COUNTRY_NAME", "dest").columns

... destino, ORIGIN_COUNTRY_NAME, recuento

Caracteres reservados y palabras clave


Una cosa que puede encontrar son caracteres reservados como espacios o guiones en los nombres de las
columnas. Manejar esto significa escapar de los nombres de las columnas de manera adecuada. En Spark,
hacemos esto usando caracteres de acento grave (`). Usemos withColumn, que acaba de aprender para crear una
columna con caracteres reservados. Mostraremos dos ejemplos: en el que se muestra aquí, no necesitamos
caracteres de escape, pero en el siguiente, sí:

// en Scala
importar org.apache.spark.sql.functions.expr

val dfWithLongColName = df.withColumn( "Este


nombre largo de columna",
expr("ORIGIN_COUNTRY_NAME"))

# en Python
dfWithLongColName = df.withColumn( "Este
nombre largo de columna",
expr("ORIGIN_COUNTRY_NAME"))

No necesitamos caracteres de escape aquí porque el primer argumento de withColumn es solo una cadena para el
nuevo nombre de columna. En este ejemplo, sin embargo, necesitamos usar acentos graves porque estamos
haciendo referencia a una columna en una expresión:

// en Scala
dfWithLongColName.selectExpr( "`Este
nombre largo de columna`", "`Este
nombre largo de columna` como `nueva
columna`") .show(2)

# en pitón
Machine Translated by Google

dfWithLongColName.selectExpr( "`Este
nombre largo de columna`", "`Este
nombre largo de columna` como `nueva columna`")\ .show(2)

dfWithLongColName.createOrReplaceTempView("dfTableLong")

­­ en SQL
SELECCIONE `Este nombre largo de columna`, `Este nombre largo de columna` como `nueva columna`
DESDE dfTableLong LÍMITE 2

Podemos referirnos a columnas con caracteres reservados (y no escapar de ellos) si estamos haciendo
una referencia explícita de cadena a columna, que se interpreta como un literal en lugar de una expresión. Solo
necesitamos escapar de las expresiones que usan caracteres reservados o palabras clave. Los siguientes
dos ejemplos dan como resultado el mismo DataFrame:

// en Scala
dfWithLongColName.select(col("Este nombre largo de columna")).columns

# en Python
dfWithLongColName.select(expr("`This Long Column­Name`")).columns

Sensibilidad de mayúsculas y minúsculas

Por defecto, Spark no distingue entre mayúsculas y minúsculas; sin embargo, puede hacer que Spark distinga entre mayúsculas y minúsculas

estableciendo la configuración:

­­ en SQL
establecer spark.sql.caseSensitive verdadero

Eliminación de columnas
Ahora que hemos creado esta columna, echemos un vistazo a cómo podemos eliminar columnas de
DataFrames. Probablemente ya haya notado que podemos hacer esto usando select. Sin embargo, también
hay un método dedicado llamado drop:

df.drop("ORIGIN_COUNTRY_NAME").columnas

Podemos eliminar varias columnas pasando varias columnas como argumentos:

dfWithLongColName.drop("ORIGIN_COUNTRY_NAME", "DEST_COUNTRY_NAME")

Cambiar el tipo de una columna (cast)


A veces, es posible que necesitemos convertir de un tipo a otro; por ejemplo, si tenemos un conjunto de
StringType que deberían ser números enteros. Podemos convertir columnas de un tipo a otro por
Machine Translated by Google

echando la columna de un tipo a otro. Por ejemplo, vamos a convertir nuestra columna de conteo de un
número entero a un tipo Long:

df.withColumn("recuento2", col("recuento").cast("largo"))

­­ en SQL
SELECCIONE *, emita (cuente tanto tiempo) COMO count2 DESDE dfTable

Filtrado de filas
Para filtrar filas, creamos una expresión que se evalúa como verdadera o falsa. Luego filtra las filas con una
expresión que es igual a false. La forma más común de hacer esto con DataFrames es crear una expresión
como String o construir una expresión usando un conjunto de manipulaciones de columnas. Hay
dos métodos para realizar esta operación: puede usar where o filter y ambos realizarán la misma operación
y aceptarán los mismos tipos de argumentos cuando se usen con DataFrames. Nos limitaremos a donde
debido a su familiaridad con SQL; sin embargo, el filtro también es válido.

NOTA

Al usar la API del conjunto de datos de Scala o Java, el filtro también acepta una función arbitraria que Spark aplicará
a cada registro en el conjunto de datos. Consulte el Capítulo 11 para obtener más información.

Los siguientes filtros son equivalentes y los resultados son los mismos en Scala y Python:

df.filter(col("recuento") < 2).mostrar(2)


df.where("recuento < 2").mostrar(2)

­­ en SQL
SELECCIONE * DESDE dfTable DONDE cuenta < 2 LÍMITE 2

Dando una salida de:

+­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­+­­­­­+
|DEST_COUNTRY_NAME|ORIGIN_COUNTRY_NAME|recuento|
+­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­+­­­­­+
Estados Unidos| Croacia| 1|
|| Estados Unidos| Singapur| 1|
+­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­+­­­­­+

Instintivamente, es posible que desee colocar varios filtros en la misma expresión. Aunque esto es
posible, no siempre es útil, porque Spark realiza automáticamente todas las operaciones de filtrado al mismo
tiempo, independientemente del orden de los filtros. Esto significa que si desea especificar múltiples filtros
AND, simplemente encadenelos secuencialmente y deje que Spark maneje el resto:
Machine Translated by Google

// en Scala
df.where(col("count") < 2).where(col("ORIGIN_COUNTRY_NAME") =!= "Croacia")
.mostrar(2)

# en Python
df.where(col("count") < 2).where(col("ORIGIN_COUNTRY_NAME") != "Croacia")\
.mostrar(2)

­­ en SQL
SELECT * FROM dfTable WHERE count < 2 AND ORIGIN_COUNTRY_NAME != "Croacia"
LÍMITE 2

Dando una salida de:

+­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­+­­­­­+
|DEST_COUNTRY_NAME|ORIGIN_COUNTRY_NAME|recuento|
+­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­+­­­­­+
Estados Unidos| Singapur| 1|
|| Moldavia| Estados Unidos| 1|
+­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­+­­­­­+

Obtener filas únicas


Un caso de uso muy común es extraer los valores únicos o distintos en un DataFrame. Estos valores pueden estar en una o
más columnas. La forma en que hacemos esto es usando el método distinto en un DataFrame, que nos permite
deduplicar cualquier fila que esté en ese DataFrame. Por ejemplo, obtengamos los orígenes únicos en nuestro conjunto
de datos. Esto, por supuesto, es una transformación que devolverá un nuevo DataFrame con solo filas únicas:

// en Scala
df.select("ORIGIN_COUNTRY_NAME", "DEST_COUNTRY_NAME").distinct().count()

# en Python
df.select("ORIGIN_COUNTRY_NAME", "DEST_COUNTRY_NAME").distinct().count()

­­ en SQL
SELECCIONE COUNT(DISTINCT(ORIGIN_COUNTRY_NAME, DEST_COUNTRY_NAME)) DE dfTable

Resultados en 256.

// en Scala
df.select("ORIGIN_COUNTRY_NAME").distinct().count()

# en Python
df.select("ORIGIN_COUNTRY_NAME").distinct().count()

­­ en SQL
SELECCIONE CONTEO ( ORIGIN_COUNTRY_NAME DISTINTO) DE dfTable
Machine Translated by Google

Resultados en 125.

Muestras aleatorias
A veces, es posible que solo desee probar algunos registros aleatorios de su DataFrame. Puede hacer esto utilizando el
método de muestra en un DataFrame, lo que le permite especificar una fracción de filas para extraer de un DataFrame y si
desea muestrear con o sin reemplazo:

val seed = 5
val withReplacement = false val
fraccion = 0.5
df.sample(withReplacement, fraccion, seed).count()

# en Python
seed = 5
withReplacement = Falso
fracción = 0.5
df.sample(withReplacement, fraction, seed).count()

Dando una salida de 126.

Divisiones aleatorias
Las divisiones aleatorias pueden ser útiles cuando necesita dividir su DataFrame en "divisiones" aleatorias del DataFrame
original. Esto se usa a menudo con algoritmos de aprendizaje automático para crear conjuntos de entrenamiento, validación
y prueba. En el siguiente ejemplo, dividiremos nuestro DataFrame en dos DataFrames diferentes estableciendo los
pesos por los cuales dividiremos el DataFrame (estos son los argumentos de la función). Debido a que este
método está diseñado para ser aleatorio, también especificaremos una semilla (simplemente reemplace la semilla con
un número de su elección en el bloque de código). Es importante tener en cuenta que si no especifica una
proporción para cada DataFrame que sume uno, se normalizarán para que así sea:

// en Scala
val dataFrames = df.randomSplit(Array(0.25, 0.75), seed)
dataFrames(0).count() > dataFrames(1).count() // Falso

# en Python
dataFrames = df.randomSplit([0.25, 0.75], seed)
dataFrames[0].count() > dataFrames[1].count() # Falso

Concatenación y adición de filas (unión)


Como aprendiste en la sección anterior, los DataFrames son inmutables. Esto significa que los usuarios no pueden
agregar a DataFrames porque eso lo cambiaría. Para agregar a un DataFrame, debe unir el DataFrame original junto con
el nuevo DataFrame. Esto solo concatena los dos
Machine Translated by Google

trama de datos. Para unir dos DataFrames, debe asegurarse de que tengan el mismo esquema y número de columnas;
de lo contrario, la unión fracasará.

ADVERTENCIA

Actualmente, las uniones se realizan según la ubicación, no según el esquema. Esto significa que las columnas no se
alinearán automáticamente de la forma en que crees que lo harían.

// en Scala
import org.apache.spark.sql.Row val
schema = df.schema val
newRows = Seq(
Fila("País Nuevo", "Otro País", 5L),
Fila ("Nuevo país 2", "Otro país 3", 1L)

) val parallelizedRows = spark.sparkContext.parallelize(newRows) val newDF =


spark.createDataFrame(parallelizedRows, schema)

df.union(newDF) .where("count
= 1") .where($"ORIGIN_COUNTRY_NAME" =!= "United
States") .show() // obténgalos todos y veremos nuestras nuevas filas al final

En Scala, debe usar el operador =!= para que no solo compare la expresión de columna no evaluada con una cadena,
sino con la evaluada:

# en Python
desde pyspark.sql import Row
schema = df.schema
newRows =
[ Row("Nuevo país", "Otro país", 5L), Row("Nuevo
país 2", "Otro país 3", 1L)

] parallelizedRows = chispa.sparkContext.parallelize(newRows) newDF =


chispa.createDataFrame(parallelizedRows, esquema)

# en Python
df.union(newDF)
\ .where("count = 1")
\ .where(col("ORIGIN_COUNTRY_NAME") != "Estados Unidos")
\ .show()

Dando la salida de:

+­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­+­­­­­+
|DEST_COUNTRY_NAME|ORIGIN_COUNTRY_NAME|recuento|
+­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­+­­­­­+
| Estados Unidos| Croacia| 1|
...
Machine Translated by Google

Estados Unidos| namibia| 1|


|| Nuevo País 2| Otro País 3| 1|
+­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­+­­­­­+

Como era de esperar, deberá usar esta nueva referencia de DataFrame para hacer referencia al DataFrame con
las filas recién agregadas. Una forma común de hacer esto es convertir el DataFrame en una vista o registrarlo
como una tabla para que pueda hacer referencia a él de forma más dinámica en su código.

Clasificación de filas
Cuando ordenamos los valores en un DataFrame, siempre queremos ordenar con los valores más
grandes o más pequeños en la parte superior de un DataFrame. Hay dos operaciones equivalentes para
ordenar y ordenar por que funcionan exactamente de la misma manera. Aceptan expresiones de columna y
cadenas, así como varias columnas. El valor predeterminado es ordenar en orden ascendente:

// en Scala
df.sort("count").show(5)
df.orderBy("count", "DEST_COUNTRY_NAME").show(5)
df.orderBy(col("count"), col("DEST_COUNTRY_NAME" )).mostrar(5)

# en Python
df.sort("recuento").show(5)
df.orderBy("recuento", "DEST_COUNTRY_NAME").show(5)
df.orderBy(col("recuento"), col("DEST_COUNTRY_NAME") ).mostrar(5)

Para especificar más explícitamente la dirección de clasificación, debe usar las funciones asc y desc si opera en
una columna. Estos le permiten especificar el orden en el que se debe ordenar una columna dada:

// en Scala
import org.apache.spark.sql.functions.{desc, asc}
df.orderBy(expr("count desc")).show(2)
df.orderBy(desc("count"), asc( "DEST_COUNTRY_NAME")).mostrar(2)

# en Python
desde pyspark.sql.functions import desc, asc
df.orderBy(expr("count desc")).show(2)
df.orderBy(col("count").desc(), col("DEST_COUNTRY_NAME" ).asc()).mostrar(2)

­­ en SQL
SELECCIONE * DESDE dfTable ORDEN POR recuento DESC, DEST_COUNTRY_NAME ASC LIMIT 2

Un consejo avanzado es usar asc_nulls_first, desc_nulls_first, asc_nulls_last o desc_nulls_last para


especificar dónde desea que aparezcan sus valores nulos en un DataFrame ordenado.

Para fines de optimización, a veces es recomendable ordenar dentro de cada partición antes de otro conjunto
de transformaciones. Puede usar el método sortWithinPartitions para hacer esto:
Machine Translated by Google

// en Scala
spark.read.format("json").load("/data/flight­data/json/*­
summary.json") .sortWithinPartitions("count")

# en Python
spark.read.format("json").load("/data/flight­data/json/*­summary.json")
\ .sortWithinPartitions("count")

Discutiremos esto más cuando veamos el ajuste y la optimización en la Parte III.

Límite
A menudo, es posible que desee restringir lo que extrae de un DataFrame; por ejemplo, es posible que
desee solo los diez primeros de algún DataFrame. Puedes hacer esto usando el método de límite:

// en Scala
df.limit(5).show()

# en Python
df.limit(5).show()

­­ en SQL
SELECCIONE * DESDE dfTable LÍMITE 6

// en Scala
df.orderBy(expr("count desc")).limit(6).show()

# en Python
df.orderBy(expr("count desc")).limit(6).show()

­­ en SQL
SELECCIONE * DESDE dfTable ORDEN POR recuento desc LÍMITE 6

Repartir y fusionar
Otra oportunidad de optimización importante es particionar los datos de acuerdo con algunas columnas filtradas
con frecuencia, que controlan el diseño físico de los datos en todo el clúster, incluido el esquema de
partición y la cantidad de particiones.

La partición incurrirá en una reorganización completa de los datos, independientemente de si es


necesario. Esto significa que, por lo general, solo debe volver a particionar cuando el número futuro de
particiones sea mayor que su número actual de particiones o cuando esté buscando particionar por un conjunto de columnas:

// en Scala
df.rdd.getNumPartitions // 1

# en Python
df.rdd.getNumPartitions() # 1
Machine Translated by Google

// en Scala
df.repartition(5)

# en Python
df.repartition(5)

Si sabe que va a filtrar por una determinada columna con frecuencia, puede valer la pena volver
a particionar en función de esa columna:

// en Scala
df.repartition(col("DEST_COUNTRY_NAME"))

# en Python
df.repartition(col("DEST_COUNTRY_NAME"))

Opcionalmente, también puede especificar el número de particiones que desea:

// en Scala
df.repartition(5, col("DEST_COUNTRY_NAME"))

# en Python
df.repartition(5, col("DEST_COUNTRY_NAME"))

Coalesce, por otro lado, no incurrirá en una mezcla completa e intentará combinar particiones. Esta
operación mezclará sus datos en cinco particiones según el nombre del país de destino y luego los
combinará (sin una mezcla completa):

// en Scala
df.repartition(5, col("DEST_COUNTRY_NAME")).coalesce(2)

# en Python
df.repartition(5, col("DEST_COUNTRY_NAME")).coalesce(2)

Recopilación de filas para el conductor


Como se discutió en capítulos anteriores, Spark mantiene el estado del clúster en el controlador. Hay
momentos en los que querrá recopilar algunos de sus datos en el controlador para manipularlos en su
máquina local.

Hasta ahora, no hemos definido explícitamente esta operación. Sin embargo, usamos varios métodos
diferentes para hacerlo que son todos iguales. recopilar obtiene todos los datos de todo el DataFrame,
toma selecciona las primeras N filas y muestra imprime una cantidad de filas muy bien.

// en Scala
val collectDF = df.limit(10)
collectDF.take(5) // toma funciona con un conteo de enteros
collectDF.show() // esto lo imprime muy bien
Machine Translated by Google

recogerDF.mostrar(5, falso)
recogerDF.recoger()

# en Python
collectDF = df.limit(10)
collectDF.take(5) # take funciona con un conteo de enteros
collectDF.show() # esto lo imprime muy bien collectDF.show(5,
False) collectDF.collect()

Hay una forma adicional de recopilar filas en el controlador para iterar sobre todo el conjunto de datos. El método
toLocalIterator recopila particiones para el controlador como un iterador. Este método le permite iterar sobre todo
el conjunto de datos partición por partición de manera serial:

recogerDF.toLocalIterator()

ADVERTENCIA

¡Cualquier recopilación de datos para el conductor puede ser una operación muy costosa! Si tiene un conjunto
de datos grande y llama por cobrar, puede bloquear el controlador. Si usa toLocalIterator y tiene particiones
muy grandes, puede bloquear fácilmente el nodo del controlador y perder el estado de su aplicación. Esto
también es costoso porque podemos operar uno por uno, en lugar de ejecutar el cálculo en paralelo.

Conclusión
Este capítulo cubrió las operaciones básicas en DataFrames. Aprendió los conceptos y las herramientas simples que
necesitará para tener éxito con Spark DataFrames. El Capítulo 6 cubre con mucho más detalle todas las diferentes
formas en que puede manipular los datos en esos DataFrames.
Machine Translated by Google

Capítulo 6. Trabajar con diferentes tipos


de datos

El Capítulo 5 presentó conceptos básicos y abstracciones de DataFrame. Este capítulo cubre la creación de expresiones, que son el pan y la

mantequilla de las operaciones estructuradas de Spark. También revisamos el trabajo con una variedad de diferentes tipos de datos,

incluidos los siguientes:

Booleanos

Números

Instrumentos de cuerda

Fechas y marcas de tiempo

manejo nulo

tipos complejos

Funciones definidas por el usuario

Dónde buscar las API


Antes de comenzar, vale la pena explicar dónde usted, como usuario, debe buscar transformaciones.

Spark es un proyecto en crecimiento, y cualquier libro (incluido este) es una instantánea en el tiempo. Una de nuestras prioridades en este

libro es enseñar dónde, a partir de este escrito, debe buscar funciones para transformar sus datos. Los siguientes son los lugares clave para

buscar:

Métodos de marco de datos (conjunto de datos)

En realidad, esto es un poco complicado porque un DataFrame es solo un conjunto de datos de tipos de fila, por lo que en realidad

terminará mirando los métodos del conjunto de datos, que están disponibles en este enlace.

Los submódulos de conjuntos de datos como DataFrameStatFunctions y DataFrameNaFunctions tienen más métodos que resuelven conjuntos

específicos de problemas. DataFrameStatFunctions, por ejemplo, contiene una variedad de funciones relacionadas estadísticamente, mientras

que DataFrameNaFunctions se refiere a funciones que son relevantes cuando se trabaja con datos nulos.

Métodos de columna

Estos se introdujeron en su mayor parte en el Capítulo 5. Contienen una variedad de métodos generales relacionados con columnas como
alias o contains. Puede encontrar la referencia de la API para los métodos de columna aquí.

org.apache.spark.sql.functions contiene una variedad de funciones para una variedad de datos diferentes
Machine Translated by Google

tipos A menudo, verá el paquete completo importado porque se usan con mucha frecuencia. Puede
encuentre funciones SQL y DataFrame aquí.

Ahora bien, esto puede parecer un poco abrumador, pero no tenga miedo, la mayoría de estas funciones son
que encontrará en SQL y sistemas analíticos. Todas estas herramientas existen para lograr un propósito,
para transformar filas de datos en un formato o estructura a otro. Esto podría crear más filas o
reducir el número de filas disponibles. Para comenzar, leamos en el DataFrame que usaremos
para este análisis:

// en escala
valor df = chispa.read.format("csv")
.option("encabezado", "verdadero")
.option("inferirEsquema", "verdadero")
.load("/data/retail­data/by­day/2010­12­01.csv")
df.imprimirEsquema()
df.createOrReplaceTempView("dfTable")

# en pitón
df = chispa.leer.formato("csv")\
.option("encabezado", "verdadero")\
.opción("inferirEsquema", "verdadero")\
.load("/data/retail­data/by­day/2010­12­01.csv")
df.imprimirEsquema()
df.createOrReplaceTempView("dfTable")

Aquí está el resultado del esquema y una pequeña muestra de los datos:

raíz
|­­ NºFactura: cadena (anulable = verdadero)
|­­ StockCode: cadena (anulable = verdadero)
|­­ Descripción: cadena (anulable = verdadero)
|­­ Cantidad: entero (anulable = verdadero)
|­­ FacturaFecha: marca de tiempo (anulable = verdadero)
|­­ PrecioUnidad: doble (anulable = verdadero)
|­­ CustomerID: doble (anulable = verdadero)
|­­ País: cadena (anulable = verdadero)

+­­­­­­­­­+­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­ +­­­­­­­­­­­­­­­­­­­+­­­­...
|Número de factura|Código de existencias| Descripción|Cantidad| FacturaFecha|Unidad...
+­­­­­­­­­+­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­ +­­­­­­­­­­­­­­­­­­­+­­­­...
536365| 85123A|CABEZA COLGANTE BLANCO...| 6|2010­12­01 08:26:00| 6| ...
|| 536365| 71053| LINTERNA METAL BLANCO| 2010­12­01 08:26:00| ...
...
536367| 21755|BLOQUE CONSTRUYENDO 3|2010­12­01 08:34:00| 4| ...
|| 536367| AMOR...| 21777|CAJA DE RECETAS CON M...| 2010­12­01 08:34:00| ...
+­­­­­­­­­+­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­ +­­­­­­­­­­­­­­­­­­­+­­­­...

Conversión a tipos de chispa


Machine Translated by Google

Una cosa que nos verá hacer a lo largo de este capítulo es convertir tipos nativos en tipos Spark. Hacemos esto usando la
primera función que presentamos aquí, la función lit. Esta función convierte un tipo en otro idioma a su representación de
Spark correspondiente. Así es como podemos convertir un par de tipos diferentes de valores de Scala y Python en sus
respectivos tipos de Spark:

// en Scala
import org.apache.spark.sql.functions.lit
df.select(lit(5), lit("cinco"), lit(5.0))

# en Python
desde pyspark.sql.functions import lit
df.select(lit(5), lit("cinco"), lit(5.0))

No hay una función equivalente necesaria en SQL, por lo que podemos usar los valores directamente:

­­ en SQL
SELECT 5, "cinco", 5.0

Trabajar con booleanos


Los valores booleanos son esenciales cuando se trata de análisis de datos porque son la base de todo filtrado. Las
declaraciones booleanas constan de cuatro elementos: y, o, verdadero y falso. Usamos estas estructuras simples para
construir declaraciones lógicas que se evalúan como verdaderas o falsas. Estas declaraciones a menudo se usan como
requisitos condicionales para cuando una fila de datos debe pasar la prueba (evaluarse como verdadero) o, de lo
contrario, se filtrará.

Usemos nuestro conjunto de datos minoristas para explorar el trabajo con valores booleanos. Podemos especificar igualdad,
así como menor que o mayor que:

// en Scala
import org.apache.spark.sql.functions.col

df.where(col("FacturaNo").equalTo(536365)) .select("FacturaNo", "Descripción") .show(5, false)

ADVERTENCIA

Scala tiene una semántica particular con respecto al uso de == y ===. En Spark, si desea filtrar por
igualdad, debe usar === (igual) o =!= (no igual). También puede usar la función not y el método
equalTo.

// en Scala
import org.apache.spark.sql.functions.col
df.where(col("FacturaNo") ===
536365) .select("FacturaNo",
"Descripción") .show(5, false)
Machine Translated by Google

Python mantiene una notación más convencional:

# en Python
desde pyspark.sql.functions import col
df.where(col("FacturaNo") !=
536365)\ .select("FacturaNo", "Descripción")
\ .show(5, False)

+­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
|FacturaNo|Descripción |
+­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
|536366 |CALIENTAMANOS UNION JACK |
...
|536367 |COCINA DE LA CASA DE JUEGOS DE POPPY |
+­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+

Otra opción, y probablemente la más limpia, es especificar el predicado como una expresión en una
cadena. Esto es válido para Python o Scala. Tenga en cuenta que esto también le da acceso a otra forma
de expresar "no es igual":

df.where("FacturaNro = 536365")
.mostrar(5, falso)

df.where("Número de factura <> 536365")


.mostrar(5, falso)

Mencionamos que puede especificar expresiones booleanas con varias partes cuando usa and or or. En
Spark, siempre debe encadenar y filtrar como un filtro secuencial.

La razón de esto es que incluso si las declaraciones booleanas se expresan en serie (una tras otra), Spark
aplanará todos estos filtros en una declaración y realizará el filtro al mismo tiempo, creando la declaración
y para nosotros. Aunque puede especificar sus declaraciones explícitamente usando y si lo desea, a
menudo son más fáciles de entender y leer si las especifica en serie. o las declaraciones deben
especificarse en la misma declaración:

// en Scala val
priceFilter = col("UnitPrice") > 600 val descripFilter =
col("Description").contains("POSTAGE") df.where(col("StockCode").isin("DOT")).
where(precioFiltro.o(descripFiltro))
.espectáculo()

# en Python
desde pyspark.sql.functions import instr priceFilter =
col("UnitPrice") > 600 descripFilter =
instr(df.Description, "FRANQUEO") >= 1
df.where(df.StockCode.isin("DOT") ).where(precioFiltro | descripciónFiltro).show()

­­ en SQL
SELECCIONE * DESDE dfTable DONDE StockCode en ("DOT") Y (UnitPrice > 600 O
Machine Translated by Google

instr(Descripción, "FRANQUEO") >= 1)

+­­­­­­­­­­­­­­+­­­­­­­­­+­­­­­­­­­­­­­­+­­­­­­­­+­­­­­ ­­­­­­­­­­­­­­+­­­­­­­­­+...
|Número de factura|Código de existencias| Descripción|Cantidad| Fecha de factura|Precio unitario|...
+­­­­­­­­­­­­­­+­­­­­­­­­+­­­­­­­­­­­­­­+­­­­­­­­+­­­­­ ­­­­­­­­­­­­­­+­­­­­­­­­+...
536544| PUNTO|FRANQUEO DOTCOM| 1|2010­12­01 14:32:00| 569.77|... 1|2010­12­01
|| 536592| PUNTO|FRANQUEO DOTCOM| 17:06:00| 607.49|...
+­­­­­­­­­­­­­­+­­­­­­­­­+­­­­­­­­­­­­­­+­­­­­­­­+­­­­­ ­­­­­­­­­­­­­­+­­­­­­­­­+...

Las expresiones booleanas no solo están reservadas a los filtros. Para filtrar un DataFrame, también puede
especificar una columna booleana:

// en Scala val
DOTCodeFilter = col("StockCode") === "DOT" val priceFilter =
col("UnitPrice") > 600 val descripFilter =
col("Description").contains("FRANQUEO") df.withColumn( "esCaro",
DOTCodeFilter.and(priceFilter.or(descripFilter)))

.where("esCaro") .select("PrecioUnitario", "esCaro").show(5)

# en Python
desde pyspark.sql.functions import instr DOTCodeFilter
= col("StockCode") == "DOT" priceFilter = col("UnitPrice")
> 600 descripFilter = instr(col("Description"),
"FRANQUEO") > = 1 df.withColumn("esCaro", DOTCodeFilter & (priceFilter |
descripFilter))\ .where("esCaro")\ .select("PrecioUnitario", "esCaro").show(5)

­­ en SQL
SELECCIONE PrecioUnitario, (StockCode = 'DOT' Y
(PrecioUnitario > 600 O instr(Descripción, "FRANQUEO") >= 1)) como esCaro
DESDE dfTable
DONDE (código de existencias = 'PUNTO' Y
(PrecioUnitario > 600 O instr(Descripción, "FRANQUEO") >= 1))

Observe cómo no necesitábamos especificar nuestro filtro como una expresión y cómo podíamos usar un
nombre de columna sin ningún trabajo adicional.

Si proviene de un entorno de SQL, todas estas declaraciones deberían parecerle bastante familiares.
De hecho, todos ellos pueden expresarse como una cláusula where. De hecho, a menudo es más fácil expresar
los filtros como declaraciones SQL que usar la interfaz programática DataFrame y Spark SQL nos permite
hacer esto sin pagar ninguna penalización en el rendimiento. Por ejemplo, las siguientes dos sentencias son
equivalentes:

// en Scala
import org.apache.spark.sql.functions.{expr, not, col} df.withColumn("isCaro",
not(col("UnitPrice").leq(250)))

.filter("esCaro") .select("Descripción", "PrecioUnitario").show(5)


Machine Translated by Google

df.withColumn("esCaro", expr("NO PrecioUnitario <= 250"))

.filter("esCaro") .select("Descripción", "PrecioUnitario").show(5)

Aquí está nuestra definición de estado:

# en Python
desde pyspark.sql.functions import expr
df.withColumn("isCaro", expr("NOT UnitPrice <= 250"))\
.where("esCaro")
\ .select("Descripción", "PrecioUnitario").show(5)

ADVERTENCIA

Un problema que puede surgir es si está trabajando con datos nulos al crear expresiones booleanas.
Si hay un nulo en sus datos, deberá tratar las cosas de manera un poco diferente. Así es como puede asegurarse
de realizar una prueba de equivalencia nula segura:

df.where(col("Descripción").eqNullSafe("hola")).show()

Aunque actualmente no está disponible (Spark 2.2), IS [NO] DISTINCT FROM vendrá en Spark 2.3 para hacer lo
mismo en SQL.

Trabajar con números


Cuando trabaje con big data, la segunda tarea más común que realizará después de filtrar cosas es contar cosas. En
su mayor parte, simplemente necesitamos expresar nuestro cálculo, y eso debería ser válido suponiendo que estamos
trabajando con tipos de datos numéricos.

Para fabricar un ejemplo artificial, imaginemos que descubrimos que registramos mal la cantidad en nuestro
conjunto de datos minoristas y que la cantidad real es igual a (la cantidad actual * el precio unitario) + 5. Esto
2
introducirá nuestra primera función numérica como así como la función pow que eleva una columna a la potencia expresada:

// en Scala
import org.apache.spark.sql.functions.{expr, pow} val
fabricatedQuantity = pow(col("Cantidad") * col("PrecioUnitario"), 2) + 5 df.select(expr("
CustomerId"), FabricatedQuantity.alias("realQuantity")).show(2)

# en Python
desde pyspark.sql.functions import expr, pow
fabricatedQuantity = pow(col("Cantidad") * col("UnitPrice"), 2) + 5
df.select(expr("CustomerId"), fabricatedQuantity.alias( "cantidadreal")).mostrar(2)

+­­­­­­­­­­+­­­­­­­­­­­­­­­­­­+
Machine Translated by Google

|IdCliente| cantidadreal|
+­­­­­­­­­­+­­­­­­­­­­­­­­­­­­+
| 17850.0|239.08999999999997| |
17850.0| 418.7156|
+­­­­­­­­­­+­­­­­­­­­­­­­­­­­­+

Observe que pudimos multiplicar nuestras columnas juntas porque ambas eran numéricas.
Naturalmente, también podemos sumar y restar según sea necesario. De hecho, también podemos hacer todo esto como una
expresión SQL:

// en Scala

df.selectExpr( "CustomerId", "(POWER((Cantidad * PrecioUnitario), 2.0) + 5) as realQuantity").show(2)

# en Python

df.selectExpr( "CustomerId", "(POWER((Cantidad * PrecioUnitario), 2.0) + 5) as realQuantity").show(2)

­­ en SQL
SELECT customerId, (POWER((Quantity * UnitPrice), 2.0) + 5) as realQuantity FROM
dfTable

Otra tarea numérica común es el redondeo. Si desea simplemente redondear a un número entero, a menudo puede
convertir el valor en un número entero y eso funcionará bien. Sin embargo, Spark también tiene funciones más detalladas para
realizar esto explícitamente y con cierto nivel de precisión. En el siguiente ejemplo, redondeamos a un decimal:

// en Scala
import org.apache.spark.sql.functions.{round, bround}
df.select(round(col("UnitPrice"), 1).alias("redondeado"), col("UnitPrice")) .mostrar(5)

De forma predeterminada, la función de redondeo redondea hacia arriba si se encuentra exactamente entre dos números.

Puede redondear hacia abajo usando el bround:

// en Scala
import org.apache.spark.sql.functions.lit
df.select(round(lit("2.5")), bround(lit("2.5"))).show(2)

# en Python
desde pyspark.sql.functions import iluminado, redondo, redondo

df.select(redondo(encendido("2.5")), redondo(encendido("2.5"))).mostrar(2)

­­ en SQL
SELECT ronda(2.5), ronda(2.5)
Machine Translated by Google

+­­­­­­­­­­­­­+­­­­­­­­­­­­­­+
|redondo(2.5, 0)|redondo(2.5, 0)|
+­­­­­­­­­­­­­+­­­­­­­­­­­­­­+
3.0| 2.0|
|| 3.0| 2.0|
+­­­­­­­­­­­­­+­­­­­­­­­­­­­­+

Otra tarea numérica es calcular la correlación de dos columnas. Por ejemplo, podemos ver
el coeficiente de correlación de Pearson para dos columnas para ver si normalmente se compran cosas más baratas
en mayores cantidades. Podemos hacer esto tanto a través de una función como a través del DataFrame
métodos estadísticos:

// en escala
importar org.apache.spark.sql.functions.{corr}
df.stat.corr("Cantidad", "PrecioUnitario")
df.select(corr("Cantidad", "PrecioUnitario")).show()

# en pitón
de pyspark.sql.functions import corr
df.stat.corr("Cantidad", "PrecioUnitario")
df.select(corr("Cantidad", "PrecioUnitario")).show()

­­ en SQL
SELECCIONE corr(Cantidad, PrecioUnitario) DESDE dfTable

+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
|corr(Cantidad, PrecioUnidad)|
+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
| ­0.04112314436835551|
+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+

Otra tarea común es calcular estadísticas de resumen para una columna o conjunto de columnas. Podemos
use el método describe para lograr exactamente esto. Esto tomará todas las columnas numéricas y
calcule el recuento, la media, la desviación estándar, el mínimo y el máximo. Debe usar esto principalmente para
viendo en la consola porque el esquema podría cambiar en el futuro:

// en escala
df.describe().show()

# en pitón
df.describe().show()

+­­­­­­­+­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­+­­­ ­­­­­­­­­­­­­­­+
|resumen| Cantidad| PrecioUnidad| Id. de cliente|
+­­­­­­­+­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­+­­­ ­­­­­­­­­­­­­­­+
| contar| 1968| 3108| 3108|
| media| 8.627413127413128| 4.151946589446603|15661.388719512195|
| dev estándar|26.371821677029203|15.638659854603892|1854.4496996893627|
| 12431.0|
min| ­24| 0.0|
Machine Translated by Google

| máximo| 600| 607.49| 18229.0|


+­­­­­­­+­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­+­­­ ­­­­­­­­­­­­­­­+

Si necesita estos números exactos, también puede realizar esto como una agregación importando las
funciones y aplicándolas a las columnas que necesita:

// en Scala
import org.apache.spark.sql.functions.{count, mean, stddev_pop, min, max}

# en Python
desde pyspark.sql.functions import count, mean, stddev_pop, min, max

Hay una serie de funciones estadísticas disponibles en el paquete StatFunctions (accesibles usando stat como
vemos en el bloque de código a continuación). Estos son métodos de DataFrame que puede usar para calcular
una variedad de cosas diferentes. Por ejemplo, puede calcular cuantiles exactos o aproximados de
sus datos utilizando el método approxQuantile:

// en Scala val
colName = "UnitPrice" val
quantileProbs = Array(0.5) val relError = 0.05
df.stat.approxQuantile("UnitPrice",
quantileProbs, relError) // 2.51

# en Python
colName = "UnitPrice"
quantileProbs = [0.5] relError =
0.05
df.stat.approxQuantile("UnitPrice", quantileProbs, relError) # 2.51

También puede usar esto para ver una tabulación cruzada o pares de elementos frecuentes (tenga cuidado, esta
salida será grande y se omite por este motivo):

// en Scala
df.stat.crosstab("StockCode", "Cantidad").show()

# en Python
df.stat.crosstab("StockCode", "Cantidad").show()

// en Scala
df.stat.freqItems(Seq("StockCode", "Cantidad")).show()

# en Python
df.stat.freqItems(["StockCode", "Cantidad"]).show()

Como última nota, también podemos agregar una ID única a cada fila usando la función
monotonically_increasing_id. Esta función genera un valor único para cada fila, comenzando con 0:
Machine Translated by Google

// en Scala
import org.apache.spark.sql.functions.monotonically_increasing_id
df.select(monotonically_increasing_id()).show(2)

# en Python
desde pyspark.sql.functions import monotonically_increasing_id
df.select(monotonically_increasing_id()).show(2)

Se agregan funciones con cada versión, así que consulte la documentación para obtener más métodos. Por ejemplo,
existen algunas herramientas de generación de datos aleatorios (p. ej., rand(), randn()) con las que puede generar
datos aleatoriamente; sin embargo, existen posibles problemas de determinismo al hacerlo.
(Puede encontrar discusiones sobre estos desafíos en la lista de correo de Spark). También hay una serie de tareas
más avanzadas, como filtrado de floración y algoritmos de dibujo, disponibles en el paquete de estadísticas que
mencionamos (y al que enlazamos) al principio de este capítulo. Asegúrese de buscar en la documentación de la API
para obtener más información y funciones.

Trabajar con cadenas


La manipulación de cadenas aparece en casi todos los flujos de datos y vale la pena explicar lo que puede hacer con
las cadenas. Es posible que esté manipulando archivos de registro realizando extracción o sustitución de expresiones
regulares, o verificando la existencia de cadenas simples, o haciendo que todas las cadenas estén en
mayúsculas o minúsculas.

Comencemos con la última tarea porque es la más sencilla. La función initcap pondrá en mayúscula cada palabra en
una cadena dada cuando esa palabra esté separada de otra por un espacio.

// en Scala
importar org.apache.spark.sql.functions.{initcap}
df.select(initcap(col("Descripción"))).show(2, false)

# en Python
desde pyspark.sql.functions importar initcap
df.select(initcap(col("Descripción"))).show()

­­ en SQL
SELECCIONE initcap (Descripción) DESDE dfTable

+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
|initcap(Descripción) |
+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
|Portalámparas T Corazón Colgante Blanco|
|Linterna de metal blanco |
+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+

Como se acaba de mencionar, también puede convertir cadenas en mayúsculas y minúsculas:

// en escala
Machine Translated by Google

importar org.apache.spark.sql.functions.{inferior, superior}


df.select(col("Descripción"),
inferior(col("Descripción")),
superior(inferior(col("Descripción")))).mostrar(2)

# en pitón
desde pyspark.sql.functions importar inferior, superior
df.select(col("Descripción"),
inferior(col("Descripción")),
superior(inferior(col("Descripción")))).mostrar(2)

­­ en SQL
SELECCIONE Descripción, inferior (Descripción), Superior (inferior (Descripción)) DESDE dfTable

+­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+­­­­­­­ ­­­­­­­­­­­­­­­­­­+
| Descripción| inferior (Descripción) | superior (inferior (Descripción)) |
+­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+­­­­­­­ ­­­­­­­­­­­­­­­­­­+
|CABEZA COLGANTE BLANCO...|cabeza colgante CABEZA COLGANTE BLANCO...|
blanca...| | LINTERNA METAL BLANCO| farol de metal blanco| LINTERNA METAL BLANCO|
+­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+­­­­­­­ ­­­­­­­­­­­­­­­­­­+

Otra tarea trivial es agregar o eliminar espacios alrededor de una cadena. Puedes hacer esto usando lpad,
ltrim, rpad y rtrim, recortar:

// en escala
importar org.apache.spark.sql.functions.{lit, ltrim, rtrim, rpad, lpad, trim}
df.select(
ltrim(lit(" ")).as("ltrim"), HOLA
rtrim(lit(" ")).as("rtrim"),HOLA
trim(lit(" ")).as("trim"),HOLA
lpad(lit("HOLA"), 3, " ").as("lp"),
rpad(lit("HOLA"), 10, " ").as("rp")).show(2)

# en pitón
desde pyspark.sql.functions import lit, ltrim, rtrim, rpad, lpad, trim
df.select(
HOLA
ltrim(lit(" ")).alias("ltrim"),
HOLA
rtrim(lit(" ")).alias("rtrim"),
HOLA
trim(lit(" ")).alias("trim"),
lpad(lit("HOLA"), 3, " ").alias("lp"),
rpad(lit("HOLA"), 10, " ").alias("rp")).show(2)

­­ en SQL
SELECCIONAR

ltrim(' HOLAOO '),


rtrim(' HOLAOO '),
recortar(' HOLAOO '),
lpad('HOLAOO ', 3, ' '),
rpad('HOLAOO ', 10, ' ')
DESDE dfTable
Machine Translated by Google

+­­­­­­­­­+­­­­­­­­­+­­­­­+­­­+­­­­­­­­­­+
| ltrim| recortando| recortar| lp| rp|
+­­­­­­­­­+­­­­­­­­­+­­­­­+­­­+­­­­­­­­­­+
|HOLA HOLA|HOLA| ÉL|HOLA
|HOLA || HOLA|HOLA| ÉL|HOLA ||

+­­­­­­­­­+­­­­­­­­­+­­­­­+­­­+­­­­­­­­­­+

Tenga en cuenta que si lpad o rpad toman un número menor que la longitud de la cadena, siempre eliminará
los valores del lado derecho de la cadena.

Expresiones regulares
Probablemente una de las tareas más frecuentes sea buscar la existencia de una cadena en otra o reemplazar
todas las menciones de una cadena con otro valor. Esto a menudo se hace con una herramienta llamada
expresiones regulares que existe en muchos lenguajes de programación. Las expresiones regulares le dan al
usuario la capacidad de especificar un conjunto de reglas para usar para extraer valores de una cadena o
reemplazarlos con otros valores.

Spark aprovecha todo el poder de las expresiones regulares de Java. La sintaxis de expresiones
regulares de Java difiere ligeramente de otros lenguajes de programación, por lo que vale la pena revisarla
antes de poner algo en producción. Hay dos funciones clave en Spark que necesitará para realizar tareas de
expresiones regulares: regexp_extract y regexp_replace. Estas funciones extraen valores y reemplazan
valores, respectivamente.

Exploremos cómo usar la función regexp_replace para reemplazar nombres de colores sustitutos en nuestra
columna de descripción:

// en Scala
import org.apache.spark.sql.functions.regexp_replace val simpleColors =
Seq("negro", "blanco", "rojo", "verde", "azul") val regexString = simpleColors.map(_.
toUpperCase).mkString("|") // el | significa `O` en la sintaxis de expresión regular
df.select( regexp_replace(col("Descripción"), regexString,

"COLOR").alias("color_clean"), col("Descripción")).show(2)

# en Python
desde pyspark.sql.functions import regexp_replace regex_string =
"NEGRO|BLANCO|ROJO|VERDE|AZUL"

df.select( regexp_replace(col("Descripción"), regex_string, "COLOR").alias("color_clean") , col("Descripción")).mostrar(2)

­­ en SQL
SELECCIONAR

regexp_replace(Descripción, 'NEGRO|BLANCO|ROJO|VERDE|AZUL', 'COLOR') as color_clean,


Descripción
DESDE dfTable
Machine Translated by Google

+­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
| color_limpio| Descripción|
+­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
|CABEZA COLGANTE COLOR...|CABEZA COLGANTE
BLANCO...| | LINTERNA COLOR METAL| LINTERNA METAL BLANCO|
+­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+

Otra tarea podría ser reemplazar los caracteres dados con otros caracteres. Construir esto como una
expresión regular podría ser tedioso, por lo que Spark también proporciona la función de traducción para
reemplazar estos valores. Esto se hace a nivel de carácter y reemplazará todas las instancias de un
carácter con el carácter indexado en la cadena de reemplazo:

// en Scala
importar org.apache.spark.sql.functions.translate
df.select(translate(col("Descripción"), "LEET", "1337"), col("Descripción"))
.mostrar(2)

# en Python
desde pyspark.sql.functions import translate
df.select(translate(col("Descripción"), "LEET", "1337"),col("Descripción"))\
.mostrar(2)

­­ en SQL
SELECCIONE traducir (Descripción, 'LEET', '1337'), Descripción DE dfTable

+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­ ­­­­­­+
|traducir(Descripción, LEET, 1337)| Descripción|
+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­ ­­­­­­+
WHI73 COLGANTE H3A...|BLANCO COLGANTE...| WHI73
|| M37A1 1AN73RN| LINTERNA METAL BLANCO|
+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­ ­­­­­­+

También podemos realizar algo similar, como extraer el primer color mencionado:

// en Scala
import org.apache.spark.sql.functions.regexp_extract val regexString =
simpleColors.map(_.toUpperCase).mkString("(", "|", ")") // the | significa O en la sintaxis de expresión regular
df.select( regexp_extract(col("Descripción"), regexString,

1).alias("color_clean"), col("Descripción")).show(2)

# en Python
desde pyspark.sql.functions import regexp_extract extract_str = "(NEGRO|
BLANCO|ROJO|VERDE|AZUL)"

df.select( regexp_extract(col("Descripción"), extract_str, 1).alias("color_clean") , col("Descripción")).mostrar(2)

­­ en SQL
Machine Translated by Google

SELECCIONE regexp_extract(Descripción, '(NEGRO|BLANCO|ROJO|VERDE|AZUL)', 1),


Descripción
DESDE dfTable

+­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
| color_limpio| Descripción|
+­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
BLANCO|CABEZA COLGANTE BLANCO...|
|| BLANCO| LINTERNA METAL BLANCO|
+­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+

A veces, en lugar de extraer valores, simplemente queremos comprobar su existencia. Podemos hacer esto con
el método contains en cada columna. Esto devolverá un valor booleano que declara si el valor que especifica
está en la cadena de la columna:

// en Scala val
contieneNegro = col("Descripción").contains("NEGRO") val contieneBlanco =
col("DESCRIPCIÓN").contains("BLANCO") df.withColumn("tieneColorSimple",
contieneNegro.o(contieneBlanco) )

.where("tieneColorSimple") .select("Descripción").show(3, false)

En Python y SQL, podemos usar la función instr:

# en Python
desde pyspark.sql.functions import instr contiene Negro
= instr(col("Descripción"), "NEGRO") >= 1 contieneBlanco =
instr(col("Descripción"), "BLANCO") >= 1 df.withColumn ("tieneColorSimple",
contieneNegro | contieneBlanco)\ .where("tieneColorSimple")\ .select("Descripción").show(3,
False)

­­ en SQL
SELECCIONE Descripción DESDE dfTable
WHERE instr(Descripción, 'NEGRO') >= 1 O instr(Descripción, 'BLANCO') >= 1

+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
|Descripción |
+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
|PORTALUCES COLGANTE CORAZON BLANCO| |
FAROL DE METAL BLANCO | |CORAZON BLANCO
BOMBON LANOSO ROJO. |
+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+

Esto es trivial con solo dos valores, pero se vuelve más complicado cuando hay valores.

Analicemos esto de una manera más rigurosa y aprovechemos la capacidad de Spark para aceptar una cantidad
dinámica de argumentos. Cuando convertimos una lista de valores en un conjunto de argumentos y los pasamos
a una función, usamos una función de lenguaje llamada varargs. Usando esta característica, podemos
Machine Translated by Google

desentrañar efectivamente una matriz de longitud arbitraria y pasarla como argumentos a una función. Esto,
junto con select, nos permite crear números arbitrarios de columnas dinámicamente:

// en Scala
val colores simples = Seq("negro", "blanco", "rojo", "verde", "azul") val columnas
seleccionadas = colores simples.map(color =>
{ col("Descripción").contains(color .toUpperCase).alias(s"es_$color")
}):+expr("*") // también podría agregar este valor
df.select(selectedColumns:_*).where(col("is_white").or(col("is_red")))
.select("Descripción").show(3, falso)

+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
|Descripción |
+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
|PORTALUCES COLGANTE CORAZON BLANCO| |
FAROL DE METAL BLANCO | |CORAZON BLANCO
BOMBON LANOSO ROJO. |
+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+

También podemos hacer esto con bastante facilidad en Python. En este caso, vamos a usar una función diferente,
localizar, que devuelve la ubicación entera (ubicación basada en 1). Luego lo convertimos a un valor booleano antes
de usarlo como la misma característica básica:

# en Python
desde pyspark.sql.functions import expr, ubique
simpleColors = ["negro", "blanco", "rojo", "verde", "azul"] def
color_locator(columna, color_string):
volver a localizar(color_string.upper(), columna)\
.cast("boolean")
\ .alias("is_" + c)
selectedColumns = [color_locator(df.Description, c) for c in simpleColors]
selectedColumns.append(expr("*")) # tiene que ser Columna tipo

df.select(*columnas seleccionadas).where(expr("es_blanco O es_rojo"))\


.select("Descripción").show(3, Falso)

Esta característica simple a menudo puede ayudarlo a generar columnas o filtros booleanos mediante programación de
una manera que es fácil de entender y ampliar. Podríamos extender esto para calcular el mínimo común
denominador para un valor de entrada dado, o si un número es primo.

Trabajar con fechas y marcas de tiempo


Las fechas y las horas son un desafío constante en los lenguajes de programación y las bases de datos. Siempre es
necesario realizar un seguimiento de las zonas horarias y asegurarse de que los formatos sean correctos y válidos.
Spark hace todo lo posible para simplificar las cosas centrándose explícitamente en dos tipos de información relacionada
con el tiempo. Hay fechas, que se enfocan exclusivamente en fechas del calendario, y marcas de tiempo, que
incluyen información de fecha y hora. Spark, como vimos con nuestro conjunto de datos actual, hará todo lo posible para
Machine Translated by Google

identificar correctamente los tipos de columna, incluidas las fechas y las marcas de tiempo cuando habilitamos inferSchema.
Podemos ver que esto funcionó bastante bien con nuestro conjunto de datos actual porque pudo identificar y leer nuestro
formato de fecha sin que tuviéramos que proporcionarle alguna especificación.

Como sugerimos anteriormente, trabajar con fechas y marcas de tiempo se relaciona estrechamente con trabajar con
cadenas porque a menudo almacenamos nuestras marcas de tiempo o fechas como cadenas y las convertimos en tipos
de fecha en tiempo de ejecución. Esto es menos común cuando se trabaja con bases de datos y datos estructurados, pero
mucho más común cuando se trabaja con archivos de texto y CSV. Experimentaremos con eso en breve.

ADVERTENCIA

Desafortunadamente, hay muchas advertencias cuando se trabaja con fechas y marcas de tiempo, especialmente
cuando se trata del manejo de la zona horaria. En la versión 2.1 y anteriores, Spark analizaba según la zona
horaria de la máquina si las zonas horarias no se especifican explícitamente en el valor que está analizando.
Puede establecer una zona horaria local de sesión si es necesario configurando spark.conf.sessionLocalTimeZone
en las configuraciones de SQL. Esto debe establecerse de acuerdo con el formato Java TimeZone.

df.imprimirEsquema()

raíz
|­­ NºFactura: cadena (anulable = verdadero)
|­­ StockCode: cadena (anulable = verdadero)
|­­ Descripción: cadena (anulable = verdadero)
|­­ Cantidad: entero (anulable = verdadero)
|­­ FacturaFecha: marca de tiempo (anulable = verdadero)
|­­ PrecioUnidad: doble (anulable = verdadero)
|­­ CustomerID: doble (anulable = verdadero)
|­­ País: cadena (anulable = verdadero)

Aunque Spark leerá fechas u horas en la medida de lo posible. Sin embargo, a veces será imposible trabajar con fechas y
horas con formatos extraños. La clave para comprender las transformaciones que necesitará aplicar es
asegurarse de saber exactamente qué tipo y formato tiene en cada paso dado del camino. Otro problema común es que la
clase TimestampType de Spark solo admite precisión de segundo nivel, lo que significa que si va a trabajar con milisegundos
o microsegundos, deberá solucionar este problema potencialmente operando en ellos durante mucho tiempo. Se eliminará
cualquier precisión adicional al forzar a un TimestampType.

Spark puede ser un poco particular sobre qué formato tiene en un momento dado. Es importante ser explícito al
analizar o convertir para asegurarse de que no haya problemas al hacerlo.
Al final del día, Spark está trabajando con fechas y marcas de tiempo de Java y, por lo tanto, cumple con esos estándares.
Comencemos con lo básico y obtengamos la fecha actual y las marcas de tiempo actuales:

// en escala
Machine Translated by Google

import org.apache.spark.sql.functions.{current_date, current_timestamp} val dateDF =


spark.range(10) .withColumn("hoy",
current_date()) .withColumn("now",
current_timestamp()) dateDF.createOrReplaceTempView
("tabla de fechas")

# en Python
desde pyspark.sql.functions import fecha_actual, marca_de_hora_actual dateDF =
chispa.rango(10)\ .withColumn("hoy",
fecha_actual())\ .withColumn("ahora",
marca_de_hora_actual())
dateDF.createOrReplaceTempView(" tabla de fechas")

fechaDF.imprimirEsquema()

raíz
|­­ id: largo (anulable = falso) |­­ hoy:
fecha (anulable = falso) |­­ ahora: marca de
tiempo (anulable = falso)

Ahora que tenemos un DataFrame simple para trabajar, sumamos y restamos cinco días a partir de hoy.
Estas funciones toman una columna y luego el número de días para sumar o restar como
argumentos:

// en Scala
import org.apache.spark.sql.functions.{date_add, date_sub}
dateDF.select(date_sub(col("hoy"), 5), date_add(col("hoy"), 5)).show (1)

# en Python
desde pyspark.sql.functions import date_add, date_sub
dateDF.select(date_sub(col("hoy"), 5), date_add(col("hoy"), 5)).show(1)

­­ en SQL
SELECCIONE date_sub (hoy, 5), date_add (hoy, 5) DE dateTable

+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­+
|date_sub(hoy, 5)|date_add(hoy, 5)|
+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­+
| 2017­06­12| 2017­06­22|
+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­+

Otra tarea común es echar un vistazo a la diferencia entre dos fechas. Podemos hacer esto con la
función dateiff que devolverá el número de días entre dos fechas. La mayoría de las veces solo nos
preocupamos por los días, y debido a que la cantidad de días varía de un mes a otro, también existe
una función, months_ between, que le brinda la cantidad de meses entre dos fechas:

// en Scala
importar org.apache.spark.sql.functions.{difffecha, meses_entre, hasta_fecha}
dateDF.withColumn("week_ago", date_sub(col("hoy"), 7))
Machine Translated by Google

.select(datediff(col("week_ago"), col("hoy"))).show(1)

dateDF.select( to_date(lit("2016­01­01")).alias("inicio"),

hasta_fecha(lit("2017­05­22")).alias("fin")) .select(meses_entre(col("inicio"), col("fin"))).mostrar(1)

# en Python
desde pyspark.sql.functions importar fechadoiff, meses_entre, hasta_fecha
dateDF.withColumn("week_ago", date_sub(col("hoy"), 7))
\ .select(datediff(col("week_ago"), col( "hoy"))).mostrar(1)

dateDF.select( to_date(lit("2016­01­01")).alias("inicio"),
to_date(lit("2017­05­22")).alias("fin"))
\ .select( meses_entre(col("inicio"), col("fin"))).mostrar(1)

­­ en SQL
SELECCIONE hasta_fecha('2016­01­01'), meses_entre('2016­01­01', '2017­01­01'),
fechado('2016­01­01', '2017­01­01')
DESDE la tabla de fechas

+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
|fechadaiff(semana_ago, hoy)|
+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
| ­7|
+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+

+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
|meses_entre(inicio, fin)|
+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
| ­16.67741935|
+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+

Observe que introdujimos una nueva función: la función to_date. La función to_date le permite convertir una
cadena en una fecha, opcionalmente con un formato específico. Especificamos nuestro formato en Java
SimpleDateFormat , al que será importante hacer referencia si usa esta función:

// en Scala
import org.apache.spark.sql.functions.{to_date, lit}
spark.range(5).withColumn("date",
lit("2017­01­01")) .select(to_date(col ("fecha"))).mostrar(1)

# en Python
desde pyspark.sql.functions import to_date, lit
spark.range(5).withColumn("date", lit("2017­01­01"))
\ .select(to_date(col("date")) ).mostrar(1)

Spark no arrojará un error si no puede analizar la fecha; más bien, simplemente devolverá nulo. Esto puede
ser un poco complicado en canalizaciones más grandes porque es posible que espere obtener sus datos
en un formato y obtenerlos en otro. Para ilustrar, echemos un vistazo al formato de fecha que ha cambiado de año­
Machine Translated by Google

mes­día a año­día­mes. Spark no podrá analizar esta fecha y en su lugar devolverá un valor nulo:

dateDF.select(to_date(lit("2016­20­12")),to_date(lit("2017­12­11"))).show(1)

+­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­+
|hasta la fecha(2016­20­12)|hasta la_fecha(2017­12­11)|
+­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­+
| nulo| 2017­12­11|
+­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­+

Encontramos que esta es una situación especialmente complicada para los errores porque algunas fechas
pueden coincidir con el formato correcto, mientras que otras no. En el ejemplo anterior, observe cómo la
segunda fecha aparece como 11 de diciembre en lugar del día correcto, 12 de noviembre. Spark no arroja un error
porque no puede saber si los días están mezclados o si esa fila específica es incorrecta.

Arreglemos esta canalización, paso a paso, y propongamos una forma sólida de evitar estos problemas por completo.
El primer paso es recordar que necesitamos especificar nuestro formato de fecha de acuerdo con el estándar
Java SimpleDateFormat.

Usaremos dos funciones para arreglar esto: to_date y to_timestamp. El primero espera opcionalmente un
formato, mientras que el segundo requiere uno:

// en Scala
import org.apache.spark.sql.functions.to_date val
dateFormat = "yyyy­dd­MM" val
cleanDateDF =
spark.range(1).select( to_date(lit("2017­12­11") , formato de
fecha). alias ("fecha"), to_date (lit ("2017­20­12"), formato de fecha). alias ("fecha 2"))
cleanDateDF.createOrReplaceTempView("dateTable2")

# en Python
desde pyspark.sql.functions import to_date
dateFormat = "yyyy­dd­MM"
cleanDateDF =
spark.range(1).select( to_date(lit("2017­12­11"), dateFormat).alias("
fecha"), to_date(lit("2017­20­12"), formato de fecha).alias("fecha2"))
cleanDateDF.createOrReplaceTempView("dateTable2")

­­ en SQL
SELECCIONE to_date(fecha, 'aaaa­dd­MM'), to_date(fecha2, 'aaaa­dd­MM'), to_date(fecha)
DESDE dateTable2

+­­­­­­­­­­+­­­­­­­­­­+
| fecha| fecha2|
+­­­­­­­­­­+­­­­­­­­­­+
|2017­11­12|2017­12­20|
+­­­­­­­­­­+­­­­­­­­­­+

Ahora usemos un ejemplo de to_timestamp, que siempre requiere que se especifique un formato:
Machine Translated by Google

// en Scala
import org.apache.spark.sql.functions.to_timestamp
cleanDateDF.select(to_timestamp(col("date"), dateFormat)).show()

# en Python
desde pyspark.sql.functions import to_timestamp
cleanDateDF.select(to_timestamp(col("date"), dateFormat)).show()

­­ en SQL
SELECCIONE to_timestamp(fecha, 'aaaa­dd­MM'), to_timestamp(fecha2, 'aaaa­dd­MM')
DESDE dateTable2

+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
|to_timestamp(`fecha`, 'aaaa­dd­MM')|
+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
| 2017­11­12 00:00:00|
+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+

La conversión entre fechas y marcas de tiempo es simple en todos los idiomas; en SQL, lo haríamos de la siguiente
manera:

­­ en SQL
SELECCIONE cast(to_date("2017­01­01", "yyyy­dd­MM") como marca de tiempo)

Una vez que tenemos nuestra fecha o marca de tiempo en el formato y tipo correctos, la comparación entre ellos es
bastante fácil. Solo debemos asegurarnos de usar un tipo de fecha/marca de tiempo o especificar nuestra cadena de
acuerdo con el formato correcto de aaaa­MM­dd si estamos comparando una fecha:

cleanDateDF.filter(col("date2") > lit("2017­12­12")).show()

Un punto menor es que también podemos establecer esto como una cadena, que Spark analiza en un literal:

cleanDateDF.filter(col("date2") > "'2017­12­12'").show()

ADVERTENCIA

La conversión implícita de tipos es una manera fácil de dispararse a sí mismo, especialmente cuando se trata de
valores nulos o fechas en diferentes zonas horarias o formatos. Le recomendamos que los analice de forma
explícita en lugar de basarse en conversiones implícitas.

Trabajar con valores nulos en los datos

Como práctica recomendada, siempre debe usar valores nulos para representar datos faltantes o vacíos en sus
marcos de datos. Spark puede optimizar el trabajo con valores nulos más que si usa cadenas vacías u otros valores.
La forma principal de interactuar con valores nulos, a escala de DataFrame, es
Machine Translated by Google

use el subpaquete .na en un DataFrame. También hay varias funciones para realizar operaciones y especificar
explícitamente cómo Spark debe manejar los valores nulos. Para obtener más información, consulte el Capítulo 5 (donde
se analiza el orden) y también consulte "Trabajar con valores booleanos".

ADVERTENCIA

Los nulos son una parte desafiante de toda la programación, y Spark no es una excepción. En nuestra
opinión, ser explícito siempre es mejor que ser implícito cuando se manejan valores nulos. Por ejemplo, en esta
parte del libro, vimos cómo podemos definir columnas con tipos nulos. Sin embargo, esto viene con una trampa.
Cuando declaramos que una columna no tiene un tiempo nulo, en realidad no se aplica. Para reiterar, cuando
define un esquema en el que se declara que todas las columnas no tienen valores nulos, Spark no lo hará cumplir
y felizmente permitirá valores nulos en esa columna. La señal anulable es simplemente para ayudar a Spark SQL
a optimizar el manejo de esa columna. Si tiene valores nulos en columnas que no deberían tener valores nulos,
puede obtener un resultado incorrecto o ver excepciones extrañas que pueden ser difíciles de depurar.

Hay dos cosas que puede hacer con los valores nulos: puede eliminar explícitamente los valores nulos o puede
completarlos con un valor (globalmente o por columna). Experimentemos con cada uno de estos ahora.

Juntarse
Spark incluye una función que le permite seleccionar el primer valor no nulo de un conjunto de columnas mediante la
función de fusión. En este caso, no hay valores nulos, por lo que simplemente devuelve la primera columna:

// en Scala
import org.apache.spark.sql.functions.coalesce
df.select(coalesce(col("Description"), col("CustomerId"))).show()

# en Python
desde pyspark.sql.functions import coalesce
df.select(coalesce(col("Description"), col("CustomerId"))).show()

ifnull, nullIf, nvl y nvl2


Hay varias otras funciones SQL que puede usar para lograr cosas similares. ifnull le permite seleccionar el segundo valor
si el primero es nulo y el valor predeterminado es el primero. Alternativamente, podría usar nullif, que devuelve nulo si los
dos valores son iguales o devuelve el segundo si no lo son. nvl devuelve el segundo valor si el primero es nulo, pero el
valor predeterminado es el primero. Finalmente, nvl2 devuelve el segundo valor si el primero no es nulo; de lo contrario,
devolverá el último valor especificado (else_value en el siguiente ejemplo):

­­ en SQL
SELECCIONAR

ifnull(null, 'return_value'), nullif('value',


'value'),
Machine Translated by Google

nvl(null, 'return_value'), nvl2('not_null',


'return_value', "else_value")
DESDE dfTable LÍMITE 1

+­­­­­­­­­­­­+­­­­+­­­­­­­­­­­­+­­­­­­­­­­­­+
| un| b| c| d|
+­­­­­­­­­­­­+­­­­+­­­­­­­­­­­­+­­­­­­­­­­­­+
|valor_devuelto|null|valor_devuelto|valor_devuelto|
+­­­­­­­­­­­­+­­­­+­­­­­­­­­­­­+­­­­­­­­­­­­+

Naturalmente, también podemos usarlos en expresiones seleccionadas en DataFrames.

gota
La función más simple es drop, que elimina las filas que contienen valores nulos. El valor predeterminado es descartar
cualquier fila en la que cualquier valor sea nulo:

df.na.drop()
df.na.drop("cualquiera")

En SQL, tenemos que hacer esto columna por columna:

­­ en SQL
SELECCIONE * DESDE dfTable DONDE La descripción NO ES NULA

Especificar "cualquiera" como argumento elimina una fila si alguno de los valores es nulo. El uso de "todos" elimina la fila solo
si todos los valores son nulos o NaN para esa fila:

df.na.drop("todos")

También podemos aplicar esto a ciertos conjuntos de columnas pasando una matriz de columnas:

// en Scala
df.na.drop("all", Seq("StockCode", "FacturaNo"))

# en Python
df.na.drop("all", subset=["StockCode", "InvoiceNo"])

llenar

Usando la función de relleno, puede llenar una o más columnas con un conjunto de valores. Esto se puede hacer especificando
un mapa, es decir, un valor particular y un conjunto de columnas.

Por ejemplo, para completar todos los valores nulos en columnas de tipo Cadena, puede especificar lo siguiente:

df.na.fill("Todos los valores nulos se convierten en esta cadena")


Machine Translated by Google

Podríamos hacer lo mismo para columnas de tipo Integer usando df.na.fill(5:Integer), o para Doubles
df.na.fill(5:Double). Para especificar columnas, simplemente pasamos una matriz de nombres de columnas como
hicimos en el ejemplo anterior:

// en Scala
df.na.fill(5, Seq("StockCode", "FacturaNo"))

# en Python
df.na.fill("all", subset=["StockCode", "FacturaNo"])

También podemos hacer esto con un Scala Map, donde la clave es el nombre de la columna y el valor es el valor
que nos gustaría usar para completar los valores nulos:

// en Scala
val fillColValues = Map("StockCode" ­> 5, "Description" ­> "Sin valor")
df.na.fill(fillColValues)

# en Python
fill_cols_vals = {"StockCode": 5, "Descripción" : "Sin valor"}
df.na.fill(fill_cols_vals)

reemplazar

Además de reemplazar los valores nulos como hicimos con soltar y rellenar, existen opciones más flexibles
que puede usar con más que solo valores nulos. Probablemente, el caso de uso más común es reemplazar todos
los valores en una determinada columna de acuerdo con su valor actual. El único requisito es que este valor sea
del mismo tipo que el valor original:

// en Scala
df.na.replace("Descripción", Mapa("" ­> "DESCONOCIDO"))

# en Python
df.na.replace([""], ["UNKNOWN"], "Descripción")

ordenar
Como discutimos en el Capítulo 5, puede usar asc_nulls_first, desc_nulls_first, asc_nulls_last o
desc_nulls_last para especificar dónde desea que aparezcan sus valores nulos en un DataFrame ordenado.

Trabajar con tipos complejos


Los tipos complejos pueden ayudarlo a organizar y estructurar sus datos de manera que tengan más sentido para
el problema que espera resolver. Hay tres tipos de tipos complejos: estructuras, matrices y mapas.
Machine Translated by Google

estructuras

Puede pensar en estructuras como marcos de datos dentro de marcos de datos. Un ejemplo trabajado ilustrará esto
más claramente. Podemos crear una estructura envolviendo un conjunto de columnas entre paréntesis en una consulta:

df.selectExpr("(Descripción, Número de factura) como complejo", "*")

df.selectExpr("struct(Description, InvoiceNo) as complex", "*")

// en Scala
import org.apache.spark.sql.functions.struct val
complexDF = df.select(struct("Description", "InvoiceNo").alias("complex"))
complexDF.createOrReplaceTempView("complexDF")

# en Python
desde pyspark.sql.functions import struct
complexDF = df.select(struct("Description", "InvoiceNo").alias("complex"))
complexDF.createOrReplaceTempView("complexDF")

Ahora tenemos un DataFrame con un complejo de columnas. Podemos consultarlo como lo haríamos con otro
DataFrame, la única diferencia es que usamos una sintaxis de punto para hacerlo, o el método de columna
getField:

complexDF.select("complejo.Descripción")
complexDF.select(col("complejo").getField("Descripción"))

También podemos consultar todos los valores en la estructura usando *. Esto trae todas las columnas al DataFrame
de nivel superior:

complejoDF.select("complejo.*")

­­ en SQL
SELECT complejo.* DESDE complexDF

arreglos
Para definir arreglos, trabajemos a través de un caso de uso. Con nuestros datos actuales, nuestro objetivo es tomar
cada palabra en nuestra columna Descripción y convertirla en una fila en nuestro marco de datos.

La primera tarea es convertir nuestra columna Descripción en un tipo complejo, una matriz.

dividir
Hacemos esto usando la función de división y especificando el delimitador:

// en Scala
importar org.apache.spark.sql.functions.split
Machine Translated by Google

df.select(split(col("Descripción"), " ")).show(2)

# en Python
desde pyspark.sql.functions import split
df.select(split(col("Description"), " ")).show(2)

­­ en SQL
SELECCIONE dividir (Descripción, ' ') DESDE dfTable

+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
|dividir(Descripción, )|
+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
| [BLANCO, COLGANTE, ...|
| [BLANCO, METAL, LA...|
+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+

Esto es bastante poderoso porque Spark nos permite manipular este tipo complejo como otra columna.
También podemos consultar los valores de la matriz utilizando una sintaxis similar a la de Python:

// en Scala
df.select(split(col("Descripción"), " ").alias("array_col"))
.selectExpr("array_col[0]").show(2)

# en Python
df.select(split(col("Descripción"), " ").alias("array_col"))\
.selectExpr("array_col[0]").show(2)

­­ en SQL
SELECCIONE dividir (Descripción, ' ') [0] DESDE dfTable

Esto nos da el siguiente resultado:

+­­­­­­­­­­­­+
|matriz_col[0]|
+­­­­­­­­­­­­+
BLANCO|
|| BLANCO|
+­­­­­­­­­­­­+

Longitud de la matriz

Podemos determinar la longitud de la matriz consultando su tamaño:

// en Scala
import org.apache.spark.sql.functions.size
df.select(size(split(col("Description"), " "))).show(2) // muestra 5 y 3

# en Python
desde el tamaño de importación de pyspark.sql.functions
Machine Translated by Google

df.select(tamaño(split(col("Descripción"), " "))).show(2) # muestra 5 y 3

matriz_contiene
También podemos ver si esta matriz contiene un valor:

// en Scala
import org.apache.spark.sql.functions.array_contains
df.select(array_contains(split(col("Description"), " "), "WHITE")).show(2)

# en Python
desde pyspark.sql.functions import array_contains
df.select(array_contains(split(col("Description"), " "), "WHITE")).show(2)

­­ en SQL
SELECCIONE array_contains(split(Descripción, ' '), 'BLANCO') DESDE dfTable

Esto nos da el siguiente resultado:

+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
|array_contains(split(Descripción, ), BLANCO)|
+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
cierto|
|| cierto|
+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+

Sin embargo, esto no resuelve nuestro problema actual. Para convertir un tipo complejo en un conjunto de filas
(una por valor en nuestra matriz), necesitamos usar la función de explosión.

explotar
La función de explosión toma una columna que consta de matrices y crea una fila (con el resto de los valores
duplicados) por valor en la matriz. La Figura 6­1 ilustra el proceso.

Figura 6­1. Explosión de una columna de texto

// en Scala
importar org.apache.spark.sql.functions.{dividir, explotar}

df.withColumn("dividido", split(col("Descripción"), " ")) .withColumn("explotado",


explosionado(col("dividido"))) .select("Descripción", "FacturaNo", "
explotado").show(2)
Machine Translated by Google

# en Python
desde pyspark.sql.functions importar dividir, explotar

df.withColumn("dividido", split(col("Descripción"), " "))\ .withColumn("explotado",


explosionado(col("dividido")))\ .select("Descripción", "FacturaNo" ,
"explotó").show(2)

­­ en SQL
SELECCIONE Descripción, Número de factura, desglosado
DESDE (SELECCIONAR *, dividir (Descripción, " ") como dividido DESDE dfTable)
VISTA LATERAL explosión (dividida) como explosión

Esto nos da el siguiente resultado:

+­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­+­­­­­­­­+
| Descripción|FacturaNo|desglosado|
+­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­+­­­­­­­­+
|CABEZA COLGANTE BLANCO...| 536365| BLANCO| |
CABEZA COLGANTE BLANCO...| 536365| COLGANTE|
+­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­+­­­­­­­­+

mapas
Los mapas se crean utilizando la función de mapa y los pares de columnas clave­valor. Luego puede
seleccionarlos como si seleccionara de una matriz:

// en Scala
import org.apache.spark.sql.functions.map
df.select(map(col("Description"), col("FacturaNo")).alias("complex_map")).show(2)

# en Python
desde pyspark.sql.functions import create_map
df.select(create_map(col("Description"), col("InvoiceNo")).alias("complex_map"))\
.mostrar(2)

­­ en SQL
SELECCIONE el mapa (Descripción, Número de factura) como mapa_complejo DESDE dfTable
DONDE Descripción NO ES NULO

Esto produce el siguiente resultado:

+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
| mapa_complejo|
+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
|Mapa(BLANCO COLGANTE...|
|Mapa(BLANCO METAL L...|
+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+

Puede consultarlos utilizando la clave adecuada. Una clave faltante devuelve nulo:
Machine Translated by Google

// en Scala
df.select(map(col("Description"), col("FacturaNo")).alias("complex_map"))
.selectExpr("complex_map['LANTERNA DE METAL BLANCO']").show(2)

# en Python
df.select(map(col("Description"), col("FacturaNo")).alias("complex_map"))\
.selectExpr("complex_map['LANTERNA DE METAL BLANCO']").show(2)

Esto nos da el siguiente resultado:

+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
|complex_map[LINTERNA DE METAL BLANCO]|
+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
nulo|
|| 536365|
+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+

También puede explotar tipos de mapas, lo que los convertirá en columnas:

// en Scala
df.select(map(col("Description"), col("FacturaNo")).alias("complex_map"))
.selectExpr("explotar(mapa_complejo)").show(2)

# en Python
df.select(map(col("Description"), col("FacturaNo")).alias("complex_map"))\
.selectExpr("explotar(mapa_complejo)").show(2)

Esto nos da el siguiente resultado:

+­­­­­­­­­­­­­­­­­­­­­+­­­­­­+
| llave| valor|
+­­­­­­­­­­­­­­­­­­­­­+­­­­­­+
|CABEZA COLGANTE BLANCO...|
536365| | LINTERNA METAL BLANCO|536365|
+­­­­­­­­­­­­­­­­­­­­­+­­­­­­+

Trabajando con JSON


Spark tiene un soporte único para trabajar con datos JSON. Puede operar directamente en cadenas de
JSON en Spark y analizar desde JSON o extraer objetos JSON. Comencemos por crear una columna
JSON:

// en Scala
val jsonDF = chispa.rango(1).selectExpr("""
'{"myJSONKey": {"myJSONValue": [1, 2, 3]}}' como jsonString""")

# en Python
jsonDF = chispa.rango(1).selectExpr("""
Machine Translated by Google

'{"myJSONKey": {"myJSONValue": [1, 2, 3]}}' como jsonString""")

Puede usar get_json_object para consultar en línea un objeto JSON, ya sea un diccionario o una matriz.
Puede usar json_tuple si este objeto tiene solo un nivel de anidamiento:

// en Scala
importar org.apache.spark.sql.functions.{get_json_object, json_tuple}

jsonDF.select( get_json_object(col("jsonString"), "$.myJSONKey.myJSONValue[1]") como "columna",


json_tuple (col("jsonString"), "myJSONKey")).show(2)

# en Python
desde pyspark.sql.functions import get_json_object, json_tuple

jsonDF.select( get_json_object(col("jsonString"), "$.myJSONKey.myJSONValue[1]") as "column",


json_tuple(col("jsonString"), "myJSONKey")).show(2)

Aquí está el equivalente en SQL:

jsonDF.selectExpr(
"json_tuple(jsonString, '$.myJSONKey.myJSONValue[1]') como columna").show(2)

Esto da como resultado la siguiente tabla:

+­­­­­­+­­­­­­­­­­­­­­­­­­­­+
|columna| c0|
+­­­­­­+­­­­­­­­­­­­­­­­­­­­+
| 2|{"miValorJSON":[1...|
+­­­­­­+­­­­­­­­­­­­­­­­­­­­+

También puede convertir un StructType en una cadena JSON mediante la función to_json:

// en Scala
import org.apache.spark.sql.functions.to_json
df.selectExpr("(FacturaNo, Descripción) as
myStruct") .select(to_json(col("myStruct")))

# en Python
desde pyspark.sql.functions import to_json
df.selectExpr("(FacturaNo, Descripción) as myStruct")
\ .select(to_json(col("myStruct")))

Esta función también acepta un diccionario (mapa) de parámetros que son los mismos que la fuente de datos
JSON. Puede usar la función from_json para volver a analizar esto (u otros datos JSON). Esto naturalmente
requiere que especifique un esquema y, opcionalmente, también puede especificar un mapa de opciones:
Machine Translated by Google

// en Scala
importar org.apache.spark.sql.functions.from_json importar
org.apache.spark.sql.types._ val parseSchema
= new StructType(Array(
new StructField("FacturaNo",StringType,true), new
StructField("Descripción",StringType,true)))
df.selectExpr("(FacturaNo, Descripción) as

myStruct") .select(to_json(col("myStruct") ).alias("nuevoJSON")) .select(from_json(col("nuevoJSON"), parseSchema), col("nuevoJSO

# en Python
desde pyspark.sql.functions import from_json desde
pyspark.sql.types import * parseSchema
=
StructType(( StructField("FacturaNo",StringType(),True),
StructField("Description",StringType(),True)) )
df.selectExpr("(FacturaNo, Descripción) as myStruct")
\ .select(to_json(col("myStruct")).alias("newJSON"))
\ .select(from_json(col("newJSON"), parseSchema ), col("nuevoJSON")).show(2)

Esto nos da el siguiente resultado:

+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
|jsontostructs(nuevoJSON)| nuevoJSON|
+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
| [536365,WHITE HAN...|{"FacturaNo":"536...| |
[536365,WHITE MET...|{"FacturaNo":"536...|
+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+

Funciones definidas por el usuario

Una de las cosas más poderosas que puede hacer en Spark es definir sus propias funciones. Estas funciones
definidas por el usuario (UDF) le permiten escribir sus propias transformaciones personalizadas
usando Python o Scala e incluso usar bibliotecas externas. Las UDF pueden tomar y devolver una o más columnas
como entrada. Los Spark UDF son increíblemente poderosos porque puede escribirlos en varios lenguajes de
programación diferentes; no es necesario que los cree en un formato esotérico o en un lenguaje específico del
dominio. Son solo funciones que operan en los datos, registro por registro.
De forma predeterminada, estas funciones se registran como funciones temporales para usarse en esa
SparkSession o Contexto específico.

Aunque puede escribir UDF en Scala, Python o Java, existen consideraciones de rendimiento que debe tener en
cuenta. Para ilustrar esto, veremos exactamente lo que sucede cuando crea UDF, lo pasa a Spark y luego ejecuta el
código usando ese UDF.

El primer paso es la función real. Crearemos uno simple para este ejemplo. Escribamos una función power3
que tome un número y lo eleve a una potencia de tres:

// en Scala
val udfExampleDF = chispa.rango(5).toDF("num")
Machine Translated by Google

def potencia3(número:Doble):Doble = número * número * número


potencia3(2.0)

# en Python
udfExampleDF = chispa.rango(5).toDF("num")
def power3(doble_valor):
devuelve valor_doble ** 3
potencia3(2.0)

En este ejemplo trivial, podemos ver que nuestras funciones funcionan como se esperaba. Podemos proporcionar una entrada
individual y producir el resultado esperado (con este caso de prueba simple). Hasta ahora, nuestras expectativas para
la entrada son altas: debe ser de un tipo específico y no puede ser un valor nulo (consulte “Trabajar con valores nulos en
los datos”).

Ahora que hemos creado estas funciones y las hemos probado, debemos registrarlas con Spark para poder usarlas en todas
las máquinas de nuestros trabajadores. Spark serializará la función en el controlador y la transferirá a través de la red a
todos los procesos ejecutores. Esto sucede independientemente del idioma.

Cuando usa la función, esencialmente ocurren dos cosas diferentes. Si la función está escrita en Scala o Java, puede usarla
dentro de Java Virtual Machine (JVM). Esto significa que habrá una pequeña penalización en el rendimiento aparte del hecho
de que no puede aprovechar las capacidades de generación de código que tiene Spark para las funciones integradas. Puede
haber problemas de rendimiento si crea o usa muchos objetos; lo cubrimos en la sección sobre optimización en el Capítulo 19.

Si la función está escrita en Python, sucede algo muy diferente. Spark inicia un proceso de Python en el trabajador,
serializa todos los datos en un formato que Python puede entender (recuerde, antes estaba en la JVM), ejecuta la
función fila por fila en esos datos en el proceso de Python y finalmente regresa los resultados de las operaciones de fila
a JVM y Spark.
La Figura 6­2 proporciona una descripción general del proceso.
Machine Translated by Google

Figura 6­2. Pie de figura

ADVERTENCIA

Comenzar este proceso de Python es costoso, pero el costo real está en serializar los datos en Python.
Esto es costoso por dos razones: es un cálculo costoso, pero también, después de que los datos
ingresan a Python, Spark no puede administrar la memoria del trabajador. Esto significa que podría causar
que un trabajador falle si tiene recursos limitados (porque tanto la JVM como Python compiten por la
memoria en la misma máquina). Recomendamos que escriba sus UDF en Scala o Java: la pequeña
cantidad de tiempo que le tomará escribir la función en Scala siempre producirá aumentos de velocidad
significativos y, además de eso, ¡todavía puede usar la función de Python!

Ahora que comprende el proceso, analicemos un ejemplo. Primero, necesitamos registrar la función para que esté
disponible como una función DataFrame:

// en Scala
import org.apache.spark.sql.functions.udf val
power3udf = udf(power3(_:Double):Double)

Podemos usar eso como cualquier otra función de DataFrame:

// en Scala
udfExampleDF.select(power3udf(col("num"))).show()
Machine Translated by Google

Lo mismo se aplica a Python: primero, lo registramos:

# en Python
desde pyspark.sql.functions import udf power3udf
= udf(power3)

Luego, podemos usarlo en nuestro código DataFrame:

# en Python
desde pyspark.sql.functions import col
udfExampleDF.select(power3udf(col("num"))).show(2)

+­­­­­­­­­­­+
|potencia3(núm)|
+­­­­­­­­­­­+
0|
|| 1|
+­­­­­­­­­­­+

En este momento, podemos usar esto solo como una función DataFrame. Es decir, no podemos usarlo dentro de
una expresión de cadena, solo en una expresión. Sin embargo, también podemos registrar esta UDF como una
función Spark SQL. Esto es valioso porque simplifica el uso de esta función dentro de SQL y entre idiomas.

Registramos la función en Scala:

// en Scala
spark.udf.register("power3", power3(_:Double):Double)
udfExampleDF.selectExpr("power3(num)").show(2)

Debido a que esta función está registrada con Spark SQL, y hemos aprendido que cualquier función o expresión
de Spark SQL es válida para usar como expresión cuando se trabaja con DataFrames, podemos dar la vuelta y usar el
UDF que escribimos en Scala, en Python. Sin embargo, en lugar de usarlo como una función de DataFrame, lo usamos
como una expresión SQL:

# en Python
udfExampleDF.selectExpr("power3(num)").show(2) # registrado
en Scala

También podemos registrar nuestra función Python para que esté disponible como una función SQL y usarla también
en cualquier idioma.

Una cosa que también podemos hacer para asegurarnos de que nuestras funciones funcionen correctamente es
especificar un tipo de retorno. Como vimos al comienzo de esta sección, Spark administra su propia información de tipo,
que no se alinea exactamente con los tipos de Python. Por lo tanto, es una buena práctica definir el tipo de valor devuelto
para su función cuando la define. Es importante tener en cuenta que no es necesario especificar el tipo de devolución,
pero es una buena práctica.
Machine Translated by Google

Si especifica el tipo que no se alinea con el tipo real devuelto por la función, Spark no generará un error, sino que solo
devolverá un valor nulo para designar una falla. Puede ver esto si tuviera que cambiar el tipo de retorno en la siguiente
función para que sea un DoubleType:

# en Python
desde pyspark.sql.types import IntegerType, DoubleType
spark.udf.register("power3py", power3, DoubleType())

# en Python
udfExampleDF.selectExpr("power3py(num)").show(2) # registrado a
través de Python

Esto se debe a que el rango crea números enteros. Cuando se opera con números enteros en Python, Python no
los convertirá en flotantes (el tipo correspondiente al tipo doble de Spark), por lo tanto, vemos nulo. Podemos remediar
esto asegurándonos de que nuestra función de Python devuelva un flotante en lugar de un número entero y la
función se comportará correctamente.

Naturalmente, también podemos usar cualquiera de estos desde SQL, después de registrarlos:

­­ en SQL
SELECCIONE power3(12), power3py(12) ­­ no funciona debido al tipo de retorno

Cuando desee devolver opcionalmente un valor de una UDF, debe devolver Ninguno en Python y un tipo de opción en
Scala:

## UDF de colmena

Como última nota, también puede usar la creación de UDF/UDAF a través de una sintaxis de Hive. Para permitir esto,
primero debe habilitar el soporte de Hive cuando crean su SparkSession (a través de
SparkSession.builder().enableHiveSupport()). Luego puede registrar UDF en SQL. Esto solo es compatible con paquetes
Scala y Java precompilados, por lo que deberá especificarlos como una dependencia:

­­ en SQL
CREAR FUNCIÓN TEMPORAL myFunc AS 'com.organization.hive.udf.FunctionName'

Además, puede registrar esto como una función permanente en Hive Metastore eliminando TEMPORAL.

Conclusión
Este capítulo demostró lo fácil que es extender Spark SQL para sus propios propósitos y hacerlo de una manera que no
es un lenguaje esotérico específico de un dominio, sino funciones simples que son fáciles de probar y mantener sin
siquiera usar Spark. Esta es una herramienta increíblemente poderosa que puede usar para especificar una lógica
comercial sofisticada que puede ejecutarse en cinco filas en sus máquinas locales.
Machine Translated by Google

oronterabytesofdataon a 1 0 0 ­ nodecluster!
Machine Translated by Google

Capítulo 7. Agregaciones

Agregar es el acto de recopilar algo y es una piedra angular del análisis de big data.
En una agregación, especificará una clave o agrupación y una función de agregación que especifica cómo debe transformar
una o más columnas. Esta función debe producir un resultado para cada grupo, dados múltiples valores de entrada. Las
capacidades de agregación de Spark son sofisticadas y maduras, con una variedad de posibilidades y casos de uso diferentes.
En general, las agregaciones se utilizan para resumir datos numéricos, normalmente mediante alguna agrupación.
Esto podría ser una suma, un producto o un simple conteo. Además, con Spark puede agregar cualquier tipo de valor en
una matriz, lista o mapa, como veremos en "Agregar a tipos complejos".

Además de trabajar con cualquier tipo de valores, Spark también nos permite crear los siguientes tipos de agrupaciones:

La agrupación más simple es simplemente resumir un DataFrame completo realizando una agregación en
una declaración de selección.

Un "agrupar por" le permite especificar una o más claves, así como una o más funciones de
agregación para transformar las columnas de valores.

Una "ventana" le brinda la posibilidad de especificar una o más claves, así como una o más funciones de
agregación para transformar las columnas de valores. Sin embargo, la entrada de filas a la función está
relacionada de alguna manera con la fila actual.

Un "conjunto de agrupación", que puede usar para agregar en múltiples niveles diferentes. Los conjuntos de
agrupación están disponibles como una primitiva en SQL y a través de acumulaciones y cubos en DataFrames.

Un “rollup” le permite especificar una o más claves, así como una o más funciones de agregación para transformar
las columnas de valores, que se resumirán jerárquicamente.

Un "cubo" le permite especificar una o más claves, así como una o más funciones de agregación para
transformar las columnas de valores, que se resumirán en todas las combinaciones de columnas.

Cada agrupación devuelve un RelationalGroupedDataset en el que especificamos nuestras agregaciones.

NOTA

Una cosa importante a considerar es qué tan exacta necesita que sea una respuesta. Cuando se realizan cálculos
sobre big data, puede ser bastante costoso obtener una respuesta exacta a una pregunta y, a menudo, es
mucho más económico simplemente solicitar una aproximación con un grado razonable de precisión. Notará
que mencionamos algunas funciones de aproximación a lo largo del libro y, a menudo, esta es una buena
oportunidad para mejorar la velocidad y la ejecución de sus trabajos de Spark, especialmente para análisis interactivos y ad hoc.
Machine Translated by Google

Comencemos leyendo nuestros datos sobre compras, dividiendo los datos para tener muchos menos
particiones (porque sabemos que es un pequeño volumen de datos almacenados en muchos archivos pequeños), y
almacenamiento en caché de los resultados para un acceso rápido:

// en escala
valor df = chispa.read.format("csv")
.option("encabezado", "verdadero")
.option("inferirEsquema", "verdadero")
.load("/datos/datos­minoristas/todos/*.csv")
.coalesce(5)
df.caché()
df.createOrReplaceTempView("dfTable")

# en pitón
df = chispa.leer.formato("csv")\
.option("encabezado", "verdadero")\
.opción("inferirEsquema", "verdadero")\
.load("/datos/datos­minoristas/todos/*.csv")\
.coalesce(5)
df.caché()
df.createOrReplaceTempView("dfTable")

Aquí hay una muestra de los datos para que pueda hacer referencia a la salida de algunas de las funciones:

+­­­­­­­­­+­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­ +­­­­­­­­­­­­­­+­­­­­­­­­+­­­­­
|Número de factura|Código de existencias| Descripción|Cantidad| Fecha de factura|Precio unitario|Cu...
+­­­­­­­­­+­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­ +­­­­­­­­­­­­­­+­­­­­­­­­+­­­­­
536365| 85123A|BLANCO COLGAR... 6|12/1/2010 8:26| 6| 2.55| ...
|| 536365| 71053|METAL BLANCO... || 12/1/2010 8:26| 3.39| ...
...
536367| 21755|BLOQUE CONSTRUYENDO 3|12/1/2010 8:34| 4| 5.95| ...
|| 536367| AMOR...| 21777|CAJA DE RECETAS CON M...| 1/12/2010 8:34| 7.95| ...
+­­­­­­­­­+­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­ +­­­­­­­­­­­­­­+­­­­­­­­­+­­­­­

Como se mencionó, las agregaciones básicas se aplican a un DataFrame completo. El ejemplo más simple es el
método de conteo:

df.cuenta() == 541909

Si ha estado leyendo este libro capítulo por capítulo, sabe que contar es en realidad una acción como
opuesto a una transformación, por lo que vuelve inmediatamente. Puedes usar contar para tener una idea de
el tamaño total de su conjunto de datos, pero otro patrón común es usarlo para almacenar en caché todo un
DataFrame en la memoria, tal como lo hicimos en este ejemplo.

Ahora, este método es un poco atípico porque existe como un método (en este caso) en lugar de un
función y se evalúa con entusiasmo en lugar de una transformación perezosa. En la siguiente sección, veremos
count también se usa como una función perezosa.
Machine Translated by Google

Funciones de agregación
Todas las agregaciones están disponibles como funciones, además de los casos especiales que pueden aparecer en
DataFrames oa través de .stat, como vimos en el Capítulo 6. Puede encontrar la mayoría de las funciones de agregación
en el paquete org.apache.spark.sql.functions.

NOTA

Existen algunas brechas entre las funciones SQL disponibles y las funciones que podemos importar en Scala
y Python. Esto cambia cada versión, por lo que es imposible incluir una lista definitiva. Esta sección cubre
las funciones más comunes.

contar
La primera función que vale la pena repasar es contar, excepto que en este ejemplo funcionará como una
transformación en lugar de una acción. En este caso, podemos hacer una de dos cosas: especificar una columna
específica para contar, o todas las columnas usando count (*) o count (1) para representar que queremos contar cada fila
como la literal, como se muestra en este ejemplo:

// en Scala
import org.apache.spark.sql.functions.count
df.select(count("StockCode")).show() // 541909

# en Python
desde pyspark.sql.functions import count
df.select(count("StockCode")).show() # 541909

­­ en SQL
SELECCIONE CONTEO (*) DE dfTable

ADVERTENCIA

Hay una serie de trampas cuando se trata de valores nulos y conteo. Por ejemplo, al realizar un recuento
(*), Spark contará los valores nulos (incluidas las filas que contienen todos los valores nulos). Sin embargo, al
contar una columna individual, Spark no contará los valores nulos.

contarDistinto
A veces, el número total no es relevante; más bien, es la cantidad de grupos únicos que desea. Para obtener este
número, puede usar la función countDistinct. Esto es un poco más relevante para columnas individuales:

// en Scala
importar org.apache.spark.sql.functions.countDistinct
Machine Translated by Google

df.select(countDistinct("StockCode")).show() // 4070

# en Python
desde pyspark.sql.functions import countDistinct
df.select(countDistinct("StockCode")).show() # 4070

­­ en SQL
SELECCIONE CONTEO (DISTINTO *) DE DFTABLE

approx_count_distinct
A menudo, nos encontramos trabajando con grandes conjuntos de datos y el recuento distinto exacto es irrelevante.
Hay momentos en los que una aproximación a un cierto grado de precisión funcionará bien, y para eso, puede usar la
función approx_count_distinct:

// en Scala
import org.apache.spark.sql.functions.approx_count_distinct
df.select(approx_count_distinct("StockCode", 0.1)).show() // 3364

# en Python
desde pyspark.sql.functions import approx_count_distinct
df.select(approx_count_distinct("StockCode", 0.1)).show() # 3364

­­ en SQL
SELECCIONE approx_count_distinct(StockCode, 0.1) DESDE DFTABLE

Notarás que approx_count_distinct tomó otro parámetro con el que puedes especificar el error máximo de estimación
permitido. En este caso, especificamos un error bastante grande y, por lo tanto, recibimos una respuesta bastante lejana pero
que se completa más rápidamente que countDistinct.
Verá ganancias de rendimiento mucho mayores con conjuntos de datos más grandes.

Primero y último
Puede obtener el primer y el último valor de un DataFrame usando estas dos funciones obviamente nombradas. Esto
se basará en las filas del DataFrame, no en los valores del DataFrame:

// en Scala
import org.apache.spark.sql.functions.{first, last}
df.select(first("StockCode"), last("StockCode")).show()

# en Python
desde pyspark.sql.functions import first, last
df.select(first("StockCode"), last("StockCode")).show()

­­ en SQL
SELECCIONE primero (código de stock), último (código de stock) DESDE dfTable
Machine Translated by Google

+­­­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
|first(StockCode, false)|último(StockCode, false)|
+­­­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
| 85123A| 22138|
+­­­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+

mínimo y máximo
Para extraer los valores mínimo y máximo de un DataFrame, use las funciones min y max:

// en Scala
import org.apache.spark.sql.functions.{min, max}
df.select(min("Cantidad"), max("Cantidad")).show()

# en Python
desde pyspark.sql.functions import min, max
df.select(min("Cantidad"), max("Cantidad")).show()

­­ en SQL
SELECCIONE min (Cantidad), max (Cantidad) DE dfTable

+­­­­­­­­­­­­­+­­­­­­­­­­­­­+
|min(Cantidad)|max(Cantidad)|
+­­­­­­­­­­­­­+­­­­­­­­­­­­­+
| ­80995| 80995|
+­­­­­­­­­­­­­+­­­­­­­­­­­­­+

suma
Otra tarea simple es sumar todos los valores en una fila usando la función de suma:

// en Scala
importar org.apache.spark.sql.functions.sum
df.select(sum("Cantidad")).show() // 5176450

# en Python
desde pyspark.sql.functions import sum
df.select(sum("Quantity")).show() # 5176450

­­ en SQL
SELECCIONE suma (Cantidad) DE dfTable

sumaDistinto
Además de sumar un total, también puede sumar un conjunto distinto de valores mediante la
función sumDistinct:

// en Scala
importar org.apache.spark.sql.functions.sumDistinct
Machine Translated by Google

df.select(sumDistinct("Cantidad")).show() // 29310

# en Python
desde pyspark.sql.functions import sumDistinct
df.select(sumDistinct("Quantity")).show() # 29310

­­ en SQL
SELECCIONE SUMA (Cantidad) DE dfTable ­ 29310

promedio

Aunque puede calcular el promedio dividiendo la suma por el conteo, Spark proporciona una forma más fácil de obtener
ese valor a través de las funciones de promedio o promedio. En este ejemplo, usamos alias para reutilizar más
fácilmente estas columnas más adelante:

// en Scala
importar org.apache.spark.sql.functions.{sum, count, avg, expr}

df.select( count("Cantidad").alias("total_transacciones"),
sum("Cantidad").alias("total_compras"),
avg("Cantidad").alias("promedio_compras"), expr("promedio
(Cantidad)").alias("promedio_compras")) .selectExpr( "total_compras/
total_transacciones",
"promedio_compras", "promedio_compras").show()

# en Python
desde pyspark.sql.functions import sum, count, avg, expr

df.select( count("Cantidad").alias("total_transacciones"),
sum("Cantidad").alias("total_compras"),
avg("Cantidad").alias("promedio_compras"), expr("promedio
(Cantidad)").alias("promedio_compras"))\ .selectExpr( "total_compras/
total_transacciones",
"promedio_compras", "promedio_compras").show()

+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­ ­­­­­­+­­­­­­­­­­­­­­­­­+
|(total_compras / total_transacciones)| compras_promedio| compras_medias|
+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­ ­­­­­­+­­­­­­­­­­­­­­­­­+
| 9.55224954743324|9.55224954743324|9.55224954743324|
+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­ ­­­­­­+­­­­­­­­­­­­­­­­­+

NOTA

También puede promediar todos los valores distintos especificando distintos. De hecho, la mayoría de las funciones agregadas
Machine Translated by Google

admite hacerlo solo en valores distintos.

Varianza y desviación estándar


Calcular la media, naturalmente, plantea preguntas sobre la varianza y la desviación estándar.
Ambas son medidas de la dispersión de los datos alrededor de la media. La varianza es el promedio de las diferencias
al cuadrado de la media, y la desviación estándar es la raíz cuadrada de la varianza. Puede calcularlos en Spark
usando sus respectivas funciones. Sin embargo, algo a tener en cuenta es que Spark tiene tanto la fórmula
para la desviación estándar de la muestra como la fórmula para la desviación estándar de la población. Estas son
fórmulas estadísticas fundamentalmente diferentes, y necesitamos diferenciarlas. De manera predeterminada, Spark
ejecuta la fórmula para la desviación estándar o la varianza de la muestra si usa las funciones de varianza o stddev.

También puede especificarlos explícitamente o hacer referencia a la desviación o varianza estándar de la población:

// en Scala
importar org.apache.spark.sql.functions.{var_pop, stddev_pop} importar
org.apache.spark.sql.functions.{var_samp, stddev_samp}
df.select(var_pop("Cantidad"), var_samp(" Cantidad"),
stddev_pop("Cantidad"), stddev_samp("Cantidad")).show()

# en Python
desde pyspark.sql.functions import var_pop, stddev_pop desde
pyspark.sql.functions import var_samp, stddev_samp
df.select(var_pop("Cantidad"), var_samp("Cantidad"),
stddev_pop("Cantidad"), stddev_samp("Cantidad")).show()

­­ en SQL
SELECCIONE var_pop(Cantidad), var_samp(Cantidad),
stddev_pop(Cantidad), stddev_samp(Cantidad)
DESDE dfTable
Machine Translated by Google

+­­­­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­ ­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
| var_pop(Cantidad)|var_samp(Cantidad)|stddev_pop(Cantidad)|stddev_samp(Cantidad...|
+­­­­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­ ­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
|47559.303646609056|47559.391409298754| 218.08095663447796| 218.081157850...|
+­­­­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­ ­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+

asimetría y curtosis
La asimetría y la curtosis son medidas de puntos extremos en sus datos. La asimetría mide la
asimetría de los valores en sus datos alrededor de la media, mientras que la curtosis es una medida
de la cola de los datos. Ambos son relevantes específicamente al modelar sus datos como una
distribución de probabilidad de una variable aleatoria. Aunque aquí no entraremos en las matemáticas
detrás de estos específicamente, puede buscar definiciones con bastante facilidad en Internet. Puedes
calcularlos usando las funciones:

import org.apache.spark.sql.functions.{sesgo, curtosis} df.select(sesgo("Cantidad"),


curtosis("Cantidad")).show()

# en Python
desde pyspark.sql.functions import sesgo, curtosis df.select(sesgo("Cantidad"),
curtosis("Cantidad")).show()

­­ en SQL
SELECCIONE asimetría (Cantidad), curtosis (Cantidad) DE dfTable

+­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­+
| asimetría(Cantidad)|curtosis(Cantidad)|
+­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­+
|­0.2640755761052562|119768.05495536952|
+­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­+

Covarianza y Correlación
Discutimos las agregaciones de una sola columna, pero algunas funciones comparan las interacciones
de los valores en dos columnas de diferencia juntas. Dos de estas funciones son cov y corr, para
covarianza y correlación, respectivamente. La correlación mide el coeficiente de correlación de
Pearson, que se escala entre ­1 y +1. La covarianza se escala de acuerdo con las entradas en los datos.

Al igual que la función var, la covarianza se puede calcular como la covarianza de la muestra o
como la covarianza de la población. Por lo tanto, puede ser importante especificar qué fórmula desea utilizar.
La correlación no tiene noción de esto y por lo tanto no tiene cálculos para población o muestra. Así
es como funcionan:

// en Scala
importar org.apache.spark.sql.functions.{corr, covar_pop, covar_samp}
Machine Translated by Google

df.select(corr("FacturaNro", "Cantidad"), covar_samp("FacturaNro", "Cantidad"),


covar_pop("FacturaNo", "Cantidad")).show()

# en Python
desde pyspark.sql.functions import corr, covar_pop, covar_samp df.select(corr("FacturaNo",
"Cantidad"), covar_samp("FacturaNo", "Cantidad"),
covar_pop("FacturaNo", "Cantidad")).show()

­­ en SQL
SELECCIONE corr (Núm. Factura, Cantidad), covar_samp (Núm. Factura, Cantidad), covar_pop
(Núm. Factura, Cantidad)
DESDE dfTable

+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­ ­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
|corr(FacturaNo, Cantidad)|covar_samp(FacturaNo, Cantidad)|covar_pop(FacturaN...|
+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­ ­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
| 4.912186085635685E­4| 1052.7280543902734| 1052.7...|
+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­ ­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+

Agregar a tipos complejos


En Spark, puede realizar agregaciones no solo de valores numéricos usando fórmulas, también puede realizarlas
en tipos complejos. Por ejemplo, podemos recopilar una lista de valores presentes en una columna determinada
o solo los valores únicos mediante la recopilación en un conjunto.

Puede usar esto para realizar más acceso programático más adelante en la canalización o pasar la colección
completa en una función definida por el usuario (UDF):

// en Scala
import org.apache.spark.sql.functions.{collect_set, collect_list} df.agg(collect_set("País"),
collect_list("País")).show()

# en Python
desde pyspark.sql.functions import collect_set, collect_list df.agg(collect_set("País"),
collect_list("País")).show()

­­ en SQL
SELECCIONE collect_set (País), collect_set (País) DESDE dfTable

+­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
|collect_set(País)|collect_list(País)|
+­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
|[Portugal, Italia,...| [Reino Unido, ...|
+­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+

Agrupamiento
Hasta ahora, solo hemos realizado agregaciones a nivel de DataFrame. Una tarea más común es realizar
cálculos basados en grupos de datos. Esto se hace típicamente en datos categóricos para
Machine Translated by Google

el cual agrupamos nuestros datos en una columna y realizamos algunos cálculos en las otras columnas que terminan
en ese grupo.

La mejor manera de explicar esto es comenzar realizando algunas agrupaciones. El primero será un conteo, tal
como lo hicimos antes. Agruparemos por cada número de factura único y obtendremos el recuento de artículos en esa
factura. Tenga en cuenta que esto devuelve otro DataFrame y se realiza con pereza.

Esta agrupación la hacemos en dos fases. Primero especificamos la(s) columna(s) en las que nos gustaría agrupar,
y luego especificamos la(s) agregación(es). El primer paso devuelve un
RelationalGroupedDataset y el segundo paso devuelve un DataFrame.

Como se mencionó, podemos especificar cualquier número de columnas en las que queremos agrupar:

df.groupBy("FacturaNo", "IdCliente").count().show()

­­ en SQL
SELECCIONE el conteo (*) DESDE dfTable GROUP BY InvoiceNo, CustomerId

+­­­­­­­­­­­­­­+­­­­­­­­­­+­­­­­+
|FacturaNo|IdCliente|recuento|
+­­­­­­­­­­­­­­+­­­­­­­­­­+­­­­­+
| 536846| 14573| 76|
...
| C544318| 12989| 1|
+­­­­­­­­­­­­­­+­­­­­­­­­­+­­­­­+

Agrupación con expresiones


Como vimos antes, contar es un caso un poco especial porque existe como método. Para ello, normalmente
preferimos utilizar la función de conteo. En lugar de pasar esa función como una expresión a una declaración de
selección, la especificamos como dentro de agg. Esto le permite pasar expresiones arbitrarias que solo necesitan
tener alguna agregación especificada. Incluso puede hacer cosas como alias de una columna después de transformarla
para su uso posterior en su flujo de datos:

// en Scala
importar org.apache.spark.sql.functions.count

df.groupBy("FacturaNo").agg(
cuenta("Cantidad").alias("cantidad"),
expr("cuenta(Cantidad)")).mostrar()

# en Python
desde pyspark.sql.functions import count

df.groupBy("FacturaNo").agg( cuenta("Cantidad").alias("cantidad"), expr("cuenta(Cantidad)")).show()

+­­­­­­­­­­­­­­+­­­­+­­­­­­­­­­­­­­­+
Machine Translated by Google

|FacturaNo|cantidad|recuento(Cantidad)|
+­­­­­­­­­­­­­­+­­­­+­­­­­­­­­­­­­­­+
| 536596| 6| 6|
...
| C542604| 8| 8|
+­­­­­­­­­­­­­­+­­­­+­­­­­­­­­­­­­­­+

Agrupación con mapas


A veces, puede ser más fácil especificar sus transformaciones como una serie de mapas para los cuales la
clave es la columna y el valor es la función de agregación (como una cadena) que le gustaría realizar.
También puede reutilizar varios nombres de columna si los especifica en línea:

// en Scala
df.groupBy("FacturaNo").agg("Cantidad"­>"promedio", "Cantidad"­>"stddev_pop").show()

# en Python
df.groupBy("FacturaNo").agg(expr("promedio(Cantidad)"),expr("stddev_pop(Cantidad)"))\
.espectáculo()

­­ en SQL
SELECCIONE avg (Cantidad), stddev_pop (Cantidad), InvoiceNo DE dfTable
GRUPO POR Número de factura

+­­­­­­­­­+­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­ +
|FacturaNo| avg(Cantidad)|stddev_pop(Cantidad)|
+­­­­­­­­­+­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­ +
| 536596| 1.5| 1.1180339887498947|
...
| C542604| ­8.0| 15.173990905493518|
+­­­­­­­­­+­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­ +

Funciones de ventana
También puede usar funciones de ventana para realizar algunas agregaciones únicas, ya sea calculando
alguna agregación en una "ventana" específica de datos, que define mediante una referencia a los datos
actuales. Esta especificación de ventana determina qué filas se pasarán a esta función.
Ahora bien, esto es un poco abstracto y probablemente similar a un grupo estándar, así que diferenciémoslos
un poco más.

Un grupo por toma datos, y cada fila puede ir solo a una agrupación. Una función de ventana calcula
un valor de retorno para cada fila de entrada de una tabla en función de un grupo de filas, denominado marco.
Cada fila puede caer en uno o más marcos. Un caso de uso común es echar un vistazo a un promedio
móvil de algún valor para el cual cada fila representa un día. Si hiciera esto, cada fila terminaría en siete
marcos diferentes. Cubriremos la definición de marcos un poco más adelante, pero para su referencia,
Spark admite tres tipos de funciones de ventana: funciones de clasificación, funciones analíticas y funciones
agregadas.
Machine Translated by Google

La Figura 7­1 ilustra cómo una fila dada puede caer en varios marcos.

Figura 7­1. Visualización de las funciones de la ventana

Para demostrarlo, agregaremos una columna de fecha que convertirá la fecha de nuestra factura en una columna
que contiene solo información de fecha (no también información de hora):

// en Scala
import org.apache.spark.sql.functions.{col, to_date} val
dfWithDate = df.withColumn("date", to_date(col("InvoiceDate"), "MM/d/yyyy
H:mm" ))
dfConFecha.createOrReplaceTempView("dfConFecha")

# en Python
desde pyspark.sql.functions import col, to_date
dfWithDate = df.withColumn("date", to_date(col("InvoiceDate"), "MM/d/yyyy H:mm"))
dfWithDate.createOrReplaceTempView("dfWithDate ")

El primer paso para una función de ventana es crear una especificación de ventana. Tenga en cuenta que la partición
por no está relacionada con el concepto de esquema de partición que hemos cubierto hasta ahora. Es solo
un concepto similar que describe cómo dividiremos nuestro grupo. El orden determina el orden dentro de una
partición dada y, finalmente, la especificación del marco (la instrucción rowsBetween) establece qué filas se
incluirán en el marco en función de su referencia a la fila de entrada actual. En el siguiente ejemplo, observamos
todas las filas anteriores hasta la fila actual:

// en Scala
import org.apache.spark.sql.expressions.Window import
org.apache.spark.sql.functions.col val windowSpec
=
Window .partitionBy("CustomerId",
"date") .orderBy(col("Cantidad ").desc)
Machine Translated by Google

.rowsBetween(Window.unboundedPreceding, Window.currentRow)

# en Python
desde pyspark.sql.window import Window
from pyspark.sql.functions import desc
windowSpec =
Window\ .partitionBy("CustomerId", "date")
\ .orderBy(desc("Quantity"))
\ .rowsBetween(Window .unboundedPreceding, Window.currentRow)

Ahora queremos usar una función de agregación para obtener más información sobre cada cliente específico. Un
ejemplo podría ser establecer la cantidad máxima de compra en todo momento. Para responder a esto, usamos las mismas
funciones de agregación que vimos anteriormente al pasar un nombre de columna o una expresión.
Además, indicamos la especificación de la ventana que define a qué marcos de datos se aplicará esta función:

import org.apache.spark.sql.functions.max val


maxPurchaseQuantity = max(col("Cantidad")).over(windowSpec)

# en Python
desde pyspark.sql.functions import max
maxPurchaseQuantity = max(col("Cantidad")).over(windowSpec)

Notará que esto devuelve una columna (o expresiones). Ahora podemos usar esto en una declaración de selección de
DataFrame. Sin embargo, antes de hacerlo, crearemos el rango de cantidad de compra. Para hacer eso, usamos la
función dense_rank para determinar qué fecha tuvo la cantidad máxima de compra para cada cliente. Usamos
dense_rank en lugar de rank para evitar brechas en la secuencia de clasificación cuando hay valores empatados
(o en nuestro caso, filas duplicadas):

// en Scala
import org.apache.spark.sql.functions.{dense_rank, rank} val
buyDenseRank = dense_rank().over(windowSpec) val buyRank
= rank().over(windowSpec)

# en Python
desde pyspark.sql.functions import dense_rank, rank
buyDenseRank = dense_rank().over(windowSpec) buyRank
= rank().over(windowSpec)

Esto también devuelve una columna que podemos usar en declaraciones de selección. Ahora podemos realizar una
selección para ver los valores de ventana calculados:

// en Scala
importar org.apache.spark.sql.functions.col

dfWithDate.where("CustomerId NO ES NULO").orderBy("CustomerId")

.select( col("IdCliente"),
Machine Translated by Google

col("fecha"),
col("Cantidad"),
orden de compra.alias("rango de cantidad"),
buyDenseRank.alias("quantityDenseRank"),
maxPurchaseQuantity.alias("maxPurchaseQuantity")).show()

# en pitón
de pyspark.sql.functions import col

dfWithDate.where("CustomerId NO ES NULO").orderBy("CustomerId")\
.seleccionar(
col("IdCliente"),
col("fecha"),
col("Cantidad"),
orden de compra.alias("rango de cantidad"),
buyDenseRank.alias("quantityDenseRank"),
maxPurchaseQuantity.alias("maxPurchaseQuantity")).show()

­­ en SQL
SELECCIONE CustomerId, fecha, Cantidad,
rango (Cantidad) SOBRE (PARTICIÓN POR CustomerId, fecha
ORDENAR POR Cantidad DESC NULLS LAST
FILAS ENTRE
ILIMITADO PRECEDENTE Y
FILA ACTUAL) como rango,

dense_rank(Cantidad) SOBRE (PARTICIÓN POR CustomerId, fecha


ORDENAR POR Cantidad DESC NULLS LAST
FILAS ENTRE
ILIMITADO PRECEDENTE Y
FILA ACTUAL) como dRank,

max(Cantidad) SOBRE (PARTICIÓN POR CustomerId, fecha


ORDENAR POR Cantidad DESC NULLS LAST
FILAS ENTRE
ILIMITADO PRECEDENTE Y
FILA ACTUAL) como maxPurchase
DESDE dfWithDate DONDE CustomerId NO ES UN PEDIDO NULO POR CustomerId

+­­­­­­­­­+­­­­­­­­­­+­­­­­­­­+­­­­­­­­­­­+­­­­­­­­­­­ ­­­­­­­­­­­­+­­­­­­­­­­­­­­­+
|IdCliente| fecha|Cantidad|quantityRank|quantityDenseRank|maxP...Cantidad|
+­­­­­­­­­+­­­­­­­­­­+­­­­­­­­+­­­­­­­­­­­+­­­­­­­­­­­ ­­­­­­­­­­­­+­­­­­­­­­­­­­­­+
| 12346|2011­01­18| 74215| 12346| 1| 1| 74215|
| 2011­01­18| ­74215| 12347|2010­12­07| 2| 2| 74215|
| 36| 12347|2010­12­07| 30| 1| 1| 36|
| 2| 2| 36|
...
| 12347|2010­12­07| 12347| 12| 4| 4| 36|
| 2010­12­07| 12347| 6| 17| 5| 36|
| 2010­12­07| 6| 17| 5| 36|
+­­­­­­­­­+­­­­­­­­­­+­­­­­­­­+­­­­­­­­­­­+­­­­­­­­­­­ ­­­­­­­­­­­­+­­­­­­­­­­­­­­­+
Machine Translated by Google

Conjuntos de agrupación

Hasta ahora, en este capítulo, hemos visto expresiones simples de agrupación que podemos usar para agregar en
un conjunto de columnas con los valores en esas columnas. Sin embargo, a veces queremos algo un poco
más completa: una agregación a través de múltiples grupos. Logramos esto mediante el uso de conjuntos de agrupación.
Los conjuntos de agrupación son una herramienta de bajo nivel para combinar conjuntos de agregaciones. te dan la
capacidad de crear agregaciones arbitrarias en sus sentencias group­by.

Trabajemos con un ejemplo para obtener una mejor comprensión. Aquí, nos gustaría obtener el
cantidad total de todos los códigos de existencias y clientes. Para hacerlo, usaremos el siguiente SQL
expresión:

// en escala
val dfNoNull = dfConFecha.drop()
dfNoNull.createOrReplaceTempView("dfNoNull")

# en pitón
dfNoNull = dfConFecha.drop()
dfNoNull.createOrReplaceTempView("dfNoNull")

­­ en SQL
SELECCIONE CustomerId, stockCode, sum (Cantidad) DE dfNoNull
GRUPO POR ID de cliente, código de existencias
ORDEN POR ID de cliente DESC, código de stock DESC

+­­­­­­­­­­+­­­­­­­­­+­­­­­­­­­­­­+
|CustomerId|stockCode|suma(Cantidad)|
+­­­­­­­­­­+­­­­­­­­­+­­­­­­­­­­­­+
| 18287| 85173| 48|
| 18287| 85040A| 48|
| 18287| 85039B| 120|
...
| 18287| 23269| 36|
+­­­­­­­­­­+­­­­­­­­­+­­­­­­­­­­­­+

Puede hacer exactamente lo mismo usando un conjunto de agrupación:

­­ en SQL
SELECCIONE CustomerId, stockCode, sum (Cantidad) DE dfNoNull
GROUP BY customerId, stockCode GROUPING SETS((customerId, stockCode))
ORDEN POR ID de cliente DESC, código de stock DESC

+­­­­­­­­­­+­­­­­­­­­+­­­­­­­­­­­­+
|CustomerId|stockCode|suma(Cantidad)|
+­­­­­­­­­­+­­­­­­­­­+­­­­­­­­­­­­+
| 18287| 85173| 48|
| 18287| 85040A| 48|
| 18287| 85039B| 120|
...
| 18287| 23269| 36|
Machine Translated by Google

+­­­­­­­­­­+­­­­­­­­­+­­­­­­­­­­­­+

ADVERTENCIA

Los conjuntos de agrupación dependen de valores nulos para los niveles de agregación. Si no filtra los valores nulos,
obtendrá resultados incorrectos. Esto se aplica a cubos, resúmenes y conjuntos de agrupación.

Bastante simple, pero ¿qué sucede si también desea incluir el número total de elementos, independientemente de
código de cliente o stock? Con una sentencia group­by convencional, esto sería imposible. Pero,
es simple con conjuntos de agrupación: simplemente especificamos que nos gustaría agregar a ese nivel, como
bueno, en nuestro conjunto de agrupación. Esto es, efectivamente, la unión de varias agrupaciones diferentes:

­­ en SQL
SELECCIONE CustomerId, stockCode, sum (Cantidad) DE dfNoNull
GROUP BY customerId, stockCode GROUPING SETS((customerId, stockCode),())
ORDEN POR ID de cliente DESC, código de stock DESC

+­­­­­­­­­­+­­­­­­­­­+­­­­­­­­­­­­+
|customerId|stockCode|suma(Cantidad)|
+­­­­­­­­­­+­­­­­­­­­+­­­­­­­­­­­­+
| 18287| 85173| 48|
| 18287| 85040A| 48|
| 18287| 85039B| 120|
...
| 18287| 23269| 36|
+­­­­­­­­­­+­­­­­­­­­+­­­­­­­­­­­­+

El operador GROUPING SETS solo está disponible en SQL. Para realizar lo mismo en DataFrames, usted

use los operadores rollup y cube, que nos permiten obtener los mismos resultados. vamos a pasar
aquellos.

resúmenes
Hasta ahora, hemos estado analizando agrupaciones explícitas. Cuando configuramos nuestras claves de agrupación de múltiples
columnas, Spark las examina, así como las combinaciones reales que son visibles en el conjunto de datos. A
rollup es una agregación multidimensional que realiza una variedad de cálculos de estilo de grupo
para nosotros.

Vamos a crear un resumen que mire a través del tiempo (con nuestra nueva columna Fecha) y el espacio (con la

columna de país) y crea un nuevo marco de datos que incluye el total general de todas las fechas, el
total general para cada fecha en el DataFrame, y el subtotal para cada país en cada fecha en el
Marco de datos:

val rollUpDF = dfNoNull.rollup("Fecha", "País").agg(sum("Cantidad"))


.selectExpr("Fecha", "País", "`suma(Cantidad)` como cantidad_total")
.orderBy("Fecha")
Machine Translated by Google

enrolladoUpDF.show()

# en pitón
enrolladoUpDF = dfNoNull.rollup("Fecha", "País").agg(sum("Cantidad"))\
.selectExpr("Fecha", "País", "`suma(Cantidad)` as cantidad_total")\
.orderBy("Fecha")
enrolladoUpDF.show()

+­­­­­­­­­+­­­­­­­­­­­­­­+­­­­­­­­­­­­­­+
| Fecha| País|cantidad_total|
+­­­­­­­­­+­­­­­­­­­­­­­­+­­­­­­­­­­­­­­+
| nulo| nulo| 5176450|
|2010­12­01|Reino Unido| | 23949|
2010­12­01| Alemania| 117|
|2010­12­01| Francia| 449|
...
|2010­12­03| | Francia| 239|
2010­12­03| | Italia| 164|
2010­12­03| Bélgica| 528|
+­­­­­­­­­+­­­­­­­­­­­­­­+­­­­­­­­­­­­­­+

Ahora, donde vea los valores nulos es donde encontrará los totales generales. Un valor nulo en ambos rollup
columnas especifica el total general en ambas columnas:

enrolladoUpDF.where("País ES NULO").show()

enrolladoUpDF.where("La fecha ES NULA").show()

+­­­­+­­­­­­­+­­­­­­­­­­­­­­+
|Fecha|País|cantidad_total|
+­­­­+­­­­­­­+­­­­­­­­­­­­­­+
|nulo| nulo| 5176450|
+­­­­+­­­­­­­+­­­­­­­­­­­­­­+

Cubo
Un cubo lleva el resumen a un nivel más profundo. En lugar de tratar los elementos jerárquicamente, un cubo
hace lo mismo en todas las dimensiones. Esto significa que no solo irá por fecha durante el
todo el período de tiempo, sino también el país. Para volver a plantear esto como una pregunta, ¿puedes hacer una tabla?
que incluye lo siguiente?

El total en todas las fechas y países

El total de cada fecha en todos los países

El total de cada país en cada fecha

El total de cada país en todas las fechas

La llamada al método es bastante similar, pero en lugar de llamar a rollup, llamamos a cube:
Machine Translated by Google

// en escala
dfNoNull.cube("Fecha", "País").agg(sum(col("Cantidad")))
.select("Fecha", "País", "suma(Cantidad)").orderBy("Fecha").show()

# en pitón
de suma de importación de pyspark.sql.functions

dfNoNull.cube("Fecha", "País").agg(sum(col("Cantidad")))\
.select("Fecha", "País", "suma(Cantidad)").orderBy("Fecha").show()

+­­­­+­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­+
|Fecha| País|suma(Cantidad)|
+­­­­+­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­+
|nulo| | Japón| 25218|
nulo| | Portugal| 16180|
nulo| | Sin especificar| 3300|
nulo| | nulo| 5176450|
nulo| Australia| 83653|
...
|nulo| | Noruega| 19247|
nulo| | Hong Kong| 4769|
nulo| | España| 26824|
nulo| República Checa| 592|
+­­­­+­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­+

Este es un resumen rápido y de fácil acceso de casi toda la información en nuestra tabla, y
es una excelente manera de crear una tabla de resumen rápida que otros pueden usar más adelante.

Agrupación de metadatos
A veces, al usar cubos y resúmenes, desea poder consultar los niveles de agregación para
que puede filtrarlos fácilmente en consecuencia. Podemos hacer esto usando grouping_id,
lo que nos da una columna que especifica el nivel de agregación que tenemos en nuestro conjunto de resultados. El
query en el ejemplo siguiente devuelve cuatro ID de agrupación distintos:

Tabla 7­1. Propósito de agrupar ID

Agrupamiento
IDENTIFICACIÓN
Descripción

Esto aparecerá para la agregación de mayor nivel, lo que nos dará la cantidad total
3
independientemente de customerId y stockCode.

2 Esto aparecerá para todas las agregaciones de códigos de acciones individuales. Esto nos da la cantidad total.
por código de stock, independientemente del cliente.

1 Esto nos dará la cantidad total por cliente, independientemente del artículo comprado.

0 Esto nos dará la cantidad total para combinaciones individuales de ID de cliente y código de stock .
Machine Translated by Google

Esto es un poco abstracto, por lo que vale la pena intentar comprender el comportamiento usted mismo:

// en escala
importar org.apache.spark.sql.functions.{grouping_id, sum, expr}

dfNoNull.cube("customerId", "stockCode").agg(grouping_id(), sum("Cantidad"))


.orderBy(expr("grouping_id()").desc)
.espectáculo()

+­­­­­­­­­+­­­­­­­­­+­­­­­­­­­­­­­+­­­­­­­­­­­­­+
|customerId|stockCode|grouping_id()|sum(Cantidad)|
+­­­­­­­­­+­­­­­­­­­+­­­­­­­­­­­­­+­­­­­­­­­­­­­+
| nulo| nulo| nulo| 3| 5176450|
| 23217| nulo| 2| 1309|
| 90059E| 2| 19|
...
+­­­­­­­­­+­­­­­­­­­+­­­­­­­­­­­­­+­­­­­­­­­­­­­+

Pivote
Los pivotes le permiten convertir una fila en una columna. Por ejemplo, en nuestros datos actuales
tenemos una columna País. Con un pivote, podemos agregar según alguna función para cada
de esos países dados y mostrarlos de una manera fácil de consultar:

// en escala
val pivoted = dfWithDate.groupBy("fecha").pivot("País").sum()

# en pitón
pivoted = dfWithDate.groupBy("fecha").pivot("País").sum()

Este DataFrame ahora tendrá una columna para cada combinación de país, variable numérica,
y una columna que especifica la fecha. Por ejemplo, para USA tenemos las siguientes columnas:
USA_sum(Cantidad), USA_sum(Precio por unidad), USA_sum(ID de cliente). Esto representa uno para
cada columna numérica en nuestro conjunto de datos (porque acabamos de realizar una agregación sobre todas ellas).

Aquí hay una consulta de ejemplo y el resultado de estos datos:

pivoted.where("fecha > '2011­12­05'").select("fecha" ,"`USA_sum(Cantidad)`").show()

+­­­­­­­­­­+­­­­­­­­­­­­­­­­­+
| fecha|USA_sum(Cantidad)|
+­­­­­­­­­­+­­­­­­­­­­­­­­­­­+
|2011­12­06| | nulo|
2011­12­09| | nulo|
2011­12­08| | ­196|
2011­12­07| nulo|
+­­­­­­­­­­+­­­­­­­­­­­­­­­­­+

Ahora todas las columnas se pueden calcular con agrupaciones individuales, pero el valor de un pivote viene
Machine Translated by Google

hasta cómo le gustaría explorar los datos. Puede ser útil, si tiene una cardinalidad lo suficientemente baja en
una determinada columna para transformarla en columnas para que los usuarios puedan ver el esquema y saber de
inmediato qué consultar.

Funciones de agregación definidas por el usuario


Las funciones de agregación definidas por el usuario (UDAF) son una forma de que los usuarios definan sus propias
funciones de agregación en función de fórmulas personalizadas o reglas comerciales. Puede usar UDAF para realizar
cálculos personalizados sobre grupos de datos de entrada (a diferencia de filas individuales). Spark mantiene un
solo AggregationBuffer para almacenar resultados intermedios para cada grupo de datos de entrada.

Para crear un UDAF, debe heredar de la clase base UserDefinedAggregateFunction e implementar los siguientes
métodos:

inputSchema representa los argumentos de entrada como un StructType

bufferSchema representa resultados UDAF intermedios como un StructType

dataType representa el tipo de datos de retorno

determinista es un valor booleano que especifica si este UDAF devolverá el mismo resultado para una
entrada dada

initialize le permite inicializar valores de un búfer de agregación

actualizar describe cómo debe actualizar el búfer interno en función de una fila determinada

merge describe cómo se deben fusionar dos búferes de agregación

evaluar generará el resultado final de la agregación

El siguiente ejemplo implementa un BoolAnd, que nos informará si todas las filas (para una columna determinada) son
verdaderas; si no lo son, devolverá falso:

// en Scala
import org.apache.spark.sql.expressions.MutableAggregationBuffer import
org.apache.spark.sql.expressions.UserDefinedAggregateFunction import
org.apache.spark.sql.Row import
org.apache.spark.sql.types._ clase BoolAnd
extiende UserDefinedAggregateFunction {
def inputSchema: org.apache.spark.sql.types.StructType =
StructType(StructField("value", BooleanType) :: Nil) def
bufferSchema: StructType =
StructType( StructField("result", BooleanType) ::

Nil ) def dataType : DataType = BooleanType


def determinista: Boolean = true def
initialize(buffer: MutableAggregationBuffer): Unidad = { buffer(0) = true

}
Machine Translated by Google

actualización def (búfer: MutableAggregationBuffer, entrada: Fila): Unidad =


{ búfer (0) = búfer.getAs[Booleano](0) && entrada.getAs[Booleano](0)

} def merge(buffer1: MutableAggregationBuffer, buffer2: Row): Unidad = { buffer1(0)


= buffer1.getAs[Boolean](0) && buffer2.getAs[Boolean](0)

} def evaluar(buffer: Fila): Cualquiera =


{ buffer(0)

}}

Ahora, simplemente instanciamos nuestra clase y/o la registramos como una función:

// en Scala
val ba = new BoolAnd
spark.udf.register("booland", ba) import
org.apache.spark.sql.functions._

spark.range(1) .selectExpr("explode(array(TRUE, TRUE ,


VERDADERO)) como t") .selectExpr("explotar(matriz(VERDADERO,
FALSO, VERDADERO)) como f",
"t") .select(ba(col("t")), expr("booland(f )")) .mostrar()

+­­­­­­­­­­+­­­­­­­­­­+
|booland(t)|booland(f)|
+­­­­­­­­­­+­­­­­­­­­­+
| cierto| falso|
+­­­­­­­­­­+­­­­­­­­­­+

Actualmente, los UDAF solo están disponibles en Scala o Java. Sin embargo, en Spark 2.3, también podrá llamar a
Scala o Java UDF y UDAF registrando la función tal como mostramos en la sección UDF en el Capítulo 6. Para obtener
más información, vaya a SPARK­19439.

Conclusión
Este capítulo analizó los diferentes tipos y clases de agregaciones que puede realizar en Spark. Aprendió acerca de las
funciones simples de agrupación en ventana, así como de resumen y cubos.
El Capítulo 8 analiza cómo realizar uniones para combinar diferentes fuentes de datos.
Machine Translated by Google

Capítulo 8. Uniones

El Capítulo 7 cubrió la agregación de conjuntos de datos únicos, lo cual es útil, pero la mayoría de las veces, sus aplicaciones Spark reunirán una gran cantidad

de conjuntos de datos diferentes. Por este motivo, las uniones son una parte esencial de casi todas las cargas de trabajo de Spark. La capacidad de Spark

para hablar con diferentes datos significa que usted obtiene la capacidad de acceder a una variedad de fuentes de datos en toda su empresa. Este

capítulo cubre no solo qué uniones existen en Spark y cómo usarlas, sino también algunas de las funciones internas básicas para que pueda pensar en cómo

Spark realmente ejecuta la unión en el clúster. Este conocimiento básico puede ayudarte a evitar quedarte sin memoria y abordar problemas que antes no podías

resolver.

Unir expresiones
Una combinación reúne dos conjuntos de datos, el izquierdo y el derecho, comparando el valor de una o más claves de la izquierda y la derecha y evaluando el

resultado de una expresión de combinación que determina si Spark debe reunir el conjunto de datos izquierdo con el conjunto correcto de datos. La expresión

de combinación más común, una combinación equitativa, compara si las claves especificadas en los conjuntos de datos izquierdo y derecho son iguales. Si

son iguales, Spark combinará los conjuntos de datos izquierdo y derecho. Lo contrario es cierto para las claves que no coinciden; Spark descarta las filas que no

tienen claves coincidentes. Spark también permite políticas de unión mucho más sofisticadas además de las uniones equitativas. Incluso podemos usar tipos

complejos y realizar algo como verificar si existe una clave dentro de una matriz cuando realiza una combinación.

Tipos de unión
Mientras que la expresión de unión determina si se deben unir dos filas, el tipo de unión determina qué debe estar en el conjunto de resultados. Hay una variedad de

diferentes tipos de unión disponibles en Spark para que los use:

Combinaciones internas (mantener filas con claves que existen en los conjuntos de datos izquierdo y derecho)

Combinaciones externas (mantener filas con claves en los conjuntos de datos izquierdo o derecho)

Combinaciones externas izquierdas (mantener filas con claves en el conjunto de datos izquierdo)

Combinaciones externas derechas (mantener filas con claves en el conjunto de datos correcto)

Semiuniones izquierdas (mantenga las filas a la izquierda, y solo a la izquierda, conjunto de datos donde aparece la clave en el conjunto de

datos derecho)

Combinaciones anti izquierdas (mantenga las filas en el conjunto de datos de la izquierda, y solo en la izquierda, donde no aparecen en el

conjunto de datos de la derecha)


Machine Translated by Google

Combinaciones naturales (realice una combinación haciendo coincidir implícitamente las columnas entre los dos conjuntos

de datos con los mismos nombres)

Uniones cruzadas (o cartesianas) (haga coincidir cada fila del conjunto de datos de la izquierda con cada fila del conjunto de

datos de la derecha)

Si alguna vez ha interactuado con un sistema de base de datos relacional, o incluso con una hoja de cálculo de Excel, el concepto de unir

diferentes conjuntos de datos no debería ser demasiado abstracto. Pasemos a mostrar ejemplos de cada tipo de combinación. Esto

facilitará la comprensión exacta de cómo puede aplicarlos a sus propios problemas. Para hacer esto, creemos algunos conjuntos de datos

simples que podemos usar en nuestros ejemplos:

// en escala
val persona = Seq(
(0, "Bill Chambers", 0, Seq(100)),
(1, "Matei Zaharia", 1, Seq(500, 250, 100)),
(2, "Michael Armbrust", 1, Seq(250, 100)))
.toDF("id", "nombre", "programa_graduado", "spark_status")
val programagraduado = Seq(
(0, "Maestría", "Escuela de Información", "UC Berkeley"), (2,
"Maestría", "EECS", "UC Berkeley"), (1,
"Ph.D.", "EECS", "UC Berkeley")) .toDF("id",
"título", "departamento", "escuela")
val chispaEstado = Seq(
(500, "Vicepresidente"),
(250, "Miembro de PMC"),
(100, "Colaborador"))
.toDF("id", "estado")

# en Python
persona = chispa.createDataFrame([
(0, "Bill Chambers", 0, [100]),
(1, "Matei Zaharia", 1, [500, 250, 100]),
(2, "Michael Armbrust", 1, [250, 100])])\
.toDF("id", "nombre", "programa_graduado", "spark_status")
programagraduado = chispa.createDataFrame([ (0,
"Maestría", "Escuela de Información", "UC Berkeley"), (2, "Maestría
", "EECS", "UC Berkeley"), (1, "Ph.D.",
"EECS", "UC Berkeley")])\ .toDF("id", "título",
"departamento", " escuela") sparkStatus =
spark.createDataFrame([ (500,
"Vicepresidente"), (250,
"Miembro de PMC"),
(100, "Colaborador")])\
.toDF("id", "estado")

A continuación, registremos estos como tablas para que los usemos a lo largo del capítulo:

persona.createOrReplaceTempView("persona")
programagraduado.createOrReplaceTempView("programagraduado")
sparkStatus.createOrReplaceTempView("sparkStatus")
Machine Translated by Google

Uniones internas

Las combinaciones internas evalúan las claves en ambos DataFrames o tablas e incluyen (y unen) solo las filas que se evalúan como

verdaderas. En el siguiente ejemplo, unimos el DataFrame del programa graduado con el DataFrame de la persona para crear un nuevo

DataFrame:

// en Scala
val joinExpression = persona.col("programa_graduado") === Programagraduado.col("id")

# en Python
joinExpression = persona["programa_graduado"] == programagraduado['id']

Las claves que no existen en ambos DataFrames no se mostrarán en el DataFrame resultante. Por ejemplo, la siguiente expresión

daría como resultado valores cero en el DataFrame resultante:

// en Scala
val expresión de unión incorrecta = persona.col("nombre") === programagraduado.col("escuela")

# en Python
expresión incorrecta de unión = persona ["nombre"] == programa graduado ["escuela"]

Las uniones internas son las uniones predeterminadas, por lo que solo necesitamos especificar nuestro marco de datos izquierdo y unirnos a

la derecha en la expresión JOIN:

person.join(ProgramaGraduado, expresiónUnirse).Mostrar()

­­ en SQL
SELECCIONE * DE persona ÚNASE al programa de posgrado
ON persona.graduado_programa = graduadoPrograma.id
Machine Translated by Google

+­­­+­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­+­­­­­­­­­­­ ­­­­+­­­+­­­­­­­+­­­­­­­­­­+­­­
| identificación | nombre|programa_graduado| chispa_estado| identificación | grado|departamento|...
+­­­+­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­+­­­­­­­­­­­ ­­­­+­­­+­­­­­­­+­­­­­­­­­­+­­­
| 0| Cámaras de Bill| 0| [100]| 0|Maestrías| Escuela...|...
| 1| Matei Zaharia| 1|[500, 250, 100]| 1| Doctorado| 1| [250, 100]| EECS|...
| 2|Michael Armbrust| 1| Doctorado| EECS|...
+­­­+­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­+­­­­­­­­­­­ ­­­­+­­­+­­­­­­­+­­­­­­­­­­+­­­

También podemos especificar esto explícitamente pasando un tercer parámetro, el tipo de unión:

// en escala
var joinType = "interior"

# en pitón
joinType = "interior"

person.join(ProgramaGraduado, ExpresiónUnirse, TipoUnirse).Mostrar()

­­ en SQL
SELECCIONE * DE persona INNER JOIN programa graduado
ON persona.graduado_programa = graduadoPrograma.id

+­­­+­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­+­­­­­­­­­­­ ­­­­+­­­+­­­­­­­+­­­­­­­­­­­­­­
| identificación | nombre|programa_graduado| chispa_estado| identificación | grado| departamento...
+­­­+­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­+­­­­­­­­­­­ ­­­­+­­­+­­­­­­­+­­­­­­­­­­­­­­
| 0| Cámaras de Bill| 0| [100]| 0|Maestrías| 1|[500, 250, 100]| 1| Escuela...
| 1| Matei Zaharia| Doctorado| 1| [250, 100]| 1| Doctorado| EECS...
| 2|Michael Armbrust| EECS...
+­­­+­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­+­­­­­­­­­­­ ­­­­+­­­+­­­­­­­+­­­­­­­­­­­­­­

Uniones externas

Las uniones externas evalúan las claves en ambos DataFrames o tablas e incluye (y une
juntas) las filas que se evalúan como verdaderas o falsas. Si no hay una fila equivalente en el lado izquierdo o
DataFrame derecho, Spark insertará nulo:

joinType = "exterior"

person.join(ProgramaGraduado, ExpresiónUnirse, TipoUnirse).Mostrar()

­­ en SQL
SELECCIONE * DE persona FULL OUTER JOIN programa graduado
ON programa_graduado = programagraduado.id

+­­­­+­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­+­­­­­­­­­­ ­­­­­+­­­+­­­­­­­+­­­­­­­­­­­­­
| identificación | nombre|programa_graduado| chispa_estado| identificación | grado| departamento...
+­­­­+­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­+­­­­­­­­­­ ­­­­­+­­­+­­­­­­­+­­­­­­­­­­­­­
| 1| Matei Zaharia| 1|[500, 250, 100]| 1| Doctorado| CEE...
Machine Translated by Google

| 2|Michael Armbrust| |nulo| nulo| | 1| [250, 100]| 1| Doctorado| nulo| 2| CEE...


nulo| 0| Maestría| [100]| 0| CEE...
0| Cámaras de Bill| Maestría| Escuela...
+­­­­+­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­+­­­­­­­­­­ ­­­­­+­­­+­­­­­­­+­­­­­­­­­­­­­

Uniones exteriores izquierdas

Las uniones externas izquierdas evalúan las claves en ambos DataFrames o tablas e incluyen todas las filas de
el DataFrame izquierdo, así como cualquier fila en el DataFrame derecho que tenga una coincidencia en el izquierdo
Marco de datos. Si no hay una fila equivalente en el DataFrame derecho, Spark insertará un valor nulo:

joinType = "izquierda_exterior"

graduateProgram.join(persona, joinExpression, joinType).show()

­­ en SQL
SELECT * FROM graduateProgram LEFT OUTER JOIN persona
ON persona.graduado_programa = graduadoPrograma.id

+­­­+­­­­­­­+­­­­­­­­­­+­­­­­­­­­­­+­­­­+­­­­­­­­­ ­­­­­­­+­­­­­­­­­­­­­­­­+­­­
| identificación | grado|departamento| escuela| identificación | nombre|programa_graduado|...
+­­­+­­­­­­­+­­­­­­­­­­+­­­­­­­­­­­+­­­­+­­­­­­­­­ ­­­­­­­+­­­­­­­­­­­­­­­­+­­­
| 0|Maestrías| Escuela...|UC Berkeley| 0| Cámaras de Bill| | 2|Maestrías| EECS|UC 0|...
Berkeley|null| nulo| | 1| Doctorado| EECS|UC Berkeley| 2|Michael Armbrust| | 1| nulo|...
Doctorado| EECS|UC Berkeley| 1| Matei Zaharia| 1|...
1|...
+­­­+­­­­­­­+­­­­­­­­­­+­­­­­­­­­­­+­­­­+­­­­­­­­­ ­­­­­­­+­­­­­­­­­­­­­­­­+­­­

Uniones externas derechas

Las uniones externas derechas evalúan las claves en ambos DataFrames o tablas e incluyen todas las filas
desde el DataFrame derecho, así como cualquier fila en el DataFrame izquierdo que tenga una coincidencia en el derecho
Marco de datos. Si no hay una fila equivalente en el DataFrame izquierdo, Spark insertará un valor nulo:

joinType = "right_outer"

person.join(ProgramaGraduado, ExpresiónUnirse, TipoUnirse).Mostrar()

­­ en SQL
SELECCIONE * DE persona DERECHA EXTERNA ÚNASE programa graduado
ON persona.graduado_programa = graduadoPrograma.id

+­­­­+­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­+­­­­­­­­­­ ­­­­­+­­­+­­­­­­­+­­­­­­­­­­­­+
| identificación | nombre|programa_graduado| chispa_estado| identificación | grado| departamento|
+­­­­+­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­+­­­­­­­­­­ ­­­­­+­­­+­­­­­­­+­­­­­­­­­­­­+
| 0| Cámaras de Bill| |nulo| 0| [100]| 0|Maestrías|Escuela de...|
nulo| nulo| 1| nulo| 2|Maestrías| EECS|
| 2|Michael Armbrust| [250, 100]| 1| Doctorado| EECS|
Machine Translated by Google

| 1| Matei Zaharia| 1|[500, 250, 100]| 1| Doctorado| EECS|


+­­­­+­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­+­­­­­­­­­­ ­­­­­+­­­+­­­­­­­+­­­­­­­­­­­­+

Semiuniones izquierdas

Las uniones semi son un poco diferentes a las otras uniones. En realidad, no incluyen ningún valor.
desde el DataFrame derecho. Solo comparan valores para ver si el valor existe en el segundo
Marco de datos. Si el valor existe, esas filas se mantendrán en el resultado, incluso si hay
claves duplicadas en el DataFrame izquierdo. Piense en las semiuniones izquierdas como filtros en un DataFrame, como
opuesto a la función de una unión convencional:

joinType = "left_semi"

graduateProgram.join(persona, joinExpression, joinType).show()

+­­­+­­­­­­­+­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­+
| identificación | grado| departamento| escuela|
+­­­+­­­­­­­+­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­+
| 0|Maestría|Escuela de Informa...|UC Berkeley|
| 1| Doctorado| EECS|UC Berkeley|
+­­­+­­­­­­­+­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­+

// en escala
val ProgramaGraduado2 = ProgramaGraduado.union(Seq(
(0, "Máster", "Fila duplicada", " Escuela duplicada").toDF())

programagraduado2.createOrReplaceTempView("programagraduado2")

# en pitón
programagraduado2 = programagraduado.union(spark.createDataFrame([
(0, "Máster", "Fila duplicada", "Escuela duplicada")]))

programagraduado2.createOrReplaceTempView("programagraduado2")

gradProgram2.join(persona, joinExpression, joinType).show()

­­ en SQL
SELECCIONE * DESDE gradProgram2 SEMI IZQUIERDO UNIRSE persona
ON gradProgram2.id = persona.graduado_programa

+­­­+­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­ ­+
| identificación | grado| departamento| escuela|
+­­­+­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­ ­+
| 0|Maestrías|Escuela de Informa...| | 1| Doctorado| Universidad de California en Berkeley|

EECS| | 0|Maestrías| Universidad de California en Berkeley|

Fila duplicada|Escuela duplicada|


+­­­+­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­ ­+
Machine Translated by Google

Combinaciones anti izquierdas

Las anti­uniones izquierdas son lo opuesto a las semi­uniones izquierdas. Al igual que las semiuniones izquierdas,
en realidad no incluyen ningún valor del DataFrame derecho. Solo comparan valores para ver si el valor existe en el
segundo DataFrame. Sin embargo, en lugar de mantener los valores que existen en el segundo DataFrame, solo
mantienen los valores que no tienen una clave correspondiente en el segundo DataFrame. Piense en las uniones
anti como un filtro de estilo NO EN SQL:

joinType = "left_anti"
graduateProgram.join(persona, joinExpression, joinType).show()

­­ en SQL
SELECT * FROM graduateProgram LEFT ANTI JOIN person
ON graduateProgram.id = person.graduate_program

+­­­+­­­­­­­+­­­­­­­­­­+­­­­­­­­­­­+
| identificación | grado|departamento| escuela|
+­­­+­­­­­­­+­­­­­­­­­­+­­­­­­­­­­­+
| 2|Maestrías| EECS|UC Berkeley|
+­­­+­­­­­­­+­­­­­­­­­­+­­­­­­­­­­­+

Uniones naturales

Las uniones naturales hacen conjeturas implícitas en las columnas en las que le gustaría unirse. Encuentra
columnas coincidentes y devuelve los resultados. Se admiten las uniones naturales izquierda, derecha y exterior.

ADVERTENCIA

¡Lo implícito siempre es peligroso! La siguiente consulta nos dará resultados incorrectos porque los dos
DataFrames/tables comparten un nombre de columna (id), pero significa cosas diferentes en los conjuntos de datos.
Siempre debe usar esta combinación con precaución.

­­ en SQL
SELECT * FROM graduateProgram NATURAL JOIN persona

Uniones cruzadas (cartesianas)


Las últimas de nuestras uniones son uniones cruzadas o productos cartesianos. Las uniones cruzadas en términos más
simples son uniones internas que no especifican un predicado. Las uniones cruzadas unirán cada fila en el marco de
datos izquierdo con cada fila en el marco de datos derecho. Esto provocará una explosión absoluta en la cantidad de filas
contenidas en el DataFrame resultante. Si tiene 1,000 filas en cada DataFrame, la combinación cruzada de estos dará
como resultado 1,000,000 (1,000 x 1,000) filas. Por esta razón, debe indicar muy explícitamente que desea una
combinación cruzada utilizando la palabra clave de combinación cruzada:
Machine Translated by Google

joinType = "cross"
graduateProgram.join(persona, joinExpression, joinType).show()

­­ en SQL
SELECT * FROM graduateProgram CROSS JOIN persona
ON programagraduado.id = persona.programa_graduado
Machine Translated by Google

+­­­+­­­­­­­+­­­­­­­­­­+­­­­­­­­­­­+­­­+­­­­­­­­­ ­­­­­­+­­­­­­­­­­­­­­­­­+­­­­­­­
| identificación | grado|departamento| escuela| identificación | nombre|programa_graduado|spar...
+­­­+­­­­­­­+­­­­­­­­­­+­­­­­­­­­­­+­­­+­­­­­­­­­ ­­­­­­+­­­­­­­­­­­­­­­­­+­­­­­­­
| 0|Maestrías| Escuela...|UC Berkeley| 0| Cámaras de Bill| | 1| ...
Doctorado| EECS|UC Berkeley| 2|Michael Armbrust| | 1| Doctorado| 0| 1| [2...
EECS|UC Berkeley| 1| Matei Zaharia| 1|[500...
+­­­+­­­­­­­+­­­­­­­­­­+­­­­­­­­­­­+­­­+­­­­­­­­­ ­­­­­­+­­­­­­­­­­­­­­­­­+­­­­­­­

Si realmente tiene la intención de tener una combinación cruzada, puede llamarlo explícitamente:

person.crossJoin(programa de posgrado).show()

­­ en SQL
SELECT * FROM graduateProgram CROSS JOIN persona

+­­­+­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­+­­­­­­­­­­­ ­­­­+­­­+­­­­­­­+­­­­­­­­­­­­­+
| identificación | nombre|programa_graduado| chispa_estado| identificación | grado| departamento...|
+­­­+­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­+­­­­­­­­­­­ ­­­­+­­­+­­­­­­­+­­­­­­­­­­­­­+
| 0| Cámaras de Bill| 0| [100]| 0|Maestrías| Escuela...|
...
| 1| Matei Zaharia| 1|[500, 250, 100]| 0|Maestrías| Escuela...|
...
| 2|Michael Armbrust| 1| [250, 100]| 0|Maestrías| Escuela...|
...
+­­­+­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­+­­­­­­­­­­­ ­­­­+­­­+­­­­­­­+­­­­­­­­­­­­­+

ADVERTENCIA

Debe usar uniones cruzadas solo si está absolutamente seguro al 100 por ciento de que esta es la unión que necesita.
Hay una razón por la que debe ser explícito al definir una unión cruzada en Spark. ¡Son peligrosos!
Los usuarios avanzados pueden establecer la configuración de nivel de sesión spark.sql.crossJoin.enable en verdadero en
para permitir combinaciones cruzadas sin advertencias o sin que Spark intente realizar otra combinación por usted.

Desafíos al usar uniones


Al realizar uniones, hay algunos desafíos específicos y algunas preguntas comunes que
surgir. El resto del capítulo proporcionará respuestas a estas preguntas comunes y luego explicará
cómo, en un alto nivel, Spark realiza uniones. Esto indicará algunas de las optimizaciones que estamos
cubriremos en partes posteriores de este libro.

Uniones en tipos complejos


Aunque esto pueda parecer un desafío, en realidad no lo es. Cualquier expresión es una combinación válida
expresión, asumiendo que devuelve un valor booleano:
Machine Translated by Google

importar org.apache.spark.sql.functions.expr

persona.withColumnRenamed("id", "personId")
.join(sparkStatus, expr("array_contains(spark_status, id)")).show()

# en pitón
desde pyspark.sql.functions import expr

persona.withColumnRenamed("id", "personId")\
.join(sparkStatus, expr("array_contains(spark_status, id)")).show()

­­ en SQL
SELECCIONAR * DESDE

(seleccione id como personId, name, graduate_program, spark_status FROM person)


UNIÓN INTERNA sparkStatus ON array_contains (spark_status, id)

+­­­­­­­­+­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­+­­­­­­ ­­­­­­­­­+­­­+­­­­­­­­­­­­­­+
|personaId| nombre|programa_graduado| chispa_estado| identificación | estado|
+­­­­­­­­+­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­+­­­­­­ ­­­­­­­­­+­­­+­­­­­­­­­­­­­­+
| 0| Cámaras de Bill| 0| [100]|100| Colaborador|
| 1| Matei Zaharia| 1|[500, 250, 100]|500|Vicepresidente|
| 1| Matei Zaharia| 1|[500, 250, 100]|250| Miembro de PMC|
| 1| Matei Zaharia| 1|[500, 250, 100]|100| Colaborador|
| 2|Michael Armbrust| 1| [250, 100]|250| Miembro de PMC|
| 2|Michael Armbrust| 1| [250, 100]|100| Colaborador|
+­­­­­­­­+­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­+­­­­­­ ­­­­­­­­­+­­­+­­­­­­­­­­­­­­+

Manejo de nombres de columna duplicados


Una de las cosas difíciles que surgen en las uniones es lidiar con nombres de columna duplicados en su
marco de datos de resultados. En un DataFrame, cada columna tiene una ID única dentro del motor SQL de Spark,
Catalizador. Esta identificación única es puramente interna y no es algo a lo que pueda hacer referencia directamente.
Esto hace que sea bastante difícil hacer referencia a una columna específica cuando tiene un DataFrame con
nombres de columna duplicados.

Esto puede ocurrir en dos situaciones distintas:

La expresión de combinación que especifica no elimina una clave de una de las entradas
DataFrames y las claves tienen el mismo nombre de columna

Dos columnas en las que no está realizando la unión tienen el mismo nombre

Vamos a crear un conjunto de datos de problemas que podamos usar para ilustrar estos problemas:

val gradProgramDupe = graduateProgram.withColumnRenamed("id", "graduate_program")

val joinExpr = gradProgramDupe.col("graduado_programa") === persona.col(


"programa de Graduados")

Tenga en cuenta que ahora hay dos columnas graduate_program, aunque nos unimos en esa clave:
Machine Translated by Google

person.join(gradProgramDupe, joinExpr).show()

El desafío surge cuando nos referimos a una de estas columnas:

person.join(gradProgramDupe, joinExpr).select("graduate_program").show()

Dado el fragmento de código anterior, recibiremos un error. En este ejemplo particular, Spark genera este
mensaje:

org.apache.spark.sql.AnalysisException: la referencia 'programa_graduado' es


ambigua, podría ser: programa_graduado#40, programa_graduado#1079.;

Enfoque 1: Expresión de combinación diferente

Cuando tiene dos claves que tienen el mismo nombre, probablemente la solución más sencilla sea cambiar la
expresión de unión de una expresión booleana a una cadena o secuencia. Esto elimina automáticamente una de
las columnas durante la unión:

person.join(gradProgramDupe,"graduado_programa").select("graduado_programa").show()

Enfoque 2: soltar la columna después de la unión

Otro enfoque es descartar la columna infractora después de la unión. Al hacer esto, debemos referirnos a la
columna a través del DataFrame de origen original. Podemos hacer esto si la combinación usa los mismos
nombres de clave o si los DataFrames de origen tienen columnas que simplemente tienen el mismo nombre:

person.join(gradProgramDupe,
joinExpr).drop(person.col("graduado_programa")) .select("graduado_programa").show()

val joinExpr = persona.col("programa_graduado") === Programagraduado.col("id")


persona.join(Programagraduado, unirseExpr).drop(Programagraduado.col("id")).show()

Este es un artefacto del proceso de análisis de SQL de Spark en el que una columna a la que se hace referencia
explícitamente pasará el análisis porque Spark no necesita resolver la columna. Observe cómo la columna usa
el método .col en lugar de una función de columna. Eso nos permite especificar implícitamente esa columna
por su ID específico.

Enfoque 3: Cambiar el nombre de una columna antes de la unión

Podemos evitar este problema por completo si cambiamos el nombre de una de nuestras columnas antes de la unión:

val gradProgram3 = gradProgram.withColumnRenamed("id", "grad_id") val joinExpr


= persona.col("graduado_programa") === gradProgram3.col("grad_id") person.join(gradProgram3,
joinExpr).show()

Cómo realiza Spark las uniones


Machine Translated by Google

Para comprender cómo Spark realiza las uniones, debe comprender los dos recursos principales en juego: la
estrategia de comunicación de nodo a nodo y la estrategia de cálculo por nodo. Es probable que estos elementos
internos sean irrelevantes para su problema comercial. Sin embargo, comprender cómo Spark realiza las
uniones puede significar la diferencia entre un trabajo que se completa rápidamente y uno que nunca se
completa.

Estrategias de comunicación
Spark aborda la comunicación del clúster de dos maneras diferentes durante las uniones. Incurre en una
combinación aleatoria, lo que da como resultado una comunicación de todos a todos o una combinación de
difusión. Tenga en cuenta que hay muchos más detalles de los que revelamos en este momento, y eso es
intencional. Es probable que algunas de estas optimizaciones internas cambien con el tiempo con nuevas
mejoras en el optimizador basado en costos y mejores estrategias de comunicación. Por esta razón, nos
centraremos en los ejemplos de alto nivel para ayudarlo a comprender exactamente lo que está
sucediendo en algunos de los escenarios más comunes, y permitirle aprovechar algunas de las frutas maduras
que puede utilizar. de distancia para tratar de acelerar algunas de sus cargas de trabajo.

La base fundamental de nuestra vista simplificada de combinaciones es que en Spark tendrá una mesa
grande o una mesa pequeña. Aunque esto es obviamente un espectro (y las cosas suceden de manera diferente
si tiene una "mesa de tamaño mediano"), puede ser útil ser binario acerca de la distinción por el bien de esta
explicación.

Mesa grande a mesa grande

Cuando une una mesa grande con otra mesa grande, termina con una combinación aleatoria, como la
que se ilustra en la Figura 8­1.
Machine Translated by Google

Figura 8­1. Uniendo dos mesas grandes

En una unión aleatoria, cada nodo habla con todos los demás nodos y comparten datos de acuerdo con
qué nodo tiene una determinada clave o conjunto de claves (en las que se está uniendo). Estas uniones
son costosas porque la red puede congestionarse con el tráfico, especialmente si sus datos no están bien
particionados.

Esta combinación describe tomar una gran tabla de datos y unirla a otra gran tabla de datos. Un ejemplo
de esto podría ser una empresa que recibe miles de millones de mensajes todos los días del Internet de
las cosas y necesita identificar los cambios que se han producido día tras día. La forma de hacerlo es
uniéndose a deviceId, messageType y date en una columna, y date ­ 1 day en la otra columna.

En la Figura 8­1, DataFrame 1 y DataFrame 2 son DataFrames grandes. Esto significa que todos los nodos
trabajadores (y potencialmente todas las particiones) deberán comunicarse entre sí durante todo el proceso de
unión (sin partición inteligente de datos).
Machine Translated by Google

Mesa grande a mesa pequeña

Cuando la tabla es lo suficientemente pequeña como para caber en la memoria de un solo nodo de trabajo,
con algo de espacio para respirar, por supuesto, podemos optimizar nuestra unión. Aunque podemos usar una
estrategia de comunicación de mesa grande a mesa grande, a menudo puede ser más eficiente usar una unión
de transmisión. Lo que esto significa es que replicaremos nuestro pequeño DataFrame en cada nodo de trabajo en
el clúster (ya sea que esté ubicado en una máquina o en muchas). Ahora esto suena caro. Sin embargo, lo
que esto hace es evitar que realicemos la comunicación de todos a todos durante todo el proceso de unión. En su
lugar, lo realizamos solo una vez al principio y luego dejamos que cada nodo de trabajo individual realice el trabajo
sin tener que esperar o comunicarse con ningún otro nodo de trabajo, como se muestra en la Figura 8­2.

Figura 8­2. Unirse a la transmisión

Al comienzo de esta unión habrá una gran comunicación, al igual que en el tipo de unión anterior.
Sin embargo, inmediatamente después de ese primero, no habrá más comunicación entre los nodos.
Esto significa que las uniones se realizarán en cada nodo individualmente, lo que convierte a la CPU en el
mayor cuello de botella. Para nuestro conjunto de datos actual, podemos ver que Spark lo configuró automáticamente
como una unión de transmisión mirando el plan de explicación:

val joinExpr = persona.col("programa_graduado") === Programagraduado.col("id")

person.join(programagraduado, joinExpr).explain()

== Plan físico ==
*BroadcastHashJoin [graduate_program#40], [id#5....
:­ LocalTableScan [id#38, name#39, graduate_progr...
+­ BroadcastExchange HashedRelationBroadcastMode(....
Machine Translated by Google

+­ LocalTableScan [id#56, grado#57, departamento....

Con la API de DataFrame, también podemos darle explícitamente al optimizador una pista de que nos gustaría
usar una unión de transmisión usando la función correcta alrededor del pequeño DataFrame en cuestión. En este
ejemplo, estos dan como resultado el mismo plan que acabamos de ver; Sin embargo, este no es siempre el caso:

import org.apache.spark.sql.functions.broadcast val


joinExpr = persona.col("programa_graduado") === programagraduado.col("id")
person.join(emisión(programagraduado), joinExpr).explain()

La interfaz SQL también incluye la capacidad de proporcionar sugerencias para realizar uniones. Sin embargo,
estos no se aplican , por lo que el optimizador puede optar por ignorarlos. Puede establecer una de estas sugerencias
utilizando una sintaxis de comentario especial. MAPJOIN, BROADCAST y BROADCASTJOIN hacen lo mismo y
son compatibles:

­­ en SQL
SELECT /*+ MAPJOIN(programagraduado) */ * FROM persona JOIN programagraduado
ON persona.programa_graduado = Programagraduado.id

Esto tampoco es gratis: si intenta transmitir algo demasiado grande, puede bloquear su nodo de controlador (porque
esa recopilación es costosa). Esta es probablemente un área de optimización en el futuro.

Mesita a mesita

Al realizar uniones con tablas pequeñas, generalmente es mejor dejar que Spark decida cómo unirlas.
Siempre puede forzar una unión de transmisión si nota un comportamiento extraño.

Conclusión
En este capítulo, discutimos las uniones, probablemente uno de los casos de uso más comunes. Una cosa que no
mencionamos pero que es importante tener en cuenta es que si divide sus datos correctamente antes de una unión,
puede terminar con una ejecución mucho más eficiente porque incluso si se planea una mezcla, si los datos de dos
DataFrames diferentes ya están ubicados en la misma máquina, Spark puede evitar la reproducción aleatoria.
Experimente con algunos de sus datos e intente particionar de antemano para ver si puede notar el aumento en
la velocidad al realizar esas uniones. En el Capítulo 9, analizaremos las API de origen de datos de Spark. Hay
implicaciones adicionales cuando decide en qué orden deben ocurrir las uniones. Debido a que algunas uniones
actúan como filtros, esto puede ser una mejora de bajo costo en sus cargas de trabajo, ya que tiene la
garantía de reducir el intercambio de datos a través de la red.

El próximo capítulo se apartará de la manipulación del usuario, como hemos visto en los últimos capítulos, y tocará
la lectura y escritura de datos usando las API estructuradas.
Machine Translated by Google

Capítulo 9. Fuentes de datos

Este capítulo presenta formalmente la variedad de otras fuentes de datos que puede usar con Spark de forma inmediata, así como
las innumerables otras fuentes creadas por la gran comunidad. Spark tiene seis fuentes de datos "básicas" y cientos de
fuentes de datos externas escritas por la comunidad. Podría decirse que la capacidad de leer y escribir desde todos los tipos
diferentes de fuentes de datos y que la comunidad cree sus propias contribuciones es una de las mayores fortalezas de Spark.
Los siguientes son los datos básicos de Spark
fuentes:

CSV

JSON

Parquet

ORCO

Conexiones JDBC/ODBC

Archivos de texto sin formato

Como se mencionó, Spark tiene numerosas fuentes de datos creadas por la comunidad. He aquí sólo una pequeña muestra:

casandra

HBase

MongoDB

Desplazamiento al rojo de AWS

XML

Y muchos, muchos otros

El objetivo de este capítulo es brindarle la capacidad de leer y escribir desde las fuentes de datos centrales de Spark y
saber lo suficiente para comprender lo que debe buscar cuando se integra con fuentes de datos de terceros. Para lograr esto,
nos centraremos en los conceptos básicos que debe poder reconocer y comprender.

La estructura de la API de fuentes de datos


Antes de continuar con cómo leer y escribir desde ciertos formatos, visitemos la estructura organizativa general de las
API de origen de datos.

Leer la estructura de la API


Machine Translated by Google

La estructura central para la lectura de datos es la siguiente:

DataFrameReader.format(...).option("clave", "valor").schema(...).load()

Usaremos este formato para leer de todas nuestras fuentes de datos. El formato es opcional porque, de forma
predeterminada, Spark utilizará el formato Parquet. La opción le permite establecer configuraciones de clave­valor
para parametrizar cómo leerá los datos. Por último, el esquema es opcional si la fuente de datos proporciona un
esquema o si pretende utilizar la inferencia de esquema. Naturalmente, hay algunas opciones requeridas para cada
formato, que discutiremos cuando veamos cada formato.

NOTA

Hay mucha notación abreviada en la comunidad de Spark, y la API de lectura de fuente de datos no
es una excepción. Tratamos de ser coherentes a lo largo del libro sin dejar de revelar algo de la notación
abreviada en el camino.

Fundamentos de la lectura de datos

La base para leer datos en Spark es DataFrameReader. Accedemos a esto a través de SparkSession a través del
atributo de lectura:

chispa.leer

Después de tener un lector de DataFrame, especificamos varios valores:

el formato

el esquema

El modo de lectura

Una serie de opciones

El formato, las opciones y el esquema devuelven un DataFrameReader que puede sufrir más transformaciones
y son opcionales, excepto una opción. Cada fuente de datos tiene un conjunto específico de opciones que determinan
cómo se leen los datos en Spark (cubriremos estas opciones en breve). Como mínimo, debe proporcionar a
DataFrameReader una ruta desde la que leer.

Aquí hay un ejemplo del diseño general:

chispa.read.format("csv")
.option("modo",
"FAILFAST") .option("inferSchema",
"true") .option("ruta", "ruta/al/

archivo(s)") .schema(someSchema) .load()


Machine Translated by Google

Hay una variedad de formas en las que puede establecer opciones; por ejemplo, puede crear un mapa y pasar sus configuraciones.
Por ahora, nos ceñiremos a la forma simple y explícita que acabas de ver.

Modos de lectura

Leer datos de una fuente externa naturalmente implica encontrar datos con formato incorrecto, especialmente cuando se trabaja
solo con fuentes de datos semiestructuradas. Los modos de lectura especifican lo que sucederá cuando Spark encuentre
registros con formato incorrecto. La Tabla 9­1 enumera los modos de lectura.

Tabla 9­1. Modos de lectura de Spark

Modo de lectura Descripción

Establece todos los campos en nulo cuando encuentra un registro corrupto y coloca todos los registros corruptos
permisivo
en una columna de cadena llamada _corrupt_record

dropMalformed Elimina la fila que contiene registros con formato incorrecto

Fallar rapido Falla inmediatamente al encontrar registros con formato incorrecto

El valor predeterminado es permisivo.

Escritura de la estructura de la API

La estructura central para escribir datos es la siguiente:

DataFrameWriter.format(...).option(...).partitionBy(...).bucketBy(...).sortBy(
...).ahorrar()

Usaremos este formato para escribir en todas nuestras fuentes de datos. El formato es opcional porque, de forma predeterminada,

Spark utilizará el formato arquet. La opción, nuevamente, nos permite configurar cómo escribir nuestros datos dados.

PartitionBy, bucketBy y sortBy funcionan solo para orígenes de datos basados en archivos; puede usarlos para controlar el diseño
específico de los archivos en el destino.

Fundamentos de la escritura de datos

La base para escribir datos es bastante similar a la de leer datos. En lugar de DataFrameReader, tenemos

DataFrameWriter. Debido a que siempre necesitamos escribir alguna fuente de datos dada, accedemos a DataFrameWriter por

cada DataFrame a través del atributo de escritura:

// en marco de
datos de Scala.escribir

Después de tener un DataFrameWriter, especificamos tres valores: el formato, una serie de opciones y el modo de guardado.

Como mínimo, debe proporcionar una ruta. Cubriremos el potencial de


Machine Translated by Google

opciones, que varían de una fuente de datos a otra, en breve.

// en Scala
dataframe.write.format("csv") .option("modo",
"OVERWRITE") .option("dateFormat",
"yyyy­MM­dd") .option("ruta", "ruta/ a/
archivo(s)") .save()

Guardar modos

Los modos de guardado especifican lo que sucederá si Spark encuentra datos en la ubicación especificada (suponiendo que todo lo
demás sea igual). La Tabla 9­2 enumera los modos de guardado.

Tabla 9­2. Modos de guardado de Spark

Modo de guardado Descripción

adjuntar Agrega los archivos de salida a la lista de archivos que ya existen en esa ubicación

Sobrescribir Sobrescribirá por completo cualquier dato que ya exista allí

errorIfExists Lanza un error y falla la escritura si los datos o archivos ya existen en la ubicación especificada

ignorar Si existen datos o archivos en la ubicación, no haga nada con el DataFrame actual

El valor predeterminado es errorIfExists. Esto significa que si Spark encuentra datos en la ubicación en la que está escribiendo, la
escritura fallará inmediatamente.

Hemos cubierto en gran medida los conceptos básicos que necesitará al usar fuentes de datos, así que ahora profundicemos en cada
una de las fuentes de datos nativas de Spark.

Archivos CSV

CSV significa valores separados por comas. Este es un formato de archivo de texto común en el que cada línea representa un solo
registro y las comas separan cada campo dentro de un registro. Los archivos CSV, aunque parecen estar bien estructurados, son
en realidad uno de los formatos de archivo más complicados que encontrará porque no se pueden hacer muchas suposiciones en los
escenarios de producción sobre lo que contienen o cómo están estructurados. Por ello, el lector de CSV dispone de una gran cantidad de
opciones. Estas opciones le brindan la capacidad de solucionar problemas como ciertos caracteres que deben escaparse, por ejemplo,
comas dentro de las columnas cuando el archivo también está delimitado por comas o valores nulos etiquetados de una manera no
convencional.

Opciones CSV
La Tabla 9­3 presenta las opciones disponibles en el lector CSV.

Tabla 9­3. Opciones de fuente de datos CSV


Machine Translated by Google

Tabla 9­3. Opciones de fuente de datos CSV

Potencial
Clave de lectura/escritura Por defecto Descripción
valores

El único personaje que es


Cualquier cadena individual
Ambos sep , utilizado como separador para cada
personaje
campo y valor.

Una bandera booleana que


declara si el primero
Ambos encabezamiento cierto, falso FALSO
línea en el(los) archivo(s) son los
nombres de las columnas.

El personaje Spark debería


Cualquier cadena
Leer escapar \ utilizar para escapar de otros
personaje
caracteres en el archivo.

Especifica si Spark
Leer inferSchema cierto, falso FALSO debe inferir tipos de columna
al leer el archivo.

Declara si conduce
Leer ignorarLeadingWhiteSpace cierto, falso FALSO espacios de valores siendo
se debe omitir la lectura.

Declara si se arrastra
Leer ignorarTrailingWhiteSpace cierto, falso FALSO espacios de valores siendo
se debe omitir la lectura.

declara qué carácter


Cualquier cadena “”
Ambos valor nulo
representa un valor nulo en
personaje
el archivo.

declara qué carácter

nanValor Cualquier cadena representa un NaN o


Ambos Yaya
personaje personaje faltante en el
archivo CSV.

Declara qué carácter(es)


Cualquier cadena o
Ambos INFPositivo información
representar un infinito positivo
personaje
valor.

Declara qué carácter(es)


Cualquier cadena o
Ambos INFnegativo ­Inf. representar un negativo
personaje
valor infinito.

Ninguno,
sin comprimir,
Declara qué compresión
bzip2, desinflar,
Ambos compresión o códec ninguno códec que debe usar Spark para
gzip, lz4 o
leer o escribir el archivo.
rápido
Machine Translated by Google

Cualquier cadena o
personaje que Declara el formato de fecha
Ambos formato de fecha De acuerdo a aaaa­MM­dd para cualquier columna que sea
java tipo de fecha
Formato de datos simple.

Cualquier cadena o
personaje que aaaa­MM Declara la marca de tiempo
Ambos formato de marca de tiempo De acuerdo a dd'T'HH:mm formato para cualquier columna
java :ss.SSSZZ que son del tipo de marca de tiempo.

Formato de datos simple.

declara el máximo
Leer maxColumns Cualquier número entero 20480 número de columnas en el
archivo.

declara el máximo
Leer maxCharsPerColumn Cualquier número entero 1000000 número de caracteres en un
columna.

Declara si Spark
Leer citas de escape cierto, falso verdadero debe escapar de las comillas que
se encuentran en filas.

Establece el número máximo

de filas mal formadas Spark


registrará para cada partición.
Leer maxMalformedLogPerPartition Cualquier entero 10
Registros mal formados más allá
este numero sera

ignorado

Especifica si todos
los valores deben estar encerrados
Escribir cotizartodo cierto, falso FALSO entre comillas, a diferencia de
simplemente escapando de valores que

tener un carácter de comillas.

Esta opción le permite


leer archivos CSV de varias líneas

donde cada fila lógica en


Leer multilínea cierto, falso FALSO
el archivo CSV puede abarcar
varias filas en el archivo
sí mismo.

Lectura de archivos CSV


Para leer un archivo CSV, como cualquier otro formato, primero debemos crear un DataFrameReader para ese
formato específico. Aquí, especificamos que el formato sea CSV:
Machine Translated by Google

chispa.read.format("csv")

Luego de esto, tenemos la opción de especificar un esquema así como modos como opciones. Pongamos un par de
opciones, algunas que vimos desde el principio del libro y otras que aún no hemos visto. Estableceremos el encabezado
en verdadero para nuestro archivo CSV, el modo en FAILFAST e inferSchema en verdadero:

// en Scala
chispa.leer.formato("csv")
.option("encabezado",
"verdadero") .option("modo",
"FAILFAST") .option("inferSchema",
"true") .load("alguna/ruta/al/archivo.csv")

Como se mencionó, podemos usar el modo para especificar cuánta tolerancia tenemos para los datos mal formados.
Por ejemplo, podemos usar estos modos y el esquema que creamos en el Capítulo 5 para garantizar que nuestros
archivos se ajusten a los datos que esperábamos:

// en Scala
import org.apache.spark.sql.types.{StructField, StructType, StringType, LongType} val myManualSchema
= new StructType(Array( new
StructField("DEST_COUNTRY_NAME", StringType, true), new
StructField("ORIGIN_COUNTRY_NAME" , StringType, true), new
StructField("count", LongType, false) ))

spark.read.format("csv")
.option("encabezado",
"verdadero") .option("modo",

"FAILFAST") .schema(myManualSchema) .load("/data/


flight­data/csv/2010­summary.csv") .show( 5)

Las cosas se complican cuando no esperamos que nuestros datos estén en un formato determinado, pero de todos
modos se presenta de esa manera. Por ejemplo, tomemos nuestro esquema actual y cambiemos todos los tipos
de columna a LongType. Esto no coincide con el esquema real , pero Spark no tiene ningún problema con que lo hagamos.
El problema solo se manifestará cuando Spark realmente lea los datos. Tan pronto como comencemos nuestro trabajo de
Spark, fallará inmediatamente (después de ejecutar un trabajo) debido a que los datos no se ajustan al esquema
especificado:

// en Scala
val myManualSchema = new StructType(Array( new
StructField("DEST_COUNTRY_NAME", LongType, true), new
StructField("ORIGIN_COUNTRY_NAME", LongType, true), new
StructField("count", LongType, false) ))

chispa.read.format("csv")
.option("encabezado",
"verdadero") .option("modo", "FAILFAST")
Machine Translated by Google

.schema(myManualSchema) .load("/data/flight­data/csv/
2010­summary.csv") .take(5)

En general, Spark fallará solo en el momento de la ejecución del trabajo en lugar del momento de la definición de
DataFrame, incluso si, por ejemplo, apuntamos a un archivo que no existe. Esto se debe a la evaluación perezosa, un
concepto que aprendimos en el Capítulo 2.

Escribir archivos CSV


Al igual que con la lectura de datos, hay una variedad de opciones (enumeradas en la Tabla 9­3) para escribir datos cuando
escribimos archivos CSV. Este es un subconjunto de las opciones de lectura porque muchas no se aplican al escribir
datos (como maxColumns e inferSchema). Aquí hay un ejemplo:

// en Scala
val csvFile = spark.read.format("csv")
.option("encabezado", "verdadero").option("modo", "FAILFAST").schema(myManualSchema) .load("/
data/flight­data/csv/2010­summary.csv")

# en Python
csvFile = spark.read.format("csv")
\ .option("header", "true")
\ .option("mode", "FAILFAST")
\ .option("inferSchema", "true" )\ .load("/
datos/datos­de­vuelo/csv/2010­summary.csv")

Por ejemplo, podemos tomar nuestro archivo CSV y escribirlo como un archivo TSV con bastante facilidad:

// en Scala
csvFile.write.format("csv").mode("overwrite").option("sep", "\t") .save("/tmp/my­tsv­
file.tsv")

# en Python
csvFile.write.format("csv").mode("overwrite").option("sep", "\t")\ .save("/tmp/my­tsv­
file.tsv")

Cuando enumera el directorio de destino, puede ver que my­tsv­file es en realidad una carpeta con numerosos archivos
dentro:

$ ls /tmp/mi­archivo­tsv.tsv/

/tmp/mi­archivo­tsv.tsv/part­00000­35cf9453­1943­4a8c­9c82­9f6ea9742b29.csv

En realidad, esto refleja la cantidad de particiones en nuestro DataFrame en el momento en que lo escribimos. Si tuviéramos
que volver a particionar nuestros datos antes de esa fecha, terminaríamos con una cantidad diferente de archivos. Discutimos
esta compensación al final de este capítulo.
Machine Translated by Google

Archivos JSON

Aquellos que vienen del mundo de JavaScript probablemente estén familiarizados con la notación de objetos de
JavaScript, o JSON, como se le llama comúnmente. Hay algunas trampas cuando se trabaja con este tipo de datos
que vale la pena considerar antes de comenzar. En Spark, cuando nos referimos a archivos JSON, nos referimos a
archivos JSON delimitados por líneas . Esto contrasta con los archivos que tienen un gran objeto JSON o una matriz
por archivo.

La compensación delimitada por línea frente a multilínea se controla mediante una única opción: multilínea. Cuando
establece esta opción en verdadero, puede leer un archivo completo como un objeto json y Spark realizará el
trabajo de analizarlo en un DataFrame. JSON delimitado por líneas es en realidad un formato mucho más estable porque
le permite agregar a un archivo un nuevo registro (en lugar de tener que leer un archivo completo y luego escribirlo),
que es lo que le recomendamos que use. Otra razón clave de la popularidad de JSON delimitado por líneas es que los
objetos JSON tienen estructura y JavaScript (en el que se basa JSON) tiene al menos tipos básicos. Esto facilita el
trabajo porque Spark puede hacer más suposiciones en nuestro nombre sobre los datos. Notará que hay
significativamente menos opciones de las que vimos para CSV debido a los objetos.

Opciones JSON
La Tabla 9­4 enumera las opciones disponibles para el objeto JSON, junto con sus descripciones.

Tabla 9­4. Opciones de fuente de datos JSON

Valores
Clave de lectura/escritura Por defecto Descripción
potenciales

declara lo
Ninguno, que
sin comprimir, El códec de
Ambos compresión o códec bzip2, deflate, gzip, ninguno compresión que
debe usar Spark
lz4 o
para leer o escribir
rápido
el archivo.

Cualquier cadena Declara el formato


o carácter que se de fecha para
Ambos formato de fecha ajuste a aaaa­MM­dd cualquier
de Java columna que sea

Formato de datos simple. de tipo fecha.

declara el

Cualquier cadena formato de


o carácter que se marca de tiempo para

Ambos ajuste a aaaa­MM­dd'T'HH:mm:ss.SSSZZ cualquier columna


formato de marca de tiempo
de Java que sea del

Formato de datos simple. tipo de marca

de tiempo.
Machine Translated by Google

infiere todo

primitivo
Leer primitivaAsString cierto, falso FALSO
valores como

tipo de cadena

Ignora
Java/C++

estilo
Leer Permitir comentarios cierto, falso FALSO
comentar en
JSON
registros.

permite
no cotizado
Leer allowUnquotedFieldNames cierto, falso FALSO
campo JSON
nombres

Permite solo
cotizaciones en
Leer permitir comillas simples cierto, falso verdadero además de
doble

citas.

permite
principal
Leer allowNumericLeadingZeros cierto, falso FALSO ceros en
números

(por ejemplo, 00012).

permite
aceptando
cotización de todos
caracteres
Leer allowBackslashEscapingAnyCharacter verdadero, falso FALSO
usando
barra invertida

citando
mecanismo.

permite
renombrando el
nuevo campo

teniendo un
malformado

cadena creada
Valor de
Leer columnNameOfCorruptRecord Cualquier cadena por
chispa.sql.column&NombreDeRegistroCorrupto
permisivo
modo. Este
anulará
el

configuración
valor.
Machine Translated by Google

Permite para
lectura en
Leer multilínea cierto, falso FALSO línea
archivos
JSON delimitados.

Ahora bien, la lectura de un archivo JSON delimitado por líneas varía únicamente en el formato y las opciones que
especificamos:

chispa.read.format("json")

Lectura de archivos JSON


Veamos un ejemplo de lectura de un archivo JSON y comparemos las opciones que estamos viendo:

// en Scala
spark.read.format("json").option("mode", "FAILFAST").schema(myManualSchema)
.load("/data/flight­data/json/2010­summary.json").show(5)

# en Python
spark.read.format("json").option("modo", "FAILFAST")
\ .option("inferSchema", "true")\ .load("/
data/flight­data/json/ 2010­resumen.json").show(5)

Escribir archivos JSON


Escribir archivos JSON es tan simple como leerlos y, como es de esperar, la fuente de datos no importa. Por lo tanto, podemos
reutilizar el marco de datos CSV que creamos anteriormente para que sea la fuente de nuestro archivo JSON. Esto también
sigue las reglas que especificamos antes: se escribirá un archivo por partición y todo el DataFrame se escribirá como
una carpeta. También tendrá un objeto JSON por línea:

// en Scala
csvFile.write.format("json").mode("overwrite").save("/tmp/my­json­file.json")

# en Python
csvFile.write.format("json").mode("overwrite").save("/tmp/my­json­file.json")

$ ls /tmp/mi­archivo­json.json/

/tmp/mi­archivo­json.json/part­00000­tid­543....json

Archivos de parquet
Parquet es un almacén de datos orientado a columnas de código abierto que proporciona una variedad de
optimizaciones de almacenamiento, especialmente para cargas de trabajo de análisis. Proporciona compresión columnar, que
Machine Translated by Google

ahorra espacio de almacenamiento y permite leer columnas individuales en lugar de archivos completos. Es un formato de
archivo que funciona excepcionalmente bien con Apache Spark y, de hecho, es el formato de archivo predeterminado.
Recomendamos escribir datos en Parquet para almacenamiento a largo plazo porque la lectura de un archivo de Parquet
siempre será más eficiente que JSON o CSV. Otra ventaja de Parquet es que admite tipos complejos. Esto significa
que si su columna es una matriz (que fallaría con un archivo CSV, por ejemplo), un mapa o una estructura, aún podrá
leer y escribir ese archivo sin problemas. Aquí se explica cómo especificar Parquet como formato de lectura:

chispa.read.format("parquet")

Lectura de archivos de parquet

Parquet tiene muy pocas opciones porque impone su propio esquema al almacenar datos. Por lo tanto, todo lo que necesita
configurar es el formato y listo. Podemos establecer el esquema si tenemos requisitos estrictos sobre el aspecto
que debería tener nuestro DataFrame. A menudo, esto no es necesario porque podemos usar el esquema en lectura, que es
similar al inferSchema con archivos CSV. Sin embargo, con los archivos de Parquet, este método es más poderoso porque el
esquema está integrado en el propio archivo (por lo que no se necesita inferencia).

Aquí hay algunos ejemplos simples de lectura de parquet:

chispa.read.format("parquet")

// en Scala
chispa.leer.formato("parquet")
.load("/data/flight­data/parquet/2010­summary.parquet").show(5)

# en Python
spark.read.format("parquet")\
.load("/data/flight­data/parquet/2010­summary.parquet").show(5)

Opciones de parquet

Como acabamos de mencionar, hay muy pocas opciones de Parquet, precisamente dos, porque tiene una especificación bien
definida que se alinea estrechamente con los conceptos de Spark. La Tabla 9­5 presenta las opciones.

ADVERTENCIA

Aunque solo hay dos opciones, aún puede encontrar problemas si está trabajando con archivos de Parquet
incompatibles. Tenga cuidado cuando escriba archivos de Parquet con diferentes versiones de Spark
(especialmente las más antiguas) porque esto puede causar un dolor de cabeza significativo.

Tabla 9­5. Opciones de fuente de datos de parquet


Machine Translated by Google

Potencial Por defecto


Clave de lectura/escritura Descripción
Valores

Ninguno, sin comprimir, Declara qué códec de compresión


compresión
Escribir bzip2, desinflar, Ninguno debe usar Spark para leer o
o códec
gzip, lz4 o escribir el archivo.

rápido

Puede agregar columnas de


forma incremental al valor
recién escrito de los archivos de configuración de Parquet en la misma
Leer esquema de combinación verdadero, falso
spark.sql.parquet.mergeSchema para tabla/carpeta. Use esta opción
habilitar o deshabilitar esta
característica.

Escribir archivos de parquet


Escribir Parquet es tan fácil como leerlo. Simplemente especificamos la ubicación del archivo. Se aplican las
mismas reglas de partición:

// en Scala
csvFile.write.format("parquet").mode("overwrite") .save("/tmp/my­parquet­
file.parquet")

# en Python
csvFile.write.format("parquet").mode("overwrite")\ .save("/tmp/my­parquet­
file.parquet")

Archivos ORC
ORC es un formato de archivo en columnas con reconocimiento de tipos y autodescripción diseñado para las cargas
de trabajo de Hadoop. Está optimizado para grandes lecturas de transmisión, pero con soporte integrado
para encontrar rápidamente las filas requeridas. ORC en realidad no tiene opciones para leer datos porque
Spark entiende bastante bien el formato de archivo. Una pregunta frecuente es: ¿Cuál es la diferencia entre ORC y Parquet?
En su mayor parte, son bastante similares; la diferencia fundamental es que Parquet está aún más optimizado
para su uso con Spark, mientras que ORC está aún más optimizado para Hive.

Leer archivos de orcos


Aquí se explica cómo leer un archivo ORC en Spark:

// en Scala
spark.read.format("orc").load("/data/flight­data/orc/2010­summary.orc").show(5)

# en Python
spark.read.format("orc").load("/data/flight­data/orc/2010­summary.orc").show(5)
Machine Translated by Google

Escribir archivos orcos


En este punto del capítulo, debería sentirse bastante cómodo adivinando cómo escribir archivos ORC. Realmente
sigue exactamente el mismo patrón que hemos visto hasta ahora, en el que especificamos el formato y luego guardamos
el archivo:

// en Scala
csvFile.write.format("orc").mode("overwrite").save("/tmp/my­json­file.orc")

# en Python
csvFile.write.format("orc").mode("overwrite").save("/tmp/my­json­file.orc")

Bases de datos SQL


Las fuentes de datos SQL son uno de los conectores más poderosos porque hay una variedad de sistemas a los que
puede conectarse (siempre que ese sistema hable SQL). Por ejemplo, puede conectarse a una base de datos MySQL,
una base de datos PostgreSQL o una base de datos Oracle. También puede conectarse a SQLite, que es lo que
haremos en este ejemplo. Por supuesto, las bases de datos no son solo un conjunto de archivos sin formato, por lo que
hay más opciones a considerar con respecto a cómo se conecta a la base de datos. Es decir, deberá comenzar a
considerar cosas como la autenticación y la conectividad (deberá determinar si la red de su clúster Spark está
conectada a la red de su sistema de base de datos).

Para evitar la distracción de configurar una base de datos para los fines de este libro, proporcionamos un ejemplo
de referencia que se ejecuta en SQLite. Podemos omitir muchos de estos detalles usando SQLite, porque puede
funcionar con una configuración mínima en su máquina local con la limitación de no poder trabajar en una configuración
distribuida. Si desea trabajar con estos ejemplos en un entorno distribuido, querrá conectarse a otro tipo de base de datos.

UNA PRIMERA INFORMACIÓN SOBRE SQLITE

SQLite es el motor de base de datos más utilizado en todo el mundo y por una buena razón. Es potente,
rápido y fácil de entender. Esto se debe a que una base de datos SQLite es solo un archivo. Eso hará que sea muy
fácil ponerlo en marcha porque incluimos el archivo fuente en el repositorio oficial de este libro. Simplemente
descargue ese archivo a su máquina local y podrá leerlo y escribir en él. Estamos usando SQLite, pero todo el
código aquí también funciona con bases de datos relacionales más tradicionales, como MySQL. La principal
diferencia está en las propiedades que incluye cuando se conecta a la base de datos. Cuando estamos
trabajando con SQLite, no hay noción de usuario o contraseña.

ADVERTENCIA

Aunque SQLite es un buen ejemplo de referencia, probablemente no sea lo que quieras usar en
producción. Además, SQLite no funcionará necesariamente bien en un entorno distribuido debido a su
Machine Translated by Google

requisito para bloquear toda la base de datos en escritura. El ejemplo que presentamos aquí también funcionará de manera similar usando
MySQL o PostgreSQL.

Para leer y escribir desde estas bases de datos, debe hacer dos cosas: incluir el controlador Java Database
Connectivity (JDBC) para su base de datos en particular en el classpath de chispa y proporcionar el JAR adecuado
para el controlador en sí. Por ejemplo, para poder leer y escribir desde PostgreSQL, puede ejecutar algo como esto:

./bin/spark­shell \ ­­driver­class­
path postgresql­9.4.1207.jar \ ­­jars postgresql­9.4.1207.jar

Al igual que con nuestras otras fuentes, hay una serie de opciones disponibles al leer y escribir en bases de datos SQL.
Solo algunos de estos son relevantes para nuestro ejemplo actual, pero la Tabla 9­6 enumera todas las opciones
que puede configurar cuando trabaja con bases de datos JDBC.

Tabla 9­6. Opciones de fuente de datos JDBC

Nombre de la propiedad Significado

La URL de JDBC a la que conectarse. Las propiedades de conexión específicas de la fuente se pueden
URL especificar en la URL; por ejemplo, jdbc:postgresql://localhost/test? usuario=pedro&contraseña=secreto.

La tabla JDBC para leer. Tenga en cuenta que se puede usar cualquier cosa que sea válida en una cláusula
dbtable FROM de una consulta SQL. Por ejemplo, en lugar de una tabla completa, también podría usar una subconsulta
entre paréntesis.

conductor El nombre de clase del controlador JDBC que se usará para conectarse a esta URL.

Si se especifica alguna de estas opciones, también se deben configurar todas las demás. Además, se
debe especificar numPartitions . Estas propiedades describen cómo particionar la tabla cuando se lee en
paralelo de varios trabajadores. La columna de partición debe ser una columna numérica
columna de partición,
de la tabla en cuestión. Tenga en cuenta que lowerBound y upperBound se usan solo para decidir el paso
límite inferior, límite superior
de la partición, no para filtrar las filas de la tabla. Por lo tanto, todas las filas de la tabla se dividirán y devolverán.
Esta opción se aplica sólo a la lectura.

El número máximo de particiones que se pueden utilizar para el paralelismo en la lectura y escritura de
tablas. Esto también determina el número máximo de conexiones JDBC simultáneas. Si el número de
númParticiones
particiones para escribir excede este límite, lo reducimos a este límite llamando a coalesce(numPartitions)
antes de escribir.

El tamaño de recuperación de JDBC, que determina cuántas filas se deben recuperar por viaje de ida y vuelta.
tamaño de búsqueda Esto puede ayudar al rendimiento en los controladores JDBC, que por defecto tienen un tamaño de búsqueda
bajo (p. ej., Oracle con 10 filas). Esta opción se aplica sólo a la lectura.

El tamaño del lote de JDBC, que determina cuántas filas insertar por viaje de ida y vuelta.
Machine Translated by Google

tamaño del lote Esto puede ayudar al rendimiento de los controladores JDBC. Esta opción se aplica solo a la escritura.
El valor predeterminado es 1000.

El nivel de aislamiento de la transacción, que se aplica a la conexión actual. Puede ser NONE,
READ_COMMITTED, READ_UNCOMMITTED, REPEATABLE_READ o SERIALIZABLE,
nivel de aislamiento correspondiente a los niveles de aislamiento de transacciones estándar definidos por el objeto
Connection de JDBC . El valor predeterminado es READ_UNCOMMITTED. Esta opción se aplica
solo a la escritura. Para obtener más información, consulte la documentación en java.sql.Connection.

Esta es una opción relacionada con el escritor JDBC. Cuando SaveMode.Overwrite está habilitado,
Spark trunca una tabla existente en lugar de eliminarla y volver a crearla. Esto puede ser más
truncar eficiente y evita que se eliminen los metadatos de la tabla (p. ej., índices). Sin embargo, no
funcionará en algunos casos, como cuando los datos nuevos tienen un esquema diferente. El valor
predeterminado es falso. Esta opción se aplica solo a la escritura.

Esta es una opción relacionada con el escritor JDBC. Si se especifica, esta opción permite
createTableOptions configurar las opciones de partición y tabla específicas de la base de datos al crear una tabla (p. ej.,
CREAR TABLA t (cadena de nombre) ENGINE=InnoDB). Esta opción se aplica solo a la escritura.

Los tipos de datos de la columna de la base de datos que se usarán en lugar de los predeterminados
al crear la tabla. La información del tipo de datos debe especificarse en el mismo formato que la

createTableColumnTypes sintaxis de las columnas CREATE TABLE (por ejemplo, “nombre CHAR(64),
comentarios VARCHAR(1024)”). Los tipos especificados deben ser tipos de datos Spark SQL válidos.
Esta opción se aplica solo a la escritura.

Lectura de bases de datos SQL


Cuando se trata de leer un archivo, las bases de datos SQL no son diferentes de las otras fuentes de datos que
vimos anteriormente. Al igual que con esas fuentes, especificamos el formato y las opciones, y luego cargamos
los datos:

// en Scala val
driver = "org.sqlite.JDBC" val path = "/data/flight­
data/jdbc/my­sqlite.db" val url = s"jdbc:sqlite:/${path}" val tablename =
"información_vuelo"

# en Python
driver = "org.sqlite.JDBC" ruta = "/data/
flight­data/jdbc/my­sqlite.db" url = "jdbc:sqlite:" + ruta tablename =
"flight_info"

Una vez que haya definido las propiedades de la conexión, puede probar su conexión a la base de datos para
asegurarse de que funciona. Esta es una excelente técnica de solución de problemas para confirmar que su base
de datos está disponible (como mínimo) para el controlador Spark. Esto es mucho menos relevante para
SQLite porque es un archivo en su máquina, pero si estuviera usando algo como MySQL, podría probar la
conexión con lo siguiente:
Machine Translated by Google

import java.sql.DriverManager val


conexión = DriverManager.getConnection(url)
conexión.isClosed()
conexión.cerrar()

Si esta conexión tiene éxito, está listo para comenzar. Avancemos y leamos el DataFrame de la tabla SQL:

// en Scala
val dbDataFrame = spark.read.format("jdbc").option("url", url)
.option("dbtable", nombre de tabla).option("controlador", controlador).load()

# en Python
dbDataFrame = spark.read.format("jdbc").option("url", url)\
.option("dbtable", nombre de tabla).option("controlador", controlador).load()

SQLite tiene configuraciones bastante simples (sin usuarios, por ejemplo). Otras bases de datos,
como PostgreSQL, requieren más parámetros de configuración. Realicemos la misma lectura que acabamos
de realizar, excepto que esta vez usamos PostgreSQL:

// en Scala
val pgDF =

spark.read .format("jdbc") .option("driver",


"org.postgresql.Driver") .option("url", "jdbc:postgresql://
database_server") . option("dbtable",
"schema.tablename") .option("usuario", "nombre de usuario").option("contraseña","mi­contraseña­secreta").load()

# en Python
pgDF = spark.read.format("jdbc")
\ .option("driver", "org.postgresql.Driver")\ .option("url",
"jdbc:postgresql://database_server")\ .option("dbtable",
"schema.tablename")\ .option("usuario",
"nombre de usuario").option("contraseña", "mi­contraseña­secreta").load()

A medida que creamos este DataFrame, no es diferente de cualquier otro: puede consultarlo, transformarlo y
unirse a él sin problemas. También notará que ya existe un esquema. Esto se debe a que Spark recopila
esta información de la propia tabla y asigna los tipos a los tipos de datos de Spark. Obtengamos solo las
distintas ubicaciones para verificar que podemos consultarlo como se esperaba:

dbDataFrame.select("DEST_COUNTRY_NAME").distinct().show(5)

+­­­­­­­­­­­­­­­­­+
|DEST_COUNTRY_NAME|
+­­­­­­­­­­­­­­­­­+
| Anguila|
| Rusia|
| Paraguay|
| Senegal|
Machine Translated by Google

| Suecia|
+­­­­­­­­­­­­­­­­­+

Impresionante, ¡podemos consultar la base de datos! Antes de continuar, hay un par de detalles matizados
que vale la pena entender.

Desplazamiento de consulta

En primer lugar, Spark hace todo lo posible por filtrar los datos en la propia base de datos antes de crear el
DataFrame. Por ejemplo, en la consulta de muestra anterior, podemos ver en el plan de consulta que
selecciona solo el nombre de columna relevante de la tabla:

dbDataFrame.select("DEST_COUNTRY_NAME").distinct().explain

== Plano físico ==
*HashAgregate(teclas=[DEST_COUNTRY_NAME#8108], funciones=[])
+­ Intercambio de partición hash (DEST_COUNTRY_NAME#8108, 200)
+­ *HashAgregate(teclas=[DEST_COUNTRY_NAME#8108], funciones=[])
+­ *Escanear JDBCRelation(flight_info) [numPartitions=1] ...

Spark en realidad puede hacerlo mejor que esto en ciertas consultas. Por ejemplo, si especificamos un filtro
en nuestro DataFrame, Spark empujará ese filtro hacia abajo en la base de datos. Podemos ver esto en el plan
de explicación en PushedFilters.

// en Scala
dbDataFrame.filter("DEST_COUNTRY_NAME in ('Anguilla', 'Sweden')").explain

# en Python
dbDataFrame.filter("DEST_COUNTRY_NAME en ('Anguila', 'Suecia')").explain()

== Plan físico ==
*Escanear JDBCRel... PushedFilters: [*In(DEST_COUNTRY_NAME, [Anguilla,Sweden])],
...

Spark no puede traducir todas sus propias funciones a las funciones disponibles en la base de datos SQL en
la que está trabajando. Por lo tanto, a veces querrá pasar una consulta completa a su SQL que devolverá los
resultados como un DataFrame. Ahora, esto puede parecer un poco complicado, pero en realidad es
bastante sencillo. En lugar de especificar un nombre de tabla, solo especifica una consulta SQL. Por supuesto,
necesita especificar esto de una manera especial; debe envolver la consulta entre paréntesis y cambiarle el
nombre a algo; en este caso, solo le di el mismo nombre de tabla:

// en Scala
val pushdownQuery = """(SELECT DISTINCT(DEST_COUNTRY_NAME) FROM flight_info)
AS flight_info""" val
dbDataFrame = spark.read.format("jdbc") .option("url",
url).option("dbtable", pushdownQuery).option("driver", driver) .load()
Machine Translated by Google

# en Python
pushdownQuery = """(SELECCIONE DISTINCT(DEST_COUNTRY_NAME) FROM flight_info)
COMO
información_de_vuelo""" dbDataFrame = chispa.read.format("jdbc")\
.option("url", url).option("dbtable", pushdownQuery).option("controlador", controlador)\ .load()

Ahora, cuando consulte esta tabla, en realidad estará consultando los resultados de esa consulta.
Podemos ver esto en el plan de explicación. Spark ni siquiera conoce el esquema real de la tabla, solo
el que resulta de nuestra consulta anterior:

dbDataFrame.explain()

== Plano físico ==
*Escanear relación JDBC(
(SELECCIONE DISTINTO(DEST_COUNTRY_NAME)
DESDE info_vuelo) como info_vuelo
) [numPartitions=1] [DEST_COUNTRY_NAME#788] ReadSchema: ...

Lectura de bases de datos en paralelo

A lo largo de este libro, hemos hablado sobre la partición y su importancia en el procesamiento


de datos. Spark tiene un algoritmo subyacente que puede leer varios archivos en una partición o, por el
contrario, leer varias particiones de un archivo, según el tamaño del archivo y la "divisibilidad"
del tipo de archivo y la compresión. La misma flexibilidad que existe con los archivos, también existe
con las bases de datos SQL, excepto que debe configurarlo un poco más manualmente. Lo que puedes
configurar, como se vio en las opciones anteriores, es la posibilidad de especificar un número
máximo de particiones que te permita limitar cuánto estás leyendo y escribiendo en paralelo:

// en Scala val
dbDataFrame = spark.read.format("jdbc") .option("url",
url).option("dbtable", tablename).option("driver", driver) .option("numPartitions" , 10).cargar()

# en Python
dbDataFrame = spark.read.format("jdbc")\ .option("url",
url).option("dbtable", tablename).option("driver", driver)\ .option("numPartitions" , 10).cargar()

En este caso, seguirá siendo una partición porque no hay demasiados datos. Sin embargo, esta
configuración puede ayudarlo a asegurarse de no sobrecargar la base de datos al leer y escribir datos:

dbDataFrame.select("DEST_COUNTRY_NAME").distinct().show()

Hay varias otras optimizaciones que desafortunadamente solo parecen estar bajo otro conjunto de API.
Puede insertar explícitamente predicados en bases de datos SQL a través de la propia conexión. Esta
optimización le permite controlar la ubicación física de ciertos datos en ciertas particiones al
Machine Translated by Google

especificando predicados. Eso es un bocado, así que veamos un ejemplo simple. Solo necesitamos datos de
dos países en nuestros datos: Anguila y Suecia. Podríamos filtrarlos y enviarlos a la base de datos, pero también
podemos ir más allá al hacer que lleguen a sus propias particiones en Spark. Lo hacemos especificando una
lista de predicados cuando creamos la fuente de datos:

// en Scala val
props = new java.util.Properties
props.setProperty("driver", "org.sqlite.JDBC") val predicates =
Array(
"DEST_COUNTRY_NAME = 'Suecia' O ORIGIN_COUNTRY_NAME = 'Suecia'",
"DEST_COUNTRY_NAME = 'Anguila' O ORIGIN_COUNTRY_NAME = 'Anguila'")
spark.read.jdbc(url, nombre de tabla, predicados, accesorios).show()
spark.read.jdbc(url, nombre de tabla, predicados, accesorios).rdd.getNumPartitions // 2

# en accesorios
de Python = {"controlador":"org.sqlite.JDBC"}
predicados = [
"NOMBRE_PAÍS_DEST = 'Suecia' O NOMBRE_PAÍS_DEST = 'Suecia'",
"NOMBRE_PAÍS_DEST = 'Anguila' OR NOMBRE_PAÍS_DEST = 'Anguila'"] spark.read.jdbc(url,
tablename, predicates=predicates, properties=props).show()
spark.read.jdbc(url,tablename,predicates=predicates,properties=props)\
.rdd.getNumPartitions() # 2

+­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­+­­­­­+
|DEST_COUNTRY_NAME|ORIGIN_COUNTRY_NAME|recuento|
+­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­+­­­­­+
| Suecia| Estados Unidos| 65|
| Estados Unidos| Suecia| 73|
| Anguila| Estados Unidos| 21|
| Estados Unidos| Anguila| 20|
+­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­+­­­­­+

Si especifica predicados que no son disjuntos, puede terminar con muchas filas duplicadas. Aquí hay un
ejemplo de conjunto de predicados que darán como resultado filas duplicadas:

// en Scala val
props = new java.util.Properties
props.setProperty("driver", "org.sqlite.JDBC") val predicates =
Array(
"NOMBRE_PAÍS_DEST != 'Suecia' O NOMBRE_PAÍS_ORIGIN != 'Suecia'",
"NOMBRE_PAÍS_DEST != 'Anguila' O NOMBRE_PAÍS_ORIGIN != 'Anguila'")
spark.read.jdbc(url, nombre de tabla, predicados, accesorios).count() // 510

# en accesorios
de Python = {"controlador":"org.sqlite.JDBC"}
predicados = [
"NOMBRE_PAÍS_DEST != 'Suecia' O NOMBRE_PAÍS_DEST != 'Suecia'", "NOMBRE_PAÍS_DEST !
= 'Anguilla' OR NOMBRE_PAÍS_ORIGIN != 'Anguilla'"] spark.read.jdbc(url, tablename,
predicates=predicates, properties=props) .contar()
Machine Translated by Google

Partición basada en una ventana deslizante

Echemos un vistazo para ver cómo podemos particionar en función de los predicados. En este ejemplo,
dividiremos según nuestra columna de conteo numérico. Aquí, especificamos un mínimo y un máximo
tanto para la primera como para la última partición. Cualquier cosa fuera de estos límites estará en la primera
partición o partición final. Luego, establecemos el número de particiones que queremos en total (este es
el nivel de paralelismo). Spark luego consulta nuestra base de datos en paralelo y devuelve particiones numPartitions.
Simplemente modificamos los límites superior e inferior para colocar ciertos valores en ciertas
particiones. No se está filtrando como vimos en el ejemplo anterior:

// en Scala
val colName = "count"
val lowerBound = 0L
val upperBound = 348113L // este es el conteo máximo en nuestra base de
datos val numPartitions = 10

# en Python
colName = "count"
lowerBound = 0L
upperBound = 348113L # este es el conteo máximo en nuestra base de
datos numPartitions = 10

Esto distribuirá los intervalos por igual de menor a mayor:

// en Scala
spark.read.jdbc(url,tablename,colName,lowerBound,upperBound,numPartitions,props)
.contar() // 255

# en Python
spark.read.jdbc(url, tablename, column=colName, properties=props,
lowerBound=lowerBound, upperBound=upperBound,
numPartitions=numPartitions).count() # 255

Escribir en bases de datos SQL


Escribir en bases de datos SQL es tan fácil como antes. Simplemente especifique el URI y escriba los datos
de acuerdo con el modo de escritura especificado que desee. En el siguiente ejemplo, especificamos
overwrite, que sobrescribe toda la tabla. Usaremos el marco de datos CSV que definimos anteriormente
para hacer esto:

// en Scala
val newPath = "jdbc:sqlite://tmp/my­sqlite.db"
csvFile.write.mode("overwrite").jdbc(newPath, tablename, props)

# en Python
newPath = "jdbc:sqlite://tmp/my­sqlite.db"
csvFile.write.jdbc(newPath, tablename, mode="overwrite", properties=props)
Machine Translated by Google

Veamos los resultados:

// en Scala
spark.read.jdbc(nuevaRuta, nombretabla, props).count() // 255

# en Python
spark.read.jdbc(newPath, tablename, properties=props).count() # 255

Por supuesto, podemos agregar a la tabla esta nueva tabla con la misma facilidad:

// en Scala
csvFile.write.mode("append").jdbc(newPath, tablename, props)

# en Python
csvFile.write.jdbc(newPath, tablename, mode="append", properties=props)

Observe que el conteo aumenta:

// en Scala
spark.read.jdbc(nuevaRuta, nombretabla, props).count() // 765

# en Python
spark.read.jdbc(nuevaRuta, nombretabla, propiedades=props).count() # 765

Archivos de texto

Spark también le permite leer archivos de texto sin formato. Cada línea del archivo se convierte en un registro en
el DataFrame. Entonces depende de usted transformarlo en consecuencia. Como ejemplo de cómo haría esto, suponga
que necesita analizar algunos archivos de registro de Apache en un formato más estructurado, o tal vez desea
analizar texto sin formato para el procesamiento de lenguaje natural. Los archivos de texto son un gran argumento
para la API de conjuntos de datos debido a su capacidad para aprovechar la flexibilidad de los tipos nativos.

Lectura de archivos de texto

La lectura de archivos de texto es sencilla: simplemente especifica el tipo para que sea textFile. Con textFile,
los nombres de los directorios particionados se ignoran. Para leer y escribir archivos de texto de acuerdo con las
particiones, debe usar texto, que respeta las particiones en lectura y escritura:

spark.read.textFile("/data/flight­data/csv/2010­summary.csv") .selectExpr("split(value,
',') as rows").show()

+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
| filas|
+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
|[DEST_COUNTRY_NAM...|
Machine Translated by Google

|[Estados Unidos, R...|


...
|[Estados Unidos, A...| |
[San Vicente y...| |[Italia,
Estados Unidos...|
+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+

Escribir archivos de texto


Cuando escribe un archivo de texto, debe asegurarse de tener solo una columna de cadena; de lo contrario,
la escritura fallará:

csvFile.select("DEST_COUNTRY_NAME").write.text("/tmp/simple­text­file.txt")

Si realiza algunas particiones al realizar su escritura (hablaremos sobre las particiones en las próximas dos
páginas), puede escribir más columnas. Sin embargo, esas columnas se manifestarán como directorios en la
carpeta en la que está escribiendo, en lugar de columnas en cada archivo:

// en Scala
csvFile.limit(10).select("DEST_COUNTRY_NAME", "count")
.write.partitionBy("count").text("/tmp/cinco­csv­files2.csv")

# en pitón

csvFile.limit(10).select("DEST_COUNTRY_NAME", "count")\
.write.partitionBy("recuento").text("/tmp/cinco­csv­files2py.csv")

Conceptos avanzados de E/S


Vimos anteriormente que podemos controlar el paralelismo de los archivos que escribimos controlando las
particiones antes de escribir. También podemos controlar el diseño de datos específicos controlando dos
cosas: el agrupamiento y la partición (discutido momentáneamente).

Compresión y tipos de archivos divisibles


Ciertos formatos de archivo son fundamentalmente "divisibles". Esto puede mejorar la velocidad porque hace
posible que Spark evite leer un archivo completo y acceda solo a las partes del archivo necesarias para
satisfacer su consulta. Además, si está utilizando algo como el sistema de archivos distribuidos de Hadoop
(HDFS), dividir un archivo puede proporcionar una mayor optimización si ese archivo abarca varios
bloques. Junto con esto, existe la necesidad de administrar la compresión. No todos los esquemas de compresión
se pueden dividir. La forma en que almacena sus datos tiene una gran importancia cuando se trata de hacer
que sus trabajos de Spark funcionen sin problemas. Recomendamos Parquet con compresión gzip.

Lectura de datos en paralelo


Múltiples ejecutores no necesariamente pueden leer del mismo archivo al mismo tiempo, pero pueden leer
Machine Translated by Google

diferentes archivos al mismo tiempo. En general, esto significa que cuando lee de una carpeta con varios archivos, cada
uno de esos archivos se convertirá en una partición en su DataFrame y los ejecutores disponibles los leerán en paralelo (con el
resto en cola detrás de los demás).

Escritura de datos en paralelo


La cantidad de archivos o datos escritos depende de la cantidad de particiones que tenga el DataFrame en el momento en que
escribe los datos. De forma predeterminada, se escribe un archivo por partición de los datos. Esto significa que aunque
especificamos un "archivo", en realidad es una cantidad de archivos dentro de una carpeta, con el nombre del archivo
especificado, con un archivo por cada partición que se escribe.

Por ejemplo, el siguiente código

csvFile.repartition(5).write.format("csv").save("/tmp/multiple.csv")

terminará con cinco archivos dentro de esa carpeta. Como puede ver en la lista de llamadas:

ls /tmp/multiple.csv

/tmp/multiple.csv/part­00000­767df509­ec97­4740­8e15­4e173d365a8b.csv /tmp/
multiple.csv/part­00001­767df509­ec97­4740­8e15­4e173d365a8b.csv /tmp/multiple .csv /
part­00002­767df509­ec97­4740­8e15­4e173d365a8b.csv /tmp/multiple.csv/
part­00003­767df509­ec97­4740­8e15­4e173d365a8b.csv /tmp/multiple.csv/part­000
04­767df509 ­ec97­4740­8e15­4e173d365a8b.csv

Fraccionamiento

El particionamiento es una herramienta que le permite controlar qué datos se almacenan (y dónde) a medida que los escribe.
Cuando escribe un archivo en un directorio (o tabla) particionado, básicamente codifica una columna como una carpeta. Lo
que esto le permite hacer es omitir muchos datos cuando vaya a leerlos más tarde, lo que le permite leer solo los datos
relevantes para su problema en lugar de tener que escanear el conjunto de datos completo. Estos son compatibles con
todas las fuentes de datos basadas en archivos:

// en Scala
csvFile.limit(10).write.mode("overwrite").partitionBy("DEST_COUNTRY_NAME")
.save("/tmp/archivos­particionados.parquet")

# en Python
csvFile.limit(10).write.mode("overwrite").partitionBy("DEST_COUNTRY_NAME")\
.save("/tmp/archivos­particionados.parquet")

Al escribir, obtiene una lista de carpetas en su "archivo" de Parquet:

$ ls /tmp/archivos­particionados.parquet

...
DEST_COUNTRY_NAME=Costa Rica/
DEST_COUNTRY_NAME=Egipto/
Machine Translated by Google

DEST_COUNTRY_NAME=Guinea Ecuatorial/
DEST_COUNTRY_NAME=Senegal/
DEST_COUNTRY_NAME=Estados Unidos/

Cada uno de estos contendrá archivos Parquet que contienen esos datos donde estaba el predicado anterior
verdadero:

$ ls /tmp/partitioned­files.parquet/DEST_COUNTRY_NAME=Senegal/

part­00000­tid.....parquet

Esta es probablemente la optimización más baja que puede usar cuando tiene una tabla que los lectores filtran con frecuencia
antes de manipularla. Por ejemplo, la fecha es particularmente común para una partición porque, en sentido descendente, a
menudo queremos ver solo los datos de la semana anterior (en lugar de escanear la lista completa de registros). Esto puede
proporcionar aceleraciones masivas para los lectores.

agrupamiento

El agrupamiento es otro enfoque de organización de archivos con el que puede controlar los datos que se escriben
específicamente en cada archivo. Esto puede ayudar a evitar mezclas más adelante cuando vaya a leer los datos porque los datos
con el mismo ID de depósito se agruparán en una partición física.
Esto significa que los datos se dividen previamente de acuerdo con la forma en que espera usar esos datos más adelante, lo que
significa que puede evitar costosas reorganizaciones al unir o agregar.

En lugar de particionar en una columna específica (lo que podría escribir una tonelada de directorios), probablemente valga la
pena explorar la agrupación de datos en su lugar. Esto creará una cierta cantidad de archivos y organizará nuestros datos en esos
"cubos":

val numberBuckets = 10 val


columnToBucketBy = "count"

csvFile.write.format("parquet").mode("overwrite") .bucketBy(numberBuckets,
columnToBucketBy).saveAsTable("bucketedFiles")

$ ls /usuario/colmena/almacén/archivos en depósito/

part­00000­tid­1020575097626332666­8....parquet part­00000­
tid­1020575097626332666­8....parquet part­00000­
tid­1020575097626332666­8....parquet
...

La creación de depósitos solo se admite para las tablas administradas por Spark. Para obtener más información sobre el
almacenamiento en depósitos y la partición, vea esta charla de Spark Summit 2017.

Escritura de tipos complejos


Como cubrimos en el Capítulo 6, Spark tiene una variedad de diferentes tipos internos. Aunque Spark puede funcionar con todos
estos tipos, no todos funcionan bien con todos los formatos de archivo de datos. Para
Machine Translated by Google

Por ejemplo, los archivos CSV no admiten tipos complejos, mientras que Parquet y ORC sí.

Administrar el tamaño del archivo


La gestión del tamaño de los archivos es un factor importante no tanto para escribir datos sino para leerlos más adelante.
Cuando está escribiendo muchos archivos pequeños, hay una sobrecarga de metadatos significativa en la que incurre al
administrar todos esos archivos. Spark especialmente no funciona bien con archivos pequeños, aunque muchos sistemas de
archivos (como HDFS) tampoco manejan bien muchos archivos pequeños. Es posible que escuche que esto se conoce
como el "problema de archivos pequeños". Lo contrario también es cierto: tampoco desea archivos que sean
demasiado grandes, porque se vuelve ineficiente tener que leer bloques completos de datos cuando solo necesita unas
pocas filas.

Spark 2.2 introdujo un nuevo método para controlar el tamaño de los archivos de forma más automática. Anteriormente
vimos que la cantidad de archivos de salida es un derivado de la cantidad de particiones que teníamos en el momento de
la escritura (y las columnas de partición que seleccionamos). Ahora, puede aprovechar otra herramienta para limitar el
tamaño de los archivos de salida para que pueda apuntar a un tamaño de archivo óptimo. Puede usar la opción
maxRecordsPerFile y especificar un número de su elección. Esto le permite controlar mejor el tamaño de los archivos al
controlar la cantidad de registros que se escriben en cada archivo. Por ejemplo, si configura una opción para un escritor
como df.write.option("maxRecordsPerFile", 5000), Spark se asegurará de que los archivos contengan como máximo 5000
registros.

Conclusión
En este capítulo, discutimos la variedad de opciones disponibles para leer y escribir datos en Spark. Esto cubre casi todo
lo que necesitará saber como usuario diario de Spark. Para los curiosos, hay formas de implementar su propia fuente de
datos; sin embargo, omitimos las instrucciones sobre cómo hacer esto porque la API está evolucionando
actualmente para admitir mejor la transmisión estructurada. Si está interesado en ver cómo implementar sus propias fuentes
de datos personalizadas, Cassandra Connector está bien organizado y mantenido y podría proporcionar una referencia
para los aventureros.

En el Capítulo 10, analizamos Spark SQL y cómo interactúa con todo lo demás que hemos visto hasta ahora en las API
estructuradas.
Machine Translated by Google

Capítulo 10. Spark SQL

Spark SQL es posiblemente una de las características más importantes y poderosas de Spark. Este capítulo
presenta los conceptos básicos de Spark SQL que debe comprender. Este capítulo no reescribirá la especificación
ANSI­SQL ni enumerará cada tipo de expresión SQL. Si lee otras partes de este libro, notará que tratamos de incluir
código SQL dondequiera que incluyamos código DataFrame para facilitar la referencia cruzada con ejemplos de
código. Otros ejemplos están disponibles en el apéndice y las secciones de referencia.

En pocas palabras, con Spark SQL puede ejecutar consultas SQL en vistas o tablas organizadas en bases de
datos. También puede usar funciones del sistema o definir funciones de usuario y analizar planes de consulta para
optimizar sus cargas de trabajo. Esto se integra directamente en DataFrame y Dataset API y, como vimos en
capítulos anteriores, puede optar por expresar algunas de sus manipulaciones de datos en SQL y otras en DataFrames
y se compilarán en el mismo código subyacente.

¿Qué es SQL?
SQL o lenguaje de consulta estructurado es un lenguaje específico de dominio para expresar operaciones
relacionales sobre datos. Se utiliza en todas las bases de datos relacionales, y muchas bases de datos "NoSQL"
crean su dialecto SQL para facilitar el trabajo con sus bases de datos. SQL está en todas partes, y aunque los
expertos en tecnología profetizaron su muerte, es una herramienta de datos extremadamente resistente de la que
dependen muchas empresas. Spark implementa un subconjunto de ANSI SQL:2003. Este estándar SQL está
disponible en la mayoría de las bases de datos SQL y este soporte significa que Spark ejecuta con éxito el popular
punto de referencia TPC­DS.

Big Data y SQL: Apache Hive


Antes del surgimiento de Spark, Hive era la capa de acceso SQL de big data de facto. Desarrollado originalmente
en Facebook, Hive se convirtió en una herramienta increíblemente popular en la industria para realizar
operaciones de SQL en big data. En muchos sentidos, ayudó a impulsar a Hadoop en diferentes industrias porque
los analistas podían ejecutar consultas SQL. Aunque Spark comenzó como un motor de procesamiento general
con conjuntos de datos distribuidos resistentes (RDD), una gran cohorte de usuarios ahora usa Spark SQL.

Grandes datos y SQL: Spark SQL


Con el lanzamiento de Spark 2.0, sus autores crearon un superconjunto de soporte de Hive, escribiendo un
analizador de SQL nativo que admite tanto consultas ANSI­SQL como HiveQL. Esto, junto con su interoperabilidad
única con DataFrames, lo convierte en una herramienta poderosa para todo tipo de empresas. Por ejemplo, a
fines de 2016, Facebook anunció que había comenzado a ejecutar cargas de trabajo de Spark y vio grandes
beneficios al hacerlo. En palabras de los autores de la publicación del blog:
Machine Translated by Google

Desafiamos a Spark a reemplazar una tubería que se descompuso en cientos de trabajos de Hive en un solo
trabajo de Spark. A través de una serie de mejoras de rendimiento y confiabilidad, pudimos escalar Spark para
manejar uno de nuestros casos de uso de procesamiento de datos de clasificación de entidades en producción...
La canalización basada en Spark produjo mejoras de rendimiento significativas (4,5–6x CPU, 3–4x reserva de
recursos y ~5x latencia) en comparación con la antigua canalización basada en Hive, y se ha estado ejecutando
en producción durante varios meses.

El poder de Spark SQL se deriva de varios hechos clave: los analistas de SQL ahora pueden aprovechar las
capacidades de computación de Spark conectándose al Thrift Server o a la interfaz SQL de Spark, mientras
que los ingenieros de datos y los científicos pueden usar Spark SQL donde corresponda en cualquier flujo de datos.
Esta API unificadora permite que los datos se extraigan con SQL, se manipulen como un DataFrame, se pasen a
uno de los algoritmos de aprendizaje automático a gran escala de Spark MLlibs, se escriban en otra fuente de
datos y todo lo demás.

NOTA

Spark SQL está diseñado para funcionar como una base de datos de procesamiento analítico en línea (OLAP), no
como una base de datos de procesamiento de transacciones en línea (OLTP). Esto significa que no está diseñado para
realizar consultas de latencia extremadamente baja. Aunque es seguro que el soporte para modificaciones en el lugar
será algo que surja en el futuro, no es algo que esté disponible actualmente.

Relación de Spark con Hive


Spark SQL tiene una excelente relación con Hive porque se puede conectar a metatiendas de Hive. El
metaalmacén de Hive es la forma en que Hive mantiene la información de la tabla para su uso entre sesiones.
Con Spark SQL, puede conectarse a su metastore de Hive (si ya tiene uno) y acceder a los metadatos de la tabla
para reducir la lista de archivos al acceder a la información. Esto es popular para los usuarios que migran desde
un entorno de Hadoop heredado y comienzan a ejecutar todas sus cargas de trabajo con Spark.

La metatienda de Hive

Para conectarse a Hive metastore, hay varias propiedades que necesitará. Primero, debe configurar la versión de
Metastore (spark.sql.hive.metastore.version) para que se corresponda con la metastore de Hive adecuada a la
que está accediendo. Por defecto, este valor es 1.2.1. También debe configurar spark.sql.hive.metastore.jars
si va a cambiar la forma en que se inicializa HiveMetastoreClient. Spark usa las versiones
predeterminadas, pero también puede especificar repositorios de Maven o una ruta de clase en el formato
estándar para la máquina virtual de Java (JVM). Además, es posible que deba proporcionar prefijos de clase
adecuados para comunicarse con diferentes bases de datos que almacenan el metaalmacén de Hive. Los
establecerá como prefijos compartidos que compartirán tanto Spark como Hive
(spark.sql.hive.metastore.sharedPrefixes).

Si se está conectando a su propia metatienda, vale la pena consultar la documentación para obtener más
actualizaciones y más información.
Machine Translated by Google

Cómo ejecutar consultas Spark SQL


Spark proporciona varias interfaces para ejecutar consultas SQL.

CLI de Spark SQL


Spark SQL CLI es una herramienta conveniente con la que puede realizar consultas básicas de Spark SQL en modo
local desde la línea de comandos. Tenga en cuenta que Spark SQL CLI no puede comunicarse con el servidor Thrift
JDBC. Para iniciar Spark SQL CLI, ejecute lo siguiente en el directorio de Spark:

./bin/spark­sql

Configure Hive colocando sus archivos hive­site.xml, core­site.xml y hdfs­site.xml en conf/.


Para obtener una lista completa de todas las opciones disponibles, puede ejecutar ./bin/spark­sql ­­help.

Interfaz SQL programática de Spark


Además de configurar un servidor, también puede ejecutar SQL de manera ad hoc a través de cualquiera de las
API de lenguaje de Spark. Puede hacerlo a través del método sql en el objeto SparkSession. Esto devuelve un
DataFrame, como veremos más adelante en este capítulo. Por ejemplo, en Python o Scala, podemos ejecutar lo
siguiente:

chispa.sql("SELECCIONE 1 + 1").mostrar()

El comando spark.sql("SELECT 1 + 1") devuelve un DataFrame que luego podemos evaluar mediante programación.
Al igual que otras transformaciones, esta no se ejecutará con entusiasmo sino con pereza.
Esta es una interfaz inmensamente poderosa porque hay algunas transformaciones que son mucho más simples de
expresar en código SQL que en DataFrames.

Puede expresar consultas de varias líneas simplemente pasando una cadena de varias líneas a la función.
Por ejemplo, podría ejecutar algo como el siguiente código en Python o Scala:

spark.sql("""SELECCIONE user_id, departamento, nombre DE profesores DONDE


departamento EN
(SELECCIONE nombre DE departamento DONDE created_date >= '2016­01­01')""")

Aún más poderoso, puede interoperar completamente entre SQL y DataFrames, como mejor le parezca. Por ejemplo,
puede crear un marco de datos, manipularlo con SQL y luego volver a manipularlo como un marco de datos. Es
una abstracción poderosa que probablemente usará bastante:

// en Scala
spark.read.json("/data/flight­data/json/2015­
summary.json") .createOrReplaceTempView("some_sql_view") // DF => SQL

chispa.sql("""
Machine Translated by Google

SELECCIONE DEST_COUNTRY_NAME, suma (recuento)


DESDE some_sql_view GRUPO POR

DEST_COUNTRY_NAME """) .where("DEST_COUNTRY_NAME like


'S%'").where("`sum(count)` > 10") .count() // SQL => DF

# en Python
spark.read.json("/data/flight­data/json/2015­summary.json")
\ .createOrReplaceTempView("some_sql_view") # DF => SQL

chispa.sql("""
SELECCIONE DEST_COUNTRY_NAME, suma (recuento)
DESDE some_sql_view GROUP BY

DEST_COUNTRY_NAME """)\ .where("DEST_COUNTRY_NAME like


'S%'").where("`sum(count)` > 10")\ .count() # SQL => DF

Servidor SparkSQL Thrift JDBC/ODBC


Spark proporciona una interfaz de conectividad de base de datos Java (JDBC) mediante la cual usted o un
programa remoto se conectan al controlador Spark para ejecutar consultas Spark SQL. Un caso de uso común
podría ser que un analista de negocios conecte software de inteligencia empresarial como Tableau a Spark.
El servidor Thrift JDBC/Open Database Connectivity (ODBC) implementado aquí corresponde al HiveServer2
en Hive 1.2.1. Puede probar el servidor JDBC con el script beeline que viene con Spark o Hive 1.2.1.

Para iniciar el servidor JDBC/ODBC, ejecute lo siguiente en el directorio de Spark:

./sbin/start­thriftserver.sh

Este script acepta todas las opciones de línea de comandos bin/spark­submit. Para ver todas las opciones
disponibles para configurar este Thrift Server, ejecute ./sbin/start­thriftserver.sh ­­help. De forma
predeterminada, el servidor escucha en localhost:10000. Puede anular esto a través de variables
ambientales o propiedades del sistema.

Para la configuración del entorno, use esto:

export HIVE_SERVER2_THRIFT_PORT=<puerto de
escucha> export HIVE_SERVER2_THRIFT_BIND_HOST=<host
de escucha> ./sbin/start­thriftserver.sh
\ ­­master <uri maestro> \
...

Para las propiedades del sistema:

./sbin/start­thriftserver.sh \ ­­hiveconf
hive.server2.thrift.port=<puerto de escucha> \ ­­hiveconf
hive.server2.thrift.bind.host=<host de escucha> \ ­­master < maestro­uri>
Machine Translated by Google

...

A continuación, puede probar esta conexión ejecutando los siguientes comandos:

./bin/línea recta

directo> !conectar jdbc:hive2://localhost:10000

Beeline le pedirá un nombre de usuario y una contraseña. En el modo no seguro, simplemente escriba el nombre de
usuario en su máquina y una contraseña en blanco. Para el modo seguro, siga las instrucciones dadas en la
documentación de beeline.

Catalogar
La abstracción de más alto nivel en Spark SQL es el Catálogo. El Catálogo es una abstracción para el
almacenamiento de metadatos sobre los datos almacenados en sus tablas, así como otras cosas útiles
como bases de datos, tablas, funciones y vistas. El catálogo está disponible en el
paquete org.apache.spark.sql.catalog.Catalog y contiene varias funciones útiles para hacer cosas como listar tablas,
bases de datos y funciones. Hablaremos de todas estas cosas en breve. Se explica por sí mismo para los usuarios,
por lo que omitiremos los ejemplos de código aquí, pero en realidad es solo otra interfaz programática para Spark
SQL. Este capítulo muestra solo el SQL que se está ejecutando; por lo tanto, si está utilizando la interfaz
programática, tenga en cuenta que necesita envolver todo en una llamada de función spark.sql para ejecutar el
código relevante.

Mesas

Para hacer algo útil con Spark SQL, primero debe definir tablas. Las tablas son lógicamente equivalentes a
un DataFrame en el sentido de que son una estructura de datos contra la cual ejecuta comandos.
Podemos unir tablas, filtrarlas, agregarlas y realizar diferentes manipulaciones que vimos en capítulos anteriores. La
principal diferencia entre las tablas y los marcos de datos es esta: usted define los marcos de datos en el
ámbito de un lenguaje de programación, mientras que define las tablas dentro de una base de datos. Esto
significa que cuando crea una tabla (suponiendo que nunca haya cambiado la base de datos), pertenecerá a la base
de datos predeterminada . Hablaremos de las bases de datos con más detalle más adelante en el capítulo.

Una cosa importante a tener en cuenta es que en Spark 2.X, las tablas siempre contienen datos. No existe la noción
de una tabla temporal, solo una vista, que no contiene datos. Esto es importante porque si va a eliminar una tabla,
puede correr el riesgo de perder los datos al hacerlo.

Tablas administradas por Spark


Una nota importante es el concepto de tablas administradas versus no administradas . Las tablas almacenan
dos piezas importantes de información. Los datos dentro de las tablas, así como los datos sobre las tablas; es
decir, los metadatos. Puede hacer que Spark administre los metadatos para un conjunto de archivos, así como para el
Machine Translated by Google

datos. Cuando define una tabla a partir de archivos en el disco, está definiendo una tabla no administrada. Cuando
usa saveAsTable en un DataFrame, está creando una tabla administrada para la cual Spark rastreará toda la
información relevante.

Esto leerá su tabla y la escribirá en una nueva ubicación en formato Spark. Puede ver esto reflejado en el nuevo
plan de explicación. En el plan de explicación, también notará que esto escribe en la ubicación predeterminada del
almacén de Hive. Puede establecer esto estableciendo la configuración de spark.sql.warehouse.dir en el directorio
que elija cuando cree su SparkSession. De forma predeterminada, Spark establece esto en /user/hive/warehouse:

Tenga en cuenta en los resultados que aparece una base de datos. Spark también tiene bases de datos de las que
hablaremos más adelante en este capítulo, pero por ahora debe tener en cuenta que también puede ver tablas en
una base de datos específica usando la consulta show tables IN databaseName, donde databaseName representa el
nombre de la base de datos que usted quiere consultar.

Si está ejecutando en un nuevo clúster o modo local, esto debería arrojar cero resultados.

Creación de tablas
Puede crear tablas a partir de una variedad de fuentes. Algo bastante exclusivo de Spark es la capacidad de
reutilizar toda la API de fuente de datos dentro de SQL. Esto significa que no necesita definir una tabla y luego cargar
datos en ella; Spark te permite crear uno sobre la marcha. Incluso puede especificar todo tipo de opciones
sofisticadas cuando lee un archivo. Por ejemplo, aquí hay una forma sencilla de leer los datos de vuelo con los que
trabajamos en capítulos anteriores:

CREAR TABLA vuelos (


CADENA DE DEST_COUNTRY_NAME, CADENA DE ORIGIN_COUNTRY_NAME, cuenta LARGA)
USO DE OPCIONES JSON (ruta '/data/flight­data/json/2015­summary.json')

UTILIZAR Y ALMACENAR COMO

La especificación de la sintaxis USING en el ejemplo anterior es de gran importancia. Si no especifica el formato,


Spark utilizará de forma predeterminada una configuración de Hive SerDe. Esto tiene implicaciones de
rendimiento para futuros lectores y escritores porque Hive SerDes es mucho más lento que la serialización
nativa de Spark. Los usuarios de Hive también pueden usar la sintaxis STORED AS para especificar que debe
ser una tabla de Hive.

También puede agregar comentarios a ciertas columnas en una tabla, lo que puede ayudar a otros desarrolladores
a comprender los datos en las tablas:

CREAR TABLA vuelos_csv (


CADENA DE DEST_COUNTRY_NAME,
ORIGIN_COUNTRY_NAME STRING COMMENT "recuerde, los EE. UU. serán los más frecuentes", cuente LARGO)

USO DE OPCIONES csv (encabezado verdadero, ruta '/data/flight­data/csv/2015­summary.csv')


Machine Translated by Google

También es posible crear una tabla a partir de una consulta:

CREAR TABLA vuelos_desde_seleccionar USANDO parquet COMO SELECCIONAR * DESDE vuelos

Además, puede especificar que se cree una tabla solo si no existe actualmente:

NOTA

En este ejemplo, estamos creando una tabla compatible con Hive porque no especificamos explícitamente el formato mediante
USING. También podemos hacer lo siguiente:

CREAR TABLA SI NO EXISTE vuelos_desde_seleccionar


COMO SELECCIONAR * DESDE vuelos

Finalmente, puede controlar el diseño de los datos escribiendo un conjunto de datos particionado, como vimos en
Capítulo 9:

CREAR TABLA de vuelos_particionados UTILIZANDO parquet PARTICIONADO POR (DEST_COUNTRY_NAME)


COMO SELECCIONAR DEST_COUNTRY_NAME, ORIGIN_COUNTRY_NAME, contar DESDE vuelos LIMIT 5

Estas tablas estarán disponibles en Spark incluso a través de sesiones; las tablas temporales no existen actualmente en Spark.
Debe crear una vista temporal, que demostraremos más adelante en este capítulo.

Creación de tablas externas


Como mencionamos al comienzo de este capítulo, Hive fue uno de los primeros sistemas SQL de macrodatos y Spark
SQL es completamente compatible con las declaraciones de Hive SQL (HiveQL). Uno de los casos de uso que puede encontrar
es migrar sus declaraciones heredadas de Hive a Spark SQL.
Afortunadamente, puede, en su mayor parte, simplemente copiar y pegar sus declaraciones de Hive directamente en Spark
SQL. Por ejemplo, en el ejemplo que sigue, creamos una tabla no administrada. Spark administrará los metadatos de la
tabla; sin embargo, los archivos no son administrados por Spark en absoluto. Esta tabla se crea mediante la instrucción CREATE

EXTERNAL TABLE.

Puede ver cualquier archivo que ya se haya definido ejecutando el siguiente comando:

CREAR TABLA EXTERNA hive_flights (


CADENA DE DEST_COUNTRY_NAME, CADENA DE ORIGIN_COUNTRY_NAME, cuenta LARGA)
CAMPOS DELIMITADOS EN FORMATO DE FILA TERMINADOS POR ',' UBICACIÓN '/data/flight­data­hive/'

También puede crear una tabla externa a partir de una cláusula de selección:

CREAR TABLA EXTERNA hive_flights_2


CAMPOS DELIMITADOS EN FORMATO DE FILA TERMINADOS POR ','
UBICACIÓN '/data/flight­data­hive/' COMO SELECCIONAR * DESDE vuelos
Machine Translated by Google

Insertar en tablas
Las inserciones siguen la sintaxis SQL estándar:

INSERTAR EN vuelos_desde_seleccionar
SELECCIONE DEST_COUNTRY_NAME, ORIGIN_COUNTRY_NAME, cuente DESDE vuelos LÍMITE 20

Opcionalmente, puede proporcionar una especificación de partición si desea escribir solo en una determinada
partición. Tenga en cuenta que una escritura también respetará un esquema de partición (lo que puede hacer que
la consulta anterior se ejecute con bastante lentitud); sin embargo, agregará archivos adicionales solo en las
particiones finales:

INSERTAR EN vuelos_particionados
PARTICIÓN (DEST_COUNTRY_NAME=" ESTADOS UNIDOS")
SELECCIONE el recuento, ORIGIN_COUNTRY_NAME FROM vuelos
DONDE DEST_COUNTRY_NAME=' ESTADOS UNIDOS' LÍMITE 12

Descripción de los metadatos de la tabla

Vimos anteriormente que puede agregar un comentario al crear una tabla. Puede ver esto describiendo los
metadatos de la tabla, que nos mostrarán el comentario relevante:

DESCRIBIR TABLA vuelos_csv

También puede ver el esquema de particionamiento de los datos usando lo siguiente (tenga en cuenta, sin embargo, que
esto solo funciona en tablas particionadas):

MOSTRAR PARTICIONES particionado_vuelos

Actualización de los metadatos de la tabla

Mantener los metadatos de la tabla es una tarea importante para asegurarse de que está leyendo el conjunto de
datos más reciente. Hay dos comandos para actualizar los metadatos de la tabla. REFRESH TABLE actualiza todas las

entradas en caché (esencialmente, archivos) asociadas con la tabla. Si la tabla se almacenó en caché
previamente, se almacenará en caché de forma perezosa la próxima vez que se escanee:

ACTUALIZAR tabla de vuelos_particionados

Otro comando relacionado es REPAIR TABLE, que actualiza las particiones mantenidas en el catálogo para esa
tabla dada. El enfoque de este comando es recopilar nueva información de partición; un ejemplo podría ser escribir una
nueva partición manualmente y la necesidad de reparar la tabla en consecuencia:

TABLA DE REPARACIÓN DE MSCK particionado_vuelos


Machine Translated by Google

Eliminación de tablas

No puede eliminar tablas: solo puede "soltarlas". Puede eliminar una tabla utilizando la palabra clave DROP. Si
elimina una tabla administrada (p. ej., vuelos_csv), se eliminarán tanto los datos como la definición de la tabla:

DROP TABLA vuelos_csv;

ADVERTENCIA

Al soltar una tabla, se eliminan los datos de la tabla, por lo que debe tener mucho cuidado al hacer esto.

Si intenta eliminar una tabla que no existe, recibirá un error. Para eliminar solo una tabla si ya existe, use DROP TABLE
IF EXISTS.

BOTAR TABLA SI EXISTE vuelos_csv;

ADVERTENCIA

Esto elimina los datos de la tabla, así que tenga cuidado al hacer esto.

Descartar tablas no administradas

Si descarta una tabla no administrada (p. ej., hive_flights), no se eliminará ningún dato, pero ya no podrá hacer referencia
a estos datos por el nombre de la tabla.

Almacenamiento en caché de tablas

Al igual que los marcos de datos, puede almacenar y recuperar tablas. Simplemente especifique qué tabla le gustaría
usar la siguiente sintaxis:

CACHE TABLE vuelos

Así es como los desbloqueas:

VUELOS DE MESA UNCACHE

Puntos de vista

Ahora que creó una tabla, otra cosa que puede definir es una vista. Una vista especifica un conjunto de transformaciones
en la parte superior de una tabla existente, básicamente planes de consulta guardados, que pueden ser convenientes
para organizar o reutilizar su lógica de consulta. Spark tiene varias nociones diferentes de
Machine Translated by Google

puntos de vista. Las vistas pueden ser globales, establecidas en una base de datos o por sesión.

Creación de vistas
Para un usuario final, las vistas se muestran como tablas, excepto que en lugar de volver a escribir todos los datos
en una nueva ubicación, simplemente realizan una transformación en los datos de origen en el momento de la
consulta. Esto podría ser un filtro, una selección o, potencialmente, un GRUPO POR o un ROLLUP
aún más grande. Por ejemplo, en el siguiente ejemplo, creamos una vista en la que el destino es Estados Unidos
para ver solo esos vuelos:

CREAR VER just_usa_view COMO


SELECCIONE * DESDE vuelos WHERE dest_country_name = 'Estados Unidos'

Al igual que las tablas, puede crear vistas temporales que están disponibles solo durante la sesión actual y no
están registradas en una base de datos:

CREAR VISTA TEMPORAL just_usa_view_temp COMO


SELECCIONE * DESDE vuelos WHERE dest_country_name = 'Estados Unidos'

O bien, puede ser una vista temporal global. Las vistas temporales globales se resuelven independientemente
de la base de datos y se pueden ver en toda la aplicación Spark, pero se eliminan al final de la sesión:

CREAR VISTA TEMPORAL GLOBAL just_usa_global_view_temp AS


SELECCIONE * DESDE vuelos WHERE dest_country_name = 'Estados Unidos'

MOSTRAR TABLAS

También puede especificar que le gustaría sobrescribir una vista si ya existe una utilizando las palabras clave
que se muestran en el ejemplo siguiente. Podemos sobrescribir tanto las vistas temporales como las
regulares:

CREAR O REEMPLAZAR TEMP VIEW just_usa_view_temp COMO


SELECCIONE * DESDE vuelos WHERE dest_country_name = 'Estados Unidos'

Ahora puede consultar esta vista como si fuera otra tabla:

SELECCIONE * DESDE just_usa_view_temp

Una vista es efectivamente una transformación y Spark la realizará solo en el momento de la consulta. Esto
significa que solo aplicará ese filtro después de que vaya a consultar la tabla (y no antes).
Efectivamente, las vistas equivalen a crear un nuevo DataFrame a partir de un DataFrame existente.

De hecho, puede ver esto comparando los planes de consulta generados por Spark DataFrames y Spark SQL. En
DataFrames, escribiríamos lo siguiente:

val vuelos = chispa.read.format("json")


Machine Translated by Google

.load("/data/flight­data/json/2015­summary.json")
val just_usa_df = vuelos.where("dest_country_name = 'Estados Unidos'") just_usa_df.selectExpr("*").explain

En SQL, escribiríamos (consultando desde nuestra vista) esto:

EXPLICAR SELECCIONAR * DESDE just_usa_view

O equivalente:

EXPLIQUE SELECCIONE * DESDE vuelos WHERE dest_country_name = 'Estados Unidos'

Debido a este hecho, debe sentirse cómodo al escribir su lógica en DataFrames o SQL, lo que le resulte más cómodo y
fácil de mantener.

Descartar vistas
Puede eliminar vistas de la misma forma que elimina tablas; simplemente especifica que lo que pretende soltar es
una vista en lugar de una tabla. La principal diferencia entre eliminar una vista y eliminar una tabla es que, con una
vista, no se eliminan los datos subyacentes, solo la definición de la vista en sí:

ABANDONAR LA VISTA SI EXISTE just_usa_view;

bases de datos
Las bases de datos son una herramienta para organizar tablas. Como se mencionó anteriormente, si no define uno,
Spark usará la base de datos predeterminada. Cualquier instrucción SQL que ejecute desde Spark (incluidos los
comandos de DataFrame) se ejecuta dentro del contexto de una base de datos. Esto significa que si cambia la base de
datos, las tablas definidas por el usuario permanecerán en la base de datos anterior y deberán consultarse de manera
diferente.

ADVERTENCIA

Esto puede ser una fuente de confusión, especialmente si está compartiendo el mismo contexto o sesión con sus compañeros de
trabajo, así que asegúrese de configurar sus bases de datos correctamente.

Puede ver todas las bases de datos utilizando el siguiente comando:

MOSTRAR BASES DE DATOS

Creación de bases de datos


Machine Translated by Google

La creación de bases de datos sigue los mismos patrones que ha visto anteriormente en este capítulo; sin embargo, aquí usas

las palabras clave CREATE DATABASE:

CREAR BASE DE DATOS some_db

Configuración de la base de datos

Es posible que desee configurar una base de datos para realizar una determinada consulta. Para ello, utilice la palabra clave
USE seguida del nombre de la base de datos:

UTILIZAR some_db

Después de configurar esta base de datos, todas las consultas intentarán resolver los nombres de las tablas en esta base de
datos. Las consultas que funcionaban bien ahora pueden fallar o generar resultados diferentes porque se encuentra en una base de
datos diferente:

MOSTRAR tablas

SELECCIONAR * DE vuelos : falla con tabla/vista no encontrada

Sin embargo, puede consultar diferentes bases de datos utilizando el prefijo correcto:

SELECCIONE * DESDE default.vuelos

Puede ver qué base de datos está utilizando actualmente ejecutando el siguiente comando:

SELECCIONE base_de_datos_actual()

Por supuesto, puede volver a la base de datos predeterminada:

UTILIZAR predeterminado;

Descartar bases de datos

Descartar o eliminar bases de datos es igual de fácil: simplemente use la palabra clave DROP DATABASE:

DROP DATABASE SI EXISTE some_db;

Seleccionar estados de cuenta

Las consultas en Spark admiten los siguientes requisitos de ANSI SQL (aquí enumeramos el diseño del

expresión SELECCIONAR):
Machine Translated by Google

SELECCIONE [TODO|DISTINTO] expresión_nombrada[, expresión_nombrada, ...]


FROM relación[, relación, ...] [vista_lateral[,
vista_lateral, ...]]
[WHERE boolean_expression]
[agregación [HAVING boolean_expression]]
[ORDENAR POR expresiones_de_clasificación]
[CLUSTER POR expresiones]
[DISTRIBUIR POR expresiones]
[ORDENAR POR ordenar_expresiones]
[VENTANA ventana_nombrada[, VENTANA ventana_nombrada, ...]]
[LIMITE número_filas]

expresión_nombrada: :
expresión [AS alias]

relación: |
unirse_relación |
(nombre_tabla|consulta|relación) [muestra] [alias AS]
: VALORES (expresiones)[, (expresiones), ...]
[AS (nombre_columna[, nombre_columna, ...])]

expresiones: :
expresión[, expresión, ...]

sort_expressions: :
expresión [ASC|DESC][, expresión [ASC|DESC], ...]

caso…cuando…entonces Declaraciones
A menudo, es posible que deba reemplazar condicionalmente los valores en sus consultas SQL. Puede hacer esto
usando una declaración de estilo case...when...then...end. Esto es esencialmente el equivalente de las
declaraciones if programáticas:

SELECCIONAR

CASO CUANDO DEST_COUNTRY_NAME = 'ESTADOS UNIDOS' ENTONCES 1


CUANDO DEST_COUNTRY_NAME = 'Egipto' ENTONCES 0
DE LO CONTRARIO ­1 FIN

DESDE vuelos_particionados

Temas avanzados
Ahora que definimos dónde residen los datos y cómo organizarlos, pasemos a consultarlos. Una consulta SQL
es una declaración SQL que solicita que se ejecute algún conjunto de comandos. Las sentencias SQL pueden
definir manipulaciones, definiciones o controles. El caso más común son las manipulaciones, que es el tema central
de este libro.

Tipos complejos
Los tipos complejos son una desviación del SQL estándar y son una característica increíblemente poderosa que
Machine Translated by Google

no existe en SQL estándar. Comprender cómo manipularlos adecuadamente en SQL es esencial. Hay tres tipos complejos básicos
en Spark SQL: estructuras, listas y mapas.

estructuras

Las estructuras son más parecidas a los mapas. Proporcionan una forma de crear o consultar datos anidados en Spark.
Para crear uno, simplemente necesita envolver un conjunto de columnas (o expresiones) entre paréntesis:

CREAR VISTA SI NO EXISTE nested_data COMO


SELECCIONE (DEST_COUNTRY_NAME, ORIGIN_COUNTRY_NAME) como país, cuente DESDE vuelos

Ahora, puede consultar estos datos para ver cómo se ven:

SELECCIONE * DESDE datos_anidados

Incluso puede consultar columnas individuales dentro de una estructura; todo lo que necesita hacer es usar la sintaxis de puntos:

SELECCIONE país.DEST_COUNTRY_NAME, cuente DESDE datos_anidados

Si lo desea, también puede seleccionar todos los subvalores de una estructura utilizando el nombre de la estructura y
seleccionar todas las subcolumnas. Aunque estas no son realmente subcolumnas, proporciona una forma más sencilla de pensar
en ellas porque podemos hacer todo lo que queramos con ellas como si fueran una columna:

SELECCIONE país.*, cuente DESDE datos_anidados

Liza

Si está familiarizado con las listas en lenguajes de programación, las listas de Spark SQL le resultarán familiares. Hay varias formas

de crear una matriz o lista de valores. Puede utilizar la función collect_list, que crea una lista de valores. También puede usar la
función collect_set, que crea una matriz sin valores duplicados. Ambas son funciones de agregación y, por lo tanto, solo se
pueden especificar en agregaciones:

SELECCIONE DEST_COUNTRY_NAME como new_name, collect_list(count) como flight_counts,


collect_set(ORIGIN_COUNTRY_NAME) como origin_set
DESDE vuelos GROUP BY DEST_COUNTRY_NAME

Sin embargo, también puede crear una matriz manualmente dentro de una columna, como se muestra aquí:

SELECCIONE DEST_COUNTRY_NAME, ARRAY(1, 2, 3) FROM vuelos

También puede consultar listas por posición utilizando una sintaxis de consulta de matriz similar a Python:

SELECCIONE DEST_COUNTRY_NAME como new_name, collect_list(count)[0]


DESDE vuelos GROUP BY DEST_COUNTRY_NAME
Machine Translated by Google

También puede hacer cosas como convertir una matriz nuevamente en filas. Para ello, utilice la función de
explosión. Para demostrarlo, creemos una nueva vista como nuestra agregación:

CREAR O REEMPLAZAR LA VISTA TEMPORAL flight_agg AS

SELECCIONE DEST_COUNTRY_NAME, collect_list (recuento) como recopilados_recuentos

DESDE vuelos GROUP BY DEST_COUNTRY_NAME

Ahora explotemos el tipo complejo en una fila en nuestro resultado para cada valor en la matriz.
DEST_COUNTRY_NAME se duplicará para cada valor en la matriz, realizando exactamente lo contrario de la
recopilación original y devolviéndonos al DataFrame original:

SELECCIONE explotar (recuentos recopilados), DEST_COUNTRY_NAME DE vuelos_agg

Funciones
Además de tipos complejos, Spark SQL proporciona una variedad de funciones sofisticadas. Puede encontrar
la mayoría de estas funciones en la referencia de funciones de DataFrames; sin embargo, también
vale la pena entender cómo encontrar estas funciones en SQL. Para ver una lista de funciones en Spark
SQL, utilice la instrucción SHOW FUNCTIONS:

MOSTRAR FUNCIONES

También puede indicar más específicamente si desea ver las funciones del sistema (es decir, las integradas en
Spark), así como las funciones del usuario:

MOSTRAR FUNCIONES DEL SISTEMA

Las funciones de usuario son aquellas definidas por usted u otra persona que comparte su entorno de Spark.
Estas son las mismas funciones definidas por el usuario de las que hablamos en capítulos anteriores (discutiremos
cómo crearlas más adelante en este capítulo):

MOSTRAR FUNCIONES DE USUARIO

Puede filtrar todos los comandos SHOW pasando una cadena con caracteres comodín (*). Aquí, podemos ver
todas las funciones que comienzan con "s":

MOSTRAR FUNCIONES "s*";

Opcionalmente, puedes incluir la palabra clave LIKE, aunque no es necesario:

MOSTRAR FUNCIONES COMO "recoger*";

Aunque la lista de funciones es ciertamente útil, a menudo es posible que desee saber más sobre funciones
específicas en sí mismas. Para ello, utilice la palabra clave DESCRIBE, que devuelve el
Machine Translated by Google

documentación para una función específica.

Funciones definidas por el usuario

Como vimos en los Capítulos 3 y 4, Spark le brinda la capacidad de definir sus propias funciones y usarlas de manera
distribuida. Puede definir funciones, tal como lo hizo antes, escribiendo la función en el idioma de su elección y
luego registrándola apropiadamente:

def potencia3(número:Doble):Doble = número * número * número


chispa.udf.register("potencia3", potencia3(_:Doble):Doble)

SELECCIONE conteo, potencia3(conteo) DESDE vuelos

También puede registrar funciones a través de la sintaxis Hive CREATE TEMPORARY FUNCTION.

Subconsultas
Con las subconsultas, puede especificar consultas dentro de otras consultas. Esto le permite especificar una lógica
sofisticada dentro de su SQL. En Spark, hay dos subconsultas fundamentales. Las subconsultas
correlacionadas utilizan alguna información del ámbito externo de la consulta para complementar la información de la
subconsulta. Las subconsultas no correlacionadas no incluyen información del ámbito externo. Cada una de
estas consultas puede devolver uno (subconsulta escalar) o más valores. Spark también incluye soporte para subconsultas
predicadas, que permiten el filtrado basado en valores.

Subconsultas de predicado no correlacionadas

Por ejemplo, echemos un vistazo a una subconsulta de predicado. En este ejemplo, esto se compone de dos consultas
no correlacionadas . La primera consulta es solo para obtener los cinco principales destinos de países según los datos
que tenemos:

SELECCIONE dest_country_name DE vuelos


GRUPO POR dest_country_name ORDEN POR sum(count) DESC LIMIT 5

Esto nos da el siguiente resultado:

+­­­­­­­­­­­­­­­­­+
|nombre_país_destino|
+­­­­­­­­­­­­­­­­­+
| Estados Unidos| |
Canadá|
| México|
| Reino Unido| |
Japón|
+­­­­­­­­­­­­­­­­­+

Ahora colocamos esta subconsulta dentro del filtro y verificamos si nuestro país de origen existe en
Machine Translated by Google

esa lista:

SELECCIONE * DESDE vuelos


DONDE nombre_país_origen EN (SELECCIONE nombre_país_destino DESDE vuelos
GRUPO POR dest_country_name ORDEN POR sum(count) DESC LIMIT 5)

Esta consulta no está correlacionada porque no incluye ninguna información del ámbito externo de la consulta. Es
una consulta que puede ejecutar por su cuenta.

Subconsultas de predicados correlacionados

Las subconsultas de predicados correlacionados le permiten usar información del ámbito externo en su consulta
interna. Por ejemplo, si desea ver si tiene un vuelo que lo llevará de regreso desde su país de destino, puede
hacerlo verificando si hay un vuelo que tiene el país de destino como origen y un vuelo que tiene el país de
origen. como destino:

SELECCIONE * DESDE vuelos f1


DONDE EXISTE (SELECCIONE 1 DESDE vuelos f2
DONDE f1.dest_country_name = f2.origin_country_name)
Y EXISTE (SELECCIONE 1 DE vuelos f2
DONDE f2.dest_country_name = f1.origin_country_name)

EXISTS simplemente comprueba si existe alguna existencia en la subconsulta y devuelve verdadero si hay un
valor. Puede voltear esto colocando el operador NOT delante de él. ¡Esto sería equivalente a encontrar un vuelo a un
destino del que no podrá regresar!

Consultas escalares no correlacionadas

Al usar consultas escalares no correlacionadas, puede traer información adicional que quizás no tenía
anteriormente. Por ejemplo, si quisiera incluir el valor máximo como su propia columna de todo el conjunto de datos
de conteos, podría hacer esto:

SELECCIONE *, (SELECCIONE max(count) FROM vuelos) COMO máximo FROM vuelos

Funciones misceláneas
Hay algunas funciones en Spark SQL que no encajan en las secciones anteriores de este capítulo, por lo que las
incluiremos aquí sin ningún orden en particular. Estos pueden ser relevantes al realizar optimizaciones o depurar su
código SQL.

Configuraciones
Hay varias configuraciones de aplicaciones Spark SQL, que enumeramos en la Tabla 10­1. Puede configurarlos en la
inicialización de la aplicación o durante la ejecución de la aplicación (como hemos visto con las particiones aleatorias
a lo largo de este libro).

Tabla 10­1. Configuraciones de Spark SQL


Machine Translated by Google

Tabla 10­1. Configuraciones de Spark SQL

Nombre de la propiedad Significado predeterminado

Cuando se establece en verdadero, Spark SQL selecciona

chispa.sql.inMemoryColumnarStorage.compressed verdadero automáticamente un códec de compresión para cada columna


en función de las estadísticas de los datos.

Controla el tamaño de los lotes para el almacenamiento en

caché en columnas. Los tamaños de lote más grandes

chispa.sql.inMemoryColumnarStorage.batchSize 10000 pueden mejorar la utilización y la compresión de la memoria,

pero corren el riesgo de errores OutOfMemoryErrors

(OOM) al almacenar datos en caché.

134217728 El número máximo de bytes para empaquetar en una sola


chispa.sql.files.maxPartitionBytes
(128 MB) partición al leer archivos.

El costo estimado para abrir un archivo, medido por la


cantidad de bytes que podrían escanearse en el mismo
tiempo. Esto se usa cuando se sobreestiman 4194304 (4
archivos
chispa.sql.files.openCostInBytes múltiples en una partición. Es mejor MB); de esa forma, las
particiones con archivos pequeños serán más rápidas que las particiones
con archivos más grandes (que se programan primero).

Tiempo de espera en segundos para el tiempo de espera de


chispa.sql.broadcastTimeout 300
transmisión en uniones de transmisión.

Configura el tamaño máximo en bytes para una tabla que


se transmitirá a todos los nodos trabajadores al realizar

una unión. Puede deshabilitar la transmisión


10485760 configurando este valor en ­1. Tenga en cuenta que
spark.sql.autoBroadcastJoinThreshold
(10 MB) actualmente las estadísticas solo son
compatibles con las tablas de Hive Metastore para las que
el comando ANALYZE TABLE COMPUTE
Se ha ejecutado ESTADÍSTICAS noscan .

Configura la cantidad de particiones que se usarán al


chispa.sql.shuffle.particiones 200 mezclar datos para uniones o agregaciones.

Establecer valores de configuración en SQL


Hablamos de configuraciones en el Capítulo 15, pero como avance, vale la pena mencionar cómo establecer
configuraciones desde SQL. Naturalmente, solo puede establecer configuraciones de Spark SQL de esa manera,
pero así es como puede establecer particiones aleatorias:

SET chispa.sql.shuffle.particiones=20
Machine Translated by Google

Conclusión
Debería quedar claro en este capítulo que Spark SQL y DataFrames están muy relacionados y que
debería poder usar casi todos los ejemplos de este libro con solo pequeños ajustes sintácticos. Este
capítulo ilustró más detalles específicos relacionados con Spark SQL. El Capítulo 11 se centra en un
nuevo concepto: Conjuntos de datos que permiten transformaciones estructuradas con seguridad de tipos.
Machine Translated by Google

Capítulo 11. Conjuntos de datos

Los conjuntos de datos son el tipo fundamental de las API estructuradas. Ya trabajamos con DataFrames, que son
Datasets de tipo Fila, y están disponibles en los diferentes lenguajes de Spark. Los conjuntos de datos son estrictamente
una característica del lenguaje Java Virtual Machine (JVM) que funciona solo con Scala y Java.
Usando Conjuntos de datos, puede definir el objeto del que constará cada fila en su Conjunto de datos. En Scala, este
será un objeto de clase de caso que esencialmente define un esquema que puede usar, y en Java, definirá un Java
Bean. Los usuarios experimentados a menudo se refieren a los conjuntos de datos como el "conjunto tipificado de API"
en Spark. Para obtener más información, consulte el Capítulo 4.

En el Capítulo 4, discutimos que Spark tiene tipos como StringType, BigIntType, StructType, etc. Esos tipos específicos de
Spark se asignan a tipos disponibles en cada uno de los lenguajes de Spark, como String, Integer y Double.
Cuando usa la API de DataFrame, no crea cadenas ni números enteros, pero Spark manipula los datos por usted
manipulando el objeto Row. De hecho, si usa Scala o Java, todos los "DataFrames" son en realidad conjuntos de datos
de tipo Fila. Para admitir de manera eficiente los objetos específicos del dominio, se requiere un concepto especial
llamado "Codificador". El codificador asigna el tipo T específico del dominio al sistema de tipo interno de Spark.

Por ejemplo, dada una clase Persona con dos campos, nombre (cadena) y edad (int), un codificador indica a
Spark que genere código en tiempo de ejecución para serializar el objeto Persona en una estructura binaria.
Al usar DataFrames o las API estructuradas "estándar", esta estructura binaria será una fila.
Cuando queremos crear nuestros propios objetos específicos de dominio, especificamos una clase de caso en Scala o un
JavaBean en Java. Spark nos permitirá manipular este objeto (en lugar de una Fila) de forma distribuida.

Cuando usa la API de conjunto de datos, para cada fila que toca, este dominio especifica el tipo, Spark convierte
el formato de Spark Row al objeto que especificó (una clase de caso o una clase de Java). Esta conversión
ralentiza sus operaciones pero puede proporcionar más flexibilidad. Notará un impacto en el rendimiento, pero este es un
orden de magnitud muy diferente de lo que podría ver en algo como una función definida por el usuario (UDF)
en Python, porque los costos de rendimiento no son tan extremos como cambiar los lenguajes de programación, pero
es algo importante a tener en cuenta.

Cuándo usar conjuntos de datos

Puede reflexionar, si voy a pagar una penalización de rendimiento cuando uso conjuntos de datos, ¿por qué debería
usarlos? Si tuviéramos que condensar esto en una lista canónica, aquí hay un par de
razones:

Cuando la(s) operación(es) que le gustaría realizar no se pueden expresar mediante manipulaciones de
DataFrame

Cuando desea o necesita seguridad tipográfica y está dispuesto a aceptar el costo de


Machine Translated by Google

rendimiento para lograrlo

Vamos a explorar estos con más detalle. Hay algunas operaciones que no se pueden expresar usando las API
estructuradas que hemos visto en los capítulos anteriores. Aunque estos no son particularmente comunes, es
posible que tenga un gran conjunto de lógica comercial que le gustaría codificar en una función específica en lugar de
en SQL o DataFrames. Este es un uso apropiado para conjuntos de datos. Además, la API del conjunto de datos es de
tipo seguro. Las operaciones que no son válidas para sus tipos, por ejemplo, restar dos tipos de cadena, fallarán en
el tiempo de compilación, no en el tiempo de ejecución. Si la corrección y el código a prueba de balas son su máxima
prioridad, a costa de algo de rendimiento, esta puede ser una excelente opción para usted. Esto no lo protege de datos
mal formados, pero puede permitirle manejarlos y organizarlos de manera más elegante.

Otro posible momento en el que podría querer usar conjuntos de datos es cuando le gustaría reutilizar una variedad de
transformaciones de filas enteras entre cargas de trabajo de un solo nodo y cargas de trabajo de Spark.
Si tiene algo de experiencia con Scala, puede notar que las API de Spark reflejan las de los tipos de secuencia de
Scala, pero funcionan de forma distribuida. De hecho, Martin Odersky, el inventor de Scala, dijo eso mismo en 2015
en Spark Summit Europe. Debido a esto, una ventaja de usar conjuntos de datos es que si define todos sus datos y
transformaciones como clases de casos de aceptación, es trivial reutilizarlos para cargas de trabajo locales y distribuidas.
Además, cuando recopile sus DataFrames en el disco local, serán de la clase y el tipo correctos, lo que a veces facilitará
la manipulación adicional.

Probablemente, el caso de uso más popular es usar DataFrames y Datasets en tándem, intercambiando
manualmente entre rendimiento y seguridad de tipo cuando es más relevante para su carga de trabajo.
Esto podría ser al final de una gran transformación de extracción, transformación y carga (ETL) basada en
DataFrame cuando desee recopilar datos en el controlador y manipularlos mediante el uso de bibliotecas de un solo
nodo, o podría ser al principio de una transformación cuando necesita realizar un análisis por fila antes de realizar el
filtrado y una mayor manipulación en Spark SQL.

Creación de conjuntos de datos

La creación de conjuntos de datos es algo así como una operación manual, lo que requiere que conozca y defina
los esquemas con anticipación.

En Java: Codificadores

Los codificadores de Java son bastante simples, simplemente especifica su clase y luego la codificará cuando
encuentre su DataFrame (que es del tipo Dataset<Row>):

importar org.apache.spark.sql.Encoders;

vuelo de clase pública implementa Serializable{


Cadena DEST_COUNTRY_NAME;
Cadena ORIGIN_COUNTRY_NAME;
Largo DEST_COUNTRY_NAME;
}
Machine Translated by Google

Conjunto de datos<Vuelo> vuelos =


chispa.leer .parquet("/data/flight­data/parquet/2010­
summary.parquet/") .as(Encoders.bean(Vuelo.clase));

En Scala: Clases de casos


Para crear conjuntos de datos en Scala, defina una clase de caso de Scala. Una clase de caso es una clase regular que tiene
las siguientes características:

Inmutable

Descomponible a través de la coincidencia de patrones

Permite la comparación basada en la estructura en lugar de la referencia

Fácil de usar y manipular

Estas características lo hacen bastante valioso para el análisis de datos porque es bastante fácil razonar sobre una clase de
caso. Probablemente la característica más importante es que las clases de casos son inmutables y permiten la comparación por
estructura en lugar de valor.

Así es como lo describe la documentación de Scala :

La inmutabilidad lo libera de la necesidad de realizar un seguimiento de dónde y cuándo se mutan las cosas.

La comparación por valor le permite comparar instancias como si fueran valores primitivos: no más incertidumbre con
respecto a si las instancias de una clase se comparan por valor o referencia.

La coincidencia de patrones simplifica la lógica de bifurcación, lo que genera menos errores y un código más legible.

Estas ventajas también se trasladan a su uso dentro de Spark.

Para comenzar a crear un conjunto de datos, definamos una clase de caso para uno de nuestros conjuntos de datos:

clase de caso Vuelo (DEST_COUNTRY_NAME: Cadena,


ORIGIN_COUNTRY_NAME: Cadena, recuento: BigInt)

Ahora que definimos una clase de caso, esto representará un solo registro en nuestro conjunto de datos. Más sucintamente,
ahora tenemos un conjunto de datos de vuelos. Esto no define ningún método para nosotros, simplemente el esquema. Cuando

leamos nuestros datos, obtendremos un DataFrame. Sin embargo, simplemente usamos el método as para convertirlo en
nuestro tipo de fila especificado:

val vuelosDF = chispa.leer


.parquet("/data/flight­data/parquet/2010­summary.parquet/") val vuelos =
vuelosDF.as[Vuelo]
Machine Translated by Google

Comportamiento

Aunque podemos ver el poder de los conjuntos de datos, lo que es importante entender es que acciones como
recopilar, tomar y contar se aplican a si estamos usando conjuntos de datos o marcos de datos:

vuelos.mostrar(2)

+­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­+­­­­­+
|DEST_COUNTRY_NAME|ORIGIN_COUNTRY_NAME|recuento|
+­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­+­­­­­+
Estados Unidos| Rumania| 1|
|| Estados Unidos| Irlanda| 264|
+­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­+­­­­­+

También notará que cuando vamos a acceder a una de las clases de casos, no necesitamos hacer ningún tipo de
coerción, simplemente especificamos el atributo con nombre de la clase de casos y obtenemos, no solo el valor
esperado sino el tipo esperado, también:

vuelos.first.DEST_COUNTRY_NAME // Estados Unidos

Transformaciones
Las transformaciones en Datasets son las mismas que vimos en DataFrames. Cualquier transformación
sobre la que lea en esta sección es válida en un conjunto de datos, y lo alentamos a consultar las secciones
específicas sobre agregaciones o uniones relevantes.

Además de esas transformaciones, los conjuntos de datos nos permiten especificar transformaciones más
complejas y fuertemente tipadas que las que podríamos realizar solo en marcos de datos porque manipulamos tipos
de máquinas virtuales Java (JVM) sin formato. Para ilustrar esta manipulación de objetos sin procesar, filtremos
el conjunto de datos que acaba de crear.

Filtración
Veamos un ejemplo simple creando una función simple que acepte un Vuelo y devuelva un valor booleano que
describa si el origen y el destino son iguales. Esto no es un UDF (al menos, en la forma en que Spark SQL define el
UDF) sino una función genérica.

CONSEJO

Notará en el siguiente ejemplo que vamos a crear una función para definir este filtro. Esta es una diferencia
importante de lo que hemos hecho hasta ahora en el libro. Al especificar una función, obligamos a Spark a
evaluar esta función en cada fila de nuestro conjunto de datos. Esto puede ser muy intensivo en
recursos. Para filtros simples, siempre se prefiere escribir expresiones SQL. Esto reducirá en gran medida
el costo de filtrar los datos y aún le permitirá manipularlos como un conjunto de datos más adelante:
Machine Translated by Google

def origenEsDestino(vuelo_fila: Vuelo): Booleano = {


return fila_vuelo.ORIGIN_COUNTRY_NAME == fila_vuelo.DEST_COUNTRY_NAME
}

Ahora podemos pasar esta función al método de filtro especificando que para cada fila debe verificar que esta función devuelve
verdadero y en el proceso filtrará nuestro conjunto de datos en consecuencia:

vuelos.filter(flight_row => originIsDestination(flight_row)).primero()

El resultado es:

Vuelo = Vuelo(Estados Unidos,Estados Unidos,348113)

Como vimos anteriormente, esta función no necesita ejecutarse en código Spark. Al igual que nuestros UDF, podemos
usarlo y probarlo en datos en nuestras máquinas locales antes de usarlo dentro de Spark.

Por ejemplo, este conjunto de datos es lo suficientemente pequeño para que lo recopilemos al controlador (como una matriz
de vuelos) en el que podemos operar y realizar exactamente la misma operación de filtrado:

vuelos.collect().filter(flight_row => originIsDestination(flight_row))

El resultado es:

Array[Vuelo] = Array(Vuelo(Estados Unidos,Estados Unidos,348113))

Podemos ver que obtenemos exactamente la misma respuesta que antes.

Cartografía
El filtrado es una transformación simple, pero a veces es necesario asignar un valor a otro valor.
Hicimos esto con nuestra función en el ejemplo anterior: acepta un vuelo y devuelve un booleano, pero otras veces podríamos
necesitar realizar algo más sofisticado como extraer un valor, comparar un conjunto de valores o algo similar.

El ejemplo más simple es manipular nuestro conjunto de datos de manera que extraigamos un valor de cada fila.
Esto está realizando efectivamente un DataFrame como seleccionar en nuestro conjunto de datos. Extraigamos el
destino:

val destinos = vuelos.map(f => f.DEST_COUNTRY_NAME)

Tenga en cuenta que terminamos con un conjunto de datos de tipo String. Esto se debe a que Spark ya conoce el tipo de
JVM que debe devolver este resultado y nos permite beneficiarnos de la verificación en tiempo de compilación si, por algún
motivo, no es válido.

Podemos recopilar esto y recuperar una serie de cadenas en el controlador:


Machine Translated by Google

val localDestinations = destinos.take(5)

Esto puede parecer trivial e innecesario; podemos hacer la mayor parte de este derecho en DataFrames. De hecho, le
recomendamos que haga esto porque obtiene muchos beneficios al hacerlo. Obtendrá ventajas como la generación de código
que simplemente no son posibles con funciones arbitrarias definidas por el usuario. Sin embargo, esto puede resultar útil
con una manipulación fila por fila mucho más sofisticada.

Uniones

Las uniones, como vimos anteriormente, se aplican de la misma manera que para los marcos de datos. Sin embargo, los
conjuntos de datos también proporcionan un método más sofisticado, el método joinWith. joinWith es aproximadamente igual a
un cogrupo (en la terminología de RDD) y básicamente termina con dos conjuntos de datos anidados dentro de uno.
Cada columna representa un conjunto de datos y estos se pueden manipular en consecuencia. Esto puede ser útil
cuando necesita mantener más información en la combinación o realizar una manipulación más sofisticada en
todo el resultado, como un mapa o filtro avanzado.

Vamos a crear un conjunto de datos de metadatos de vuelo falso para demostrar joinWith:

clase de caso FlightMetadata (recuento: BigInt, randomData: BigInt)

val vuelosMeta = chispa.rango(500).map(x => (x,


scala.util.Random.nextLong)) .withColumnRenamed("_1",
"count").withColumnRenamed("_2", "randomData") . como[Metadatos de vuelo]

val vuelos2 = vuelos


.joinWith(vuelosMeta, vuelos.col("recuento") === vuelosMeta.col("recuento"))

Tenga en cuenta que terminamos con un conjunto de datos de una especie de par clave­valor, en el que cada fila representa
un vuelo y los metadatos del vuelo. Por supuesto, podemos consultarlos como un conjunto de datos o un marco de datos con
tipos complejos:

vuelos2.selectExpr("_1.DEST_COUNTRY_NAME")

Podemos recopilarlos tal como lo hicimos antes:

vuelos2.tomar(2)

Array[(Vuelo, FlightMetadata)] = Array((Vuelo(Estados Unidos,Rumania,1),...

Por supuesto, una unión "regular" también funcionaría bastante bien, aunque notará que en este caso terminamos con un
DataFrame (y por lo tanto perdemos nuestra información de tipo de JVM).

val vuelos2 = vuelos.join(vuelosMeta, Seq("recuento"))


Machine Translated by Google

Siempre podemos definir otro conjunto de datos para recuperar esto. También es importante tener en cuenta que no hay
problemas para unir un DataFrame y un Dataset; terminamos con el mismo resultado:

val vuelos2 = vuelos.join(vuelosMeta.toDF(), Seq("recuento"))

Agrupaciones y Agregaciones
La agrupación y las agregaciones siguen los mismos estándares fundamentales que vimos en el capítulo de agregación
anterior, por lo que groupBy rollup y cube aún se aplican, pero estos devuelven DataFrames en lugar de Datasets (se
pierde la información de tipo):

vuelos.groupBy("DEST_COUNTRY_NAME").count()

Esto a menudo no es un gran problema, pero si desea mantener la información de tipo, hay otras agrupaciones y
agregaciones que puede realizar. Un excelente ejemplo es el método groupByKey. Esto le permite agrupar por una clave
específica en el conjunto de datos y obtener un conjunto de datos escrito a cambio. Esta función, sin embargo, no
acepta un nombre de columna específico sino una función. Esto le permite especificar funciones de agrupación más
sofisticadas que son mucho más parecidas a algo como esto:

vuelos.groupByKey(x => x.DEST_COUNTRY_NAME).count()

Aunque esto proporciona flexibilidad, es una compensación porque ahora estamos introduciendo tipos de JVM, así como
funciones que Spark no puede optimizar. Esto significa que verá una diferencia de rendimiento y podemos ver esto cuando
inspeccionamos el plan de explicación. A continuación, puede ver que estamos agregando efectivamente una nueva
columna al DataFrame (el resultado de nuestra función) y luego realizando la agrupación en eso:

vuelos.groupByKey(x => x.DEST_COUNTRY_NAME).count().explain

== Plano físico ==
*HashAgregate(claves=[valor#1396], funciones=[recuento(1)])
+­ Intercambio de partición hash (valor # 1396, 200)
+­ *HashAgregate(claves=[valor#1396], funciones=[recuento_parcial(1)])
+­ *Proyecto [valor#1396]
+­ AppendColumns <función1>, nueva instancia (clase ...
[invocación estática (clase org.apache.spark.unsafe.types.UTF8String, ...
+­ *FileScan parqué [D...

Después de realizar una agrupación con una clave en un conjunto de datos, podemos operar en el conjunto de datos de
valor clave con funciones que manipularán las agrupaciones como objetos sin procesar:

def grpSum(nombrePaís:String, valores: Iterador[Vuelo]) =


{ valores.dropWhile(_.count < 5).map(x => (nombrePaís, x)) }

vuelos.groupByKey(x => x.DEST_COUNTRY_NAME) .flatMapGroups(grpSum).mostrar(5)


Machine Translated by Google

+­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
| _1| _2|
+­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
|Anguila|[Anguila,Unidos ...| |Paraguay|
[Paraguay,Unido ...| | Rusia|
[Rusia,Estados Unidos...| | Senegal|
[Senegal,Estados Unidos...| | Suecia|
[Suecia,Estados Unidos...|
+­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+

def grpSum2(f:Vuelo):Entero = {
1

} vuelos.groupByKey(x => x.DEST_COUNTRY_NAME).mapValues(grpSum2).count().take(5)

Incluso podemos crear nuevas manipulaciones y definir cómo se deben reducir los grupos:

def sum2(izquierda:Vuelo, derecha:Vuelo) = {


Vuelo (left.DEST_COUNTRY_NAME, null, left.count + right.count)

} vuelos.groupByKey(x => x.DEST_COUNTRY_NAME).reduceGroups((l, r) => sum2(l, r))


.tomar(5)

Debería ser lo suficientemente sencillo como para comprender que este es un proceso más costoso que agregar
inmediatamente después del escaneo, especialmente porque termina en el mismo resultado final:

vuelos.groupBy("DEST_COUNTRY_NAME").count().explain

== Plan físico ==
*HashAgregate(keys=[DEST_COUNTRY_NAME#1308], functions=[count(1)])
+­ Intercambio de partición hash (DEST_COUNTRY_NAME#1308, 200)
+­ *HashAgregate(keys=[DEST_COUNTRY_NAME#1308], functions=[parcial_count(1)])
+­ *FileScan parquet [DEST_COUNTRY_NAME#1308] Lote: tru...

Esto debería motivar el uso de conjuntos de datos solo con codificación definida por el usuario quirúrgicamente y solo donde
tenga sentido. Esto podría ser al comienzo de una canalización de big data o al final de una.

Conclusión
En este capítulo, cubrimos los conceptos básicos de los conjuntos de datos y brindamos algunos ejemplos motivadores.
Aunque breve, este capítulo le enseña básicamente todo lo que necesita saber sobre los conjuntos de datos y cómo
usarlos. Puede ser útil pensar en ellos como una combinación entre las API estructuradas de nivel superior y las API RDD
de bajo nivel, que es el tema del Capítulo 12.
Machine Translated by Google

Parte III. API de bajo nivel


Machine Translated by Google

Capítulo 12. Conjuntos de datos


distribuidos resilientes (RDD)

La parte anterior del libro cubrió las API estructuradas de Spark. Debe favorecer en gran medida estas API en casi todos
los escenarios. Dicho esto, hay momentos en que la manipulación de alto nivel no resolverá el problema comercial o
de ingeniería que está tratando de resolver. Para esos casos, es posible que deba usar las API de nivel inferior de
Spark, específicamente el conjunto de datos distribuido resistente (RDD), SparkContext y variables compartidas
distribuidas como acumuladores y variables de transmisión. Los capítulos que siguen en esta parte cubren estas
API y cómo usarlas.

ADVERTENCIA

Si es nuevo en Spark, este no es el lugar para comenzar. Comience con las API estructuradas, ¡será
más productivo más rápidamente!

¿Qué son las API de bajo nivel?


Hay dos conjuntos de API de bajo nivel: uno para manipular datos distribuidos (RDD) y otro para distribuir y manipular
variables compartidas distribuidas (variables de transmisión y acumuladores).

¿Cuándo usar las API de bajo nivel?


Por lo general, debe usar las API de nivel inferior en tres situaciones:

Necesita alguna funcionalidad que no puede encontrar en las API de nivel superior; por ejemplo, si necesita un
control muy estricto sobre la ubicación física de los datos en el clúster.

Debe mantener una base de código heredada escrita con RDD.

Necesita hacer alguna manipulación personalizada de variables compartidas. Discutiremos más las
variables compartidas en el Capítulo 14.

Esas son las razones por las que debe usar estas herramientas de nivel inferior, pero aún así es útil comprender
estas herramientas porque todas las cargas de trabajo de Spark se compilan en estas primitivas fundamentales.
Cuando llama a una transformación DataFrame, en realidad solo se convierte en un conjunto de transformaciones
RDD. Esta comprensión puede facilitar su tarea a medida que comienza a depurar cargas de trabajo cada vez más
complejas.

Incluso si es un desarrollador avanzado que espera sacar el máximo provecho de Spark, le recomendamos que se centre
en las API estructuradas. Sin embargo, hay momentos en los que es posible que desee "desplegar"
Machine Translated by Google

a algunas de las herramientas de nivel inferior para completar su tarea. Es posible que deba desplegar estas
API para usar algún código heredado, implementar algún particionador personalizado o actualizar y rastrear el
valor de una variable en el transcurso de la ejecución de una canalización de datos. Estas herramientas le
brindan un control más detallado a expensas de evitar que se dispare en el pie.

¿Cómo usar las API de bajo nivel?

Un SparkContext es el punto de entrada para la funcionalidad de API de bajo nivel. Accede a él a través de
SparkSession, que es la herramienta que utiliza para realizar cálculos en un clúster de Spark. Hablamos de
esto más a fondo en el Capítulo 15 , pero por ahora, simplemente necesita saber que puede acceder a un
SparkContext a través de la siguiente llamada:

chispa.chispaContexto

Acerca de los RDD

Los RDD fueron la API principal en la serie Spark 1.X y todavía están disponibles en 2.X, pero no se usan con
tanta frecuencia. Sin embargo, como hemos señalado anteriormente en este libro, prácticamente todo el código
de Spark que ejecuta, ya sean marcos de datos o conjuntos de datos, se compila en un RDD. La interfaz
de usuario de Spark, que se trata en la siguiente parte del libro, también describe la ejecución del trabajo en
términos de RDD. Por lo tanto, le convendrá tener al menos una comprensión básica de qué es un RDD y cómo usarlo.

En resumen, un RDD representa una colección inmutable y dividida de registros que se pueden operar en paralelo.
Sin embargo, a diferencia de los DataFrames, donde cada registro es una fila estructurada que contiene
campos con un esquema conocido, en los RDD los registros son solo objetos Java, Scala o Python a elección
del programador.

Los RDD le brindan un control completo porque cada registro en un RDD es solo un objeto Java o Python.
Puede almacenar lo que quiera en estos objetos, en cualquier formato que desee. Esto le da un gran poder, pero
no sin problemas potenciales. Cada manipulación e interacción entre valores debe definirse a mano, lo que significa
que debe "reinventar la rueda" para cualquier tarea que intente realizar. Además, las optimizaciones requerirán
mucho más trabajo manual, porque Spark no comprende la estructura interna de sus registros como lo hace
con las API estructuradas.
Por ejemplo, las API estructuradas de Spark almacenan datos automáticamente en un formato binario
comprimido y optimizado, por lo que para lograr la misma eficiencia de espacio y rendimiento, también
deberá implementar este tipo de formato dentro de sus objetos y todas las operaciones de bajo nivel para
calcular sobre ella. Del mismo modo, las optimizaciones como la reordenación de filtros y agregaciones que
ocurren automáticamente en Spark SQL deben implementarse a mano. Por este y otros motivos,
recomendamos encarecidamente utilizar las API estructuradas de Spark cuando sea posible.

La API de RDD es similar al conjunto de datos, que vimos en la parte anterior del libro, excepto que los RDD no
se almacenan ni se manipulan con el motor de datos estructurados. Sin embargo, es trivial realizar
conversiones entre RDD y conjuntos de datos, por lo que puede usar ambas API para aprovechar las fortalezas
y debilidades de cada API. Mostraremos cómo hacer esto a lo largo de este
Machine Translated by Google

parte del libro.

Tipos de RDD
Si revisa la documentación de la API de Spark, notará que hay muchas subclases de RDD. En su mayor parte, se trata de
representaciones internas que utiliza la API de DataFrame para crear planes de ejecución física optimizados. Sin
embargo, como usuario, es probable que solo cree dos tipos de RDD: el tipo de RDD "genérico" o un RDD de clave­valor
que proporciona funciones adicionales, como la agregación por clave. Para sus propósitos, estos serán los únicos
dos tipos de RDD que importan. Ambos solo representan una colección de objetos, pero los RDD clave­valor tienen
operaciones especiales, así como un concepto de partición personalizada por clave.

Definamos formalmente los RDD. Internamente, cada RDD se caracteriza por cinco propiedades principales:

Una lista de particiones

Una función para calcular cada división

Una lista de dependencias en otros RDD

Opcionalmente, un particionador para RDD clave­valor (p. ej., para decir que el RDD está particionado
mediante hash)

Opcionalmente, una lista de ubicaciones preferidas en las que calcular cada división (p. ej., ubicaciones de
bloque para un archivo del sistema de archivos distribuidos de Hadoop [HDFS])

NOTA

El particionador es probablemente una de las principales razones por las que podría querer usar RDD en su código.
Especificar su propio particionador personalizado puede brindarle mejoras significativas en el rendimiento y
la estabilidad si lo usa correctamente. Esto se analiza con mayor profundidad en el Capítulo 13 cuando presentamos
los RDD de pares clave­valor.

Estas propiedades determinan toda la capacidad de Spark para programar y ejecutar el programa del usuario.
Los diferentes tipos de RDD implementan sus propias versiones de cada una de las propiedades antes mencionadas, lo que
le permite definir nuevas fuentes de datos.

Los RDD siguen exactamente los mismos paradigmas de programación de Spark que vimos en capítulos anteriores.
Proporcionan transformaciones, que se evalúan con pereza, y acciones, que se evalúan con entusiasmo, para manipular
datos de forma distribuida. Estos funcionan de la misma manera que las transformaciones y acciones en DataFrames
y Datasets. Sin embargo, no existe el concepto de "filas" en los RDD; los registros individuales son solo objetos
Java/Scala/Python sin procesar, y usted los manipula manualmente en lugar de acceder al repositorio de funciones que
tiene en las API estructuradas.

Las API de RDD están disponibles en Python, así como en Scala y Java. Para Scala y Java, el rendimiento es en
su mayor parte el mismo, los grandes costos incurridos en la manipulación de la materia prima
Machine Translated by Google

objetos. Python, sin embargo, puede perder una cantidad sustancial de rendimiento al usar RDD.
Ejecutar RDD de Python equivale a ejecutar funciones definidas por el usuario (UDF) de Python fila por fila.
Tal como vimos en el Capítulo 6. Serializamos los datos en el proceso de Python, los operamos en Python y
luego los serializamos de nuevo en la Máquina Virtual de Java (JVM). Esto provoca una gran sobrecarga
para las manipulaciones de Python RDD. Aunque muchas personas ejecutaron código de producción con
ellos en el pasado, recomendamos construir sobre las API estructuradas en Python y solo descender a
RDD si es absolutamente necesario.

¿Cuándo usar los RDD?

En general, no debe crear RDD manualmente a menos que tenga una razón muy, muy específica para
hacerlo. Son una API de nivel mucho más bajo que brinda mucha potencia pero también carece de muchas de
las optimizaciones que están disponibles en las API estructuradas. Para la gran mayoría de los casos de
uso, los DataFrames serán más eficientes, más estables y más expresivos que los RDD.

La razón más probable por la que querrá usar RDD es porque necesita un control detallado sobre la
distribución física de los datos (particionamiento personalizado de los datos).

Conjuntos de datos y RDD de clases de casos

Notamos esta pregunta en la web y la encontramos interesante: ¿cuál es la diferencia entre los RDD de las
clases de casos y los conjuntos de datos? La diferencia es que los conjuntos de datos aún pueden tomar
aproveche la gran cantidad de funciones y optimizaciones que ofrecen las API estructuradas.
Con Datasets, no necesita elegir entre operar solo en tipos de JVM o en tipos de Spark, puede elegir lo
que sea más fácil de hacer o más flexible. Obtienes los dos mejores mundos.

Creación de RDD
Ahora que discutimos algunas propiedades clave de RDD, comencemos a aplicarlas para que pueda
comprender mejor cómo usarlas.

Interoperabilidad entre marcos de datos, conjuntos de datos y RDD


Una de las formas más fáciles de obtener RDD es a partir de un DataFrame o Dataset existente. Convertirlos
a un RDD es simple: solo use el método rdd en cualquiera de estos tipos de datos. Notará que si realiza una
conversión de un conjunto de datos [T] a un RDD, obtendrá el tipo T nativo apropiado (recuerde que esto
se aplica solo a Scala y Java):

// en Scala: convierte un Dataset[Largo] a RDD[Largo]


spark.range(500).rdd

Debido a que Python no tiene conjuntos de datos, solo tiene marcos de datos, obtendrá un RDD de tipo
Fila:
Machine Translated by Google

# en Python
chispa.rango(10).rdd

Para operar con estos datos, deberá convertir este objeto Fila al tipo de datos correcto o extraer valores de él, como se
muestra en el siguiente ejemplo. Este es ahora un RDD de tipo Fila:

// en Scala
spark.range(10).toDF().rdd.map(rowObject => rowObject.getLong(0))

# en Python
spark.range(10).toDF("id").rdd.map( fila lambda: fila[0])

Puede usar la misma metodología para crear un marco de datos o un conjunto de datos a partir de un RDD. Todo
lo que necesita hacer es llamar al método toDF en el RDD:

// en Scala
chispa.rango(10).rdd.toDF()

# en Python
spark.range(10).rdd.toDF()

Este comando crea un RDD de tipo Fila. Esta fila es el formato interno de Catalyst que Spark usa para representar
datos en las API estructuradas. Esta funcionalidad le permite saltar entre las API estructuradas y de bajo nivel
según se adapte a su caso de uso. (Hablamos de esto en el Capítulo 13.)

La API de RDD se sentirá bastante similar a la API de conjunto de datos en el Capítulo 11 porque son extremadamente
similares entre sí (los RDD son una representación de nivel inferior de los conjuntos de datos) que no tienen muchas de
las funciones e interfaces convenientes que tienen las API estructuradas. .

De una colección local


Para crear un RDD a partir de una colección, deberá usar el método de paralelización en un SparkContext
(dentro de una SparkSession). Esto convierte una colección de un solo nodo en una colección paralela. Al
crear esta colección paralela, también puede indicar explícitamente la cantidad de particiones en las que le
gustaría distribuir esta matriz. En este caso, estamos creando dos particiones:

// en Scala
val myCollection = "Spark The Definitive Guide: Big Data Processing Made Simple"
.dividir(" ")
val palabras = chispa.sparkContext.parallelize(myCollection, 2)

# en Python
myCollection = "Spark The Definitive Guide: Big Data Processing Made Simple"\
.split(" ")
palabras = chispa.sparkContext.parallelize(miColección, 2)
Machine Translated by Google

Una característica adicional es que luego puede nombrar este RDD para que aparezca en la interfaz de usuario de Spark de acuerdo
con un nombre dado:

// en Scala
palabras.setName("misPalabras")
palabras.nombre // misPalabras

# en Python
palabras.setName("misPalabras")
palabras.nombre() # misPalabras

De fuentes de datos
Aunque puede crear RDD a partir de fuentes de datos o archivos de texto, a menudo es preferible usar las API de fuentes de
datos. Los RDD no tienen una noción de "API de fuente de datos" como lo hacen los marcos de datos; definen principalmente sus
estructuras de dependencia y listas de particiones. La API de fuente de datos que vimos en el Capítulo 9 casi siempre es una mejor
manera de leer datos. Dicho esto, también puede leer datos como RDD usando sparkContext. Por ejemplo, leamos un archivo de

texto línea por línea:

spark.sparkContext.textFile("/alguna/ruta/conTextFiles")

Esto crea un RDD para el cual cada registro en el RDD representa una línea en ese archivo o archivos de texto.
Alternativamente, puede leer datos para los cuales cada archivo de texto debe convertirse en un solo registro. El caso de uso aquí
sería donde cada archivo es un archivo que consta de un objeto JSON grande o algún documento en el que operará como
individuo:

spark.sparkContext.wholeTextFiles("/alguna/ruta/conTextFiles")

En este RDD, el nombre del archivo es el primer objeto y el valor del archivo de texto es el segundo objeto de cadena.

Manipulación de RDD
Manipula los RDD de la misma manera que manipula los marcos de datos. Como se mencionó, la principal diferencia es que
manipula objetos Java o Scala sin formato en lugar de tipos Spark.
También hay una escasez de métodos o funciones de "ayuda" a los que puede recurrir para simplificar los cálculos. Más
bien, debe definir cada filtro, funciones de mapa, agregación y cualquier otra manipulación que desee como función.

Para demostrar cierta manipulación de datos, usemos el RDD simple (palabras) que creamos anteriormente para definir
algunos detalles más.

Transformaciones
Machine Translated by Google

En su mayor parte, muchas transformaciones reflejan la funcionalidad que encuentra en las API estructuradas.
Tal como lo hace con DataFrames y Datasets, especifica transformaciones en un RDD para crear otro. Al hacerlo,
definimos un RDD como una dependencia de otro junto con cierta manipulación de los datos contenidos en
ese RDD.

distinto
Una llamada de método distinta en un RDD elimina los duplicados del RDD:

palabras.distinto().contar()

Esto da un resultado de 10.

filtrar
Filtrar es equivalente a crear una cláusula where similar a SQL. Puede revisar nuestros registros en el RDD y ver
cuáles coinciden con alguna función de predicado. Esta función solo necesita devolver un tipo booleano para
usarse como una función de filtro. La entrada debe ser cualquiera que sea su fila dada. En este siguiente ejemplo,
filtramos el RDD para mantener solo las palabras que comienzan con la letra "S":

// en Scala
def comienza con S(individual:String) =
{ individual.comienzaCon("S") }

# en Python
def empieza con S(individual):
devuelve individual.empieza con("S")

Ahora que definimos la función, filtremos los datos. Esto debería resultarle bastante familiar si lee el Capítulo 11
porque simplemente usamos una función que opera registro por registro en el RDD. La función está definida
para trabajar en cada registro en el RDD individualmente:

// en Scala
palabras.filter(palabra => comienza con S(palabra)).collect()

# en Python
palabras.filter( palabra lambda: comienza con S(palabra)).collect()

Esto da un resultado de Spark y Simple. Podemos ver, como la API de conjunto de datos, que esto devuelve tipos
nativos. Esto se debe a que nunca obligamos a nuestros datos a escribir Fila, ni necesitamos convertir los datos
después de recopilarlos.

mapa
Machine Translated by Google

El mapeo es nuevamente la misma operación sobre la que puede leer en el Capítulo 11. Usted especifica una función
que devuelve el valor que desea, dada la entrada correcta. Luego aplica eso, registro por registro. Realicemos algo similar a lo
que acabamos de hacer. En este ejemplo, asignaremos la palabra actual a la palabra, su letra inicial y si la palabra comienza
con "S".

Observe en este caso que definimos nuestras funciones completamente en línea usando la sintaxis lambda relevante:

// en Scala val
palabras2 = palabras.mapa(palabra => (palabra, palabra(0), palabra.comienzaCon("S")))

# en Python
palabras2 = palabras.mapa(palabra lambda : (palabra, palabra[0], palabra.comienza con("S")))

Posteriormente, puede filtrar esto seleccionando el valor booleano relevante en una nueva función:

// en Scala
palabras2.filter(registro => registro._3).tomar(5)

# en Python
words2.filter( registro lambda: registro[2]).take(5)

Esto devuelve una tupla de "Spark", "S" y "true", así como "Simple", "S" y "True".

mapa plano

flatMap proporciona una extensión simple de la función de mapa que acabamos de ver. A veces, cada fila actual debería
devolver varias filas en su lugar. Por ejemplo, es posible que desee tomar su conjunto de palabras y convertirlo en un conjunto

de caracteres. Debido a que cada palabra tiene varios caracteres, debe usar flatMap para expandirla. flatMap requiere que la

salida de la función de mapa sea un iterable que se pueda expandir:

// en Scala
palabras.flatMap(palabra => palabra.toSeq).take(5)

# en Python
words.flatMap(palabra lambda : lista(palabra)).take(5)

Esto produce S, P, A, R, K.

clasificar

Para ordenar un RDD, debe usar el método sortBy y, al igual que cualquier otra operación de RDD, lo hace especificando una
función para extraer un valor de los objetos en sus RDD y luego ordenar en función de eso. Por ejemplo, el siguiente
ejemplo ordena por longitud de palabra de la más larga a la más corta:

// en escala
Machine Translated by Google

palabras.sortBy(palabra => palabra.longitud() * ­1).tomar(2)

# en Python
palabras.sortBy(palabra lambda : len(palabra) * ­1).take(2)

Divisiones aleatorias
También podemos dividir aleatoriamente un RDD en una matriz de RDD utilizando el método randomSplit, que acepta
una matriz de pesos y una semilla aleatoria:

// en Scala
val 50FiftySplit = palabras.randomSplit(Array[Double](0.5, 0.5))

# en Python
50FiftySplit = palabras.randomSplit([0.5, 0.5])

Esto devuelve una matriz de RDD que puede manipular individualmente.

Comportamiento

Tal como lo hacemos con DataFrames y Datasets, especificamos acciones para iniciar nuestras
transformaciones especificadas. Las acciones recopilan datos en el controlador o escriben en una fuente de datos externa.

reducir
Puede usar el método de reducción para especificar una función para "reducir" un RDD de cualquier tipo de valor a
un valor. Por ejemplo, dado un conjunto de números, puede reducirlo a su suma especificando una función que tome
como entrada dos valores y los reduzca a uno. Si tienes experiencia en programación funcional, este no debería
ser un concepto nuevo:

// en Scala
spark.sparkContext.parallelize(1 a 20).reduce(_ + _) // 210

# en Python
spark.sparkContext.parallelize(range(1, 21)).reduce(lambda x, y: x + y) # 210

También puede usar esto para obtener algo así como la palabra más larga en nuestro conjunto de palabras que
definimos hace un momento. La clave es simplemente definir la función correcta:

// en Scala
def wordLengthReducer(leftWord:String, rightWord:String): String = {
if (palabraizquierda.longitud > palabraderecha.longitud)
devuelve

palabraizquierda else devuelve palabraderecha


}
Machine Translated by Google

palabras.reduce(palabraLengthReducer)

# en Python
def wordLengthReducer(palabraizquierda, palabraderecha):
if len(palabraizquierda) > len(palabraderecha):
return palabraizquierda
else:
return palabraderecha

palabras.reduce(palabraLengthReducer)

Este reductor es un buen ejemplo porque puede obtener una de dos salidas. Debido a que la operación de reducción en las
particiones no es determinista, puede tener "definitivo" o "procesamiento" (ambos de longitud 10) como la palabra "izquierda".
Esto significa que a veces puedes terminar con uno, mientras que otras veces terminas con el otro.

contar
Este método es bastante autoexplicativo. Utilizándolo, podría, por ejemplo, contar el número de filas en el RDD:

palabras.contar()

recuento aproximado

Aunque la firma de retorno de este tipo es un poco extraña, es bastante sofisticada. Esta es una aproximación del método de

conteo que acabamos de ver, pero debe ejecutarse dentro de un tiempo de espera (y puede devolver resultados incompletos si
excede el tiempo de espera).

La confianza es la probabilidad de que los límites de error del resultado contengan el valor verdadero.

Es decir, si se llamara repetidamente a countApprox con una confianza de 0,9, esperaríamos que el 90 % de los resultados
contuvieran el recuento real. La confianza debe estar en el rango [0,1], o se lanzará una excepción:

val confianza = 0,95 val


tiempo de espera Millisegundos = 400
palabras.contar aproximadamente (tiempo de espera Millisegundos, confianza)

recuentoAproximadoDistinto

Hay dos implementaciones de esto, ambas basadas en la implementación de streamlib de "HyperLogLog en la


práctica: ingeniería algorítmica de un algoritmo de estimación de cardinalidad de última generación".

En la primera implementación, el argumento que pasamos a la función es la precisión relativa.


Los valores más pequeños crean contadores que requieren más espacio. El valor debe ser mayor que 0.000017:
Machine Translated by Google

palabras.countApproxDistinct(0.05)

Con la otra implementación tienes un poco más de control; usted especifica la precisión relativa basada en dos
parámetros: uno para datos "regulares" y otro para una representación escasa.

Los dos argumentos son p y sp, donde p es precisión y sp es precisión escasa. La precisión relativa es de
PAG

aproximadamente 1,054 / sqrt(2 ). Establecer un valor distinto de cero (sp > p) puede reducir el consumo
de memoria y aumentar la precisión cuando la cardinalidad es pequeña. Ambos valores son enteros:

palabras.countApproxDistinct(4, 10)

contar por valor

Este método cuenta el número de valores en un RDD dado. Sin embargo, lo hace cargando finalmente el conjunto
de resultados en la memoria del controlador. Debe usar este método solo si se espera que el mapa resultante sea
pequeño porque todo se carga en la memoria del controlador. Por lo tanto, este método solo tiene sentido en
un escenario en el que el número total de filas es bajo o el número de elementos distintos es bajo:

palabras.countByValue()

recuento por valor aproximado

Esto hace lo mismo que la función anterior, pero lo hace como una aproximación. Esto debe ejecutarse dentro del
tiempo de espera especificado (primer parámetro) (y puede devolver resultados incompletos si excede el
tiempo de espera).

La confianza es la probabilidad de que los límites de error del resultado contengan el valor verdadero.
Es decir, si se llamara repetidamente a countApprox con una confianza de 0,9, esperaríamos que el 90 % de los
resultados contuvieran el recuento real. La confianza debe estar en el rango [0,1], o se lanzará una excepción:

palabras.countByValueApprox(1000, 0.95)

primero

El primer método devuelve el primer valor en el conjunto de datos:

palabras.primero()

máximo y mínimo

max y min devuelven los valores máximo y mínimo, respectivamente:

chispa.sparkContext.parallelize(1 a 20).max()
Machine Translated by Google

chispa.sparkContext.parallelize(1 a 20).min()

llevar

take y sus métodos derivados toman una serie de valores de su RDD. Esto funciona escaneando primero
una partición y luego usando los resultados de esa partición para estimar la cantidad de particiones
adicionales necesarias para satisfacer el límite.

Hay muchas variaciones de esta función, como takeOrdered, takeSample y top. Puede usar takeSample
para especificar una muestra aleatoria de tamaño fijo de su RDD. Puede especificar si esto debe hacerse
usando withReplacement, el número de valores, así como la semilla aleatoria. top es efectivamente lo
contrario de takeOrdered en el sentido de que selecciona los valores principales de acuerdo con el
orden implícito:

palabras.tomar(5)
palabras.tomarPedido(5)

palabras.superior(5) val
conReemplazo = true val
númeroParaTomar = 6
valsemilla aleatoria = 100L palabras.tomarMuestra(conReemplazo, númeroParaTomar, semilla aleatoria)

Guardar archivos
Guardar archivos significa escribir en archivos de texto sin formato. Con los RDD, en realidad no puede
"guardar" en una fuente de datos en el sentido convencional. Debe iterar sobre las particiones para
guardar el contenido de cada partición en alguna base de datos externa. Este es un enfoque de bajo nivel
que revela la operación subyacente que se realiza en las API de nivel superior. Spark tomará cada
partición y la escribirá en el destino.

guardar como archivo de texto

Para guardar en un archivo de texto, simplemente especifique una ruta y, opcionalmente, un códec de compresión:

palabras.saveAsTextFile("archivo:/tmp/título del libro")

Para establecer un códec de compresión, debemos importar el códec adecuado de Hadoop. Puede
encontrarlos en la biblioteca org.apache.hadoop.io.compress:

// en Scala
import org.apache.hadoop.io.compress.BZip2Codec
words.saveAsTextFile("file:/tmp/bookTitleCompressed", classOf[BZip2Codec])

Archivos de secuencia
Machine Translated by Google

Spark originalmente surgió del ecosistema de Hadoop, por lo que tiene una integración bastante estrecha con una variedad

de herramientas de Hadoop. Un archivo de secuencia es un archivo plano que consta de pares binarios de clave­valor. Se usa
ampliamente en MapReduce como formatos de entrada/salida.

Spark puede escribir en los archivos de secuencia usando el método saveAsObjectFile o escribiendo explícitamente pares clave­
valor, como se describe en el Capítulo 13:

palabras.saveAsObjectFile("/tmp/my/sequenceFilePath")

Archivos Hadoop
Hay una variedad de diferentes formatos de archivo de Hadoop en los que puede guardar. Estos le permiten especificar clases,
formatos de salida, configuraciones de Hadoop y esquemas de compresión. (Para obtener información sobre estos
formatos, lea Hadoop: The Definitive Guide [O'Reilly, 2015]). Estos formatos son en gran medida irrelevantes, excepto si
está trabajando profundamente en el ecosistema de Hadoop o con algunos trabajos heredados de mapReduce.

almacenamiento en caché

Se aplican los mismos principios para el almacenamiento en caché de RDD que para DataFrames y Datasets. Puede almacenar
en caché o conservar un RDD. De manera predeterminada, la memoria caché y la persistencia solo manejan datos en la

memoria. Podemos nombrarlo si usamos la función setName a la que hicimos referencia anteriormente en este capítulo:

palabras.cache()

Podemos especificar un nivel de almacenamiento como cualquiera de los niveles de almacenamiento en el

objeto singleton: org.apache.spark.storage.StorageLevel, que son combinaciones de memoria solamente; solo disco; y por
separado, fuera del montón.

Posteriormente, podemos consultar este nivel de almacenamiento (hablamos de los niveles de almacenamiento cuando discutimos
la persistencia en el Capítulo 20):

// en Scala
palabras.getStorageLevel

# en palabras
de Python.getStorageLevel()

punto de control
Una característica que no está disponible en la API de DataFrame es el concepto de puntos de control. La creación de puntos de
control es el acto de guardar un RDD en el disco para que las referencias futuras a este RDD apunten a esas
particiones intermedias en el disco en lugar de volver a calcular el RDD desde su fuente original. Esto es similar al almacenamiento
en caché, excepto que no se almacena en la memoria, solo en el disco. Esto puede ser útil cuando
Machine Translated by Google

realizando cálculos iterativos, similares a los casos de uso para el almacenamiento en caché:

chispa.sparkContext.setCheckpointDir("/alguna/ruta/para/punto de control")
palabras.punto de control()

Ahora, cuando hagamos referencia a este RDD, se derivará del punto de control en lugar de los datos de origen.
Esta puede ser una optimización útil.

Conectar RDD a los comandos del sistema


El método de tubería es probablemente uno de los métodos más interesantes de Spark. Con pipe, puede devolver un
RDD creado por elementos de tubería a un proceso externo bifurcado. El RDD resultante se calcula ejecutando el proceso
dado una vez por partición. Todos los elementos de cada partición de entrada se escriben en la entrada estándar de un proceso
como líneas de entrada separadas por una nueva línea. La partición resultante consta de la salida estándar del proceso, y
cada línea de la salida estándar da como resultado un elemento de la partición de salida. Se invoca un proceso incluso para
particiones vacías.

El comportamiento de impresión se puede personalizar proporcionando dos funciones.

Podemos usar un ejemplo simple y canalizar cada partición al comando wc. Cada fila se pasará como una nueva línea,
por lo que si realizamos un conteo de líneas, obtendremos el número de líneas, una por partición:

palabras.pipe("wc ­l").collect()

En este caso, tenemos cinco líneas por partición.

mapPartitions
El comando anterior reveló que Spark opera por partición cuando se trata de ejecutar código. Es posible que también haya
notado anteriormente que la firma de retorno de una función de mapa en un RDD es en realidad MapPartitionsRDD. Esto se
debe a que map es solo un alias de filas para mapPartitions, lo que le permite mapear una partición individual (representada
como un iterador). Eso es porque físicamente en el clúster operamos en cada partición individualmente (y no en una fila
específica). Un ejemplo simple crea el valor "1" para cada partición en nuestros datos, y la suma de la siguiente expresión
contará la cantidad de particiones que tenemos:

// en Scala
palabras.mapPartitions(part => Iterator[Int](1)).sum() // 2

# en Python
words.mapPartitions( parte lambda: [1]).sum() # 2

Naturalmente, esto significa que operamos por partición y nos permite realizar una operación en toda la partición. Esto
es valioso para realizar algo en todo un
Machine Translated by Google

subconjunto de datos de su RDD. Puede reunir todos los valores de una clase o grupo de partición en una
partición y luego operar en todo ese grupo usando funciones y controles arbitrarios. Un caso de uso de ejemplo
de esto sería que podría canalizar esto a través de algún algoritmo de aprendizaje automático personalizado
y entrenar un modelo individual para la parte del conjunto de datos de esa empresa. Un ingeniero de Facebook
tiene una demostración interesante de su implementación particular del operador de tubería con un caso de
uso similar demostrado en Spark Summit East 2017.

Otras funciones similares a mapPartitions incluyen mapPartitionsWithIndex. Con esto, especifica una
función que acepta un índice (dentro de la partición) y un iterador que recorre todos los elementos dentro de la
partición. El índice de partición es el número de partición en su RDD, que identifica dónde se encuentra
cada registro en nuestro conjunto de datos (y potencialmente le permite depurar). Puede usar esto para probar
si las funciones de su mapa se están comportando correctamente:

// en Scala
def indexedFunc(partitionIndex:Int, withinPartIterator: Iterator[String]) = {
withinPartIterator.toList.map( value =>
s"Partition: $partitionIndex => $value").iterator

} palabras.mapPartitionsWithIndex(indexedFunc).collect()

# en Python
def indexedFunc(partitionIndex, withinPartIterator):
return ["partición: {} => {}".format(partitionIndex, x) for x in
withinPartIterator]
palabras.mapPartitionsWithIndex(indexedFunc).collect()

para cada partición


Aunque mapPartitions necesita un valor de retorno para funcionar correctamente, esta siguiente función
no lo necesita. foreachPartition simplemente itera sobre todas las particiones de los datos. La diferencia es
que la función no tiene valor de retorno. Esto lo hace ideal para hacer algo con cada partición, como
escribirlo en una base de datos. De hecho, así es como se escriben muchos conectores de origen de datos.
Puede crear nuestra propia fuente de archivo de texto si lo desea especificando salidas al directorio
temporal con una ID aleatoria:

palabras.foreachPartition { iter =>


import java.io._
import scala.util.Random val
randomFileName = new Random().nextInt() val pw =
new PrintWriter(new File(s"/tmp/random­file­${randomFileName}.txt")) while (iter.hasNext)
{ pw.write(iter.next())

} pw.cerrar() }

Encontrará estos dos archivos si escanea su directorio /tmp .


Machine Translated by Google

encanto
glom es una función interesante que toma todas las particiones de su conjunto de datos y las convierte en
matrices. Esto puede ser útil si va a recopilar los datos en el controlador y desea tener una matriz para
cada partición. Sin embargo, esto puede causar serios problemas de estabilidad porque si tiene
particiones grandes o una gran cantidad de particiones, es fácil bloquear el controlador.

En el siguiente ejemplo, puede ver que tenemos dos particiones y cada palabra cae en una partición cada
una:

// en Scala
spark.sparkContext.parallelize(Seq("Hola", "Mundo"), 2).glom().collect()
// Matriz(Matriz(Hola), Matriz(Mundo))

# en Python
spark.sparkContext.parallelize(["Hola", "Mundo"], 2).glom().collect()
# [['Hola Mundo']]

Conclusión
En este capítulo, vio los conceptos básicos de las API de RDD, incluida la manipulación única de RDD.
El Capítulo 13 aborda conceptos de RDD más avanzados, como combinaciones y RDD de clave­valor.
Machine Translated by Google

Capítulo 13. RDD avanzados

El Capítulo 12 exploró los conceptos básicos de la manipulación de un único RDD. Aprendió cómo crear RDD y por qué
podría querer usarlos. Además, discutimos mapear, filtrar, reducir y cómo crear funciones para transformar datos únicos de
RDD. Este capítulo cubre las operaciones avanzadas de RDD y se centra en los RDD de clave­valor, una
poderosa abstracción para manipular datos. También abordamos algunos temas más avanzados, como el particionamiento
personalizado, una razón por la que es posible que desee utilizar RDD en primer lugar. Con una función de partición
personalizada, puede controlar exactamente cómo se distribuyen los datos en el clúster y manipular esa partición individual
en consecuencia. Antes de llegar allí, resumamos los temas clave que cubriremos:

Agregaciones y RDD de clave­valor

Particionamiento personalizado

RDD se une

NOTA

Este conjunto de API ha existido desde, esencialmente, el comienzo de Spark, y hay un montón de
ejemplos en toda la web en este conjunto de API. Esto hace que sea trivial buscar y encontrar ejemplos
que le muestren cómo usar estas operaciones.

Usemos el mismo conjunto de datos que usamos en el último capítulo:

// en Scala
val myCollection = "Spark The Definitive Guide: Big Data Processing Made Simple"
.dividir(" ")
val palabras = chispa.sparkContext.parallelize(myCollection, 2)

# en Python
myCollection = "Spark The Definitive Guide: Big Data Processing Made Simple"\
.split(" ")
palabras = chispa.sparkContext.parallelize(miColección, 2)

Conceptos básicos de valor­clave (RDD de valor­clave)

Hay muchos métodos en RDD que requieren que coloque sus datos en un formato de clave­valor. Una pista de que esto
es necesario es que el método incluirá <alguna­operación>ByKey. Siempre que vea ByKey en el nombre de un método, significa
que puede realizar esto solo en un tipo PairRDD. La forma más fácil es mapear su RDD actual a una estructura básica de
clave­valor. Esto significa tener dos valores en cada registro de su RDD:
Machine Translated by Google

// en Scala
palabras.map(palabra => (palabra.toLowerCase, 1))

# en Python
palabras.map( palabra lambda: (palabra.inferior(), 1))

clavePor
El ejemplo anterior demostró una forma sencilla de crear una clave. Sin embargo, también puede usar la
función keyBy para lograr el mismo resultado especificando una función que crea la clave a partir de su valor
actual. En este caso, está tecleando por la primera letra de la palabra. Spark luego mantiene el registro como
el valor para el RDD con clave:

// en Scala val
palabra clave = palabras.keyBy(palabra => palabra.toLowerCase.toSeq(0).toString)

# en Python
palabra clave = palabras.keyBy(palabra lambda : palabra.inferior()[0])

Mapeo sobre valores


Una vez que tenga un conjunto de pares clave­valor, puede comenzar a manipularlos como tales. Si tenemos
una tupla, Spark asumirá que el primer elemento es la clave y el segundo es el valor. Cuando está en este
formato, puede elegir explícitamente mapear los valores (e ignorar las claves individuales).
Por supuesto, podría hacer esto manualmente, pero esto puede ayudar a prevenir errores cuando sabe que solo
va a modificar los valores:

// en Scala
palabra clave.mapValues(palabra => palabra.toUpperCase).collect()

# en Python
palabra clave.mapValues( palabra lambda: palabra.superior()).collect()

Aquí está la salida en Python:

[('s', 'CHISPA'), ('t',


'EL'), ('d',
'DEFINITIVO'), ('g', 'GUÍA'),
(':', ':') , ('b',
'GRANDE'),
('d', 'DATOS'), ('p',
'PROCESANDO'),
('m', 'HECHO'), ('s',
'SIMPLE') ]

(Los valores en Scala son los mismos pero se omiten por brevedad).
Machine Translated by Google

Puede hacer FlatMap sobre las filas, como vimos en el Capítulo 12, para expandir el número de filas que tiene
que hacer para que cada fila represente un carácter. En el siguiente ejemplo, omitiremos la salida, pero sería
simplemente cada carácter a medida que los convertimos en matrices:

// en Scala
palabra clave.flatMapValues(palabra => palabra.toUpperCase).collect()

# en Python
palabra clave.flatMapValues( palabra lambda: word.upper()).collect()

Extracción de claves y valores


Cuando estamos en el formato de par clave­valor, también podemos extraer las claves o valores específicos
utilizando los siguientes métodos:

// en Scala
palabra clave.keys.collect()
palabra clave.valores.collect()

# en Python
palabra clave.keys().collect()
palabra clave.values().collect()

buscar
Una tarea interesante que quizás desee realizar con un RDD es buscar el resultado de una clave en particular.
Tenga en cuenta que no existe un mecanismo de cumplimiento con respecto a que solo haya una clave para
cada entrada, por lo que si buscamos "s", obtendremos ambos valores asociados con eso: "Spark" y "Simple":

palabra clave.búsqueda("s")

muestra por clave


Hay dos formas de muestrear un RDD mediante un conjunto de claves. Podemos hacerlo a través de una
aproximación o exactamente. Ambas operaciones pueden hacerlo con o sin reemplazo, así como el muestreo por
una fracción por una clave dada. Esto se hace a través de un muestreo aleatorio simple con una pasada sobre
el RDD, lo que produce una muestra de tamaño que es aproximadamente igual a la suma de math.ceil(numItems
* samplingRate) sobre todos los valores clave:

// en Scala val
distintosChars = palabras.flatMap(palabra => palabra.toLowerCase.toSeq).distinct
.collect()
import scala.util.Random val
sampleMap = distintChars.map(c => (c, new Random().nextDouble())).toMap words.map(word =>
(word.toLowerCase.toSeq(0 ), palabra))
Machine Translated by Google

.sampleByKey(verdadero, mapa de muestra,


6L) .collect()

# en Python
importar
caracteres distintos aleatorios = palabras.flatMap(palabra lambda : lista(palabra.inferior())).distinct()\
.recolectar()
sampleMap = dict(mapa(lambda c: (c, aleatorio.aleatorio()), distintosChars))
palabras.map( palabra lambda : (palabra.inferior()[0], palabra))
\ .sampleByKey(True, sampleMap, 6).recoger()

Este método se diferencia de sampleByKey en que realiza pases adicionales sobre el RDD para crear
un tamaño de muestra que sea exactamente igual a la suma de math.ceil(numItems * samplingRate) sobre
todos los valores clave con un 99,99 % de confianza. Al muestrear sin reemplazo, necesita un paso
adicional sobre el DDR para garantizar el tamaño de la muestra; al muestrear con reemplazo, necesita
dos pases adicionales:

// en Scala
palabras.map(palabra => (palabra.toLowerCase.toSeq(0),
palabra)) .sampleByKeyExact(true, sampleMap, 6L).collect()

Agregaciones
Puede realizar agregaciones en RDD simples o en PairRDD, según el método que esté utilizando.
Usemos algunos de nuestros conjuntos de datos para demostrar esto:

// en Scala val
chars = palabras.flatMap(palabra => palabra.toLowerCase.toSeq) val
KVcharacters = chars.map(letra => (letra, 1)) def maxFunc(izquierda:Int,
derecha:Int) = matemáticas. max(izquierda, derecha) def addFunc(izquierda:Int,
derecha:Int) = izquierda + derecha val nums = sc.parallelize(1 a
30, 5)

# en caracteres
Python = palabras.flatMap(palabra lambda : palabra.inferior())
KVcharacters = chars.map( letra lambda : (letra, 1)) def
maxFunc(izquierda, derecha):
return max(izquierda, derecha)
def addFunc(izquierda, derecha):
return izquierda + derecha
nums = sc.parallelize(rango(1,31), 5)

Después de tener esto, puede hacer algo como countByKey, que cuenta los elementos por cada clave.

contar por clave


Puede contar la cantidad de elementos para cada clave, recopilando los resultados en un Mapa local. Tú
Machine Translated by Google

también puede hacer esto con una aproximación, lo que le permite especificar un tiempo de espera y confianza
al usar Scala o Java:

// en Scala
val timeout = 1000L //milisegundos val
confianza = 0.95
KVcharacters.countByKey()
KVcharacters.countByKeyApprox(tiempo de espera, confianza)

# en Python
KVcharacters.countByKey()

Comprender las implementaciones de agregación


Hay varias formas de crear sus Pares RDD clave­valor; sin embargo, la implementación es bastante
importante para la estabilidad laboral. Comparemos las dos opciones fundamentales, groupBy y reduce.
Haremos esto en el contexto de una clave, pero los mismos principios básicos se aplican a los métodos
groupBy y reduce.

grupoPorClave

Mirando la documentación de la API, podría pensar que groupByKey con un mapa sobre cada agrupación es la
mejor manera de resumir los recuentos de cada clave:

// en Scala
KVcharacters.groupByKey().map(fila => (fila._1, fila._2.reduce(addFunc))).collect()

# en Python
KVcharacters.groupByKey().map( fila lambda: (fila[0], reduce(addFunc, fila[1])))\
.recolectar()
# tenga en cuenta que esto es Python 2, reduce debe importarse desde functools en Python 3

Sin embargo, esta es, para la mayoría de los casos, la forma incorrecta de abordar el problema. El
problema fundamental aquí es que cada ejecutor debe tener en memoria todos los valores de una clave
dada antes de aplicarles la función. ¿Por qué es esto problemático? Si tiene un sesgo de clave masivo, algunas
particiones pueden estar completamente sobrecargadas con una tonelada de valores para una clave
determinada, y obtendrá OutOfMemoryErrors. Obviamente, esto no causa ningún problema con nuestro conjunto
de datos actual, pero puede causar problemas graves a escala. Esto no está garantizado que suceda, pero puede suceder.

Hay casos de uso en los que groupByKey tiene sentido. Si tiene tamaños de valor consistentes para cada
clave y sabe que caben en la memoria de un ejecutor dado, estará bien. Es bueno saber exactamente en qué
te estás metiendo cuando haces esto. Existe un enfoque preferido para los casos de uso aditivo: reduceByKey.

reducirPorClave

Debido a que estamos realizando un conteo simple, un enfoque mucho más estable es realizar el mismo
Machine Translated by Google

flatMap y luego simplemente realice un mapa para asignar cada instancia de letra al número uno, y luego realice
un reduceByKey con una función de suma para recopilar la matriz. Esta implementación es mucho más
estable porque la reducción ocurre dentro de cada partición y no necesita poner todo en la memoria. Además,
no se produce ninguna mezcla durante esta operación; todo sucede en cada trabajador individualmente antes de
realizar una reducción final. Esto mejora en gran medida la velocidad a la que puede realizar la operación, así como
la estabilidad de la operación:

KVcharacters.reduceByKey(addFunc).collect()

Este es el resultado de la operación:

Matriz((d,4), (p,3), (t,3), (b,1), (h,1), (n,2),


...
(a,4), (i,7), (k,1), (u,1), (o,1), (g,3), (m,2), (c,1))

El método reduceByKey devuelve un RDD de un grupo (la clave) y una secuencia de elementos que no se
garantiza que tengan un orden. Por lo tanto, este método es completamente apropiado cuando nuestra carga de
trabajo es asociativa pero inapropiado cuando el orden es importante.

Otros métodos de agregación


Existe una serie de métodos de agregación avanzados. En su mayor parte, estos son en gran parte detalles
de implementación que dependen de su carga de trabajo específica. Encontramos que es muy raro que los
usuarios se encuentren con este tipo de carga de trabajo (o necesiten realizar este tipo de operación) en el
Spark actual. Simplemente no hay muchas razones para usar estas herramientas de nivel extremadamente bajo
cuando puede realizar agregaciones mucho más simples usando las API estructuradas. Estas funciones le permiten
en gran medida un control muy específico y de muy bajo nivel sobre cómo se realiza exactamente una agregación
determinada en el grupo de máquinas.

agregar

Otra función es agregada. Esta función requiere un valor nulo y de inicio y luego requiere que especifique dos
funciones diferentes. Los primeros agregados dentro de las particiones, los segundos agregados entre
particiones. El valor inicial se utilizará en ambos niveles de agregación:

// en Scala
nums.aggregate(0)(maxFunc, addFunc)

# en Python
nums.aggregate(0, maxFunc, addFunc)

El agregado tiene algunas implicaciones de rendimiento porque realiza la agregación final en el controlador. Si los
resultados de los ejecutores son demasiado grandes, pueden desactivar el controlador con un OutOfMemoryError.
Hay otro método, treeAggregate que hace lo mismo que
Machine Translated by Google

agregado (a nivel de usuario) pero lo hace de una manera diferente. Básicamente, "empuja hacia abajo" algunas
de las subagregaciones (creando un árbol de ejecutor a ejecutor) antes de realizar la agregación final en el
controlador. Tener varios niveles puede ayudarlo a garantizar que el controlador no se quede sin memoria en el
proceso de agregación. Estas implementaciones basadas en árboles a menudo intentan mejorar la estabilidad en
ciertas operaciones:

// en Scala val
profundidad = 3
nums.treeAggregate(0)(maxFunc, addFunc, profundidad)

# en Python
profundidad
= 3 nums.treeAggregate(0, maxFunc, addFunc, profundidad)

agregado por clave

Esta función hace lo mismo que el agregado pero en lugar de hacerlo partición por partición, lo hace por clave. El
valor inicial y las funciones siguen las mismas propiedades:

// en Scala
KVcharacters.aggregateByKey(0)(addFunc, maxFunc).collect()

# en Python
KVcharacters.aggregateByKey(0, addFunc, maxFunc).collect()

combinarPorClave

En lugar de especificar una función de agregación, puede especificar un combinador. Este combinador
opera en una clave determinada y fusiona los valores de acuerdo con alguna función. Luego va a fusionar las
diferentes salidas de los combinadores para darnos nuestro resultado. También podemos especificar el número de
particiones de salida como un particionador de salida personalizado:

// en Scala val
valToCombiner = (value:Int) => List(value) val
mergeValuesFunc = (vals:List[Int], valToAppend:Int) => valToAppend :: vals val mergeCombinerFunc
= (vals1:List[Int], vals2:List[Int]) => vals1 ::: vals2 // ahora las definimos como variables de función val
outputPartitions = 6 KVcharacters .combineByKey(

valToCombiner,
mergeValuesFunc,
mergeCombinerFunc,
particiones de
salida) .collect()

# en Python
def valToCombiner(valor):
devuelve [valor]
Machine Translated by Google

def mergeValuesFunc(vals, valToAppend):


vals.append(valToAppend)
return vals
def mergeCombinerFunc(vals1, vals2):
devuelve vals1 + vals2
particiones de salida = 6

KVcaracteres\ .combineByKey(
valToCombiner,
mergeValuesFunc,
mergeCombinerFunc,
particiones de salida)
\ .collect()

plegarPorClave

foldByKey fusiona los valores de cada clave mediante una función asociativa y un "valor cero" neutral, que se
puede agregar al resultado un número arbitrario de veces y no debe cambiar el resultado (p. ej., 0 para la suma o 1
para la multiplicación) :

// en Scala
KVcaracteres.foldByKey(0)(addFunc).collect()

# en Python
KVcharacters.foldByKey(0, addFunc).collect()

Cogrupos
CoGroups le brinda la capacidad de agrupar hasta tres RDD de valor clave en Scala y dos en Python. Esto une los
valores dados por clave. Esto es efectivamente solo una unión basada en grupos en un RDD. Al hacer esto, también
puede especificar una cantidad de particiones de salida o una función de partición personalizada para controlar
exactamente cómo se distribuyen estos datos en el clúster (hablaremos de las funciones de partición más adelante
en este capítulo):

// en Scala
importamos scala.util.Random
val distintosChars = palabras.flatMap(palabra => palabra.toLowerCase.toSeq).distinct val
charRDD = distinguiblesChars.map(c => (c, new Random().nextDouble()) ) val
charRDD2 = distintosChars.map(c => (c, new Random().nextDouble())) val charRDD3
= distintosChars.map(c => (c, new Random().nextDouble())) charRDD.cogroup
(charRDD2, charRDD3).tomar(5)

# en Python
importar
caracteres distintos aleatorios = palabras.flatMap( palabra lambda:
palabra.inferior()).distinct() charRDD = caracteres distintos.map(lambda
c: (c, aleatorio.aleatorio())) charRDD2 = caracteres distintos.map(lambda
c: (c, aleatorio.aleatorio())) charRDD.cogrupo(charRDD2).tomar(5)
Machine Translated by Google

El resultado es un grupo con nuestra clave en un lado y todos los valores relevantes en el otro lado.

Uniones

Los RDD tienen uniones muy parecidas a las que vimos en la API estructurada, aunque los RDD son
mucho más complicados para usted. Todos siguen el mismo formato básico: los dos RDD que nos gustaría
unir y, opcionalmente, el número de particiones de salida o la función de partición del cliente a la que deben
enviar. Hablaremos de las funciones de partición más adelante en este capítulo.

Unir internamente

Ahora demostraremos una unión interna. Observe cómo estamos configurando el número de particiones de
salida que nos gustaría ver:

// en Scala
val keyedChars = distintosChars.map(c => (c, new Random().nextDouble())) val
outputPartitions = 10
KVcharacters.join(keyedChars).count()
KVcharacters.join(keyedChars, outputPartitions).count()

# en Python
keyedChars = differentChars.map(lambda c: (c, random.random()))
outputPartitions = 10
KVcharacters.join(keyedChars).count()
KVcharacters.join(keyedChars, outputPartitions).count()

No proporcionaremos un ejemplo para las otras uniones, pero todas siguen el mismo formato básico. Puede
obtener información sobre los siguientes tipos de unión a nivel conceptual en el Capítulo 8:

fullOuterJoin

izquierda combinación externa

rightOuterJoin

cartesiano (¡Esto, nuevamente, es muy peligroso! No acepta una clave de unión y puede tener una
salida masiva).

cremalleras

El último tipo de combinación no es realmente una combinación, pero combina dos RDD, por lo que vale
la pena etiquetarlo como una combinación. zip le permite "comprimir" juntos dos RDD, asumiendo que
tienen la misma longitud. Esto crea un PairRDD. Los dos RDD deben tener el mismo número de particiones así
como el mismo número de elementos:

// en Scala
val numRange = sc.parallelize(0 a 9, 2)
Machine Translated by Google

palabras.zip(numRange).collect()

# en Python
numRange = sc.parallelize(rango(10), 2)
palabras.zip(numRange).collect()

Esto nos da el siguiente resultado, una matriz de claves comprimidas en los valores:

[('Chispa', 0),
('El', 1),
('Definitivo', 2),
('Guía', 3), (':',
4),
('Grande', 5),
('Datos', 6),
('Tratamiento', 7),
('Hecho', 8),
('Sencillo', 9)]

Controlando Particiones
Con los RDD, tiene control sobre cómo se distribuyen físicamente los datos exactamente en el clúster.
Algunos de estos métodos son básicamente los mismos que tenemos en las API estructuradas, pero la adición
clave (que no existe en las API estructuradas) es la capacidad de especificar una función de particionamiento
(formalmente, un particionador personalizado, que discutiremos más adelante cuando mira los métodos
básicos).

juntarse
coalesce efectivamente colapsa las particiones en el mismo trabajador para evitar una mezcla de datos al
volver a particionar. Por ejemplo, nuestras palabras RDD actualmente son dos particiones, podemos
colapsar eso en una partición usando coalesce sin provocar una mezcla de datos:

// en Scala
palabras.coalesce(1).getNumPartitions // 1

# en Python
palabras.coalesce(1).getNumPartitions() # 1

reparto
La operación de repartición le permite volver a particionar sus datos hacia arriba o hacia abajo, pero
realiza una mezcla entre los nodos en el proceso. Aumentar el número de particiones puede aumentar el nivel
de paralelismo cuando se opera en operaciones de tipo mapa y filtro:

words.repartition(10) // nos da 10 particiones


Machine Translated by Google

repartitionAndSortWithinPartitions
Esta operación le brinda la capacidad de volver a particionar y especificar el orden de cada una de esas
particiones de salida. Omitiremos el ejemplo porque la documentación es buena, pero el usuario puede
especificar tanto la partición como las comparaciones de claves.

Particionamiento personalizado

Esta capacidad es una de las razones principales por las que querría usar RDD. Los particionadores
personalizados no están disponibles en las API estructuradas porque en realidad no tienen una contraparte
lógica. Son detalles de implementación de bajo nivel que pueden tener un efecto significativo sobre si sus
trabajos se ejecutan correctamente. El ejemplo canónico para motivar la partición personalizada para esta
operación es PageRank, mediante el cual buscamos controlar el diseño de los datos en el clúster y evitar
las mezclas. En nuestro conjunto de datos de compras, esto podría significar la partición por ID de cada cliente
(veremos este ejemplo en un momento).

En resumen, el único objetivo de la partición personalizada es igualar la distribución de sus datos en el clúster
para que pueda solucionar problemas como la asimetría de datos.

Si va a utilizar particiones personalizadas, debe desplegar RDD desde las API estructuradas, aplicar su partición
personalizada y luego volver a convertirla en un DataFrame o Dataset. De esta manera, obtiene lo mejor de
ambos mundos, y solo baja a la partición personalizada cuando lo necesita.
a.

Para realizar particiones personalizadas, debe implementar su propia clase que amplíe Partitioner.
Debe hacer esto solo cuando tenga mucho conocimiento del dominio sobre su espacio problemático; si solo
está buscando particionar en un valor o incluso un conjunto de valores (columnas), vale la pena hacerlo en la
API de DataFrame.

Vamos a sumergirnos en un ejemplo:

// en Scala val
df = spark.read.option("header", "true").option("inferSchema", "true")
.csv("/datos/datos­minoristas/todos/")
val rdd = df.coalesce(10).rdd

# en Python df
= spark.read.option("header", "true").option("inferSchema", "true")\ .csv("/data/retail­data/all/") rdd
= df. coalesce(10).rdd

df.imprimirEsquema()

Spark tiene dos particionadores integrados que puede aprovechar en la API de RDD, un
HashPartitioner para valores discretos y un RangePartitioner. Estos dos funcionan para valores discretos y
valores continuos, respectivamente. Las API estructuradas de Spark ya las usarán, aunque podemos usar
lo mismo en RDD:
Machine Translated by Google

// en Scala
importar org.apache.spark.HashPartitioner
rdd.map(r => r(6)).take(5).foreach(println) val
keyedRDD = rdd.keyBy(fila => fila(6).asInstanceOf [Int].toDouble)

keyedRDD.partitionBy(nuevo HashPartitioner(10)).take(10)

Aunque los particionadores hash y range son útiles, son bastante rudimentarios. A veces, necesitará
realizar algunas particiones de muy bajo nivel porque está trabajando con datos muy grandes y una gran
asimetría de claves. Key skew simplemente significa que algunas claves tienen muchos, muchos más
valores que otras claves. Desea romper estas claves tanto como sea posible para mejorar el paralelismo
y evitar OutOfMemoryErrors durante el curso de la ejecución.

Una instancia podría ser que necesite particionar más claves si y solo si la clave coincide con un
formato determinado. Por ejemplo, podemos saber que hay dos clientes en su conjunto de datos que
siempre bloquean su análisis y necesitamos dividirlos más que otros ID de clientes. De hecho, estos
dos están tan sesgados que deben operarse solos, mientras que todos los demás pueden agruparse en
grandes grupos. Obviamente, este es un ejemplo un poco caricaturizado, pero también puede ver situaciones
similares en sus datos:

// en Scala
import org.apache.spark.Partitioner class
DomainPartitioner extends Partitioner { def numPartitions
= 3 def getPartition(key:
Any): Int = { val customerId =
key.asInstanceOf[Double].toInt if (customerId == 17850.0
| | IDcliente == 12583.0) {
return 0 }
else
{ return new java.util.Random().nextInt(2) + 1
}

}}

keyedRDD .partitionBy(nuevo DomainPartitioner).map(_._1).glom().map(_.toSet.toSeq.length) .take(5)

Después de ejecutar esto, verá el recuento de resultados en cada partición. Los segundos dos números
variarán, porque los estamos distribuyendo aleatoriamente (como verá cuando hagamos lo mismo en
Python), pero se aplican los mismos principios:

# en Python
def particiónFunc(clave):
importar
aleatorio si clave == 17850 o clave ==
12583:

devuelve 0 más: devuelve aleatorio.randint(1,2)


Machine Translated by Google

keyedRDD = rdd.keyBy(lambda fila: fila[6])

keyedRDD\ .partitionBy(3, particiónFunc)


\ .map(lambda x: x[0])\ .glom()

\ .map(lambda x: len(set (x)))\ .tomar(5)

Esta lógica de distribución de claves personalizada solo está disponible en el nivel de RDD. Por supuesto, este es
un ejemplo simple, pero muestra el poder de usar lógica arbitraria para distribuir los datos alrededor del clúster
de manera física.

Serialización personalizada
El último tema avanzado del que vale la pena hablar es el tema de la serialización de Kryo. Cualquier objeto que
desee paralelizar (o funcionar) debe ser serializable:

// en la clase
Scala SomeClass extiende Serializable { var someValue
= 0 def setSomeValue(i:Int)
={
algúnValor = yo
esto

}}

sc.parallelize(1 a 10).map(num => new SomeClass().setSomeValue(num))

La serialización predeterminada puede ser bastante lenta. Spark puede usar la biblioteca Kryo (versión 2) para
serializar objetos más rápidamente. Kryo es significativamente más rápido y más compacto que la serialización de
Java (a menudo hasta 10x), pero no es compatible con todos los tipos serializables y requiere que registre
las clases que usará en el programa con anticipación para obtener el mejor rendimiento.

Puede usar Kryo inicializando su trabajo con SparkConf y configurando el valor de "spark.serializer"
en "org.apache.spark.serializer.KryoSerializer" (discutimos esto en la siguiente parte del libro). Esta configuración
configura el serializador utilizado para mezclar datos entre nodos trabajadores y serializar RDD en el
disco. La única razón por la que Kryo no es el predeterminado es por el requisito de registro personalizado, pero
recomendamos probarlo en cualquier aplicación de uso intensivo de la red. Desde Spark 2.0.0, usamos
internamente el serializador Kryo cuando mezclamos RDD con tipos simples, matrices de tipos simples o tipo de
cadena.

Spark incluye automáticamente serializadores Kryo para las muchas clases básicas de Scala de uso común
cubiertas en AllScalaRegistrar de la biblioteca Chill de Twitter.

Para registrar sus propias clases personalizadas con Kryo, use el método registerKryoClasses:

// en escala
Machine Translated by Google

val conf = new SparkConf().setMaster(...).setAppName(...)


conf.registerKryoClasses(Array(classOf[MyClass1], classOf[MyClass2])) val sc =
new SparkContext(conf)

Conclusión
En este capítulo discutimos muchos de los temas más avanzados relacionados con los RDD. De particular
interés fue la sección sobre particiones personalizadas, que le permite funciones muy específicas para diseñar
sus datos. En el Capítulo 14, analizamos otra de las herramientas de bajo nivel de Spark: las variables distribuidas.
Machine Translated by Google

Capítulo 14. Variables compartidas distribuidas

Además de la interfaz Resilient Distributed Dataset (RDD), el segundo tipo de API de bajo nivel en Spark son dos
tipos de "variables compartidas distribuidas": variables de transmisión y acumuladores. Estas son
variables que puede usar en sus funciones definidas por el usuario (por ejemplo, en una función de mapa en
un RDD o un DataFrame) que tienen propiedades especiales cuando se ejecutan en un clúster.
Específicamente, los acumuladores le permiten agregar datos de todas las tareas en un resultado compartido (p.
ej., implementar un contador para que pueda ver cuántos de los registros de entrada de su trabajo no se
analizaron), mientras que las variables de transmisión le permiten ahorrar un gran valor en todos. los nodos de
trabajo y reutilícelo en muchas acciones de Spark sin volver a enviarlo al clúster. Este capítulo analiza algunas
de las motivaciones para cada uno de estos tipos de variables, así como también cómo utilizarlos.

Variables de difusión
Las variables de difusión son una forma de compartir un valor inmutable de manera eficiente en todo el clúster sin
encapsular esa variable en un cierre de función. La forma normal de usar una variable en su nodo de controlador
dentro de sus tareas es simplemente hacer referencia a ella en los cierres de su función (por ejemplo, en una
operación de mapa), pero esto puede ser ineficiente, especialmente para variables grandes como una tabla de
búsqueda o una máquina. modelo de aprendizaje La razón de esto es que cuando usa una variable en un
cierre, debe deserializarse en los nodos trabajadores muchas veces (una por tarea). Además, si usa la misma
variable en varias acciones y trabajos de Spark, se volverá a enviar a los trabajadores con cada trabajo en lugar de
una vez.

Aquí es donde entran las variables de difusión. Las variables de difusión son variables inmutables compartidas que
se almacenan en caché en cada máquina del clúster en lugar de serializarse con cada tarea. El caso de uso
canónico es pasar una gran tabla de búsqueda que cabe en la memoria de los ejecutores y usarla en una función,
como se ilustra en la Figura 14­1.
Machine Translated by Google

Figura 14­1. Variables de difusión

Por ejemplo, suponga que tiene una lista de palabras o valores:

// en Scala
val myCollection = "Spark The Definitive Guide: Big Data Processing Made Simple"
.dividir(" ")
val palabras = chispa.sparkContext.parallelize(myCollection, 2)

# en Python
my_collection = "Spark The Definitive Guide: Big Data Processing Made Simple"\
.split(" ")
palabras = chispa.sparkContext.parallelize(mi_colección, 2)

Le gustaría complementar su lista de palabras con otra información que tenga, que tiene un tamaño de muchos
kilobytes, megabytes o incluso gigabytes. Esta es técnicamente una unión correcta si lo pensamos en términos de
SQL:

// en Scala
val datos suplementarios = Mapa("Chispa" ­> 1000, "Definitivo" ­> 200,
"Grande" ­> ­300, "Simple" ­> 100)

# en Python
datos suplementarios = {"Chispa":1000, "Definitivo":200,
"Grande":­300, "Simple":100}

Podemos transmitir esta estructura a través de Spark y hacer referencia a ella mediante suppBroadcast. Este
valor es inmutable y se replica con pereza en todos los nodos del clúster cuando activamos una acción:

// en Scala
val suppBroadcast = spark.sparkContext.broadcast(supplementalData)

# en Python
suppBroadcast = spark.sparkContext.broadcast(supplementalData)

Hacemos referencia a esta variable a través del método de valor, que devuelve el valor exacto que teníamos
antes. Se puede acceder a este método dentro de las funciones serializadas sin tener que serializar los datos.
Esto puede ahorrarle una gran cantidad de costos de serialización y deserialización porque Spark transfiere datos
de manera más eficiente alrededor del clúster mediante transmisiones:

// en Scala
suppBroadcast.valor

# en Python
suppBroadcast.value
Machine Translated by Google

Ahora podríamos transformar nuestro RDD usando este valor. En este caso, crearemos un par clave­valor según el
valor que tengamos en el mapa. Si nos falta el valor, simplemente lo reemplazaremos con 0:

// en Scala
palabras.map(palabra => (palabra, suppBroadcast.value.getOrElse(palabra, 0)))
.sortBy(Palabras =>
WordPair._2) .collect()

# en Python
words.map(palabra lambda : (palabra, suppBroadcast.value.get(palabra, 0)))\
.sortBy(lambda wordPair: wordPair[1])
\ .collect()

Esto devuelve el siguiente valor en Python y los mismos valores en un tipo de matriz en Scala:

[('Grande', ­300),
('El', 0),
...
('Definitivo', 200),
('Chispa', 1000)]

La única diferencia entre esto y pasarlo al cierre es que lo hemos hecho de una manera mucho más eficiente
(Naturalmente, esto depende de la cantidad de datos y la cantidad de ejecutores. Para datos muy pequeños (KB
bajos) en clústeres pequeños , puede que no lo sea). Aunque este pequeño diccionario probablemente no tenga un
costo demasiado grande, si tiene un valor mucho mayor, el costo de serializar los datos para cada tarea puede
ser bastante significativo.

Una cosa a tener en cuenta es que usamos esto en el contexto de un RDD; también podemos usar esto en un UDF o
en un conjunto de datos y lograr el mismo resultado.

Acumuladores
Los acumuladores (Figura 14­2), el segundo tipo de variable compartida de Spark, son una forma de actualizar un
valor dentro de una variedad de transformaciones y propagar ese valor al nodo controlador de manera eficiente y
tolerante a fallas.
Machine Translated by Google

Figura 14­2. variable acumulador

Los acumuladores proporcionan una variable mutable que un clúster de Spark puede actualizar de forma segura por fila. Puede
usarlos con fines de depuración (por ejemplo, para realizar un seguimiento de los valores de una determinada variable por partición a fin
de usarla de manera inteligente con el tiempo) o para crear una agregación de bajo nivel.
Los acumuladores son variables que se “añaden” solo a través de una operación asociativa y conmutativa y, por lo tanto, pueden
admitirse de manera eficiente en paralelo. Puede usarlos para implementar contadores (como en MapReduce) o sumas. Spark admite
de forma nativa acumuladores de tipos numéricos y los programadores pueden agregar compatibilidad con nuevos tipos.

Para las actualizaciones del acumulador realizadas solo dentro de las acciones, Spark garantiza que la actualización del acumulador de
cada tarea se aplicará solo una vez, lo que significa que las tareas reiniciadas no actualizarán el valor. En las transformaciones,
debe tener en cuenta que la actualización de cada tarea se puede aplicar más de una vez si se vuelven a ejecutar las tareas o las
etapas del trabajo.

Los acumuladores no cambian el modelo de evaluación perezoso de Spark. Si un acumulador se actualiza dentro de una
operación en un RDD, su valor se actualiza solo una vez que se calcula realmente el RDD (por ejemplo, cuando llama a una
acción en ese RDD o un RDD que depende de él).
En consecuencia, no se garantiza que las actualizaciones del acumulador se ejecuten cuando se realizan dentro de una transformación

diferida como map().

Los acumuladores pueden ser tanto con nombre como sin nombre. Los acumuladores con nombre mostrarán sus resultados de
ejecución en la interfaz de usuario de Spark, mientras que los sin nombre no lo harán.

Ejemplo básico
Experimentemos realizando una agregación personalizada en el conjunto de datos de vuelo que creamos anteriormente en el
libro. En este ejemplo, usaremos la API de conjunto de datos en lugar de la API de RDD, pero la extensión es bastante similar:

// en el caso
de Scala clase Vuelo(DEST_COUNTRY_NAME: String,
ORIGIN_COUNTRY_NAME: Cadena, recuento: BigInt)
Machine Translated by Google

val vuelos =
chispa.read .parquet("/data/flight­data/parquet/2010­
summary.parquet") .as[Vuelo]

# en Python
vuelos =
spark.read\ .parquet("/data/flight­data/parquet/2010­summary.parquet")

Ahora vamos a crear un acumulador que cuente la cantidad de vuelos hacia o desde China. Aunque
podríamos hacer esto de una manera bastante sencilla en SQL, muchas cosas podrían no ser tan sencillas.
Los acumuladores proporcionan una forma programática de permitirnos hacer este tipo de conteos. Lo siguiente
demuestra la creación de un acumulador sin nombre:

// en Scala
import org.apache.spark.util.LongAccumulator val
accUnnamed = new LongAccumulator val
acc = spark.sparkContext.register(accUnnamed)

# en Python
accChina = chispa.chispaContexto.acumulador(0)

Nuestro caso de uso se ajusta un poco mejor a un acumulador con nombre. Hay dos formas de hacer esto:
un método abreviado y uno largo. El más simple es usar SparkContext. Alternativamente, podemos
instanciar el acumulador y registrarlo con un nombre:

// en Scala
val accChina = new LongAccumulator
val accChina2 = chispa.sparkContext.longAccumulator("China")
chispa.sparkContext.register(accChina, "China")

Especificamos el nombre del acumulador en el valor de cadena que pasamos a la función, o como segundo
parámetro en la función de registro. Los acumuladores con nombre se mostrarán en la interfaz de usuario de
Spark, mientras que los sin nombre no.

El siguiente paso es definir la forma en que agregamos a nuestro acumulador. Esta es una función
bastante sencilla:

// en Scala
def accChinaFunc(flight_row: Flight) = {
val destino = fila_vuelo.DEST_COUNTRY_NAME val
origen = fila_vuelo.ORIGIN_COUNTRY_NAME if
(destino == "China")
{ accChina.add(flight_row.count.toLong) } if

(origen == "China")
{ accChina.add(flight_row.count .toLargo) } }
Machine Translated by Google

# en Python
def accChinaFunc(flight_row):
destino = vuelo_fila["DEST_COUNTRY_NAME"]
origen = vuelo_fila["ORIGIN_COUNTRY_NAME"]
if destino == "China":
accChina.add(flight_row["count"]) if
origen == "China":
accChina.add(flight_row["count"])

Ahora, iteremos sobre cada fila en nuestro conjunto de datos de vuelos a través del método foreach. La razón
de esto es que foreach es una acción y Spark puede proporcionar garantías que se ejecutan solo dentro de las
acciones.

El método foreach se ejecutará una vez para cada fila en el DataFrame de entrada (asumiendo que no lo
filtramos) y ejecutará nuestra función en cada fila, incrementando el acumulador en consecuencia:

// en Scala
vuelos.foreach(flight_row => accChinaFunc(flight_row))

# en Python
vuelos.foreach(lambda fila_vuelo: accChinaFunc(fila_vuelo))

Esto se completará con bastante rapidez, pero si navega a la interfaz de usuario de Spark, puede ver el valor
relevante, en un nivel por ejecutor, incluso antes de consultarlo mediante programación, como se muestra
en la figura 14­3 .
Machine Translated by Google

Figura 14­3. Interfaz de usuario de Ejecutor Spark

Por supuesto, también podemos consultarlo programáticamente. Para hacer esto, usamos la propiedad de valor:

// en Scala
accChina.value // 953

# en Python
accChina.valor # 953

Acumuladores personalizados
Aunque Spark proporciona algunos tipos de acumuladores predeterminados, a veces es posible que desee
crear su propio acumulador personalizado. Para hacer esto, necesita crear una subclase de la clase AccumulatorV2.
Hay varios métodos abstractos que debe implementar, como puede ver en el siguiente ejemplo. En este
ejemplo, agregaremos solo valores que son pares al acumulador. Aunque esto es de nuevo simplista,
debería mostrarle lo fácil que es construir sus propios acumuladores:

// en Scala
importar scala.collection.mutable.ArrayBuffer importar
org.apache.spark.util.AccumulatorV2

valor arr = ArrayBuffer[BigInt]()

class EvenAccumulator extiende AccumulatorV2[BigInt, BigInt] { private


var num:BigInt = 0 def reset():
Unit = { this.num = 0

} def add(intValue: BigInt): Unidad = { if


(intValue % 2 == 0) { this.num
+= intValue
}

} def merge(other: AccumulatorV2[BigInt,BigInt]): Unidad = { this.num


+= otro.valor

} def value():BigInt =
{ this.num

} def copy(): AccumulatorV2[BigInt,BigInt] = { new


EvenAccumulator

} def esCero():Booleano =
{ this.num == 0

} } val acc = new EvenAccumulator


val newAcc = sc.register(acc, "evenAcc")
Machine Translated by Google

// en Scala
acc.value // 0
vuelos.foreach(flight_row => acc.add(flight_row.count)) acc.value //
31390

Si es predominantemente un usuario de Python, también puede crear sus propios acumuladores personalizados
subclasificando AccumulatorParam y usándolo como vimos en el ejemplo anterior.

Conclusión
En este capítulo, cubrimos las variables distribuidas. Estas pueden ser herramientas útiles para optimizaciones
o para la depuración. En el Capítulo 15, definimos cómo se ejecuta Spark en un clúster para comprender mejor
cuándo pueden ser útiles.
Machine Translated by Google

Parte IV. Aplicaciones de producción


Machine Translated by Google

Capítulo 15. Cómo se ejecuta Spark en


un clúster

Hasta ahora en el libro, nos enfocamos en las propiedades de Spark como interfaz de programación. Hemos discutido cómo
las API estructuradas toman una operación lógica, la dividen en un plan lógico y la convierten en un plan físico que en
realidad consta de operaciones de conjuntos de datos distribuidos resistentes (RDD) que se ejecutan en el clúster de
máquinas. Este capítulo se centra en lo que sucede cuando Spark ejecuta ese código. Discutimos esto de una manera
independiente de la implementación; esto no depende ni del administrador de clústeres que esté usando ni del código que esté
ejecutando.
Al final del día, todo el código de Spark se ejecuta de la misma manera.

Este capítulo cubre varios temas clave:

La arquitectura y los componentes de una aplicación Spark

El ciclo de vida de una aplicación Spark dentro y fuera de Spark

Importantes propiedades de ejecución de bajo nivel, como la canalización

Lo que se necesita para ejecutar una aplicación Spark, como continuación al Capítulo 16.

Comencemos con la arquitectura.

La arquitectura de una aplicación Spark


En el Capítulo 2, discutimos algunos de los componentes de alto nivel de una aplicación Spark. Repasemos esos de nuevo:

El conductor de chispa

El controlador es el proceso "en el asiento del conductor" de su aplicación Spark. Es el controlador de la ejecución de una
Aplicación Spark y mantiene todo el estado del clúster Spark (el estado y las tareas de los ejecutores). Debe interactuar
con el administrador de clústeres para obtener recursos físicos y ejecutar ejecutores. Al final del día, esto es solo
un proceso en una máquina física que es responsable de mantener el estado de la aplicación que se ejecuta en el
clúster.

Los ejecutores de Spark

Los ejecutores de Spark son los procesos que realizan las tareas asignadas por el controlador de Spark.
Los ejecutores tienen una responsabilidad central: tomar las tareas asignadas por el conductor, ejecutarlas e informar su
estado (éxito o fracaso) y resultados. Cada aplicación Spark tiene sus propios procesos ejecutores independientes.

El administrador del clúster


Machine Translated by Google

El controlador y los ejecutores de Spark no existen en un vacío, y aquí es donde entra en juego el administrador
de clústeres. El administrador de clústeres es responsable de mantener un clúster de máquinas que ejecutarán
sus aplicaciones Spark. De manera un tanto confusa, un administrador de clúster tendrá sus propias
abstracciones de "controlador" (a veces llamado maestro) y "trabajador". La principal diferencia es que
estos están vinculados a máquinas físicas en lugar de procesos (como lo están en Spark). La figura 15­1
muestra una configuración de clúster básica. La máquina a la izquierda de la ilustración es el nodo del controlador
del administrador de clústeres. Los círculos representan procesos daemon que se ejecutan y administran
cada uno de los nodos trabajadores individuales. Todavía no se está ejecutando ninguna aplicación Spark;
estos son solo los procesos del administrador de clústeres.

Figura 15­1. Un controlador de clúster y un trabajador (todavía no hay una aplicación Spark)

Cuando llega el momento de ejecutar una aplicación Spark, solicitamos recursos del administrador del clúster
para ejecutarla. Dependiendo de cómo esté configurada nuestra aplicación, esto puede incluir un lugar para
ejecutar el controlador Spark o pueden ser solo recursos para los ejecutores de nuestra aplicación Spark. En el
transcurso de la ejecución de la aplicación Spark, el administrador del clúster será responsable de administrar las
máquinas subyacentes en las que se ejecuta nuestra aplicación.

Actualmente, Spark admite tres administradores de clústeres: un administrador de clústeres independiente


integrado simple, Apache Mesos y Hadoop YARN. Sin embargo, esta lista seguirá creciendo, así que asegúrese de
consultar la documentación de su administrador de clústeres favorito.

Ahora que hemos cubierto los componentes básicos de una aplicación, veamos una de las primeras elecciones
que deberá hacer al ejecutar sus aplicaciones: elegir el modo de ejecución.

Modos de ejecución

Un modo de ejecución le da el poder de determinar dónde se encuentran físicamente los recursos antes
mencionados cuando va a ejecutar su aplicación. Tienes tres modos para elegir:

Modo de clúster
Machine Translated by Google

modo cliente

Modo local

Revisaremos cada uno de estos en detalle usando la Figura 15­1 como plantilla. En la siguiente sección, los rectángulos
con bordes sólidos representan el proceso del controlador Spark , mientras que los que tienen bordes punteados
representan los procesos ejecutores.

Modo de clúster

El modo de clúster es probablemente la forma más común de ejecutar aplicaciones Spark. En el modo de clúster, un usuario
envía un JAR precompilado, una secuencia de comandos de Python o una secuencia de comandos R a un administrador
de clústeres. Luego, el administrador del clúster inicia el proceso del controlador en un nodo trabajador dentro del clúster,
además de los procesos ejecutores. Esto significa que el administrador del clúster es responsable de mantener todos los
procesos relacionados con la aplicación Spark. La figura 15­2 muestra que el administrador de clústeres colocó nuestro
controlador en un nodo trabajador y los ejecutores en otros nodos trabajadores.

Figura 15­2. Modo clúster de Spark

modo cliente

El modo de cliente es casi igual que el modo de clúster, excepto que el controlador Spark permanece en la máquina cliente
que envió la aplicación. Esto significa que la máquina cliente es responsable de mantener el proceso del controlador
Spark y el administrador del clúster mantiene los procesos ejecutores.
En la Figura 15­3, estamos ejecutando la aplicación Spark desde una máquina que no está ubicada en el clúster. Estas
máquinas se conocen comúnmente como máquinas de puerta de enlace o nodos perimetrales. En la Figura 15­3,
puede ver que el controlador se ejecuta en una máquina fuera del clúster pero que el
Machine Translated by Google

los trabajadores están ubicados en las máquinas del clúster.

Figura 15­3. Modo cliente de Spark

Modo local

El modo local es una desviación significativa de los dos modos anteriores: ejecuta toda la aplicación Spark en una
sola máquina. Logra el paralelismo a través de subprocesos en esa única máquina.
Esta es una forma común de aprender Spark, probar sus aplicaciones o experimentar iterativamente con el desarrollo
local. Sin embargo, no recomendamos usar el modo local para ejecutar aplicaciones de producción.

El ciclo de vida de una aplicación Spark (fuera de Spark)


Hasta ahora, este capítulo ha cubierto el vocabulario necesario para analizar las aplicaciones Spark.
Ahora es el momento de hablar sobre el ciclo de vida general de las aplicaciones Spark desde "fuera" del código Spark
real. Haremos esto con un ejemplo ilustrado de una aplicación que se ejecuta con Spark Submit (presentado en
el Capítulo 3). Suponemos que ya se está ejecutando un clúster con cuatro nodos, un controlador (no un controlador de
Spark sino un controlador de administrador de clústeres) y tres nodos de trabajo. El clúster real
Machine Translated by Google

manager no importa en este punto: esta sección utiliza el vocabulario de la sección anterior para recorrer
paso a paso el ciclo de vida de la aplicación Spark desde la inicialización hasta la salida del programa.

NOTA
Esta sección también hace uso de ilustraciones y sigue la misma notación que presentamos anteriormente.
Además, ahora presentamos líneas que representan la comunicación en red. Las flechas más oscuras
representan la comunicación por Spark o procesos relacionados con Spark, mientras que las líneas discontinuas
representan una comunicación más general (como la comunicación de administración de clústeres).

Solicitud de cliente
El primer paso es que envíe una solicitud real. Este será un JAR o biblioteca precompilado. En este
punto, está ejecutando código en su máquina local y va a realizar una solicitud al nodo del controlador
del administrador de clústeres (Figura 15­4). Aquí, solicitamos explícitamente recursos solo para el
proceso del controlador Spark . Suponemos que el administrador del clúster acepta esta oferta y
coloca el controlador en un nodo del clúster. El proceso del cliente que envió el trabajo original
sale y la aplicación está apagada y ejecutándose en el clúster.

Figura 15­4. Solicitud de recursos para un conductor

Para hacer esto, ejecutará algo como el siguiente comando en su terminal:


Machine Translated by Google

./bin/spark­enviar \
­­class <clase principal> \
­­master <url­maestra> \ ­­
deploy­mode cluster \ ­­conf
<clave>=<valor> \ # otras
... opciones <jar­
aplicación> \
[argumentos­aplicación]

Lanzamiento

Ahora que el proceso del controlador se ha colocado en el clúster, comienza a ejecutar el código de
usuario (Figura 15­5). Este código debe incluir una SparkSession que inicialice un clúster de Spark (por
ejemplo, controlador + ejecutores). Posteriormente, SparkSession se comunicará con el administrador del clúster
(la línea más oscura) y le pedirá que inicie los procesos del ejecutor de Spark en todo el clúster (las líneas
más claras). El usuario establece la cantidad de ejecutores y sus configuraciones relevantes a través de los
argumentos de la línea de comandos en la llamada de envío de chispa original.

Figura 15­5. Lanzamiento de la aplicación Spark

El administrador del clúster responde iniciando los procesos ejecutores (suponiendo que todo vaya bien) y envía
la información relevante sobre sus ubicaciones al proceso controlador. Después de que todo esté conectado
correctamente, tenemos un "Grupo de chispas" como probablemente lo considere hoy.
Machine Translated by Google

Ejecución
Ahora que tenemos un "clúster de Spark", Spark se dedica a ejecutar código de manera alegre, como
se muestra en la figura 15­6. El conductor y los trabajadores se comunican entre ellos, ejecutando
código y moviendo datos. El conductor programa tareas para cada trabajador, y cada trabajador
responde con el estado de esas tareas y el éxito o fracaso. (Cubriremos estos detalles en breve).

Figura 15­6. Ejecución de aplicaciones

Terminación
Después de que se completa una aplicación Spark, el controlador finaliza con éxito o falla (Figura
15­7). Luego, el administrador del clúster apaga los ejecutores en ese clúster de Spark para el
controlador. En este punto, puede ver el éxito o el fracaso de la aplicación Spark solicitando esta
información al administrador del clúster.
Machine Translated by Google

Figura 15­7. Cerrar la aplicación

El ciclo de vida de una aplicación Spark (Dentro de Spark)


Acabamos de examinar el ciclo de vida de una aplicación de Spark fuera del código de usuario (básicamente,
la infraestructura que admite Spark), pero podría decirse que es más importante hablar sobre lo que sucede dentro
de Spark cuando ejecuta una aplicación. Este es el "código de usuario" (el código real que escribe que define
su aplicación Spark). Cada aplicación se compone de uno o más trabajos de Spark.
Los trabajos de Spark dentro de una aplicación se ejecutan en serie (a menos que use subprocesos para
iniciar varias acciones en paralelo).

La SparkSession
El primer paso de cualquier aplicación Spark es crear una SparkSession. En muchos modos interactivos, esto se
hace por usted, pero en una aplicación, debe hacerlo manualmente.

Parte de su código heredado podría usar el nuevo patrón SparkContext. Esto debe evitarse en favor del método
de creación en SparkSession, que crea una instancia más sólida de los contextos Spark y SQL y garantiza que
no haya conflicto de contexto, dado que puede haber varias bibliotecas que intentan crear una sesión en
la misma aplicación Spark:

// Crear una SparkSession en Scala import


org.apache.spark.sql.SparkSession val spark =
SparkSession.builder().appName("Databricks Spark Example")
Machine Translated by Google

.config("spark.sql.warehouse.dir", "/usuario/colmena/
almacén") .getOrCreate()

# Creando una SparkSession en Python


desde pyspark.sql import SparkSession
spark = SparkSession.builder.master("local").appName("Word Count")
\ .config("spark.some.config.option", "some­value ")
\ .getOrCreate()

Después de tener una SparkSession, debería poder ejecutar su código Spark. Desde
SparkSession, también puede acceder a todos los contextos y configuraciones heredados y
de bajo nivel en consecuencia. Tenga en cuenta que la clase SparkSession solo se agregó en Spark 2.X.
El código anterior que podría encontrar crearía directamente un SparkContext y un SQLContext
para las API estructuradas.

El SparkContext

Un objeto SparkContext dentro de SparkSession representa la conexión al clúster de Spark.


Esta clase es cómo te comunicas con algunas de las API de nivel inferior de Spark, como RDD.
Comúnmente se almacena como la variable sc en ejemplos y documentación más antiguos.
A través de SparkContext, puede crear RDD, acumuladores y variables de transmisión, y puede
ejecutar código en el clúster.

En su mayor parte, no debería necesitar inicializar explícitamente un SparkContext; solo debería poder
acceder a él a través de SparkSession. Si lo desea, debe crearlo de la manera más general, a través
del método getOrCreate:

// en Scala
importar org.apache.spark.SparkContext
val sc = SparkContext.getOrCreate()
Machine Translated by Google

LA SPARKSESSION, SQLCONTEXT Y HIVECONTEXT

En versiones anteriores de Spark, SQLContext y HiveContext brindaban la capacidad de trabajar


con DataFrames y Spark SQL y se almacenaban comúnmente como la variable sqlContext
en ejemplos, documentación y código heredado. Como punto histórico, Spark 1.X tuvo efectivamente
dos contextos. SparkContext y SQLContext. Estos dos cada uno realizó cosas diferentes. El
primero se centró en un control más detallado de las abstracciones centrales de Spark, mientras
que el segundo se centró en las herramientas de nivel superior como Spark SQL. En Spark 2.X, la
comunidad combinó las dos API en la SparkSession centralizada que tenemos hoy. Sin embargo,
ambas API aún existen y puede acceder a ellas a través de SparkSession. Es importante tener en
cuenta que nunca debería necesitar usar SQLContext y rara vez necesita usar SparkContext.

Después de inicializar su SparkSession, es hora de ejecutar algún código. Como sabemos por los
capítulos anteriores, todo el código de Spark se compila en RDD. Por lo tanto, en la siguiente sección,
tomaremos algunas instrucciones lógicas (un trabajo de DataFrame) y veremos, paso a paso, lo que
sucede con el tiempo.

Instrucciones lógicas
Como vio al comienzo del libro, el código Spark consiste esencialmente en transformaciones y acciones. La
forma en que los construya depende de usted, ya sea a través de SQL, manipulación de RDD de
bajo nivel o algoritmos de aprendizaje automático. Comprender cómo tomamos instrucciones
declarativas como DataFrames y las convertimos en planes de ejecución física es un paso importante para
comprender cómo se ejecuta Spark en un clúster. En esta sección, asegúrese de ejecutar esto en un
entorno nuevo (un nuevo shell de Spark) para seguir los números de trabajo, etapa y tarea.

Instrucciones lógicas a ejecución física.

Mencionamos esto en la Parte II, pero vale la pena reiterarlo para que pueda comprender mejor cómo
Spark toma su código y ejecuta los comandos en el clúster. Revisaremos un poco más de código, línea
por línea, explicaremos lo que sucede detrás de escena para que pueda irse con una mejor comprensión
de sus aplicaciones Spark. En capítulos posteriores, cuando analicemos el monitoreo, realizaremos un
seguimiento más detallado de un trabajo de Spark a través de la interfaz de usuario de Spark. En este ejemplo
actual, tomaremos un enfoque más simple. Vamos a hacer un trabajo de tres pasos: usando un marco
de datos simple, lo volveremos a particionar, realizaremos una manipulación de valor por valor y
luego agregaremos algunos valores y recopilaremos el resultado final.

NOTA

Este código se escribió y se ejecuta con Spark 2.2 en Python (obtendrá el mismo resultado en Scala, por lo que lo
hemos omitido). Es poco probable que la cantidad de trabajos cambie drásticamente, pero podría haber mejoras en
las optimizaciones subyacentes de Spark que cambien las estrategias de ejecución física.
Machine Translated by Google

# en Python
df1 = chispa.rango(2, 10000000, 2) df2
= chispa.rango(2, 10000000, 4) paso1 =
df1.repartición(5) paso12 =
df2.repartición(6) paso2 =
paso1.selectExpr(" id * 5 como id") paso3 =
paso2.join(paso12, ["id"]) paso4 =
paso3.selectExpr("suma(id)")

paso4.recoger() # 2500000000000

Cuando ejecuta este código, podemos ver que su acción desencadena un trabajo de Spark completo. Echemos un
vistazo al plan de explicación para fundamentar nuestra comprensión del plan de ejecución física. También
podemos acceder a esta información en la pestaña SQL (después de ejecutar una consulta) en la interfaz de usuario de Spark:

paso4.explicar()

== Plano físico ==
*Agregado hash(teclas=[], funciones=[suma(id#15L)])
+­ Intercambiar partición única
+­ *Agregado hash(teclas=[], funciones=[suma_parcial(id#15L)])
+­ *Proyecto [id#15L]
+­ *SortMergeJoin [id#15L], [id#10L], Interior
:­ *Ordenar [id#15L ASC NULLS PRIMERO], falso, 0
: +­ Intercambiar partición hash (id#15L, 200)
: +­ *Proyecto [(id#7L * 5) AS id#15L]
: +­ Intercambio RoundRobinPartitioning(5)
: +­ *Rango (2, 10000000, paso=2, divisiones=8)
+­ *Ordenar [id#10L ASC NULLS PRIMERO], falso, 0
+­ Intercambiar partición hash (id#10L, 200)
+­ Intercambio RoundRobinPartitioning(6)
+­ *Rango (2, 10000000, paso=4, divisiones=8)

Lo que tiene cuando llama a recopilar (o cualquier acción) es la ejecución de un trabajo de Spark que consta
individualmente de etapas y tareas. Vaya a localhost:4040 si está ejecutando esto en su máquina local para ver
la interfaz de usuario de Spark. Seguiremos en la pestaña "trabajos" y finalmente saltaremos a etapas y tareas a
medida que avancemos a niveles adicionales de detalle.

Un trabajo de chispa

En general, debe haber un trabajo de Spark para una acción. Las acciones siempre devuelven resultados. Cada
trabajo se divide en una serie de etapas, el número de las cuales depende de cuántas operaciones de
barajado deben realizarse.

Este trabajo se divide en las siguientes etapas y tareas:

Etapa 1 con 8 Tareas


Machine Translated by Google

Etapa 2 con 8 Tareas

Etapa 3 con 6 Tareas

Etapa 4 con 5 Tareas

Etapa 5 con 200 tareas

Etapa 6 con 1 Tarea

Espero que al menos esté algo confundido acerca de cómo llegamos a estos números para que podamos tomarnos
el tiempo de comprender mejor lo que está sucediendo.

Etapas
Las etapas en Spark representan grupos de tareas que se pueden ejecutar juntas para calcular la misma
operación en varias máquinas. En general, Spark intentará empaquetar la mayor cantidad de trabajo posible (es
decir, tantas transformaciones como sea posible dentro de su trabajo) en la misma etapa, pero el motor inicia
nuevas etapas después de las operaciones llamadas mezclas . Una reproducción aleatoria representa una nueva
partición física de los datos; por ejemplo, ordenar un DataFrame o agrupar datos que se cargaron desde un
archivo por clave (lo que requiere enviar registros con la misma clave al mismo nodo). Este tipo de partición requiere
la coordinación entre los ejecutores para mover los datos. Spark inicia una nueva etapa después de cada
reproducción aleatoria y realiza un seguimiento del orden en el que deben ejecutarse las etapas para calcular el resultado final.

En el trabajo que vimos anteriormente, las dos primeras etapas corresponden al rango que realiza para crear sus
DataFrames. De manera predeterminada, cuando crea un DataFrame con rango, tiene ocho particiones. El
siguiente paso es el reparticionamiento. Esto cambia el número de particiones mezclando los datos. Estos
DataFrames se barajan en seis particiones y cinco particiones, correspondientes al número de tareas en
las etapas 3 y 4.

Las etapas 3 y 4 se realizan en cada uno de esos DataFrames y el final de la etapa representa la unión (una
mezcla). De repente, tenemos 200 tareas. Esto se debe a una configuración de Spark SQL. El valor
predeterminado de spark.sql.shuffle.partitions es 200, lo que significa que cuando se realiza una reproducción
aleatoria durante la ejecución, genera 200 particiones aleatorias de forma predeterminada. Puede cambiar este
valor y cambiará el número de particiones de salida.

CONSEJO

Cubrimos el número de particiones con un poco más de detalle en el Capítulo 19 porque es un parámetro muy
importante. Este valor debe establecerse de acuerdo con la cantidad de núcleos en su clúster para garantizar una ejecución
eficiente. Aquí se explica cómo configurarlo:

chispa.conf.set("chispa.sql.shuffle.particiones", 50)

Una buena regla general es que el número de particiones debe ser mayor que el número de
Machine Translated by Google

ejecutores en su clúster, potencialmente por múltiples factores dependiendo de la carga de trabajo. Si está ejecutando
código en su máquina local, le convendría establecer este valor más bajo porque es poco probable que su máquina
local pueda ejecutar esa cantidad de tareas en paralelo. Esto es más un valor predeterminado para un clúster en el que
puede haber muchos más núcleos ejecutores para usar. Independientemente del número de particiones, toda esa
etapa se calcula en paralelo. El resultado final agrega esas particiones individualmente, las lleva todas a una sola
partición antes de enviar finalmente el resultado final al controlador. Veremos esta configuración varias veces a lo largo
de esta parte del libro.

Tareas
Las etapas en Spark consisten en tareas. Cada tarea corresponde a una combinación de bloques de datos y un
conjunto de transformaciones que se ejecutarán en un único ejecutor. Si hay una partición grande en nuestro
conjunto de datos, tendremos una tarea. Si hay 1000 particiones pequeñas, tendremos 1000 tareas que se pueden
ejecutar en paralelo. Una tarea es solo una unidad de cálculo aplicada a una unidad de datos (la partición).
Particionar sus datos en una mayor cantidad de particiones significa que se pueden ejecutar más en paralelo.
Esto no es una panacea, pero es un lugar simple para comenzar con la optimización.

Detalles de ejecución
Las tareas y etapas en Spark tienen algunas propiedades importantes que vale la pena revisar antes de cerrar este
capítulo. En primer lugar, Spark canaliza automáticamente etapas y tareas que se pueden realizar juntas, como
una operación de mapa seguida de otra operación de mapa. En segundo lugar, para todas las operaciones
aleatorias, Spark escribe los datos en un almacenamiento estable (por ejemplo, un disco) y puede reutilizarlos en
varios trabajos. Hablaremos de estos conceptos uno por uno porque aparecerán cuando comience a inspeccionar
aplicaciones a través de la interfaz de usuario de Spark.

Canalización
Una parte importante de lo que convierte a Spark en una "herramienta de cálculo en memoria" es que, a diferencia de
las herramientas anteriores (por ejemplo, MapReduce), Spark realiza tantos pasos como puede en un momento
dado antes de escribir datos en la memoria o el disco. . Una de las optimizaciones clave que realiza Spark es la
canalización, que se produce en el nivel de RDD y por debajo de él. Con la canalización, cualquier secuencia
de operaciones que alimentan datos directamente entre sí, sin necesidad de moverlos entre nodos, se contrae en
una sola etapa de tareas que realizan todas las operaciones juntas. Por ejemplo, si escribe un programa basado en RDD
que hace un mapa, luego un filtro, luego otro mapa, esto dará como resultado una sola etapa de tareas que leerán
inmediatamente cada registro de entrada, lo pasarán por el primer mapa, lo pasarán por el filtro y páselo a través de la
última función de mapa si es necesario. Esta versión segmentada del cálculo es mucho más rápida que escribir los
resultados intermedios en la memoria o el disco después de cada paso. El mismo tipo de canalización ocurre para un
cálculo de DataFrame o SQL que realiza una selección, un filtro y una selección.

Desde un punto de vista práctico, la canalización será transparente para usted mientras escribe una aplicación (el
tiempo de ejecución de Spark lo hará automáticamente), pero lo verá si alguna vez inspecciona su
Machine Translated by Google

aplicación a través de la interfaz de usuario de Spark o a través de sus archivos de registro, donde verá que varias
operaciones de RDD o DataFrame se canalizaron en una sola etapa.

Persistencia aleatoria
La segunda propiedad que verá a veces es la persistencia de la reproducción aleatoria. Cuando Spark necesita ejecutar
una operación que tiene que mover datos entre nodos, como una operación de reducción por clave (donde los datos de
entrada para cada clave deben reunirse primero desde muchos nodos), el motor ya no puede realizar la canalización. y, en
su lugar, realiza una reproducción aleatoria entre redes. Spark siempre ejecuta mezclas haciendo que las tareas de
"origen" (aquellas que envían datos) escriban primero los archivos de mezcla en sus discos locales durante su etapa de
ejecución. Luego, la etapa que realiza la agrupación y la reducción inicia y ejecuta tareas que obtienen sus registros
correspondientes de cada archivo aleatorio y realiza ese cálculo (p. ej., obtiene y procesa los datos para un rango
específico de claves). Guardar los archivos aleatorios en el disco permite que Spark ejecute esta etapa más tarde que la
etapa de origen (por ejemplo, si no hay suficientes ejecutores para ejecutar ambos al mismo tiempo), y también permite
que el motor se vuelva a iniciar para reducir las tareas en caso de falla sin volver a ejecutar todas las tareas de entrada.

Un efecto secundario que observará en la persistencia de la reproducción aleatoria es que ejecutar un nuevo trabajo
sobre datos que ya se mezclaron no vuelve a ejecutar el lado "fuente" de la reproducción aleatoria. Debido a que los
archivos aleatorios ya se escribieron en el disco anteriormente, Spark sabe que puede usarlos para ejecutar las etapas
posteriores del trabajo y no necesita rehacer las anteriores. En la interfaz de usuario y los registros de Spark, verá las
etapas previas a la reproducción aleatoria marcadas como "omitidas". Esta optimización automática puede ahorrar tiempo
en una carga de trabajo que ejecuta varios trabajos sobre los mismos datos, pero, por supuesto, para obtener un
rendimiento aún mejor, puede realizar su propio almacenamiento en caché con el método de caché DataFrame o RDD,
que le permite controlar exactamente qué datos se guardan y dónde. Se acostumbrará rápidamente a este comportamiento
después de ejecutar algunas acciones de Spark en datos agregados e inspeccionarlos en la interfaz de usuario.

Conclusión
En este capítulo, discutimos lo que sucede con las aplicaciones Spark cuando vamos a ejecutarlas en un clúster. Esto
significa cómo el clúster realmente ejecutará ese código, así como lo que sucede dentro de Spark Applications durante el
proceso. En este punto, debería sentirse bastante cómodo al comprender lo que sucede dentro y fuera de una
aplicación Spark. Esto le dará un punto de partida para depurar sus aplicaciones. El Capítulo 16 discutirá cómo escribir
aplicaciones Spark y las cosas que debe considerar al hacerlo.
Machine Translated by Google

Capítulo 16. Desarrollo de


aplicaciones Spark

En el Capítulo 15, aprendió cómo Spark ejecuta su código en el clúster. Ahora le mostraremos lo fácil que es desarrollar
una aplicación Spark independiente e implementarla en un clúster. Haremos esto usando una plantilla simple que
comparte algunos consejos sencillos sobre cómo estructurar sus aplicaciones, incluida la configuración de
herramientas de compilación y pruebas unitarias. Esta plantilla está disponible en el repositorio de código del libro.
Esta plantilla no es realmente necesaria, porque escribir aplicaciones desde cero no es difícil, pero ayuda. Comencemos
con nuestra primera aplicación.

Escribir aplicaciones Spark


Las aplicaciones de Spark son la combinación de dos cosas: un clúster de Spark y su código. En este caso, el
clúster será en modo local y la aplicación será una que esté predefinida. Veamos una aplicación en cada idioma.

Una aplicación simple basada en Scala


Scala es el lenguaje "nativo" de Spark y, naturalmente, es una excelente manera de escribir aplicaciones. Realmente no
es diferente a escribir una aplicación Scala.

CONSEJO

Scala puede parecer intimidante, dependiendo de su experiencia, pero vale la pena aprenderlo, aunque solo
sea para comprender Spark un poco mejor. Además, no es necesario que aprenda todos los entresijos del idioma;
comience con lo básico y verá que es fácil ser productivo en Scala en muy poco tiempo. Usar Scala también te
abrirá muchas puertas. Con un poco de práctica, no es demasiado difícil hacer un seguimiento a nivel de código a
través de la base de código de Spark.

Puede crear aplicaciones con sbt o Apache Maven, dos herramientas de creación basadas en Java Virtual Machine
(JVM). Al igual que con cualquier herramienta de compilación, cada una tiene sus propias peculiaridades, pero
probablemente sea más fácil comenzar con sbt. Puede descargar, instalar y obtener información sobre sbt en el sitio web
de sbt. También puede instalar Maven desde su sitio web respectivo .

Para configurar una compilación sbt para nuestra aplicación Scala, especificamos un archivo build.sbt para
administrar la información del paquete. Dentro del archivo build.sbt , hay algunas cosas clave para incluir:

Metadatos del proyecto (nombre del paquete, información de versión del paquete, etc.)

Dónde resolver las dependencias


Machine Translated by Google

Dependencias necesarias para su biblioteca

Hay muchas más opciones que puede especificar; sin embargo, están más allá del alcance de este libro
(puede encontrar información sobre esto en la web y en la documentación de sbt). También hay algunos libros
sobre el tema que pueden servir como una referencia útil tan pronto como haya ido más allá de cualquier
cosa que no sea trivial. Este es el aspecto que podría tener un archivo Scala built.sbt de muestra (y el que
incluimos en la plantilla). Observe cómo debemos especificar la versión de Scala y la versión de Spark:

nombre := "ejemplo"
organización := "com.databricks" versión :=
"0.1­SNAPSHOT" scalaVersion :=
"2.11.8"

// Información de chispa val


sparkVersion = "2.2.0"

// nos permite incluir solucionadores de paquetes


Spark += "bintray­spark­packages" en
"https://dl.bintray.com/spark­packages/maven/"

resolutores += "Repositorio simple seguro para tipos" en


"http://repo.typesafe.com/typesafe/simple/maven­releases/"

resolutores += "MavenRepository" en "https://


mvnrepository.com/"

bibliotecaDependencias ++= Seq( //


chispa core
"org.apache.spark" %% "spark­core" % sparkVersion,
"org.apache.spark" %% "spark­sql" % sparkVersion,
// el resto del archivo se omite por brevedad)

Ahora que hemos definido el archivo de compilación, podemos agregar código a nuestro proyecto. Usaremos la
estructura de proyecto estándar de Scala, que puede encontrar en el manual de referencia de sbt (esta es la
misma estructura de directorio que los proyectos de Maven):

src/
main/
resources/
<archivos a incluir en el contenedor principal
aquí>
scala/ <fuentes principales
de
Scala> java/ <fuentes
principales de Java> test/
recursos
<archivos para incluir en el jar de prueba aquí>
scala/
<fuentes de prueba de Scala>
Machine Translated by Google

Java/
<probar fuentes Java>

Ponemos el código fuente en los directorios de Scala y Java. En este caso, ponemos algo como lo siguiente en un
archivo; esto inicializa SparkSession, ejecuta la aplicación y luego sale:

objeto DataFrameExample extiende Serializable { def


main(args: Array[String]) = {

val pathToDataFolder = args(0)

// inicia SparkSession // junto con


establecer explícitamente un valor de configuración dado
spark = SparkSession.builder().appName("Ejemplo de
Spark") .config("spark.sql.warehouse.dir", "/user/hive/
almacén") .getOrCreate()

// registro de udf
spark.udf.register("myUDF", someUDF(_:String):String) val df =
spark.read.json(pathToDataFolder + "data.json") val manipulado =
df.groupBy(expr(" myUDF(grupo)")).sum().collect()
.foreach(x => imprimirln(x))

}}

Observe cómo definimos una clase principal que podemos ejecutar desde la línea de comando cuando usamos

spark­submit para enviarlo a nuestro clúster para su ejecución.

Ahora que tenemos nuestro proyecto configurado y le hemos agregado algo de código, es hora de construirlo. Podemos
usar sbt ensamblar para construir un "uber­jar" o "fat­jar" que contenga todas las dependencias en un JAR. Esto puede
ser simple para algunas implementaciones pero puede causar complicaciones (especialmente conflictos de dependencia)
para otras. Un enfoque más ligero es ejecutar el paquete sbt, que reunirá todas sus dependencias en la carpeta de
destino pero no las empaquetará todas en un JAR grande.

Ejecutando la aplicación

La carpeta de destino contiene el JAR que podemos usar como argumento para enviar la chispa. Después de
construir el paquete Scala, termina con algo que puede enviar en su máquina local usando el siguiente código
(este fragmento aprovecha el alias para crear la variable $SPARK_HOME; puede reemplazar $SPARK_HOME con el
directorio exacto que contiene su versión descargada de Spark):

$SPARK_HOME/bin/spark­submit \ ­­
class com.databricks.example.DataFrameExample \ ­­master
local \ target/
scala­2.11/example_2.11­0.1­SNAPSHOT.jar "hola"
Machine Translated by Google

Escribir aplicaciones de Python


Escribir aplicaciones PySpark realmente no es diferente a escribir aplicaciones o paquetes Python normales. Es
bastante similar a escribir aplicaciones de línea de comandos en particular. Spark no tiene un concepto de
compilación, solo secuencias de comandos de Python, por lo que para ejecutar una aplicación, simplemente ejecute la
secuencia de comandos en el clúster.

Para facilitar la reutilización del código, es común empaquetar varios archivos de Python en archivos egg o ZIP de
código Spark. Para incluir esos archivos, puede usar el argumento ­­py­files de spark­submit para agregar
archivos .py, .zip o .egg para que se distribuyan con su aplicación.

Cuando llega el momento de ejecutar su código, crea el equivalente de una "clase principal de Scala/Java" en
Python. Especifique una determinada secuencia de comandos como secuencia de comandos ejecutable que crea
SparkSession. Este es el que pasaremos como argumento principal para spark­submit:

# en Python
desde __future__ import print_function if
__name__ == '__main__': from
pyspark.sql import SparkSession spark =
SparkSession.builder \ .master("local")
\ .appName(" Conteo
de palabras") \ .config("spark.
some.config.option", "algún valor") \ .getOrCreate()

imprimir(chispa.rango(5000).where("id > 500").selectExpr("suma(id)").collect())

Cuando haga esto, obtendrá una SparkSession que puede pasar por su aplicación. Es una buena práctica
pasar esta variable en tiempo de ejecución en lugar de instanciarla dentro de cada clase de Python.

Un consejo útil al desarrollar en Python es usar pip para especificar PySpark como una dependencia.
Puede hacer esto ejecutando el comando pip install pyspark. Esto le permite usarlo de una manera que podría usar otros
paquetes de Python. Esto también hace que la finalización del código sea muy útil en muchos editores. Esto es
completamente nuevo en Spark 2.2, por lo que es posible que se necesite una o dos versiones para estar
completamente listo para la producción, pero Python es muy popular en la comunidad de Spark y seguramente será la
piedra angular del futuro de Spark.

Ejecutando la aplicación

Una vez que haya escrito su código, es hora de enviarlo para su ejecución. (Estamos ejecutando el mismo código que
tenemos en la plantilla del proyecto). Solo necesita llamar a spark­submit con esa información:

$SPARK_HOME/bin/spark­submit ­­master local pyspark_template/main.py

Escritura de aplicaciones Java


Machine Translated by Google

Escribir aplicaciones Java Spark es, si entrecierra los ojos, lo mismo que escribir aplicaciones Scala. Las
principales diferencias involucran cómo especificas tus dependencias.

Este ejemplo asume que está utilizando Maven para especificar sus dependencias. En este caso,
utilizará el siguiente formato. En Maven, debe agregar el repositorio de Spark Packages para que pueda
obtener dependencias de esas ubicaciones:

<dependencias>
<dependencia>
<groupId>org.apache.spark</groupId>
<artifactId>spark­core_2.11</artifactId>
<versión>2.1.0</version> </
dependency>
<dependencia>
<groupId> org.apache.spark</groupId>
<artifactId>spark­sql_2.11</artifactId>
<version>2.1.0</version> </
dependency>
<dependency>
<groupId>graphframes</groupId>
<artifactId>graphframes </artifactId>
<version>0.4.0­spark2.1­s_2.11</version> </
dependency>
</dependencies>
<repositorios>
<!­­ lista de otros repositorios ­­>
<repositorio>
<id>SparkPackagesRepo</id>
<url>http://dl.bintray.com/spark­packages/maven</url> </
repository> </
repositories>

Naturalmente, sigue la misma estructura de directorios que en la versión del proyecto Scala (dado que ambos
se ajustan a la especificación Maven). Luego solo seguimos los ejemplos relevantes de Java para
construir y ejecutar el código. Ahora podemos crear un ejemplo simple que especifique una clase principal
contra la que ejecutar (más sobre esto al final del capítulo):

importar org.apache.spark.sql.SparkSession;
clase pública SimpleExample
{ public static void main(String[] args)
{ SparkSession chispa =

SparkSession .builder() .getOrCreate(); chispa.rango(1, 2000).cuenta();


}
}

Luego lo empaquetamos usando el paquete mvn (necesita tener instalado Maven para hacerlo).

Ejecutando la aplicación
Machine Translated by Google

Esta operación va a ser exactamente igual que ejecutar la aplicación Scala (o la aplicación Python, para el caso).
Simplemente use spark­submit:

$SPARK_HOME/bin/spark­submit \ ­­
class com.databricks.example.SimpleExample \ ­­master
local \ target/spark­
example­0.1­SNAPSHOT.jar "hola"

Prueba de aplicaciones Spark


Ahora sabe lo que se necesita para escribir y ejecutar una aplicación Spark, así que pasemos a un tema menos
emocionante pero muy importante: las pruebas. La prueba de Spark Applications se basa en un par de principios y tácticas
clave que debe tener en cuenta al escribir sus aplicaciones.

Principios Estratégicos
Probar sus canalizaciones de datos y aplicaciones Spark es tan importante como escribirlas.
Esto se debe a que desea asegurarse de que sean resistentes a cambios futuros, en datos, lógica y salida. En esta
sección, primero discutiremos lo que podría querer probar en una aplicación Spark típica, luego discutiremos
cómo organizar su código para facilitar la prueba.

Resiliencia de datos de entrada

Ser resistente a diferentes tipos de datos de entrada es algo fundamental para la forma en que escribe sus canalizaciones de
datos. Los datos cambiarán porque las necesidades del negocio cambiarán.
Por lo tanto, sus aplicaciones y canalizaciones de Spark deben ser resistentes al menos a algún grado de cambio en los
datos de entrada o, de lo contrario, garantizar que estas fallas se manejen de manera elegante y resistente. En su mayor
parte, esto significa ser inteligente al escribir sus pruebas para manejar esos casos extremos de diferentes entradas y
asegurarse de que el localizador solo se active cuando se trate de algo realmente importante.

Resiliencia y evolución de la lógica empresarial

Es probable que la lógica comercial en sus canalizaciones cambie, así como los datos de entrada. Aún más importante,
desea asegurarse de que lo que está deduciendo de los datos sin procesar es lo que realmente cree que está
deduciendo. Esto significa que deberá realizar pruebas lógicas sólidas con datos realistas para asegurarse de que realmente
obtiene lo que desea. Una cosa a tener en cuenta aquí es tratar de escribir un montón de "Pruebas de unidades de Spark"
que solo prueben la funcionalidad de Spark. No quieres estar haciendo eso; en su lugar, desea probar su lógica comercial y
asegurarse de que la canalización comercial compleja que configuró realmente esté haciendo lo que cree que debería estar
haciendo.

Resiliencia en la producción y atomicidad

Suponiendo que esté preparado para desviaciones en la estructura de los datos de entrada y que su lógica comercial esté bien
probada, ahora desea asegurarse de que su estructura de salida sea lo que espera. Esto significa que deberá manejar
correctamente la resolución del esquema de salida. No es frecuente que los datos sean
Machine Translated by Google

simplemente volcado en algún lugar, para no volver a leerlo nunca más; la mayoría de sus canalizaciones
de Spark probablemente estén alimentando otras canalizaciones de Spark. Por esta razón, querrá asegurarse de
que sus consumidores intermedios entiendan el "estado" de los datos; esto podría significar la frecuencia con la
que se actualizan y si los datos están "completos" (por ejemplo, no hay datos atrasados). ) o que no habrá
correcciones de última hora en los datos.

Todos los problemas antes mencionados son principios en los que debe pensar a medida que construye sus
canalizaciones de datos (en realidad, independientemente de si está usando Spark). Este pensamiento estratégico
es importante para sentar las bases del sistema que le gustaría construir.

Conclusiones tácticas
Aunque el pensamiento estratégico es importante, hablemos un poco más en detalle sobre algunas de las
tácticas que puede usar para hacer que su aplicación sea fácil de probar. El enfoque de mayor valor es verificar
que su lógica de negocios sea correcta mediante el empleo de pruebas unitarias adecuadas y garantizar que
sea resistente a los datos de entrada cambiantes o que los haya estructurado para que la evolución del
esquema no se vuelva difícil de manejar en el futuro. La decisión de cómo hacer esto recae en gran medida
en usted como desarrollador, ya que variará según su dominio comercial y su experiencia en el dominio.

Administrar SparkSessions

Probar su código Spark usando un marco de prueba de unidad como JUnit o ScalaTest es relativamente fácil
debido al modo local de Spark: simplemente cree un modo local SparkSession como parte de su arnés de
prueba para ejecutarlo. Sin embargo, para que esto funcione bien, debe intentar realizar la inyección de
dependencia tanto como sea posible al administrar SparkSessions en su código. Es decir, inicialice SparkSession
solo una vez y páselo a funciones y clases relevantes en tiempo de ejecución de una manera que facilite la
sustitución durante la prueba. Esto hace que sea mucho más fácil probar cada función individual con una
SparkSession ficticia en pruebas unitarias.

¿Qué API de Spark usar?

Spark ofrece varias opciones de API, que van desde SQL hasta DataFrames y Datasets, y cada uno de estos
puede tener diferentes impactos en la capacidad de mantenimiento y prueba de su aplicación. Para ser
completamente honesto, la API correcta depende de su equipo y sus necesidades: algunos equipos y proyectos
necesitarán las API de SQL y DataFrame menos estrictas para acelerar el desarrollo, mientras que otros querrán
usar conjuntos de datos o RDD con seguridad de tipos.

En general, recomendamos documentar y probar los tipos de entrada y salida de cada función, independientemente
de la API que utilice. La API con seguridad de tipos impone automáticamente un contrato mínimo para su función
que facilita la creación de otro código a partir de ella. Si su equipo prefiere usar DataFrames o SQL, dedique
algún tiempo a documentar y probar qué devuelve cada función y qué tipos de entradas acepta para evitar
sorpresas posteriores, como en cualquier lenguaje de programación de tipo dinámico. Si bien la API de RDD de
nivel inferior también se tipifica de forma estática, recomendamos acceder a ella solo si necesita funciones de
bajo nivel, como la creación de particiones, que no están presentes en los conjuntos de datos, lo que no debería
ser muy común; la API de conjunto de datos permite más optimizaciones de rendimiento y es probable que
proporcione aún más en el futuro.
Machine Translated by Google

Un conjunto similar de consideraciones se aplica a qué lenguaje de programación usar para su aplicación:
ciertamente no hay una respuesta correcta para cada equipo, pero según sus necesidades, cada lenguaje proporcionará
diferentes beneficios. Por lo general, recomendamos usar lenguajes tipificados estáticamente como Scala y Java
para aplicaciones más grandes o aquellas en las que desea poder pasar a un código de bajo nivel para controlar
completamente el rendimiento, pero Python y R pueden ser significativamente mejores en otros casos, por ejemplo, si
necesita usar algunas de sus otras bibliotecas. El código de Spark debería poder probarse fácilmente en los marcos
de prueba de unidad estándar en todos los idiomas.

Conexión a marcos de pruebas unitarias


Para realizar pruebas unitarias de su código, recomendamos usar los marcos estándar en su idioma (p. ej., JUnit o
ScalaTest) y configurar sus arneses de prueba para crear y limpiar una SparkSession para cada prueba. Diferentes marcos
ofrecen diferentes mecanismos para hacer esto, como los métodos "antes" y "después". Hemos incluido algún código
de prueba de unidad de muestra en las plantillas de aplicación para este capítulo.

Conexión a fuentes de datos


En la medida de lo posible, debe asegurarse de que su código de prueba no se conecte a las fuentes de datos de
producción, de modo que los desarrolladores puedan ejecutarlo fácilmente de forma aislada si estas fuentes de datos
cambian. Una manera fácil de hacer que esto suceda es hacer que todas sus funciones de lógica comercial tomen
DataFrames o Datasets como entrada en lugar de conectarse directamente a varias fuentes; después de todo, el código
subsiguiente funcionará de la misma manera sin importar la fuente de datos. Si está utilizando las API estructuradas en
Spark, otra forma de hacer que esto suceda son las tablas con nombre: simplemente puede registrar algunos conjuntos
de datos ficticios (por ejemplo, cargados desde un archivo de texto pequeño o desde objetos en memoria) como varios
nombres de tabla y continuar desde allí. .

El proceso de desarrollo
El proceso de desarrollo con Spark Applications es similar a los flujos de trabajo de desarrollo que probablemente ya haya
utilizado. Primero, puede mantener un espacio temporal, como un cuaderno interactivo o algún equivalente, y luego, a
medida que crea componentes y algoritmos clave, los mueve a una ubicación más permanente, como una biblioteca o un
paquete. La experiencia del cuaderno es una que recomendamos a menudo (y estamos usando para escribir este libro)
debido a su simplicidad en la experimentación. También hay algunas herramientas, como Databricks, que también
le permiten ejecutar portátiles como aplicaciones de producción.

Cuando se ejecuta en su máquina local, Spark­Shell y sus diversas implementaciones específicas del idioma son
probablemente la mejor manera de desarrollar aplicaciones. En su mayor parte, el shell es para aplicaciones interactivas,
mientras que spark­submit es para aplicaciones de producción en su clúster de Spark. Puede usar el shell para ejecutar
Spark de forma interactiva, tal como le mostramos al comienzo de este libro. Este es el modo con el que ejecutará PySpark,
Spark SQL y SparkR. En la carpeta bin, cuando descargue Spark, encontrará las diversas formas de iniciar estos shells.
Machine Translated by Google

Simplemente ejecute spark­shell (para Scala), spark­sql, pyspark y sparkR.

Una vez que haya terminado su aplicación y haya creado un paquete o secuencia de comandos para ejecutar, spark­submit
se convertirá en su mejor amigo para enviar este trabajo a un clúster.

Lanzamiento de aplicaciones
La forma más común de ejecutar aplicaciones Spark es a través de Spark­Submit. Anteriormente en este capítulo, le
mostramos cómo ejecutar spark­submit; simplemente especifica tus opciones, el archivo JAR o script de la aplicación
y los argumentos relevantes:

./bin/spark­enviar \
­­class <clase principal> \ ­­
master <url­maestra> \ ­­deploy­
mode <modo­implementación> \ ­­conf
<clave>=<valor> \ # otras
... opciones <application­
jar­or­ script> \ [argumentos de la
aplicación]

Siempre puede especificar si se ejecutará en modo de cliente o de clúster cuando envíe un trabajo de Spark con spark­
submit. Sin embargo, casi siempre debería favorecer la ejecución en modo clúster (o en modo cliente en el propio
clúster) para reducir la latencia entre los ejecutores y el controlador.

Al enviar aplicaciones, pase un archivo .py en lugar de un .jar y agregue Python .zip, .egg o .py a la ruta de búsqueda
con ­­py­files.

Como referencia, la Tabla 16­1 enumera todas las opciones de envío de chispas disponibles, incluidas aquellas que
son particulares de algunos administradores de clústeres. Para enumerar todas estas opciones usted mismo, ejecute
Spark Submit con ­­help.

Tabla 16­1. Spark enviar texto de ayuda

Descripción de parámetros

­­maestro
chispa://host:puerto, mesos://host:puerto, hilo o local
MASTER_URL

­­desplegar
Si iniciar el programa del controlador localmente ("cliente") o en una de las máquinas de trabajo
modo
dentro del clúster ("clúster") (Predeterminado: cliente)
MODO_DEPLOY

­­clase
La clase principal de su aplicación (para aplicaciones Java / Scala).
NOMBRE DE LA CLASE

­­name NOMBRE Un nombre de su aplicación.

­­jars JARS Lista separada por comas de archivos JAR locales para incluir en las rutas de clase del controlador y del ejecutor.
Machine Translated by Google

Lista separada por comas de coordenadas Maven de JAR para incluir en las rutas de clase del controlador y del ejecutor.
Buscará en el repositorio local de Maven, luego en Maven Central y cualquier repositorio remoto adicional proporcionado por ­­
­­paquetes
repositories. El formato de las coordenadas debe ser groupId:artifactId:version.

­­excluir Lista separada por comas de groupId:artifactId, para excluir mientras se resuelven las dependencias proporcionadas en ­­

paquetes packages para evitar conflictos de dependencia.

­­
Lista separada por comas de repositorios remotos adicionales para buscar las coordenadas de Maven proporcionadas con
­­packages. repositorios

­­py­archivos
Lista separada por comas de archivos .zip, .egg o .py para colocar en las aplicaciones PYTHONPATH para Python.
PY_ARCHIVOS

­­archivos
Lista de archivos separados por comas que se colocarán en el directorio de trabajo de cada ejecutor.
ARCHIVOS

­­conf
Propiedad de configuración arbitraria de Spark.
PROPIEDAD=VALOR

­­
Ruta a un archivo desde el que cargar propiedades adicionales. Si no se especifica, buscará conf/spark­defaults.conf.
archivo de

propiedades ARCHIVO

­­conductor
Memoria para controlador (p. ej., 1000M, 2G) (Predeterminado: 1024M).
memoria MEM

­­conductor
Opciones extra de Java para pasar al controlador.
java­opciones

­­conductor
Entradas de ruta de biblioteca adicionales para pasar al
controlador. biblioteca­ruta

­­conductor Entradas de rutas de clases adicionales para pasar al controlador. Tenga en cuenta que los archivos JAR agregados

class­path con ­­jars se incluyen automáticamente en el classpath.

­­ejecutor
Memoria por ejecutor (por ejemplo, 1000M, 2G) (Predeterminado: 1G).
memoria MEM

­­proxy­usuario Usuario a suplantar al enviar la solicitud. Este argumento no funciona con ­­principal / ­­keytab.

NOMBRE

­­help, ­h Muestra este mensaje de ayuda y sale.

­­prolijo, ­
Imprimir salida de depuración adicional.
v

­­versión Imprime la versión de Spark actual.

También existen algunas configuraciones específicas de la implementación (consulte la Tabla 16­2).

Tabla 16­2. Configuraciones específicas de implementación


Machine Translated by Google

Tabla 16­2. Configuraciones específicas de implementación

Grupo
Modos Conf. Descripción
Gerentes

­­conductor
Ser único Grupo Núcleos para el controlador (Predeterminado: 1).
núcleos NÚM

Clúster independiente/Mesos ­­supervise Si se proporciona, reinicia el controlador en caso de falla.

­­matar
Clúster Independiente/Mesos Si se proporciona, mata al controlador especificado.
SUBMISSION_ID

­­estado
Clúster Independiente/Mesos Si se proporciona, solicita el estado del controlador especificado.
SUBMISSION_ID

­­total
Independiente/Mesos Cualquiera ejecutor Núcleos totales para todos los ejecutores.

núcleos NÚM

­­ejecutor Número de núcleos por ejecutor. (Predeterminado: 1 en modo YARN o todos


Independiente/YARN Cualquiera
núcleos NUM1 los núcleos disponibles en el trabajador en modo independiente)

­­conductor Número de núcleos utilizados por el controlador, solo en modo clúster


HILO Cualquiera
núcleos NÚM (predeterminado: 1).

HILO Cualquiera cola


La cola de YARN a la que enviar (Predeterminado: "predeterminado").
QUEUE_NAME

Número de ejecutores a lanzar (Predeterminado: 2). Si la asignación


­­num
HILO Cualquiera dinámica está habilitada, el número inicial de ejecutores será al menos NUM.
ejecutores NUM

­­archivo Lista de archivos separados por comas que se extraerán en el directorio


HILO Cualquiera
ARCHIVO de trabajo de cada ejecutor.

­­principal Principal que se usará para iniciar sesión en KDC, mientras se ejecuta
HILO Cualquiera
PRINCIPAL en HDFS seguro.

La ruta completa al archivo que contiene la tabla de claves para el


principal especificado anteriormente. Esta tabla de claves se copiará en el
HILO Cualquiera
­­keytab
nodo que ejecuta Application Master a través de Secure Distributed
TECLADO
Cache, para renovar periódicamente los tickets de inicio de sesión y los
tokens de delegación.

Ejemplos de lanzamiento de aplicaciones


Ya cubrimos algunos ejemplos de aplicaciones de modo local anteriormente en este capítulo, pero también vale la
pena ver cómo usamos algunas de las opciones antes mencionadas. Spark también incluye varios ejemplos y
aplicaciones de demostración en el directorio de ejemplos que se incluye al descargar Spark. Si no sabes cómo usar
ciertos parámetros, simplemente pruébalos primero en
Machine Translated by Google

su máquina local y use la clase SparkPi como la clase principal:

./bin/spark­enviar \
­­class org.apache.spark.examples.SparkPi \ ­­
master spark://207.184.161.138:7077 \ ­­
executor­memory 20G \ ­­
total­executor­cores 100 \ replace/
with/path/to/ ejemplos.jar \ 1000

El siguiente fragmento hace lo mismo para Python. Lo ejecuta desde el directorio de Spark y esto le permitirá enviar
una aplicación de Python (todo en un script) al administrador de clústeres independiente. También puedes
establecer los mismos límites de ejecutor que en el ejemplo anterior:

./bin/spark­enviar \
­­master chispa://207.184.161.138:7077 \
ejemplos/src/main/python/pi.py \ 1000

También puede cambiar esto para que se ejecute en modo local configurando el maestro en local o local[*] para que se
ejecute en todos los núcleos de su máquina. También deberá cambiar /path/to/examples.jar a las versiones relevantes de
Scala y Spark que está ejecutando.

Configuración de aplicaciones
Spark incluye varias configuraciones diferentes, algunas de las cuales cubrimos en el Capítulo 15.
Hay muchas configuraciones diferentes, dependiendo de lo que espera lograr. Esta sección cubre esos mismos
detalles. En su mayor parte, esta información se incluye como referencia y probablemente solo valga la pena hojearla, a
menos que esté buscando algo en particular. La mayoría de las configuraciones se dividen en las siguientes
categorías:

Propiedades de la aplicación

Entorno de ejecución

Comportamiento aleatorio

Interfaz de usuario de Spark

Compresión y serialización

Gestión de la memoria

Comportamiento de ejecución

Redes

Planificación
Machine Translated by Google

Asignación dinámica

Seguridad

Cifrado

Chispa SQL

transmisión de chispas

SparkR

Spark proporciona tres ubicaciones para configurar el sistema:

Las propiedades de Spark controlan la mayoría de los parámetros de la aplicación y se pueden configurar mediante un

objeto SparkConf

Propiedades del sistema Java

Archivos de configuración codificados

Hay varias plantillas que puede usar, que puede encontrar en el directorio /conf disponible en la raíz de la carpeta de inicio de
Spark. Puede establecer estas propiedades como variables codificadas en sus aplicaciones o especificándolas en tiempo de
ejecución. Puede usar variables de entorno para establecer configuraciones por máquina, como la dirección IP, a través
del script conf/spark­env.sh en cada nodo.
Por último, puede configurar el registro a través de log4j.properties.

La SparkConf
SparkConf administra todas las configuraciones de nuestras aplicaciones. Crea uno a través de la declaración de
importación, como se muestra en el siguiente ejemplo. Después de crearlo, SparkConf es inmutable para esa aplicación Spark
específica:

// en Scala
import org.apache.spark.SparkConf val
conf = new SparkConf().setMaster("local[2]").setAppName("DefinitiveGuide") .set("some.conf",
"to.some. valor")

# en Python
desde pyspark import SparkConf
conf = SparkConf().setMaster("local[2]").setAppName("DefinitiveGuide")
\ .set("some.conf", "to.some.value")

Utilice SparkConf para configurar aplicaciones Spark individuales con propiedades Spark. Estas propiedades de Spark controlan
cómo se ejecuta la aplicación Spark y cómo se configura el clúster. El siguiente ejemplo configura el clúster local para que
tenga dos subprocesos y especifica el nombre de la aplicación que aparece en la interfaz de usuario de Spark.

Puede configurarlos en tiempo de ejecución, como vio anteriormente en este capítulo a través de la línea de comandos.
Machine Translated by Google

argumentos Esto es útil al iniciar un Spark Shell que incluirá automáticamente una aplicación Spark básica
para usted; por ejemplo:

./bin/spark­submit ­­name "DefinitiveGuide" ­­master local[4] ...

Cabe destacar que al establecer propiedades basadas en la duración del tiempo, debe usar el siguiente formato:

25ms (milisegundos)

5s (segundos)

10m o 10min (minutos)

3h (horas)

5d (días)

1y (años)

Propiedades de la aplicación
Las propiedades de la aplicación son aquellas que establece desde Spark­submit o cuando crea su aplicación
Spark. Definen los metadatos básicos de la aplicación, así como algunas características de
ejecución. La Tabla 16­3 presenta una lista de las propiedades de la aplicación actual.

Tabla 16­3. Propiedades de la aplicación

Nombre de la propiedad Significado predeterminado

El nombre de su aplicación. Esto aparecerá en la interfaz de usuario y en los datos de registro.


chispa.app.nombre (ninguno)

chispa.conductor.núcleos 1 Número de núcleos a usar para el proceso del controlador, solo en modo de clúster.

Límite del tamaño total de los resultados serializados de todas las particiones para cada

acción de Spark (p. ej., recopilar). Debe ser al menos 1M, o 0 para ilimitado.

Los trabajos se cancelarán si el tamaño total supera este límite. Tener un límite alto puede

chispa.driver.maxResultSize 1g causar OutOfMemoryErrors en el controlador (depende de spark.driver.memory y la sobrecarga

de memoria de los objetos en JVM).

Establecer un límite adecuado puede proteger al controlador de

OutOfMemoryErrors.

Cantidad de memoria que se usará para el proceso del controlador, donde se

inicializa SparkContext . (por ejemplo , 1 g, 2 g). Nota: en el modo de cliente, esto no debe

configurarse a través de SparkConf directamente en su aplicación, porque la JVM del controlador


chispa.driver.memoria 1g
ya se inició en ese punto. En su lugar, configure esto a través de la opción de línea de

comandos ­­driver­memory o en su archivo de propiedades predeterminado.

chispa.executor.memoria 1g Cantidad de memoria a usar por proceso ejecutor (por ejemplo, 2g, 8g).
Machine Translated by Google

Una lista separada por comas de clases que implementan SparkListener; al inicializar

SparkContext, las instancias de estas clases se crearán y registrarán con el bus de escucha de

Spark. Si una clase tiene un constructor de un solo argumento que acepta una SparkConf,

chispa.extraListeners (ninguno) se llamará a ese constructor; de lo contrario, se llamará a un constructor de cero

argumentos. Si no se puede encontrar un constructor válido, la creación de SparkContext fallará


con una excepción.

Registra la SparkConf efectiva como INFO cuando se inicia un SparkContext .


chispa.logConf FALSO

El administrador de clústeres al que conectarse. Consulte la lista de URL maestras permitidas.


chispa.master (ninguno)

El modo de implementación del programa del controlador Spark, ya sea "cliente" o "clúster", lo

chispa.submit.deployMode (ninguno) que significa iniciar el programa del controlador localmente ("cliente") o de forma remota ("clúster")

en uno de los nodos dentro del clúster.

Información de la aplicación que se escribirá en el registro de auditoría de HDFS/

registro de RM de Yarn cuando se ejecute en Yarn/HDFS. Su longitud depende de la

chispa.log.callerContext (ninguno) configuración de Hadoop hadoop.caller.context.max.size.

Debe ser conciso y normalmente puede tener hasta 50 caracteres.

Si es verdadero, reinicia el controlador automáticamente si falla con un estado de salida distinto

chispa.conductor.supervisar FALSO de cero. Solo tiene efecto en el modo autónomo de Spark o en el modo de implementación

de clúster de Mesos.

Puede asegurarse de que ha configurado correctamente estos valores comprobando la interfaz de usuario web de la
aplicación en el puerto 4040 del controlador en la pestaña "Entorno". Solo aparecerán los valores especificados
explícitamente a través de spark defaults.conf, SparkConf o la línea de comando. Para todas las demás
propiedades de configuración, puede suponer que se utiliza el valor predeterminado.

Propiedades de tiempo de ejecución

Aunque es menos común, hay ocasiones en las que también puede necesitar configurar el entorno de tiempo de
ejecución de su aplicación. Debido a limitaciones de espacio, no podemos incluir aquí todo el conjunto de
configuración. Consulte la tabla correspondiente sobre el entorno de tiempo de ejecución en la documentación de
Spark. Estas propiedades le permiten configurar classpaths adicionales y rutas de python para controladores y
ejecutores, configuraciones de trabajo de Python, así como propiedades de registro diversas.

Propiedades de ejecución
Estas configuraciones son algunas de las más relevantes para configurar porque le brindan un control más detallado
sobre la ejecución real. Debido a limitaciones de espacio, no podemos incluir todo el
Machine Translated by Google

configuración establecida aquí. Consulte la tabla correspondiente sobre Comportamiento de ejecución en la

documentación de Spark. Las configuraciones más comunes para cambiar son spark.executor.cores (para controlar la cantidad de

núcleos disponibles) y spark.files.maxPartitionBytes (tamaño máximo de partición al leer archivos).

Configuración de la gestión de memoria


Hay momentos en los que es posible que necesite administrar manualmente las opciones de memoria para intentar optimizar
sus aplicaciones. Muchos de estos no son particularmente relevantes para los usuarios finales porque involucran muchos
conceptos heredados o controles detallados que se obviaron en Spark 2.X debido a la administración automática de memoria.
Debido a limitaciones de espacio, no podemos incluir aquí todo el conjunto de configuración. Consulte la tabla correspondiente
sobre Administración de memoria en la documentación de Spark.

Configuración del comportamiento aleatorio


Hemos enfatizado cómo las mezclas pueden ser un cuello de botella en los trabajos de Spark debido a su alta sobrecarga
de comunicación. Por lo tanto, hay una serie de configuraciones de bajo nivel para controlar el comportamiento aleatorio.
Debido a limitaciones de espacio, no podemos incluir aquí todo el conjunto de configuración. Consulte la tabla correspondiente sobre
Comportamiento aleatorio en la documentación de Spark.

Variables ambientales
Puede configurar ciertos ajustes de Spark a través de variables de entorno, que se leen desde el script conf/spark­env.sh en el directorio
donde está instalado Spark (o conf/spark­env.cmd en Windows). En los modos Independiente y Mesos, este archivo puede
brindar información específica de la máquina, como nombres de host. También se obtiene cuando se ejecutan aplicaciones Spark locales
o scripts de envío.

Tenga en cuenta que conf/spark­env.sh no existe de forma predeterminada cuando se instala Spark. Sin embargo, puede copiar conf/
spark­env.sh.template para crearlo. Asegúrese de hacer la copia ejecutable.

Las siguientes variables se pueden configurar en spark­env.sh:

JAVA_HOME

Ubicación donde está instalado Java (si no está en su RUTA predeterminada).

PYSPARK_PYTHON

Ejecutable binario de Python para usar con PySpark tanto en el controlador como en los trabajadores (el valor

predeterminado es python2.7 si está disponible; de lo contrario, python). La propiedad spark.pyspark.python tiene


prioridad si está configurada.

PYSPARK_DRIVER_PYTHON

Ejecutable binario de Python para usar con PySpark solo en el controlador (el valor predeterminado es PYSPARK_PYTHON).

La propiedad spark.pyspark.driver.python tiene prioridad si está configurada.


Machine Translated by Google

SPARKR_DRIVER_R

Ejecutable binario R para usar con el shell SparkR (el valor predeterminado es R). La

propiedad spark.r.shell.command tiene prioridad si está configurada.

SPARK_LOCAL_IP

Dirección IP de la máquina a la que enlazar.

SPARK_PUBLIC_DNS

El nombre de host que su programa Spark anunciará a otras máquinas.

Además de las variables que se enumeran, también hay opciones para configurar los scripts de clúster independientes de Spark,
como la cantidad de núcleos para usar en cada máquina y la memoria máxima. Debido a que spark­env.sh es un script de shell, puede
configurar algunos de estos mediante programación; por ejemplo, puede calcular SPARK_LOCAL_IP buscando la IP de una

interfaz de red específica.

NOTA

Al ejecutar Spark en YARN en modo de clúster, debe configurar las variables de entorno mediante la propiedad
spark.yarn.appMasterEnv.[EnvironmentVariableName] en su archivo conf/spark­defaults.conf . Las variables de
entorno que se configuran en spark­env.sh no se reflejarán en el proceso maestro de aplicaciones de YARN en el
modo de clúster. Consulte las propiedades de Spark relacionadas con YARN para obtener más información.

Programación de trabajos dentro de una aplicación


Dentro de una aplicación Spark determinada, se pueden ejecutar varios trabajos paralelos simultáneamente si se enviaron desde
subprocesos separados. Por trabajo, en esta sección, nos referimos a una acción de Spark y cualquier tarea que deba ejecutarse
para evaluar esa acción. El programador de Spark es completamente seguro para subprocesos y admite este caso de uso para
habilitar aplicaciones que atienden múltiples solicitudes (por ejemplo, consultas para múltiples usuarios).

De manera predeterminada, el programador de Spark ejecuta trabajos en modo FIFO . Si los trabajos que encabezan la cola no
necesitan usar todo el clúster, los trabajos posteriores pueden comenzar a ejecutarse de inmediato, pero si los trabajos que
encabezan la cola son grandes, es posible que los trabajos posteriores se retrasen considerablemente.

También es posible configurar un reparto justo entre trabajos. Con el uso compartido justo, Spark asigna tareas entre trabajos de
forma rotativa para que todos los trabajos obtengan una parte aproximadamente igual de los recursos del clúster. Esto
significa que los trabajos cortos enviados mientras se ejecuta un trabajo largo pueden comenzar a recibir recursos de inmediato y
aun así lograr buenos tiempos de respuesta sin esperar a que termine el trabajo largo. Este modo es el mejor para la configuración
multiusuario.

Para habilitar el programador justo, establezca la propiedad spark.scheduler.mode en FAIR al configurar un SparkContext.

El programador justo también admite la agrupación de trabajos en grupos y la configuración de diferentes opciones de
programación, o pesos, para cada grupo. Esto puede ser útil para crear un grupo de alta prioridad para más
Machine Translated by Google

trabajos importantes o para agrupar los trabajos de cada usuario y dar a los usuarios partes iguales independientemente de
cuántos trabajos simultáneos tengan en lugar de dar trabajos a partes iguales. Este enfoque sigue el modelo de Hadoop
Fair Scheduler.

Sin ninguna intervención, los trabajos recién enviados van a un grupo predeterminado, pero los grupos de trabajos se pueden
configurar agregando la propiedad local spark.scheduler.pool al SparkContext en el subproceso que los envía. Esto se hace
de la siguiente manera (suponiendo que sc sea su SparkContext:

sc.setLocalProperty("spark.scheduler.pool", "pool1")

Después de configurar esta propiedad local, todos los trabajos enviados dentro de este subproceso utilizarán este nombre de
grupo. La configuración es por subproceso para facilitar que un subproceso ejecute varios trabajos en nombre del mismo usuario.
Si desea borrar el grupo al que está asociado un subproceso, configúrelo en nulo.

Conclusión
Este capítulo cubrió mucho sobre las aplicaciones Spark; aprendimos a escribirlos, probarlos, ejecutarlos y configurarlos
en todos los lenguajes de Spark. En el Capítulo 17, hablamos sobre la implementación y las opciones de administración
de clústeres que tiene cuando se trata de ejecutar aplicaciones Spark.
Machine Translated by Google

Capítulo 17. Implementación de Spark

Este capítulo explora la infraestructura que necesita para que usted y su equipo puedan ejecutar
Aplicaciones de chispa:

Opciones de implementación de clústeres

Los diferentes administradores de clústeres de Spark

Consideraciones de implementación y configuración de implementaciones

En su mayor parte, parte de Spark debería funcionar de manera similar con todos los administradores de clúster compatibles; sin
embargo, personalizar la configuración significa comprender las complejidades de cada uno de los sistemas de administración
de clústeres. La parte difícil es decidir sobre el administrador de clústeres (o elegir un servicio administrado).
Aunque nos complacerá incluir todos los detalles minuciosos sobre cómo puede configurar diferentes clústeres con diferentes
administradores de clústeres, es simplemente imposible que este libro brinde detalles hiperespecíficos para cada situación en
cada entorno individual. El objetivo de este capítulo, por lo tanto, no es discutir cada uno de los administradores de clústeres en
detalle, sino más bien observar sus diferencias fundamentales y proporcionar una referencia para gran parte del material que ya
está disponible en el sitio web de Spark. Desafortunadamente, no hay una respuesta fácil a "cuál es el administrador de clústeres
más fácil de ejecutar" porque varía mucho según el caso de uso, la experiencia y los recursos. El sitio de documentación
de Spark ofrece muchos detalles sobre la implementación de Spark con ejemplos prácticos. Hacemos nuestro mejor esfuerzo
para discutir los puntos más relevantes.

Al momento de escribir este artículo, Spark tiene tres administradores de clúster con soporte oficial:

modo independiente

HILO de Hadoop

apache mesos

Estos administradores de clúster mantienen un conjunto de máquinas en las que puede implementar aplicaciones
Spark. Naturalmente, cada uno de estos administradores de clústeres tiene una opinión obstinada sobre la administración,
por lo que hay compensaciones y semánticas que deberá tener en cuenta.
Sin embargo, todos ejecutan las aplicaciones Spark de la misma manera (como se describe en el Capítulo 16). Comencemos con
el primer punto: dónde implementar su clúster.

Dónde implementar su clúster para ejecutar aplicaciones Spark


Hay dos opciones de alto nivel sobre dónde implementar los clústeres de Spark: implementar en un clúster local o en la nube
pública. Esta elección es consecuente y, por lo tanto, vale la pena discutirla.

Implementaciones de clústeres locales


Machine Translated by Google

La implementación de Spark en un clúster local a veces es una opción razonable, especialmente para las
organizaciones que ya administran sus propios centros de datos. Como con todo lo demás, hay compensaciones en
este enfoque. Un clúster local le brinda control total sobre el hardware utilizado, lo que significa que puede
optimizar el rendimiento para su carga de trabajo específica. Sin embargo, también presenta algunos desafíos,
especialmente cuando se trata de cargas de trabajo de análisis de datos como Spark. En primer lugar, con la
implementación local, su clúster tiene un tamaño fijo, mientras que las demandas de recursos de las cargas
de trabajo de análisis de datos suelen ser elásticas. Si hace que su clúster sea demasiado pequeño, será difícil lanzar
consultas de análisis muy grandes ocasionales o trabajos de capacitación para un nuevo modelo de aprendizaje
automático, mientras que si lo hace grande, tendrá recursos inactivos. En segundo lugar, para los clústeres
locales, debe seleccionar y operar su propio sistema de almacenamiento, como un sistema de archivos Hadoop o un
almacén de clave­valor escalable. Esto incluye configurar la replicación geográfica y la recuperación ante
desastres si es necesario.

Si va a implementar localmente, la mejor manera de combatir el problema de utilización de recursos es usar un


administrador de clústeres que le permita ejecutar muchas aplicaciones Spark y reasignar dinámicamente
recursos entre ellas, o incluso permitir aplicaciones que no sean Spark en el mismo grupo. Todos los administradores
de clústeres compatibles con Spark permiten varias aplicaciones simultáneas, pero YARN y Mesos tienen una mejor
compatibilidad con el uso compartido dinámico y, además, admiten cargas de trabajo que no son de Spark.
Es probable que manejar el uso compartido de recursos sea la mayor diferencia que sus usuarios vean día a día con
Spark en las instalaciones versus en la nube: en las nubes públicas, es fácil dar a cada aplicación su propio clúster
del tamaño exacto requerido solo por la duración de ese trabajo.

Para el almacenamiento, tiene varias opciones diferentes, pero cubrir todas las compensaciones y los detalles
operativos en profundidad probablemente requiera su propio libro. Los sistemas de almacenamiento más comunes
que se utilizan para Spark son los sistemas de archivos distribuidos, como HDFS de Hadoop, y los almacenes
de clave­valor, como Apache Cassandra. Los sistemas de bus de transmisión de mensajes, como Apache Kafka,
también se usan a menudo para ingerir datos. Todos estos sistemas tienen distintos grados de compatibilidad con la
administración, la copia de seguridad y la replicación geográfica, a veces integrados en el sistema y, a veces,
solo a través de herramientas comerciales de terceros. Antes de elegir una opción de almacenamiento, recomendamos
evaluar el rendimiento de su conector Spark y evaluar las herramientas de administración disponibles.

chispa en la nube
Si bien los primeros sistemas de big data se diseñaron para la implementación local, la nube ahora es una
plataforma cada vez más común para implementar Spark. La nube pública tiene varias ventajas cuando se trata de
grandes cargas de trabajo de datos. En primer lugar, los recursos se pueden iniciar y cerrar de forma elástica, de modo
que puede ejecutar ese trabajo "monstruoso" ocasional que requiere cientos de máquinas durante unas pocas
horas sin tener que pagar por ellas todo el tiempo. Incluso para el funcionamiento normal, puede elegir un tipo
diferente de máquina y tamaño de clúster para cada aplicación a fin de optimizar su rendimiento de costos; por ejemplo,
inicie máquinas con unidades de procesamiento de gráficos (GPU) solo para sus trabajos de aprendizaje
profundo. En segundo lugar, las nubes públicas incluyen almacenamiento georeplicado de bajo costo que facilita la
administración de grandes cantidades de datos.

Muchas empresas que buscan migrar a la nube imaginan que ejecutarán sus aplicaciones de la misma manera
que ejecutan sus clústeres locales. Todos los principales proveedores de nube (Amazon Web
Machine Translated by Google

Services [AWS], Microsoft Azure, Google Cloud Platform [GCP] e IBM Bluemix) incluyen clústeres de Hadoop
administrados para sus clientes, que proporcionan HDFS para almacenamiento, así como Apache Spark. Sin
embargo, esta no es una excelente manera de ejecutar Spark en la nube, porque al usar un clúster y un sistema de
archivos de tamaño fijo, no podrá aprovechar la elasticidad. En su lugar, generalmente es una mejor idea usar
sistemas de almacenamiento global que estén desacoplados de un clúster específico, como Amazon S3, Azure Blob
Storage o Google Cloud Storage y activar máquinas dinámicamente para cada carga de trabajo de Spark. Con el
cómputo y el almacenamiento desacoplados, podrá pagar los recursos de cómputo solo cuando los necesite, escalarlos
dinámicamente y combinar diferentes tipos de hardware. Básicamente, tenga en cuenta que ejecutar Spark en la nube
no significa necesariamente migrar una instalación local a máquinas virtuales: puede ejecutar Spark de forma nativa en
el almacenamiento en la nube para aprovechar al máximo la elasticidad de la nube, el beneficio de ahorro de costos y
las herramientas de administración sin tener que administrar una pila informática local dentro de su entorno de nube.

Varias empresas ofrecen servicios basados en Spark "nativos de la nube" y, por supuesto, todas las instalaciones de
Apache Spark pueden conectarse al almacenamiento en la nube. Databricks, la empresa iniciada por el equipo Spark de
UC Berkeley, es un ejemplo de un proveedor de servicios creado específicamente para Spark en la nube.
Databricks proporciona una forma sencilla de ejecutar cargas de trabajo de Spark sin el pesado equipaje de una
instalación de Hadoop. La empresa ofrece una serie de funciones para ejecutar Spark de manera más eficiente
en la nube, como el escalado automático, la terminación automática de clústeres y conectores optimizados para
el almacenamiento en la nube, así como un entorno colaborativo para trabajar en portátiles y trabajos independientes.
La compañía también ofrece una Community Edition gratuita para aprender Spark, donde puede ejecutar cuadernos
en un pequeño clúster y compartirlos en vivo con otros. Un hecho divertido es que todo este libro se escribió con la
Community Edition gratuita de Databricks, porque descubrimos que los cuadernos Spark integrados, la colaboración
en vivo y la administración de clústeres son la forma más fácil de producir y probar este contenido.

Si ejecuta Spark en la nube, es posible que gran parte del contenido de este capítulo no sea relevante porque, a
menudo, puede crear un clúster de Spark separado y de corta duración para cada trabajo que ejecute. En ese caso, el
administrador de clústeres independiente es probablemente el más fácil de usar. Sin embargo, es posible que desee
leer este contenido si desea compartir un clúster de mayor duración entre muchas aplicaciones o instalar Spark en
máquinas virtuales usted mismo.

Administradores de clústeres
A menos que esté usando un servicio administrado de alto nivel, tendrá que decidir qué administrador de clúster
usará para Spark. Spark es compatible con los tres administradores de clústeres mencionados anteriormente:
clústeres independientes, Hadoop YARN y Mesos. Repasemos cada uno de estos.

Modo independiente
El administrador de clústeres independiente de Spark es una plataforma liviana creada específicamente para las cargas
de trabajo de Apache Spark. Al usarlo, puede ejecutar varias aplicaciones Spark en el mismo clúster. También
proporciona interfaces simples para hacerlo, pero puede escalar a grandes cargas de trabajo de Spark. El principal
Machine Translated by Google

La desventaja del modo independiente es que es más limitado que los otros administradores de clústeres; en
particular, su clúster solo puede ejecutar Spark. Sin embargo, es probablemente el mejor punto de partida si solo
desea que Spark se ejecute rápidamente en un clúster y no tiene experiencia en el uso de YARN o Mesos.

Inicio de un clúster independiente

Iniciar un clúster independiente requiere aprovisionar las máquinas para hacerlo. Eso significa iniciarlos,
asegurarse de que puedan comunicarse entre sí a través de la red y obtener la versión de Spark que le gustaría
ejecutar en esos conjuntos de máquinas. Después de eso, hay dos formas de iniciar el clúster: a mano o mediante
secuencias de comandos de inicio integradas.

Primero lancemos un clúster a mano. El primer paso es iniciar el proceso maestro en la máquina en la que
queremos que se ejecute, usando el siguiente comando:

$SPARK_HOME/sbin/start­master.sh

Cuando ejecutamos este comando, el proceso maestro del administrador de clústeres se iniciará en esa máquina.
Una vez iniciado, el maestro imprime un spark://HOST:PORT URI. Usa esto cuando inicia cada uno de los nodos de
trabajo del clúster y puede usarlo como argumento maestro para su SparkSession en la inicialización
de la aplicación. También puede encontrar este URI en la interfaz de usuario web del maestro, que es http://
master­ip­address:8080 de forma predeterminada. Con ese URI, inicie los nodos trabajadores iniciando sesión
en cada máquina y ejecutando el siguiente script con el URI que acaba de recibir del nodo principal. La máquina
maestra debe estar disponible en la red de los nodos trabajadores que está utilizando, y el puerto también debe estar
abierto en el nodo maestro:

$SPARK_HOME/sbin/start­slave.sh <URI­de­chispa­maestra>

¡Tan pronto como lo haya ejecutado en otra máquina, tendrá un clúster de Spark ejecutándose! Este proceso es
naturalmente un poco manual; Afortunadamente, existen scripts que pueden ayudar a automatizar este proceso.

Scripts de inicio de clúster

Puede configurar secuencias de comandos de lanzamiento de clústeres que pueden automatizar el lanzamiento de
clústeres independientes. Para hacer esto, cree un archivo llamado conf/slaves en su directorio de Spark que
contendrá los nombres de host de todas las máquinas en las que desea iniciar los trabajadores de Spark, uno
por línea. Si este archivo no existe, todo se iniciará localmente. Cuando vaya a iniciar realmente el clúster,
la máquina maestra accederá a cada una de las máquinas trabajadoras a través de Secure Shell (SSH). De forma
predeterminada, SSH se ejecuta en paralelo y requiere que configure el acceso sin contraseña (usando una clave
privada). Si no tiene una configuración sin contraseña, puede establecer la variable de entorno
SPARK_SSH_FOREGROUND y proporcionar en serie una contraseña para cada trabajador.

Después de configurar este archivo, puede iniciar o detener su clúster mediante los siguientes scripts de shell,
basados en los scripts de implementación de Hadoop y disponibles en $SPARK_HOME/sbin:

$SPARK_HOME/sbin/start­master.sh
Machine Translated by Google

Inicia una instancia maestra en la máquina en la que se ejecuta el script.

$SPARK_HOME/sbin/start­slaves.sh

Inicia una instancia esclava en cada máquina especificada en el archivo conf/slaves .

$SPARK_HOME/sbin/start­slave.sh

Inicia una instancia esclava en la máquina en la que se ejecuta el script.

$SPARK_HOME/sbin/start­all.sh
Inicia tanto un maestro como varios esclavos como se describió anteriormente.

$SPARK_HOME/sbin/stop­master.sh

Detiene el maestro que se inició mediante el script bin/start­master.sh .

$SPARK_HOME/sbin/stop­slaves.sh

Detiene todas las instancias de esclavos en las máquinas especificadas en el archivo conf/slaves .

$SPARK_HOME/sbin/stop­all.sh

Detiene tanto al maestro como a los esclavos como se describió anteriormente.

Configuraciones de clúster independientes

Los clústeres independientes tienen varias configuraciones que puede usar para ajustar su aplicación.
Estos controlan todo, desde lo que sucede con los archivos antiguos en cada trabajador para las
aplicaciones terminadas hasta el núcleo y los recursos de memoria del trabajador. Estos se controlan mediante
variables de entorno o mediante propiedades de la aplicación. Debido a limitaciones de espacio, no podemos
incluir aquí todo el conjunto de configuración. Consulte la tabla correspondiente sobre Variables de entorno
independientes en la documentación de Spark.

Envío de solicitudes

Después de crear el clúster, puede enviarle aplicaciones mediante el URI spark:// del maestro. Puede hacer esto
en el propio nodo principal o en otra máquina mediante Spark­Submit.
Hay algunos argumentos de línea de comandos específicos para el modo independiente, que cubrimos en "Inicio
de aplicaciones".

Chispa en HILO
Hadoop YARN es un marco para la programación de trabajos y la gestión de recursos de clúster. Aunque Spark
a menudo se (mal) clasifica como parte del "Ecosistema Hadoop", en realidad, Spark tiene poco que ver con Hadoop.
Spark admite de forma nativa el administrador de clústeres de Hadoop YARN, pero no requiere nada de Hadoop en
sí.

Puede ejecutar sus trabajos de Spark en Hadoop YARN especificando el maestro como YARN en los
argumentos de la línea de comandos de envío de chispa. Al igual que con el modo independiente, hay una serie de
Machine Translated by Google

perillas que puede ajustar de acuerdo con lo que le gustaría que hiciera el grupo. La cantidad de perillas es
naturalmente mayor que la del modo independiente de Spark porque Hadoop YARN es un programador genérico
para una gran cantidad de marcos de ejecución diferentes.

La configuración de un clúster de YARN está más allá del alcance de este libro, pero hay algunos libros excelentes
sobre el tema, así como servicios administrados que pueden simplificar esta experiencia.

Envío de solicitudes

Al enviar aplicaciones a YARN, la principal diferencia con otras implementaciones es que el maestro se convertirá
en yarn en lugar de la IP del nodo maestro, ya que está en modo independiente. En su lugar, Spark encontrará los
archivos de configuración de YARN mediante la variable de entorno HADOOP_CONF_DIR o YARN_CONF_DIR. Una
vez que haya configurado esas variables de entorno en el directorio de configuración de su instalación de Hadoop,
puede ejecutar spark­submit como vimos en el Capítulo 16.

NOTA

Hay dos modos de implementación que puede usar para iniciar Spark en YARN. Como se discutió en
capítulos anteriores, el modo de clúster tiene el controlador de chispa como un proceso administrado por el clúster
de YARN, y el cliente puede salir después de crear la aplicación. En modo cliente, el controlador se ejecutará en el
proceso del cliente y, por lo tanto, YARN será responsable solo de otorgar recursos de ejecución a la aplicación, no
de mantener el nodo maestro. También cabe destacar que en el modo de clúster, Spark no se ejecuta necesariamente
en la misma máquina en la que se está ejecutando. Por lo tanto, las bibliotecas y los archivos jar externos deben
distribuirse manualmente o mediante el argumento de la línea de comandos ­­jars.

Hay algunas propiedades específicas de YARN que puede establecer mediante Spark­Submit. Estos le permiten
controlar las colas de prioridad y cosas como las fichas de claves para la seguridad. Los cubrimos en
"Lanzamiento de aplicaciones" en el Capítulo 16.

Configuración de Spark en aplicaciones YARN


La implementación de Spark como aplicaciones YARN requiere que comprenda la variedad de configuraciones
diferentes y sus implicaciones para sus aplicaciones Spark. Esta sección cubre algunas de las mejores prácticas para
configuraciones básicas e incluye referencias a algunas de las configuraciones importantes para ejecutar sus
aplicaciones Spark.

Configuraciones de Hadoop

Si planea leer y escribir desde HDFS usando Spark, debe incluir dos archivos de configuración de Hadoop
en la ruta de clase de Spark: hdfs­site.xml, que proporciona comportamientos predeterminados para el cliente HDFS;
y core­site.xml, que establece el nombre del sistema de archivos predeterminado. La ubicación de estos archivos
de configuración varía según las versiones de Hadoop, pero una ubicación común es dentro de /etc/
hadoop/conf. Algunas herramientas también crean estas configuraciones sobre la marcha, por lo que es importante
comprender cómo su servicio administrado también podría implementarlas.
Machine Translated by Google

Para que Spark pueda ver estos archivos, establezca HADOOP_CONF_DIR en $SPARK_HOME/spark­env.sh en una
ubicación que contenga los archivos de configuración o como una variable de entorno cuando envíe su aplicación a
Spark.

Propiedades de aplicación para YARN

Hay una serie de configuraciones relacionadas con Hadoop y cosas que surgen que en gran medida no tienen mucho
que ver con Spark, solo ejecutar o asegurar YARN de una manera que influye en cómo se ejecuta Spark. Debido a
limitaciones de espacio, no podemos incluir aquí la configuración establecida. Consulte la tabla correspondiente sobre
configuraciones de YARN en la documentación de Spark.

Chispa en Mesos
Apache Mesos es otro sistema de agrupación en clústeres en el que se puede ejecutar Spark. Un hecho divertido sobre
Mesos es que el proyecto también fue iniciado por muchos de los autores originales de Spark, incluido uno de
los autores de este libro. En palabras del propio proyecto Mesos:

Apache Mesos abstrae la CPU, la memoria, el almacenamiento y otros recursos informáticos de las máquinas
(físicas o virtuales), lo que permite que los sistemas distribuidos elásticos y tolerantes a fallas se construyan fácilmente
y se ejecuten de manera efectiva.

En su mayor parte, Mesos tiene la intención de ser un administrador de clústeres de escala de centro de datos que
administra no solo aplicaciones de corta duración como Spark, sino también aplicaciones de larga ejecución como
aplicaciones web u otras interfaces de recursos. Mesos es el administrador de clústeres de mayor peso, simplemente
porque puede elegir este administrador de clústeres solo si su organización ya tiene una implementación a gran
escala de Mesos, pero de todos modos es un buen administrador de clústeres.

Mesos es una gran pieza de infraestructura y, lamentablemente, simplemente hay demasiada información para que
podamos cubrir cómo implementar y mantener los clústeres de Mesos. Hay muchos libros excelentes sobre el tema
para eso, incluido Mastering Mesos de Dipa Dubhashi y Akhil Das (O'Reilly, 2016).
El objetivo aquí es mencionar algunas de las consideraciones que deberá tener en cuenta al ejecutar Spark
Applications en Mesos.

Por ejemplo, una cosa común que escuchará sobre Spark en Mesos es el modo de grano fino versus el de grano
grueso. Históricamente, Mesos admitía una variedad de modos diferentes (granularidad fina y granularidad gruesa),
pero en este punto, solo admite la programación de granularidad gruesa (la granularidad fina ha quedado obsoleta).
El modo de granularidad gruesa significa que cada ejecutor de Spark se ejecuta como una sola tarea de Mesos.
Los ejecutores de Spark se dimensionan de acuerdo con las siguientes propiedades de la aplicación:

chispa.executor.memoria

chispa.ejecutor.núcleos

chispa.núcleos.max/chispa.executor.núcleos

Envío de solicitudes

Enviar aplicaciones a un clúster de Mesos es similar a hacerlo para el otro clúster de Spark
Machine Translated by Google

gerentes En su mayor parte, debe favorecer el modo de clúster cuando use Mesos. El modo de cliente
requiere una configuración adicional de su parte, especialmente con respecto a la distribución de recursos en el
clúster.

Por ejemplo, en modo cliente, el controlador necesita información de configuración adicional en spark­env.sh para
trabajar con Mesos.

En spark­env.sh establece algunas variables de entorno:

export MESOS_NATIVE_JAVA_LIBRARY=<ruta a libmesos.so>

Esta ruta suele ser <prefix>/lib/libmesos.so , donde el prefijo es /usr/local de forma predeterminada. En Mac OS X,
la biblioteca se llama libmesos.dylib en lugar de libmesos.so:

export SPARK_EXECUTOR_URI=<URL de spark­2.2.0.tar.gz cargado arriba>

Por último, establezca la propiedad Spark Application spark.executor.uri en <URL of spark 2.2.0.tar.gz>.
Ahora, al iniciar una aplicación Spark en el clúster, pase una URL mesos:// como principal al crear un
SparkContex y establezca esa propiedad como un parámetro en su variable SparkConf o la inicialización de una
SparkSession:

// en Scala
import org.apache.spark.sql.SparkSession val
spark =
SparkSession.builder .master("mesos://

HOST:5050") .appName("mi aplicación") .config("spark.executor.uri ", "<ruta a


spark­2.2.0.tar.gz cargada arriba>") .getOrCreate()

El envío de solicitudes en modo clúster es bastante sencillo y sigue la misma estructura de envío de Spark
que leíste antes. Cubrimos esto en "Lanzamiento de aplicaciones".

Configuración de mesos

Al igual que cualquier otro administrador de clústeres, hay varias formas en que podemos configurar nuestras
aplicaciones Spark cuando se ejecutan en Mesos. Debido a limitaciones de espacio, no podemos incluir aquí
todo el conjunto de configuración. Consulte la tabla correspondiente sobre Configuraciones de Mesos en la
documentación de Spark.

Configuraciones de implementación segura


Spark también brinda cierta capacidad de bajo nivel para hacer que sus aplicaciones se ejecuten de
manera más segura, especialmente en entornos que no son de confianza. Tenga en cuenta que la mayor parte de
esta configuración ocurrirá fuera de Spark. Estas configuraciones se basan principalmente en la red para
ayudar a Spark a ejecutarse de manera más segura. Esto significa autenticación, cifrado de red y
configuración de TLS y SSL. Debido a limitaciones de espacio, no podemos incluir aquí todo el conjunto de configuración.
Machine Translated by Google

Consulte la tabla correspondiente sobre Configuraciones de seguridad en la documentación de Spark.

Configuraciones de redes de clúster


Así como las mezclas son importantes, puede haber algunas cosas que vale la pena ajustar en la red. Esto también
puede ser útil al realizar configuraciones de implementación personalizadas para sus clústeres de Spark cuando necesita
usar proxies entre ciertos nodos. Si está buscando aumentar el rendimiento de Spark, estas no deberían
ser las primeras configuraciones que ajuste, pero pueden surgir en escenarios de implementación personalizados.
Debido a limitaciones de espacio, no podemos incluir aquí todo el conjunto de configuración. Consulte
la tabla correspondiente sobre Configuraciones de red en la documentación de Spark.

Programación de aplicaciones
Spark tiene varias instalaciones para programar recursos entre cálculos. Primero, recuerde que, como se describió
anteriormente en el libro, cada aplicación Spark ejecuta un conjunto independiente de procesos ejecutores.
Los administradores de clústeres brindan las instalaciones para la programación en todas las aplicaciones de Spark.
En segundo lugar, dentro de cada aplicación de Spark, varios trabajos (es decir, acciones de Spark) pueden
ejecutarse simultáneamente si fueron enviados por diferentes subprocesos. Esto es común si su aplicación atiende
solicitudes a través de la red. Spark incluye un programador justo para programar recursos dentro de cada aplicación.
Hemos introducido este tema en el capítulo anterior.

Si varios usuarios necesitan compartir su clúster y ejecutar diferentes aplicaciones Spark, existen diferentes
opciones para administrar la asignación, según el administrador del clúster. La opción más sencilla, disponible en
todos los administradores de clústeres, es la partición estática de recursos. Con este enfoque, a cada aplicación
se le otorga una cantidad máxima de recursos que puede usar y conserva esos recursos durante toda la
duración. En spark­submit hay una serie de propiedades que puede configurar para controlar la asignación de recursos
de una aplicación en particular. Consulte el Capítulo 16 para obtener más información. Además, la asignación
dinámica (que se describe a continuación) se puede activar para permitir que las aplicaciones aumenten o disminuyan
dinámicamente en función de su número actual de tareas pendientes. Si, en cambio, desea que los usuarios puedan
compartir la memoria y los recursos del ejecutor de manera detallada, puede iniciar una sola aplicación Spark y usar la
programación de subprocesos dentro de ella para atender varias solicitudes en paralelo.

Asignación dinámica

Si desea ejecutar varias aplicaciones Spark en el mismo clúster, Spark proporciona un mecanismo para ajustar
dinámicamente los recursos que ocupa su aplicación en función de la carga de trabajo.
Esto significa que su aplicación puede devolver recursos al clúster si ya no se usan y volver a solicitarlos más tarde
cuando haya demanda. Esta característica es especialmente útil si varias aplicaciones comparten recursos en su
clúster de Spark.

Esta función está deshabilitada de forma predeterminada y está disponible en todos los administradores de clústeres
de granularidad gruesa; es decir, modo independiente, modo YARN y modo de grano grueso Mesos. Hay dos requisitos
para usar esta característica. Primero, su aplicación debe configurar spark.dynamicAllocation.enabled para
Machine Translated by Google

verdadero. En segundo lugar, debe configurar un servicio aleatorio externo en cada nodo trabajador en el mismo
clúster y establecer spark.shuffle.service.enabled en verdadero en su aplicación. El propósito del servicio de reproducción
aleatoria externa es permitir que los ejecutores se eliminen sin eliminar los archivos de reproducción aleatoria escritos
por ellos. Esto se configura de manera diferente para cada administrador de clústeres y se describe en la configuración
de programación de trabajos. Debido a limitaciones de espacio, no podemos incluir el conjunto de configuración para la
asignación dinámica. Consulte la tabla correspondiente sobre Configuraciones de asignación dinámica.

Consideraciones misceláneas
Hay varios otros temas a considerar al implementar aplicaciones Spark que pueden afectar su elección de administrador
de clústeres y su configuración. Estas son solo cosas en las que debe pensar al comparar diferentes opciones de
implementación.

Una de las consideraciones más importantes es la cantidad y el tipo de aplicaciones que pretende ejecutar. Por ejemplo,
YARN es excelente para aplicaciones basadas en HDFS, pero no se usa comúnmente para mucho más. Además, no está
bien diseñado para admitir la nube, porque espera que la información esté disponible en HDFS. Además, el
cómputo y el almacenamiento están acoplados en gran medida, lo que significa que escalar su clúster implica escalar
tanto el almacenamiento como el cómputo en lugar de solo uno u otro. Mesos mejora esto un poco conceptualmente y es
compatible con una amplia gama de tipos de aplicaciones, pero aún requiere máquinas de aprovisionamiento previo
y, en cierto sentido, requiere una aceptación a una escala mucho mayor. Por ejemplo, realmente no tiene sentido tener
un clúster Mesos solo para ejecutar aplicaciones Spark. El modo independiente de Spark es el administrador de clústeres
más liviano y es relativamente simple de entender y aprovechar, pero luego creará más infraestructura de
administración de aplicaciones que podría obtener mucho más fácilmente con YARN o Mesos.

Otro desafío es administrar diferentes versiones de Spark. Sus manos están en gran parte atadas si desea intentar ejecutar
una variedad de aplicaciones diferentes que ejecutan diferentes versiones de Spark y, a menos que use un servicio bien
administrado, necesitará pasar una buena cantidad de tiempo administrando diferentes scripts de configuración para
diferentes servicios de Spark o eliminando la posibilidad de que sus usuarios usen una variedad de diferentes aplicaciones
de Spark.

Independientemente del administrador de clústeres que elija, querrá considerar cómo va a configurar el registro, almacenar
registros para referencia futura y permitir que los usuarios finales depuren sus aplicaciones. Estos son más "listos
para usar" para YARN o Mesos y es posible que necesiten algunos ajustes si los usa de forma independiente.

Una cosa que quizás desee considerar, o que podría influir en su toma de decisiones, es mantener un metaalmacén
para mantener los metadatos sobre sus conjuntos de datos almacenados, como un catálogo de tablas. Vimos cómo surge
esto en Spark SQL cuando creamos y mantenemos tablas.
Mantener un metaalmacén de Apache Hive, un tema que está más allá del alcance de este libro, podría ser algo
que valga la pena hacer para facilitar una referencia entre aplicaciones más productiva a los mismos conjuntos de datos.

Dependiendo de su carga de trabajo, podría valer la pena considerar usar la reproducción aleatoria externa de Spark
Machine Translated by Google

servicio. Por lo general, Spark almacena bloques aleatorios (salida aleatoria) en un disco local en ese nodo en particular.
Un servicio de reproducción aleatoria externo permite almacenar esos bloques de reproducción aleatoria para que estén
disponibles para todos los ejecutores, lo que significa que puede matar ejecutores arbitrariamente y aún tener sus salidas
aleatorias disponibles para otras aplicaciones.

Finalmente, deberá configurar al menos alguna solución de monitoreo básica y ayudar a los usuarios a depurar sus trabajos
de Spark que se ejecutan en sus clústeres. Esto va a variar según las opciones de administración de clústeres y mencionamos
algunas de las cosas que quizás desee configurar en el Capítulo 18.

Conclusión
Este capítulo analizó el mundo de las opciones de configuración que tiene al elegir cómo implementar Spark. Aunque la
mayor parte de la información es irrelevante para la mayoría de los usuarios, vale la pena mencionarla si está realizando
casos de uso más avanzados. Puede parecer una falacia, pero hay otras configuraciones que hemos omitido que controlan
incluso el comportamiento de nivel inferior. Puede encontrarlos en la documentación de Spark o en el código fuente de
Spark. El Capítulo 18 habla sobre algunas de las opciones que tenemos al monitorear aplicaciones Spark.
Machine Translated by Google

Capítulo 18. Supervisión y depuración

Este capítulo cubre los detalles clave que necesita para monitorear y depurar sus aplicaciones Spark. Para hacer esto,
recorreremos la interfaz de usuario de Spark con una consulta de ejemplo diseñada para ayudarlo a comprender
cómo rastrear sus propios trabajos a lo largo del ciclo de vida de ejecución. El ejemplo que veremos también lo ayudará a
comprender cómo depurar sus trabajos y dónde es probable que ocurran errores.

El panorama de monitoreo
En algún momento, deberá monitorear sus trabajos de Spark para comprender dónde se producen problemas en ellos. Vale
la pena revisar las diferentes cosas que realmente podemos monitorear y describir algunas de las opciones para hacerlo.
Revisemos los componentes que podemos monitorear (vea la Figura 18­1).

Aplicaciones y trabajos de Spark

Lo primero que querrá comenzar a monitorear al depurar o simplemente comprender mejor cómo se ejecuta su
aplicación en el clúster es la interfaz de usuario de Spark y los registros de Spark.
Estos informan información sobre las aplicaciones que se ejecutan actualmente a nivel de conceptos en Spark, como
RDD y planes de consulta. Hablamos en detalle sobre cómo usar estas herramientas de monitoreo de Spark
a lo largo de este capítulo.

JVM

Spark ejecuta los ejecutores en máquinas virtuales Java (JVM) individuales. Por lo tanto, el siguiente nivel de
detalle sería monitorear las máquinas virtuales (VM) individuales para comprender mejor cómo se ejecuta
su código. Las utilidades de JVM, como jstack para proporcionar seguimientos de pila, jmap para crear volcados de
pila, jstat para generar informes de estadísticas de series temporales y jconsole para explorar visualmente varias
propiedades de JVM, son útiles para aquellos que se sienten cómodos con las funciones internas de JVM.
También puede usar una herramienta como jvisualvm para ayudar a perfilar los trabajos de Spark. Parte de esta
información se proporciona en la interfaz de usuario de Spark, pero para la depuración de muy bajo
nivel, las herramientas antes mencionadas pueden ser útiles.

SO/Máquina

Las JVM se ejecutan en un sistema operativo host (SO) y es importante monitorear el estado de esas máquinas
para asegurarse de que estén en buen estado. Esto incluye monitorear cosas como CPU, red y E/S. Estos a
menudo se informan en soluciones de monitoreo a nivel de clúster; sin embargo, existen herramientas más
específicas que puede usar, incluidas dstat, iostat e iotop.

Grupo

Naturalmente, puede monitorear el clúster en el que se ejecutarán sus aplicaciones Spark. Esto podría ser un
YARN, Mesos o un clúster independiente. Por lo general, es importante tener algún tipo de solución de monitoreo aquí
porque, obviamente, si su clúster no funciona, usted
Machine Translated by Google

probablemente debería saber bastante rápido. Algunas herramientas populares de monitoreo a nivel de clúster
incluyen Ganglia y Prometheus.

Figura 18­1. Componentes de una aplicación Spark que puede monitorear

Qué monitorear
Después de ese breve recorrido por el panorama del monitoreo, analicemos cómo podemos monitorear y depurar
nuestras aplicaciones Spark. Hay dos cosas principales que querrá monitorear: los procesos que ejecutan su aplicación
(a nivel de uso de CPU, uso de memoria, etc.) y la ejecución de consultas dentro de ella (por ejemplo, trabajos y
tareas).

Procesos de controlador y ejecutor


Cuando esté monitoreando una aplicación Spark, definitivamente querrá vigilar al controlador. Aquí es donde vive
todo el estado de su aplicación, y deberá asegurarse de que esté
Machine Translated by Google

funcionando de manera estable. Si pudiera monitorear solo una máquina o una sola JVM, definitivamente sería el controlador.
Dicho esto, comprender el estado de los ejecutores también es extremadamente importante para monitorear trabajos
individuales de Spark. Para ayudar con este desafío, Spark tiene un sistema de métricas configurable basado en la biblioteca de
métricas de Dropwizard. El sistema de métricas se configura a través de un archivo de configuración que Spark espera que esté
presente en $SPARK_HOME/conf/metrics.properties. Se puede especificar una ubicación de
archivo personalizada cambiando la propiedad de configuración spark.metrics.conf. Estas métricas se pueden enviar a una

variedad de receptores diferentes, incluidas las soluciones de monitoreo de clústeres como Ganglia.

Consultas, Trabajos, Etapas y Tareas


Aunque es importante monitorear los procesos del controlador y del ejecutor, a veces es necesario depurar lo que sucede en
el nivel de una consulta específica. Spark brinda la capacidad de sumergirse en consultas, trabajos, etapas y tareas.
(Aprendimos sobre esto en el Capítulo 15). Esta información le permite saber exactamente qué se está ejecutando en el clúster en
un momento dado. Cuando busque ajustes de rendimiento o depuración, aquí es donde es más probable que
comience.

Ahora que sabemos lo que queremos monitorear, veamos las dos formas más comunes de hacerlo: los registros de Spark y la
interfaz de usuario de Spark.

Registros de chispa

Una de las formas más detalladas de monitorear Spark es a través de sus archivos de registro. Naturalmente, los eventos extraños
en los registros de Spark, o en el registro que agregó a su aplicación Spark, pueden ayudarlo a tomar nota de dónde fallan
exactamente los trabajos o qué está causando esa falla. Si usa la plantilla de la aplicación provista con el libro, el marco de
registro que configuramos en la plantilla permitirá que los registros de su aplicación aparezcan junto con los propios registros
de Spark, lo que los hace muy fáciles de correlacionar.
Sin embargo, un desafío es que Python no podrá integrarse directamente con la biblioteca de registro basada en Java de

Spark. Sin embargo, el uso del módulo de registro de Python o incluso declaraciones de impresión simples aún imprimirá los
resultados con un error estándar y los hará fáciles de encontrar.

Para cambiar el nivel de registro de Spark, simplemente ejecute el siguiente comando:

chispa.sparkContext.setLogLevel("INFO")

Esto le permitirá leer los registros y, si usa nuestra plantilla de aplicación, puede registrar su propia información relevante junto
con estos registros, lo que le permitirá inspeccionar tanto su propia aplicación como Spark. Los registros mismos se
imprimirán con un error estándar cuando se ejecute una aplicación en modo local, o el administrador del clúster los guardará en
archivos cuando ejecute Spark en un clúster.
Consulte la documentación de cada administrador de clústeres sobre cómo encontrarlos; por lo general, están disponibles a
través de la interfaz de usuario web del administrador de clústeres.

No siempre encontrará la respuesta que necesita simplemente buscando registros, pero puede ayudarlo a identificar el
problema dado que está encontrando y posiblemente agregar nuevas declaraciones de registro en su
Machine Translated by Google

aplicación para entenderlo mejor. También es conveniente recopilar registros a lo largo del tiempo para poder consultarlos en el futuro.

Por ejemplo, si su aplicación falla, querrá depurar el motivo, sin acceso a la aplicación ahora bloqueada. También es posible que desee enviar

los registros de la máquina en la que se escribieron para conservarlos si una máquina falla o se apaga (por ejemplo, si se ejecuta en la nube).

La interfaz de usuario de Spark

La interfaz de usuario de Spark proporciona una forma visual de monitorear las aplicaciones mientras se ejecutan, así como métricas sobre

su carga de trabajo de Spark, a nivel de Spark y JVM. Cada SparkContext que se ejecuta inicia una interfaz de usuario web, de forma

predeterminada en el puerto 4040, que muestra información útil sobre la aplicación. Cuando ejecuta Spark en modo local, por

ejemplo, simplemente navegue a http://localhost:4040 para ver la interfaz de usuario cuando ejecute una aplicación

Spark en su máquina local. Si está ejecutando varias aplicaciones, lanzarán interfaces de usuario web al aumentar los números de puerto

(4041, 4042, …). Los administradores de clúster también se vincularán a la interfaz de usuario web de cada aplicación desde su propia

interfaz de usuario.

La figura 18­2 muestra todas las pestañas disponibles en la interfaz de usuario de Spark.

Figura 18­2. Pestañas de la IU de Spark

Estas pestañas son accesibles para cada una de las cosas que nos gustaría monitorear. En su mayor parte, cada uno de estos debe explicarse

por sí mismo:

La pestaña Trabajos hace referencia a los trabajos de Spark.

La pestaña Etapas pertenece a etapas individuales (y sus tareas relevantes).

La pestaña Almacenamiento incluye información y los datos que se almacenan actualmente en caché en nuestra aplicación Spark.

La pestaña Entorno contiene información relevante sobre las configuraciones y ajustes actuales de la aplicación Spark.

La pestaña SQL hace referencia a nuestras consultas API estructuradas (incluidos SQL y DataFrames).

La pestaña Ejecutores proporciona información detallada sobre cada ejecutor que ejecuta nuestra aplicación.

Veamos un ejemplo de cómo puede profundizar en una consulta determinada. Abra un nuevo shell de Spark, ejecute el siguiente código y

rastrearemos su ejecución a través de la interfaz de usuario de Spark:

# en Python

spark.read\ .option("header", "true")\


Machine Translated by Google

.csv("/data/retail­data/all/online­retail­dataset.csv")

\ .repartition(2)\ .selectExpr("instr(Description, 'GLASS') >= 1 as is_glass")


\ . groupBy("es_vidrio")
\ .count()
\ .collect()

Esto da como resultado tres filas de varios valores. El código inicia una consulta SQL, así que
vayamos a la pestaña SQL, donde debería ver algo similar a la Figura 18­3.
Machine Translated by Google

Figura 18­3. La pestaña SQL

Lo primero que ve son estadísticas agregadas sobre esta consulta:

Hora de envío: 2017/04/08 16:24:41


Duración: 2 s
Trabajos exitosos: 2
Machine Translated by Google

Estos se volverán importantes en un minuto, pero primero echemos un vistazo al gráfico acíclico dirigido (DAG) de las
etapas de Spark. Cada cuadro azul en estas pestañas representa una etapa de las tareas de Spark. Todo el grupo de
estas etapas representa nuestro trabajo Spark. Echemos un vistazo a cada etapa en detalle para que podamos
comprender mejor lo que sucede en cada nivel, comenzando con la Figura 18­4.

Figura 18­4. La etapa uno

El cuadro en la parte superior, etiquetado como WholeStateCodegen, representa un escaneo completo del archivo
CSV. El cuadro a continuación representa una mezcla que forzamos cuando llamamos a la partición. Esto convirtió
nuestro conjunto de datos original (de un número de particiones aún por especificar) en dos particiones.

El siguiente paso es nuestra proyección (seleccionar/agregar/filtrar columnas) y la agregación. Observe que en la Figura
18­5 el número de filas de salida es seis. Esto se alinea convenientemente con el número de filas de salida multiplicado
por el número de particiones en el momento de la agregación. Esto se debe a que Spark realiza una agregación para cada
partición (en este caso, una agregación basada en hash) antes de mezclar los datos en preparación para la
etapa final.
Machine Translated by Google

Figura 18­5. etapa dos

La última etapa es la agregación de las subagregaciones que vimos ocurrir por partición en la
etapa anterior. Combinamos esas dos particiones en las últimas tres filas que son el resultado
de nuestra consulta total (Figura 18­6).
Machine Translated by Google

Figura 18­6. etapa tres

Veamos más a fondo la ejecución del trabajo. En la pestaña Trabajos, junto a Trabajos exitosos, haga clic en 2.
Como muestra la Figura 18­7 , nuestro trabajo se divide en tres etapas (lo que corresponde a lo que vimos en la
pestaña SQL).

Figura 18­7. La pestaña Trabajos

Estas etapas tienen más o menos la misma información que la que se muestra en la Figura 18­6, pero al
hacer clic en la etiqueta de una de ellas, se mostrarán los detalles de una etapa determinada. En este ejemplo,
se ejecutaron tres etapas, con ocho, dos y luego doscientas tareas cada una. Antes de profundizar en los detalles
del escenario, repasemos por qué es así.

La primera etapa tiene ocho tareas. Los archivos CSV se pueden dividir y Spark dividió el trabajo para
distribuirlo de manera relativamente uniforme entre los diferentes núcleos de la máquina. Esto sucede a nivel
de clúster y apunta a una optimización importante: cómo almacena sus archivos. La siguiente
Machine Translated by Google

stage tiene dos tareas porque llamamos explícitamente a una partición para mover los datos a dos particiones.
La última etapa tiene 200 tareas porque el valor predeterminado de las particiones aleatorias es 200.

Ahora que revisamos cómo llegamos aquí, haga clic en el escenario con ocho tareas para ver el siguiente nivel de detalle, como se
muestra en la Figura 18­8.

Figura 18­8. Tareas de chispa

Spark proporciona muchos detalles sobre lo que hizo este trabajo cuando se ejecutó. Hacia la parte superior, observe la sección
Resumen de métricas. Esto proporciona una sinopsis de estadísticas con respecto a varias métricas. Lo que debe buscar son las
distribuciones desiguales de los valores (hablamos de esto en el Capítulo 19). En este caso, todo parece muy consistente; no hay
amplias oscilaciones en la distribución de valores. En la tabla en la parte inferior, también podemos examinar por ejecutor (uno
para cada núcleo en esta máquina en particular, en este caso). Esto puede ayudar a identificar si un ejecutor en particular está
luchando con su carga de trabajo.

Spark también pone a disposición un conjunto de métricas más detalladas, como se muestra en la Figura 18­8, que probablemente no
sean relevantes para la gran mayoría de los usuarios. Para verlos, haga clic en Mostrar métricas adicionales y luego elija
(Des)seleccionar todo o seleccione métricas individuales, según lo que desee ver.

Puede repetir este análisis básico para cada etapa que desee analizar. Eso lo dejamos como ejercicio para el lector.

Otras pestañas de la IU de Spark

Las pestañas restantes de Spark, Almacenamiento, Entorno y Ejecutores, se explican por sí mismas. La pestaña Almacenamiento muestra
información sobre los RDD/DataFrames almacenados en caché en el clúster. Esto puede ayudarlo a ver si ciertos datos han sido
desalojados del caché con el tiempo. La pestaña Entorno le muestra información sobre el Entorno de tiempo de ejecución, incluida
información sobre Scala y Java como
Machine Translated by Google

así como las diversas propiedades de Spark que configuró en su clúster.

Configuración de la interfaz de usuario de Spark

Hay una serie de configuraciones que puede establecer con respecto a la interfaz de usuario de Spark. Muchos de ellos son
configuraciones de red, como habilitar el control de acceso. Otros le permiten configurar cómo se comportará la interfaz
de usuario de Spark (por ejemplo, cuántos trabajos, etapas y tareas se almacenan). Debido a limitaciones de
espacio, no podemos incluir aquí todo el conjunto de configuración. Consulte la tabla correspondiente sobre
configuraciones de interfaz de usuario de Spark en la documentación de Spark.

API REST de Spark


Además de la interfaz de usuario de Spark, también puede acceder al estado y las métricas de Spark a través de una API
REST. Está disponible en http://localhost:4040/api/v1 y es una forma de crear visualizaciones y herramientas de
monitoreo además de Spark. En su mayor parte, esta API expone la misma información que se presenta en la interfaz de
usuario web, excepto que no incluye ninguna información relacionada con SQL. Esta puede ser una herramienta útil si
desea crear su propia solución de informes basada en la información disponible en la interfaz de usuario de
Spark. Debido a limitaciones de espacio, no podemos incluir aquí la lista de puntos finales de la API. Consulte la tabla
correspondiente sobre los puntos finales de la API REST en la documentación de Spark.

Servidor de historial de interfaz de usuario de Spark

Normalmente, la interfaz de usuario de Spark solo está disponible mientras se ejecuta SparkContext, entonces, ¿cómo
puede acceder a ella después de que su aplicación falla o finaliza? Para hacer esto, Spark incluye una herramienta
llamada Spark History Server que le permite reconstruir la interfaz de usuario de Spark y la API REST, siempre que la
aplicación se haya configurado para guardar un registro de eventos. Puede encontrar información actualizada sobre cómo
usar esta herramienta en la documentación de Spark.

Para usar el servidor de historial, primero debe configurar su aplicación para almacenar registros de eventos en una
ubicación determinada. Puede hacerlo habilitando spark.eventLog.enabled y la ubicación del registro de eventos con la
configuración spark.eventLog.dir. Luego, una vez que haya almacenado los eventos, puede ejecutar el servidor de
historial como una aplicación independiente y reconstruirá automáticamente la interfaz de usuario web en función de estos
registros. Algunos administradores de clústeres y servicios en la nube también configuran el registro automáticamente
y ejecutan un servidor de historial de forma predeterminada.

Hay una serie de otras configuraciones para el servidor de historial. Debido a limitaciones de espacio, no podemos incluir
aquí todo el conjunto de configuración. Consulte la tabla correspondiente sobre Configuraciones del servidor de
historial de Spark en la documentación de Spark.

Depuración y Primeros Auxilios Spark


Las secciones anteriores definieron algunos "signos vitales" básicos, es decir, cosas que podemos monitorear para
verificar el estado de una aplicación Spark. En el resto del capítulo, adoptaremos un enfoque de "primeros auxilios" para
la depuración de Spark: revisaremos algunos signos y síntomas de problemas en sus trabajos de Spark, incluidos los signos
que podría observar (por ejemplo, tareas lentas) como así como los síntomas
Machine Translated by Google

desde el propio Spark (por ejemplo, OutOfMemoryError). Hay muchos problemas que pueden afectar los trabajos de Spark, por lo
que es imposible cubrir todo. Pero discutiremos algunos de los problemas de Spark más comunes que puede encontrar.
Además de los signos y síntomas, también veremos algunos posibles tratamientos para estos problemas.

La mayoría de las recomendaciones sobre la solución de problemas se refieren a las herramientas de configuración discutidas
en el Capítulo 16.

Los trabajos de Spark no se inician


Este problema puede surgir con frecuencia, especialmente cuando acaba de comenzar con una implementación o un
entorno nuevos.

Signos y síntomas

Los trabajos de Spark no se inician.

La interfaz de usuario de Spark no muestra ningún nodo en el clúster, excepto el controlador.

La interfaz de usuario de Spark parece estar informando información incorrecta.

Tratamientos potenciales

Esto ocurre principalmente cuando su clúster o las demandas de recursos de su aplicación no están configuradas correctamente.
Spark, en un entorno distribuido, hace algunas suposiciones sobre redes, sistemas de archivos y otros recursos. Durante el
proceso de configuración del clúster, probablemente configuró algo incorrectamente y ahora el nodo que ejecuta el controlador no
puede comunicarse con los ejecutores. Esto podría deberse a que no especificó qué IP y puerto están abiertos o no abrió el
correcto. Lo más probable es que se trate de un problema de nivel de clúster, máquina o configuración. Otra opción es que su
aplicación solicite más recursos por ejecutor de los que su administrador de clúster tiene actualmente libres, en cuyo caso
el controlador esperará eternamente a que se inicien los ejecutores.

Asegúrese de que las máquinas puedan comunicarse entre sí en los puertos que espera.
Idealmente, debería abrir todos los puertos entre los nodos trabajadores a menos que tenga restricciones de seguridad
más estrictas.

Asegúrese de que sus configuraciones de recursos de Spark sean correctas y que su administrador de clústeres esté
configurado correctamente para Spark. Primero intente ejecutar una aplicación simple para ver si funciona.
Un problema común puede ser que haya solicitado más memoria por ejecutor de la que el administrador de
clústeres tiene libre para asignar, así que verifique cuánto informa libre (en su interfaz de usuario) y su configuración

de memoria de envío de chispa.

Errores antes de la ejecución

Esto puede suceder cuando está desarrollando una nueva aplicación y anteriormente ejecutó código en este clúster, pero ahora algún
código nuevo no funcionará.
Machine Translated by Google

Signos y síntomas

Los comandos no se ejecutan en absoluto y generan grandes mensajes de error.

Verifica la interfaz de usuario de Spark y parece que no se ejecutan trabajos, etapas o tareas.

Tratamientos potenciales

Después de verificar y confirmar que la pestaña del entorno de Spark UI muestra la información correcta para su aplicación, vale la
pena verificar dos veces su código. Muchas veces, puede haber un error tipográfico simple o un nombre de columna incorrecto que
impida que el trabajo de Spark se compile en su plan de Spark subyacente (al usar la API de DataFrame).

Debería echar un vistazo al error devuelto por Spark para confirmar que no hay ningún problema en su código, como
proporcionar la ruta del archivo de entrada o el nombre del campo incorrectos.

Verifique dos veces para verificar que el clúster tenga la conectividad de red que espera entre su controlador, sus
trabajadores y el sistema de almacenamiento que está utilizando.

Es posible que haya problemas con las bibliotecas o las rutas de clase que provocan que se cargue la versión incorrecta
de una biblioteca para acceder al almacenamiento. Intente simplificar su aplicación hasta que obtenga una versión más
pequeña que reproduzca el problema (p. ej., solo lea un conjunto de datos).

Errores durante la ejecución


Este tipo de problema ocurre cuando ya está trabajando en un clúster o en partes de su ejecución de la aplicación Spark
antes de encontrar un error. Esto puede ser parte de un trabajo programado que se ejecuta en algún intervalo o parte de alguna
exploración interactiva que parece fallar después de un tiempo.

Signos y síntomas

Un trabajo de Spark se ejecuta correctamente en todo el clúster, pero el siguiente falla.

Un paso en una consulta de varios pasos falla.

Un trabajo programado que se ejecutó ayer está fallando hoy.

Mensaje de error difícil de analizar.

Tratamientos potenciales

Verifique si sus datos existen o están en el formato que espera. Esto puede cambiar con el tiempo o algún cambio
anterior puede haber tenido consecuencias no deseadas en su aplicación.

Si aparece un error rápidamente cuando ejecuta una consulta (es decir, antes de que se inicien las tareas), lo más
probable es que se trate de un error de análisis durante la planificación de la consulta. Esto significa que
probablemente escribió mal el nombre de una columna a la que se hace referencia en la consulta o que una columna,
vista o tabla a la que hizo referencia no existe.
Machine Translated by Google

Lea el seguimiento de la pila para tratar de encontrar pistas sobre qué componentes están involucrados
(por ejemplo, en qué operador y etapa se estaba ejecutando).

Intente aislar el problema verificando progresivamente los datos de entrada y asegurándose de que los
datos se ajusten a sus expectativas. También intente eliminar la lógica hasta que pueda aislar el problema
en una versión más pequeña de su aplicación.

Si un trabajo ejecuta tareas durante algún tiempo y luego falla, podría deberse a un problema con los
datos de entrada en sí, donde el esquema podría especificarse incorrectamente o una fila en particular no
se ajusta al esquema esperado. Por ejemplo, a veces su esquema puede especificar que los datos no
contienen valores nulos, pero sus datos en realidad contienen valores nulos, lo que puede causar que
ciertas transformaciones fallen.

También es posible que su propio código para procesar los datos se bloquee, en cuyo caso Spark le
mostrará la excepción lanzada por su código. En este caso, verá una tarea marcada como "fallida" en la
interfaz de usuario de Spark, y también puede ver los registros en esa máquina para comprender qué
estaba haciendo cuando falló. Intente agregar más registros dentro de su código para averiguar qué registro
de datos se estaba procesando.

Tareas lentas o rezagados


Este problema es bastante común cuando se optimizan aplicaciones, y puede ocurrir debido a que el trabajo no se
distribuye uniformemente entre sus máquinas ("sesgo") o debido a que una de sus máquinas es más lenta que las
otras (por ejemplo, debido a un problema de hardware) .

Signos y síntomas

Cualquiera de los siguientes son síntomas apropiados del problema:

Las etapas de chispa parecen ejecutarse hasta que solo quedan un puñado de tareas. Esas tareas
luego toman mucho tiempo.

Estas tareas lentas aparecen en la interfaz de usuario de Spark y se producen de manera constante en los mismos conjuntos de datos.

Estos ocurren en etapas, una tras otra.

Aumentar la cantidad de máquinas asignadas a la aplicación Spark realmente no ayuda: algunas tareas aún
toman mucho más tiempo que otras.

En las métricas de Spark, ciertos ejecutores leen y escriben muchos más datos que otros.

Tratamientos potenciales

Las tareas lentas a menudo se denominan "rezagadas". Hay muchas razones por las que pueden ocurrir, pero la
mayoría de las veces la fuente de este problema es que sus datos están particionados de manera desigual
en particiones DataFrame o RDD. Cuando esto sucede, es posible que algunos ejecutores deban trabajar en
cantidades de trabajo mucho mayores que otros. Un caso particularmente común es que utiliza una operación de grupo por clave y
Machine Translated by Google

una de las claves simplemente tiene más datos que otras. En este caso, cuando observa la interfaz de usuario de Spark, es posible
que vea que los datos aleatorios para algunos nodos son mucho más grandes que para otros.

Intente aumentar el número de particiones para tener menos datos por partición.

Intente volver a particionar por otra combinación de columnas. Por ejemplo, los rezagados pueden aparecer cuando
se divide por una columna de ID sesgada o una columna donde muchos valores son nulos. En el último caso, podría
tener sentido filtrar primero los valores nulos.

Intente aumentar la memoria asignada a sus ejecutores si es posible.

Supervise el ejecutor que tiene problemas y vea si es la misma máquina en todos los trabajos; también puede tener un
ejecutor o una máquina en mal estado en su clúster, por ejemplo, uno cuyo disco está casi lleno.

Si este problema está asociado con una unión o una agregación, consulte "Uniones lentas" o "Agregaciones
lentas".

Compruebe si sus funciones definidas por el usuario (UDF) son un desperdicio en su asignación de objetos o
lógica comercial. Intente convertirlos a código DataFrame si es posible.

Asegúrese de que sus UDF o funciones agregadas definidas por el usuario (UDAF) se ejecuten en un lote de datos lo
suficientemente pequeño. A menudo, una agregación puede extraer una gran cantidad de datos en la memoria
para una clave común, lo que hace que ese ejecutor tenga que hacer mucho más trabajo que otros.

Al activar la especulación, que analizamos en "Lecturas y escrituras lentas", Spark ejecutará una segunda copia de tareas
que son extremadamente lentas. Esto puede ser útil si el problema se debe a un nodo defectuoso porque la tarea se
ejecutará en uno más rápido. Sin embargo, la especulación tiene un costo, porque consume recursos adicionales.
Además, para algunos sistemas de almacenamiento que usan coherencia eventual, podría terminar con datos de salida

duplicados si sus escrituras no son idempotentes. (Discutimos las configuraciones de especulación en el Capítulo 17.)

Otro problema común puede surgir cuando trabaja con conjuntos de datos. Debido a que los conjuntos de datos realizan
una gran cantidad de instancias de objetos para convertir registros en objetos Java para UDF, pueden generar una
gran cantidad de recolección de elementos no utilizados. Si usa conjuntos de datos, observe las métricas de
recolección de elementos no utilizados en la interfaz de usuario de Spark para ver si son coherentes con las tareas lentas.

Los rezagados pueden ser uno de los problemas más difíciles de depurar, simplemente porque hay muchas causas posibles. Sin
embargo, con toda probabilidad, la causa será algún tipo de sesgo de datos, por lo que definitivamente comience verificando
la interfaz de usuario de Spark en busca de cantidades desequilibradas de datos en todas las tareas.

Agregaciones lentas
Si tiene una agregación lenta, comience por revisar los problemas en la sección "Tareas lentas" antes de continuar. Después de
haberlos probado, es posible que continúe viendo el mismo problema.
Machine Translated by Google

Signos y síntomas

Tareas lentas durante una llamada groupBy.

Los trabajos posteriores a la agregación también son lentos.

Tratamientos potenciales

Desafortunadamente, este problema no siempre se puede resolver. A veces, los datos en su trabajo solo tienen algunas claves
sesgadas, y la operación que desea ejecutar en ellas debe ser lenta.

Aumentar la cantidad de particiones, antes de una agregación, podría ayudar al reducir la cantidad de claves diferentes
procesadas en cada tarea.

El aumento de la memoria del ejecutor también puede ayudar a aliviar este problema. Si una sola clave tiene muchos
datos, esto permitirá que su ejecutor se derrame en el disco con menos frecuencia y termine más rápido, aunque aún
puede ser mucho más lento que los ejecutores que procesan otras claves.

Si encuentra que las tareas posteriores a la agregación también son lentas, esto significa que su conjunto de datos

podría haber permanecido desequilibrado después de la agregación. Intente insertar una llamada de partición para
particionarla aleatoriamente.

Asegurarse de que todos los filtros y las declaraciones SELECT que pueden estar por encima de la agregación puede
ayudar a garantizar que esté trabajando solo en los datos en los que necesita trabajar y nada más. El optimizador de
consultas de Spark hará esto automáticamente para las API estructuradas.

Asegúrese de que los valores nulos se representen correctamente (usando el concepto nulo de Spark) y no como un
valor predeterminado como " " o "VACÍO". Spark a menudo se optimiza para omitir valores nulos al principio del trabajo

cuando es posible, pero no puede hacerlo para sus propios valores de marcador de posición.

Algunas funciones de agregación también son inherentemente más lentas que otras. Por ejemplo, collect_list y

collect_set son funciones de agregación muy lentas porque deben devolver todos los objetos coincidentes al
controlador y deben evitarse en el código crítico para el rendimiento.

Uniones lentas
Las uniones y las agregaciones son aleatorias, por lo que comparten algunos de los mismos síntomas generales y tratamientos.

Signos y síntomas

Una etapa de unión parece estar tomando mucho tiempo. Esto puede ser una tarea o muchas tareas.

Las etapas antes y después de la unión parecen funcionar con normalidad.

Tratamientos potenciales

Muchas uniones se pueden optimizar (manual o automáticamente) para otros tipos de uniones. Nosotros
Machine Translated by Google

cubrió cómo seleccionar diferentes tipos de unión en el Capítulo 8.

Experimentar con diferentes órdenes de unión puede ayudar a acelerar los trabajos, especialmente si algunas de esas
uniones filtran una gran cantidad de datos; haz eso primero.

Particionar un conjunto de datos antes de unirlo puede ser muy útil para reducir el movimiento de datos en el clúster,
especialmente si el mismo conjunto de datos se utilizará en varias operaciones de unión.
Vale la pena experimentar con diferentes particiones previas a la unión. Tenga en cuenta, nuevamente, que esto no
es "gratis" y tiene el costo de una mezcla.

Las uniones lentas también pueden ser causadas por datos sesgados. No siempre hay mucho que pueda hacer aquí,
pero puede ayudar dimensionar la aplicación Spark y/o aumentar el tamaño de los ejecutores, como se describe en
secciones anteriores.

Asegurarse de que todos los filtros y las declaraciones de selección que pueden estar por encima de la combinación
puede ayudar a garantizar que esté trabajando solo en los datos que necesita para la combinación.

Asegúrese de que los valores nulos se manejen correctamente (que está usando nulos) y no un valor predeterminado
como " " o "VACÍO", como con las agregaciones.

A veces, Spark no puede planificar correctamente una unión de transmisión si no conoce ninguna estadística
sobre el DataFrame o la tabla de entrada. Si sabe que una de las tablas a las que se está uniendo es pequeña, puede
intentar forzar una transmisión (como se explica en el Capítulo 8), o usar los comandos de recopilación de estadísticas
de Spark para permitirle analizar la tabla.

Lecturas y escrituras lentas


La E/S lenta puede ser difícil de diagnosticar, especialmente con sistemas de archivos en red.

Signos y síntomas

Lectura lenta de datos de un sistema de archivos distribuido o un sistema externo.

Escrituras lentas desde sistemas de archivos de red o almacenamiento de blobs.

Tratamientos potenciales

Activar la especulación (establecer spark.speculation en verdadero) puede ayudar con lecturas y escrituras lentas. Esto
lanzará tareas adicionales con la misma operación en un intento de ver si es solo un problema transitorio en la
primera tarea. La especulación es una herramienta poderosa y funciona bien con sistemas de archivos consistentes. Sin
embargo, puede causar escrituras de datos duplicados con algunos servicios en la nube eventualmente consistentes,
como Amazon S3, así que verifique si es compatible con el conector del sistema de almacenamiento que está utilizando.

Garantizar una conectividad de red suficiente puede ser importante: es posible que su clúster de Spark simplemente
no tenga suficiente ancho de banda de red total para llegar a su sistema de almacenamiento.

Para sistemas de archivos distribuidos como HDFS que se ejecutan en los mismos nodos que Spark, haga
Machine Translated by Google

Asegúrese de que Spark vea los mismos nombres de host para los nodos que el sistema de archivos. Esto permitirá que
Spark realice una programación consciente de la localidad, que podrá ver en la columna "localidad" en la interfaz de
usuario de Spark. Hablaremos un poco más sobre la localidad en el próximo capítulo.

Driver OutOfMemoryError o el controlador no responde


Este suele ser un problema bastante serio porque bloqueará su aplicación Spark. A menudo sucede debido a que se recopilan
demasiados datos en el controlador, lo que hace que se quede sin memoria.

Signos y síntomas

La aplicación Spark no responde o se bloqueó.

OutOfMemoryErrors o mensajes de recolección de elementos no utilizados en los registros del controlador.

Los comandos tardan mucho tiempo en ejecutarse o no se ejecutan en absoluto.

La interactividad es muy baja o inexistente.

El uso de memoria es alto para el controlador JVM.

Tratamientos potenciales

Hay una variedad de razones potenciales para que esto suceda, y el diagnóstico no siempre es sencillo.

Es posible que su código haya intentado recopilar un conjunto de datos demasiado grande para el nodo del controlador

mediante operaciones como recopilar.

Es posible que esté utilizando una unión de transmisión en la que los datos que se transmitirán son demasiado
grandes. Use la configuración de unión de transmisión máxima de Spark para controlar mejor el tamaño que
transmitirá.

Una aplicación de ejecución prolongada generó una gran cantidad de objetos en el controlador y no puede liberarlos.
La herramienta jmap de Java puede ser útil para ver qué objetos están llenando la mayor parte de la memoria de su
controlador JVM al imprimir un histograma del montón. Sin embargo, tenga en cuenta que jmap pausará esa JVM
mientras se ejecuta.

Aumente la asignación de memoria del controlador si es posible para que funcione con más datos.

Pueden ocurrir problemas con las JVM que se quedan sin memoria si está utilizando otro enlace de idioma, como Python,
debido a que la conversión de datos entre los dos requiere demasiada memoria en la JVM. Intente ver si su problema
es específico de su idioma elegido y devuelva menos datos al nodo del controlador, o escríbalos en un archivo en lugar
de traerlos de vuelta como objetos en memoria.

Si está compartiendo un SparkContext con otros usuarios (p. ej., a través del servidor SQL JDBC y algunos entornos de
notebook), asegúrese de que las personas no estén tratando de hacer algo que pueda causar una gran cantidad de
asignación de memoria en el controlador (como trabajar demasiado).
Machine Translated by Google

grandes arreglos en su código o recolectando grandes conjuntos de datos).

Executor OutOfMemoryError o Executor no responde


Las aplicaciones de Spark a veces pueden recuperarse de esto automáticamente, según el verdadero problema
subyacente.

Signos y síntomas

OutOfMemoryErrors o mensajes de recolección de basura en los registros del ejecutor. Puede encontrarlos en la
interfaz de usuario de Spark.

Ejecutores que fallan o dejan de responder.

Tareas lentas en ciertos nodos que nunca parecen recuperarse.

Tratamientos potenciales

Intente aumentar la memoria disponible para los ejecutores y el número de ejecutores.

Intente aumentar el tamaño del trabajador PySpark a través de las configuraciones relevantes de Python.

Busque mensajes de error de recolección de elementos no utilizados en los registros del ejecutor. Algunas de las
tareas que se ejecutan, especialmente si usa UDF, pueden crear muchos objetos que deben recolectarse como
elementos no utilizados. Reparticione sus datos para aumentar el paralelismo, reducir la cantidad de registros por
tarea y asegurarse de que todos los ejecutores obtengan la misma cantidad de trabajo.

Asegúrese de que los valores nulos se manejen correctamente (que está usando nulos) y no un valor
predeterminado como " " o "VACÍO", como discutimos anteriormente.

Esto es más probable que suceda con RDD o con conjuntos de datos debido a las instancias de
objetos. Intente usar menos UDF y más operaciones estructuradas de Spark cuando sea posible.

Utilice las herramientas de supervisión de Java, como jmap, para obtener un histograma del uso de la memoria
en montón en sus ejecutores y ver qué clases ocupan más espacio.

Si los ejecutores se colocan en nodos que también tienen otras cargas de trabajo en ejecución, como un almacén
de clave­valor, intente aislar sus trabajos de Spark de otros trabajos.

Nulos inesperados en los resultados

Signos y síntomas

Valores nulos inesperados después de las transformaciones.

Los trabajos de producción programados que solían funcionar ya no funcionan o ya no producen los resultados
correctos.
Machine Translated by Google

Tratamientos potenciales

Es posible que su formato de datos haya cambiado sin ajustar su lógica comercial.
Esto significa que el código que funcionó antes ya no es válido.

Utilice un acumulador para intentar contar registros o ciertos tipos, así como errores de análisis o
procesamiento en los que se salta un registro. Esto puede ser útil porque podría pensar que está analizando datos
de cierto formato, pero algunos de los datos no lo hacen. La mayoría de las veces, los usuarios colocan el
acumulador en una UDF cuando están analizando sus datos sin procesar en un formato más controlado y
realizan los conteos allí. Esto le permite contar registros válidos e inválidos y luego operar en consecuencia después
del hecho.

Asegúrese de que sus transformaciones realmente den como resultado planes de consulta válidos.
Spark SQL a veces realiza coerciones de tipos implícitas que pueden generar resultados confusos. Por ejemplo,
la expresión SQL SELECT 5*"23" da como resultado 115 porque la cadena "25" se convierte en el valor 25 como
un número entero, pero la expresión SELECT 5 * " " da como resultado nulo porque convierte la cadena
vacía en un número entero. da nulo. Asegúrese de que sus conjuntos de datos intermedios tengan el
esquema que espera que tengan (intente usar printSchema en ellos) y busque cualquier operación CAST en el
plan de consulta final.

No queda espacio en errores de disco

Signos y síntomas

Ve errores de "no queda espacio en el disco" y sus trabajos fallan.

Tratamientos potenciales

La forma más fácil de aliviar esto, por supuesto, es agregar más espacio en disco. Puede hacerlo dimensionando
los nodos en los que está trabajando o adjuntando almacenamiento externo en un entorno de nube.

Si tiene un clúster con espacio de almacenamiento limitado, es posible que algunos nodos se agoten primero debido
al sesgo. Volver a particionar los datos como se describió anteriormente puede ayudar aquí.

También hay una serie de configuraciones de almacenamiento con las que puede experimentar.
Algunos de estos determinan cuánto tiempo deben mantenerse los registros en la máquina antes de
eliminarlos. Para obtener más información, consulte las configuraciones continuas de registros del ejecutor de
Spark en el Capítulo 16.

Intente eliminar manualmente algunos archivos de registro antiguos o archivos aleatorios antiguos de las
máquinas en cuestión. Esto puede ayudar a aliviar parte del problema, aunque obviamente no es una
solución permanente.

Errores de serialización
Machine Translated by Google

Signos y síntomas

Ve errores de serialización y sus trabajos fallan.

Tratamientos potenciales

Esto es muy poco común cuando se trabaja con las API estructuradas, pero es posible que esté
intentando realizar alguna lógica personalizada en ejecutores con UDF o RDD y la tarea que está
tratando de serializar en estos ejecutores o los datos que está tratando de compartir no pueden ser
serializado. Esto sucede a menudo cuando trabaja con algún código o datos que no se pueden serializar
en una UDF o función, o si trabaja con tipos de datos extraños que no se pueden serializar. Si está usando
(o tiene la intención de usar la serialización Kryo), verifique que realmente esté registrando sus
clases para que estén realmente serializadas.

Intente no hacer referencia a ningún campo del objeto adjunto en sus UDF al crear UDF dentro de una
clase Java o Scala. Esto puede hacer que Spark intente serializar todo el objeto adjunto, lo que puede no
ser posible. En su lugar, copie los campos relevantes a las variables locales en el mismo ámbito que el
cierre y utilícelos.

Conclusión
Este capítulo cubrió algunas de las herramientas principales que puede usar para monitorear y depurar sus trabajos
y aplicaciones de Spark, así como los problemas más comunes que vemos y sus soluciones. Al igual que con la
depuración de cualquier software complejo, recomendamos adoptar un enfoque basado en principios paso a paso
para depurar problemas. Agregue declaraciones de registro para averiguar dónde falla su trabajo y qué tipo de datos
llegan a cada etapa, intente aislar el problema en el código más pequeño posible y trabaje a partir de ahí. Para
problemas de sesgo de datos, que son exclusivos de la computación paralela, use la interfaz de usuario de Spark
para obtener una descripción general rápida de cuánto trabajo está haciendo cada tarea. En el Capítulo 19,
analizamos el ajuste del rendimiento en particular y varias herramientas que puede utilizar para ello.
Machine Translated by Google

Capítulo 19. Ajuste del rendimiento

El Capítulo 18 cubrió la interfaz de usuario (UI) de Spark y los primeros auxilios básicos para su aplicación Spark.
Usando las herramientas descritas en ese capítulo, debería poder asegurarse de que sus trabajos se ejecuten de manera confiable.
Sin embargo, a veces también necesitará que se ejecuten más rápido o de manera más eficiente por una variedad de
razones. De eso trata este capítulo. Aquí, presentamos una discusión de algunas de las opciones de rendimiento que
están disponibles para hacer que sus trabajos se ejecuten más rápido.

Al igual que con el monitoreo, hay varios niveles diferentes que puede intentar sintonizar. Por ejemplo, si tuviera una red
extremadamente rápida, eso haría que muchos de sus trabajos de Spark fueran más rápidos porque las mezclas suelen ser uno de
los pasos más costosos en un trabajo de Spark. Lo más probable es que no tengas mucha habilidad para controlar esas cosas; por
lo tanto, vamos a discutir las cosas que puede controlar a través de opciones de código o configuración.

Hay una variedad de partes diferentes de los trabajos de Spark que quizás desee optimizar, y es valioso ser específico. Las
siguientes son algunas de las áreas:

Opciones de diseño a nivel de código (por ejemplo, RDD versus DataFrames)

Los datos en reposo

Uniones

Agregaciones

Datos en vuelo

Propiedades de aplicación individuales

Dentro de la Máquina Virtual Java (JVM) de un ejecutor

Nodos trabajadores

Propiedades de clúster e implementación

Esta lista no es exhaustiva, pero al menos sirve de base para la conversación y los temas que cubrimos en este capítulo.
Además, hay dos formas de intentar lograr las características de ejecución que nos gustaría de los trabajos de Spark. Podemos
hacerlo indirectamente estableciendo valores de configuración o cambiando el entorno de tiempo de ejecución. Esto debería
mejorar las cosas en las aplicaciones de Spark o en los trabajos de Spark. Alternativamente, podemos intentar cambiar directamente
las características de ejecución o las opciones de diseño en el nivel de trabajo, etapa o tarea individual de Spark. Este tipo de
correcciones son muy específicas para esa área de nuestra aplicación y, por lo tanto, tienen un impacto general limitado. Hay
numerosas cosas que se encuentran a ambos lados de la división indirecta versus directa , y dibujaremos líneas en la arena
en consecuencia.

Una de las mejores cosas que puede hacer para averiguar cómo mejorar el rendimiento es implementar una buena supervisión
y seguimiento del historial de trabajos. Sin esta información, puede ser difícil saber
Machine Translated by Google

si realmente está mejorando su rendimiento laboral.

Mejoras de rendimiento indirectas


Como se mencionó, hay una serie de mejoras indirectas que puede realizar para ayudar a que sus trabajos
de Spark se ejecuten más rápido. Omitiremos los obvios como "mejorar su hardware" y nos centraremos más
en las cosas que están bajo su control.

Opciones de diseño
Si bien las buenas elecciones de diseño parecen una forma un tanto obvia de optimizar el rendimiento, a
menudo no priorizamos este paso en el proceso. Al diseñar sus aplicaciones, es muy importante tomar
buenas decisiones de diseño porque no solo lo ayuda a escribir mejores aplicaciones Spark, sino también a
lograr que se ejecuten de una manera más estable y consistente a lo largo del tiempo y frente a cambios o
variaciones externas. Ya hemos discutido algunos de estos temas anteriormente en el libro, pero aquí
resumiremos algunos de los fundamentales.

Scala frente a Java frente a Python frente a R

Esta pregunta es casi imposible de responder en el sentido general porque mucho dependerá de su caso
de uso. Por ejemplo, si desea realizar un aprendizaje automático de un solo nodo después de realizar un
trabajo ETL grande, le recomendamos que ejecute su código de extracción, transformación y carga (ETL)
como código SparkR y luego use el ecosistema de aprendizaje automático masivo de R para ejecutar su
algoritmos de aprendizaje automático de un solo nodo. Esto le brinda lo mejor de ambos mundos y
aprovecha la fuerza de R así como la fuerza de Spark sin sacrificios. Como mencionamos en
numerosas ocasiones, las API estructuradas de Spark son uniformes en todos los idiomas en términos de
velocidad y estabilidad. Eso significa que debe codificar con el lenguaje con el que se sienta más cómodo
o que sea más adecuado para su caso de uso.

Las cosas se complican un poco más cuando necesita incluir transformaciones personalizadas que no se
pueden crear en las API estructuradas. Estos pueden manifestarse como transformaciones RDD
o funciones definidas por el usuario (UDF). Si va a hacer esto, R y Python no son necesariamente la mejor
opción simplemente por cómo se ejecuta realmente. También es más difícil proporcionar garantías más
estrictas de tipos y manipulaciones cuando define funciones que saltan entre idiomas. Descubrimos que usar
Python para la mayor parte de la aplicación, y trasladar parte de él a Scala o escribir UDF específicos en
Scala a medida que su aplicación evoluciona, es una técnica poderosa: permite un buen equilibrio entre la
usabilidad general, la capacidad de mantenimiento y el rendimiento.

DataFrames frente a SQL frente a conjuntos de datos frente a RDD

Esta pregunta también surge con frecuencia. La respuesta es simple. En todos los lenguajes,
DataFrames, Datasets y SQL son equivalentes en velocidad. Esto significa que si usa DataFrames
en cualquiera de estos lenguajes, el rendimiento es igual. Sin embargo, si va a definir UDF, tendrá un
impacto en el rendimiento al escribirlos en Python o R, y hasta cierto punto un
Machine Translated by Google

menor impacto en el rendimiento en Java y Scala. Si desea optimizar para obtener un rendimiento puro, le
convendría intentar volver a DataFrames y SQL lo más rápido posible. Aunque todo el código de DataFrame,
SQL y Dataset se compila en RDD, el motor de optimización de Spark escribirá un código RDD "mejor" que el
que puede escribir manualmente y ciertamente lo hará con mucho menos esfuerzo. Además, perderá las nuevas
optimizaciones que se agregan al motor SQL de Spark en cada versión.

Por último, si desea usar RDD, definitivamente recomendamos usar Scala o Java. Si eso no es posible, le
recomendamos que restrinja al mínimo el "área de superficie" de los RDD en su aplicación. Esto se debe a que
cuando Python ejecuta el código RDD, serializa una gran cantidad de datos hacia y desde el proceso de Python.
Esto es muy costoso de ejecutar sobre datos muy grandes y también puede disminuir la estabilidad.

Aunque no es exactamente relevante para el ajuste del rendimiento, es importante tener en cuenta que también
hay algunas lagunas en la funcionalidad que se admite en cada uno de los idiomas de Spark. Hablamos de esto
en el Capítulo 16.

Serialización de objetos en RDD


En la Parte III, discutimos brevemente las bibliotecas de serialización que se pueden usar dentro de
las transformaciones RDD. Cuando trabaje con tipos de datos personalizados, querrá serializarlos con Kryo porque
es más compacto y mucho más eficiente que la serialización de Java. Sin embargo, esto tiene el
inconveniente de registrar las clases que utilizará en su aplicación.

Puede usar la serialización de Kryo configurando spark.serializer en


org.apache.spark.serializer.KryoSerializer. También deberá registrar explícitamente las clases que le gustaría
registrar con el serializador Kryo a través de la configuración
spark.kryo.classesToRegister. También hay una serie de parámetros avanzados para controlar esto
con mayor detalle que se describen en la documentación de Kryo.

Para registrar sus clases, use la SparkConf que acaba de crear y pase los nombres de sus clases:

conf.registerKryoClasses(Array(claseDe[MiClase1], claseDe[MiClase2]))

Configuraciones de clúster
Esta área tiene enormes beneficios potenciales, pero es probablemente una de las más difíciles de
prescribir debido a la variación entre el hardware y los casos de uso. En general, monitorear el rendimiento de las
máquinas en sí será el enfoque más valioso para optimizar las configuraciones de su clúster, especialmente
cuando se trata de ejecutar varias aplicaciones (sean Spark o no) en un solo clúster.

Dimensionamiento y uso compartido de clústeres/aplicaciones


Machine Translated by Google

De alguna manera, esto se reduce a un problema de programación y uso compartido de recursos; sin embargo,
hay muchas opciones sobre cómo desea compartir recursos a nivel de clúster o de aplicación.
Eche un vistazo a las configuraciones enumeradas al final del Capítulo 16 , así como a algunas configuraciones
en el Capítulo 17.

Asignación dinámica

Spark proporciona un mecanismo para ajustar dinámicamente los recursos que ocupa su aplicación en función de
la carga de trabajo. Esto significa que su aplicación puede devolver recursos al clúster si ya no se usan y volver
a solicitarlos más tarde cuando haya demanda. Esta característica es especialmente útil si varias
aplicaciones comparten recursos en su clúster de Spark. Esta función está deshabilitada de forma predeterminada
y está disponible en todos los administradores de clústeres de granularidad gruesa; es decir, modo
independiente, modo YARN y modo de grano grueso Mesos. Si desea habilitar esta función, debe establecer
spark.dynamicAllocation.enabled en verdadero. La documentación de Spark presenta una serie de parámetros
individuales que puede ajustar.

Planificación
En el transcurso de los capítulos anteriores, discutimos una serie de posibles optimizaciones diferentes
que puede aprovechar para ayudar a que los trabajos de Spark se ejecuten en paralelo con los grupos de
programadores o ayudar a que las aplicaciones de Spark se ejecuten en paralelo con algo como la asignación
dinámica o la configuración de max­executor. ­núcleos. Las optimizaciones de programación implican un
poco de investigación y experimentación, y desafortunadamente no hay soluciones súper rápidas
más allá de configurar spark.scheduler.mode en FAIR para permitir un mejor intercambio de recursos entre
múltiples usuarios, o configurar ­­max­executor­cores, que especifica el número máximo de núcleos ejecutor
que necesitará su aplicación. Especificar este valor puede garantizar que su aplicación no consuma todos los
recursos del clúster. También puede cambiar el valor predeterminado, según su administrador de clústeres,
estableciendo la configuración spark.cores.max en el valor predeterminado de su elección. Los administradores
de clústeres también proporcionan algunas primitivas de programación que pueden ser útiles al optimizar varias
aplicaciones Spark, como se explica en los capítulos 16 y 17.

Los datos en reposo

La mayoría de las veces, cuando está guardando datos, se leerán muchas veces cuando otras personas de su
organización accedan a los mismos conjuntos de datos para ejecutar diferentes análisis. Asegurarse de que está
almacenando sus datos para lecturas efectivas más adelante es absolutamente esencial para el éxito de
los proyectos de big data. Esto implica elegir su sistema de almacenamiento, elegir su formato de datos y
aprovechar funciones como la partición de datos en algunos formatos de almacenamiento.

Almacenamiento de datos a largo plazo basado en archivos

Hay varios formatos de archivo diferentes disponibles, desde simples archivos de valores separados por
comas (CSV) y blobs binarios, hasta formatos más sofisticados como Apache Parquet. Una de las formas
más fáciles de optimizar sus trabajos de Spark es seguir las mejores prácticas al almacenar datos y elegir
Machine Translated by Google

el formato de almacenamiento más eficiente posible.

En general, siempre debe favorecer los tipos binarios estructurados para almacenar sus datos, especialmente cuando
accederá a ellos con frecuencia. Aunque los archivos como "CSV" parecen estar bien estructurados, son muy lentos de
analizar y, a menudo, también están llenos de casos extremos y puntos débiles. Por ejemplo, los caracteres de nueva línea con
escape incorrecto a menudo pueden causar muchos problemas al leer una gran cantidad de archivos. El formato de archivo
más eficiente que generalmente puede elegir es Apache Parquet. Parquet almacena datos en archivos binarios con
almacenamiento orientado a columnas y también realiza un seguimiento de algunas estadísticas sobre cada archivo que
permiten omitir rápidamente los datos que no son necesarios para una consulta. Está bien integrado con Spark a través
de la fuente de datos Parquet incorporada.

Compresión y tipos de archivos divisibles

Independientemente del formato de archivo que elija, debe asegurarse de que sea "divisible", lo que significa que
diferentes tareas pueden leer diferentes partes del archivo en paralelo. Vimos por qué esto es importante en el Capítulo
18. Cuando leímos el archivo, todos los núcleos pudieron hacer parte del trabajo. Eso es porque el archivo era divisible. Si no
usamos un tipo de archivo divisible, digamos algo como un archivo JSON con formato incorrecto, necesitaremos leer el
archivo completo en una sola máquina, lo que reducirá en gran medida el paralelismo.

El lugar principal en el que entra la capacidad de división son los formatos de compresión. Un archivo ZIP o TAR no se puede
dividir, lo que significa que incluso si tenemos 10 archivos en un archivo ZIP y 10 núcleos, solo un núcleo puede leer esos
datos porque no podemos paralelizar el acceso al archivo ZIP. Este es un mal uso de los recursos. Por el contrario,
los archivos comprimidos con gzip, bzip2 o lz4 generalmente se pueden dividir si fueron escritos por un marco de
procesamiento paralelo como Hadoop o Spark. Para sus propios datos de entrada, la forma más sencilla de hacerlos divisibles
es cargarlos como archivos separados, idealmente cada uno no mayor de unos pocos cientos de megabytes.

Particionamiento de tablas

Discutimos el particionamiento de tablas en el Capítulo 9, y solo usaremos esta sección como un recordatorio. La partición de
tablas se refiere al almacenamiento de archivos en directorios separados en función de una clave, como el campo de fecha en
los datos. Los administradores de almacenamiento como Apache Hive admiten este concepto, al igual que muchas de las fuentes
de datos integradas de Spark. Particionar sus datos correctamente le permite a Spark omitir muchos archivos irrelevantes
cuando solo requiere datos con un rango específico de claves. Por ejemplo, si los usuarios filtran con frecuencia por "fecha" o
"Id. de cliente" en sus consultas, divida sus datos por esas columnas. Esto reducirá en gran medida la cantidad de datos que
los usuarios finales deben leer en la mayoría de las consultas y, por lo tanto, aumentará drásticamente la velocidad.

Sin embargo, la única desventaja de la partición es que si se realiza una partición con una granularidad demasiado fina, puede
generar muchos archivos pequeños y una gran sobrecarga al tratar de enumerar todos los archivos en el sistema de
almacenamiento.

agrupamiento

También discutimos el almacenamiento en depósitos en el Capítulo 9, pero para recapitular, la esencia es que el almacenamiento en depósitos

de sus datos le permite a Spark "particionar previamente" los datos de acuerdo con cómo es probable que sean las uniones o agregaciones.
Machine Translated by Google

realizado por los lectores. Esto puede mejorar el rendimiento y la estabilidad porque los datos se pueden
distribuir de manera uniforme entre particiones en lugar de sesgarse en solo una o dos. Por ejemplo, si las uniones se
realizan con frecuencia en una columna inmediatamente después de una lectura, puede usar la creación de depósitos para
asegurarse de que los datos estén bien particionados de acuerdo con esos valores. Esto puede ayudar a evitar una
reproducción aleatoria antes de una unión y, por lo tanto, ayudar a acelerar el acceso a los datos. La agrupación
generalmente funciona de la mano con la partición como una segunda forma de dividir físicamente los datos.

el numero de archivos

Además de organizar sus datos en depósitos y particiones, también querrá considerar la cantidad de archivos y el tamaño
de los archivos que está almacenando. Si hay muchos archivos pequeños, pagará una lista de precios y obtendrá
cada uno de esos archivos individuales. Por ejemplo, si está leyendo datos de Hadoop Distributed File System (HDFS),
estos datos se administran en bloques que tienen un tamaño de hasta 128 MB (de forma predeterminada). Esto significa
que si tiene 30 archivos, de 5 MB cada uno, tendrá que solicitar potencialmente 30 bloques, aunque los mismos datos
podrían haber cabido en 2 bloques (150 MB en total).

Aunque no existe necesariamente una panacea sobre cómo desea almacenar sus datos, la compensación se puede resumir
como tal. Tener muchos archivos pequeños hará que el programador trabaje mucho más para localizar los datos y
ejecutar todas las tareas de lectura. Esto puede aumentar la red y

programar los gastos generales del trabajo. Tener menos archivos grandes alivia el dolor del programador, pero también
hará que las tareas se ejecuten por más tiempo. En este caso, sin embargo, siempre puede iniciar más tareas que
archivos de entrada hay si desea más paralelismo: Spark dividirá cada archivo en varias tareas, suponiendo que esté
utilizando un formato divisible. En general, recomendamos dimensionar sus archivos para que cada uno contenga al
menos algunas decenas de megabytes de datos.

Una forma de controlar la partición de datos cuando escribe sus datos es a través de una opción de escritura
introducida en Spark 2.2. Para controlar cuántos registros van en cada archivo, puede especificar la opción
maxRecordsPerFile para la operación de escritura.

localidad de datos

Otro aspecto que puede ser importante en los entornos de clústeres compartidos es la localidad de los datos. La localidad
de datos básicamente especifica una preferencia por ciertos nodos que contienen ciertos datos, en lugar de tener que
intercambiar estos bloques de datos a través de la red. Si ejecuta su sistema de almacenamiento en los mismos
nodos que Spark y el sistema admite sugerencias de localidad, Spark intentará programar tareas cerca de cada bloque
de datos de entrada. Por ejemplo, el almacenamiento HDFS ofrece esta opción. Hay varias configuraciones que
afectan la localidad, pero generalmente se usará de manera predeterminada si Spark detecta que está usando un sistema
de almacenamiento local. También verá las tareas de lectura de datos marcadas como "locales" en la interfaz de
usuario web de Spark.

Recopilación de estadísticas

Spark incluye un optimizador de consultas basado en costos que planifica las consultas en función de las propiedades
de los datos de entrada cuando se usan las API estructuradas. Sin embargo, para permitir que el optimizador basado en
costos tome este tipo de decisiones, debe recopilar (y mantener) estadísticas sobre sus tablas que pueda
Machine Translated by Google

usar. Hay dos tipos de estadísticas: estadísticas de nivel de tabla y estadísticas de nivel de columna. La recopilación de estadísticas está

disponible solo en tablas con nombre, no en DataFrames o RDD arbitrarios.

Para recopilar estadísticas a nivel de tabla, puede ejecutar el siguiente comando:

ANALIZAR TABLA table_name CALCULAR ESTADÍSTICAS

Para recopilar estadísticas a nivel de columna, puede nombrar las columnas específicas:

ANALIZAR TABLA nombre_tabla CALCULAR ESTADÍSTICAS PARA


COLUMNAS nombre_columna1, nombre_columna2, ...

Las estadísticas a nivel de columna son más lentas de recopilar, pero brindan más información para que el optimizador basado en costos

use sobre esas columnas de datos. Ambos tipos de estadísticas pueden ayudar con uniones, agregaciones, filtros y otras

cosas potenciales (p. ej., elegir automáticamente cuándo hacer una transmisión de unión). Esta es una parte de Spark de rápido crecimiento,

por lo que es probable que en el futuro se agreguen diferentes optimizaciones basadas en estadísticas.

NOTA

Puede seguir el progreso de la optimización basada en costos en su problema JIRA. También puede leer el documento
de diseño en SPARK­16026 para obtener más información sobre esta función. Esta es un área activa de desarrollo
en Spark en el momento de escribir este artículo.

Configuraciones aleatorias
La configuración del servicio de reproducción aleatoria externa de Spark (discutido en los capítulos 16 y 17) a menudo puede

aumentar el rendimiento porque permite que los nodos lean datos aleatorios de máquinas remotas incluso cuando los ejecutores de esas

máquinas están ocupados (por ejemplo, con la recolección de elementos no utilizados). Sin embargo, esto tiene un costo de complejidad y

mantenimiento, por lo que podría no valer la pena en su implementación. Más allá de configurar este servicio externo,

también existen una serie de configuraciones para shuffles, como el número de conexiones concurrentes por ejecutor, aunque estas suelen

tener buenos valores por defecto.

Además, para los trabajos basados en RDD, el formato de serialización tiene un gran impacto en el rendimiento de la

reproducción aleatoria; siempre prefiera la serialización Kryo sobre Java, como se describe en "Serialización de objetos en RDD". Además,

para todos los trabajos, el número de particiones de una mezcla es importante. Si tiene muy pocas particiones, muy pocos nodos estarán

trabajando y puede haber sesgo, pero si tiene demasiadas particiones, hay una sobrecarga para iniciar cada una que puede comenzar a

dominar. Trate de aspirar al menos unas pocas decenas de megabytes de datos por partición de salida en su reproducción aleatoria.

Presión de memoria y recolección de basura


Durante el transcurso de la ejecución de trabajos de Spark, las máquinas ejecutoras o controladoras pueden tener dificultades para
Machine Translated by Google

completar sus tareas debido a la falta de memoria suficiente o "presión de la memoria". Esto puede ocurrir cuando una
aplicación ocupa demasiada memoria durante la ejecución o cuando la recolección de elementos no utilizados se
ejecuta con demasiada frecuencia o su ejecución es lenta, ya que se crea una gran cantidad de objetos en la JVM y,
posteriormente, se recolectan los elementos no utilizados cuando ya no se utilizan. Una estrategia para aliviar este problema
es asegurarse de usar las API estructuradas tanto como sea posible. Esto no solo aumentará la eficiencia con la que se
ejecutarán sus trabajos de Spark, sino que también reducirá en gran medida la presión de la memoria porque los objetos
JVM nunca se realizan y Spark SQL simplemente realiza el cálculo en su formato interno.

La documentación de Spark incluye algunos consejos excelentes sobre cómo ajustar la recolección de elementos no
utilizados para aplicaciones basadas en RDD y UDF, y parafraseamos las siguientes secciones a partir de esa información.

Medición del impacto de la recolección de basura

El primer paso en el ajuste de la recolección de basura es recopilar estadísticas sobre la frecuencia con la que ocurre
la recolección de basura y la cantidad de tiempo que lleva. Puede hacer esto agregando ­verbose:gc ­ XX:+PrintGCDetails
­XX:+PrintGCTimeStamps a las opciones de JVM de Spark usando el parámetro de configuración
spark.executor.extraJavaOptions. La próxima vez que ejecute su trabajo de Spark, verá mensajes impresos en los
registros del trabajador cada vez que se produzca una recolección de elementos no utilizados. Estos registros estarán en
los nodos trabajadores de su clúster (en los archivos stdout en sus directorios de trabajo), no en el controlador.

Ajuste de recolección de basura

Para ajustar aún más la recolección de basura, primero debe comprender cierta información básica sobre la administración
de memoria en la JVM:

El espacio de almacenamiento dinámico de Java se divide en dos regiones: Young y Old. La generación joven
está destinada a contener objetos de corta duración, mientras que la generación antigua está destinada a
objetos con una vida útil más larga.

La generación joven se divide además en tres regiones: Eden, Survivor1 y Survivor2.

Aquí hay una descripción simplificada del procedimiento de recolección de basura:

1. Cuando Eden está lleno, se ejecuta una recolección de basura menor en Eden y los objetos que están vivos
de Eden y Survivor1 se copian en Survivor2.

2. Las regiones de Survivor se intercambian.

3. Si un objeto es lo suficientemente antiguo o si Survivor2 está lleno, ese objeto se mueve a Antiguo.

4. Finalmente, cuando Old está casi lleno, se invoca una recolección de basura completa. Esto involucra
rastreando todos los objetos en el montón, eliminando los no referenciados y moviendo los demás para llenar el
espacio no utilizado, por lo que generalmente es la operación de recolección de elementos no utilizados
más lenta.
Machine Translated by Google

El objetivo del ajuste de la recolección de basura en Spark es garantizar que solo los conjuntos de datos en caché de larga
duración se almacenen en la generación anterior y que la generación joven tenga el tamaño suficiente para almacenar todos
los objetos de corta duración. Esto ayudará a evitar recolecciones de basura completas para recopilar objetos temporales
creados durante la ejecución de la tarea. Aquí hay algunos pasos que pueden ser útiles.

Recopile estadísticas de recolección de basura para determinar si se está ejecutando con demasiada frecuencia. Si se
invoca una recolección de basura completa varias veces antes de que se complete una tarea, significa que no hay suficiente
memoria disponible para ejecutar tareas, por lo que debe disminuir la cantidad de memoria que Spark usa para el

almacenamiento en caché (spark.memory.fraction).

Si hay demasiadas recolecciones menores pero no muchas recolecciones de elementos no utilizados importantes, ayudaría
asignar más memoria para Eden. Puede configurar el tamaño de Eden para que sea una sobreestimación de la cantidad de
memoria que necesitará cada tarea. Si se determina que el tamaño de Eden es E, puede establecer el tamaño de la generación

joven usando la opción ­Xmn=4/3*E. (La ampliación de 4/3 es también para tener en cuenta el espacio utilizado por las regiones
supervivientes).

Como ejemplo, si su tarea es leer datos de HDFS, la cantidad de memoria utilizada por la tarea se puede estimar usando el
tamaño del bloque de datos leído de HDFS. Tenga en cuenta que el tamaño de un bloque descomprimido suele ser dos o
tres veces el tamaño del bloque. Entonces, si desea tener tres o cuatro tareas de espacio de trabajo y el tamaño del bloque HDFS
es de 128 MB, podemos estimar que el tamaño de Eden es de 43,128 MB.

Pruebe el recolector de basura G1GC con ­XX:+UseG1GC. Puede mejorar el rendimiento en algunas situaciones en las
que la recolección de elementos no utilizados es un cuello de botella y no tiene forma de reducirlo más dimensionando las
generaciones. Tenga en cuenta que con grandes tamaños de almacenamiento dinámico del ejecutor, puede ser importante

aumentar el tamaño de la región G1 con ­XX:G1HeapRegionSize.

Supervise cómo cambia la frecuencia y el tiempo que tarda la recolección de elementos no utilizados con la nueva configuración.

Nuestra experiencia sugiere que el efecto del ajuste de la recolección de elementos no utilizados depende de su aplicación y de
la cantidad de memoria disponible. Hay muchas más opciones de ajuste descritas en línea, pero a un alto nivel, administrar la
frecuencia con la que se realiza la recolección completa de elementos no utilizados puede ayudar a reducir los gastos generales.
Puede especificar indicadores de ajuste de recolección de elementos no utilizados para ejecutores configurando

spark.executor.extraJavaOptions en la configuración de un trabajo.

Mejoras de rendimiento directo


En la sección anterior, mencionamos algunas mejoras generales de rendimiento que se aplican a todos los trabajos. Asegúrese
de hojear las dos páginas anteriores antes de saltar a esta sección y las soluciones aquí. Estas soluciones aquí están
pensadas como "curitas" para problemas con etapas o trabajos específicos, pero requieren inspeccionar y optimizar cada etapa
o trabajo por separado.

Paralelismo
Lo primero que debe hacer cuando intente acelerar una etapa específica es aumentar el grado de paralelismo. En general,
recomendamos tener al menos dos o tres tareas por núcleo de CPU
Machine Translated by Google

en su clúster si la etapa procesa una gran cantidad de datos. Puede configurar esto a través de la propiedad
spark.default.parallelism, así como ajustar las particiones spark.sql.shuffle.partitions de acuerdo con la cantidad de núcleos en
su clúster.

Filtrado mejorado
Otra fuente frecuente de mejoras de rendimiento es mover los filtros a la parte más temprana posible de su trabajo de Spark.
A veces, estos filtros se pueden insertar en las fuentes de datos y esto significa que puede evitar leer y trabajar con
datos que son irrelevantes para su resultado final. Habilitar la partición y el depósito también ayuda a lograr esto. Siempre
busque filtrar tantos datos como pueda desde el principio, y encontrará que sus trabajos de Spark casi siempre se ejecutarán
más rápido.

Repartición y fusión
Las llamadas de partición pueden incurrir en una mezcla. Sin embargo, hacer algo puede optimizar la ejecución general de un
trabajo al equilibrar los datos en todo el clúster, por lo que puede valer la pena. En general, debe intentar mezclar la menor
cantidad de datos posible. Por esta razón, si está reduciendo la cantidad de particiones generales en un DataFrame o
RDD, primero pruebe el método coalesce, que no realizará una reproducción aleatoria sino que fusionará particiones en el
mismo nodo en una sola partición. El método de partición más lento también mezclará datos a través de la red para lograr un
equilibrio de carga uniforme. Las reparticiones pueden ser particularmente útiles cuando se realizan uniones o antes de una
llamada de caché. Recuerde que volver a particionar no es gratuito, pero puede mejorar el rendimiento general de
la aplicación y el paralelismo de sus trabajos.

Particionamiento personalizado

Si sus trabajos aún son lentos o inestables, es posible que desee explorar la realización de particiones personalizadas en el
nivel de RDD. Esto le permite definir una función de partición personalizada que organizará los datos en todo el clúster con
un nivel de precisión más fino que el que está disponible en el nivel de DataFrame. Esto rara vez es necesario, pero es una
opción. Para obtener más información, consulte la Parte III.

Funciones definidas por el usuario (UDF)


En general, evitar las UDF es una buena oportunidad de optimización. Los UDF son costosos porque fuerzan la representación
de datos como objetos en la JVM y, a veces, lo hacen varias veces por registro en una consulta. Debe intentar usar las API
estructuradas tanto como sea posible para realizar sus manipulaciones simplemente porque van a realizar las
transformaciones de una manera mucho más eficiente de lo que puede hacer en un lenguaje de alto nivel. También hay
trabajo en curso para hacer que los datos estén disponibles para las UDF en lotes, como la extensión UDF vectorizada para
Python que le da a su código múltiples registros a la vez usando un marco de datos de Pandas. Discutimos las FDU y sus
costos en el Capítulo 18.
Machine Translated by Google

Almacenamiento temporal de datos (caché)


En las aplicaciones que reutilizan los mismos conjuntos de datos una y otra vez, una de las optimizaciones más útiles es el almacenamiento en caché.

El almacenamiento en caché colocará un DataFrame, una tabla o un RDD en un almacenamiento temporal (ya sea memoria o disco) en los

ejecutores de su clúster y hará que las lecturas posteriores sean más rápidas. Aunque el almacenamiento en caché puede sonar como algo que

deberíamos hacer todo el tiempo, no siempre es bueno hacerlo. Esto se debe a que el almacenamiento en caché de datos incurre en un costo de

serialización, deserialización y almacenamiento. Por ejemplo, si solo va a procesar un conjunto de datos una vez (en una transformación posterior),

el almacenamiento en caché solo lo ralentizará.

El caso de uso para el almacenamiento en caché es simple: mientras trabaja con datos en Spark, ya sea dentro de una sesión interactiva o una

aplicación independiente, a menudo querrá reutilizar un determinado conjunto de datos (por ejemplo, un DataFrame o RDD). Por

ejemplo, en una sesión interactiva de ciencia de datos, puede cargar y limpiar sus datos y luego reutilizarlos para probar varios modelos

estadísticos. O en una aplicación independiente, puede ejecutar un algoritmo iterativo que reutilice el mismo conjunto de datos. Puede decirle a Spark

que almacene en caché un conjunto de datos utilizando el método de caché en DataFrames o RDD.

El almacenamiento en caché es una operación perezosa, lo que significa que las cosas se almacenarán en caché solo cuando se acceda a ellas.

La API RDD y la API estructurada difieren en la forma en que realmente realizan el almacenamiento en caché, así que revisemos los detalles

sangrientos antes de repasar los niveles de almacenamiento. Cuando almacenamos en caché un RDD, almacenamos en caché los datos físicos

reales (es decir, los bits). los pedacitos Cuando se vuelve a acceder a estos datos, Spark devuelve los datos adecuados. Esto se hace a través de

la referencia RDD. Sin embargo, en la API estructurada, el almacenamiento en caché se realiza en función del plan físico. Esto significa que

almacenamos efectivamente el plan físico como nuestra clave (a diferencia de la referencia del objeto) y realizamos una búsqueda antes de la

ejecución de un trabajo estructurado.

Esto puede causar confusión porque a veces puede esperar acceder a datos sin procesar, pero debido a que otra persona ya almacenó los

datos en caché, en realidad está accediendo a su versión en caché.

Téngalo en cuenta cuando utilice esta función.

Hay diferentes niveles de almacenamiento que puede usar para almacenar en caché sus datos, especificando qué tipo de almacenamiento usar.

La Tabla 19­1 enumera los niveles.

Tabla 19­1. Niveles de almacenamiento de caché de datos

Nivel de almacenamiento Significado

Almacene RDD como objetos Java deserializados en la JVM. Si el RDD no cabe en la memoria,
SOLO_MEMORIA algunas particiones no se almacenarán en caché y se volverán a calcular sobre la marcha cada vez que se
necesiten. Este es el nivel por defecto.

Almacene RDD como objetos Java deserializados en la JVM. Si el RDD no cabe en la memoria,
MEMORIA_Y_DISCO almacene las particiones que no caben en el disco y léalas desde allí cuando las necesite.

Almacene RDD como objetos Java serializados (matriz de un byte por partición). Esto generalmente es más
MEMORY_ONLY_SER
eficiente en espacio que los objetos deserializados, especialmente cuando se usa un serializador rápido, pero
(Java y Scala)
requiere más CPU para leer.

MEMORY_AND_DISK_SER Similar a MEMORY_ONLY_SER, pero derrama las particiones que no caben en la memoria en el disco
Machine Translated by Google

(Java y Scala) en lugar de volver a calcularlos sobre la marcha cada vez que se necesitan.

DISK_ONLY Almacene las particiones RDD solo en el disco.

MEMORIA_SOLO_2,

MEMORIA_Y_DISCO_2, Igual que los niveles anteriores, pero replica cada partición en dos nodos de clúster.
etc.

OFF_HEAP Similar a MEMORY_ONLY_SER, pero almacena los datos en la memoria fuera del montón. Esto requiere que la
(experimental) memoria fuera del montón esté habilitada.

Para obtener más información sobre estas opciones, consulte "Configuración de la gestión de la memoria".

La figura 19­1 presenta ilustraciones simples del proceso. Cargamos un DataFrame inicial desde un archivo
CSV y luego derivamos algunos DataFrames nuevos usando transformaciones. Podemos evitar tener que
volver a calcular el DataFrame original (es decir, cargar y analizar el archivo CSV) muchas veces agregando
una línea para almacenarlo en caché en el camino.

Figura 19­1. Un marco de datos en caché

Ahora veamos el código:

# en Python #
Código de carga original que *no* almacena en caché DataFrame DF1 =
spark.read.format("csv")\
.option("inferSchema", "true")\ .option("header",
"true")\ .load("/data/flight­data/csv/2015­
summary.csv")
DF2 = DF1.groupBy("DEST_COUNTRY_NAME").count().collect()
DF3 = DF1.groupBy("ORIGIN_COUNTRY_NAME").count().collect()
DF4 = DF1.groupBy("contar").contar().recoger()
Machine Translated by Google

Verá aquí que tenemos nuestro DataFrame (DF1) creado "perezosamente", junto con otros tres DataFrames que acceden
a los datos en DF1. Todos nuestros DataFrames descendentes comparten ese padre común (DF1) y repetirán el mismo

trabajo cuando realicemos el código anterior. En este caso, solo se trata de leer y analizar los datos CSV sin procesar, pero
puede ser un proceso bastante intensivo, especialmente para grandes conjuntos de datos.

En mi máquina, esos comandos tardan uno o dos segundos en ejecutarse. Afortunadamente, el almacenamiento en caché
puede ayudar a acelerar las cosas. Cuando solicitamos que se almacene en caché un DataFrame, Spark guardará los datos en la
memoria o en el disco la primera vez que los calcule. Luego, cuando aparezcan otras consultas, solo se referirán a la almacenada
en la memoria en lugar del archivo original. Haces esto usando el método de caché de DataFrame:

DF1.caché()
DF1.cuenta()

Usamos el conteo anterior para almacenar en caché los datos con entusiasmo (básicamente realizar una acción para obligar a
Spark a almacenarlos en la memoria), porque el almacenamiento en caché en sí mismo es lento: los datos se almacenan en
caché solo la primera vez que ejecuta una acción en el DataFrame. Ahora que los datos están en caché, los comandos
anteriores serán más rápidos, como podemos ver al ejecutar el siguiente código:

# en Python
DF2 = DF1.groupBy("DEST_COUNTRY_NAME").count().collect()
DF3 = DF1.groupBy("ORIGIN_COUNTRY_NAME").count().collect()
DF4 = DF1.groupBy("contar").contar().recoger()

Cuando ejecutamos este código, ¡redujo el tiempo a más de la mitad! Esto puede no parecer tan descabellado, pero imagine
un gran conjunto de datos o uno que requiere mucho cálculo para crear (no solo leer en un archivo). Los ahorros pueden ser
inmensos. También es excelente para las cargas de trabajo iterativas de aprendizaje automático porque a menudo
necesitarán acceder a los mismos datos varias veces, lo que veremos en breve.

El comando de caché en Spark siempre coloca datos en la memoria de forma predeterminada, almacenando en caché solo una

parte del conjunto de datos si la memoria total del clúster está llena. Para tener más control, también hay un método de persistencia

que toma un objeto StorageLevel para especificar dónde almacenar en caché los datos: en la memoria, en el disco o en ambos.

Uniones
Las uniones son un área común para la optimización. El arma más grande que tiene cuando se trata de optimizar uniones
es simplemente educarse sobre lo que hace cada unión y cómo se realiza.
Esto te ayudará más. Además, las uniones equitativas son las más fáciles de optimizar para Spark en este punto y, por lo tanto,
deben preferirse siempre que sea posible. Más allá de eso, cosas simples como tratar de usar la capacidad de filtrado de las
uniones internas cambiando el orden de las uniones pueden generar grandes aceleraciones.
Además, el uso de sugerencias de unión de difusión puede ayudar a Spark a tomar decisiones de planificación inteligentes cuando
se trata de crear planes de consulta, como se describe en el Capítulo 8. Evitar las uniones cartesianas o incluso las uniones
externas completas a menudo es una fruta fácil de alcanzar para la estabilidad y las optimizaciones porque a menudo pueden
ser optimizado en diferentes combinaciones de estilo de filtrado cuando observa todo el flujo de datos en lugar de solo
Machine Translated by Google

esa área de trabajo en particular. Por último, seguir algunas de las otras secciones de este capítulo puede tener un
efecto significativo en las uniones. Por ejemplo, la recopilación de estadísticas en las tablas antes de una unión
ayudará a Spark a tomar decisiones de unión inteligentes. Además, agrupar sus datos adecuadamente también
puede ayudar a Spark a evitar grandes cambios cuando se realizan uniones.

Agregaciones
En su mayor parte, no hay muchas formas de optimizar agregaciones específicas más allá del filtrado de
datos antes de que la agregación tenga una cantidad suficientemente alta de particiones.
Sin embargo, si usa RDD, controlar exactamente cómo se realizan estas agregaciones (por ejemplo, usar
reduceByKey cuando sea posible sobre groupByKey) puede ser muy útil y mejorar la velocidad y la estabilidad de su
código.

Variables de difusión
Nos referimos a las uniones de transmisión y las variables en capítulos anteriores, y estas son una buena opción
para la optimización. La premisa básica es que si se utilizará una gran cantidad de datos en varias llamadas
UDF en su programa, puede transmitirlos para guardar solo una copia de solo lectura en cada nodo y evitar volver a
enviar estos datos con cada trabajo. Por ejemplo, las variables de transmisión pueden ser útiles para guardar una
tabla de búsqueda o un modelo de aprendizaje automático. También puede transmitir objetos arbitrarios
mediante la creación de variables de transmisión utilizando su SparkContext, y luego simplemente hacer referencia
a esas variables en sus tareas, como discutimos en el Capítulo 14.

Conclusión
Hay muchas formas diferentes de optimizar el rendimiento de sus aplicaciones Spark y hacer que se ejecuten
más rápido y a un costo menor. En general, las cosas principales que querrá priorizar son (1) leer la menor cantidad
de datos posible a través de particiones y formatos binarios eficientes, (2) asegurarse de que haya suficiente
paralelismo y que no haya datos sesgados en el clúster usando particiones, y ( 3) usar API de alto nivel como las
API estructuradas tanto como sea posible para tomar código ya optimizado. Al igual que con cualquier otro trabajo
de optimización de software, también debe asegurarse de que está optimizando las operaciones correctas
para su trabajo: las herramientas de monitoreo de Spark descritas en el Capítulo 18 le permitirán ver qué etapas
están tomando más tiempo y concentrar sus esfuerzos en ellas. Una vez que haya identificado el trabajo que cree
que se puede optimizar, las herramientas de este capítulo cubrirán las oportunidades de optimización del
rendimiento más importantes para la mayoría de los usuarios.
Machine Translated by Google

Parte V. Transmisión
Machine Translated by Google

Capítulo 20. Fundamentos del


procesamiento de flujo

El procesamiento de flujo es un requisito clave en muchas aplicaciones de big data. Tan pronto como una aplicación
calcula algo de valor, por ejemplo, un informe sobre la actividad del cliente o un nuevo modelo de aprendizaje
automático, una organización querrá calcular este resultado continuamente en un entorno de producción. Como resultado,
las organizaciones de todos los tamaños están comenzando a incorporar el procesamiento de secuencias, a menudo
incluso en la primera versión de una nueva aplicación.

Afortunadamente, Apache Spark tiene un largo historial de soporte de alto nivel para la transmisión. En 2012, el
proyecto incorporó Spark Streaming y su API DStreams, una de las primeras API en permitir el procesamiento de
transmisiones mediante operadores funcionales de alto nivel como map y reduce. Cientos de organizaciones
ahora usan DStreams en producción para grandes aplicaciones en tiempo real, a menudo procesando terabytes de
datos por hora. Sin embargo, al igual que la API Resilient Distributed Dataset (RDD), la API DStreams se basa en
operaciones de nivel relativamente bajo en objetos Java/Python que limitan las oportunidades de optimización de
nivel superior. Por lo tanto, en 2016, el proyecto Spark agregó Structured Streaming, una nueva API de transmisión
construida directamente en DataFrames que admite optimizaciones ricas y una integración
significativamente más simple con otro código de DataFrame y Dataset. La API de transmisión estructurada se marcó
como estable en Apache Spark 2.2 y también experimentó una adopción rápida en toda la comunidad de Spark.

En este libro, nos centraremos solo en la API de transmisión estructurada, que se integra directamente con las API de
DataFrame y Dataset que analizamos anteriormente en el libro y es el marco de trabajo elegido.

para escribir nuevas aplicaciones de transmisión. Si está interesado en DStreams, muchos otros libros cubren esa
API, incluidos varios libros dedicados solo a Spark Streaming, como Learning Spark Streaming de Francois Garillot
y Gerard Maas (O'Reilly, 2017). Sin embargo, al igual que ocurre con los RDD frente a los DataFrames, la transmisión
estructurada ofrece un superconjunto de la mayoría de las funciones de los DStream y, a menudo, funcionará
mejor debido a la generación de código y al optimizador Catalyst.

Antes de analizar las API de transmisión en Spark, definamos de manera más formal la transmisión y el procesamiento
por lotes. Este capítulo discutirá algunos de los conceptos centrales en esta área que necesitaremos a lo largo de
esta parte del libro. No será una disertación sobre este tema, pero cubrirá suficientes conceptos para permitirle dar
sentido a los sistemas en este espacio.

¿Qué es el procesamiento de flujo?


El procesamiento de flujo es el acto de incorporar continuamente nuevos datos para calcular un resultado. En el
procesamiento de flujo, los datos de entrada son ilimitados y no tienen un principio ni un final predeterminados.
Simplemente forma una serie de eventos que llegan al sistema de procesamiento de flujo (por ejemplo,
transacciones con tarjeta de crédito, clics en un sitio web o lecturas de sensores de dispositivos de Internet de las cosas [IoT]). Usuario
Machine Translated by Google

las aplicaciones pueden luego calcular varias consultas sobre este flujo de eventos (p. ej., rastrear un conteo continuo de
cada tipo de evento o agregarlos en ventanas por hora). La aplicación generará múltiples versiones del resultado a
medida que se ejecuta, o tal vez lo mantendrá actualizado en un sistema de "sumidero" externo, como un almacén de
clave­valor.

Naturalmente, podemos comparar la transmisión con el procesamiento por lotes, en el que el cálculo se ejecuta en un
conjunto de datos de entrada fija. A menudo, esto puede ser un conjunto de datos a gran escala en un almacén de
datos que contiene todos los eventos históricos de una aplicación (p. ej., todas las visitas al sitio web o lecturas de
sensores del último mes). El procesamiento por lotes también requiere una consulta para calcular, similar al
procesamiento de flujo, pero solo calcula el resultado una vez.

Aunque la transmisión y el procesamiento por lotes suenan diferentes, en la práctica, a menudo necesitan trabajar
juntos. Por ejemplo, las aplicaciones de transmisión a menudo necesitan unir los datos de entrada con un conjunto
de datos escrito periódicamente por un trabajo por lotes, y la salida de los trabajos de transmisión suele ser archivos o
tablas que se consultan en trabajos por lotes. Además, cualquier lógica comercial en sus aplicaciones debe
funcionar de manera consistente en la transmisión y la ejecución por lotes: por ejemplo, si tiene un código
personalizado para calcular el monto de facturación de un usuario, sería dañino obtener un resultado diferente al ejecutarlo
en una transmisión versus lote de moda! Para manejar estas necesidades, Structured Streaming se diseñó desde el
principio para interoperar fácilmente con el resto de Spark, incluidas las aplicaciones por lotes. De hecho, los
desarrolladores de Structured Streaming acuñaron el término aplicaciones continuas para capturar aplicaciones de
extremo a extremo que consisten en trabajos interactivos, por lotes y de transmisión, todos trabajando en los mismos
datos para entregar un producto final. La transmisión estructurada se enfoca en simplificar la creación de tales aplicaciones
de manera integral en lugar de solo manejar el procesamiento por registro a nivel de transmisión.

Casos de uso de procesamiento de flujo


Definimos el procesamiento de flujo como el procesamiento incremental de conjuntos de datos ilimitados, pero esa es
una forma extraña de motivar un caso de uso. Antes de entrar en las ventajas y desventajas de la transmisión,
expliquemos por qué es posible que desee utilizar la transmisión. Describiremos seis casos de uso comunes con
diferentes requisitos del sistema de procesamiento de secuencias subyacente.

Notificaciones y alertas

Probablemente, el caso de uso de transmisión más obvio involucra notificaciones y alertas. Dada una serie de eventos,
se debe activar una notificación o alerta si ocurre algún tipo de evento o serie de eventos. Esto no implica
necesariamente una toma de decisiones autónoma o preprogramada; las alertas también se pueden usar para notificar
a una contraparte humana sobre alguna acción que debe tomarse. Un ejemplo podría ser enviar una alerta a un empleado
en un centro logístico de que necesita obtener un determinado artículo de una ubicación en el almacén y enviarlo a un
cliente. En cualquier caso, la notificación debe ocurrir rápidamente.

Informes en tiempo real

Muchas organizaciones usan sistemas de transmisión para ejecutar tableros en tiempo real que cualquier empleado
puede ver. Por ejemplo, los autores de este libro aprovechan la transmisión estructurada todos los días para ejecutar
Machine Translated by Google

paneles de informes de tiempo en Databricks (donde trabajan ambos autores de este libro). Usamos estos paneles para monitorear
el uso total de la plataforma, la carga del sistema, el tiempo de actividad e incluso el uso de nuevas funciones a medida que se
implementan, entre otras aplicaciones.

ETL incremental

Una de las aplicaciones de transmisión más comunes es reducir la latencia que las empresas deben soportar mientras recuperan
información en un almacén de datos; en resumen, "mi trabajo por lotes, pero transmisión".
Los trabajos por lotes de Spark a menudo se usan para cargas de trabajo de extracción, transformación y carga (ETL) que convierten los
datos sin procesar en un formato estructurado como Parquet para permitir consultas eficientes. Con el uso de transmisión estructurada,
estos trabajos pueden incorporar nuevos datos en segundos, lo que permite a los usuarios consultarlos más rápido en sentido descendente.
En este caso de uso, es fundamental que los datos se procesen exactamente una vez y con tolerancia a fallas: no queremos perder ningún
dato de entrada antes de que llegue al almacén, y no queremos cargar el mismo datos dos veces. Además, el sistema de transmisión
debe realizar actualizaciones en el almacén de datos de manera transaccional para no confundir las consultas que se
ejecutan en él con datos parcialmente escritos.

Actualizar datos para servir en tiempo real

Los sistemas de transmisión se utilizan con frecuencia para calcular datos que otra aplicación ofrece de forma interactiva. Por ejemplo,
un producto de análisis web como Google Analytics podría realizar un seguimiento continuo del número de visitas a cada página y utilizar
un sistema de transmisión para mantener estos recuentos actualizados. Cuando los usuarios interactúan con la interfaz de usuario
del producto, esta aplicación web consulta los últimos recuentos.
La compatibilidad con este caso de uso requiere que el sistema de transmisión pueda realizar actualizaciones incrementales en un
almacén de clave­valor (u otro sistema de servicio) como una sincronización y, a menudo, también que estas actualizaciones
sean transaccionales, como en el caso de ETL, para evitar dañar los datos en la aplicación.

Toma de decisiones en tiempo real

La toma de decisiones en tiempo real en un sistema de transmisión implica analizar nuevas entradas y responder a ellas automáticamente
utilizando la lógica empresarial. Un ejemplo de caso de uso sería un banco que desea verificar automáticamente si una nueva
transacción en la tarjeta de crédito de un cliente representa un fraude en función de su historial reciente y denegar la transacción
si se determina que el cargo es fraudulento. Esta decisión debe tomarse en tiempo real mientras se procesa cada transacción, de modo
que los desarrolladores puedan implementar esta lógica comercial en un sistema de transmisión y ejecutarla contra el flujo de
transacciones.
Es probable que este tipo de aplicación necesite mantener una cantidad significativa de estado sobre cada usuario para realizar un
seguimiento de sus patrones de gasto actuales y comparar automáticamente este estado con cada nueva transacción.

Aprendizaje automático en línea

Un derivado cercano del caso de uso de toma de decisiones en tiempo real es el aprendizaje automático en línea. En este escenario,
es posible que desee entrenar un modelo en una combinación de transmisión y datos históricos de varios usuarios. Un ejemplo podría ser
más sofisticado que el caso de uso de transacción de tarjeta de crédito mencionado anteriormente: en lugar de reaccionar con
reglas codificadas basadas en el comportamiento de un cliente, la empresa puede querer actualizar continuamente un modelo a partir del
comportamiento de todos los clientes y probar
Machine Translated by Google

cada transacción en su contra. Este es el caso de uso más desafiante del grupo para los sistemas de
procesamiento de flujo porque requiere agregación entre múltiples clientes, uniones contra conjuntos de datos
estáticos, integración con bibliotecas de aprendizaje automático y tiempos de respuesta de baja latencia.

Ventajas del procesamiento de flujo


Ahora que hemos visto algunos casos de uso para la transmisión, cristalicemos algunas de las ventajas del
procesamiento de transmisión. En su mayor parte, lote es mucho más simple de entender, solucionar problemas
y escribir aplicaciones para la mayoría de los casos de uso. Además, la capacidad de procesar datos por lotes
permite un rendimiento de procesamiento de datos mucho mayor que muchos sistemas de transmisión.
Sin embargo, el procesamiento de flujo es esencial en dos casos. Primero, el procesamiento de transmisión
permite una latencia más baja: cuando su aplicación necesita responder rápidamente (en una escala de tiempo de
minutos, segundos o milisegundos), necesitará un sistema de transmisión que pueda mantener el estado en la
memoria para obtener un rendimiento aceptable. Muchos de los casos de uso de toma de decisiones y alertas
que describimos caen dentro de este campo. En segundo lugar, el procesamiento de secuencias también puede
ser más eficaz para actualizar un resultado que los trabajos por lotes repetidos, ya que incrementa automáticamente
el cálculo. Por ejemplo, si queremos calcular estadísticas de tráfico web durante las últimas 24 horas, un trabajo por
lotes implementado de forma ingenua podría escanear todos los datos cada vez que se ejecuta, siempre procesando
los datos de 24 horas. Por el contrario, un sistema de transmisión puede recordar el estado del cálculo anterior y
solo contar los datos nuevos. Si le dice al sistema de transmisión que actualice su informe cada hora, por
ejemplo, solo necesitaría procesar 1 hora de datos cada vez (los nuevos datos desde el último informe). En un
sistema por lotes, tendría que implementar este tipo de cómputo incremental a mano para obtener el mismo
rendimiento, lo que generaría mucho trabajo adicional que el sistema de transmisión le brindará automáticamente
de manera inmediata.

Desafíos del procesamiento de flujo


Discutimos las motivaciones y las ventajas del procesamiento de transmisión, pero como probablemente sepa,
nunca hay un almuerzo gratis. Analicemos algunos de los desafíos de operar en flujos.

Para fundamentar este ejemplo, imaginemos que nuestra aplicación recibe mensajes de entrada de un sensor (por
ejemplo, dentro de un automóvil) que informan su valor en diferentes momentos. Entonces queremos buscar
dentro de este flujo ciertos valores, o ciertos patrones de valores. Un desafío específico es que los registros de
entrada pueden llegar a nuestra aplicación fuera de orden: debido a retrasos y retransmisiones, por ejemplo,
podemos recibir la siguiente secuencia de actualizaciones en orden, donde el campo de tiempo muestra la
hora en que el valor fue realmente medido:

{valor: 1, hora: "2017­04­07T00:00:00"} {valor:


2, hora: "2017­04­07T01:00:00"} {valor: 5, hora:
"2017­04­ 07T02:00:00"} {valor: 10, hora:
"2017­04­07T01:30:00"} {valor: 7, hora:
"2017­04­07T03:00:00"}

En cualquier sistema de procesamiento de datos, podemos construir lógica para realizar alguna acción basada en
recibir el valor único de "5". En un sistema de transmisión, también podemos responder a este evento individual
Machine Translated by Google

rápidamente. Sin embargo, las cosas se vuelven más complicadas si solo desea desencadenar alguna acción en función de una secuencia

específica de valores recibidos, por ejemplo, 2 luego 10 luego 5. En el caso del procesamiento por lotes, esto no es particularmente

difícil porque simplemente podemos ordenar todos los eventos que tenemos por campo de tiempo para ver que 10 llegaron entre 2 y 5. Sin

embargo, esto es más difícil para los sistemas de procesamiento de flujo. La razón es que el sistema de transmisión va a recibir

cada evento individualmente y necesitará rastrear algún estado a través de los eventos para recordar los eventos 2 y 5 y darse cuenta

de que el evento 10 estaba entre ellos. La necesidad de recordar tal estado sobre la corriente.

crea más desafíos. Por ejemplo, ¿qué sucede si tiene un volumen de datos masivo (por ejemplo, millones de flujos de sensores) y el estado en

sí es masivo? ¿Qué pasa si una máquina en el sistema falla y pierde algún estado? ¿Qué pasa si la carga está desequilibrada y una máquina

es lenta? ¿Y cómo puede su aplicación señalar a los consumidores intermedios cuando el análisis de algún evento está "terminado" (p. ej.,

el patrón 2­10­5 no ocurrió )? ¿Debería esperar una cantidad fija de tiempo o recordar algún estado indefinidamente? Todos estos desafíos y

otros, como hacer que la entrada y la salida del sistema sean transaccionales, pueden surgir cuando desee implementar una aplicación de

transmisión.

Para resumir, los desafíos que describimos en el párrafo anterior y un par de otros, son los siguientes:

Procesamiento de datos desordenados en función de las marcas de tiempo de la aplicación (también llamado tiempo de evento)

Mantener grandes cantidades de estado

Admite un alto rendimiento de datos

Procesar cada evento exactamente una vez a pesar de las fallas de la máquina

Manipulación de desequilibrios de carga y rezagados

Responder a eventos con baja latencia

Unión con datos externos en otros sistemas de almacenamiento

Determinar cómo actualizar los sumideros de salida a medida que llegan nuevos eventos

Escritura de datos de forma transaccional en sistemas de salida

Actualización de la lógica empresarial de su aplicación en tiempo de ejecución

Cada uno de estos temas es un área activa de investigación y desarrollo en sistemas de transmisión a gran escala. Para comprender

cómo los diferentes sistemas de transmisión han abordado estos desafíos, describimos algunos de los conceptos de diseño más

comunes que verá en ellos.

Puntos de diseño de procesamiento de flujo


Para respaldar los desafíos de procesamiento de transmisión que describimos, incluido el alto rendimiento, la baja latencia y los datos

desordenados, existen varias formas de diseñar un sistema de transmisión. Aquí describimos las opciones de diseño más comunes, antes de

describir las opciones de Spark en la siguiente sección.


Machine Translated by Google

API de registro a la vez frente a API declarativas


La forma más sencilla de diseñar una API de transmisión sería simplemente pasar cada evento a la aplicación y dejar que
reaccione usando un código personalizado. Este es el enfoque que implementaron muchos de los primeros sistemas de
transmisión, como Apache Storm, y tiene un lugar importante cuando las aplicaciones necesitan un control total sobre el
procesamiento de datos. La transmisión que proporciona este tipo de API de registro a la vez solo le brinda al usuario
una colección de "tuberías" para conectarse en una aplicación. Sin embargo, la desventaja de estos sistemas es
que la mayoría de los factores complicados que describimos anteriormente, como el mantenimiento del estado, se rigen
únicamente por la aplicación. Por ejemplo, con una API de registro a la vez, usted es responsable de rastrear el estado
durante períodos de tiempo más largos, descartarlo después de un tiempo para liberar espacio y responder de manera
diferente a los eventos duplicados después de una falla. Programar estos sistemas correctamente puede ser todo un
desafío. En esencia, las API de bajo nivel requieren una gran experiencia para desarrollarlas y mantenerlas.

Como resultado, muchos sistemas de transmisión más nuevos brindan API declarativas , donde su aplicación especifica
qué calcular pero no cómo calcularlo en respuesta a cada nuevo evento y cómo recuperarse de una falla. La API DStreams
original de Spark, por ejemplo, ofrecía una API funcional basada en operaciones como mapear, reducir y filtrar flujos.
Internamente, la API de DStream rastreó automáticamente la cantidad de datos que cada operador había procesado,
guardó cualquier estado relevante de manera confiable y recuperó el cálculo de fallas cuando fue necesario. Los
sistemas como Google Dataflow y Apache Kafka Streams proporcionan API funcionales similares. La transmisión
estructurada de Spark en realidad lleva este concepto aún más lejos, cambiando de operaciones funcionales a operaciones
relacionales (similares a SQL) que permiten una optimización automática aún más rica de la ejecución sin esfuerzo de
programación.

Tiempo de evento versus tiempo de procesamiento


Para los sistemas con API declarativas, una segunda preocupación es si el sistema admite el tiempo de eventos de forma
nativa. El tiempo del evento es la idea de procesar datos en función de las marcas de tiempo insertadas en cada
registro en el origen, en oposición al momento en que se recibe el registro en la aplicación de transmisión (que
se denomina tiempo de procesamiento ). En particular, cuando se utiliza la hora del evento, los registros pueden llegar al
sistema desordenados (por ejemplo, si viajaron de regreso por rutas de red diferentes) y diferentes fuentes también
pueden estar desincronizadas entre sí (algunos registros pueden llegar más tarde que otros). registros para la misma
hora del evento). Si su aplicación recopila datos de fuentes remotas que pueden retrasarse, como teléfonos móviles o
dispositivos IoT, el procesamiento de tiempo de eventos es crucial: sin él, perderá patrones importantes cuando algunos
datos se retrasen. Por el contrario, si su aplicación solo procesa eventos locales (p. ej., los generados en el mismo centro
de datos), es posible que no necesite un procesamiento de tiempo de evento sofisticado.

Cuando se usa el tiempo de eventos, varios problemas se vuelven preocupaciones comunes en todas las aplicaciones,
incluido el seguimiento del estado de una manera que permite que el sistema incorpore eventos tardíos y determinar
cuándo es seguro generar un resultado para una ventana de tiempo determinada en el tiempo del evento (es decir, cuando
es probable que el sistema haya recibido todas las entradas hasta ese momento). Debido a esto, muchos sistemas
declarativos, incluida la transmisión estructurada, tienen soporte "nativo" para el tiempo de eventos integrado en todos sus sistemas.
Machine Translated by Google

API, para que estas inquietudes puedan manejarse automáticamente en todo su programa.

Ejecución continua frente a microlotes


La decisión de diseño final que a menudo verá surgir es sobre la ejecución continua versus la ejecución de micro
lotes. En los sistemas basados en procesamiento continuo , cada nodo del sistema escucha continuamente
los mensajes de otros nodos y envía nuevas actualizaciones a sus nodos secundarios. Por ejemplo, suponga
que su aplicación implementa un cálculo de reducción de mapa en varios flujos de entrada. En un sistema de
procesamiento continuo, cada uno de los nodos que implementan el mapa leería los registros uno por uno de
una fuente de entrada, calcularía su función en ellos y los enviaría al reductor apropiado. El reductor luego
actualizaría su estado cada vez que obtenga un nuevo registro. La idea clave es que esto suceda en cada registro
individual, como se ilustra en la Figura 20­1.

Figura 20­1. Procesamiento continuo

El procesamiento continuo tiene la ventaja de ofrecer la latencia más baja posible cuando la tasa de entrada total
es relativamente baja, porque cada nodo responde inmediatamente a un nuevo mensaje.
Sin embargo, los sistemas de procesamiento continuo generalmente tienen un rendimiento máximo más bajo,
porque incurren en una cantidad significativa de sobrecarga por registro (por ejemplo, llamar al sistema operativo para
enviar un paquete a un nodo descendente). Además, los sistemas continuos generalmente tienen una topología fija
de operadores que no se pueden mover en tiempo de ejecución sin detener todo el sistema, lo que puede
generar problemas de equilibrio de carga.

Por el contrario, los sistemas de microlotes esperan para acumular pequeños lotes de datos de entrada (por
ejemplo, 500 ms) y luego procesan cada lote en paralelo usando una colección distribuida de tareas, similar a la
ejecución de un trabajo por lotes en Spark. Los sistemas de microlotes a menudo pueden lograr un alto rendimiento
por nodo porque aprovechan las mismas optimizaciones que los sistemas de lotes (por ejemplo,
procesamiento vectorizado) y no incurren en ninguna sobrecarga adicional por registro, como se ilustra en la Figura 20­2 .
Machine Translated by Google

Figura 20­2. Micro­lote

Por lo tanto, necesitan menos nodos para procesar la misma tasa de datos. Los sistemas de microlotes también
pueden usar técnicas de balanceo de carga dinámico para manejar cargas de trabajo cambiantes (p. ej., aumentar
o disminuir la cantidad de tareas). Sin embargo, la desventaja es una latencia base más alta debido a la
espera para acumular un microlote. En la práctica, las aplicaciones de transmisión que son lo suficientemente
grandes como para distribuir su computación tienden a priorizar el rendimiento, por lo que Spark ha
implementado tradicionalmente el procesamiento de microlotes. En la transmisión estructurada, sin
embargo, existe un esfuerzo de desarrollo activo para admitir también un modo de procesamiento continuo bajo la misma API.

Al elegir entre estos dos modos de ejecución, los factores principales que debe tener en cuenta son la latencia
deseada y el costo total de operación (TCO). Los sistemas de microlotes pueden ofrecer cómodamente latencias
de 100 ms a un segundo, según la aplicación. Dentro de este régimen, generalmente requerirán menos nodos para
lograr el mismo rendimiento y, por lo tanto, un menor costo operativo (incluido un menor costo de mantenimiento
debido a fallas de nodos menos frecuentes). Para latencias mucho más bajas, debe considerar un sistema
de procesamiento continuo o usar un sistema de microlotes junto con una capa de servicio rápido para proporcionar
consultas de baja latencia (p. ej., enviar datos a MySQL o Apache Cassandra, donde se pueden servir para
clientes en milisegundos).

API de transmisión de Spark


Cubrimos algunos enfoques de diseño de alto nivel para el procesamiento de transmisiones, pero hasta ahora
no hemos discutido las API de Spark en detalle. Spark incluye dos API de transmisión, como comentamos al
comienzo de este capítulo. La API DStream anterior en Spark Streaming está puramente orientada a microlotes.
Tiene una API declarativa (basada en funciones), pero no es compatible con la hora del evento. La nueva API
de transmisión estructurada agrega optimizaciones de alto nivel, tiempo de eventos y soporte para
procesamiento continuo.

La API DStream
La API DStream original de Spark se ha utilizado ampliamente para el procesamiento de secuencias desde su
primer lanzamiento en 2012. Por ejemplo, DStreams fue el motor de procesamiento más utilizado en la
encuesta de Datanami de 2016. Muchas empresas usan y operan Spark Streaming a escala en producción hoy en
día debido a su interfaz API de alto nivel y su semántica simple exactamente una vez. Las interacciones con el
código RDD, como las uniones con datos estáticos, también se admiten de forma nativa en Spark Streaming.
Operar Spark Streaming no es mucho más difícil que operar un clúster Spark normal. sin embargo, el
Machine Translated by Google

La API de DStreams tiene varias limitaciones. Primero, se basa únicamente en objetos y funciones de Java/
Python, a diferencia del concepto más rico de tablas estructuradas en DataFrames y Datasets. Esto limita la
oportunidad del motor para realizar optimizaciones. En segundo lugar, la API se basa únicamente en el tiempo
de procesamiento: para manejar operaciones de tiempo de eventos, las aplicaciones deben implementarlas por su
cuenta. Por último, DStreams solo puede funcionar en forma de microlotes y expone la duración de los microlotes
en algunas partes de su API, lo que dificulta la compatibilidad con modos de ejecución alternativos.

Transmisión estructurada
La transmisión estructurada es una API de transmisión de alto nivel construida desde cero sobre las API
estructuradas de Spark. Está disponible en todos los entornos donde se ejecuta el procesamiento
estructurado, incluidos Scala, Java, Python, R y SQL. Al igual que DStreams, es una API declarativa basada en
operaciones de alto nivel, pero al desarrollar el modelo de datos estructurados presentado en la parte anterior del
libro, el Streaming estructurado puede realizar más tipos de optimizaciones automáticamente.
Sin embargo, a diferencia de DStreams, Structured Streaming tiene soporte nativo para datos de tiempo de eventos
(todos sus operadores de ventanas lo admiten automáticamente). A partir de Apache Spark 2.2, el sistema solo se
ejecuta en un modelo de microlotes, pero el equipo de Spark en Databricks ha anunciado un esfuerzo llamado
Procesamiento continuo para agregar un modo de ejecución continua. Esto debería convertirse en una opción para
los usuarios de Spark 2.3.

Más fundamentalmente, más allá de simplificar el procesamiento de transmisiones, la transmisión estructurada


también está diseñada para facilitar la creación de aplicaciones continuas de un extremo a otro mediante Apache
Spark que combinan consultas interactivas, por lotes y de transmisión. Por ejemplo, Structured Streaming no usa
una API separada de DataFrames: simplemente escribe un cálculo normal de DataFrame (o SQL) y lo
inicia en una transmisión. La transmisión estructurada actualizará automáticamente el resultado de este cálculo de
forma incremental a medida que lleguen los datos. Esta es una gran ayuda al escribir aplicaciones de datos de un
extremo a otro: los desarrolladores no necesitan mantener una versión de transmisión separada de su código por
lotes, posiblemente para un sistema de ejecución diferente, y corren el riesgo de que estas dos versiones del código
no estén sincronizadas. . Como otro ejemplo, la transmisión estructurada puede enviar datos a sumideros estándar
utilizables por Spark SQL, como las tablas de Parquet, lo que facilita consultar el estado de su transmisión desde
otras aplicaciones de Spark. En futuras versiones de Apache Spark, esperamos que más y más componentes del
proyecto se integren con el Streaming estructurado, incluidos los algoritmos de aprendizaje en línea en MLlib.

En general, Structured Streaming está destinado a ser una evolución más fácil de usar y de mayor
rendimiento de la API DStream de Spark Streaming, por lo que nos centraremos únicamente en esta nueva API
en este libro. Muchos de los conceptos, como construir un cálculo a partir de un gráfico de transformaciones,
también se aplican a DStreams, pero dejamos la exposición de eso para otros libros.

Conclusión
Este capítulo cubrió los conceptos e ideas básicos que necesitará para comprender el procesamiento de flujo. Los
enfoques de diseño presentados en este capítulo deben aclarar cómo puede
Machine Translated by Google

evaluar los sistemas de transmisión para una aplicación determinada. También debe sentirse
cómodo al comprender las compensaciones que han hecho los autores de DStreams y Structured Streaming, y
por qué el soporte directo para los programas DataFrame es de gran ayuda cuando se usa Structured
Streaming: no hay necesidad de duplicar la lógica de su aplicación. En los próximos capítulos, nos
sumergiremos directamente en la transmisión estructurada para comprender cómo usarla.
Machine Translated by Google

Capítulo 21. Conceptos básicos de transmisión estructurada

Ahora que hemos cubierto una breve descripción general del procesamiento de transmisión, profundicemos directamente en
la transmisión estructurada. En este capítulo, nuevamente, estableceremos algunos de los conceptos clave detrás de
la transmisión estructurada y luego los aplicaremos con algunos ejemplos de código que muestran cuán fácil es el sistema.
usar.

Conceptos básicos de transmisión estructurada

La transmisión estructurada, como discutimos al final del Capítulo 20, es un marco de procesamiento de
transmisión basado en el motor Spark SQL. En lugar de introducir una API independiente, la transmisión estructurada
utiliza las API estructuradas existentes en Spark (marcos de datos, conjuntos de datos y SQL), lo que significa que se admiten
todas las operaciones con las que está familiarizado. Los usuarios expresan un cálculo de transmisión de la misma
manera que escribirían un cálculo por lotes en datos estáticos. Al especificar esto y especificar un destino de transmisión,
el motor de transmisión estructurada se encargará de ejecutar su consulta de forma incremental y continua a medida que
lleguen nuevos datos al sistema. Estas instrucciones lógicas para el cálculo luego se ejecutan usando el mismo motor
Catalyst discutido en la Parte II de este libro, incluida la optimización de consultas, la generación de código, etc.
Más allá del motor de procesamiento estructurado central, la transmisión estructurada incluye una serie de características
específicas para la transmisión. Por ejemplo, la transmisión estructurada garantiza un procesamiento de extremo
a extremo, exactamente una vez, así como tolerancia a fallas a través de puntos de control y registros de escritura anticipada.

La idea principal detrás de Structured Streaming es tratar un flujo de datos como una tabla a la que se agregan datos
continuamente. Luego, el trabajo verifica periódicamente si hay nuevos datos de entrada, los procesa, actualiza algún
estado interno ubicado en un almacén de estado si es necesario y actualiza su resultado. Una piedra angular de la API es
que no debería tener que cambiar el código de su consulta cuando realiza un procesamiento por lotes o de
transmisión; solo debe especificar si ejecutará esa consulta por lotes o por transmisión. Internamente, Structured Streaming
descubrirá automáticamente cómo "incrementar" su consulta, es decir, actualizar su resultado de manera eficiente cada
vez que lleguen nuevos datos, y lo ejecutará de manera tolerante a fallas.
Machine Translated by Google

Figura 21­1. Entrada de transmisión estructurada

En términos más simples, la transmisión estructurada es "su marco de datos, pero la transmisión". Esto hace que sea
muy fácil comenzar a usar aplicaciones de transmisión. ¡Probablemente ya tengas el código para ellos!
Sin embargo, existen algunos límites para los tipos de consultas que la transmisión estructurada podrá ejecutar, así
como algunos conceptos nuevos en los que debe pensar que son específicos de la transmisión, como la hora del
evento y los datos fuera de orden. Discutiremos esto en este capítulo y en los siguientes.

Finalmente, al integrarse con el resto de Spark, Structured Streaming permite a los usuarios crear lo que llamamos
aplicaciones continuas. Una aplicación continua es una aplicación integral que reacciona a los datos en tiempo real
mediante la combinación de una variedad de herramientas: trabajos de transmisión, trabajos por lotes, uniones
entre datos de transmisión y sin conexión, y consultas ad­hoc interactivas. Debido a que la mayoría de los trabajos
de transmisión en la actualidad se implementan en el contexto de una aplicación continua más grande, los
desarrolladores de Spark buscaron facilitar la especificación de toda la aplicación en un marco y obtener resultados
consistentes en estas diferentes partes. Por ejemplo, puede usar la transmisión estructurada para actualizar
continuamente una tabla que los usuarios consultan de forma interactiva con Spark SQL, servir un modelo de
aprendizaje automático entrenado por MLlib o unir transmisiones con datos sin conexión en cualquiera de las fuentes de
datos de Spark, aplicaciones que serían mucho más complejas. para construir utilizando una combinación de diferentes herramientas.

Conceptos básicos
Ahora que presentamos la idea de alto nivel, cubramos algunos de los conceptos importantes en un trabajo de
transmisión estructurada. Una cosa que con suerte encontrará es que no hay muchos. Eso es porque la transmisión
estructurada está diseñada para ser simple. Lea algunos otros libros de transmisión de big data y notará que
comienzan con la introducción de terminología como topologías de procesamiento de transmisión distribuida
para reductores de datos sesgados (una caricatura, pero precisa) y otras palabrerías complejas. El objetivo de Spark
es manejar estas preocupaciones automáticamente y brindar a los usuarios una forma simple de ejecutar cualquier
cálculo de Spark en una transmisión.

Transformaciones y Acciones
Machine Translated by Google

El Streaming Estructurado mantiene el mismo concepto de transformaciones y acciones que hemos visto a lo largo de este libro. Las

transformaciones disponibles en Streaming estructurado son, con algunas restricciones, exactamente las mismas transformaciones que vimos

en la Parte II. Las restricciones generalmente involucran algunos tipos de consultas que el motor aún no puede incrementar, aunque

algunas de las limitaciones se eliminan en las nuevas versiones de Spark. En general, solo hay una acción disponible en la transmisión

estructurada: la de iniciar una transmisión, que luego se ejecutará de forma continua y generará resultados.

Fuentes de entrada
La transmisión estructurada admite varias fuentes de entrada para leer en forma de transmisión. A partir de Spark 2.2, las fuentes de

entrada admitidas son las siguientes:

Apache Kafka 0.10

Archivos en un sistema de archivos distribuido como HDFS o S3 (Spark leerá continuamente archivos nuevos en un

directorio)

Una fuente de socket para probar

Hablaremos de esto en profundidad más adelante en este capítulo, pero vale la pena mencionar que los autores de Spark están

trabajando en una API fuente estable para que pueda crear sus propios conectores de transmisión.

Fregaderos

Así como las fuentes le permiten obtener datos en Streaming estructurado, los sumideros especifican el destino para el conjunto de

resultados de ese flujo. Los sumideros y el motor de ejecución también son responsables de realizar un seguimiento fiable del progreso

exacto del procesamiento de datos. Estos son los sumideros de salida admitidos a partir de Spark 2.2:

Apache Kafka 0.10

Casi cualquier formato de archivo

Un sumidero foreach para ejecutar cálculos arbitrarios en los registros de salida

Un fregadero de consola para probar

Un sumidero de memoria para la depuración

Discutimos esto con más detalle más adelante en el capítulo cuando discutimos las fuentes.

Modos de salida
Definir un sumidero para nuestro trabajo de transmisión estructurada es solo la mitad de la historia. También debemos definir cómo queremos

que Spark escriba datos en ese receptor. Por ejemplo, ¿solo queremos agregar nueva información? ¿Queremos actualizar las filas a

medida que recibamos más información sobre ellas con el tiempo (por ejemplo, actualizar el recuento de clics para una página web

determinada)? ¿Queremos sobrescribir completamente el


Machine Translated by Google

conjunto de resultados cada vez (es decir, escribir siempre un archivo con el recuento completo de clics para todas las páginas)?
Para hacer esto, definimos un modo de salida, similar a cómo definimos los modos de salida en las API estructuradas estáticas.

Los modos de salida admitidos son los siguientes:

Agregar (solo agregar nuevos registros al receptor de salida)

Actualizar (actualizar los registros modificados en su lugar)

Completar (reescribir la salida completa)

Un detalle importante es que ciertas consultas y ciertos sumideros solo admiten ciertos modos de salida, como veremos más adelante en

el libro. Por ejemplo, suponga que su trabajo es solo realizar un mapa en una corriente. Los datos de salida crecerán indefinidamente a

medida que lleguen nuevos registros, por lo que no tendría sentido utilizar el modo Completo, que requiere escribir todos los datos en un

nuevo archivo a la vez. Por el contrario, si está realizando una agregación en un número limitado de claves, los modos Completar y Actualizar

tendrían sentido, pero no Agregar, porque los valores de algunas claves deben actualizarse con el tiempo.

disparadores

Mientras que los modos de salida definen cómo se emiten los datos, los activadores definen cuándo se emiten los datos, es decir,

cuándo la transmisión estructurada debe buscar nuevos datos de entrada y actualizar su resultado. De forma predeterminada, la

transmisión estructurada buscará nuevos registros de entrada tan pronto como haya terminado de procesar el último grupo de datos de

entrada, lo que brinda la latencia más baja posible para los nuevos resultados. Sin embargo, este comportamiento puede llevar a escribir

muchos archivos de salida pequeños cuando el receptor es un conjunto de archivos. Por lo tanto, Spark también admite

disparadores basados en el tiempo de procesamiento (solo busca nuevos datos en un intervalo fijo). En el futuro, es posible que

también se admitan otros tipos de disparadores.

Procesamiento de tiempo de evento

La transmisión estructurada también es compatible con el procesamiento de tiempo de eventos (es decir, el procesamiento de datos en

función de las marcas de tiempo incluidas en el registro que pueden llegar fuera de orden). Hay dos ideas clave que deberá comprender
aquí por el momento; Hablaremos de ambos con mucha más profundidad en el próximo capítulo, así que no se preocupe si no los

tiene perfectamente claros en este punto.

Datos de tiempo de evento

Tiempo de evento significa campos de tiempo que están incrustados en sus datos. Esto significa que, en lugar de procesar los

datos según la hora en que llegan a su sistema, los procesa según la hora en que se generaron, incluso si los registros llegan desordenados

a la aplicación de transmisión debido a cargas lentas o retrasos en la red. Expresar el procesamiento de tiempo de eventos es simple en

Streaming estructurado.

Debido a que el sistema ve los datos de entrada como una tabla, la hora del evento es solo otro campo en esa tabla, y su aplicación

puede realizar agrupaciones, agregaciones y ventanas utilizando operadores SQL estándar. Sin embargo, bajo el capó, Structured

Streaming puede tomar algunas acciones especiales cuando


Machine Translated by Google

sabe que una de sus columnas es un campo de tiempo de evento, incluida la optimización de la ejecución de consultas o la
determinación de cuándo es seguro olvidar el estado de una ventana de tiempo. Muchas de estas acciones se pueden
controlar mediante marcas de agua.

marcas de agua

Las marcas de agua son una característica de los sistemas de transmisión que le permiten especificar qué tan tarde esperan ver
los datos en el tiempo del evento. Por ejemplo, en una aplicación que procesa registros desde dispositivos móviles, se podría
esperar que los registros se retrasen hasta 30 minutos debido a demoras en la carga. Los sistemas que admiten el tiempo
de eventos, incluida la transmisión estructurada, generalmente permiten establecer marcas de agua para limitar el tiempo que
necesitan para recordar datos antiguos. Las marcas de agua también se pueden usar para controlar cuándo generar un resultado
para una ventana de tiempo de evento en particular (por ejemplo, esperar hasta que haya pasado la marca de agua).

Transmisión estructurada en acción


Vayamos a un ejemplo aplicado de cómo puede usar la transmisión estructurada. Para nuestros ejemplos, vamos a trabajar
con el conjunto de datos de reconocimiento de actividad humana de heterogeneidad. Los datos consisten en lecturas de
sensores de teléfonos inteligentes y relojes inteligentes de una variedad de dispositivos, específicamente, el
acelerómetro y el giroscopio, muestreados a la frecuencia más alta posible admitida por los dispositivos. Las lecturas
de estos sensores se registraron mientras los usuarios realizaban actividades como andar en bicicleta, sentarse, pararse,
caminar, etc. Se utilizan varios teléfonos inteligentes y relojes inteligentes diferentes, y nueve usuarios en total.
Puedes descargar los datos aquí, en la carpeta de datos de actividad.

CONSEJO

Este conjunto de datos es bastante grande. Si es demasiado grande para su máquina, puede eliminar algunos de los
archivos y funcionará bien.

Leamos la versión estática del conjunto de datos como un DataFrame:

// en Scala val
static = spark.read.json("/datos/datos­de­actividad/") val dataSchema
= static.schema

# en Python
estático = chispa.read.json("/datos/datos­de­actividad/")
dataSchema = estático.esquema

Aquí está el esquema:

raíz
|­­ Arrival_Time: largo (anulable = verdadero)
|­­ Creation_Time: largo (anulable = verdadero)
Machine Translated by Google

|­­ Dispositivo: cadena (anulable = verdadero)


|­­ Índice: largo (anulable = verdadero)
|­­ Modelo: cadena (anulable = verdadero)
|­­ Usuario: cadena (anulable = verdadero) |­­
_corrupt_record: cadena (anulable = verdadero) |­­ gt: cadena
(anulable = verdadero) |­­ x: doble (anulable =
verdadero) |­­ y: doble (anulable = verdadero)
|­­ z: doble (anulable = verdadero)

Aquí hay una muestra del DataFrame:

+­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­+­­­­­­­­+­­­­­+­ ­­­­­+­­­­+­­­­­­­­+­­­­­+­­­­­
| Hora_de_llegada| | Hora_de_la_creación| Dispositivo|Índice| Modelo|Usuario|_c...ord|. gt| nulo|estar|­0... X

1424696634224|142469663222623685|nexus4_1| 62|nexo4| un|


...
|1424696660715|142469665872381726|nexus4_1| 2342|nexo4| un| nulo|estar|­0...
+­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­+­­­­­­­­+­­­­­+­ ­­­­­+­­­­+­­­­­­­­+­­­­­+­­­­­

Puede ver en el ejemplo anterior, que incluye una serie de columnas de marca de tiempo, modelos, usuario e información del

dispositivo. El campo gt especifica qué actividad estaba haciendo el usuario en ese momento.

A continuación, creemos una versión de transmisión del mismo conjunto de datos, que leerá cada archivo de entrada en el conjunto
de datos uno por uno como si fuera una transmisión.

Los marcos de datos de transmisión son en gran medida los mismos que los marcos de datos estáticos. Los creamos dentro de
las aplicaciones Spark y luego realizamos transformaciones en ellos para obtener nuestros datos en el formato correcto.
Básicamente, todas las transformaciones que están disponibles en las API estructuradas estáticas se aplican a Streaming
DataFrames. Sin embargo, una pequeña diferencia es que la transmisión estructurada no le permite realizar inferencias de
esquemas sin habilitarla explícitamente. Puede habilitar la inferencia de esquema para esto configurando la configuración

spark.sql.streaming.schemaInference en true. Dado ese hecho, leeremos el esquema de un archivo (que sabemos que tiene un
esquema válido) y pasaremos el objeto dataSchema de nuestro DataFrame estático a nuestro DataFrame de transmisión. Como

se mencionó, debe evitar hacer esto en un escenario de producción donde sus datos pueden (accidentalmente) cambiar debajo
de usted:

// en Scala val
streaming = spark.readStream.schema(dataSchema)
.option("maxFilesPerTrigger", 1).json("/datos/actividad­datos")

# en Python
streaming = spark.readStream.schema(dataSchema).option("maxFilesPerTrigger", 1)\
.json("/datos/datos­de­actividad")

NOTA

Discutimos maxFilesPerTrigger un poco más adelante en este capítulo, pero esencialmente le permite controlar la
rapidez con la que Spark leerá todos los archivos en la carpeta. Al especificar este valor más bajo, estamos
Machine Translated by Google

limitando artificialmente el flujo de la transmisión a un archivo por disparador. Esto nos ayuda a demostrar cómo la
transmisión estructurada se ejecuta de manera incremental en nuestro ejemplo, pero probablemente no sea algo que
usaría en producción.

Al igual que con otras API de Spark, la creación y ejecución de DataFrame en streaming es lenta. En
particular, ahora podemos especificar transformaciones en nuestro DataFrame de transmisión antes de
finalmente llamar a una acción para iniciar la transmisión. En este caso, mostraremos una transformación simple:
agruparemos y contaremos los datos por la columna gt, que es la actividad que realiza el usuario en ese momento:

// en Scala
val activityCounts = streaming.groupBy("gt").count()

# en Python
activityCounts = streaming.groupBy("gt").count()

Debido a que este código se está escribiendo en modo local en una máquina pequeña, estableceremos las
particiones aleatorias en un valor pequeño para evitar crear demasiadas particiones aleatorias:

chispa.conf.set("chispa.sql.shuffle.particiones", 5)

Ahora que configuramos nuestra transformación, solo necesitamos especificar nuestra acción para iniciar la consulta.
Como se mencionó anteriormente en el capítulo, especificaremos un destino de salida o un receptor de salida para
nuestro resultado de esta consulta. Para este ejemplo básico, vamos a escribir en un sumidero de memoria que
mantiene una tabla en memoria de los resultados.

En el proceso de especificar este sumidero, necesitaremos definir cómo Spark generará esos datos. En este
ejemplo, usamos el modo de salida completo . Este modo reescribe todas las claves junto con sus conteos después de
cada activación:

// en Scala
val actividadConsulta = actividadCuentas.writeStream.queryName("actividad_cuentas")
.format("memoria").outputMode("completo") .start()

# en Python
actividadConsulta = actividadCuentas.writeStream.queryName("actividad_cuentas")\
.format("memoria").outputMode("completo")
\ .start()

¡Ahora estamos escribiendo nuestra transmisión! Notará que establecemos un nombre de consulta único para
representar esta secuencia, en este caso, activity_counts. Especificamos nuestro formato como una tabla en memoria
y configuramos el modo de salida.

Cuando ejecutamos el código anterior, también queremos incluir la siguiente línea:


Machine Translated by Google

actividadConsulta.esperarTerminación()

Después de ejecutar este código, el cálculo de la transmisión habrá comenzado en segundo plano. El objeto de consulta
es un identificador de esa consulta de transmisión activa y debemos especificar que nos gustaría esperar a que finalice
la consulta mediante activityQuery.awaitTermination() para evitar que el proceso del controlador se cierre mientras la
consulta está activa. Omitiremos esto de nuestras futuras partes del libro para facilitar la lectura, pero debe incluirse en
sus aplicaciones de producción; de lo contrario, su transmisión no podrá ejecutarse.

Spark enumera esta secuencia y otras activas en las secuencias activas de nuestra SparkSession. Podemos ver una
lista de esos flujos ejecutando lo siguiente:

chispa.streams.activo

Spark también asigna a cada transmisión un UUID, por lo que, si es necesario, puede recorrer la lista de
transmisiones en ejecución y seleccionar la anterior. En este caso, lo asignamos a una variable, por lo que no es
necesario.

Ahora que esta transmisión se está ejecutando, podemos experimentar con los resultados consultando la tabla en
memoria que mantiene de la salida actual de nuestra agregación de transmisión. Esta tabla se llamará
activity_counts, igual que la secuencia. Para ver los datos actuales en esta tabla de salida, ¡simplemente necesitamos
consultarlos! Haremos esto en un bucle simple que imprimirá los resultados de la consulta de transmisión cada segundo:

// en Scala
para ( i <­ 1 a 5 )
{ chispa.sql("SELECCIONAR * DE recuentos_de_actividad").show()
Subproceso.dormir(1000)
}

# en Python
desde el tiempo de importación
del sueño para x en el
rango (5): chispa.sql ("SELECCIONAR * DE los recuentos de actividad").
mostrar () dormir (1)

A medida que se ejecutan las consultas anteriores, debería ver que los recuentos de cada actividad cambian con el
tiempo. Por ejemplo, la primera llamada muestra el siguiente resultado (porque lo consultamos mientras la transmisión
estaba leyendo el primer archivo):

+­­­+­­­­­+
| gt|contar|
+­­­+­­­­­+
+­­­+­­­­­+

La llamada de presentación anterior muestra el siguiente resultado: tenga en cuenta que el resultado probablemente
variará cuando ejecute este código personalmente porque probablemente lo iniciará en un momento diferente:
Machine Translated by Google

+­­­­­­­­­­+­­­­­­+
| gt|contar|
+­­­­­­­­­­+­­­­­­+
| sentarse| 8207|
...
nulo| 6966|
|| bicicleta| 7199|
+­­­­­­­­­­+­­­­­­+

Con este simple ejemplo, el poder de la transmisión estructurada debería quedar claro. Puede tomar las mismas operaciones que
usa por lotes y ejecutarlas en una secuencia de datos con muy pocos cambios de código (esencialmente, simplemente
especificando que es una secuencia). El resto de este capítulo trata sobre algunos de los detalles sobre las diversas
manipulaciones, fuentes y sumideros que puede usar con la transmisión estructurada.

Transformaciones en corrientes
Las transformaciones de transmisión, como mencionamos, incluyen casi todas las transformaciones estáticas
de DataFrame que ya vio en la Parte II. Se admiten todas las transformaciones de selección, filtro y simples, al igual que todas
las funciones de DataFrame y las manipulaciones de columnas individuales. Las limitaciones surgen en las transformaciones
que no tienen sentido en el contexto de la transmisión de datos. Por ejemplo, a partir de Apache Spark 2.2, los usuarios no

pueden ordenar secuencias que no están agregadas y no pueden realizar varios niveles de agregación sin usar el
procesamiento con estado (que se trata en el siguiente capítulo).
Estas limitaciones pueden eliminarse a medida que la transmisión estructurada continúa desarrollándose, por lo que le
recomendamos que consulte la documentación de su versión de Spark para obtener actualizaciones.

Selecciones y Filtrado
Todas las transformaciones de selección y filtro son compatibles con la transmisión estructurada, al igual que todas las funciones
de DataFrame y las manipulaciones de columnas individuales. Mostramos un ejemplo simple usando selecciones y filtrado a
continuación. En este caso, debido a que no estamos actualizando ninguna clave con el tiempo, usaremos el modo de
salida Agregar, para que los nuevos resultados se agreguen a la tabla de salida:

// en Scala
import org.apache.spark.sql.functions.expr val
simpleTransform = streaming.withColumn("escaleras", expr("gt como '%escaleras%'"))

.where("escaleras") .where("gt
no es nulo") .select("gt", "modelo", "hora_llegada",

"hora_creación") .writeStream .queryName("transformación_simple") .format("memoria ") .outputMode("agregar") .start()

# en Python
desde pyspark.sql.functions import expr
Machine Translated by Google

simpleTransform = streaming.withColumn("escaleras", expr("gt como '%escaleras%'"))\ .where("escaleras")


\ .where("gt no es nulo")
\ .select("gt", "modelo",
"hora_de_llegada", "hora_de_creación")\ .writeStream\ .queryName("simple_transform")
\ .format("memory")
\ .outputMode("append")\ .start()

Agregaciones
La transmisión estructurada tiene un excelente soporte para agregaciones. Puede especificar
agregaciones arbitrarias, como vio en las API estructuradas. Por ejemplo, puede usar una
agregación más exótica, como un cubo, en el modelo y la actividad del teléfono y las aceleraciones x, y,
z promedio de nuestro sensor (regrese al Capítulo 7 para ver posibles agregaciones en las que puede
ejecutar tu corriente):

// en Scala val
deviceModelStats = streaming.cube("gt",

"modelo").avg() .drop("avg(Llegada_hora)") .drop("avg(Creation_Time)") .drop("avg(Index )") .writeStream.queryName("device_counts").format("m

# en Python
deviceModelStats = streaming.cube("gt", "modelo").avg()\
.drop("avg(Llegada_hora)")
\ .drop("avg(Creation_Time)")
\ .drop("avg(Índice)")
\ .writeStream.queryName("dispositivo_recuentos").format("memoria")
\ .outputMode("completa")\ .start()

Consultar esa tabla nos permite ver los resultados:

SELECCIONE * DESDE device_counts

+­­­­­­­­­­+­­­­­­+­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­ ­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
| gt| modelo| promedio(x)| promedio(y)| promedio(z)|
+­­­­­­­­­­+­­­­­­+­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­ ­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
sentarse| nulo|­3.682775300344...|1.242033094787975...|­4.22021191297611...| soporte|
|| nulo|­4.415368069618...|­5.30657295890281...|2.264837548081631...|
...
| caminar|nexus4|­0.007342235359...|0.004341030525168...|­6.01620400184307...| |escalera abajo|nexus4|
0.0309175199508...|­0.02869185568293...| 0.11661923308518365|
...
+­­­­­­­­­­+­­­­­­+­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­ ­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
Machine Translated by Google

Además de estas agregaciones en columnas sin procesar en el conjunto de datos, la transmisión estructurada tiene soporte
especial para columnas que representan la hora del evento, incluido soporte de marcas de agua y ventanas. Discutiremos
esto con más detalle en el Capítulo 22.

NOTA

A partir de Spark 2.2, la única limitación de las agregaciones es que varias agregaciones
"encadenadas" (agregaciones en agregaciones de transmisión) no se admiten en este momento. Sin embargo, puede
lograr esto escribiendo en un receptor de datos intermedio, como Kafka o un receptor de archivos. Esto cambiará en
el futuro a medida que la comunidad de transmisión estructurada agregue esta funcionalidad.

Uniones

A partir de Apache Spark 2.2, la transmisión estructurada admite la unión de marcos de datos de transmisión a marcos
de datos estáticos. Spark 2.3 agregará la capacidad de unir múltiples transmisiones. Puede hacer uniones de varias
columnas y complementar los datos de transmisión con los de fuentes de datos estáticas:

// en Scala
val historicAgg = static.groupBy("gt", "model").avg() val
deviceModelStats = streaming.drop("Arrival_Time", "Creation_Time", "Index")
.cube("gt",
"modelo").avg() .join(historicalAgg, Seq("gt",
"modelo")) .writeStream.queryName("device_counts").format("memoria").outputMode( "completa") .start()

# en Python
historyAgg = static.groupBy("gt", "model").avg()
deviceModelStats = streaming.drop("Arrival_Time", "Creation_Time", "Index")\
.cube("gt", "modelo").avg()
\ .join(historicalAgg, ["gt", "modelo"])
\ .writeStream.queryName("device_counts").format("memoria")\ .
modo de salida("completo")
\ .start()

En Spark 2.2, no se admiten las uniones externas completas, las uniones izquierdas con la secuencia en el lado derecho
y las uniones derechas con la secuencia en el lado izquierdo. La transmisión estructurada tampoco admite uniones de
transmisión a transmisión, pero esta también es una función en desarrollo activo.

Entrada y salida
Esta sección profundiza en los detalles de cómo funcionan las fuentes, los sumideros y los modos de salida en la
transmisión estructurada. Específicamente, discutimos cómo, cuándo y dónde fluyen los datos hacia adentro y hacia
afuera del sistema. Al momento de escribir este artículo, Structured Streaming admite varias fuentes y
receptores, incluidos Apache Kafka, archivos y varias fuentes y receptores para pruebas y depuración. Es posible que
se agreguen más fuentes con el tiempo, así que asegúrese de consultar la documentación para obtener la información más actualizada.
Machine Translated by Google

información. Analizamos la fuente y el sumidero para un sistema de almacenamiento en particular juntos en este capítulo,
pero en realidad puede mezclarlos y combinarlos (por ejemplo, usar una fuente de entrada de Kafka con un sumidero de
archivos).

Dónde se leen y escriben los datos (fuentes y sumideros)


El streaming estructurado admite varias fuentes y sumideros de producción (archivos y Apache Kafka), así como algunas
herramientas de depuración como el sumidero de la tabla de memoria. Los mencionamos al comienzo del capítulo, pero ahora
vamos a cubrir los detalles de cada uno.

Fuente y sumidero de archivos

Probablemente la fuente más simple en la que pueda pensar es la fuente de archivo simple. Es fácil de razonar y entender. Si
bien esencialmente cualquier fuente de archivo debería funcionar, las que vemos en la práctica son Parquet, texto, JSON y
CSV.

La única diferencia entre usar la fuente/sumidero de archivos y la fuente de archivos estáticos de Spark es que con la
transmisión, podemos controlar la cantidad de archivos que leemos durante cada activación a través de la opción

maxFilesPerTrigger que vimos anteriormente.

Tenga en cuenta que cualquier archivo que agregue a un directorio de entrada para un trabajo de transmisión debe aparecer en
él de forma atómica. De lo contrario, Spark procesará los archivos parcialmente escritos antes de que haya terminado. En
sistemas de archivos que muestran escrituras parciales, como archivos locales o HDFS, esto se hace mejor escribiendo el
archivo en un directorio externo y moviéndolo al directorio de entrada cuando termine. En Amazon S3, los objetos normalmente
solo aparecen una vez que están completamente escritos.

Fuente y fregadero Kafka

Apache Kafka es un sistema distribuido de publicación y suscripción para flujos de datos. Kafka le permite publicar y suscribirse
a flujos de registros como lo haría con una cola de mensajes: estos se almacenan como flujos de registros con tolerancia a
fallas. Piense en Kafka como un búfer distribuido.
Kafka le permite almacenar secuencias de registros en categorías que se denominan temas. Cada registro en Kafka consta de
una clave, un valor y una marca de tiempo. Los temas consisten en secuencias inmutables de registros para los cuales la
posición de un registro en una secuencia se denomina desplazamiento . Leer datos se llama suscribirse a un tema y escribir
datos es tan simple como publicar en un tema.

Spark le permite leer desde Kafka con marcos de datos tanto por lotes como de transmisión.

A partir de Spark 2.2, Structured Streaming admite la versión 0.10 de Kafka. Es probable que esto también se amplíe en el
futuro, así que asegúrese de consultar la documentación para obtener más información sobre las versiones de Kafka
disponibles. Solo hay unas pocas opciones que debe especificar cuando lea de Kafka.

Lectura de la Fuente Kafka


Para leer, primero debe elegir una de las siguientes opciones: asignar, suscribirse o suscribirPatrón. Solo uno de

estos puede estar presente como una opción cuando vaya a leer desde
Machine Translated by Google

Kafka. Asignar es una forma detallada de especificar no solo el tema, sino también las particiones de temas de las que le
gustaría leer. Esto se especifica como una cadena JSON {"topicA": [0,1],"topicB":[2,4]}. subscribe y
subscribePattern son formas de suscribirse a uno o más temas, ya sea especificando una lista de temas (en el primero) o
mediante un patrón (a través del último).

En segundo lugar, deberá especificar kafka.bootstrap.servers que proporciona Kafka para conectarse al servicio.

Después de haber especificado sus opciones, tiene varias otras opciones para especificar:

Compensaciones iniciales y compensaciones finales

El punto de inicio cuando se inicia una consulta, ya sea más temprano, que es desde los primeros desplazamientos;
más reciente, que es solo de las últimas compensaciones; o una cadena JSON que especifica un desplazamiento inicial
para cada TopicPartition. En JSON, ­2 como compensación se puede usar para referirse a la más antigua, ­1 a la

más reciente. Por ejemplo, la especificación JSON podría ser {"temaA":


{"0":23,"1":­1},"temaB":{"0":­2}}. Esto se aplica solo cuando se inicia una nueva consulta de transmisión, y la reanudación
siempre se reanudará desde donde se detuvo la consulta. Las particiones recién descubiertas durante una
consulta se iniciarán lo antes posible. Los desplazamientos finales para una consulta determinada.

failOnDataLoss

Si falla la consulta cuando es posible que se pierdan datos (por ejemplo, los temas se eliminan o las
compensaciones están fuera de rango). Esto podría ser una falsa alarma. Puedes desactivarlo cuando no funcione como
esperabas. El defecto es cierto.

maxOffsetsPerTrigger

El número total de compensaciones para leer en un disparador dado.

También hay opciones para configurar los tiempos de espera del consumidor de Kafka, los reintentos de recuperación y los intervalos.

Para leer de Kafka, haga lo siguiente en Streaming estructurado:

// en Scala //
Suscríbete a 1 tema val ds1
= spark.readStream.format("kafka")
.option("kafka.bootstrap.servers",

"host1:puerto1,host2:puerto2") .option("suscribirse", "tema1") .load()


// Suscríbete a varios temas val ds2
= spark.readStream.format("kafka")
.option("kafka.bootstrap.servers",
"host1:puerto1,host2:puerto2") .option("suscribirse",
"tema1,tema2") .load()
// Suscríbete a un patrón de temas val ds3
= spark.readStream.format("kafka")
.option("kafka.bootstrap.servers",

"host1:puerto1,host2:puerto2") .option("subscribePattern", "tema.*") .load()


Machine Translated by Google

Python es bastante similar:

# en Python #
Suscríbete a 1 tema df1 =
spark.readStream.format("kafka")\
.option("kafka.bootstrap.servers", "host1:puerto1,host2:puerto2")
\ .option("suscribirse", "tema1")\ .load()

# Suscríbete a varios temas df2 =


spark.readStream.format("kafka")\
.option("kafka.bootstrap.servers", "host1:puerto1,host2:puerto2")
\ .option("suscribirse", "tema1,tema2")\ .load()

# Suscríbete a un patrón df3 =


spark.readStream.format("kafka")\
.option("kafka.bootstrap.servers", "host1:puerto1,host2:puerto2")
\ .option("subscribePattern", "tema.*")\ .load()

Cada fila en la fuente tendrá el siguiente esquema:

clave: binaria

valor: binario

tema: cadena

partición: int

desplazamiento: largo

marca de tiempo: larga

Es probable que cada mensaje en Kafka esté serializado de alguna manera. Al utilizar las funciones nativas de Spark en las API
estructuradas o una función definida por el usuario (UDF), puede analizar el mensaje en un análisis de formato más estructurado. Un
patrón común es usar JSON o Avro para leer y escribir en Kafka.

Escribiendo al fregadero de Kafka


Escribir en las consultas de Kafka es prácticamente lo mismo que leerlas, excepto por menos parámetros.
Aún deberá especificar los servidores de arranque de Kafka, pero la única otra opción que deberá proporcionar es una columna con la
especificación del tema o proporcionarla como una opción. Por ejemplo, las siguientes escrituras son equivalentes:

// en Scala
ds1.selectExpr("tema", "CAST(clave COMO CADENA)", "CAST(valor COMO CADENA)")

.writeStream.format("kafka") .option("checkpointLocation", "/to/HDFS­


compatible/dir") .option("kafka.bootstrap.servers", "host1:port1,host2:port2") .start
()
ds1.selectExpr("CAST(clave COMO CADENA)", "CAST(valor COMO CADENA)")
Machine Translated by Google

.writeStream.format("kafka") .option("kafka.bootstrap.servers",
"host1:port1,host2:port2") .option("checkpointLocation", "/to/HDFS­
compatible/dir")\ . opción("tema",
"tema1") .start()

# en Python
df1.selectExpr("tema", "CAST(clave COMO CADENA)", "CAST(valor COMO CADENA)")\

.writeStream\ .format("kafka")\ .option("kafka.bootstrap.servers",


"host1:puerto1,host2:puerto2")\ .option("checkpointLocation", "/to/
HDFS­compatible/dir" )\ .inicio()
df1.selectExpr("CAST(clave COMO CADENA)", "CAST(valor COMO
CADENA)")

\ .writeStream\ .format("kafka")\ .option("kafka.bootstrap.servers",


"host1:port1, host2:puerto2")\ .option("ubicación del punto de control",
"/a/compatible con HDFS/dir")
\ .option("tema", "tema1")\ .start()

Fregadero foreach

El sumidero foreach es similar a foreachPartitions en la API de conjunto de datos. Esta operación permite calcular
operaciones arbitrarias por partición, en paralelo. Inicialmente, está disponible en Scala y Java, pero es probable que se
transfiera a otros idiomas en el futuro. Para usar el sumidero foreach, debe implementar la interfaz ForeachWriter, que está
disponible en los documentos de Scala/Java, que contiene tres métodos: abrir, procesar y cerrar. Los métodos relevantes
se llamarán siempre que haya una secuencia de filas generadas como salida después de un disparador.

Aquí hay algunos detalles importantes:

El escritor debe ser serializable, como si fuera una UDF o una función de mapa de conjunto de datos.

Los tres métodos (abrir, procesar, cerrar) se llamarán en cada ejecutor.

El escritor debe hacer toda su inicialización, como abrir conexiones o iniciar transacciones solo en el método abierto.
Una fuente común de errores es que si la inicialización ocurre fuera del método abierto (por ejemplo, en la
clase que está usando), eso sucede en el controlador en lugar del ejecutor.

Debido a que el sumidero de Foreach ejecuta un código de usuario arbitrario, una cuestión clave que debe tener en cuenta al
usarlo es la tolerancia a fallas. Si Structured Streaming le pidió a su receptor que escribiera algunos datos, pero luego se
bloqueó, no puede saber si su escritura original tuvo éxito. Por lo tanto, la API proporciona algunos parámetros
adicionales para ayudarlo a lograr un procesamiento exactamente una vez.

Primero, la llamada abierta en su ForeachWriter recibe dos parámetros que identifican de forma única el conjunto de filas
sobre las que se debe actuar. El parámetro de versión es un Id. que aumenta de forma monótona y aumenta según el
activador, y el Idpartición es el Id. de la partición de la salida en
Machine Translated by Google

tu tarea. Su método abierto debería devolver si procesar este conjunto de filas. Si realiza un seguimiento externo de la salida de
su sumidero y ve que este conjunto de filas ya se emitió (por ejemplo, escribió la última versión y el ID de partición escrito en su

sistema de almacenamiento), puede devolver false desde abierto para omitir el procesamiento de este conjunto de filas. De lo

contrario, devuelve verdadero. Su ForeachWriter se abrirá nuevamente para que se escriba el valor de los datos de cada
disparador.

A continuación, se llamará al método de proceso para cada registro en los datos, suponiendo que su método abierto sea verdadero.

Esto es bastante sencillo: solo procese o escriba sus datos.

Finalmente, cada vez que se llama a open, también se llama al método close (a menos que el nodo se bloquee antes de

eso), independientemente de si open devolvió verdadero. Si Spark detectó un error durante el procesamiento, el método

de cierre recibe ese error. Es su responsabilidad limpiar cualquier recurso abierto durante el cierre.

En conjunto, la interfaz ForeachWriter le permite implementar su propio sumidero, incluida su propia lógica para rastrear qué datos
de disparadores se han escrito o sobrescribirlos de manera segura en caso de fallas. Mostramos un ejemplo de pasar un

ForeachWriter a continuación:

//en Scala
datasetOfString.write.foreach(new ForeachWriter[String] { def
open(partitionId: Long, version: Long): Boolean = {
// abrir una conexión a la base de datos

} def proceso(registro: Cadena) = { //


escribir cadena en la conexión

} def close(errorOrNull: Throwable): Unidad = {


// cerrar la conexion

} })

Fuentes y sumideros para pruebas

Spark también incluye varias fuentes y sumideros de prueba que puede usar para crear prototipos o depurar sus consultas de
transmisión (estos deben usarse solo durante el desarrollo y no en escenarios de producción, ya que no brindan tolerancia a
fallas de extremo a extremo para su aplicación):

Fuente de enchufe

La fuente de socket le permite enviar datos a sus Streams a través de sockets TCP. Para iniciar uno, especifique un host
y un puerto desde el que leer los datos. Spark abrirá una nueva conexión TCP para leer desde esa dirección. La fuente
de socket no debe usarse en producción porque el socket se encuentra en el controlador y no proporciona garantías de
tolerancia a fallas de un extremo a otro.

Aquí hay un breve ejemplo de cómo configurar esta fuente para leer desde localhost: 9999:

// en Scala
val socketDF = chispa.readStream.format("socket")
.option("host", "localhost").option("puerto", 9999).load()
Machine Translated by Google

# en Python
socketDF = spark.readStream.format("socket")
\ .option("host", "localhost").option("puerto", 9999).load()

Si realmente desea escribir datos en esta aplicación, deberá ejecutar un servidor que escuche en el puerto
9999. En sistemas similares a Unix, puede hacerlo usando la utilidad NetCat, que le permitirá escribir texto en el
primer conexión que se abre en el puerto 9999. Ejecute el siguiente comando antes de iniciar su aplicación Spark,
luego escriba en él:

nc­lk 9999

El origen del socket devolverá una tabla de cadenas de texto, una por línea en los datos de entrada.

Fregadero consola

El receptor de la consola le permite escribir parte de su consulta de transmisión en la consola. Esto es útil para la
depuración, pero no es tolerante a fallas. Escribir en la consola es simple y solo imprime algunas filas de su
consulta de transmisión en la consola. Esto es compatible con los modos de salida agregar y completar:

actividadCuentas.format("consola").write()

Sumidero de memoria

El sumidero de memoria es una fuente simple para probar su sistema de transmisión. Es similar al sumidero
de la consola, excepto que en lugar de imprimir en la consola, recopila los datos en el controlador y luego los
pone a disposición como una tabla en memoria que está disponible para consultas interactivas. Este sumidero
no es tolerante a fallas y no debe usarlo en producción, pero es excelente para probar y consultar su transmisión
durante el desarrollo. Esto es compatible con los modos de salida agregar y completar:

// en Scala
activityCounts.writeStream.format("memory").queryName("my_device_table")

Si desea generar datos en una tabla para consultas SQL interactivas en producción, los autores recomiendan usar
el receptor de archivos Parquet en un sistema de archivos distribuido (por ejemplo, S3). A continuación, puede consultar
los datos desde cualquier aplicación de Spark.

Cómo se envían los datos (modos de salida)


Ahora que sabe dónde pueden ir sus datos, analicemos cómo se verá el conjunto de datos resultante cuando llegue
allí. Esto es lo que llamamos el modo de salida. Como mencionamos, son el mismo concepto que los modos de guardado
en DataFrames estáticos. Hay tres modos compatibles con la transmisión estructurada.
Veamos cada uno de ellos.

Modo de adición
Machine Translated by Google

El modo Agregar es el comportamiento predeterminado y el más simple de entender. Cuando se agregan nuevas filas
a la tabla de resultados, se enviarán al sumidero en función del activador (explicado a continuación) que especifique.
Este modo garantiza que cada fila se emita una vez (y solo una vez), suponiendo que tenga un sumidero tolerante a
fallas. Cuando usa el modo agregar con tiempo de evento y marcas de agua (tratado en el Capítulo 22), solo el
resultado final se enviará al sumidero.

modo completo

El modo completo generará el estado completo de la tabla de resultados en su receptor de salida. Esto es útil cuando
está trabajando con algunos datos con estado para los cuales se espera que todas las filas cambien con el tiempo
o el receptor que está escribiendo no admite actualizaciones de nivel de fila. Piense en ello como el estado de una
secuencia en el momento en que se ejecutó el lote anterior.

Modo de actualizacion

El modo de actualización es similar al modo completo, excepto que solo las filas que son diferentes de la escritura
anterior se escriben en el receptor. Naturalmente, su sumidero debe admitir actualizaciones de nivel de fila para admitir
este modo. Si la consulta no contiene agregaciones, esto es equivalente al modo de adición.

¿Cuándo puedes usar cada modo?

La transmisión estructurada limita el uso de cada modo a las consultas donde tiene sentido. Por ejemplo, si
su consulta solo realiza una operación de mapa, la transmisión estructurada no permitirá el modo completo, ya que
esto requeriría recordar todos los registros de entrada desde el inicio del trabajo y reescribir toda la tabla de salida.
Este requisito se volverá prohibitivamente costoso a medida que se ejecuta el trabajo. Discutiremos cuándo se
admite cada modo con más detalle en el próximo capítulo, una vez que también cubramos el procesamiento de
tiempo de evento y las marcas de agua. Si el modo elegido no está disponible, Spark Streaming generará una
excepción cuando inicie su transmisión.

Aquí hay una tabla práctica de la documentación que explica todo esto. Tenga en cuenta que esto cambiará en el
futuro, por lo que querrá consultar la documentación para obtener la versión más actualizada.

La Tabla 21­1 muestra cuándo puede usar cada modo de salida.

Tabla 21­1. Modos de salida de transmisión estructurada a partir de Spark 2.2

Soportado
Tipo de consulta
Tipo de consulta Salida notas
(continuación)
Modos

El modo Agregar utiliza una marca de agua para


descartar el estado de agregación anterior. Esto significa que a
Agregación medida que se agregan nuevas filas a la tabla, Spark solo
Agregar,
en tiempo de evento mantendrá las filas que están debajo de la "marca de agua". El
Consultas con agregación Actualizar,
con modo de actualización también usa la marca de agua para eliminar
Completo
marca de agua el estado de agregación anterior. Por definición, el modo completo
no elimina el estado de agregación anterior, ya que este modo
conserva todos los datos en la tabla de resultados.
Machine Translated by Google

Dado que no se define ninguna marca de agua (solo se


Otro Completo, define en otra categoría), el estado de agregación
agregaciones Actualizar anterior no se elimina. El modo de adición no es
compatible, ya que los agregados pueden actualizarse, violando
así la semántica de este modo.

Consultas con
Actualizar
mapGroupsWithState

Adjuntar
Consultas con Se permiten agregaciones después de
operación flatMapGroupsWithState Adjuntar
modo flatMapGroupsWithState.

Modo de
Agregaciones no permitidas después
operación de Actualizar
de flatMapGroupsWithState.
actualización

Adjuntar, No se admite el modo completo, ya que no es factible mantener


Otras consultas
Actualizar todos los datos no agregados en la tabla de resultados.

Cuando se emiten datos (disparadores)


Para controlar cuándo se envían los datos a nuestro receptor, configuramos un disparador. De forma predeterminada, la transmisión
estructurada iniciará los datos tan pronto como el activador anterior complete el procesamiento. Puede usar activadores para
asegurarse de no sobrecargar su receptor de salida con demasiadas actualizaciones o para intentar controlar el tamaño de los archivos
en la salida. Actualmente, hay un tipo de disparador periódico, basado en el tiempo de procesamiento, así como un disparador "único"
para ejecutar manualmente un paso de procesamiento una vez. Es probable que se agreguen más disparadores en el futuro.

Disparador de tiempo de procesamiento

Para el disparador de tiempo de procesamiento, simplemente especificamos una duración como una cadena (también puede usar

una Duración en Scala o TimeUnit en Java). Mostraremos el formato de cadena a continuación.

// en Scala
importar org.apache.spark.sql.streaming.Trigger

actividadCounts.writeStream.trigger(Trigger.ProcessingTime("100 segundos"))
.format("consola").outputMode("completo").start()

# en Python
activityCounts.writeStream.trigger(processingTime='5 segundos')\
.format("consola").outputMode("completo").start()

El activador ProcessingTime esperará múltiplos de la duración dada para generar datos.


Por ejemplo, con una duración del activador de un minuto, el activador se activará a las 12:00, 12:01, 12:02, etc. Si se pierde un tiempo
de activación porque el procesamiento anterior aún no se ha completado, entonces Spark esperará hasta el siguiente punto de activación
(es decir, el siguiente minuto), en lugar de disparar inmediatamente.
Machine Translated by Google

después de que se complete el procesamiento anterior.

Una vez disparado

También puede ejecutar un trabajo de transmisión una vez configurándolo como desencadenante. Esto puede parecer un caso
extraño, pero en realidad es extremadamente útil tanto en desarrollo como en producción. Durante el desarrollo, puede
probar su aplicación solo con los datos de un disparador a la vez. Durante la producción, el disparador Una vez se puede usar para
ejecutar su trabajo manualmente a un ritmo bajo (por ejemplo, importar nuevos datos en una tabla de resumen ocasionalmente).
Debido a que la transmisión estructurada aún realiza un seguimiento completo de todos los archivos de entrada procesados y el
estado del cálculo, esto es más fácil que escribir su propia lógica personalizada para realizar un seguimiento de esto en un
trabajo por lotes y ahorra una gran cantidad de recursos en lugar de ejecutar un trabajo continuo las 24 horas del día, los 7 días de
la semana :

// en Scala
importar org.apache.spark.sql.streaming.Trigger

activityCounts.writeStream.trigger(Trigger.Once()) .format("console").outputMode("complete").start()

# en Python
activityCounts.writeStream.trigger(once=True)\
.format("consola").outputMode("completo").start()

API de conjuntos de datos de transmisión

Una última cosa a tener en cuenta sobre la transmisión estructurada es que no está limitado solo a la API de DataFrame
para la transmisión. También puede usar Conjuntos de datos para realizar el mismo cálculo pero con seguridad de tipos. Puede
convertir un marco de datos de transmisión en un conjunto de datos de la misma manera que lo hizo con uno estático. Como antes,
los elementos del conjunto de datos deben ser clases de casos de Scala o clases de bean de Java.

Aparte de eso, los operadores DataFrame y Dataset funcionan como lo hacían en una configuración estática y también se convertirán
en un plan de ejecución de transmisión cuando se ejecuten en una transmisión.

Aquí hay un ejemplo usando el mismo conjunto de datos que usamos en el Capítulo 11:

// en el caso
de Scala clase Vuelo(DEST_COUNTRY_NAME: String, ORIGIN_COUNTRY_NAME: String,
cuenta: BigInt)
val dataSchema = chispa.leer
.parquet("/data/flight­data/parquet/2010­summary.parquet/") .schema

val DF de vuelos = chispa.readStream.schema(dataSchema)


.parquet("/data/flight­data/parquet/2010­summary.parquet/") val vuelos =
vuelosDF.as[Vuelo] def
originIsDestination(vuelo_fila: Vuelo): Boolean = {
return fila_vuelo.ORIGIN_COUNTRY_NAME == fila_vuelo.DEST_COUNTRY_NAME

} vuelos.filter(flight_row => originIsDestination(flight_row))


.groupByKey(x =>
x.DEST_COUNTRY_NAME).count() .writeStream.queryName("device_counts").format("memory").outputMode("complete")
Machine Translated by Google

.comenzar()

Conclusión
Debe quedar claro que la transmisión estructurada presenta una forma poderosa de escribir aplicaciones de
transmisión. Tomar un trabajo por lotes que ya ejecuta y convertirlo en un trabajo de transmisión casi sin cambios de código
es simple y extremadamente útil desde el punto de vista de la ingeniería si necesita que este trabajo interactúe
estrechamente con el resto de su aplicación de procesamiento de datos.
El Capítulo 22 se sumerge en dos conceptos avanzados relacionados con la transmisión: procesamiento de tiempo de
evento y procesamiento con estado. Luego, después de eso, el Capítulo 23 aborda lo que debe hacer para ejecutar la
transmisión estructurada en producción.
Machine Translated by Google

Capítulo 22. Tiempo de evento y


procesamiento con estado

El Capítulo 21 cubrió los conceptos centrales y las API básicas; este capítulo se sumerge en el tiempo de eventos y el procesamiento

con estado. El procesamiento de tiempo de eventos es un tema candente porque analizamos la información con respecto al momento

en que se creó, no se procesó. La idea clave entre este estilo de procesamiento es que durante la vida útil del trabajo, Spark

mantendrá un estado relevante que puede actualizar durante el transcurso del trabajo antes de enviarlo al sumidero.

Analicemos estos conceptos con mayor detalle antes de comenzar a trabajar con el código para demostrar que funcionan.

Hora del evento


La hora del evento es un tema importante que se debe cubrir de forma discreta porque la API DStream de Spark no admite el

procesamiento de información con respecto a la hora del evento. En un nivel más alto, en los sistemas de procesamiento de

flujo hay efectivamente dos momentos relevantes para cada evento: el momento en que realmente ocurrió (tiempo del evento) y el

momento en que se procesó o llegó al sistema de procesamiento de flujo (tiempo de procesamiento).

Hora del evento

El tiempo del evento es el tiempo que está incrustado en los propios datos. La mayoría de las veces, aunque no se requiere que lo sea,

es el momento en que realmente ocurre un evento. Esto es importante porque proporciona una forma más robusta de comparar

eventos entre sí. El desafío aquí es que los datos de eventos pueden estar retrasados o desordenados. Esto significa que el sistema

de procesamiento de flujo debe poder manejar datos fuera de servicio o retrasados.

Tiempo de procesamiento

El tiempo de procesamiento es el momento en el que el sistema de procesamiento de flujo recibe realmente los datos.

Esto suele ser menos importante que el tiempo del evento porque cuando se procesa es en gran medida un detalle de

implementación. Esto nunca puede estar fuera de servicio porque es una propiedad del sistema de transmisión en un momento

determinado (no un sistema externo como el tiempo del evento).

Esas explicaciones son agradables y abstractas, así que usemos un ejemplo más tangible. Supongamos que tenemos un centro de datos

ubicado en San Francisco. Un evento ocurre en dos lugares al mismo tiempo: uno en Ecuador, el otro en Virginia (ver Figura 22­1).
Machine Translated by Google

Figura 22­1. Hora del evento en todo el mundo

Debido a la ubicación del centro de datos, es probable que el evento en Virginia aparezca en nuestro centro
de datos antes del evento en Ecuador. Si tuviéramos que analizar estos datos en función del tiempo de
procesamiento, parecería que el evento en Virginia ocurrió antes que el evento en Ecuador: algo que
sabemos que está mal. Sin embargo, si tuviéramos que analizar los datos según la hora del evento
(ignorando en gran medida la hora en que se procesa), veríamos que estos eventos ocurrieron al mismo tiempo.

Como mencionamos, la idea fundamental es que el orden de la serie de eventos en el sistema de


procesamiento no garantiza un orden en el tiempo de los eventos. Esto puede ser algo poco intuitivo,
pero vale la pena reforzarlo. Las redes informáticas no son fiables. Eso significa que los eventos pueden
eliminarse, ralentizarse, repetirse o enviarse sin problemas. Debido a que no se garantiza que los eventos
individuales sufran un destino u otro, debemos reconocer que pueden pasar muchas cosas a estos eventos
en el camino desde la fuente de información hasta nuestro sistema de procesamiento de flujo. Para esto
Machine Translated by Google

Por esta razón, necesitamos operar en el tiempo del evento y mirar el flujo general con referencia a esta información
contenida en los datos en lugar de cuándo llega al sistema. Esto significa que esperamos comparar eventos en función
del momento en que ocurrieron.

Procesamiento con estado


El otro tema que debemos cubrir en este capítulo es el procesamiento con estado. En realidad, ya demostramos esto
muchas veces en el Capítulo 21. El procesamiento con estado solo es necesario cuando necesita usar o actualizar
información intermedia (estado) durante períodos de tiempo más largos (ya sea en un enfoque de microlote o de registro
a la vez) . Esto puede suceder cuando usa el tiempo del evento o cuando realiza una agregación en una clave, ya sea que
involucre el tiempo del evento o no.

En su mayor parte, cuando realiza operaciones con estado. Spark maneja toda esta complejidad por usted. Por
ejemplo, cuando especifica una agrupación, Structured Streaming mantiene y actualiza la información por usted. Simplemente
especifique la lógica. Al realizar una operación con estado, Spark almacena la información intermedia en un almacén de
estado. La implementación del almacén de estado actual de Spark es un almacén de estado en memoria que se vuelve
tolerante a fallas al almacenar el estado intermedio en el directorio del punto de control.

Procesamiento con estado arbitrario


Las capacidades de procesamiento con estado descritas anteriormente son suficientes para resolver muchos
problemas de transmisión. Sin embargo, hay momentos en los que necesita un control detallado sobre qué estado debe
almacenarse, cómo se actualiza y cuándo debe eliminarse, ya sea explícitamente o mediante un tiempo de espera.
Esto se denomina procesamiento con estado arbitrario (o personalizado) y Spark le permite almacenar esencialmente
cualquier información que desee en el transcurso del procesamiento de una transmisión. Esto proporciona una gran
flexibilidad y potencia, y permite manejar con bastante facilidad cierta lógica empresarial compleja. Tal como lo hicimos
antes, vamos a fundamentar esto con algunos ejemplos:

Le gustaría registrar información sobre las sesiones de usuario en un sitio de comercio electrónico. Para

Por ejemplo, es posible que desee realizar un seguimiento de las páginas que visitan los usuarios en el
transcurso de esta sesión para proporcionar recomendaciones en tiempo real durante su próxima sesión.
Naturalmente, estas sesiones tienen tiempos de inicio y finalización completamente arbitrarios que son únicos para ese usuario.

A su empresa le gustaría informar sobre errores en la aplicación web, pero solo si ocurren cinco eventos
durante la sesión de un usuario. Podría hacer esto con ventanas basadas en conteo que solo emiten un resultado
si ocurren cinco eventos de algún tipo.

Le gustaría deduplicar registros con el tiempo. Para hacerlo, deberá realizar un seguimiento de cada registro
que vea antes de desduplicarlo.

Ahora que hemos explicado los conceptos básicos que vamos a necesitar en este capítulo, cubramos todo esto con algunos
ejemplos que puede seguir y expliquemos algunas de las advertencias importantes que debe tener en cuenta al procesar en
este manera.
Machine Translated by Google

Conceptos básicos de tiempo de evento

Comencemos con el mismo conjunto de datos del capítulo anterior. Cuando trabajamos con la hora del evento, es solo
otra columna en nuestro conjunto de datos, y eso es realmente todo lo que debemos preocuparnos; simplemente
usamos esa columna, como se demuestra aquí:

// en Scala
spark.conf.set("spark.sql.shuffle.partitions", 5) val static =
spark.read.json("/datos/actividad­datos") val streaming =

spark .readStream .schema(static .schema) .option("maxFilesPerTrigger", 10) .json("/datos/actividad­datos")

# en Python
spark.conf.set("spark.sql.shuffle.partitions", 5) static = spark.read.json("/
data/activity­data") streaming = spark\ .readStream\ .schema(static.
esquema)

\ .option("maxFilesPerTrigger",
10)\ .json("/datos/actividad­datos")

streaming.printSchema()

raíz
|­­ Arrival_Time: largo (anulable = verdadero)
|­­ Creation_Time: largo (anulable = verdadero)
|­­ Dispositivo: cadena (anulable = verdadero)
|­­ Índice: largo (anulable = verdadero)
|­­ Modelo: cadena (anulable = verdadero)
|­­ Usuario: cadena (anulable = verdadero) |­­
gt: cadena (anulable = verdadero) |­­ x: doble
(anulable = verdadero) |­­ y: doble (anulable
= verdadero) |­­ z: doble (anulable =
verdadero)

En este conjunto de datos, hay dos columnas basadas en el tiempo. La columna Creation_Time define cuándo se creó
un evento, mientras que Arrival_Time define cuándo un evento llegó a nuestros servidores en algún lugar anterior.
Usaremos Creation_Time en este capítulo. Este ejemplo se lee desde un archivo pero, como vimos en el capítulo
anterior, sería sencillo cambiarlo a Kafka si ya tiene un clúster en funcionamiento.

Windows en el tiempo del evento

El primer paso en el análisis de tiempo de evento es convertir la columna de marca de tiempo en el tipo de marca
de tiempo Spark SQL adecuado. Nuestra columna actual es nanosegundos unixtime (representados como largos),
Machine Translated by Google

por lo tanto, vamos a tener que hacer una pequeña manipulación para que tenga el formato adecuado:

// en Scala
val withEventTime = streaming.selectExpr(
"*",
"cast(cast(Creation_Time as double)/1000000000 as timestamp) as event_time")

# en Python
conEventTime = streaming\.selectExpr(
"*",
"cast(cast(Creation_Time as double)/1000000000 as timestamp) as event_time")

¡Ahora estamos preparados para realizar operaciones arbitrarias en el tiempo del evento! Tenga en cuenta que esta
experiencia es similar a la que haríamos en las operaciones por lotes: no hay una API o DSL especial. Simplemente
usamos columnas, tal como lo haríamos en lote, la agregación, y estamos trabajando con el tiempo del evento.

ventanas que caen


La operación más simple es simplemente contar el número de ocurrencias de un evento en una ventana dada. La
figura 22­2 muestra el proceso cuando se realiza una suma simple basada en los datos de entrada y una clave.

Figura 22­2. ventanas que caen

Estamos realizando una agregación de claves durante un período de tiempo. Actualizamos la tabla de resultados
(según el modo de salida) cuando se ejecuta cada disparador, que operará con los datos recibidos desde el último disparador.
En el caso de nuestro conjunto de datos real (y la Figura 22­2), lo haremos en ventanas de 10 minutos sin ninguna
superposición entre ellas (cada una y solo un evento puede caer en una ventana).
Machine Translated by Google

Esto también se actualizará en tiempo real, lo que significa que si se agregaran nuevos eventos en sentido ascendente a nuestro
sistema, la transmisión estructurada actualizaría esos recuentos en consecuencia. Este es el modo de salida completo, Spark
generará la tabla de resultados completa independientemente de si hemos visto el conjunto de datos completo:

// en Scala
import org.apache.spark.sql.functions.{window, col}
withEventTime.groupBy(window(col("event_time"), "10 minutes").count()

.writeStream .queryName("eventos_por_ventana") .format("memoria") .outputMode("completo") .start()

# en Python
desde la ventana de importación de pyspark.sql.functions , col
withEventTime.groupBy(window(col("event_time"), "10 minutes")).count()\

.writeStream\ .queryName("pyevents_per_window")
\ .format("memory")
\ .outputMode("complete")\ .start()

Ahora estamos escribiendo en el receptor en memoria para la depuración, por lo que podemos consultarlo con SQL después de que
tengamos la transmisión en ejecución:

chispa.sql("SELECCIONE * DE eventos_por_ventana").printSchema()

SELECCIONE * DESDE eventos_por_ventana

Esto nos muestra algo como el siguiente resultado, dependiendo de la cantidad de datos procesados cuando ejecutó la consulta:

+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+­­­ ­­+
|ventana |contar|
+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+­­­ ­­+
|[2015­02­23 10:40:00.0,2015­02­23 10:50:00.0]|11035| |[2015­02­24
11:50:00.0,2015­02­24 12:00:00.0]|18854|
...
|[2015­02­23 13:40:00.0,2015­02­23 13:50:00.0]|20870| |[2015­02­23
11:20:00.0,2015­02­23 11:30:00.0]|9392 |
+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+­­­ ­­+

Como referencia, aquí está el esquema que obtenemos de la consulta anterior:

raíz
|­­ ventana: estructura (anulable = falso) | |
|­­ inicio: marca de tiempo (anulable = verdadero) |­­
final: marca de tiempo (anulable = verdadero)
Machine Translated by Google

|­­ recuento: largo (anulable = falso)

Observe cómo la ventana es en realidad una estructura (un tipo complejo). Usando esto, podemos consultar esta estructura
para las horas de inicio y finalización de una ventana en particular.

Es importante el hecho de que también podemos realizar una agregación en varias columnas, incluida la columna de hora del
evento. Tal como vimos en el capítulo anterior, incluso podemos realizar estas agregaciones usando métodos como
cube. Si bien no repetiremos el hecho de que podemos realizar la agregación de múltiples claves a continuación, esto se
aplica a cualquier agregación de estilo de ventana (o cálculo con estado) que nos gustaría:

// en Scala
import org.apache.spark.sql.functions.{window, col}
withEventTime.groupBy(window(col("event_time"), "10 minutes"), "User").count()

.writeStream .queryName("eventos_por_ventana") .format("memoria") .outputMode("completo") .start()

# en Python
desde la ventana de importación de
pyspark.sql.functions , col withEventTime.groupBy(window(col("event_time"), "10 minutes"), "User").count()\

.writeStream\ .queryName("pyevents_per_window")\ .format("memory")\ .outputMode("complete")\ .start()

Ventanas correderas

El ejemplo anterior eran conteos simples en una ventana dada. Otro enfoque es que podemos desacoplar la ventana del
tiempo de inicio de la ventana. La figura 22­3 ilustra lo que
significar.
Machine Translated by Google

Figura 22­3. Ventanas corredizas

En la figura, estamos ejecutando una ventana deslizante a través de la cual observamos un incremento de una
hora, pero nos gustaría obtener el estado cada 10 minutos. Esto significa que actualizaremos los valores con el
tiempo e incluiremos las últimas horas de datos. En este ejemplo, tenemos ventanas de 10 minutos, comenzando
cada cinco minutos. Por lo tanto, cada evento se dividirá en dos ventanas diferentes. Puede modificar esto aún
más según sus necesidades:

// en Scala
import org.apache.spark.sql.functions.{window, col}
withEventTime.groupBy(window(col("event_time"), "10 minutes", "5 minutes"))

.count() .writeStream .queryName("eventos_por_ventana") .format("memoria") .outputMode("completo") .start()

# en Python
desde la ventana de importación de pyspark.sql.functions ,
col withEventTime.groupBy(window(col("event_time"), "10 minutes", "5 minutes"))\
.count()

\ .writeStream\ .queryName("pyevents_per_window")
\ .format("memory")
\ .outputMode("complete")\ .start()

Naturalmente, podemos consultar la tabla en memoria:


Machine Translated by Google

SELECCIONE * DESDE eventos_por_ventana

Esta consulta nos da el siguiente resultado. Tenga en cuenta que los tiempos de inicio de cada ventana ahora
están en intervalos de 5 minutos en lugar de 10, como vimos en la consulta anterior:

+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+­­­ ­­+
|ventana |contar|
+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+­­­ ­­+
|[2015­02­23 14:15:00.0,2015­02­23 14:25:00.0]|40375| |[2015­02­24
11:50:00.0,2015­02­24 12:00:00.0]|56549|
...
|[2015­02­24 11:45:00.0,2015­02­24 11:55:00.0]|51898| |[2015­02­23
10:40:00.0,2015­02­23 10:50:00.0]|33200|
+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+­­­ ­­+

Manejo de datos tardíos con marcas de agua


Los ejemplos anteriores son geniales, pero tienen un defecto. Nunca especificamos qué tan tarde esperamos ver
los datos. Esto significa que Spark necesitará almacenar esos datos intermedios para siempre porque nunca
especificamos una marca de agua o un momento en el que no esperamos ver más datos. Esto se aplica a todo
el procesamiento con estado que opera en tiempo de evento. Debemos especificar esta marca de agua para que
los datos superen la antigüedad en la transmisión (y, por lo tanto, en el estado) para que no abrumemos el sistema
durante un largo período de tiempo.

Concretamente, una marca de agua es una cantidad de tiempo después de un evento o conjunto de eventos
dado, después del cual no esperamos ver más datos de ese tiempo. Sabemos que esto puede suceder debido
a retrasos en la red, dispositivos que pierden la conexión o cualquier otro problema. En la API de DStreams,
no había una forma sólida de manejar los datos atrasados de esta manera: si ocurría un evento en un momento
determinado pero no llegaba al sistema de procesamiento cuando comenzaba el lote para una ventana
determinada, aparecía. en otros lotes de procesamiento. La transmisión estructurada soluciona esto. En tiempo
de eventos y procesamiento con estado, el estado o conjunto de datos de una ventana determinada se
desacopla de una ventana de procesamiento. Eso significa que a medida que ingresen más eventos, Structured
Streaming continuará actualizando una ventana con más información.

Volvamos a nuestro ejemplo de tiempo de evento del comienzo del capítulo, que se muestra ahora en la figura
22­4.
Machine Translated by Google

Figura 22­4. Marca de agua de tiempo de evento

En este ejemplo, imaginemos que con frecuencia vemos cierto retraso por parte de nuestros clientes en América Latina. Por
lo tanto, especificamos una marca de agua de 10 minutos. Al hacer esto, le indicamos a Spark que debe ignorar
cualquier evento que ocurra más de 10 minutos de "tiempo de evento" después de un evento anterior. Por el contrario,
esto también establece que esperamos ver todos los eventos en 10 minutos. Después de eso, Spark debería eliminar el estado
intermedio y, según el modo de salida, hacer algo con el resultado. Como se mencionó al comienzo del capítulo, necesitamos
especificar marcas de agua porque si no lo hiciéramos, necesitaríamos mantener todas nuestras ventanas para siempre,
esperando que se actualicen para siempre. Esto nos lleva a la pregunta central cuando se trabaja con el tiempo del
evento: "¿Qué tan tarde espero ver los datos?" La respuesta a esta pregunta será la marca de agua que configurará para
sus datos.

Volviendo a nuestro conjunto de datos, si sabemos que normalmente vemos los datos como producidos aguas abajo en
Machine Translated by Google

minutos pero hemos visto retrasos en eventos de hasta cinco horas después de que ocurren (quizás el usuario perdió la
conectividad del teléfono celular), especificaríamos la marca de agua de la siguiente manera:

// en Scala
importar org.apache.spark.sql.functions.{window, col} withEventTime

.withWatermark("event_time", "5
hours") .groupBy(window(col("event_time"), "10 minutes", "5

minutes")) .count() .writeStream .queryName("events_per_window") .format ("memoria") .outputMode("completo") .start()

# en Python
desde la ventana de importación de pyspark.sql.functions , col

withEventTime\ .withWatermark("event_time", "30 minutes")


\ .groupBy(window(col("event_time"), "10 minutes", "5 minutes") )\ .count()

\ .writeStream\ .queryName("pyevents_per_window")
\ .format("memory")
\ .outputMode("complete")\ .start()

Es bastante sorprendente, pero casi nada cambió en nuestra consulta. Básicamente, solo agregamos otra
configuración. Ahora, la transmisión estructurada esperará hasta 30 minutos después de la marca de tiempo
final de esta ventana móvil de 10 minutos antes de finalizar el resultado de esa ventana. Podemos consultar nuestra
tabla y ver los resultados intermedios porque estamos usando el modo completo: se actualizarán con el tiempo. En el
modo de adición, esta información no se mostrará hasta que se cierre la ventana.

SELECCIONE * DESDE eventos_por_ventana

+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+­­­ ­­+
|ventana |contar|
+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+­­­ ­­+
|[2015­02­23 14:15:00.0,2015­02­23 14:25:00.0]|9505 | |[2015­02­24
11:50:00.0,2015­02­24 12:00:00.0]|13159|
...
|[2015­02­24 11:45:00.0,2015­02­24 11:55:00.0]|12021| |[2015­02­23
10:40:00.0,2015­02­23 10:50:00.0]|7685 |
+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+­­­ ­­+

En este punto, realmente sabe todo lo que necesita saber sobre el manejo de datos atrasados. Spark hace todo el
trabajo pesado por ti. Solo para reforzar el punto, si no especifica qué tan tarde cree que verá los datos, Spark
mantendrá esos datos en la memoria para siempre. Especificar una marca de agua le permite liberar esos objetos de la
memoria, lo que permite que su transmisión continúe ejecutándose durante mucho tiempo.
Machine Translated by Google

tiempo.

Soltar duplicados en una transmisión


Una de las operaciones más difíciles en los sistemas de registro a la vez es eliminar los duplicados de la secuencia. Casi
por definición, debe operar en un lote de registros a la vez para encontrar duplicados; hay una gran sobrecarga de
coordinación en el sistema de procesamiento. La deduplicación es una herramienta importante en muchas aplicaciones,
especialmente cuando los sistemas ascendentes pueden entregar los mensajes varias veces. Un ejemplo perfecto de esto
son las aplicaciones de Internet de las cosas (IoT) que tienen productores ascendentes que generan mensajes en
entornos de red no estables, y el mismo mensaje puede terminar enviándose varias veces. Sus aplicaciones y
agregaciones descendentes deberían poder asumir que solo hay uno de cada mensaje.

Esencialmente, la transmisión estructurada facilita la toma de sistemas de mensajes que brindan semántica de al
menos una vez y los convierte en exactamente una vez eliminando mensajes duplicados a medida que ingresan, en
función de claves arbitrarias. Para eliminar los datos duplicados, Spark mantendrá una cantidad de claves
especificadas por el usuario y se asegurará de que se ignoren los duplicados.

ADVERTENCIA

Al igual que otras aplicaciones de procesamiento con estado, debe especificar una marca de agua para asegurarse
de que el estado mantenido no crezca infinitamente en el transcurso de su transmisión.

Comencemos el proceso de deduplicación. El objetivo aquí será eliminar la duplicación de la cantidad de eventos
por usuario mediante la eliminación de eventos duplicados. Observe cómo necesita especificar la columna de hora
del evento como una columna duplicada junto con la columna que debe eliminar los duplicados. La suposición
central es que los eventos duplicados tendrán la misma marca de tiempo y el mismo identificador. En este modelo,
las filas con dos marcas de tiempo diferentes son dos registros diferentes:

// en Scala
importar org.apache.spark.sql.functions.expr

withEventTime .withWatermark("evento_hora", "5


segundos") .dropDuplicates("Usuario",

"evento_hora") .groupBy("Usuario") .count() .writeStream .queryName("desduplicado") .format("memoria" ) .outputMode("com

# en Python
desde pyspark.sql.functions import expr
Machine Translated by Google

withEventTime\ .withWatermark("event_time", "5


segundos")\ .dropDuplicates(["Usuario",
"event_time"])

\ .groupBy("Usuario")\ .count()

\ .writeStream\ .queryName("pydeduplicated ")\ .format("memoria")\ .outputMode("completo")\ .start()

El resultado será similar al siguiente y continuará actualizándose con el tiempo a medida que su transmisión lea más datos:

+­­­­+­­­­­+
|Usuario|recuento|
+­­­­+­­­­­+
| un| 8085|
| b| 9123|
| c| 7715|
| g| 9167|
| h| 7733|
| mi| 9891|
| f| 9206|
| d| 8124|
| yo| 9255|
+­­­­+­­­­­+

Procesamiento con estado arbitrario


La primera sección de este capítulo demuestra cómo Spark mantiene la información y actualiza las ventanas según
nuestras especificaciones. Pero las cosas difieren cuando tienes conceptos más complejos de ventanas; aquí es donde entra en
juego el procesamiento con estado arbitrario. Esta sección incluye varios ejemplos de diferentes casos de uso junto con
ejemplos que le muestran cómo podría configurar su lógica empresarial. El procesamiento con estado solo está disponible
en Scala en Spark 2.2. Es probable que esto cambie en el futuro.

Al realizar un procesamiento con estado, es posible que desee hacer lo siguiente:

Crear ventana basada en recuentos de una clave determinada

Emite una alerta si hay una cantidad de eventos dentro de un período de tiempo determinado

Mantenga sesiones de usuario de una cantidad de tiempo indeterminada y guarde esas sesiones para realizar

algún análisis más adelante.

Al final del día, hay dos cosas que querrá hacer al realizar este estilo de procesamiento:
Machine Translated by Google

Asigne grupos en sus datos, opere en cada grupo de datos y genere como máximo una sola fila para cada grupo. La

API relevante para este caso de uso es mapGroupsWithState.

Asigne grupos en sus datos, opere en cada grupo de datos y genere una o más filas para cada grupo. La API relevante

para este caso de uso es flatMapGroupsWithState.

Cuando decimos "operar" en cada grupo de datos, eso significa que puede actualizar arbitrariamente cada grupo independientemente
de cualquier otro grupo de datos. Esto significa que puede definir tipos de ventanas arbitrarias que no se ajusten a las ventanas
giratorias o deslizantes como vimos anteriormente en el capítulo.
Un beneficio importante que obtenemos cuando realizamos este estilo de procesamiento es el control sobre la configuración
de los tiempos de espera en el estado. Con las ventanas y las marcas de agua, es muy simple: simplemente se agota el tiempo
de espera de una ventana cuando la marca de agua pasa el inicio de la ventana. Esto no se aplica al procesamiento con estado
arbitrario, porque el estado se administra en función de conceptos definidos por el usuario. Por lo tanto, debe desconectar
adecuadamente su estado. Hablemos de esto un poco más.

Tiempos de espera

Como se mencionó en el Capítulo 21, un tiempo de espera especifica cuánto tiempo debe esperar antes de que se agote algún
estado intermedio. Un tiempo de espera es un parámetro global en todos los grupos que se configura por grupo. Los tiempos de
espera pueden basarse en el tiempo de procesamiento (GroupStateTimeout.ProcessingTimeTimeout)

o en el tiempo del evento (GroupStateTimeout.EventTimeTimeout). Cuando utilice tiempos

de espera, verifique primero el tiempo de espera antes de procesar los valores. Puede obtener esta información comprobando

el indicador state.hasTimedOut o comprobando si el iterador de valores está vacío. Debe establecer algún estado (es decir, el estado
debe definirse, no eliminarse) para que se establezcan los tiempos de espera.

Con un tiempo de espera basado en el tiempo de procesamiento, puede establecer la duración del tiempo de espera

llamando a GroupState.setTimeoutDuration (veremos ejemplos de código de esto más adelante en esta sección del capítulo). El
tiempo muerto se producirá cuando el reloj haya avanzado la duración establecida. Las garantías que ofrece este time­out con una
duración de D ms son las siguientes:

El tiempo de espera nunca ocurrirá antes de que la hora del reloj haya avanzado D ms

El tiempo de espera se producirá eventualmente cuando haya un activador en la consulta (es decir, después de D ms).
Por lo tanto, no hay un límite superior estricto sobre cuándo ocurrirá el tiempo de espera. Por ejemplo, el intervalo de
activación de la consulta afectará cuando realmente se agote el tiempo de espera. Si no hay datos en la secuencia (para
cualquier grupo) durante un tiempo, no habrá ningún activador y la llamada a la función de tiempo de espera no se
producirá hasta que haya datos.

Dado que el tiempo de espera de procesamiento se basa en la hora del reloj, se ve afectado por las variaciones en el reloj del sistema.
Esto significa que los cambios de zona horaria y el sesgo del reloj son variables importantes a considerar.

Con un tiempo de espera basado en la hora del evento, el usuario también debe especificar la marca de agua de la hora del evento
en la consulta mediante marcas de agua. Cuando se establece, se filtran los datos anteriores a la marca de agua. Como
desarrollador, puede establecer la marca de tiempo a la que debe hacer referencia la marca de agua estableciendo un tiempo de espera
Machine Translated by Google

marca de tiempo usando la API GroupState.setTimeoutTimestamp(...). El tiempo de espera ocurriría cuando la marca de
agua avance más allá de la marca de tiempo establecida. Naturalmente, puede controlar el retraso del tiempo de espera
especificando marcas de agua más largas o simplemente actualizando el tiempo de espera a medida que procesa su
transmisión. Debido a que puede hacer esto en código arbitrario, puede hacerlo por grupo. La garantía proporcionada
por este tiempo de espera es que nunca ocurrirá antes de que la marca de agua haya excedido el tiempo de espera
establecido.

De manera similar a los tiempos de espera de tiempo de procesamiento, no hay un límite superior estricto en el retraso
cuando realmente ocurre el tiempo de espera. La marca de agua puede avanzar solo cuando hay datos en la
transmisión y la hora del evento de los datos realmente ha avanzado.

NOTA

Mencionamos esto hace unos momentos, pero vale la pena reforzarlo. Aunque los tiempos de espera son importantes,
es posible que no siempre funcionen como espera. Por ejemplo, al momento de escribir este artículo, Structured
Streaming no tiene ejecución de trabajo asincrónica, lo que significa que Spark no generará datos (o datos de tiempo
de espera) entre el momento en que finaliza una época y comienza la siguiente, porque no está procesando cualquier
dato en ese momento. Además, si un lote de procesamiento de datos no tiene registros (tenga en cuenta que se trata de
un lote, no de un grupo), no hay actualizaciones y no puede haber un tiempo de espera del evento. Esto podría cambiar
en futuras versiones.

Modos de salida
Un último problema cuando se trabaja con este tipo de procesamiento con estado arbitrario es el hecho de que no se
admiten todos los modos de salida discutidos en el Capítulo 21 . Es seguro que esto cambiará a medida que
Spark continúe cambiando, pero, a partir de este escrito, mapGroupsWithState solo admite el modo de salida de
actualización, mientras que flatMapGroupsWithState admite agregar y actualizar. el modo de adición significa que solo
después del tiempo de espera (lo que significa que la marca de agua ha pasado) aparecerán los datos en el conjunto de
resultados. Esto no sucede automáticamente, es su responsabilidad generar la fila adecuada
o filas.

Consulte la Tabla 21­1 para ver qué modos de salida se pueden usar cuando.

mapGroupsWithState
Nuestro primer ejemplo de procesamiento con estado utiliza una característica llamada mapGroupsWithState. Esto
es similar a una función de agregación definida por el usuario que toma como entrada un conjunto actualizado de datos
y luego lo resuelve en una clave específica con un conjunto de valores. Hay varias cosas que necesitará definir en el
camino:

Tres definiciones de clase: una definición de entrada, una definición de estado y, opcionalmente, una
definición de salida.

Una función para actualizar el estado en función de una clave, un iterador de eventos y un estado anterior.
Machine Translated by Google

Un parámetro de tiempo de espera (como se describe en la sección de tiempos de espera).

Con estos objetos y definiciones, puede controlar el estado arbitrario al crearlo, actualizarlo con el tiempo y eliminarlo.
Comencemos con un ejemplo de simplemente actualizar la clave en función de una cierta cantidad de estado, y luego pasar
a cosas más complejas como la sesionización.

Debido a que estamos trabajando con datos de sensores, busquemos la primera y la última marca de tiempo en que un
usuario determinado realizó una de las actividades en el conjunto de datos. Esto significa que la clave en la que
estaremos agrupando (y mapeando) es una combinación de usuario y actividad.

NOTA

Cuando usa mapGroupsWithState, la salida del sueño contendrá solo una fila por clave (o grupo) en todo
momento. Si desea que cada grupo tenga múltiples salidas, debe usar flatMapGroupsWithState
(que se cubre en breve).

Establezcamos las definiciones de entrada, estado y salida:

case class InputRow(usuario:String, timestamp:java.sql.Timestamp, actividad:String) case class


UserState(usuario:String, var actividad:String,
var start:java.sql.Timestamp,
var end:java.sql.Timestamp )

Para facilitar la lectura, configure la función que define cómo actualizará su estado en función de un determinado
fila:

def actualizarEstadoUsuarioConEvento(estado:EstadoUsuario, entrada:FilaEntrada):EstadoUsuario = {


if (Opción(entrada.marca de tiempo).isEmpty) {
estado de retorno

} if (estado.actividad == entrada.actividad) {

if (input.timestamp.after(state.end)) { state.end =
input.timestamp

} if (input.timestamp.before(state.start)) { state.start =
input.timestamp

} } else { if
(entrada.marca de tiempo.después(estado.fin))
{ estado.inicio = entrada.marca de
tiempo estado.fin = entrada.marca
de tiempo estado.actividad = entrada.actividad
}
}

estado
Machine Translated by Google

Ahora, escriba la función que define la forma en que se actualiza el estado en función de una época de filas:

import org.apache.spark.sql.streaming.{GroupStateTimeout, OutputMode, GroupState} def updateAcrossEvents(usuario:String,


entradas: Iterator[InputRow], oldState:
GroupState[UserState]):UserState =
{ var state:UserState = if ( oldState.exists) oldState.get else
UserState(usuario,
"",
nuevo java.sql.Timestamp(6284160000000L), nuevo
java.sql.Timestamp(6284160L)
)
// simplemente especificamos una fecha anterior con la que podemos comparar y // actualizar
inmediatamente en función de los valores de nuestros datos

para (entrada <­ entradas) {


estado = updateUserStateWithEvent(estado, entrada)
oldState.update(estado) } estado

Cuando tengamos eso, es hora de comenzar su consulta pasando la información relevante. Lo único que tendrá que agregar cuando

especifique mapGroupsWithState es si necesita agotar el tiempo de espera del estado de un grupo determinado. Esto solo le
brinda un mecanismo para controlar lo que se debe hacer con el estado que no recibe actualizaciones después de un cierto período
de tiempo. En este caso, desea mantener el estado indefinidamente, por lo tanto, especifique que Spark no debe exceder el tiempo de
espera.

Utilice el modo de salida de actualización para obtener actualizaciones sobre la actividad del usuario:

import org.apache.spark.sql.streaming.GroupStateTimeout

withEventTime .selectExpr("User as user",


"cast(Creation_Time/1000000000 as timestamp) as timestamp", "gt as

activity") .as[InputRow] .groupByKey(_ .user) .mapGroupsWithState(GroupStateTimeout.NoTimeout)

(updateAcrossEvents) .writeStream .queryName("events_per_window") .format("memory") .outputMode("update") .start()

SELECCIONE * DESDE events_per_window orden por usuario, comience

Aquí hay una muestra de nuestro conjunto de resultados:

+­­­­+­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­ ­­­­­­+
|usuario|actividad| empezar| fin|
Machine Translated by Google

+­­­­+­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­ ­­­­­­+
un| bicicleta|2015­02­23 13:30:...|2015­02­23 14:06:...|
|| un| bicicleta|2015­02­23 13:30:...|2015­02­23 14:06:...|
...
| d| bicicleta|2015­02­24 13:07:...|2015­02­24 13:42:...|
+­­­­+­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­ ­­­­­­+

Un aspecto interesante de nuestros datos es que la última actividad realizada en un momento dado es “bicicleta”.
Esto está relacionado con la forma en que probablemente se realizó el experimento, en el que cada participante realizó las
mismas actividades en orden.

EJEMPLO: VENTANAS BASADAS EN CONTEO

Las operaciones de ventana típicas se construyen a partir de las horas de inicio y finalización para las cuales todos los
eventos que se encuentran entre esos dos puntos contribuyen al conteo o suma que está realizando.
Sin embargo, hay ocasiones en las que, en lugar de crear ventanas basadas en el tiempo, prefiere crearlas en función
de una cantidad de eventos, independientemente del estado y las horas de los eventos, y realizar alguna agregación
en esa ventana de datos. Por ejemplo, es posible que queramos calcular un valor por cada 500 eventos recibidos,
independientemente de cuándo se reciban.

El siguiente ejemplo analiza el conjunto de datos de actividad de este capítulo y genera la lectura promedio de cada
dispositivo periódicamente, creando una ventana basada en el conteo de eventos y emitiéndola cada vez que acumula
500 eventos para ese dispositivo. Defina dos clases de casos para esta tarea: el formato de fila de entrada (que es
simplemente un dispositivo y una marca de tiempo); y las filas de estado y salida (que contienen el recuento actual de
registros recopilados, la identificación del dispositivo y una matriz de lecturas para los eventos en la ventana).

Aquí están nuestras diversas definiciones de clases de casos autodescriptivas:

case class InputRow (dispositivo: cadena, marca de tiempo: java.sql.Timestamp, x: doble)


case class DeviceState (dispositivo: cadena, valores de var : matriz [doble],
recuento de var : int)
case class OutputRow(dispositivo: Cadena, anteriorPromedio: Doble)

Ahora, puede definir la función para actualizar el estado individual en función de una sola fila de entrada.
Puede escribir esto en línea o de varias otras maneras, pero este ejemplo facilita ver exactamente cómo actualiza en
función de una fila determinada:

def updateWithEvent(state:DeviceState, input:InputRow):DeviceState = { state.count


+= 1 // mantener
una matriz de los valores del eje x state.values =
state.values ++ Array(input.x) state

Ahora es el momento de definir la función que se actualiza en una serie de filas de entrada. Observe en el ejemplo que
sigue que tenemos una clave específica, el iterador de entradas y el estado anterior, y
Machine Translated by Google

actualizamos ese estado anterior con el tiempo a medida que recibimos nuevos eventos. Esto, a su vez, devolverá
nuestras filas de salida con las actualizaciones en un nivel por dispositivo según la cantidad de conteos que ve.
Este caso es bastante sencillo, después de un número determinado de eventos, actualiza el estado y lo restablece.
A continuación, crea una fila de salida. Puede ver esta fila en la tabla de salida:

importar org.apache.spark.sql.streaming.{GroupStateTimeout, OutputMode,


estado del grupo}

def updateAcrossEvents(dispositivo:String, entradas: Iterador[InputRow], oldState:


GroupState[DeviceState]):Iterator[OutputRow] =
{ input.toSeq.sortBy(_.timestamp.getTime).toIterator.flatMap { input => val state = si
(estadoantiguo.existe) estadoantiguo.get else
DeviceState(dispositivo, Array(), 0)

val newState = actualizarConEvento(estado, entrada) if


(newState.count >= 500) {
// Una de nuestras ventanas está completa; reemplace nuestro estado con un //
DeviceState vacío y genere el promedio de los últimos 500 elementos del // estado
anterior
oldState.update(DeviceState(device, Array(), 0))
Iterador(FilaSalida(dispositivo,
nuevoEstado.valores.suma / nuevoEstado.valores.longitud.toDouble))

} else
{ // Actualizar el objeto DeviceState actual en su lugar y no generar // registros

oldState.update(newState)
iterador()

}}
}

Ahora puede ejecutar su transmisión. Notará que necesita indicar explícitamente el modo de salida, que es
append. También debe establecer un GroupStateTimeout. Este tiempo de espera especifica la cantidad de tiempo
que desea esperar antes de que una ventana se emita como completa (incluso si no alcanzó el conteo requerido).
En ese caso, establezca un tiempo de espera infinito, lo que significa que si un dispositivo nunca llega al umbral
requerido de 500 conteos, mantendrá ese estado para siempre como "incompleto" y no lo mostrará en la
tabla de resultados.

Al especificar ambos parámetros, puede pasar la función updateAcrossEvents e iniciar la transmisión:

importar org.apache.spark.sql.streaming.GroupStateTimeout

withEventTime .selectExpr("Dispositivo como dispositivo",


"cast(Creation_Time/1000000000 as timestamp) as timestamp",

"x") .as[InputRow] .groupByKey(_.device) .flatMapGroupsWithState(OutputMode.Append,


Machine Translated by Google

GroupStateTimeout.NoTimeout)(actualizaciónAcrossEvents)

.writeStream .queryName("dispositivo_basado_conteo") .format("memoria") .outputMode("agregar") .start()

Después de iniciar la transmisión, es hora de consultarla. Aquí están los resultados:

SELECCIONE * DESDE dispositivo_basado_conteo

+­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
| dispositivo| anteriorPromedio|
+­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
|nexo4_1| 4.660034012E­4| |nexus4_1|
0.001436279298199...|
...
|nexus4_1|1.049804683999999...| |
nexus4_1|­0.01837188737960...|
+­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+

Puede ver cómo cambian los valores en cada una de esas ventanas a medida que agrega nuevos datos al conjunto de resultados.

flatMapGroupsWithState
Nuestro segundo ejemplo de procesamiento con estado utilizará una característica llamada flatMapGroupsWithState.

Esto es bastante similar a mapGroupsWithState, excepto que en lugar de tener una sola clave con una sola salida como máximo, una sola

clave puede tener muchas salidas. Esto puede proporcionarnos un poco más de flexibilidad y la misma estructura fundamental que se

aplica mapGroupsWithState. Esto es lo que necesitaremos definir.

Tres definiciones de clase: una definición de entrada, una definición de estado y, opcionalmente, una definición de salida.

Una función para actualizar el estado en función de una clave, un iterador de eventos y un estado anterior.

Un parámetro de tiempo de espera (como se describe en la sección de tiempos de espera).

Con estos objetos y definiciones, podemos controlar el estado arbitrario al crearlo, actualizarlo con el tiempo y eliminarlo. Comencemos con un

ejemplo de sesionización.
Machine Translated by Google

EJEMPLO: SESIONIZACIÓN

Las sesiones son simplemente ventanas de tiempo no especificadas con una serie de eventos que ocurren. Por lo
general, desea registrar estos diferentes eventos en una matriz para comparar estas sesiones con otras sesiones
en el futuro. En una sesión, probablemente tendrá una lógica arbitraria para mantener y actualizar su estado a lo
largo del tiempo, así como ciertas acciones para definir cuándo finaliza el estado (como un conteo) o un tiempo de
espera simple. Construyamos sobre el ejemplo anterior y definámoslo un poco más estrictamente como una sesión.

A veces, es posible que tenga un ID de sesión explícito que puede usar en su función. Obviamente, esto lo
hace mucho más fácil porque puede realizar una agregación simple y es posible que ni siquiera necesite su
propia lógica con estado. En este caso, está creando sesiones sobre la marcha a partir de un ID de usuario y cierta
información de tiempo y si no ve ningún evento nuevo de ese usuario en cinco segundos, la sesión finaliza. También
notará que este código usa los tiempos de espera de manera diferente a como lo hacemos en otros ejemplos.

Puede seguir el mismo proceso de creación de sus clases, definiendo nuestra función de actualización de un solo
evento y luego la función de actualización de múltiples eventos:

case class InputRow(uid:String, timestamp:java.sql.Timestamp, x:Double, activity:String)


case class
UserSession(val uid:String, var timestamp:java.sql.Timestamp,
var actividades: Array[String], var valores: Array[Double])
case class UserSessionOutput(val uid:String, var actividades: Array[String], var xAvg:Double)

def actualizarConEvento(estado:SesiónUsuario, entrada:FilaEntrada):SesiónUsuario = {


// manejar fechas mal formadas
if (Option(input.timestamp).isEmpty) {
estado de retorno
}

estado.marca de tiempo = entrada.marca


de tiempo estado.valores = estado.valores ++
Matriz(entrada.x) if (!estado.actividades.contiene(entrada.actividad)) {
estado.actividades = estado.actividades ++ Array(entrada.actividad) }

estado
}

importar org.apache.spark.sql.streaming.{GroupStateTimeout, OutputMode,


estado del grupo}

def updateAcrossEvents(uid:String,
entradas: Iterador[InputRow],
oldState: GroupState[UserSession]):Iterator[UserSessionOutput] = {

entradas.toSeq.sortBy(_.timestamp.getTime).toIterator.flatMap { input => val state = if


(oldState.exists) oldState.get else UserSession(
Machine Translated by Google

uid,
new java.sql.Timestamp(6284160000000L),
Array(),
Array())
val newState = updateWithEvent(state, input)

if (oldState.hasTimedOut) { val
state = oldState.get
oldState.remove()
Iterator(UserSessionOutput(uid,
state.activities,
newState.values.sum / newState.values.length.toDouble)) } else if
(state.values.length > 1000) { val state =
oldState.get oldState.remove()

Iterator(UserSessionOutput(uid,
state.activities,
newState.values.sum / newState.values.length.toDouble)) } else

{ oldState.update(newState)
oldState.setTimeoutTimestamp(newState.timestamp.getTime(), "5 segundos" )
iterador()
}

}
}

Verá en este que solo esperamos ver un evento como máximo con cinco segundos de retraso. Cualquier otra
cosa que no sea eso y lo ignoraremos. Usaremos un EventTimeTimeout para configurar el tiempo de espera
que queremos en función del tiempo del evento en esta operación con estado:

importar org.apache.spark.sql.streaming.GroupStateTimeout

withEventTime.where("x no es
nulo") .selectExpr("usuario como uid",
"cast(Creation_Time/1000000000 como marca de tiempo) como marca
de tiempo", "x", "gt como

actividad") .as[InputRow] .withWatermark("marca


de tiempo", "5
segundos") .groupByKey(_.uid) .flatMapGroupsWithState( OutputMode.Append,
GroupStateTimeout.EventTimeTimeout)(actualizar entre eventos)

.writeStream .queryName("dispositivo_basado_conteo") .format("memoria") .start()

Consultar esta tabla le mostrará las filas de salida para cada usuario durante este período de tiempo:

SELECCIONE * DESDE dispositivo_basado_conteo


Machine Translated by Google

+­­­+­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­+
|uido| actividades| xPromedio|

+­­­+­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­+
| un| [de pie, nulo, sentado]|­9.10908533566433...| | un|
[sentarse, nulo, caminar]|­0.00654280428601...|
...
| c|[nulo, escaleras abajo...|­0.03286657789999995|
+­­­+­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­+

Como es de esperar, las sesiones que tienen varias actividades tienen un valor de giroscopio del
eje x más alto que las que tienen menos actividades. También debería ser trivial extender
este ejemplo a conjuntos de problemas más relevantes para su propio dominio.

Conclusión
Este capítulo cubrió algunos de los temas más avanzados en el flujo estructurado, incluido el tiempo del
evento y el procesamiento con estado. Esta es efectivamente la guía del usuario para ayudarlo a
desarrollar la lógica de su aplicación y convertirla en algo que proporcione valor. A continuación,
analizaremos lo que debemos hacer para llevar esta aplicación a producción y mantenerla y actualizarla con el tiempo.
Machine Translated by Google

Capítulo 23. Streaming estructurado


en producción

Los capítulos anteriores de esta parte del libro han cubierto la transmisión estructurada desde la perspectiva del
usuario. Naturalmente, este es el núcleo de su aplicación. Este capítulo cubre algunas de las herramientas
operativas necesarias para ejecutar Structured Streaming de manera sólida en producción después de
haber desarrollado una aplicación.

La transmisión estructurada se marcó como lista para producción en Apache Spark 2.2.0, lo que significa que esta
versión tiene todas las funciones necesarias para el uso en producción y estabiliza la API. Muchas
organizaciones ya están usando el sistema en producción porque, francamente, no es muy diferente de ejecutar
otras aplicaciones Spark de producción. De hecho, a través de características tales como fuentes/sumideros
transaccionales y procesamiento exactamente una vez, los diseñadores de Structured Streaming buscaron que sea
lo más fácil de operar posible. Este capítulo lo guiará a través de algunas de las tareas operativas clave
específicas de la transmisión estructurada. Esto debería complementar todo lo que vimos y aprendimos sobre las
operaciones de Spark en la Parte II.

Tolerancia a fallas y puntos de control


La preocupación operativa más importante para una aplicación de transmisión es la recuperación de fallas. Las
fallas son inevitables: perderá una máquina en el clúster, un esquema cambiará por accidente sin una
migración adecuada o incluso puede reiniciar intencionalmente el clúster o la aplicación. En cualquiera de estos
casos, el Streaming Estructurado te permite recuperar una aplicación con solo reiniciarla. Para hacer esto, debe
configurar la aplicación para usar registros de puntos de control y escritura anticipada, los cuales son manejados
automáticamente por el motor. Específicamente, debe configurar una consulta para escribir en una ubicación de
punto de control en un sistema de archivos confiable (por ejemplo, HDFS, S3 o cualquier sistema de
archivos compatible). La transmisión estructurada guardará periódicamente toda la información de progreso
relevante (por ejemplo, el rango de compensaciones procesadas en un disparador dado), así como los valores de
estado intermedio actuales en la ubicación del punto de control. En un escenario de falla, simplemente
necesita reiniciar su aplicación, asegurándose de apuntar a la misma ubicación del punto de control, y
automáticamente recuperará su estado y comenzará a procesar los datos donde los dejó. No es necesario que
administre manualmente este estado en nombre de la aplicación: la transmisión estructurada lo hace por usted.

Para usar puntos de control, especifique la ubicación de su punto de control antes de iniciar su aplicación a través
de la opción checkpointLocation en writeStream. Puedes hacer esto de la siguiente manera:

// en Scala
val static = chispa.read.json("/datos/actividad­datos") val
streaming =

chispa .readStream .schema(static.schema) .option("maxFilesPerTrigger", 10)


Machine Translated by Google

.json("/datos/datos­de­

actividad") .groupBy("gt") .count()


val query =

streaming .writeStream .outputMode("complete") .option("checkpointLocation", "/alguna/ubicación/" ) .queryName("test_stre

# en Python
estático = chispa.read.json("/datos/actividad­datos")
streaming =

chispa\ .readStream\ .schema(static.schema)


\ .option("maxFilesPerTrigger",
10)\ .json("/ datos/
datos de
actividad")
\ .groupBy("gt")
\ .count() consulta =
streaming\ .writeStream\ .outputMode("complete")
\ .option("checkpointLocation", "/alguna/
python/ubicación/ ")
\ .queryName("test_python_stream")\ .format("memoria")\ .start ()

Si pierde su directorio de puntos de control o la información que contiene, su aplicación no podrá recuperarse
de las fallas y tendrá que reiniciar su transmisión desde cero.

Actualización de su aplicación
Los puntos de control son probablemente lo más importante que debe habilitarse para ejecutar sus aplicaciones
en producción. Esto se debe a que el punto de control almacenará toda la información sobre lo que su
transmisión ha procesado hasta el momento y cuál es el estado intermedio que puede estar almacenando.
Sin embargo, los puntos de control vienen con un pequeño inconveniente: tendrás que razonar sobre los
datos de tus puntos de control anteriores cuando actualices tu aplicación de transmisión. Cuando actualice su
aplicación, tendrá que asegurarse de que su actualización no sea un cambio importante. Abordemos esto en
detalle cuando revisemos los dos tipos de actualizaciones: una actualización del código de su aplicación o
la ejecución de una nueva versión de Spark.

Actualización de su código de aplicación de transmisión


La transmisión estructurada está diseñada para permitir ciertos tipos de cambios en el código de la
aplicación entre los reinicios de la aplicación. Lo que es más importante, puede cambiar las funciones definidas
por el usuario (UDF) siempre que tengan el mismo tipo de firma. Esta característica puede ser muy útil para
corregir errores. Por ejemplo, imagine que su aplicación comienza a recibir un nuevo tipo de datos y uno de
Machine Translated by Google

las funciones de análisis de datos en su lógica actual fallan. Con la transmisión estructurada, puede volver a compilar la
aplicación con una nueva versión de esa función y retomarla en el mismo punto de la transmisión donde se bloqueó anteriormente.

Si bien los pequeños ajustes, como agregar una nueva columna o cambiar una UDF, no rompen los cambios y no requieren un
nuevo directorio de puntos de control, hay cambios más grandes que requieren un directorio de puntos de control completamente
nuevo. Por ejemplo, si actualiza su aplicación de transmisión para agregar una nueva clave de agregación o cambia
fundamentalmente la consulta en sí, Spark no puede construir el estado requerido para la nueva consulta desde un directorio
de punto de control antiguo. En estos casos, la transmisión estructurada arrojará una excepción que indica que no puede
comenzar desde un directorio de punto de control y que debe comenzar desde cero con un directorio nuevo (vacío) como
ubicación de su punto de control.

Actualización de su versión de Spark


Las aplicaciones de transmisión estructurada deberían poder reiniciarse desde un directorio de punto de control anterior a
través de actualizaciones de versión de parche a Spark (por ejemplo, pasar de Spark 2.2.0 a 2.2.1 a 2.2.2). El formato del
punto de control está diseñado para ser compatible con versiones posteriores, por lo que la única forma en que se puede romper
es debido a la corrección de errores críticos. Si una versión de Spark no puede recuperarse de los puntos de control anteriores,
esto se documentará claramente en sus notas de la versión. Los desarrolladores de Structured Streaming también tienen
como objetivo mantener la compatibilidad del formato en las actualizaciones de versiones menores (por ejemplo, Spark 2.2.x a
2.3.x), pero debe consultar las notas de la versión para ver si esto es compatible con cada actualización. En cualquier caso, si
no puede comenzar desde un punto de control, deberá iniciar su aplicación nuevamente usando un nuevo directorio de
puntos de control.

Dimensionamiento y cambio de escala de su aplicación


En general, el tamaño de su clúster debería poder manejar cómodamente ráfagas por encima de su velocidad de datos. Las
métricas clave que debe monitorear en su aplicación y clúster se describen a continuación. En general, si ve que su tasa de
entrada es mucho más alta que su tasa de procesamiento (desarrollada momentáneamente), es hora de escalar su
clúster o aplicación. Dependiendo de su administrador de recursos y su implementación, es posible que solo pueda agregar
ejecutores dinámicamente a su aplicación. Cuando llegue el momento, puede reducir su aplicación de la misma manera:
elimine los ejecutores (posiblemente a través de su proveedor de nube) o reinicie su aplicación con menos recursos. Es
probable que estos cambios generen algún retraso en el procesamiento (ya que los datos se vuelven a calcular o las particiones
se mezclan cuando se eliminan los ejecutores). Al final, es una decisión comercial si vale la pena crear un sistema con
capacidades de administración de recursos más sofisticadas.

Si bien a veces es necesario realizar cambios en la infraestructura subyacente del clúster o la aplicación, otras veces un
cambio puede requerir solo un reinicio de la aplicación o flujo con una nueva configuración. Por ejemplo, no se admite
cambiar spark.sql.shuffle.partitions mientras se está ejecutando una transmisión (en realidad, no cambiará la cantidad de
particiones aleatorias).
Esto requiere reiniciar la transmisión real, no necesariamente toda la aplicación. Los cambios de peso más pesados, como
cambiar las configuraciones arbitrarias de la aplicación Spark, probablemente requerirán un
Machine Translated by Google

reinicio de la aplicación.

Métricas y Monitoreo
Las métricas y la supervisión en las aplicaciones de transmisión son en gran medida las mismas que para las
aplicaciones generales de Spark que usan las herramientas descritas en el Capítulo 18. Sin embargo, la transmisión
estructurada agrega varias especificaciones más para ayudarlo a comprender mejor el estado de su aplicación. Hay dos
API clave que puede aprovechar para consultar el estado de una consulta de transmisión y ver su progreso de ejecución
reciente. Con estas dos API, puede tener una idea de si su transmisión se está comportando como se esperaba o no.

Estado de la consulta

El estado de la consulta es la API de monitoreo más básica, por lo que es un buen punto de partida. Su objetivo es responder
a la pregunta: "¿Qué procesamiento está realizando mi transmisión en este momento?" Esta información se
informa en el campo de estado del objeto de consulta devuelto por startStream. Por ejemplo, podría tener un flujo de
conteos simple que proporcione conteos de dispositivos IOT definidos por la siguiente consulta (aquí solo estamos usando
la misma consulta del capítulo anterior sin el código de inicialización):

estado de la consulta

Para obtener el estado de una consulta dada, simplemente ejecutar el comando query.status devolverá el estado actual
de la transmisión. Esto nos brinda detalles sobre lo que está sucediendo en ese momento en la secuencia. Aquí hay una
muestra de lo que obtendrá al consultar este estado:

{
"mensaje": "Obteniendo compensaciones de...",
"isDataAvailable": verdadero,
"isTriggerActive": verdadero
}

El fragmento anterior describe cómo obtener las compensaciones de una fuente de datos de transmisión estructurada (de
ahí el mensaje que describe la obtención de compensaciones). Hay una variedad de mensajes para describir la corriente
estado.

NOTA

Hemos mostrado el comando de estado en línea aquí de la forma en que lo llamarías en un shell de Spark. Sin
embargo, para una aplicación independiente, es posible que no tenga un shell adjunto para ejecutar código
arbitrario dentro de su proceso. En ese caso, puede exponer su estado implementando un servidor de monitoreo,
como un pequeño servidor HTTP que escucha en un puerto y devuelve query.status cuando recibe una solicitud. Como
alternativa, puede usar la API StreamingQueryListener más rica que se describe más adelante para escuchar más eventos.
Machine Translated by Google

Progreso reciente
Si bien es útil ver el estado actual de la consulta, igualmente importante es la capacidad de ver el progreso
de la consulta. La API de progreso nos permite responder preguntas como "¿A qué velocidad estoy
procesando tuplas?" o "¿Qué tan rápido llegan las tuplas desde la fuente?" Al ejecutar
query.recentProgress, obtendrá acceso a más información basada en el tiempo, como la tasa de procesamiento
y la duración de los lotes. El progreso de la consulta de transmisión también incluye información sobre las fuentes
de entrada y los sumideros de salida detrás de su transmisión.

consulta.recentProgress

Aquí está el resultado de la versión de Scala después de ejecutar el código anterior; el de Python será similar:

Array({ "id" : "d9b5eac5­2b27­4655­8dd3­4be626b1b59b",


"runId" : "f8da8bc7­5d0a­4554­880d­d21fe43b983d", "name" :
"test_stream", "timestamp" :
"2017­ 08­06T21:11:21.141Z", "numInputRows":
780119,
"processedRowsPerSecond": 19779.89350912779,
"durationMs":
{ "addBatch": 38179,
"getBatch": 235,
"getOffset": 518,
"queryPlan ning" : 138 ,
"triggerExecution": 39440,
"walCommit": 312 },

"stateOperators":
[ { "numRowsTotal" : 7,
"numRowsUpdated" :
7 } ],
"fuentes" : [ {
"descripción": "FileStreamSource[/some/stream/source/]", "startOffset":
nulo, "endOffset": {

"logOffset" : 0 },

"numInputRows" : 780119,
"processedRowsPerSecond" : 19779.89350912779 } ],

"sink" :
{ "description" : "MemorySink"

} })

Como puede ver en el resultado que se acaba de mostrar, esto incluye una serie de detalles sobre el estado de
la transmisión. Es importante tener en cuenta que esta es una instantánea en el tiempo (según cuando preguntamos
por el progreso de la consulta). Para obtener resultados consistentes sobre el estado de la transmisión, necesitará
Machine Translated by Google

para consultar esta API para el estado actualizado repetidamente. La mayoría de los campos en el resultado anterior
deben explicarse por sí mismos. Sin embargo, repasemos en detalle algunos de los campos más importantes.

Tasa de entrada y tasa de procesamiento

La tasa de entrada especifica la cantidad de datos que fluyen hacia la transmisión estructurada desde nuestra fuente de
entrada. La tasa de procesamiento es la rapidez con la que la aplicación puede analizar esos datos. En el caso ideal, las
tasas de entrada y procesamiento deberían variar juntas. Otro caso podría ser cuando la tasa de entrada es mucho mayor que
la tasa de procesamiento. Cuando esto sucede, la transmisión se está quedando atrás y deberá escalar el clúster para
manejar la carga más grande.

Duración del lote

Casi todos los sistemas de transmisión utilizan procesamiento por lotes para operar con un rendimiento razonable (algunos
tienen una opción de latencia alta a cambio de un rendimiento más bajo). El streaming estructurado logra ambos.
A medida que opera con los datos, es probable que observe que la duración de los lotes oscila a medida que la transmisión
estructurada procesa una cantidad variable de eventos a lo largo del tiempo. Naturalmente, esta métrica tendrá poca o
ninguna relevancia cuando el motor de procesamiento continuo se convierta en una opción de ejecución.

CONSEJO

En general, es una mejor práctica visualizar los cambios en la duración del lote y las tasas de entrada y procesamiento.
Es mucho más útil que simplemente informar cambios a lo largo del tiempo.

Interfaz de usuario de Spark

La interfaz de usuario web de Spark, cubierta en detalle en el Capítulo 18, también muestra tareas, trabajos y métricas de
procesamiento de datos para aplicaciones de transmisión estructurada. En la interfaz de usuario de Spark, cada aplicación de
transmisión aparecerá como una secuencia de trabajos cortos, uno para cada activador. Sin embargo, puede usar la misma
interfaz de usuario para ver métricas, planes de consulta, duraciones de tareas y registros de su aplicación. Una desviación a
tener en cuenta de la API de DStream es que el Streaming estructurado no utiliza la pestaña Streaming.

alertando

Comprender y observar las métricas de sus consultas de transmisión estructurada es un primer paso importante. Sin embargo,
esto implica observar constantemente un tablero o las métricas para descubrir posibles problemas. Necesitará alertas
automáticas sólidas para notificarle cuando sus trabajos están fallando o no se mantienen al día con la tasa de datos de
entrada sin monitorearlos manualmente. Hay varias formas de integrar las herramientas de alerta existentes con
Spark, generalmente basándose en la API de progreso reciente que cubrimos antes. Por ejemplo, puede alimentar
directamente las métricas a un sistema de monitoreo como la biblioteca de código abierto Coda Hale Metrics o Prometheus, o
simplemente puede registrarlas y usar un sistema de agregación de registros como Splunk. Además de
monitorear y alertar sobre consultas, también querrá monitorear y alertar sobre
Machine Translated by Google

el estado del clúster y la aplicación general (si está ejecutando varias consultas juntas).

Monitoreo avanzado con Streaming Listener


Ya mencionamos algunas de las herramientas de monitoreo de alto nivel en la transmisión estructurada. Con
un poco de lógica de adhesión, puede usar las API de estado y queryProgress para generar eventos de
monitoreo en la plataforma de monitoreo que elija su organización (p. ej., un sistema de agregación de registros
o un tablero de Prometheus). Más allá de estos enfoques, también existe una forma de nivel inferior pero
más poderosa de observar la ejecución de una aplicación: la clase StreamingQueryListener.

La clase StreamingQueryListener le permitirá recibir actualizaciones asincrónicas de la consulta de transmisión


para enviar automáticamente esta información a otros sistemas e implementar mecanismos sólidos
de monitoreo y alerta. Empiece por desarrollar su propio objeto para extender StreamingQueryListener, luego
adjúntelo a una SparkSession en ejecución. Una vez que adjunte su oyente personalizado con
sparkSession.streams.addListener(), su clase recibirá notificaciones cuando se inicie o detenga una
consulta, o cuando se realice un progreso en una consulta activa. Aquí hay un ejemplo simple de un oyente de
la documentación de transmisión estructurada:

valor chispa: SparkSession = ...

spark.streams.addListener(new StreamingQueryListener() { override


def onQueryStarted(queryStarted: QueryStartedEvent): Unidad = { println("Consulta
iniciada: " + consultaStarted.id)

} invalidar definición en ConsultaTerminada(


consultaTerminada: ConsultaTerminadaEvento): Unidad =
{ println("Consulta terminada: " + consultaTerminada.id)

} override def onQueryProgress(queryProgress: QueryProgressEvent): Unidad =


"
{ println("Consulta progresó: + queryProgress.progress)
}
})

Los oyentes de transmisión le permiten procesar cada actualización de progreso o cambio de estado
utilizando un código personalizado y pasarlo a sistemas externos. Por ejemplo, el
siguiente código para un StreamingQueryListener que reenviará toda la información de progreso de la
consulta a Kafka. Tendrá que analizar esta cadena JSON una vez que lea los datos de Kafka para
acceder a las métricas reales:

class KafkaMetrics(servidores: String) extiende StreamingQueryListener {


val kafkaProperties = nuevas Propiedades()
kafkaProperties.put(
"bootstrap.servers",

servidores)

kafkaProperties.put( "key.serializer", "kafkashaded.org.apache.kafka.common.serialization.StringSerializer")


Machine Translated by Google

kafkaProperties.put( "valor.serializador", "kafkashaded.org.apache.kafka.common.serialization.StringSerializer")

val productor = nuevo KafkaProducer[String, String](kafkaProperties)

importar org.apache.spark.sql.streaming.StreamingQueryListener importar


org.apache.kafka.clients.producer.KafkaProducer

invalidar def onQueryProgress(evento:


StreamingQueryListener.QueryProgressEvent): Unidad =
{ productor. enviar (nuevo ProducerRecord ("mediciones de
transmisión", evento. progreso. json))

} invalidar definición onQueryStarted(evento:


StreamingQueryListener.QueryStartedEvent): Unidad = {} invalidar
definición onQueryTerpressed(evento:
StreamingQueryListener.QueryTerpressedEvent): Unidad = {}
}

Usando la interfaz StreamingQueryListener, incluso puede monitorear aplicaciones de transmisión estructurada


en un clúster ejecutando una aplicación de transmisión estructurada en ese mismo (u otro) clúster. También
puede administrar múltiples flujos de esta manera.

Conclusión
En este capítulo, cubrimos las principales herramientas necesarias para ejecutar la transmisión estructurada
en producción: puntos de control para la tolerancia a fallas y varias API de monitoreo que le permiten
observar cómo se ejecuta su aplicación. Por suerte para usted, si ya está ejecutando Spark en producción, muchos
de los conceptos y herramientas son similares, por lo que debería poder reutilizar gran parte de su conocimiento existente.
Asegúrese de consultar la Parte IV para ver otras herramientas útiles para monitorear aplicaciones Spark.
Machine Translated by Google

Parte VI. Analítica avanzada y


aprendizaje automático
Machine Translated by Google

Capítulo 24. Descripción general de análisis


avanzado y aprendizaje automático

Hasta ahora, hemos cubierto las API de flujo de datos bastante generales. Esta parte del libro profundizará en algunas de
las API de análisis avanzado más específicas disponibles en Spark. Más allá del análisis y la transmisión de SQL a gran
escala, Spark también brinda soporte para estadísticas, aprendizaje automático y análisis de gráficos. Estos abarcan un
conjunto de cargas de trabajo a las que nos referiremos como análisis avanzado.
Esta parte del libro cubrirá las herramientas de análisis avanzadas en Spark, que incluyen:

Preprocesamiento de sus datos (limpieza de datos e ingeniería de funciones)

Aprendizaje supervisado

Aprendizaje de recomendaciones

Motores no supervisados

Análisis gráfico

Aprendizaje profundo

Este capítulo ofrece una descripción general básica del análisis avanzado, algunos casos de uso de ejemplo y un flujo de
trabajo básico de análisis avanzado. Luego, cubriremos las herramientas de análisis que acabamos de mencionar y le
enseñaremos cómo aplicarlas.

ADVERTENCIA

Este libro no pretende enseñarle todo lo que necesita saber sobre el aprendizaje automático desde cero.
No entraremos en definiciones y formulaciones matemáticas estrictas, no por falta de importancia, sino
simplemente porque es demasiada información para incluir. Esta parte del libro no es una guía de
algoritmos que le enseñará los fundamentos matemáticos de cada algoritmo disponible ni las estrategias de
implementación detalladas utilizadas. Los capítulos que se incluyen aquí sirven como guía para los usuarios,
con el propósito de describir lo que necesita saber para usar las API de análisis avanzado de Spark.

Breve introducción a la analítica avanzada


El análisis avanzado se refiere a una variedad de técnicas destinadas a resolver el problema central de obtener
información y hacer predicciones o recomendaciones basadas en datos. La mejor ontología para el aprendizaje automático
se estructura en función de la tarea que le gustaría realizar. Las tareas más comunes incluyen:

Aprendizaje supervisado, incluida la clasificación y la regresión, donde el objetivo es predecir una etiqueta para
cada punto de datos en función de varias características.
Machine Translated by Google

Motores de recomendación para sugerir productos a los usuarios en función del comportamiento.

Aprendizaje no supervisado, incluido el agrupamiento, la detección de anomalías y el modelado de temas,


donde el objetivo es descubrir la estructura de los datos.

Tareas de análisis gráfico como la búsqueda de patrones en una red social.

Antes de discutir las API de Spark en detalle, revisemos cada una de estas tareas junto con algunos casos de uso
comunes de aprendizaje automático y análisis avanzado. Si bien ciertamente hemos tratado de hacer que esta introducción
sea lo más accesible posible, a veces es posible que necesite consultar otros recursos para comprender completamente el
material. O'Reilly, ¿deberíamos enlazar o mencionar alguno específico? Además, citaremos los siguientes libros a lo
largo de los próximos capítulos porque son excelentes recursos para aprender más sobre el análisis individual (y, como
beneficio adicional, están disponibles gratuitamente en la web):

Introducción al aprendizaje estadístico por Gareth James, Daniela Witten, Trevor Hastie y Robert Tibshirani. Nos
referimos a este libro como “ISL”.

Elementos del aprendizaje estadístico por Trevor Hastie, Robert Tibshirani y Jerome Friedman. Nos
referimos a este libro como “ESL”.

Aprendizaje profundo por Ian Goodfellow, Yoshua Bengio y Aaron Courville. Nos referimos a este libro como
“DLB”.

Aprendizaje supervisado
El aprendizaje supervisado es probablemente el tipo más común de aprendizaje automático. El objetivo es simple: usar datos
históricos que ya tienen etiquetas (a menudo llamadas variables dependientes), entrenar un modelo para predecir los valores
de esas etiquetas en función de varias características de los puntos de datos. Un ejemplo sería predecir los ingresos de
una persona (la variable dependiente) en función de la edad (una característica). Este proceso de entrenamiento
generalmente procede a través de un algoritmo de optimización iterativo como el descenso de gradiente. El algoritmo de
entrenamiento comienza con un modelo básico y lo mejora gradualmente ajustando varios parámetros internos (coeficientes)
durante cada iteración de entrenamiento. El resultado de este proceso es un modelo entrenado que puede usar para hacer
predicciones sobre nuevos datos. Hay una serie de tareas diferentes que deberemos completar como parte del proceso
de entrenamiento y hacer predicciones, como medir el éxito de los modelos entrenados antes de usarlos en el campo, pero
el principio fundamental es simple: entrenar con datos históricos. , asegúrese de que se generalice a los datos en
los que no entrenamos y luego haga predicciones sobre los nuevos datos.

Podemos organizar aún más el aprendizaje supervisado según el tipo de variable que buscamos predecir. Llegaremos
a eso a continuación.

Clasificación

Un tipo común de aprendizaje supervisado es la clasificación. La clasificación es el acto de entrenar un algoritmo para predecir
una variable dependiente que es categórica (perteneciente a un conjunto discreto y finito de valores). El caso más común es la
clasificación binaria, donde nuestro modelo resultante hará una
Machine Translated by Google

predicción de que un elemento dado pertenece a uno de dos grupos. El ejemplo canónico es la clasificación del spam de correo
electrónico. Usando un conjunto de correos electrónicos históricos que están organizados en grupos de correos electrónicos no
deseados y no correos electrónicos no deseados, entrenamos un algoritmo para analizar las palabras y cualquier cantidad de
propiedades de los correos electrónicos históricos y hacer predicciones sobre ellos. Una vez que estamos satisfechos con el
rendimiento del algoritmo, usamos ese modelo para hacer predicciones sobre futuros correos electrónicos que el modelo nunca
antes había visto.

Cuando clasificamos elementos en más de dos categorías, lo llamamos clasificación multiclase.


Por ejemplo, podemos tener cuatro categorías diferentes de correo electrónico (a diferencia de las dos categorías del párrafo anterior):
spam, personal, relacionado con el trabajo y otros. Hay muchos casos de uso para la clasificación, que incluyen:

Predicción de enfermedades

Un médico u hospital puede tener un conjunto de datos históricos de atributos fisiológicos y de comportamiento de un conjunto
de pacientes. Podrían usar este conjunto de datos para entrenar un modelo con estos datos históricos (y evaluar su éxito y
las implicaciones éticas antes de aplicarlo) y luego aprovecharlo para predecir si un paciente tiene una enfermedad cardíaca o
no. Este es un ejemplo de clasificación binaria (corazón sano, corazón no sano) o clasificación multiclase (corazón sano o
una de varias enfermedades diferentes).

Clasificación de imágenes

Hay una serie de aplicaciones de empresas como Apple, Google o Facebook que pueden predecir quién está en una foto
determinada mediante la ejecución de un modelo de clasificación que ha sido entrenado en imágenes históricas de
personas en tus fotos anteriores. Otro caso de uso común es clasificar imágenes o etiquetar los objetos en imágenes.

Predecir la rotación de clientes

Un caso de uso más orientado a los negocios podría ser predecir la rotación de clientes, es decir, qué clientes es
probable que dejen de usar un servicio. Puede hacer esto entrenando un clasificador binario en clientes anteriores que se han
retirado (y no se han retirado) y usándolo para intentar predecir si los clientes actuales se retirarán o no.

Comprar o no comprar

Las empresas a menudo quieren predecir si los visitantes de su sitio web comprarán un producto determinado. Pueden
usar información sobre el patrón de navegación de los usuarios o atributos como la ubicación para impulsar esta
predicción.

Hay muchos más casos de uso para la clasificación más allá de estos ejemplos. Presentaremos más casos de uso, así como las API
de clasificación de Spark, en el Capítulo 26.

Regresión

En clasificación, nuestra variable dependiente es un conjunto de valores discretos. En la regresión, en cambio, tratamos de predecir
una variable continua (un número real). En términos más simples, en lugar de predecir una categoría, queremos predecir un
valor en una recta numérica. El resto del proceso es básicamente el mismo,
Machine Translated by Google

por eso ambas son formas de aprendizaje supervisado. Nos entrenaremos sobre datos históricos para hacer predicciones sobre
datos que nunca hemos visto. Estos son algunos ejemplos típicos:

Predicción de ventas

Una tienda puede querer predecir las ventas totales de productos en datos dados usando datos históricos de ventas.
Hay una serie de posibles variables de entrada, pero un ejemplo simple podría ser usar los datos de ventas de la
semana pasada para predecir los datos del día siguiente.

altura de predicción

Con base en las alturas de dos individuos, podríamos querer predecir las alturas de sus hijos potenciales.

Predecir el número de espectadores de un programa

Una empresa de medios como Netflix podría intentar predecir cuántos de sus suscriptores verán un programa en particular.

Presentaremos más casos de uso, así como los métodos de regresión de Spark, en el Capítulo 27.

Recomendación
La recomendación es una de las aplicaciones más intuitivas de analítica avanzada. Al estudiar las preferencias explícitas de
las personas (a través de calificaciones) o implícitas (a través del comportamiento observado) para varios productos o
elementos, un algoritmo puede hacer recomendaciones sobre lo que le puede gustar a un usuario al establecer similitudes
entre los usuarios o elementos. Al observar estas similitudes, el algoritmo hace recomendaciones a los usuarios en función
de lo que les gustó a los usuarios similares o qué otros productos se parecen a los que el usuario ya compró. La
recomendación es un caso de uso común para Spark y se adapta bien a big data. Estos son algunos ejemplos de casos de uso:

Recomendaciones de películas

Netflix usa Spark, aunque no necesariamente sus bibliotecas integradas, para hacer recomendaciones de películas a
gran escala a sus usuarios. Lo hace estudiando qué películas ven y no ven los usuarios en la aplicación de Netflix.
Además, es probable que Netflix tenga en cuenta cuán similares son las calificaciones de un usuario dado a las de
otros usuarios.

Recomendaciones de productos

Amazon utiliza las recomendaciones de productos como una de sus principales herramientas para aumentar las
ventas. Por ejemplo, en función de los artículos en nuestro carrito de compras, Amazon puede recomendar otros artículos
que se agregaron a carritos de compras similares en el pasado. Asimismo, en cada página de producto, Amazon muestra
productos similares comprados por otros usuarios.

Presentaremos más casos de uso de recomendaciones, así como los métodos de Spark para generar recomendaciones, en
el Capítulo 28.

Aprendizaje sin supervisión


Machine Translated by Google

El aprendizaje no supervisado es el acto de tratar de encontrar patrones o descubrir la estructura subyacente en un conjunto de datos
determinado. Esto difiere del aprendizaje supervisado porque no hay una variable dependiente (etiqueta) para predecir.

Algunos ejemplos de casos de uso para el aprendizaje no supervisado incluyen:

Detección de anomalías

Dado que algún tipo de evento estándar ocurre a menudo con el tiempo, es posible que deseemos informar cuando ocurre un
tipo de evento no estándar. Por ejemplo, un oficial de seguridad podría querer recibir notificaciones cuando se observe un
objeto extraño (piense en un vehículo, un patinador o un ciclista) en un camino.

Segmentación de usuarios

Dado un conjunto de comportamientos de los usuarios, es posible que deseemos comprender mejor qué atributos comparten
ciertos usuarios con otros usuarios. Por ejemplo, una empresa de juegos podría agrupar a los usuarios en función de propiedades
como la cantidad de horas jugadas en un juego determinado. El algoritmo podría revelar que los jugadores ocasionales tienen un
comportamiento muy diferente al de los jugadores incondicionales, por ejemplo, y permitir que la empresa ofrezca diferentes
recomendaciones o recompensas a cada jugador.

Modelado de temas

Dado un conjunto de documentos, podríamos analizar las diferentes palabras contenidas en ellos para ver si existe alguna
relación subyacente entre ellos. Por ejemplo, dada una cantidad de páginas web sobre análisis de datos, un algoritmo de modelado
de temas puede agruparlas en páginas sobre aprendizaje automático, SQL, transmisión, etc., en función de grupos de
palabras que son más comunes en un tema que en otros.

Intuitivamente, es fácil ver cómo la segmentación de clientes podría ayudar a una plataforma a atender mejor a cada grupo de usuarios.
Sin embargo, puede ser difícil descubrir si este conjunto de segmentos de usuarios es "correcto" o no. Por esta razón, puede ser
difícil determinar si un modelo en particular es bueno o no. Discutiremos el aprendizaje no supervisado en detalle en el Capítulo 29.

Análisis gráfico
Si bien es menos común que la clasificación y la regresión, el análisis gráfico es una herramienta poderosa.
Fundamentalmente, el análisis de grafos es el estudio de estructuras en las que especificamos vértices (que son objetos) y aristas (que
representan las relaciones entre esos objetos). Por ejemplo, los vértices pueden representar personas y productos, y los bordes
pueden representar una compra. Al observar las propiedades de los vértices y las aristas, podemos comprender mejor las conexiones
entre ellos y la estructura general del gráfico. Dado que los gráficos tienen que ver con las relaciones, cualquier cosa que especifique
una relación es un gran caso de uso para el análisis de gráficos. Algunos ejemplos incluyen:

predicción de fraude

Capital One utiliza las capacidades de análisis gráfico de Spark para comprender mejor las redes de fraude. Mediante el uso de
información histórica fraudulenta (como números de teléfono, direcciones o nombres), descubren solicitudes de crédito o
transacciones fraudulentas. Por ejemplo, cualquier cuenta de usuario dentro de dos
Machine Translated by Google

los saltos de un número de teléfono fraudulento pueden considerarse sospechosos.

Detección de anomalías

Al observar cómo las redes de individuos se conectan entre sí, los valores atípicos y las anomalías se pueden
marcar para el análisis manual. Por ejemplo, si normalmente en nuestros datos cada vértice tiene diez aristas
asociadas y un vértice dado solo tiene una arista, podría valer la pena investigarlo como algo extraño.

Clasificación

Dados algunos datos sobre ciertos vértices en una red, puede clasificar otros vértices según su conexión
con el nodo original. Por ejemplo, si un determinado individuo es etiquetado como influencer en una red social,
podríamos clasificar a otros individuos con estructuras de red similares como influencers.

Recomendación

El algoritmo de recomendación web original de Google, PageRank, es un algoritmo gráfico que analiza las
relaciones de los sitios web para clasificar la importancia de las páginas web. Por ejemplo, una página web que
tiene muchos enlaces se clasifica como más importante que una que no tiene enlaces.

Discutiremos más ejemplos de análisis de gráficos en el Capítulo 30.

El proceso de análisis avanzado


Debe tener una comprensión sólida de algunos casos de uso fundamentales para el aprendizaje automático y el análisis
avanzado. Sin embargo, encontrar un caso de uso es solo una pequeña parte del proceso de análisis avanzado
real. Hay mucho trabajo para preparar sus datos para el análisis, probar diferentes formas de modelarlos y evaluar
estos modelos. Esta sección proporcionará una estructura para el proceso analítico general y los pasos que
debemos seguir para no solo realizar una de las tareas que se acaban de describir, sino evaluar el éxito de manera
objetiva para comprender si debemos o no aplicar nuestro modelo al mundo real ( Figura 24­1).
Machine Translated by Google

Figura 24­1. El flujo de trabajo de aprendizaje automático

El proceso general implica los siguientes pasos (con algunas variaciones):

1. Recopilación y recolección de los datos relevantes para su tarea.

2. Limpiar e inspeccionar los datos para comprenderlos mejor.

3. Realizar ingeniería de características para permitir que el algoritmo aproveche los datos de una manera adecuada
(p. ej., convertir los datos en vectores numéricos).

4. Usar una parte de estos datos como un conjunto de entrenamiento para entrenar uno o más algoritmos para generar
algunos modelos candidatos.

5. Evaluar y comparar modelos con sus criterios de éxito al evaluar objetivamente


medir los resultados en un subconjunto de los mismos datos que no se utilizó para el entrenamiento. Esto
le permite comprender mejor cómo puede funcionar su modelo en la naturaleza.

6. Aprovechar los conocimientos del proceso anterior y/o usar el modelo para hacer predicciones, detectar
anomalías o resolver desafíos comerciales más generales.

Estos pasos no serán los mismos para todas las tareas de análisis avanzado. Sin embargo, este flujo de trabajo sirve como
marco general para lo que necesitará para tener éxito con el análisis avanzado. Tal como hicimos con las diversas tareas
de análisis avanzado anteriormente en el capítulo, analicemos el proceso para comprender mejor el objetivo general de
cada paso.

Recopilación de datos

Naturalmente, es difícil crear un conjunto de entrenamiento sin recopilar datos primero. Por lo general, esto significa al
menos recopilar los conjuntos de datos que querrá aprovechar para entrenar su algoritmo. Spark es una excelente herramienta
para esto debido a su capacidad para hablar con una variedad de fuentes de datos y trabajar con datos grandes y
Machine Translated by Google

pequeño.

Limpieza de datos

Una vez que haya recopilado los datos adecuados, necesitará limpiarlos e inspeccionarlos. Esto generalmente se hace
como parte de un proceso llamado análisis exploratorio de datos o EDA. EDA generalmente significa usar consultas interactivas
y métodos de visualización para comprender mejor las distribuciones, las correlaciones y otros detalles en sus datos.
Durante este proceso, puede notar que necesita eliminar algunos valores que pueden haberse registrado incorrectamente en sentido
ascendente o que pueden faltar otros valores. Cualquiera que sea el caso, siempre es bueno saber qué hay en sus datos para evitar
errores en el futuro. La multitud de funciones de Spark en las API estructuradas proporcionará una forma sencilla de limpiar e
informar sobre sus datos.

Ingeniería de características

Ahora que recopiló y limpió su conjunto de datos, es hora de convertirlo a una forma adecuada para los algoritmos de
aprendizaje automático, lo que generalmente significa características numéricas. La ingeniería de características
adecuada a menudo puede hacer o deshacer una aplicación de aprendizaje automático, por lo que esta es una tarea que querrá
hacer con cuidado. El proceso de ingeniería de funciones incluye una variedad de tareas, como la normalización de datos, la
adición de variables para representar las interacciones de otras variables, la manipulación de variables categóricas y su conversión
al formato adecuado para ingresar a nuestro modelo de aprendizaje automático. En MLlib, la biblioteca de aprendizaje
automático de Spark, todas las variables generalmente deberán ingresarse como vectores de dobles (independientemente de lo que
realmente representen). Cubrimos el proceso de ingeniería de características en gran profundidad en el Capítulo 25. Como verá en
ese capítulo, Spark proporciona los elementos esenciales que necesitará para manipular sus datos utilizando una variedad de
técnicas estadísticas de aprendizaje automático.

NOTA

Los siguientes pasos (modelos de entrenamiento, ajuste de modelos y evaluación) no son relevantes para todos
los casos de uso. Este es un flujo de trabajo general que puede variar significativamente según el objetivo final
que le gustaría lograr.

Modelos de entrenamiento

En este punto del proceso, tenemos un conjunto de datos de información histórica (p. ej., correos electrónicos no deseados o no
deseados) y una tarea que nos gustaría completar (p. ej., clasificar los correos electrónicos no deseados). A continuación, querremos
entrenar un modelo para predecir la salida correcta, dada alguna entrada. Durante el proceso de entrenamiento, los parámetros
dentro del modelo cambiarán según el desempeño del modelo en los datos de entrada. Por ejemplo, para clasificar los correos
electrónicos no deseados, nuestro algoritmo probablemente encontrará que ciertas palabras predicen mejor el correo no deseado
que otras y, por lo tanto, ponderará más los parámetros asociados con esas palabras. Al final, el modelo entrenado encontrará
que ciertas palabras deberían tener más influencia (debido a su asociación constante con los correos electrónicos no deseados)
que otras. El resultado del proceso de entrenamiento es lo que llamamos un modelo. Luego, los modelos se pueden usar para
obtener información o para hacer
Machine Translated by Google

predicciones futuras. Para hacer predicciones, le dará una entrada al modelo y producirá una salida basada en una
manipulación matemática de estas entradas. Usando el ejemplo de clasificación, dadas las propiedades de un correo
electrónico, predecirá si ese correo electrónico es correo no deseado o no al compararlo con el historial de correos
electrónicos no deseados y no deseados en los que se entrenó.

Sin embargo, solo entrenar un modelo no es el objetivo; queremos aprovechar nuestro modelo para generar
conocimientos. Por lo tanto, debemos responder a la pregunta: ¿cómo sabemos que nuestro modelo es bueno en lo que se
supone que debe hacer? Ahí es donde entran en juego el ajuste y la evaluación del modelo.

Ajuste y evaluación del modelo

Probablemente notó anteriormente que mencionamos que debe dividir sus datos en varias partes y usar solo una
para el entrenamiento. Este es un paso esencial en el proceso de aprendizaje automático porque cuando crea un modelo
de análisis avanzado, quiere que ese modelo se generalice a datos que no ha visto antes. Dividir nuestro conjunto de datos
en múltiples porciones nos permite probar objetivamente la efectividad del modelo entrenado contra un conjunto de datos
que nunca antes había visto. El objetivo es ver si su modelo comprende algo fundamental sobre este proceso
de datos o si solo notó las cosas particulares del conjunto de entrenamiento (a veces llamado sobreajuste). Es por eso
que se llama un conjunto de prueba. En el proceso de entrenamiento de modelos, también podemos tomar otro
subconjunto separado de datos y tratarlo como otro tipo de conjunto de prueba, llamado conjunto de validación, para
probar diferentes hiperparámetros (parámetros que afectan el proceso de entrenamiento) y comparar diferentes
variaciones del mismo modelo sin sobreajustar al equipo de prueba.

ADVERTENCIA

Seguir las mejores prácticas de capacitación, validación y conjuntos de prueba adecuados es esencial para usar con
éxito el aprendizaje automático. Es fácil terminar sobreajustando (entrenando un modelo que no se generaliza bien a nuevos
datos) si no aislamos adecuadamente estos conjuntos de datos. No podemos cubrir este problema en profundidad en
este libro, pero casi cualquier libro de aprendizaje automático cubrirá este tema.

Para continuar con el ejemplo de clasificación al que hicimos referencia anteriormente, tenemos tres conjuntos de datos: un
conjunto de entrenamiento para entrenar modelos, un conjunto de validación para probar diferentes variaciones de los
modelos que estamos entrenando y, por último, un conjunto de prueba que usaremos para la evaluación final de nuestras
diferentes variaciones de modelo para ver cuál funcionó mejor.

Aprovechar el modelo y/o los conocimientos

Después de ejecutar el modelo a través del proceso de capacitación y terminar con un modelo de buen rendimiento,
¡ahora está listo para usarlo! Llevar su modelo a producción puede ser un desafío significativo en sí mismo.
Discutiremos algunas tácticas más adelante en este capítulo.

Kit de herramientas de análisis avanzado de Spark


La descripción general anterior es solo un flujo de trabajo de ejemplo y no abarca todos los casos de uso o
Machine Translated by Google

flujos de trabajo potenciales. Además, probablemente hayas notado que casi no hablamos de Spark. Esta sección discutirá
las capacidades de análisis avanzado de Spark. Spark incluye varios paquetes principales y muchos paquetes externos para
realizar análisis avanzados. El paquete principal es MLlib, que proporciona una interfaz para crear canalizaciones de
aprendizaje automático.

¿Qué es MLlib?
MLlib es un paquete, integrado e incluido en Spark, que proporciona interfaces para recopilar y limpiar datos, diseñar y
seleccionar funciones, entrenar y ajustar modelos de aprendizaje automático supervisados y no supervisados a
gran escala, y usar esos modelos en producción.

ADVERTENCIA

MLlib en realidad consta de dos paquetes que aprovechan diferentes estructuras de datos centrales. El paquete
org.apache.spark.ml incluye una interfaz para usar con DataFrames. Este paquete también ofrece una interfaz
de alto nivel para crear canalizaciones de aprendizaje automático que ayudan a estandarizar la forma en que realiza
los pasos anteriores. El paquete de nivel inferior, org.apache.spark.mllib, incluye interfaces para las API RDD de bajo
nivel de Spark. Este libro se centrará exclusivamente en la API de DataFrame. La API de RDD es la interfaz de nivel
inferior, que se encuentra en modo de mantenimiento (lo que significa que solo recibirá correcciones de errores, no
nuevas funciones) en este momento. También se ha tratado de manera bastante extensa en libros anteriores sobre
Spark y, por lo tanto, se omite aquí.

Cuándo y por qué debería usar MLlib (frente a scikit­learn, TensorFlow o foo package)

En un nivel alto, MLlib puede sonar como muchos otros paquetes de aprendizaje automático de los que probablemente
haya oído hablar, como scikit­learn para Python o la variedad de paquetes R para realizar tareas similares. Entonces, ¿por
qué debería molestarse con MLlib? Existen numerosas herramientas para realizar el aprendizaje automático en una
sola máquina, y aunque hay varias opciones excelentes para elegir, estas herramientas de una sola máquina tienen sus
límites en términos del tamaño de los datos en los que puede entrenar o el tiempo de procesamiento. Esto significa que las
herramientas de una sola máquina suelen ser complementarias a MLlib. Cuando encuentre esos problemas de
escalabilidad, aproveche las capacidades de Spark.

Hay dos casos de uso clave en los que desea aprovechar la capacidad de escalar de Spark. En primer lugar, desea aprovechar
Spark para el preprocesamiento y la generación de funciones para reducir la cantidad de tiempo que puede llevar producir
conjuntos de entrenamiento y prueba a partir de una gran cantidad de datos. Luego, puede aprovechar las bibliotecas de
aprendizaje de una sola máquina para entrenar en esos conjuntos de datos dados. En segundo lugar, cuando los datos de
entrada o el tamaño del modelo se vuelvan demasiado difíciles o inconvenientes para colocarlos en una máquina, use Spark
para hacer el trabajo pesado. Spark hace que el aprendizaje automático distribuido sea muy simple.

Una advertencia importante para todo esto es que, si bien el entrenamiento y la preparación de datos se simplifican, todavía
hay algunas complejidades que deberá tener en cuenta, especialmente cuando se trata de implementar un modelo
entrenado. Por ejemplo, Spark no proporciona una forma integrada de entregar predicciones de baja latencia desde un
modelo, por lo que es posible que desee exportar el modelo a otra publicación.
Machine Translated by Google

sistema o una aplicación personalizada para hacerlo. MLlib generalmente está diseñado para permitir la inspección y
exportación de modelos a otras herramientas cuando sea posible.

Conceptos de MLlib de alto nivel


En MLlib hay varios tipos "estructurales" fundamentales: transformadores, estimadores, evaluadores y canalizaciones. Por
estructural, queremos decir que pensará en términos de estos tipos cuando defina una canalización de aprendizaje
automático de extremo a extremo. Proporcionarán el lenguaje común para definir qué pertenece a qué parte de la
canalización. La figura 24­2 ilustra el flujo de trabajo general que seguirá al desarrollar modelos de aprendizaje automático
en Spark.

Figura 24­2. El flujo de trabajo de aprendizaje automático, en Spark

Los transformadores son funciones que convierten datos sin procesar de alguna manera. Esto podría ser para crear una
nueva variable de interacción (a partir de otras dos variables), normalizar una columna o simplemente cambiar un
número entero a un tipo doble para introducirlo en un modelo. Un ejemplo de un transformador es uno que convierte
variables categóricas de cadena en valores numéricos que se pueden usar en MLlib.
Los transformadores se utilizan principalmente en el preprocesamiento y la ingeniería de funciones. Los transformadores
toman un DataFrame como entrada y producen un nuevo DataFrame como salida, como se ilustra en la Figura 24­3.
Machine Translated by Google

Figura 24­3. Un transformador estándar

Los estimadores son uno de dos tipos de cosas. Primero, los estimadores pueden ser una especie de transformador
que se inicializa con datos. Por ejemplo, para normalizar datos numéricos, necesitaremos inicializar nuestra
transformación con alguna información sobre los valores actuales en la columna que nos gustaría normalizar. Esto

requiere dos pases sobre nuestros datos: el pase inicial genera los valores de inicialización y el segundo aplica la
función generada sobre los datos. En la nomenclatura de Spark, los algoritmos que permiten a los usuarios
entrenar un modelo a partir de datos también se denominan estimadores.

Un evaluador nos permite ver cómo se desempeña un modelo determinado de acuerdo con los criterios que
especificamos, como una curva característica operativa del receptor (ROC). Después de usar un evaluador para
seleccionar el mejor modelo de los que probamos, podemos usar ese modelo para hacer predicciones.

Desde un nivel alto podemos especificar cada una de las transformaciones, estimaciones y evaluaciones una a una,
pero muchas veces es más fácil especificar nuestros pasos como etapas en un pipeline. Esta canalización es similar al
concepto de canalización de scikit­learn.

Tipos de datos de bajo nivel

Además de los tipos estructurales para construir canalizaciones, también hay varios tipos de datos de nivel inferior
con los que puede necesitar trabajar en MLlib (siendo Vector el más común). Siempre que pasemos un conjunto de
funciones a un modelo de aprendizaje automático, debemos hacerlo como un vector que consta de dobles.
Este vector puede ser escaso (donde la mayoría de los elementos son cero) o denso (donde hay muchos valores
únicos). Los vectores se crean de diferentes maneras. Para crear un vector denso, podemos especificar una matriz
de todos los valores. Para crear un vector disperso, podemos especificar el tamaño total y los índices y valores de los
elementos distintos de cero. Disperso es el mejor formato, como habrás adivinado, cuando la mayoría de los valores son
cero, ya que esta es una representación más comprimida. Aquí hay un ejemplo de cómo crear manualmente un vector:

// en Scala
importar org.apache.spark.ml.linalg.Vectors val
denseVec = Vectores.dense(1.0, 2.0, 3.0)
Machine Translated by Google

val size = 3
val idx = Array(1,2) // ubicaciones de elementos distintos de cero en el vector
val valores = Array(2.0,3.0) val
sparseVec = Vectores.sparse(tamaño, idx, valores)
sparseVec.toDense
denseVec. escaso

# en Python
desde pyspark.ml.linalg importar vectores
denseVec = Vectores.dense(1.0, 2.0, 3.0)
tamaño
= 3 idx = [1, 2] # ubicaciones de elementos distintos de cero en
valores vectoriales =
[2.0, 3.0] sparseVec = Vectores.esparcido(tamaño, idx, valores)

ADVERTENCIA

De manera confusa, existen tipos de datos similares que se refieren a los que se pueden usar en DataFrames y otros
que solo se pueden usar en RDD. Las implementaciones de RDD se incluyen en el paquete mllib, mientras que las
implementaciones de DataFrame se incluyen en ml.

MLlib en acción
Ahora que hemos descrito algunas de las piezas centrales que puede esperar encontrar, creemos una canalización simple
para demostrar cada uno de los componentes. Usaremos un pequeño conjunto de datos sintéticos que ayudará a ilustrar
nuestro punto. Leamos los datos y veamos una muestra antes de seguir hablando de ello:

// en Scala
var df = spark.read.json("/data/simple­ml")
df.orderBy("value2").show()

# en Python
df = spark.read.json("/data/simple­ml")
df.orderBy("value2").show()

He aquí una muestra de los datos:

+­­­­­+­­­­+­­­­­­+­­­­­­­­­­­­­­­­­­+
|color| laboratorio|valor1| valor2|
+­­­­­+­­­­+­­­­­­+­­­­­­­­­­­­­­­­­­+
|verde|bueno| 1|14.386294994851129|
...
| rojo| mal| | 16|14.386294994851129|
verde|bueno| 12|14.386294994851129|
+­­­­­+­­­­+­­­­­­+­­­­­­­­­­­­­­­­­­+
Machine Translated by Google

Este conjunto de datos consta de una etiqueta categórica con dos valores (bueno o malo), una variable categórica (color)
y dos variables numéricas. Si bien los datos son sintéticos, imaginemos que este conjunto de datos representa la salud
del cliente de una empresa. La columna de "color" representa una calificación de salud categórica hecha por un
representante de servicio al cliente. La columna "laboratorio" representa la verdadera salud del cliente. Los otros dos valores
son algunas medidas numéricas de actividad dentro de una aplicación (p. ej., minutos dedicados en el sitio y compras).
Supongamos que queremos entrenar un modelo de clasificación en el que esperamos predecir una variable binaria, la
etiqueta, a partir de los otros valores.

CONSEJO

Además de JSON, existen algunos formatos de datos específicos que se usan comúnmente para el aprendizaje
supervisado, incluido LIBSVM. Estos formatos tienen etiquetas de valores reales y datos de entrada escasos. Spark puede
leer y escribir para estos formatos utilizando su API de origen de datos. Aquí hay un ejemplo de cómo leer datos de un
archivo libsvm usando esa API de fuente de datos.

chispa.read.format("libsvm").load( "/
data/sample_libsvm_data.txt")

Para obtener más información sobre LIBSVM, consulte la documentación.

Ingeniería de funciones con transformadores


Como ya se mencionó, los transformadores nos ayudan a manipular nuestras columnas actuales de una forma u
otra. La manipulación de estas columnas es a menudo en busca de características de construcción (que ingresaremos
en nuestro modelo). Los transformadores existen para reducir la cantidad de funciones, agregar más funciones, manipular
las actuales o simplemente para ayudarnos a formatear nuestros datos correctamente. Los transformadores agregan
nuevas columnas a los marcos de datos.

Cuando usamos MLlib, todas las entradas a los algoritmos de aprendizaje automático (con varias excepciones
que se analizan en capítulos posteriores) en Spark deben ser de tipo Doble (para etiquetas) y
Vector[Doble] (para características). El conjunto de datos actual no cumple con ese requisito y, por lo tanto, debemos
transformarlo al formato adecuado.

Para lograr esto en nuestro ejemplo, vamos a especificar una RFormula. Este es un lenguaje declarativo para
especificar transformaciones de aprendizaje automático y es fácil de usar una vez que comprende la sintaxis.
RFormula admite un subconjunto limitado de operadores R que en la práctica funcionan bastante bien para modelos y
manipulaciones simples (demostramos el enfoque manual de este problema en el Capítulo 25). Los operadores básicos
de RFormula son:

Objetivo y términos separados

términos concatenados; “+ 0” significa eliminar el intercepto (esto significa que el intercepto en y de la línea
Machine Translated by Google

que encajaremos será 0)

Eliminar un término; "­ 1" significa eliminar la intersección (esto significa que la intersección y de la
línea que ajustaremos será 0; sí, esto hace lo mismo que "+ 0"

Interacción (multiplicación de valores numéricos o valores categóricos binarizados)

Todas las columnas excepto la variable objetivo/dependiente

Para especificar transformaciones con esta sintaxis, necesitamos importar la clase relevante. Luego
pasamos por el proceso de definición de nuestra fórmula. En este caso, queremos usar todas las
variables disponibles (el .) y también agregar las interacciones entre valor1 y color y valor2 y color,
tratándolas como nuevas características:

// en Scala
import org.apache.spark.ml.feature.RFormula val
supervisado = new
.
RFormula() .setFormula("lab ~ + color:value1 + color:value2")

# en Python
desde pyspark.ml.feature import RFormula
.
supervisado = RFormula(formula="lab ~ + color:value1 + color:value2")

En este punto, hemos especificado declarativamente cómo nos gustaría cambiar nuestros datos en lo que
entrenaremos nuestro modelo. El siguiente paso es ajustar el transformador RFormula a los datos para
permitirle descubrir los posibles valores de cada columna. No todos los transformadores tienen este
requisito, pero debido a que RFormula manejará automáticamente las variables categóricas por nosotros,
necesita determinar qué columnas son categóricas y cuáles no, así como cuáles son los distintos valores
de las columnas categóricas. Por esta razón, tenemos que llamar al método de ajuste. Una vez que llamamos
fit, devuelve una versión "entrenada" de nuestro transformador que podemos usar para transformar realmente nuestros datos.

NOTA

Estamos usando el transformador RFormula porque hace que realizar varias transformaciones sea extremadamente fácil
de hacer. En el Capítulo 25, mostraremos otras formas de especificar un conjunto similar de transformaciones y
describiremos las partes componentes de RFormula cuando cubramos los transformadores específicos en MLlib.

Ahora que cubrimos esos detalles, continuemos y preparemos nuestro DataFrame:

// en Scala
val fitRF = supervisado.fit(df)
Machine Translated by Google

val DFpreparado = RFAjustado.transform(df)


DFpreparado.show()

# en Python
equipadoRF = supervisado.ajuste(df)
preparadoDF = equipadoRF.transform(df)
preparadoDF.show()

Este es el resultado del proceso de formación y transformación:

+­­­­­+­­­­+­­­­­­+­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­ ­­­­­­­­+­­­­­­+
|color| laboratorio|valor1| valor2| caracteristicas|etiqueta|
+­­­­­+­­­­+­­­­­­+­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­ ­­­­­­­­+­­­­­­+
|verde|bueno| 1|14.386294994851129|(10,[1,2,3,5,8],[...| 1,0|
...
| rojo| mal| 2|14,386294994851129|(10,[0,2,3,4,7],[...| 0,0|
+­­­­­+­­­­+­­­­­­+­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­ ­­­­­­­­+­­­­­­+

En el resultado podemos ver el resultado de nuestra transformación: una columna llamada características que tiene
nuestros datos sin procesar previamente. Lo que sucede detrás de escena es bastante simple. RFormula inspecciona
nuestros datos durante la llamada de ajuste y genera un objeto que transformará nuestros datos de acuerdo con la fórmula
especificada, que se llama RFormulaModel. Este transformador "entrenado" siempre tiene la palabra Modelo en la firma
de tipo. Cuando usamos este transformador, Spark convierte automáticamente nuestra variable categórica en Dobles
para que podamos ingresarla en un modelo de aprendizaje automático (aún por especificar). En particular, asigna un
valor numérico a cada categoría de color posible, crea funciones adicionales para las variables de interacción
entre colores y valor1/valor2, y las coloca todas en un solo vector. Luego llamamos a transform en ese
objeto para transformar nuestros datos de entrada en los datos de salida esperados.

Hasta ahora, usted (pre)procesó los datos y agregó algunas características en el camino. Ahora es el momento de
entrenar un modelo (o un conjunto de modelos) en este conjunto de datos. Para hacer esto, primero debe preparar un
conjunto de prueba para la evaluación.

CONSEJO

Tener un buen conjunto de prueba es probablemente lo más importante que puede hacer para asegurarse de entrenar un
modelo que realmente pueda usar en el mundo real (de manera confiable). No crear un conjunto de prueba representativo o
usar su conjunto de prueba para el ajuste de hiperparámetros son formas seguras de crear un modelo que no funciona bien
en escenarios del mundo real. No se salte la creación de un conjunto de prueba: ¡es un requisito saber qué tan bien
funciona realmente su modelo!

Vamos a crear ahora un conjunto de prueba simple basado en una división aleatoria de los datos (usaremos este conjunto
de prueba en el resto del capítulo):

// en escala
Machine Translated by Google

val Array(entrenamiento, prueba) = preparadoDF.randomSplit(Array(0.7, 0.3))

# en Python
tren, prueba = preparadoDF.randomSplit([0.7, 0.3])

Estimadores
Ahora que hemos transformado nuestros datos en el formato correcto y creado algunos valiosos

características, es hora de adaptarse realmente a nuestro modelo. En este caso utilizaremos un algoritmo de
clasificación llamado regresión logística. Para crear nuestro clasificador, instanciamos una instancia
de LogisticRegression, utilizando la configuración predeterminada o los hiperparámetros. Luego configuramos las columnas
de etiquetas y las columnas de características; los nombres de columna que estamos configurando (etiqueta y funciones)
son en realidad las etiquetas predeterminadas para todos los estimadores en Spark MLlib, y en capítulos posteriores los omitiremos:

// en Scala
import org.apache.spark.ml.classification.LogisticRegression val lr = new
LogisticRegression().setLabelCol("label").setFeaturesCol("features")

# en Python
desde pyspark.ml.classification import LogisticRegression lr =
LogisticRegression(labelCol="label",featuresCol="features")

Antes de comenzar a entrenar este modelo, inspeccionemos los parámetros. Esta es también una excelente manera de
recordar las opciones disponibles para cada modelo en particular:

// en Scala
println(lr.explainParams())

# en Python
imprimir lr.explainParams()

Si bien el resultado es demasiado grande para reproducirlo aquí, muestra una explicación de todos los parámetros para la
implementación de la regresión logística de Spark. El método ExplainParams existe en todos los algoritmos disponibles
en MLlib.

Al instanciar un algoritmo no entrenado, llega el momento de ajustarlo a los datos. En este caso, esto devuelve un
LogisticRegressionModel:

// en Scala
val equipadoLR = lr.fit(tren)

# en Python
equipadoLR = lr.fit(tren)

Este código iniciará un trabajo de Spark para entrenar el modelo. A diferencia de las transformaciones que vio a lo largo del
libro, el ajuste de un modelo de aprendizaje automático es rápido y se realiza de inmediato.
Machine Translated by Google

Una vez completado, puede usar el modelo para hacer predicciones. Lógicamente esto significa transformar
características en etiquetas. Hacemos predicciones con el método de transformación. Por ejemplo, podemos
transformar nuestro conjunto de datos de entrenamiento para ver qué etiquetas asignó nuestro modelo a los datos de
entrenamiento y cómo se comparan con los resultados reales. Esto, nuevamente, es solo otro DataFrame que podemos manipular.
Realicemos esa predicción con el siguiente fragmento de código:

equipadoLR.transform(tren).select("etiqueta", "predicción").show()

Esto resulta en:

+­­­­­+­­­­­­­­­­+
|etiqueta|predicción|
+­­­­­+­­­­­­­­­­+
| 0.0| 0.0|
...
| 0.0| 0.0|
+­­­­­+­­­­­­­­­­+

Nuestro siguiente paso sería evaluar manualmente este modelo y calcular métricas de rendimiento como la tasa de
verdaderos positivos, la tasa de falsos negativos, etc. Entonces podríamos dar la vuelta y probar un conjunto
diferente de parámetros para ver si funcionan mejor. Sin embargo, si bien este es un proceso útil, también puede ser
bastante tedioso. Spark lo ayuda a evitar probar manualmente diferentes modelos y criterios de evaluación al
permitirle especificar su carga de trabajo como una canalización de trabajo declarativa que incluye todas sus
transformaciones y ajusta sus hiperparámetros.

UNA REVISIÓN DE LOS HIPERPARÁMETROS

Aunque los mencionamos anteriormente, definamos más formalmente los hiperparámetros.


Los hiperparámetros son parámetros de configuración que afectan el proceso de entrenamiento, como la arquitectura
del modelo y la regularización. Se fijan antes de comenzar el entrenamiento. Por ejemplo, la regresión logística
tiene un hiperparámetro que determina cuánta regularización se debe realizar en nuestros datos durante
la fase de entrenamiento (la regularización es una técnica que empuja a los modelos contra el sobreajuste de
datos). Verá en las siguientes páginas que podemos configurar nuestra canalización para probar diferentes valores
de hiperparámetros (por ejemplo, diferentes valores de regularización) para comparar diferentes variaciones del
mismo modelo entre sí.

Canalización de nuestro flujo de trabajo

Como probablemente haya notado, si está realizando muchas transformaciones, escribir todos los pasos y realizar un
seguimiento de los DataFrames termina siendo bastante tedioso. Por eso Spark incluye el concepto Pipeline.
Una canalización le permite configurar un flujo de datos de las transformaciones relevantes que finaliza con un
estimador que se ajusta automáticamente de acuerdo con sus especificaciones, lo que da como resultado un modelo
ajustado listo para usar. La figura 24­4 ilustra este proceso.
Machine Translated by Google

Figura 24­4. Canalización del flujo de trabajo de ML

Tenga en cuenta que es esencial que las instancias de transformadores o modelos no se reutilicen en diferentes

canalizaciones. Siempre cree una nueva instancia de un modelo antes de crear otra canalización.

Para asegurarnos de no sobreajustar, vamos a crear un conjunto de prueba de reserva y ajustar nuestros
hiperparámetros en función de un conjunto de validación (tenga en cuenta que creamos este conjunto de validación
en función del conjunto de datos original, no del preparado DF utilizado en el anterior páginas):

// en Scala
val Array(tren, prueba) = df.randomSplit(Array(0.7, 0.3))

# en el tren
Python , prueba = df.randomSplit([0.7, 0.3])

Ahora que tiene un conjunto reservado, creemos las etapas base en nuestra canalización. Una etapa simplemente
representa un transformador o un estimador. En nuestro caso, tendremos dos estimadores. RFomula primero analizará
nuestros datos para comprender los tipos de características de entrada y luego los transformará para crear nuevas
características. Posteriormente, el objeto LogisticRegression es el algoritmo que entrenaremos para producir un modelo:

// en Scala
val rForm = new RFormula()
val lr = new LogisticRegression().setLabelCol("label").setFeaturesCol("features")

# en Python
rForm = RFormula()
Machine Translated by Google

lr = LogisticRegression().setLabelCol("etiqueta").setFeaturesCol("características")

Estableceremos los valores potenciales para RFormula en la siguiente sección. Ahora, en lugar de usar manualmente
nuestras transformaciones y luego ajustar nuestro modelo, simplemente las hacemos etapas en la canalización
general, como en el siguiente fragmento de código:

// en Scala
importar org.apache.spark.ml.Pipeline val
etapas = Array(rForm, lr) val
tubería = new Pipeline().setStages(etapas)

# en Python
desde pyspark.ml import Etapas de
canalización = [rForm,
lr] canalización = Canalización().setStages(etapas)

Capacitación y Evaluación
Ahora que dispuso la canalización lógica, el siguiente paso es la capacitación. En nuestro caso, no entrenaremos
solo un modelo (como hicimos anteriormente); entrenaremos varias variaciones del modelo especificando
diferentes combinaciones de hiperparámetros que nos gustaría que Spark probara. Luego, seleccionaremos el mejor
modelo utilizando un Evaluador que compare sus predicciones con nuestros datos de validación. Podemos probar
diferentes hiperparámetros en todo el proceso, incluso en la RFormula que usamos para manipular los datos sin
procesar. Este código muestra cómo hacemos eso:

// en Scala
import org.apache.spark.ml.tuning.ParamGridBuilder val
params = new
ParamGridBuilder() .addGrid(rForm.formula,
. ~ + color:value1",
Array( "lab
.
"lab ~ + color:value1 + color :valor2"))
.addGrid(lr.elasticNetParam, Array(0.0, 0.5,
1.0)) .addGrid(lr.regParam, Array(0.1,
2.0)) .build()

# en Python
desde pyspark.ml.tuning import ParamGridBuilder
params = ParamGridBuilder()
\ .addGrid(rForm.formula,
[ "lab ~ +. color:value1", "lab ~
.
+ color:value1 + color:value2"])\ .
agregarGrid(lr.elasticNetParam, [0.0, 0.5, 1.0])
\ .addGrid(lr.regParam, [0.1, 2.0])\ .build()

En nuestra cuadrícula de parámetros actual, hay tres hiperparámetros que divergen de los valores predeterminados:

Dos versiones diferentes de la RFormula


Machine Translated by Google

Tres opciones diferentes para el parámetro ElasticNet

Dos opciones diferentes para el parámetro de regularización

Esto nos da un total de 12 combinaciones diferentes de estos parámetros, lo que significa que estaremos entrenando
12 versiones diferentes de regresión logística. Explicamos el parámetro ElasticNet así como las opciones de
regularización en el Capítulo 26.

Ahora que la cuadrícula está construida, es hora de especificar nuestro proceso de evaluación. El evaluador nos
permite comparar de forma automática y objetiva varios modelos con la misma métrica de evaluación. Hay evaluadores
para clasificación y regresión, que se tratarán en capítulos posteriores, pero en este caso usaremos el
BinaryClassificationEvaluator, que tiene varias métricas de evaluación potenciales, como veremos en el Capítulo 26.
En este caso, usaremos areaUnderROC , que es el área total bajo la característica operativa del receptor, una medida
común del rendimiento de clasificación:

// en Scala
importar org.apache.spark.ml.evaluacion.BinaryClassificationEvaluator val
evaluador = new

BinaryClassificationEvaluator() .setMetricName("areaUnderROC") .setRawPredictionCol("prediction") .setLabelCol("label")

# en Python
desde pyspark.ml.e Evaluation import BinaryClassificationEvaluator evaluador
= BinaryClassificationEvaluator()
\ .setMetricName("areaUnderROC")
\ .setRawPredictionCol("prediction")
\ .setLabelCol("label")

Ahora que tenemos una canalización que especifica cómo se deben transformar nuestros datos, realizaremos la
selección del modelo para probar diferentes hiperparámetros en nuestro modelo de regresión logística y
medir el éxito comparando su rendimiento con la métrica areaUnderROC.

Como comentamos, es una práctica recomendada en el aprendizaje automático ajustar los hiperparámetros en un
conjunto de validación (en lugar de su conjunto de prueba) para evitar el sobreajuste. Por esta razón, no podemos usar
nuestro conjunto de prueba de exclusión (que creamos antes) para ajustar estos parámetros. Afortunadamente,
Spark ofrece dos opciones para realizar el ajuste de hiperparámetros automáticamente. Podemos usar
TrainValidationSplit, que simplemente realizará una división aleatoria arbitraria de nuestros datos en dos
grupos diferentes, o CrossValidator, que realiza una validación cruzada de K­fold al dividir el conjunto de datos
en k pliegues particionados aleatoriamente que no se superponen :

// en Scala
import org.apache.spark.ml.tuning.TrainValidationSplit val tvs =
new TrainValidationSplit()
.setTrainRatio(0.75) // también el

predeterminado. .setEstimatorParamMaps(parámetros) .setEstimator(canalización) .setEvaluator(evaluador)


Machine Translated by Google

# en Python
desde pyspark.ml.tuning import TrainValidationSplit tvs =
TrainValidationSplit()

\ .setTrainRatio(0.75)\ .setEstimatorParamMaps(params)\ .setEstimator(pipeline)\ .setEvaluator(evaluator)

Ejecutemos toda la canalización que construimos. Para repasar, al ejecutar esta canalización se probarán todas las
versiones del modelo con el conjunto de validación. Tenga en cuenta que el tipo de tvsFitted es
TrainValidationSplitModel. Cada vez que ajustamos un modelo dado, genera un tipo de "modelo":

// en Scala
val tvsFitted = tvs.fit(tren)

# en Python
tvsFitted = tvs.fit(tren)

¡Y, por supuesto, evalúe cómo funciona en el conjunto de prueba!

evaluador.evaluar(tvsFitted.transform(test)) // 0.9166666666666667

También podemos ver un resumen de entrenamiento para algunos modelos. Para hacer esto, lo extraemos de la
canalización, lo convertimos al tipo adecuado e imprimimos nuestros resultados. Las métricas disponibles en cada
modelo se analizan a lo largo de los próximos capítulos. Así es como podemos ver los resultados:

// en Scala
import org.apache.spark.ml.PipelineModel import
org.apache.spark.ml.classification.LogisticRegressionModel val TrainedPipeline
= tvsFitted.bestModel.asInstanceOf[PipelineModel] val TrainedLR =
TrainedPipeline.stages(1).asInstanceOf[LogisticRegressionModel ] val summaryLR =
TrainedLR.summary
summaryLR.objectiveHistory // 0.6751425885789243, 0.5543659647777687, 0.473776...

El historial objetivo que se muestra aquí proporciona detalles relacionados con el rendimiento de nuestro algoritmo
en cada iteración de entrenamiento. Esto puede ser útil porque podemos observar el progreso de nuestro algoritmo
hacia el mejor modelo. Por lo general, se esperan grandes saltos al principio, pero con el tiempo los valores
deberían volverse cada vez más pequeños, con solo pequeñas cantidades de variación entre los valores.

Persistencia y aplicación de modelos


Ahora que entrenamos este modelo, podemos conservarlo en el disco para usarlo con fines de predicción más adelante:

tvsFitted.write.overwrite().save("/tmp/modelLocation")

Después de escribir el modelo, podemos cargarlo en otro programa Spark para hacer predicciones. A
Machine Translated by Google

Para hacer esto, necesitamos usar una versión de "modelo" de nuestro algoritmo particular para cargar nuestro modelo
persistente desde el disco. Si tuviéramos que usar CrossValidator, tendríamos que leer en la versión persistente como
CrossValidatorModel, y si tuviéramos que usar LogisticRegression manualmente, tendríamos que usar
LogisticRegressionModel. En este caso, usamos TrainValidationSplit, que genera TrainValidationSplitModel:

// en Scala
import org.apache.spark.ml.tuning.TrainValidationSplitModel val model
= TrainValidationSplitModel.load("/tmp/modelLocation") model.transform(test)

Patrones de implementación
En Spark hay varios patrones de implementación diferentes para poner en producción modelos de aprendizaje automático.
La figura 24­5 ilustra flujos de trabajo comunes.

Figura 24­5. El proceso de produccion

Estas son las diversas opciones sobre cómo podría implementar un modelo de Spark. Estas son las opciones generales
que debería poder vincular al proceso ilustrado en la Figura 24­5.

Entrene su modelo de aprendizaje automático (ML) fuera de línea y luego suministre datos fuera de línea. En
este contexto, queremos decir que los datos fuera de línea son datos que se almacenan para su análisis, y no
datos de los que necesita obtener una respuesta rápidamente. Spark se adapta bien a este tipo de implementación.

Entrene su modelo sin conexión y luego coloque los resultados en una base de datos (generalmente un
almacén de clave­valor). Esto funciona bien para algo como la recomendación, pero no para algo como la
clasificación o la regresión, donde no puede simplemente buscar un valor para un usuario determinado, sino
que debe calcular uno en función de la entrada.
Machine Translated by Google

Entrene su algoritmo de ML sin conexión, guarde el modelo en el disco y luego utilícelo para servir.
Esta no es una solución de baja latencia si usa Spark para la parte de servicio, ya que la sobrecarga de
iniciar un trabajo de Spark puede ser alta, incluso si no se está ejecutando en un clúster.
Además, esto no funciona bien en paralelo, por lo que es probable que tenga que colocar un equilibrador de
carga frente a varias réplicas de modelos y crear una integración de API REST usted mismo.
Existen algunas soluciones potenciales interesantes para este problema, pero actualmente no existen
estándares para este tipo de servicio de modelos.

Convierta manualmente (o mediante algún otro software) su modelo distribuido en uno que pueda
ejecutarse mucho más rápido en una sola máquina. Esto funciona bien cuando no hay demasiada
manipulación de los datos sin procesar en Spark, pero puede ser difícil de mantener con el tiempo.
Nuevamente, hay varias soluciones en progreso. Por ejemplo, MLlib puede exportar algunos modelos
a PMML, un formato común de intercambio de modelos.

Entrene su algoritmo ML en línea y utilícelo en línea. Esto es posible cuando se usa junto con la
transmisión estructurada, pero puede ser complejo para algunos modelos.

Si bien estas son algunas de las opciones, existen muchas otras formas de realizar la implementación y administración
del modelo. Esta es un área en fuerte desarrollo y actualmente se está trabajando en muchas innovaciones
potenciales.

Conclusión
En este capítulo, cubrimos los conceptos básicos detrás del análisis avanzado y MLlib. También te mostramos
cómo usarlos. El próximo capítulo analizará el preprocesamiento en profundidad, incluidas las herramientas de
Spark para la ingeniería de funciones y la limpieza de datos. Luego pasaremos a descripciones detalladas
de cada algoritmo disponible en MLlib junto con algunas herramientas para análisis de gráficos y aprendizaje profundo.
Machine Translated by Google

Capítulo 25. Preprocesamiento e ingeniería de


características

Cualquier científico de datos que se precie sabe que uno de los mayores desafíos (y pérdidas de tiempo) en el
análisis avanzado es el preprocesamiento. No es que sea una programación particularmente complicada, sino que
requiere un conocimiento profundo de los datos con los que está trabajando y una comprensión de lo que su modelo
necesita para aprovechar con éxito estos datos. Este capítulo cubre los detalles de cómo puede usar Spark para
realizar el preprocesamiento y la ingeniería de funciones. Veremos los requisitos básicos que deberá cumplir para
entrenar un modelo MLlib en términos de cómo se estructuran sus datos. Luego discutiremos las diferentes
herramientas que Spark pone a disposición para realizar este tipo de trabajo.

Formato de modelos según su caso de uso


Para preprocesar datos para las diferentes herramientas de análisis avanzado de Spark, debe considerar su
objetivo final. La siguiente lista recorre los requisitos para la estructura de datos de entrada para cada tarea de
análisis avanzado en MLlib:

En el caso de la mayoría de los algoritmos de clasificación y regresión, desea obtener sus datos en una
columna de tipo Doble para representar la etiqueta y una columna de tipo Vector (denso o disperso) para
representar las características.

En el caso de la recomendación, desea obtener sus datos en una columna de usuarios, una columna
de elementos (por ejemplo, películas o libros) y una columna de calificaciones.

En el caso del aprendizaje no supervisado, se necesita una columna de tipo Vector (denso o disperso) para
representar las características.

En el caso del análisis de gráficos, necesitará un marco de datos de vértices y un marco de datos de
bordes.

La mejor manera de obtener sus datos en estos formatos es a través de transformadores. Los transformadores
son funciones que aceptan un DataFrame como argumento y devuelven un nuevo DataFrame como respuesta.
Este capítulo se centrará en qué transformadores son relevantes para casos de uso particulares en lugar de
intentar enumerar todos los transformadores posibles.

NOTA

Spark proporciona varios transformadores como parte del paquete org.apache.spark.ml.feature. El paquete
correspondiente en Python es pyspark.ml.feature. Constantemente aparecen nuevos transformadores
en Spark MLlib y, por lo tanto, es imposible incluir una lista definitiva en este libro. La información más
actualizada se puede encontrar en el sitio de documentación de Spark.
Machine Translated by Google

Antes de continuar, vamos a leer varios conjuntos de datos de muestra diferentes, cada uno de los cuales tiene
diferentes propiedades que manipularemos en este capítulo:

// en escala
val ventas = chispa.read.format("csv")
.option("encabezado", "verdadero")
.option("inferirEsquema", "verdadero")
.load("/datos/datos­minoristas/por­día/*.csv")
.coalesce(5)
.where("La descripción NO ES NULA")
val fakeIntDF = chispa.read.parquet("/data/simple­ml­integers")
var simpleDF = chispa.read.json("/datos/simple­ml")
val scaleDF = chispa.read.parquet("/data/simple­ml­scaling")

# en pitón
ventas = spark.read.format("csv")\
.option("encabezado", "verdadero")\
.opción("inferirEsquema", "verdadero")\
.load("/data/retail­data/by­day/*.csv")\
.coalesce(5)\
.where("La descripción NO ES NULA")
fakeIntDF = chispa.read.parquet("/data/simple­ml­integers")
simpleDF = chispa.read.json("/datos/simple­ml")
scaleDF = chispa.read.parquet("/data/simple­ml­scaling")

Además de estos datos de ventas realistas, vamos a utilizar varios conjuntos de datos sintéticos simples como
Bueno. FakeIntDF, simpleDF y scaleDF tienen muy pocas filas. Esto le dará la capacidad
para centrarnos en la manipulación de datos exacta que estamos realizando en lugar de las diversas inconsistencias
de cualquier conjunto de datos en particular. Debido a que vamos a acceder a los datos de ventas varias veces,
vamos a almacenarlo en caché para poder leerlo de manera eficiente desde la memoria en lugar de leerlo desde
disco cada vez que lo necesitemos. También revisemos las primeras filas de datos para mejorar
comprender lo que hay en el conjunto de datos:

ventas.cache()
ventas.mostrar()

+­­­­­­­­­+­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­ +­­­­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­
|Número de factura|Código de existencias| Descripción|Cantidad| FacturaFecha|UnidadPr...
+­­­­­­­­­+­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­ +­­­­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­
| 580538| 23084| LUZ DE NOCHE CONEJO| 48|2011­12­05 08:38:00| 1...
...
| 580539| 22375|BOLSA AIRLINE VINTA...| 4|2011­12­05 08:39:00| 4...
+­­­­­­­­­+­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­ +­­­­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­

NOTA
Machine Translated by Google

Es importante tener en cuenta que filtramos los valores nulos aquí. MLlib no siempre funciona bien con valores nulos en este
momento. Esta es una causa frecuente de problemas y errores y un gran primer paso cuando está depurando. También se
realizan mejoras con cada versión de Spark para mejorar el manejo de algoritmos de valores nulos.

Transformadores
Discutimos los transformadores en el capítulo anterior, pero vale la pena revisarlos nuevamente aquí.
Los transformadores son funciones que convierten datos sin procesar de alguna manera. Esto podría ser para crear una
nueva variable de interacción (a partir de otras dos variables), para normalizar una columna o simplemente para convertirla en

un Doble para ingresar en un modelo. Los transformadores se utilizan principalmente en el preprocesamiento o la generación
de características.

El transformador de Spark solo incluye un método de transformación. Esto se debe a que no cambiará en función de los datos
de entrada. La figura 25­1 es una ilustración simple. A la izquierda hay un DataFrame de entrada con la columna que se va a
manipular. A la derecha está el DataFrame de entrada con una nueva columna que representa la transformación de salida.

Figura 25­1. Un transformador de chispa

El Tokenizer es un ejemplo de transformador. Tokeniza una cadena, se divide en un carácter dado y no tiene nada
que aprender de nuestros datos; simplemente aplica una función. Discutiremos el tokenizador con más profundidad más
adelante en este capítulo, pero aquí hay un pequeño fragmento de código que muestra cómo se construye un tokenizador
para aceptar la columna de entrada, cómo transforma los datos y luego el resultado de esa transformación:

// en Scala
import org.apache.spark.ml.feature.Tokenizer val tkn = new
Tokenizer().setInputCol("Descripción")
tkn.transform(sales.select("Descripción")).show(false)

+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­ ­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
Machine Translated by Google

|Descripción |tok_7de4dfc81ab7__salida |
+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­ ­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
|LUZ NOCTURNA CONEJO |[conejo, noche, luz] |[rosquilla,
|BRILLO DE LABIOS DOUGHUT labio, brillo] ||

...
|AIRLINE BAG VINTAGE WORLD CHAMPION |[airline, bag, vintage, world, champion] |
|BOLSO AIRLINE VINTAGE JET SET MARRÓN |[airline, bolso, vintage, jet, set, marrón] |
+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­ ­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+

Estimadores para preprocesamiento


Otra herramienta para el preprocesamiento son los estimadores. Un estimador es necesario cuando una transformación
que le gustaría realizar debe inicializarse con datos o información sobre la columna de entrada (a menudo derivada
al pasar por la columna de entrada). Por ejemplo, si quisiera escalar los valores en nuestra columna para tener media
cero y varianza unitaria, necesitaría realizar un pase sobre todos los datos para calcular los valores que usaría para
normalizar los datos a media cero y unidad. diferencia. En efecto, un estimador puede ser un transformador configurado
de acuerdo con sus datos de entrada particulares. En términos más simples, puede aplicar ciegamente una
transformación (un tipo de transformador "regular") o realizar una transformación basada en sus datos (un tipo de
estimador). La Figura 25­2 es una ilustración simple de un estimador que se ajusta a un conjunto de datos de entrada
en particular, generando un transformador que luego se aplica al conjunto de datos de entrada para agregar
una nueva columna (de los datos transformados).

Figura 25­2. Un estimador Spark

Un ejemplo de este tipo de estimador es StandardScaler, que escala su columna de entrada según el rango de
valores en esa columna para tener una media de cero y una varianza de 1 en cada dimensión. Por esa razón primero
debe realizar un pase sobre los datos para crear el transformador.
Aquí hay un fragmento de código de muestra que muestra todo el proceso, así como el resultado:

// en Scala
import org.apache.spark.ml.feature.StandardScaler val ss = new
StandardScaler().setInputCol("features")
Machine Translated by Google

ss.fit(scaleDF).transform(scaleDF).show(false)

+­­­+­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­ ­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
|identificación |características |stdScal_d66fbeac10ea__salida |
+­­­+­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­ ­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
|0 |[1.0,0.1,­1.0]|[1.1952286093343936,0.02337622911060922,­0.5976143046671968]|
...
|1 |[3.0,10.1,3.0]|[3.5856858280031805,2.3609991401715313,1.7928429140015902] |
+­­­+­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­ ­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+

Usaremos estimadores y transformadores a lo largo y cubriremos más sobre estos estimadores particulares
(y agregaremos ejemplos en Python) más adelante en este capítulo.

Propiedades del transformador


Todos los transformadores requieren que especifique, como mínimo, inputCol y outputCol, que representan
el nombre de columna de la entrada y la salida, respectivamente. Los configura con setInputCol
y setOutputCol. Hay algunos valores predeterminados (puede encontrarlos en la documentación),
pero es una buena práctica especificarlos manualmente usted mismo para mayor claridad. Además de las
columnas de entrada y salida, todos los transformadores tienen diferentes parámetros que puede
ajustar (siempre que mencionemos un parámetro en este capítulo, debe configurarlo con un método
set()). En Python, también tenemos otro método para establecer estos valores con argumentos de palabras
clave para el constructor del objeto. Los excluimos de los ejemplos en el próximo capítulo por coherencia. Los
estimadores requieren que ajuste el transformador a su conjunto de datos particular y luego llame a
transform en el objeto resultante.

NOTA

Spark MLlib almacena metadatos sobre las columnas que usa en cada DataFrame como un atributo en la
propia columna. Esto le permite almacenar (y anotar) correctamente que una columna de Dobles en realidad
puede representar una serie de variables categóricas en lugar de valores continuos. Sin embargo, los metadatos
no aparecerán cuando imprima el esquema o el DataFrame.

Transformadores de alto nivel


Los transformadores de alto nivel, como el RFormula que vimos en el capítulo anterior, le permiten
especificar de manera concisa varias transformaciones en una. Estos operan a un "alto nivel" y le permiten
evitar realizar manipulaciones o transformaciones de datos uno por uno. En general, debe intentar utilizar los
transformadores de más alto nivel que pueda para minimizar el riesgo de error y ayudarlo a concentrarse en
el problema comercial en lugar de los detalles más pequeños de la implementación. Si bien esto no siempre
es posible, es un buen objetivo.

RFórmula
Machine Translated by Google

RFormula es el transformador más fácil de usar cuando tiene datos formateados "convencionalmente".
Spark toma prestado este transformador del lenguaje R para simplificar la especificación declarativa de un conjunto de
transformaciones para sus datos. Con este transformador, los valores pueden ser numéricos o categóricos y no
necesita extraer valores de cadenas ni manipularlos de ninguna manera.
RFormula manejará automáticamente las entradas categóricas (especificadas como cadenas) al realizar algo llamado
codificación one­hot. En resumen, la codificación one­hot convierte un conjunto de valores en un conjunto de columnas
binarias que especifican si el punto de datos tiene o no cada valor en particular (hablaremos de la codificación one­
hot con más profundidad más adelante en este capítulo). Con RFormula, las columnas numéricas se convertirán
en Double pero no se codificarán en caliente. Si la columna de la etiqueta es de tipo String, primero se transformará a
Double con StringIndexer.

ADVERTENCIA

La conversión automática de columnas numéricas a Double sin codificación one­hot tiene algunas
implicaciones importantes. Si tiene variables categóricas con valores numéricos, solo se convertirán en Doble,
especificando implícitamente un orden. Es importante asegurarse de que los tipos de entrada correspondan a
la conversión esperada. Si tiene variables categóricas que realmente no tienen una relación de orden, deben
convertirse en String. También puede indexar columnas manualmente (consulte “Trabajar con características categóricas”).

La RFormula le permite especificar sus transformaciones en sintaxis declarativa. Es fácil de usar una vez que
comprende la sintaxis. Actualmente, RFormula admite un subconjunto limitado de operadores R que, en la práctica,
funcionan bastante bien para transformaciones simples. Los operadores básicos son:

Objetivo y términos separados

Concatenar términos; "+ 0" significa eliminar la intersección (esto significa que la intersección y de la línea que
ajustaremos será 0)

Eliminar un término; "­ 1" significa eliminar el intercepto (esto significa que el intercepto en y de la línea que
ajustaremos será 0)

Interacción (multiplicación de valores numéricos o valores categóricos binarizados)

Todas las columnas excepto la variable objetivo/dependiente

RFormula también usa columnas predeterminadas de etiqueta y funciones para etiquetar, lo adivinó, la etiqueta y el
conjunto de funciones que genera (para el aprendizaje automático supervisado). Los modelos que se tratan más adelante
en este capítulo requieren de forma predeterminada esos nombres de columna, lo que facilita pasar el resultado
Machine Translated by Google

transformó DataFrame en un modelo para entrenamiento. Si esto aún no tiene sentido, no se preocupe—
quedará claro una vez que realmente comencemos a usar modelos en capítulos posteriores.

Usemos RFormula en un ejemplo. En este caso, queremos usar todas las variables disponibles (el .) y
luego especifique una interacción entre valor1 y color y valor2 y color como adicional
caracteristicas para generar:

// en escala
importar org.apache.spark.ml.feature.RFormula
val supervisado = nueva RFormula()
.setFormula("laboratorio. ~ + color:valor1 + color:valor2")
supervisado.fit(simpleDF).transform(simpleDF).show()

# en pitón
desde pyspark.ml.feature importar RFormula

.
supervisado = RFormula(formula="lab ~ + color:valor1 + color:valor2")
supervisado.fit(simpleDF).transform(simpleDF).show()

+­­­­­+­­­­+­­­­­­+­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­ ­­­­­­­­+­­­­­­+
|color| laboratorio|valor1| valor2| caracteristicas|etiqueta|
+­­­­­+­­­­+­­­­­­+­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­ ­­­­­­­­+­­­­­­+
|verde|bueno| 1|14.386294994851129|(10,[1,2,3,5,8],[...| 1,0|
| azul| mal| 8|14,386294994851129|(10,[2,3,6,9],[8....| 0,0|
...
| rojo| mal| | 1| 38.97187133755819|(10,[0,2,3,4,7],[...| 0,0|
rojo| mal| 2|14,386294994851129|(10,[0,2,3,4,7],[...| 0,0|
+­­­­­+­­­­+­­­­­­+­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­ ­­­­­­­­+­­­­­­+

Transformadores SQL
Un SQLTransformer le permite aprovechar la amplia biblioteca de manipulaciones relacionadas con SQL de Spark
tal como lo haría con una transformación MLlib. Cualquier declaración SELECT que pueda usar en SQL es válida
transformación. Lo único que debe cambiar es que, en lugar de usar el nombre de la tabla,
solo debe usar la palabra clave ESTE. Es posible que desee utilizar SQLTransformer si desea
codificar formalmente alguna manipulación de DataFrame como un paso de preprocesamiento, o probar SQL diferente
expresiones para funciones durante el ajuste de hiperparámetros. También tenga en cuenta que la salida de este
la transformación se agregará como una columna al DataFrame de salida.

Es posible que desee utilizar un SQLTransformer para representar todas sus manipulaciones en el
la forma más cruda de sus datos para que pueda versionar diferentes variaciones de manipulaciones como
transformadores Esto le brinda el beneficio de construir y probar diferentes canalizaciones, todo simplemente
cambio de transformadores. El siguiente es un ejemplo básico del uso de SQLTransformer:

// en escala
importar org.apache.spark.ml.feature.SQLTransformer

val basicTransformation = nuevo SQLTransformer()


Machine Translated by Google

.setStatement("""
SELECCIONE suma (cantidad), recuento (*), CustomerID
DE __ESTE__
GRUPO POR CustomerID
""")

basicTransformation.transform(ventas).show()

# en Python
desde pyspark.ml.feature import SQLTransformer

transformaciónbásica = SQLTransformer()
\ .setStatement("""
SELECCIONE suma (cantidad), recuento (*), CustomerID
DE __ESTE__
GRUPO POR CustomerID
""")

basicTransformation.transform(ventas).show()

Aquí hay una muestra de la salida:

­­­­­­­­­­­­­+­­­­­­­­+­­­­­­­­­­+
|suma(Cantidad)|recuento(1)|IDCliente|
+­­­­­­­­­­­­­+­­­­­­­­+­­­­­­­­­­+
| 119| 62| 14452.0|
...
| 138| 18| 15776.0|
+­­­­­­­­­­­­­+­­­­­­­­+­­­­­­­­­­+

Para obtener ejemplos extensos de estas transformaciones, consulte la Parte II.

Ensamblador de vectores
VectorAssembler es una herramienta que usará en casi todas las canalizaciones que genere. Ayuda a
concatenar todas sus funciones en un gran vector que luego puede pasar a un estimador. Por lo general, se
usa en el último paso de una canalización de aprendizaje automático y toma como entrada una cantidad de
columnas de Boolean, Double o Vector. Esto es particularmente útil si va a realizar una serie de manipulaciones
utilizando una variedad de transformadores y necesita reunir todos esos resultados.

El resultado del siguiente fragmento de código dejará claro cómo funciona esto:

// en Scala
import org.apache.spark.ml.feature.VectorAssembler val va
= new VectorAssembler().setInputCols(Array("int1", "int2", "int3"))
va.transform(fakeIntDF).show( )

# en Python
desde pyspark.ml.feature import VectorAssembler va =
VectorAssembler().setInputCols(["int1", "int2", "int3"])
Machine Translated by Google

va.transform(falsoIntDF).show()

+­­­­+­­­­+­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­ ­­­­­­­­­­+
|int1|int2|int3|VectorAssembler_403ab93eacd5585ddd2d__salida|
+­­­­+­­­­+­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­ ­­­­­­­­­­+
| 1| 2| 3| [1.0,2.0,3.0]|
| 4| 5| 6| [4.0,5.0,6.0]|
| 7| 8| 9| [7.0,8.0,9.0]|
+­­­­+­­­­+­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­ ­­­­­­­­­­+

Trabajar con funciones continuas


Las características continuas son solo valores en la recta numérica, desde infinito positivo hasta infinito negativo.
Hay dos transformadores comunes para características continuas. Primero, puede convertir continuo
características en características categóricas a través de un proceso llamado agrupamiento, o puede escalar y normalizar
sus características de acuerdo con varios requisitos diferentes. Estos transformadores solo funcionarán en
Tipos dobles, así que asegúrese de haber cambiado cualquier otro valor numérico a Doble:

// en escala
val contDF = spark.range(20).selectExpr("cast(id as double)")

# en pitón
contDF = spark.range(20).selectExpr("cast(id as double)")

agrupamiento

El enfoque más sencillo para agrupar o agrupar es usar el Bucketizer. Esta voluntad
dividir una característica continua dada en los cubos de su designación. Usted especifica cómo los cubos
debe crearse a través de una matriz o lista de valores dobles. Esto es útil porque es posible que desee
simplifique las características en su conjunto de datos o simplifique sus representaciones para su interpretación posterior.
Por ejemplo, imagina que tienes una columna que representa el peso de una persona y te gustaría
predecir algún valor basado en esta información. En algunos casos, podría ser más sencillo crear tres
cubos de "sobrepeso", "promedio" y "bajo peso".

Para especificar el depósito, establezca sus bordes. Por ejemplo, establecer divisiones en 5.0, 10.0, 250.0 en nuestro
contDF en realidad fallará porque no cubrimos todos los rangos de entrada posibles. Al especificar su
puntos de cubo, los valores que pase a las divisiones deben cumplir tres requisitos:

El valor mínimo en su matriz de divisiones debe ser menor que el valor mínimo en su
Marco de datos.

El valor máximo en su matriz de divisiones debe ser mayor que el valor máximo en
su marco de datos.

Debe especificar como mínimo tres valores en la matriz de divisiones, lo que crea dos
baldes
Machine Translated by Google

ADVERTENCIA

El Bucketizer puede ser confuso porque especificamos los bordes de los cubos a través del método de división, pero en
realidad no son divisiones.

Para cubrir todos los rangos posibles, scala.Double.NegativeInfinity podría ser otra opción de división, con
scala.Double.PositiveInfinity para cubrir todos los rangos posibles fuera de las divisiones internas.
En Python especificamos esto de la siguiente manera: float("inf"), float("­inf").

Para manejar valores nulos o NaN, debemos especificar el parámetro handleInvalid como un valor determinado.
Podemos mantener esos valores (mantener), error o nulo, u omitir esas filas.
Aquí hay un ejemplo del uso de cubos:

// en Scala
import org.apache.spark.ml.feature.Bucketizer val
bucketBorders = Array(­1.0, 5.0, 10.0, 250.0, 600.0) val bucketer =
new Bucketizer().setSplits(bucketBorders).setInputCol("id" ) cubo.transformar(contDF).mostrar()

# en Python
desde pyspark.ml.feature import Bucketizer
bucketBorders = [­1.0, 5.0, 10.0, 250.0, 600.0] bucketer
= Bucketizer().setSplits(bucketBorders).setInputCol("id")
bucketer.transform(contDF).show ()

+­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
| id|Bucketizer_4cb1be19f4179cc2545d__salida|
+­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
| 0.0| 0.0|
...
|10.0| | 2.0|
11.0| 2.0|
...
+­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+

Además de dividir en función de los valores codificados, otra opción es dividir en función de los percentiles en nuestros
datos. Esto se hace con QuantileDiscretizer, que dividirá los valores en depósitos especificados por el usuario con
las divisiones determinadas por valores de cuantiles aproximados. Por ejemplo, el cuantil 90 es el punto de sus datos en
el que el 90 % de los datos está por debajo de ese valor. Puede controlar la precisión con la que se deben dividir los
cubos configurando el error relativo para el cálculo de cuantiles aproximados mediante setRelativeError. Spark hace
esto al permitirle especificar la cantidad de cubos que desea de los datos y dividirá sus datos en consecuencia. Lo
siguiente es un ejemplo:

// en Scala
importar org.apache.spark.ml.feature.QuantileDiscretizer
Machine Translated by Google

val depósito = new QuantileDiscretizer().setNumBuckets(5).setInputCol("id") val


equipadoBucketer = depósito.fit(contDF)
ajustadoBucketer.transform(contDF).show()

# en Python
desde pyspark.ml.feature import QuantileDiscretizer bucketer
= QuantileDiscretizer().setNumBuckets(5).setInputCol("id") equipadoBucketer =
bucketer.fit(contDF)
equipadoBucketer.transform(contDF).show()

+­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
| id|quantileDiscretizer_cd87d1a1fb8e__output|
+­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
| 0.0| 0.0|
...
| 6.0| 1.0|
| 7.0| 2.0|
...
|14.0| | 3.0|
15.0| 4.0|
...
+­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+

Técnicas avanzadas de cubetas

Las técnicas descritas aquí son las formas más comunes de agrupar datos, pero existen otras formas en Spark hoy
en día. Todos estos procesos son iguales desde la perspectiva del flujo de datos: comience con datos continuos y
colóquelos en cubos para que se vuelvan categóricos. Surgen diferencias según el algoritmo utilizado para
calcular estos cubos. Los ejemplos simples que acabamos de ver son fáciles de interpretar y trabajar con ellos, pero
también están disponibles en MLlib técnicas más avanzadas como el hashing de sensibilidad de localidad
(LSH).

Escalado y Normalización
Vimos cómo podemos usar la agrupación para crear grupos a partir de variables continuas. Otra tarea común
es escalar y normalizar datos continuos. Si bien no siempre es necesario, hacerlo suele ser una mejor práctica. Es
posible que desee hacer esto cuando sus datos contengan una cantidad de columnas basadas en diferentes escalas.
Por ejemplo, digamos que tenemos un DataFrame con dos columnas: peso (en onzas) y altura (en pies). Si no escala
ni normaliza, el algoritmo será menos sensible a las variaciones de altura porque los valores de altura en pies son
mucho más bajos que los valores de peso en onzas. Ese es un ejemplo en el que debe escalar sus datos.

Un ejemplo de normalización podría involucrar la transformación de los datos para que el valor de cada punto sea una
representación de su distancia desde la media de esa columna. Usando el mismo ejemplo anterior, podríamos
querer saber qué tan lejos está la altura de un individuo dado de la altura media.
Muchos algoritmos asumen que sus datos de entrada están normalizados.

Como puede imaginar, hay una multitud de algoritmos que podemos aplicar a nuestros datos para escalarlos o
normalizarlos. Enumerarlos a todos es innecesario aquí porque están cubiertos en muchos otros
Machine Translated by Google

Textos y bibliotecas de aprendizaje automático. Si no está familiarizado con el concepto en detalle, consulte cualquiera de
los libros a los que se hace referencia en el capítulo anterior. Solo tenga en cuenta el objetivo fundamental: queremos que
nuestros datos estén en la misma escala para que los valores puedan compararse fácilmente entre sí de manera
sensata. En MLlib, esto siempre se hace en columnas de tipo Vector. MLlib buscará en todas las filas de una columna dada
(de tipo Vector) y luego tratará cada dimensión en esos vectores como su propia columna particular. Luego aplicará la función
de escalado o normalización en cada dimensión por separado.

Un ejemplo simple podría ser los siguientes vectores en una columna:

1,2
3,4

Cuando aplicamos nuestra función de escalado (pero no de normalización), el "3" y el "1" se ajustarán de acuerdo con esos dos
valores, mientras que el "2" y el "4" se ajustarán entre sí. Esto se conoce comúnmente como comparaciones por
componentes.

escalador estándar
StandardScaler estandariza un conjunto de características para que tengan una media cero y una desviación estándar

de 1. El indicador withStd escalará los datos a la desviación estándar de la unidad, mientras que el indicador withMean
(falso de forma predeterminada) centrará los datos antes de escalarlos.

ADVERTENCIA

Centrar puede ser muy costoso en vectores dispersos porque generalmente los convierte en vectores densos, así
que tenga cuidado antes de centrar sus datos.

Aquí hay un ejemplo del uso de un StandardScaler:

// en Scala
import org.apache.spark.ml.feature.StandardScaler val sScaler = new
StandardScaler().setInputCol("features") sScaler.fit(scaleDF).transform(scaleDF).show()

# en Python
desde pyspark.ml.feature import StandardScaler sScaler =
StandardScaler().setInputCol("features")
sScaler.fit(scaleDF).transform(scaleDF).show()

La salida se muestra a continuación:

+­­­+­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­ ­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
|identificación |características |StandardScaler_41aaa6044e7c3467adc3__salida |
+­­­+­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­ ­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
Machine Translated by Google

|0 |[1.0,0.1,­1.0]|[1.1952286093343936,0.02337622911060922,­0.5976143046671968]|
...
|1 |[3.0,10.1,3.0]|[3.5856858280031805,2.3609991401715313,1.7928429140015902] |
+­­­+­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­ ­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+

MinMaxScaler

El MinMaxScaler escalará los valores en un vector (componente inteligente) a los valores proporcionales
en una escala desde un valor mínimo dado hasta un valor máximo. Si especifica que el valor mínimo
sea 0 y el valor máximo sea 1, entonces todos los valores estarán entre 0 y 1:

// en Scala
import org.apache.spark.ml.feature.MinMaxScaler val
minMax = new MinMaxScaler().setMin(5).setMax(10).setInputCol("features") val addedminMax
= minMax.fit(scaleDF)
addedminMax .transform(scaleDF).mostrar()

# en Python
desde pyspark.ml.feature import MinMaxScaler
minMax = MinMaxScaler().setMin(5).setMax(10).setInputCol("características")
equipadominMax = minMax.fit(escalaDF)
equipadominMax.transform(escalaDF).show( )

+­­­+­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­ ­­­­­­­­­­­+
| identificación | características|MinMaxScaler_460cbafafbe6b9ab7c62__salida|
+­­­+­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­ ­­­­­­­­­­­+
| 0|[1.0,0.1,­1.0]| [5.0,5.0,5.0]|
...
| 1|[3.0,10.1,3.0]| [10.0,10.0,10.0]|
+­­­+­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­ ­­­­­­­­­­­+

MaxAbsScaler

El escalador máximo absoluto (MaxAbsScaler) escala los datos dividiendo cada valor por el valor
absoluto máximo en esta función. Por lo tanto, todos los valores terminan entre −1 y 1. Este
transformador no cambia ni centra los datos en absoluto en el proceso:

// en Scala
import org.apache.spark.ml.feature.MaxAbsScaler val
maScaler = new MaxAbsScaler().setInputCol("features") val
addedmaScaler = maScaler.fit(scaleDF)
addedmaScaler.transform(scaleDF).show()

# en Python
desde pyspark.ml.feature import MaxAbsScaler
maScaler = MaxAbsScaler().setInputCol("features")
addedmaScaler = maScaler.fit(scaleDF)
addedmaScaler.transform(scaleDF).show()

+­­­+­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­ ­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
Machine Translated by Google

|identificación |características |MaxAbsScaler_402587e1d9b6f268b927__salida |


+­­­+­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­ ­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
|0 |[1.0,0.1,­1.0]|[0.3333333333333333,0.009900990099009901,­0.3333333333333]|
...
|1 |[3.0,10.1,3.0]|[1.0,1.0,1.0] |
+­­­+­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­ ­­­­­­­­­­­­­­­­­­­­­­­­­­­­+

ElementwiseProducto

ElementwiseProduct nos permite escalar cada valor en un vector por un valor arbitrario. Por ejemplo, dado el
siguiente vector y la fila "1, 0.1, ­1", la salida será "10, 1.5, ­20".
Naturalmente, las dimensiones del vector de escala deben coincidir con las dimensiones del vector dentro de la
columna correspondiente:

// en Scala
import org.apache.spark.ml.feature.ElementwiseProduct import
org.apache.spark.ml.linalg.Vectors val scaleUpVec
= Vectores.dense(10.0, 15.0, 20.0) val scalingUp = new
ElementwiseProduct() .setScalingVec

(scaleUpVec) .setInputCol("características") scalingUp.transform(scaleDF).show()

# en Python
desde pyspark.ml.feature import ElementwiseProduct from
pyspark.ml.linalg import Vectores scaleUpVec
= Vectors.dense(10.0, 15.0, 20.0) scalingUp =
ElementwiseProduct()
\ .setScalingVec(scaleUpVec)
\ .setInputCol("features")
scalingUp.transform(scaleDF).show()

+­­­+­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­ ­­­­­­­­­­­­­­­­­+
| identificación | características|ElementwiseProduct_42b29ea5a55903e9fea6__output|
+­­­+­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­ ­­­­­­­­­­­­­­­­­+
| 0|[1.0,0.1,­1.0]| [10.0,1.5,­20.0]|
...
| 1|[3.0,10.1,3.0]| [30.0,151.5,60.0]|
+­­­+­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­ ­­­­­­­­­­­­­­­­­+

normalizador

El normalizador nos permite escalar vectores multidimensionales usando una de varias normas de potencia,
establecidas a través del parámetro “p”. Por ejemplo, podemos usar la norma de Manhattan (o la distancia de
Manhattan) con p = 1, la norma euclidiana con p = 2, y así sucesivamente. La distancia de Manhattan es una medida
de distancia en la que solo se puede viajar de un punto a otro a lo largo de las líneas rectas de un eje (como las
calles de Manhattan).

Aquí hay un ejemplo del uso del Normalizador:


Machine Translated by Google

// en Scala
import org.apache.spark.ml.feature.Normalizer val manhattanDistance
= new Normalizer().setP(1).setInputCol("features") manhattanDistance.transform(scaleDF).show()

# en Python
desde pyspark.ml.feature import Normalizer manhattanDistance
= Normalizer().setP(1).setInputCol("features") manhattanDistance.transform(scaleDF).show()

+­­­+­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­ ­+
| identificación | características|normalizador_1bf2cd17ed33__salida|
+­­­+­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­ ­+
| 0|[1.0,0.1,­1.0]| | 1| [0.47619047619047...|
[2.0,1.1,1.0]| | 0|[1.0,0.1,­1.0]| [0.48780487804878...|
| 1| [2.0,1.1,1.0]| | 1| [0.47619047619047...|
[3.0,10.1,3.0]| [0.48780487804878...|
[0.18633540372670...|
+­­­+­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­ ­+

Trabajar con características categóricas

La tarea más común para las características categóricas es la indexación. La indexación convierte una
variable categórica en una columna en una numérica que puede conectar a los algoritmos de aprendizaje automático.
Si bien esto es conceptualmente simple, hay algunas trampas que es importante tener en cuenta para que Spark
pueda hacer esto de manera estable y repetible.

En general, recomendamos volver a indexar cada variable categórica durante el preprocesamiento solo por
motivos de coherencia. Esto puede ser útil para mantener sus modelos a largo plazo, ya que sus prácticas de
codificación pueden cambiar con el tiempo.

Indizador de cadenas

La forma más sencilla de indexar es a través de StringIndexer, que asigna cadenas a diferentes ID numéricos.
StringIndexer de Spark también crea metadatos adjuntos al DataFrame que especifican qué entradas corresponden
a qué salidas. Esto nos permite más tarde recuperar las entradas de sus respectivos valores de índice:

// en Scala
import org.apache.spark.ml.feature.StringIndexer val lblIndxr = new
StringIndexer().setInputCol("lab").setOutputCol("labelInd") val idxRes = lblIndxr.fit(simpleDF).transform(simpleDF )
idxRes.mostrar()

# en Python
desde pyspark.ml.feature import StringIndexer lblIndxr =
StringIndexer().setInputCol("lab").setOutputCol("labelInd") idxRes = lblIndxr.fit(simpleDF).transform(simpleDF)
Machine Translated by Google

idxRes.mostrar()

+­­­­­+­­­­+­­­­­­+­­­­­­­­­­­­­­­­­­+­­­­­­­­+
|color| laboratorio|valor1| valor2|etiquetaInd|
+­­­­­+­­­­+­­­­­­+­­­­­­­­­­­­­­­­­­+­­­­­­­­+
|verde|bueno| 1|14.386294994851129| 1.0|
...
| rojo| mal| 2|14.386294994851129| 0.0|
+­­­­­+­­­­+­­­­­­+­­­­­­­­­­­­­­­­­­+­­­­­­­­+

También podemos aplicar StringIndexer a columnas que no son cadenas, en cuyo caso, serán
convertido a cadenas antes de ser indexado:

// en escala
val valIndexer = nuevo StringIndexer()
.setInputCol("valor1")
.setOutputCol("valueInd")

valIndexer.fit(simpleDF).transform(simpleDF).show()

# en pitón
valIndexer = StringIndexer().setInputCol("valor1").setOutputCol("valueInd")
valIndexer.fit(simpleDF).transform(simpleDF).show()

+­­­­­+­­­­+­­­­­­+­­­­­­­­­­­­­­­­­­+­­­­­­­­+
|color| laboratorio|valor1| valor2|valorInd|
+­­­­­+­­­­+­­­­­­+­­­­­­­­­­­­­­­­­­+­­­­­­­­+
|verde|bueno| 1|14.386294994851129| 1.0|
...
| rojo| mal| 2|14.386294994851129| 0.0|
+­­­­­+­­­­+­­­­­­+­­­­­­­­­­­­­­­­­­+­­­­­­­­+

Tenga en cuenta que StringIndexer es un estimador que debe ajustarse a los datos de entrada. Este

significa que debe ver todas las entradas para seleccionar una asignación de entradas a ID. Si entrenas un StringIndexer
en las entradas "a", "b" y "c" y luego vaya a usarlo contra la entrada "d", arrojará un error por
por defecto. Otra opción es omitir toda la fila si el valor de entrada no fue un valor visto durante
capacitación. Siguiendo con el ejemplo anterior, un valor de entrada de "d" haría que esa fila
ser salteado por completo. Podemos configurar esta opción antes o después de entrenar el indexador o la canalización. Más
Es posible que se agreguen opciones a esta función en el futuro, pero a partir de Spark 2.2, solo puede omitir o lanzar
un error en las entradas no válidas.

valIndexer.setHandleInvalid("saltar")
valIndexer.fit(simpleDF).setHandleInvalid("saltar")

Conversión de valores indexados de nuevo a texto


Al inspeccionar los resultados de aprendizaje automático, es probable que desee volver a mapear a la
valores originales. Dado que los modelos de clasificación de MLlib hacen predicciones utilizando los valores indexados,
Machine Translated by Google

esta conversión es útil para convertir las predicciones del modelo (índices) a las categorías
originales. Podemos hacer esto con IndexToString. Notará que no tenemos que ingresar nuestro valor en
la tecla String; MLlib de Spark mantiene estos metadatos por usted. Opcionalmente, puede especificar
las salidas.

// en Scala
import org.apache.spark.ml.feature.IndexToString val labelReverse =
new IndexToString().setInputCol("labelInd") labelReverse.transform(idxRes).show()

# en Python
desde pyspark.ml.feature import IndexToString labelReverse =
IndexToString().setInputCol("labelInd") labelReverse.transform(idxRes).show()

+­­­­­+­­­­+­­­­­­+­­­­­­­­­­­­­­­­­­+­­­­­­­­+­­­ ­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
|color| laboratorio|valor1| valor2|etiquetaInd|ÍndiceDeCadena_415...2a0d__salida|
+­­­­­+­­­­+­­­­­­+­­­­­­­­­­­­­­­­­­+­­­­­­­­+­­­ ­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
|verde|bueno| 1|14.386294994851129| 1.0| bueno|
...
| rojo| mal| 2|14.386294994851129| 0.0| mal|
+­­­­­+­­­­+­­­­­­+­­­­­­­­­­­­­­­­­­+­­­­­­­­+­­­ ­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+

Indexación en vectores
VectorIndexer es una herramienta útil para trabajar con variables categóricas que ya se encuentran
dentro de los vectores en su conjunto de datos. Esta herramienta encontrará automáticamente
características categóricas dentro de sus vectores de entrada y las convertirá en características categóricas
con índices de categoría basados en cero. Por ejemplo, en el siguiente DataFrame, la primera columna
de nuestro Vector es una variable categórica con dos categorías diferentes mientras que el
resto de las variables son continuas. Al establecer maxCategories en 2 en nuestro VectorIndexer, le
indicamos a Spark que tome cualquier columna de nuestro vector con dos o menos valores distintos y la
convierta en una variable categórica. Esto puede ser útil cuando sabe cuántos valores únicos hay en
su categoría más grande porque puede especificar esto e indexará automáticamente los valores en
consecuencia. Por el contrario, Spark cambia los datos en función de este parámetro, por lo que
si tiene variables continuas que no parecen particularmente continuas (muchos valores repetidos),
estas pueden convertirse involuntariamente en variables categóricas si hay muy pocos valores únicos.

// en Scala
import org.apache.spark.ml.feature.VectorIndexer import
org.apache.spark.ml.linalg.Vectors val idxIn =
spark.createDataFrame(Seq( (Vectors.dense(1, 2, 3),1 ),
(Vectores.denso(2, 5, 6),2),
(Vectores.denso(1, 8,
9),3) )).toDF("características",
"etiqueta") val indxr = new VectorIndexer( )
Machine Translated by Google

.setInputCol("características") .setOutputCol("idxed") .setMaxCategories(2)


indxr.fit(idxIn).transform(idxIn).mostrar

# en Python
de pyspark.ml.feature importar VectorIndexer de
pyspark.ml.linalg importar vectores idxIn =
spark.createDataFrame([ (Vectors.dense(1,
2, 3),1), (Vectors.dense(2, 5,
6),2), (Vectores.dense(1, 8,
9),3) ]).toDF("características",
"etiqueta") indxr = VectorIndexer()
\
.setInputCol("características")
\ .setOutputCol("idxed")
\ .setMaxCategories(2)
indxr.fit(idxIn).transform(idxIn).show()

+­­­­­­­­­­­­­+­­­­­+­­­­­­­­­­­­­+
| caracteristicas|etiqueta| idxed|
+­­­­­­­­­­­­­+­­­­­+­­­­­­­­­­­­­+
|[1.0,2.0,3.0]| | 1|[0.0,2.0,3.0]| 2|
[2.0,5.0,6.0]| | [1.0,5.0,6.0]| 3|
[1.0,8.0,9.0]| [0.0,8.0,9.0]|
+­­­­­­­­­­­­­+­­­­­+­­­­­­­­­­­­­+

Codificación One­Hot
Indexar variables categóricas es solo la mitad de la historia. La codificación one­hot es una transformación
de datos extremadamente común que se realiza después de indexar variables categóricas. Esto se debe a
que la indexación no siempre representa nuestras variables categóricas de la manera correcta para que las
procesen los modelos posteriores. Por ejemplo, cuando indexamos nuestra columna "color", notará que
algunos colores tienen un valor (o número de índice) más alto que otros (en nuestro caso, el azul es 1 y el verde es 2).

Esto es incorrecto porque da la apariencia matemática de que la entrada al algoritmo de aprendizaje


automático parece especificar que verde > azul, lo que no tiene sentido en el caso de las categorías actuales.
Para evitar esto, usamos OneHotEncoder, que convertirá cada valor distinto en un indicador booleano (1 o 0)
como un componente en un vector. Cuando codificamos el valor del color, podemos ver que ya no están
ordenados, lo que facilita el procesamiento de los modelos posteriores (por ejemplo, un modelo lineal):

// en Scala
import org.apache.spark.ml.feature.{StringIndexer, OneHotEncoder} val lblIndxr
= new StringIndexer().setInputCol("color").setOutputCol("colorInd") val colorLab =
lblIndxr.fit(simpleDF) .transform(simpleDF.select("color")) valor = new
OneHotEncoder().setInputCol("colorInd")
ohe.transform(colorLab).show()

# en pitón
Machine Translated by Google

desde pyspark.ml.feature importar OneHotEncoder, StringIndexer


lblIndxr = StringIndexer().setInputCol("color").setOutputCol("colorInd")
colorLab = lblIndxr.fit(simpleDF).transform(simpleDF.select("color"))
ohe = OneHotEncoder().setInputCol("colorInd")
ohe.transform(colorLab).show()

+­­­­­+­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­ ­­­­­­­­+
|color|colorInd|OneHotEncoder_46b5ad1ef147bb355612__salida|
+­­­­­+­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­ ­­­­­­­­+
|verde| 1.0| (2,[1],[1.0])|
| azul| 2.0| (2,[],[])|
...
| rojo| | 0.0| (2,[0],[1.0])|
rojo| 0.0| (2,[0],[1.0])|
+­­­­­+­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­ ­­­­­­­­+

Transformadores de datos de texto


El texto siempre es una entrada complicada porque a menudo requiere mucha manipulación para mapear a un formato que
un modelo de aprendizaje automático podrá usarse de manera efectiva. Generalmente hay dos tipos de textos.
verá: texto de forma libre y variables categóricas de cadena. Esta sección se enfoca principalmente en el texto de forma libre
porque ya discutimos las variables categóricas.

Tokenización de texto
La tokenización es el proceso de convertir texto de formato libre en una lista de "tokens" o tokens individuales.

palabras. La forma más fácil de hacerlo es usando la clase Tokenizer. Este transformador tomará un
cadena de palabras, separadas por espacios en blanco, y convertirlas en una matriz de palabras. Por ejemplo,
en nuestro conjunto de datos, es posible que deseemos convertir el campo Descripción en una lista de tokens.

// en escala
importar org.apache.spark.ml.feature.Tokenizer
val tkn = nuevo Tokenizador().setInputCol("Descripción").setOutputCol("DescOut")
val tokenizado = tkn.transform(sales.select("Description"))
tokenizado.show (falso)

# en pitón
de pyspark.ml.feature importar Tokenizer
tkn = Tokenizador().setInputCol("Descripción").setOutputCol("DescOut")
tokenizado = tkn.transform(ventas.select("Descripción"))
tokenizado.show(20, Falso)

+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­ ­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
|Descripción DescOut |
+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­ ­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
|LUZ NOCTURNA CONEJO |[conejo, noche, luz] | |
|BRILLO DE LABIOS DOUGHUT [rosquilla, labio, brillo] |
...
|AIRLINE BAG VINTAGE WORLD CHAMPION |[airline, bag, vintage, world, champion] |
Machine Translated by Google

|BOLSO AIRLINE VINTAGE JET SET MARRÓN |[airline, bolso, vintage, jet, set, marrón] |
+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­ ­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+

También podemos crear un Tokenizer que no se base solo en un espacio en blanco, sino en una expresión
regular con RegexTokenizer. El formato de la expresión regular debe ajustarse a la sintaxis de expresiones
regulares de Java (RegEx):

// en Scala
import org.apache.spark.ml.feature.RegexTokenizer val rt = new

RegexTokenizer() .setInputCol("Descripción") .setOutputCol("DescOut") .setPattern(" ") // expresión más simple .setToLowercase( verdadero) rt.transform(ve

# en Python
desde pyspark.ml.feature import RegexTokenizer rt =
RegexTokenizer()
\ .setInputCol("Description")
\ .setOutputCol("DescOut")
\ .setPattern(" ")
\ .setToLowercase(True)
rt.transform(sales.select("Descripción")).show(20, Falso)

+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­ ­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
|Descripción DescOut |
+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­ ­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
|LUZ NOCTURNA CONEJO |[conejo, noche, luz] |[rosquilla,
|BRILLO DE LABIOS DOUGHUT labio, brillo] ||

...
|AIRLINE BAG VINTAGE WORLD CHAMPION |[airline, bag, vintage, world, champion] |
|BOLSO AIRLINE VINTAGE JET SET MARRÓN |[airline, bolso, vintage, jet, set, marrón] |
+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­ ­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+

Otra forma de usar RegexTokenizer es usarlo para generar valores que coincidan con el patrón proporcionado
en lugar de usarlo como un espacio. Hacemos esto configurando el parámetro gaps en falso. Hacer esto con
un espacio como patrón devuelve todos los espacios, lo que no es muy útil, pero si hicimos que nuestro
patrón capturara palabras individuales, podríamos devolverlas:

// en Scala
import org.apache.spark.ml.feature.RegexTokenizer val rt = new

RegexTokenizer() .setInputCol("Descripción") .setOutputCol("DescOut") .setPattern(" ") .setGaps(false) .setToLowercase (verdadero) rt.transform(ventas.sele


Machine Translated by Google

# en pitón
de pyspark.ml.feature importar RegexTokenizer
rt = RegexTokenizer()\
.setInputCol("Descripción")\
.setOutputCol("DescOut")\
.establecerPatrón(" ")\
.setGaps(Falso)\
.setToLowercase(Verdadero)
rt.transform(sales.select("Descripción")).show(20, Falso)

+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­ ­­­­­+
|Descripción DescOut |
+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­ ­­­­­+
|LUZ NOCTURNA CONEJO |[ , | ] |
|BRILLO DE LABIOS DOUGHUT [, , ] |
...
|BOLSO AIRLINE VINTAGE CAMPEON DEL MUNDO |[ , , , , |
|BOLSO AIRLINE VINTAGE JET SET MARRÓN |[ , , , , ]] |
+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­ ­­­­­+

Eliminar palabras comunes


Una tarea común después de la tokenización es filtrar palabras vacías, palabras comunes que no son relevantes en
muchos tipos de análisis y, por lo tanto, debe eliminarse. Palabras vacías frecuentes en inglés
incluyen "el", "y" y "pero". Spark contiene una lista de palabras vacías predeterminadas que puede ver llamando
el siguiente método, que puede hacer que no se distinga entre mayúsculas y minúsculas si es necesario (a partir de Spark 2.2,
Los idiomas admitidos para palabras vacías son "danés", "holandés", "inglés", "finlandés", "francés"
“alemán”, “húngaro”, “italiano”, “noruego”, “portugués”, “ruso”, “español”, “sueco”,
y “turco”):

// en escala
importar org.apache.spark.ml.feature.StopWordsRemover
val englishStopWords = StopWordsRemover.loadDefaultStopWords("inglés")
val se detiene = nuevo StopWordsRemover()
.setStopWords(inglésStopWords)
.setInputCol("DescOut")
para.transformar (tokenizado).mostrar()

# en pitón
de pyspark.ml.feature importar StopWordsRemover
englishStopWords = StopWordsRemover.loadDefaultStopWords("inglés")
paradas = StopWordsRemover()\
.setStopWords(inglésStopWords)\
.setInputCol("DescOut")
para.transformar (tokenizado).mostrar()

El siguiente resultado muestra cómo funciona esto:

+­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+­­­­­­­ ­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
| Descripción| DescOut|StopWordsRemover_4ab18...6ed__output|
Machine Translated by Google

+­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+­­­­­­­ ­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
...
|SET DE 4 KNICK KN...|[set, de, 4, knic...| [conjunto, 4, knick, k...|
...
+­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+­­­­­­­ ­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+

Observe cómo se elimina la palabra de en la columna de salida. Esto se debe a que es una palabra tan común que no es relevante para

ninguna manipulación posterior y simplemente agrega ruido a nuestro conjunto de datos.

Crear combinaciones de palabras


La tokenización de nuestras cadenas y el filtrado de palabras vacías nos deja con un conjunto limpio de palabras para usar como

características. A menudo es de interés observar combinaciones de palabras, generalmente observando palabras colocadas. Las

combinaciones de palabras se conocen técnicamente como n­gramas, es decir , secuencias de palabras de longitud n. Un n­grama de longitud

1 se denomina unigrama; los de longitud 2 se denominan bigramas, y los de longitud 3 se denominan trigramas (cualquier cosa por encima

de ellos es solo de cuatro gramos, cinco gramos, etc.). El orden importa con la creación de n­gramas, por lo que convertir una oración con

tres palabras en la representación de bigramas daría como resultado dos bigramas. El objetivo al crear n­gramas es capturar mejor la

estructura de la oración y más información de la que se puede obtener simplemente mirando todas las palabras individualmente. Vamos a

crear algunos n­gramas para ilustrar este concepto.

Los bigramas de “Big Data Processing Made Simple” son:

"Grandes datos"

"Procesamiento de datos"

“Tratamiento realizado”

"Simplificado"

Mientras que los trigramas son:

“Procesamiento de grandes datos”

“Tratamiento de datos realizado”

“Procesamiento simplificado”

Con los n­gramas, podemos observar secuencias de palabras que comúnmente ocurren al mismo tiempo y usarlas como entradas

para un algoritmo de aprendizaje automático. Estos pueden crear mejores funciones que simplemente mirar todas las palabras

individualmente (por ejemplo, tokenizadas en un carácter de espacio):

// en Scala
import org.apache.spark.ml.feature.NGram val
unigram = new NGram().setInputCol("DescOut").setN(1) val bigram
= new NGram().setInputCol("DescOut"). setN(2)
unigram.transform(tokenized.select("DescOut")).show(false)
bigram.transform(tokenized.select("DescOut")).show(false)
Machine Translated by Google

# en pitón
de pyspark.ml.feature importar NGram
unigrama = NGram().setInputCol("DescOut").setN(1)
bigrama = NGram().setInputCol("DescOut").setN(2)
unigram.transform(tokenized.select("DescOut")).show(False)
bigram.transform(tokenized.select("DescOut")).show(False)

+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+­­­­­­­ ­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­
DescOut |ngram_104c4da6a01b__salida ...
+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+­­­­­­­ ­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­
|[conejo, noche, luz] | |[conejo, noche, luz] | ...
[rosquilla, labio, brillo] [rosquilla, labio, brillo] ...
...
|[aerolínea, bolso, vintage, mundial, campeón] |[aerolínea, bolso, vintage, mundial, cha...
|[aerolínea, bolso, vintage, jet, conjunto, marrón] |[aerolínea, bolso, vintage, jet, conjunto, ...
+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+­­­­­­­ ­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­

Y el resultado para bigramas:

+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+­­­­­­ ­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­
DescOut |ngram_6e68fb3a642a__salida ...
+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+­­­­­­ ­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­
|[conejo, noche, luz] | |[noche de conejo, luz de noche] | ...
[rosquilla, labio, brillo] [labio de dona, brillo de labios] ...
...
|[airline, bag, vintage, world, champion] |[airline bag, bag vintage, vintag...
|[airline, bag, vintage, jet, set, brown] |[airline bag, bag vintage, vintag...
+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+­­­­­­ ­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­

Convertir palabras en representaciones numéricas


Una vez que tenga características de palabras, es hora de comenzar a contar instancias de palabras y palabras.
combinaciones para usar en nuestros modelos. La forma más sencilla es simplemente incluir recuentos binarios de una palabra
en un documento dado (en nuestro caso, una fila). Esencialmente, estamos midiendo si cada fila
contiene una palabra dada. Esta es una forma sencilla de normalizar el tamaño y la aparición de documentos.
cuenta y obtener características numéricas que nos permitan clasificar los documentos en función del contenido. En
Además, podemos contar palabras usando un CountVectorizer, o volver a pesarlas de acuerdo con el
prevalencia de una palabra dada en todos los documentos utilizando una transformación TF­IDF (discutida a continuación).

Un CountVectorizer opera con nuestros datos tokenizados y hace dos cosas:

1. Durante el proceso de ajuste, encuentra el conjunto de palabras en todos los documentos y luego cuenta
las apariciones de esas palabras en esos documentos.

2. Luego cuenta las ocurrencias de una palabra dada en cada fila de la columna DataFrame
durante el proceso de transformación y genera un vector con los términos que ocurren en ese
fila.

Conceptualmente, este transformador trata cada fila como un documento y cada palabra como un término y el
Machine Translated by Google

colección total de todos los términos como el vocabulario. Todos estos son parámetros ajustables, lo que
significa que podemos establecer la frecuencia mínima del término (minTF) para que el término se
incluya en el vocabulario (eliminando efectivamente las palabras raras del vocabulario); número mínimo de
documentos en los que debe aparecer un término (minDF) antes de ser incluido en el vocabulario (otra
forma de eliminar palabras raras del vocabulario); y finalmente, el tamaño máximo total del vocabulario (vocabSize).
Por último, de forma predeterminada, CountVectorizer generará los recuentos de un término en un documento.
Para devolver si una palabra existe o no en un documento, podemos usar setBinary(true). Aquí hay un
ejemplo del uso de CountVectorizer:

// en Scala
import org.apache.spark.ml.feature.CountVectorizer val cv = new

CountVectorizer() .setInputCol("DescOut") .setOutputCol("countVec") .setVocabSize(500) .setMinTF(1) .setMinDF( 2) val addedCV = cv.fit(tokenizado) added

# en Python
desde pyspark.ml.feature import CountVectorizer cv = CountVectorizer()
\
.setInputCol("DescOut")
\ .setOutputCol("countVec")

\ .setVocabSize(500)\ .setMinTF(1)\ .setMinDF(2)


equipadoCV = cv.fit(tokenizado) equipadoCV.transform(tokenizado).show( FALSO)

Si bien la salida parece un poco complicada, en realidad es solo un vector disperso que contiene el tamaño total
del vocabulario, el índice de la palabra en el vocabulario y luego los recuentos de esa palabra en particular:

+­­­­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­ ­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
DescOut |countVec |
+­­­­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­ ­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
|[conejo, noche, luz] |[rosquilla, |(500,[150.185.212],[1.0,1.0,1.0]) |(500,[462.463.492],
labio, brillo] [1.0,1.0,1.0]) ||

...
|[aerolínea, bolso, vintage, mundo,...|(500,[2,6,328],[1.0,1.0,1.0]) | |[aerolínea, bolso, vintage, jet, s...|(500,[0,2,6,328,405],
[1.0,1.0,1.0,1.0,1.0]) |
+­­­­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­ ­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+

Frecuencia de término–frecuencia de documento inversa

Otra forma de abordar el problema de convertir texto en una representación numérica es utilizar el término
frecuencia­frecuencia inversa del documento (TF­IDF). En términos más simples, las medidas TF­IDF
Machine Translated by Google

con qué frecuencia aparece una palabra en cada documento, ponderada según en cuántos documentos aparece
esa palabra. El resultado es que las palabras que aparecen en unos pocos documentos tienen más peso que las
palabras que aparecen en muchos documentos. En la práctica, una palabra como "el" tendría una ponderación muy
baja debido a su prevalencia, mientras que una palabra más especializada como "transmisión" aparecería en
menos documentos y, por lo tanto, tendría una ponderación más alta. En cierto modo, TF–IDF ayuda a encontrar
documentos que comparten temas similares. Echemos un vistazo a un ejemplo: primero, inspeccionaremos
algunos de los documentos en nuestros datos que contienen la palabra "rojo":

// en Scala
val tfIdfIn = tokenizado
.where("array_contains(DescOut,

'red')") .select("DescOut") .limit(10) tfIdfIn.show(false)

# en Python
tfIdfIn = tokenizado\
.where("array_contains(DescOut, 'red')")
\ .select("DescOut")
\ .limit(10)
tfIdfIn.show(10, False)

+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
DescOut |
+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
|[vichy, corazón, tope de puerta, rojo] |
...
|[rojo, retrospot, horno, guante] |
[rojo, retrospot, plato] ||

+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+

Podemos ver algunas palabras superpuestas en estos documentos, pero estas palabras proporcionan al menos una
representación aproximada de un tema. Ahora ingresemos eso en TF–IDF. Para hacer esto, vamos a codificar cada
palabra y convertirla en una representación numérica, y luego pesaremos cada palabra en el vocabulario de
acuerdo con la frecuencia inversa del documento. Hashing es un proceso similar a CountVectorizer, pero es irreversible,
es decir, desde nuestro índice de salida para una palabra, no podemos obtener nuestra palabra de entrada
(varias palabras pueden asignarse al mismo índice de salida):

// en Scala
import org.apache.spark.ml.feature.{HashingTF, IDF} val tf =
new

HashingTF() .setInputCol("DescOut") .setOutputCol("TFOut") .setNumFeatures(10000) val idf = new IDF() .setInputCol("TFOut"


Machine Translated by Google

# en Python
desde pyspark.ml.feature import HashingTF, IDF tf =
HashingTF()
\ .setInputCol("DescOut")
\ .setOutputCol("TFOut")
\ .setNumFeatures(10000)
idf = IDF()
\ .setInputCol(" TFOut")
\ .setOutputCol("IDFOut")
\ .setMinDocFreq(2)

// en Scala
idf.fit(tf.transform(tfIdfIn)).transform(tf.transform(tfIdfIn)).show(false)

# en Python
idf.fit(tf.transform(tfIdfIn)).transform(tf.transform(tfIdfIn)).show(10, False)

Si bien la salida es demasiado grande para incluirla aquí, observe que se asigna cierto valor a "rojo" y que este valor aparece en todos
los documentos. También tenga en cuenta que este término tiene un peso extremadamente bajo porque aparece en todos los

documentos. El formato de salida es un Vector disperso que posteriormente podemos ingresar en un modelo de aprendizaje automático
de una forma como esta:

(10000,[2591,4291,4456],[1.0116009116784799,0.0,0.0])

Este vector se representa mediante tres valores diferentes: el tamaño total del vocabulario, el hash de cada palabra que aparece
en el documento y la ponderación de cada uno de esos términos. Esto es similar a la salida de CountVectorizer.

Word2Vec
Word2Vec es una herramienta basada en el aprendizaje profundo para calcular una representación vectorial de un conjunto de palabras.
El objetivo es tener palabras similares cerca unas de otras en este espacio vectorial, para que luego podamos hacer generalizaciones
sobre las palabras mismas. Este modelo es fácil de entrenar y usar, y se ha demostrado que es útil en varias aplicaciones de
procesamiento de lenguaje natural, incluido el reconocimiento de entidades, la eliminación de ambigüedades, el análisis, el
etiquetado y la traducción automática.

Word2Vec se destaca por capturar las relaciones entre palabras en función de su semántica. Por ejemplo, si v~rey, v~reina,
v~hombre y v~mujer representan los vectores de esas cuatro palabras, a menudo obtendremos una representación donde v~rey −
v~hombre + v~mujer ~= v ~ reina. Para ello, Word2Vec utiliza una técnica llamada “skip­grams” para convertir una oración de palabras
en una representación vectorial (opcionalmente de un tamaño específico). Lo hace mediante la creación de un vocabulario, y
luego, para cada oración, elimina un token y entrena el modelo para predecir el token faltante en la representación "n­gram".
Word2Vec funciona mejor con texto continuo y de forma libre en forma de fichas

Aquí hay un ejemplo simple de la documentación:


Machine Translated by Google

// en Scala
import org.apache.spark.ml.feature.Word2Vec import
org.apache.spark.ml.linalg.Vector import
org.apache.spark.sql.Row // Datos de
entrada: cada fila es una bolsa de palabras de una oración o documento. val documentDF
= spark.createDataFrame(Seq( "Hola, escuché sobre
Spark".split(" "), "Desearía que Java pudiera
usar clases de casos".split(" "), "Los modelos de regresión
logística están ordenados".split(" ")
).map(Tuple1.aplicar)).toDF("texto")
// Aprende un mapeo de palabras a Vectores. val
word2Vec = new

Word2Vec() .setInputCol("texto") .setOutputCol("resultado") .setVectorSize(3) .setMinCount(0)


val model = word2Vec.fit(documentDF) val result = model.transform(documentDF)
resultado .collect().foreach { case Row(texto: Seq[_], características: Vector) => println(s"Texto: [${text.mkString(", ")}] => \nVector: $c

# en Python
desde pyspark.ml.feature importar Word2Vec #
Datos de entrada: cada fila es una bolsa de palabras de una oración o documento.
documentDF = chispa.createDataFrame([
("Hola, escuché sobre Spark".split(" "), ), ("Desearía
que Java pudiera usar clases de casos".split(" "), ), ("Los modelos
de regresión logística son geniales".split(" ") , ) ], ["texto"])

# Aprenda un mapeo de palabras a vectores.


word2Vec = Word2Vec(vectorSize=3, minCount=0, inputCol="text",
outputCol="result")
model = word2Vec.fit(documentDF)
result = model.transform(documentDF) for
row in result.collect(): text , vector
= fila print("Texto: [%s]
=> \nVector: %s\n" % (", ".join(texto), str(vector)))

Texto: [Hola, escuché, sobre Spark] => Vector:


[­0.008142343163490296,0.02051363289356232,0.03255096450448036]

Texto: [I, wish, Java, could, use, case, classes] => Vector:
[0.043090314205203734,0.035048123182994974,0.023512658663094044]

Texto: [Logística, regresión, modelos, are, aseado] => Vector:


[0.038572299480438235,­0.03250147425569594,­0.01552378609776497]

La implementación de Word2Vec de Spark incluye una variedad de parámetros de ajuste que se pueden encontrar
en la documentación.
Machine Translated by Google

Manipulación de características
Si bien casi todos los transformadores en ML manipulan el espacio de características de alguna manera, los siguientes
algoritmos y herramientas son medios automatizados para expandir los vectores de características de entrada
o reducirlos a un número menor de dimensiones.

PCA

El análisis de componentes principales (PCA) es una técnica matemática para encontrar los aspectos más
importantes de nuestros datos (los componentes principales). Cambia la representación de características de nuestros
datos al crear un nuevo conjunto de características ("aspectos"). Cada característica nueva es una combinación de
las características originales. El poder de PCA es que puede crear un conjunto más pequeño de características
más significativas para ingresar en su modelo, con el costo potencial de la interpretabilidad.

Le gustaría usar PCA si tiene un gran conjunto de datos de entrada y desea reducir la cantidad total de funciones que
tiene. Esto surge con frecuencia en el análisis de texto, donde todo el espacio de funciones es masivo y muchas
de las funciones son en gran medida irrelevantes. Usando PCA, podemos encontrar las combinaciones de
funciones más importantes y solo incluirlas en nuestro modelo de aprendizaje automático. PCA toma un parámetro,
especificando el número de características de salida para crear. En general, esto debería ser mucho más pequeño
que la dimensión de sus vectores de entrada.

NOTA

Elegir lo correcto no es trivial y no hay receta que podamos dar. Consulte los capítulos
correspondientes en ESL e ISL para obtener más información.

Entrenemos PCA con un de 2:

// en Scala
import org.apache.spark.ml.feature.PCA val
pca = new PCA().setInputCol("features").setK(2)
pca.fit(scaleDF).transform(scaleDF).show(false )

# en Python
desde pyspark.ml.feature import PCA pca
= PCA().setInputCol("features").setK(2)
pca.fit(scaleDF).transform(scaleDF).show(20, False)

+­­­+­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­ ­­­­­­­­­­­­+
|identificación |características |pca_7c5c4aa7674e__salida |
+­­­+­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­ ­­­­­­­­­­­­+
|0 |[1.0,0.1,­1.0]|[0.0713719499248418,­0.4526654888147822] |
...
|1 |[3.0,10.1,3.0]|[­10.872398139848944,0.030962697060150646]|
+­­­+­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­ ­­­­­­­­­­­­+
Machine Translated by Google

Interacción
En algunos casos, es posible que tenga conocimiento de dominio sobre variables específicas en su conjunto de datos.
Por ejemplo, es posible que sepa que cierta interacción entre las dos variables es una variable importante para incluir
en un estimador descendente. La interacción del transformador de características le permite crear una interacción entre
dos variables manualmente. Simplemente multiplica las dos características juntas, algo que un modelo lineal
típico no haría para cada posible par de características en sus datos. Este transformador actualmente solo está disponible
directamente en Scala, pero se puede llamar desde cualquier idioma usando RFormula. Recomendamos a los usuarios
que utilicen RFormula en lugar de crear interacciones manualmente.

Expansión polinomial
La expansión polinomial se utiliza para generar variables de interacción de todas las columnas de entrada. Con la
expansión de polinomios, especificamos hasta qué punto nos gustaría ver varias interacciones. Por ejemplo, para un
polinomio de grado 2, Spark toma cada valor en nuestro vector de características, lo multiplica por cualquier otro valor
en el vector de características y luego almacena los resultados como características. Por ejemplo, si tenemos dos
características de entrada, obtendremos cuatro características de salida si usamos un polinomio de segundo grado (2x2).
Si tenemos tres características de entrada, obtendremos nueve características de salida (3x3). Si usamos un
polinomio de tercer grado, obtendremos 27 características de salida (3x3x3) y así sucesivamente. Esta transformación es
útil cuando desea ver interacciones entre características particulares pero no está necesariamente seguro de qué
interacciones considerar.

ADVERTENCIA

La expansión polinomial puede aumentar considerablemente su espacio de funciones, lo que genera altos costos
computacionales y sobreajuste. Úselo con precaución, especialmente para grados superiores.

Aquí hay un ejemplo de un polinomio de segundo grado:

// en Scala
import org.apache.spark.ml.feature.PolynomialExpansion val pe
= new PolynomialExpansion().setInputCol("features").setDegree(2)
pe.transform(scaleDF).show(false)

# en Python
desde pyspark.ml.feature import PolynomialExpansion pe =
PolynomialExpansion().setInputCol("features").setDegree(2)
pe.transform(scaleDF).show()

+­­­+­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­ ­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
|identificación |características |poli_9b2e603812cb__salida |
+­­­+­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­ ­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+
|0 |[1.0,0.1,­1.0]|[1.0,1.0,0.1,0.1,0.010000000000000002,­1.0,­1.0,­0.1,1.0] |
...
Machine Translated by Google

|1 |[3.0,10.1,3.0]|[3.0,9.0,10.1,30.299999999999997,102.00999999999999,3.0... |
+­­­+­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­ ­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+

Selección de características

A menudo, tendrá una amplia gama de funciones posibles y querrá seleccionar un subconjunto más pequeño para
usar en el entrenamiento. Por ejemplo, muchas funciones pueden estar correlacionadas, o el uso de demasiadas
funciones puede dar lugar a un sobreajuste. Este proceso se llama selección de características. Hay varias
formas de evaluar la importancia de las características una vez que haya entrenado un modelo, pero otra opción es
realizar un filtrado aproximado de antemano. Spark tiene algunas opciones simples para hacerlo, como ChiSqSelector.

SelectorChiSq
ChiSqSelector aprovecha una prueba estadística para identificar características que no son independientes de la
etiqueta que intentamos predecir y elimina las características no correlacionadas. A menudo se usa con datos
categóricos para reducir la cantidad de características que ingresará en su modelo, así como para reducir la
dimensionalidad de los datos de texto (en forma de frecuencias o conteos). Dado que este método se basa en la
prueba Chi­Square, hay varias formas diferentes de elegir las "mejores" características.
Los métodos son numTopFeatures, que está ordenado por valor p; percentil, que toma una proporción de las
características de entrada (en lugar de solo las N características superiores); y fpr, que establece un valor p de corte .

Demostraremos esto con la salida del CountVectorizer creado anteriormente en este capítulo:

// en Scala
import org.apache.spark.ml.feature.{ChiSqSelector, Tokenizer} val tkn = new
Tokenizer().setInputCol("Description").setOutputCol("DescOut") val tokenized = tkn

.transform(sales.select("Description", "CustomerId")) .where("CustomerId NO


ES NULO") val prechi =
equipadoCV.transform(tokenizado) val chisq = new
ChiSqSelector() .setFeaturesCol("countVec") .

setLabelCol("CustomerId") .setNumTopFeatures(2) chisq.fit(prechi).transform(prechi)


.drop("Idcliente", "Descripción", "DescOut").show()

# en Python
desde pyspark.ml.feature import ChiSqSelector, Tokenizer tkn =
Tokenizer().setInputCol("Description").setOutputCol("DescOut") tokenized =

tkn\ .transform(sales.select("Description", "CustomerId" ))\ .where("CustomerId NO


ES NULO") prechi =
equipadoCV.transform(tokenizado)\ .where("CustomerId
NO ES NULO") chisq = ChiSqSelector()
\ .setFeaturesCol("countVec")\
Machine Translated by Google

.setLabelCol("CustomerId")
\ .setNumTopFeatures(2)
chisq.fit(prechi).transform(prechi)\
.drop("Idcliente", "Descripción", "DescOut").show()

Temas avanzados
Hay varios temas avanzados relacionados con transformadores y estimadores. Aquí mencionamos los dos transformadores
persistentes más comunes, además de escribir los personalizados.

Transformadores persistentes
Una vez que haya usado un estimador para configurar un transformador, puede ser útil escribirlo en el disco y simplemente cargarlo
cuando sea necesario (por ejemplo, para usarlo en otra sesión de Spark). Vimos esto en el capítulo anterior cuando
persistimos una canalización completa. Para persistir un transformador individualmente, usamos el método de escritura en el

transformador instalado (o el transformador estándar) y especificamos la ubicación:

// en Scala
val PCA ajustado = pca.fit(scaleDF)
PCA ajustado.write.overwrite().save("/tmp/PCA ajustado")

# en Python
PCA ajustado = pca.fit(scaleDF)
PCA ajustado.write().overwrite().save("/tmp/PCA ajustado")

Luego podemos cargarlo de nuevo en:

// en Scala
importar org.apache.spark.ml.feature.PCAModel val
cargadoPCA = PCAModel.load("/tmp/fittedPCA")
cargadoPCA.transform(scaleDF).show()

# en Python
desde pyspark.ml.feature importar PCAModel
cargadoPCA = PCAModel.load("/tmp/fittedPCA")
cargadoPCA.transform(scaleDF).show()

Escribir un transformador personalizado


Escribir un transformador personalizado puede ser valioso cuando desea codificar parte de su propia lógica comercial en
una forma que pueda encajar en una canalización de ML, pasar a la búsqueda de hiperparámetros, etc. En general, debe intentar

usar los módulos incorporados (por ejemplo, SQLTransformer) tanto como sea posible porque están optimizados para ejecutarse de
manera eficiente. Pero a veces no tenemos ese lujo. Vamos a crear un tokenizador simple para demostrar:
Machine Translated by Google

importar org.apache.spark.ml.UnaryTransformer
importar org.apache.spark.ml.util.{DefaultParamsReadable, DefaultParamsWritable,
identificable}
importar org.apache.spark.sql.types.{ArrayType, StringType, DataType} importar
org.apache.spark.ml.param.{IntParam, ParamValidators}

clase MyTokenizer (anular valor uid: Cadena)


extiende UnaryTransformer[String, Seq[String],
MyTokenizer] con DefaultParamsWritable {

def this() = this(Identificable.randomUID("myTokenizer"))

val maxWords: IntParam = new IntParam(this, "maxWords",


"El número máximo de palabras para devolver.",
ParamValidators.gtEq(0))

def setMaxPalabras(valor: Int): this.type = set(maxPalabras, valor)

def obtenerMaxPalabras: Integer = $(maxPalabras)

invalidar protected def createTransformFunc: String => Seq[String] = (


inputString: String) =>
{ inputString.split("\\s").take($(maxWords))
}

invalidar protegido def validarTipoEntrada(tipoEntrada: TipoDatos): Unidad = {

require( inputType == StringType, s"Tipo de entrada incorrecto: $inputType. Requiere String.")


}

invalidar protegido def outputDataType: DataType = new ArrayType(StringType,


verdadero)

// esto le permitirá volver a leerlo usando este objeto. objeto MyTokenizer


extiende DefaultParamsReadable[MyTokenizer]

val myT = new MyTokenizer().setInputCol("someCol").setMaxWords(2)


myT.transform(Seq("hola mundo. Este texto no se mostrará").toDF("someCol")).show()

También es posible escribir un estimador personalizado donde debe personalizar la transformación en


función de los datos de entrada reales. Sin embargo, esto no es tan común como escribir un
transformador independiente y, por lo tanto, no se incluye en este libro. Una buena manera de hacer esto es mirar
uno de los estimadores simples que vimos antes y modificar el código para adaptarlo a su caso de uso. Un buen
lugar para comenzar podría ser StandardScaler.

Conclusión
Este capítulo ofreció un recorrido relámpago por muchas de las transformaciones de preprocesamiento más
comunes que Spark tiene disponibles. Hay varios dominios específicos para los que no teníamos espacio suficiente.
Machine Translated by Google

cover (por ejemplo, Discrete Cosine Transform), pero puede encontrar más información en la
documentación. Esta área de Spark también crece constantemente a medida que la comunidad desarrolla nuevos
unos.

Otro aspecto importante de este conjunto de herramientas de ingeniería de características es la consistencia. En el


capítulo anterior, cubrimos el concepto de canalización, una herramienta esencial para empaquetar y entrenar flujos de
trabajo de ML de extremo a extremo. En el próximo capítulo, comenzaremos a analizar la variedad de tareas de aprendizaje
automático que puede tener y qué algoritmos están disponibles para cada una.
Machine Translated by Google

Capítulo 26. Clasificación

La clasificación es la tarea de predecir una etiqueta, categoría, clase o variable discreta dadas algunas características de entrada. La

diferencia clave con otras tareas de ML, como la regresión, es que la etiqueta de salida tiene un conjunto finito de valores posibles (p. ej.,

tres clases).

Casos de uso
La clasificación tiene muchos casos de uso, como discutimos en el Capítulo 24. Aquí hay algunos más para considerar como un

refuerzo de la multitud de formas en que se puede usar la clasificación en el mundo real.

Predicción del riesgo de crédito

Una empresa de financiación puede considerar una serie de variables antes de ofrecer un préstamo a una empresa o individuo.

Ofrecer o no el préstamo es un problema de clasificación binaria.

Clasificación de noticias

Se puede entrenar un algoritmo para predecir el tema de un artículo de noticias (deportes, política, negocios, etc.).

Clasificación de la actividad humana

Al recopilar datos de sensores como el acelerómetro de un teléfono o un reloj inteligente, puede predecir la actividad de la persona.

La salida será una de un conjunto finito de clases (por ejemplo, caminar, dormir, estar de pie o correr).
Machine Translated by Google

Tipos de clasificación
Antes de continuar, repasemos varios tipos diferentes de clasificación.

Clasificación binaria
El ejemplo más simple de clasificación es la clasificación binaria, donde solo hay dos etiquetas que puedes predecir. Un ejemplo
es el análisis de fraude, donde una determinada transacción puede clasificarse como fraudulenta o no; o spam de correo
electrónico, donde un correo electrónico determinado puede clasificarse como spam o no spam.

Clasificación multiclase
Más allá de la clasificación binaria se encuentra la clasificación multiclase, en la que se elige una etiqueta entre más de dos posibles
etiquetas distintas. Un ejemplo típico es Facebook prediciendo las personas en una foto determinada o un meteorólogo prediciendo
el clima (lluvioso, soleado, nublado, etc.). Tenga en cuenta que siempre hay un conjunto finito de clases para predecir; nunca es
ilimitado. Esto también se llama clasificación multinomial.

Clasificación multietiqueta
Finalmente, existe la clasificación multietiqueta, donde una entrada dada puede producir múltiples etiquetas. Por ejemplo, es
posible que desee predecir el género de un libro en función del texto del libro en sí. Si bien esto podría ser multiclase,
probablemente sea más adecuado para multietiqueta porque un libro puede pertenecer a varios géneros. Otro ejemplo de
clasificación multietiqueta es identificar el número de objetos que aparecen en una imagen. Tenga en cuenta que en este ejemplo, la
cantidad de predicciones de salida no es necesariamente fija y puede variar de una imagen a otra.

Modelos de clasificación en MLlib


Spark tiene varios modelos disponibles para realizar clasificaciones binarias y multiclase listas para usar. Los siguientes modelos
están disponibles para la clasificación en Spark:

Regresión logística

Árboles de decisión

Bosques aleatorios

Árboles potenciados por gradientes

Spark no admite la realización de predicciones multietiqueta de forma nativa. Para entrenar un modelo multietiqueta, debe
entrenar un modelo por etiqueta y combinarlos manualmente. Una vez construidos manualmente, existen herramientas
integradas que admiten la medición de este tipo de modelos (discutido al final del capítulo).
Machine Translated by Google

Este capítulo cubrirá los conceptos básicos de cada uno de estos modelos proporcionando:

Una explicación simple del modelo y la intuición detrás de él.

Hiperparámetros del modelo (las diferentes formas en que podemos inicializar el modelo)

Parámetros de entrenamiento (parámetros que afectan cómo se entrena el modelo)

Parámetros de predicción (parámetros que afectan cómo se hacen las predicciones)

Puede configurar los hiperparámetros y los parámetros de entrenamiento en un ParamGrid como vimos en
Capítulo 24.

Escalabilidad del modelo


La escalabilidad del modelo es una consideración importante al elegir su modelo. En general, Spark tiene
gran soporte para entrenar modelos de aprendizaje automático a gran escala (nota, estos son a gran escala; en
cargas de trabajo de un solo nodo, hay otras herramientas que también funcionan bien). La tabla 26­1 es una
cuadro de mando de escalabilidad de modelo simple para encontrar el mejor modelo para su tarea en particular (si
la escalabilidad es su principal consideración). La escalabilidad real dependerá de su configuración,
tamaño de la máquina y otros detalles, pero debería ser una buena heurística.

Tabla 26­1. Referencia de escalabilidad del modelo

Modelo Las características cuentan Ejemplos de entrenamiento Clases de salida

Regresión logística 1 a 10 millones Sin límite Características x Clases < 10 millones

Árboles de decisión 1,000s Sin límite Características x Clases < 10,000s

Bosque aleatorio 10,000s Sin límite Características x Clases < 100,000s

Árboles potenciados por gradiente 1,000s Sin límite Características x Clases < 10,000s

Podemos ver que casi todos estos modelos escalan a grandes colecciones de datos de entrada y hay
trabajo en curso para escalarlos aún más. La razón por la que no existe un límite para el número de
ejemplos de entrenamiento se debe a que estos se entrenan utilizando métodos como el descenso de gradiente estocástico y
L­BFGS. Estos métodos están optimizados específicamente para trabajar con conjuntos de datos masivos y para
elimine cualquier restricción que pueda existir en la cantidad de ejemplos de capacitación que esperaría
aprender sobre

Comencemos a ver los modelos de clasificación cargando algunos datos:

// en escala
val bInput = chispa.read.format("parquet").load("/data/binary­classification")
.selectExpr("características", "cast(etiqueta como doble) como etiqueta")

# en pitón
Machine Translated by Google

bInput = chispa.read.format("parquet").load("/data/binary­classification")\
.selectExpr("características", "cast(etiqueta como doble) como etiqueta")

NOTA

Al igual que nuestros otros capítulos de análisis avanzado, este no puede enseñarle los fundamentos matemáticos
de cada modelo. Vea el Capítulo 4 en ISL y ESL para una revisión de la clasificación.

Regresión logística
La regresión logística es uno de los métodos de clasificación más populares. Es un método lineal que combina cada
una de las entradas individuales (o características) con pesos específicos (estos pesos se generan durante el
proceso de entrenamiento) que luego se combinan para obtener una probabilidad de pertenecer a una clase en
particular. Estos pesos son útiles porque son buenas representaciones de la importancia de las características; si
tiene un peso grande, puede suponer que las variaciones en esa característica tienen un efecto significativo en
el resultado (suponiendo que haya realizado la normalización). Un peso más pequeño significa que es menos
probable que la función sea importante.

Consulte ISL 4.3 y ESL 4.4 para obtener más información.

Hiperparámetros del modelo


Los hiperparámetros del modelo son configuraciones que determinan la estructura básica del propio modelo.
Los siguientes hiperparámetros están disponibles para la regresión logística:

familia

Puede ser multinomial (dos o más etiquetas distintas; clasificación multiclase) o binaria (solo dos etiquetas
distintas; clasificación binaria).

elasticNetParam

Un valor de punto flotante de 0 a 1. Este parámetro especifica la mezcla de regularización L1 y L2


según la regularización neta elástica (que es una combinación lineal de las dos). Su elección de L1 o L2
depende mucho de su caso de uso particular, pero la intuición es la siguiente: la regularización de L1 (un valor de
1) creará escasez en el modelo porque ciertos pesos de características se convertirán en cero (que tienen
pocas consecuencias para el producción). Por esta razón, puede usarse como un método simple de
selección de características. Por otro lado, la regularización L2 (un valor de 0) no crea escasez
porque los pesos correspondientes para características particulares solo se dirigirán hacia cero, pero nunca
llegarán a cero por completo.
ElasticNet nos brinda lo mejor de ambos mundos: podemos elegir un valor entre 0 y 1 para especificar una
combinación de regularización L1 y L2. En su mayor parte, debe ajustar esto probando diferentes valores.

encajarInterceptar
Machine Translated by Google

Puede ser verdadero o falso. Este hiperparámetro determina si se ajusta o no la intersección o el número
arbitrario que se agrega a la combinación lineal de entradas y pesos del modelo.
Por lo general, querrá ajustar la intersección si no hemos normalizado nuestros datos de entrenamiento.

regParam

Un valor ≥ 0. que determina cuánto peso dar al término de regularización en la función objetivo. Elegir
un valor aquí nuevamente será una función del ruido y la dimensionalidad en nuestro conjunto de
datos. En una canalización, pruebe con una amplia gama de valores (p. ej., 0, 0,01, 0,1, 1).

Estandarización

Puede ser verdadero o falso, ya sea que se estandaricen o no las entradas antes de pasarlas al modelo.
Consulte el Capítulo 25 para obtener más información.

Parámetros de entrenamiento
Los parámetros de entrenamiento se utilizan para especificar cómo realizamos nuestro entrenamiento. Estos son los
parámetros de entrenamiento para la regresión logística.

maxIter

Número total de iteraciones sobre los datos antes de detenerse. Cambiar este parámetro probablemente no
cambiará mucho sus resultados, por lo que no debería ser el primer parámetro que busque ajustar. El valor
predeterminado es 100.

tol

Este valor especifica un umbral por el cual los cambios en los parámetros muestran que optimizamos nuestros pesos
lo suficiente y podemos detener la iteración. Permite que el algoritmo se detenga antes de las iteraciones de maxIter.
El valor predeterminado es 1.0E­6. Este tampoco debería ser el primer parámetro que busque ajustar.

pesoCol

El nombre de una columna de ponderación utilizada para ponderar ciertas filas más que otras. Esta puede
ser una herramienta útil si tiene alguna otra medida de cuán importante es un ejemplo de entrenamiento en
particular y tiene un peso asociado con él. Por ejemplo, podría tener 10 000 ejemplos en los que sabe que
algunas etiquetas son más precisas que otras. Puede sopesar las etiquetas que sabe que son correctas
más que las que no.

Parámetros de predicción
Estos parámetros ayudan a determinar cómo el modelo debería hacer predicciones en el momento de la
predicción, pero no afectan el entrenamiento. Estos son los parámetros de predicción para la regresión
logística:

límite

Un Double en el rango de 0 a 1. Este parámetro es el umbral de probabilidad para cuando se debe predecir
una clase determinada. Puede ajustar este parámetro de acuerdo con sus requisitos para
Machine Translated by Google

Equilibrio entre falsos positivos y falsos negativos. Por ejemplo, si una predicción errónea fuera costosa, es
posible que desee que su umbral de predicción sea muy alto.

umbrales

Este parámetro le permite especificar una matriz de valores de umbral para cada clase al usar la
clasificación multiclase. Funciona de manera similar al parámetro de umbral único descrito anteriormente.

Ejemplo
Aquí hay un ejemplo simple usando el modelo LogisticRegression. Observe cómo no especificamos ningún
parámetro porque aprovecharemos los valores predeterminados y nuestros datos se ajustan a la denominación
de columna adecuada. En la práctica, probablemente no necesite cambiar muchos de los parámetros:

// en Scala
importar org.apache.spark.ml.classification.LogisticRegression val lr =
new LogisticRegression()
println(lr.explainParams()) // ver todos los parámetros val
lrModel = lr.fit(bInput)

# en Python
desde pyspark.ml.classification import LogisticRegression lr =
LogisticRegression() print
lr.explainParams() # ver todos los parámetros lrModel
= lr.fit(bInput)

Una vez que se entrena el modelo, puede obtener información sobre el modelo al observar los coeficientes
y la intersección. Los coeficientes corresponden a los pesos de características individuales (cada peso de
característica se multiplica por cada característica respectiva para calcular la predicción), mientras que la
intersección es el valor de la intersección en cursiva (si elegimos ajustar una al especificar el modelo).
Ver los coeficientes puede ser útil para inspeccionar el modelo que construyó y comparar cómo las características
afectan la predicción:

// en Scala
println(lrModel.coeficientes)
println(lrModel.intercepción)

# en Python
print lrModel.coficients print
lrModel.intercept

Para un modelo multinomial (el actual es binario), se pueden usar lrModel.coficientMatrix y


lrModel.interceptVector para obtener los coeficientes y la intercepción. Estos devolverán tipos Matrix y Vector que
representan los valores o cada una de las clases dadas.
Machine Translated by Google

Resumen Modelo
La regresión logística proporciona un resumen del modelo que le brinda información sobre el modelo final
entrenado. Esto es análogo a los mismos tipos de resúmenes que vemos en muchos paquetes de
aprendizaje automático de lenguaje R. El resumen del modelo actualmente solo está disponible para problemas
de regresión logística binaria, pero es probable que se agreguen resúmenes multiclase en el futuro.
Con el resumen binario, podemos obtener todo tipo de información sobre el modelo en sí, incluido el área bajo
la curva ROC, la medida f por umbral, la precisión, la recuperación, la recuperación por umbrales y la curva
ROC. Tenga en cuenta que para el área bajo la curva, la ponderación de instancias no se tiene en cuenta, por
lo que si desea ver cómo se desempeñó en los valores que ponderó más, tendría que hacerlo manualmente.
Esto probablemente cambiará en futuras versiones de Spark. Puede ver el resumen utilizando las
siguientes API:

// en Scala
importar org.apache.spark.ml.classification.BinaryLogisticRegressionSummary val
resumen = lrModel.summary val
bSummary = resumen.asInstanceOf[BinaryLogisticRegressionSummary]
println(bSummary.areaUnderROC)
bSummary.roc.show()
bSummary.pr.show( )

# en Python
resumen = lrModel.summary
print resumen.areaUnderROC
resumen.roc.show()
resumen.pr.show()

La velocidad a la que el modelo desciende hasta el resultado final se muestra en el historial de objetivos.
Podemos acceder a esto a través de la historia objetiva en el resumen del modelo:

resumen.objetivoHistoria

Esta es una matriz de dobles que especifica cómo, en cada iteración de entrenamiento, nos estamos
desempeñando con respecto a nuestra función objetivo. Esta información es útil para ver si tenemos
suficientes iteraciones o si necesitamos ajustar otros parámetros.

Árboles de decisión
Los árboles de decisión son uno de los modelos más amigables e interpretables para realizar la clasificación
porque son similares a los modelos de decisión simples que los humanos usan con bastante frecuencia. Por
ejemplo, si tiene que predecir si alguien comerá o no helado cuando se lo ofrezcan, una buena
característica podría ser si a esa persona le gusta o no el helado. En pseudocódigo,
si a la persona le gusta ("ice_cream"), comerá helado; de lo contrario, no comerán helado. Un árbol de
decisión crea este tipo de estructura con todas las entradas y sigue un conjunto de ramas cuando llega el
momento de hacer una predicción. Esto lo convierte en un gran modelo de punto de partida porque es fácil de
Machine Translated by Google

razonar, fácil de inspeccionar y hace muy pocas suposiciones sobre la estructura de los datos. En resumen, en lugar de
intentar entrenar coeficientes para modelar una función, simplemente crea un gran árbol de decisiones a seguir en el
momento de la predicción. Este modelo también admite la clasificación multiclase y proporciona resultados como
predicciones y probabilidades en dos columnas diferentes.

Si bien este modelo suele ser un gran comienzo, tiene un costo. Puede sobreajustar datos extremadamente rápido.
Con eso queremos decir que, sin restricciones, el árbol de decisiones creará un camino desde el principio basado en cada
ejemplo de entrenamiento. Eso significa que codifica toda la información del conjunto de entrenamiento en el modelo.
Esto es malo porque entonces el modelo no se generalizará a nuevos datos (verá un bajo rendimiento de predicción del
conjunto de prueba). Sin embargo, hay varias formas de intentar controlar el modelo limitando su estructura de ramificación
(por ejemplo, limitando su altura) para obtener un buen poder predictivo.

Consulte ISL 8.1 y ESL 9.2 para obtener más información.

Hiperparámetros del modelo


Hay muchas formas diferentes de configurar y entrenar árboles de decisión. Estos son los hiperparámetros
que admite la implementación de Spark:

máxima profundidad

Dado que estamos entrenando un árbol, puede ser útil especificar una profundidad máxima para evitar el
ajuste excesivo al conjunto de datos (en el extremo, cada fila termina como su propio nodo de hoja). El valor
predeterminado es 5.

maxBins

En los árboles de decisión, las entidades continuas se convierten en entidades categóricas y maxBins determina
cuántos contenedores se deben crear a partir de entidades continuas. Más contenedores dan un mayor nivel de
granularidad. El valor debe ser mayor o igual a 2 y mayor o igual al número de categorías en cualquier característica
categórica de su conjunto de datos. El valor predeterminado es 32.

impureza

Para construir un "árbol", debe configurar cuándo debe ramificarse el modelo. La impureza representa la
métrica (ganancia de información) para determinar si el modelo debe dividirse o no en un nodo de hoja en particular. Este
parámetro se puede configurar para que sea "entropía" o "gini" (predeterminado), dos métricas de impureza de uso
común.

minInfoGain

Este parámetro determina la ganancia de información mínima que se puede utilizar para una división. Un valor
más alto puede evitar el sobreajuste. Esto es en gran parte algo que debe determinarse probando diferentes
variaciones del modelo de árbol de decisión. El valor predeterminado es cero.

minInstancePerNode

Este parámetro determina el número mínimo de instancias de entrenamiento que deben terminar en un
Machine Translated by Google

nodo particular. Piense en esto como otra forma de controlar la profundidad máxima. Podemos evitar el sobreajuste
limitando la profundidad o podemos evitarlo especificando que, como mínimo, una cierta cantidad de valores de
entrenamiento deben terminar en un nodo hoja en particular. Si no se cumple, “podemos” el árbol hasta que se
cumpla ese requisito. Un valor más alto puede evitar el sobreajuste. El valor predeterminado es 1, pero puede
ser cualquier valor mayor que 1.

Parámetros de entrenamiento
Estas son configuraciones que especificamos para manipular cómo realizamos nuestro entrenamiento. Aquí está el
parámetro de entrenamiento para los árboles de decisión:

Intervalo de punto de control

La creación de puntos de control es una forma de guardar el trabajo del modelo durante el curso del entrenamiento
para que, si los nodos del clúster fallan por algún motivo, no pierda su trabajo. Un valor de 10 significa que el
modelo se comprobará cada 10 iteraciones. Establézcalo en ­1 para desactivar los puntos de control. Este parámetro
debe configurarse junto con un checkpointDir (un directorio para el punto de control) y con useNodeIdCache=true.
Consulte la documentación de Spark para obtener más información sobre los puntos de control.

Parámetros de predicción

Solo hay un parámetro de predicción para los árboles de decisión: los umbrales. Consulte la explicación de los umbrales
en "Regresión logística".

Aquí hay un ejemplo mínimo pero completo del uso de un clasificador de árbol de decisión:

// en Scala
import org.apache.spark.ml.classification.DecisionTreeClassifier val dt = new
DecisionTreeClassifier()
println(dt.explainParams()) val
dtModel = dt.fit(bInput)

# en Python
desde pyspark.ml.classification import DecisionTreeClassifier dt =
DecisionTreeClassifier() print
dt.explainParams() dtModel
= dt.fit(bInput)

Bosque aleatorio y árboles potenciados por degradado

Estos métodos son extensiones del árbol de decisión. En lugar de entrenar un árbol en todos los datos, entrena múltiples
árboles en diferentes subconjuntos de datos. La intuición detrás de hacer esto es que varios árboles de decisión se
convertirán en "expertos" en ese dominio en particular, mientras que otros se convertirán en expertos en otros. Al
combinar estos diversos expertos, se obtiene un efecto de "sabiduría de las multitudes", donde el desempeño del
grupo supera a cualquier individuo. Además, estos métodos pueden
Machine Translated by Google

ayudar a prevenir el sobreajuste.

Los bosques aleatorios y los árboles potenciados por gradientes son dos métodos distintos para combinar árboles de decisión.
En los bosques aleatorios, simplemente entrenamos muchos árboles y luego promediamos su respuesta para hacer una predicción.
Con los árboles potenciados por gradientes, cada árbol hace una predicción ponderada (de modo que algunos árboles tienen más
poder predictivo para algunas clases que para otras). Tienen en gran medida los mismos parámetros, que anotamos a
continuación. Una limitación actual es que los árboles potenciados por gradiente actualmente solo admiten etiquetas binarias.

NOTA

Hay varias herramientas populares para aprender modelos basados en árboles. Por ejemplo, la biblioteca XGBoost
proporciona un paquete de integración para Spark que se puede usar para ejecutarlo en Spark.

Consulte ISL 8.2 y ESL 10.1 para obtener más información sobre estos modelos de conjuntos de árboles.

Hiperparámetros del modelo


Los bosques aleatorios y los árboles potenciados por gradiente proporcionan todos los mismos hiperparámetros del modelo
respaldados por árboles de decisión. Además, añaden varios propios.

Solo bosque aleatorio

numArboles

El número total de árboles a entrenar.

featureSubsetStrategy Este

parámetro determina cuántas características se deben considerar para las divisiones. Puede ser una variedad de valores
diferentes, incluidos "auto", "all", "sqrt", "log2" o un número "n". Cuando su entrada es "n", el modelo utilizará n * número de
funciones durante el entrenamiento. Cuando n está en el rango (1, número de funciones), el modelo utilizará n funciones durante
el entrenamiento. Aquí no existe una solución única para todos, por lo que vale la pena experimentar con diferentes valores en
su canalización.

Solo árboles potenciados por gradiente (GBT)

tipo de pérdida

Esta es la función de pérdida para que los árboles potenciados por gradientes se minimicen durante el entrenamiento.
Actualmente, solo se admite la pérdida logística.

maxIter

Número total de iteraciones sobre los datos antes de detenerse. Cambiar esto probablemente no cambie mucho sus
resultados, por lo que no debería ser el primer parámetro que busque ajustar. El valor predeterminado es 100.
Machine Translated by Google

Numero de pie

Esta es la tasa de aprendizaje del algoritmo. Un tamaño de paso más grande significa que se realizan saltos más grandes entre

iteraciones de entrenamiento. Esto puede ayudar en el proceso de optimización y es algo que debe probarse en el entrenamiento. El valor

predeterminado es 0.1 y puede ser cualquier valor de 0 a 1.

Parámetros de entrenamiento

Solo hay un parámetro de entrenamiento para estos modelos, checkpointInterval. Consulte la explicación en "Árboles de decisión" para obtener

detalles sobre los puntos de control.

Parámetros de predicción

Estos modelos tienen los mismos parámetros de predicción que los árboles de decisión. Consulte los parámetros de predicción de ese

modelo para obtener más información.

Aquí hay un ejemplo de código corto del uso de cada uno de estos clasificadores:

// en Scala
import org.apache.spark.ml.classification.RandomForestClassifier val
rfClassifier = new RandomForestClassifier()
println(rfClassifier.explainParams()) val
TrainedModel = rfClassifier.fit(bInput)

// en Scala
import org.apache.spark.ml.classification.GBTClassifier val
gbtClassifier = new GBTClassifier()
println(gbtClassifier.explainParams()) val
TrainedModel = gbtClassifier.fit(bInput)

# en Python
desde pyspark.ml.classification import RandomForestClassifier rfClassifier
= RandomForestClassifier() print
rfClassifier.explainParams() TrainedModel
= rfClassifier.fit(bInput)

# en Python
desde pyspark.ml.classification import GBTClassifier
gbtClassifier = GBTClassifier() print
gbtClassifier.explainParams() modelo
entrenado = gbtClassifier.fit(bInput)

bayesiana ingenua

Los clasificadores Naive Bayes son una colección de clasificadores basados en el teorema de Bayes. La suposición central detrás

de los modelos es que todas las características de sus datos son independientes entre sí.

Naturalmente, la independencia estricta es un poco ingenua, pero incluso si se viola, aún se pueden producir modelos útiles. Los clasificadores

Naive Bayes se usan comúnmente en la clasificación de textos o documentos,


Machine Translated by Google

aunque también se puede utilizar como un clasificador de propósito más general. Hay dos tipos de modelos diferentes:
un modelo de Bernoulli multivariado, donde las variables indicadoras representan la existencia de un término en un
documento; o el modelo multinomial, donde se utilizan los recuentos totales de términos.

Una nota importante cuando se trata de Naive Bayes es que todas las características de entrada deben ser no negativas.

Consulte ISL 4.4 y ESL 6.6 para obtener más información sobre estos modelos.

Hiperparámetros del modelo


Estas son configuraciones que especificamos para determinar la estructura básica de los modelos:

tipo de modelo

O "bernoulli" o "multinomial". Consulte la sección anterior para obtener más información sobre esta opción.

pesoCol

Permite pesar diferentes puntos de datos de manera diferente. Consulte "Parámetros de entrenamiento" para obtener la
explicación de este hiperparámetro.

Parámetros de entrenamiento
Estas son configuraciones que especifican cómo realizamos nuestro entrenamiento:

alisado

Esto determina la cantidad de regularización que debe tener lugar mediante el suavizado aditivo. Esto ayuda
a suavizar los datos categóricos y evita el sobreajuste de los datos de entrenamiento al cambiar la probabilidad esperada
para ciertas clases. El valor predeterminado es 1.

Parámetros de predicción

Naive Bayes comparte el mismo parámetro de predicción, umbrales, que todos nuestros otros modelos.
Vuelva a consultar la explicación anterior sobre el umbral para ver cómo usarlo.

Aquí hay un ejemplo del uso de un clasificador Naive Bayes.

// en Scala
import org.apache.spark.ml.classification.NaiveBayes val nb =
new NaiveBayes()
println(nb.explainParams()) val
TrainedModel = nb.fit(bInput.where("label != 0"))

# en Python
desde pyspark.ml.classification import NaiveBayes nb =
NaiveBayes() print
nb.explainParams()
Machine Translated by Google

modelo entrenado = nb.fit(bInput.where("etiqueta != 0"))

ADVERTENCIA

Tenga en cuenta que en este conjunto de datos de ejemplo, tenemos características que tienen valores negativos.
En este caso, las filas con características negativas corresponden a filas con etiqueta “0”. Por lo tanto, solo los
filtraremos (a través de la etiqueta) en lugar de procesarlos más para demostrar la ingenua API de Bayes.

Evaluadores para Clasificación y Automatización de Ajuste de Modelos


Como vimos en el Capítulo 24, los evaluadores nos permiten especificar la métrica de éxito de nuestro modelo. Un evaluador no
ayuda demasiado cuando está solo; sin embargo, cuando lo usamos en una tubería, podemos automatizar una búsqueda en
cuadrícula de nuestros diversos parámetros de los modelos y transformadores, probando todas las combinaciones de los
parámetros para ver cuáles funcionan mejor. Los evaluadores son más útiles en este contexto de canalización y cuadrícula de
parámetros. Para la clasificación, hay dos evaluadores y esperan dos columnas: una etiqueta predicha del modelo y una
etiqueta verdadera. Para la clasificación binaria usamos BinaryClassificationEvaluator. Esto admite la optimización para dos

métricas diferentes "areaUnderROC" y areaUnderPR". Para la clasificación multiclase, necesitamos usar el


MulticlassClassificationEvaluator, que admite la optimización para "f1", "weightedPrecision", "weightedRecall" y "accuracy".

Para usar evaluadores, construimos nuestra canalización, especificamos los parámetros que nos gustaría probar y luego la
ejecutamos y vemos los resultados. Consulte el Capítulo 24 para ver un ejemplo de código.

Métricas de evaluación detalladas


MLlib también contiene herramientas que le permiten evaluar varias métricas de clasificación a la vez.
Desafortunadamente, estas clases de métricas no se han transferido al paquete de ML basado en DataFrame de Spark desde el
marco RDD subyacente. Entonces, en el momento de escribir este artículo, aún debe crear un RDD para usarlos. En el futuro,
es probable que esta funcionalidad se transfiera a DataFrames y es posible que la siguiente ya no sea la mejor manera de ver
las métricas (aunque aún podrá usar estas API).

Hay tres métricas de clasificación diferentes que podemos usar:

Métricas de clasificación binaria

Métricas de clasificación multiclase

Métricas de clasificación multietiqueta

Todas estas medidas siguen el mismo estilo aproximado. Compararemos los resultados generados con valores reales y el modelo
calculará todas las métricas relevantes para nosotros. Luego podemos consultar el objeto para los valores de cada una de
las métricas:
Machine Translated by Google

// en Scala
importar org.apache.spark.mllib.evaluacion.BinaryClassificationMetrics val out =
model.transform(bInput) .select("prediction",
"label") .rdd.map(x => (x(0).
asInstanceOf[Double], x(1).asInstanceOf[Double])) val metrics = new
BinaryClassificationMetrics(out)

# en Python
desde pyspark.mllib.e Evaluation import BinaryClassificationMetrics out =
model.transform(bInput)\
.select("predicción", "etiqueta")
\ .rdd.map(lambda x: (float(x[0]), float(x[1])))
métricas = BinaryClassificationMetrics(fuera)

Una vez que hayamos hecho eso, podemos ver las métricas de éxito de clasificación típicas en el objeto de
esta métrica usando una API similar a la que vimos con la regresión logística:

// en Scala
metrics.areaUnderPR
metrics.areaUnderROC
println("Característica operativa del receptor")
metrics.roc.toDF().show()

# en Python
print metrics.areaUnderPR
print metrics.areaUnderROC
print "Característica operativa del receptor"
metrics.roc.toDF().show()

Clasificador uno contra resto


Hay algunos modelos de MLlib que no admiten la clasificación multiclase. En estos casos, los usuarios pueden
aprovechar un clasificador de uno contra el resto para realizar una clasificación multiclase dado solo un
clasificador binario. La intuición detrás de esto es que para cada clase que espera predecir, el clasificador de
uno contra el resto convertirá el problema en un problema de clasificación binaria al aislar una clase como clase
objetivo y agrupar todas las demás clases en una sola. Así, la predicción de la clase se vuelve binaria (¿es
esta clase o no es esta clase?).

One­vs­rest se implementa como un estimador. Para el clasificador base, toma instancias del clasificador
y crea un problema de clasificación binaria para cada una de las clases. El clasificador de la clase i está
entrenado para predecir si la etiqueta es i o no, distinguiendo la clase i de todas las demás clases.

Las predicciones se realizan evaluando cada clasificador binario y el índice del clasificador más confiable se
genera como etiqueta.

Consulte la documentación de Spark para ver un buen ejemplo del uso de one­vs­rest.
Machine Translated by Google

Perceptrón multicapa
El perceptrón multicapa es un clasificador basado en redes neuronales con un número configurable de capas (y
tamaños de capa). Lo discutiremos en el Capítulo 31.

Conclusión
En este capítulo cubrimos la mayoría de las herramientas que Spark proporciona para la clasificación: predecir
una de un conjunto finito de etiquetas para cada punto de datos en función de sus características. En el próximo
capítulo, veremos la regresión, donde la salida requerida es continua en lugar de categórica.
Machine Translated by Google

Capítulo 27. Regresión

La regresión es una extensión lógica de la clasificación. En lugar de simplemente predecir un valor único a partir de un conjunto de
valores, la regresión es el acto de predecir un número real (o variable continua) a partir de un conjunto de características
(representadas como números).

La regresión puede ser más difícil que la clasificación porque, desde una perspectiva matemática, hay un número infinito de
posibles valores de salida. Además, nuestro objetivo es optimizar alguna métrica de error entre el valor predicho y el verdadero,
en lugar de una tasa de precisión. Aparte de eso, la regresión y la clasificación son bastante similares. Por esta razón,
veremos muchos de los mismos conceptos subyacentes aplicados a la regresión como lo hicimos con la clasificación.

Casos de uso
El siguiente es un pequeño conjunto de casos de uso de regresión que pueden ayudarlo a pensar en posibles problemas de
regresión en su propio dominio:

Predicción de la audiencia de películas

Dada la información sobre una película y el público que la ve, como cuántas personas vieron el tráiler o lo compartieron
en las redes sociales, es posible que desee predecir cuántas personas verán la película cuando se estrene.

Predecir los ingresos de la empresa

Dada una trayectoria de crecimiento actual, el mercado y la estacionalidad, es posible que desee predecir cuántos
ingresos obtendrá una empresa en el futuro.

Predicción del rendimiento del cultivo

Dada la información sobre el área particular en la que se cultiva un cultivo, así como el clima actual durante todo el año, es
posible que desee predecir el rendimiento total del cultivo para una parcela de tierra en particular.

Modelos de regresión en MLlib


Hay varios modelos de regresión fundamentales en MLlib. Algunos de estos modelos son remanentes del Capítulo 26. Otros solo
son relevantes para el dominio del problema de regresión. Esta lista está actualizada a partir de Spark 2.2 pero crecerá:

regresión lineal

Regresión lineal generalizada

regresión isotónica
Machine Translated by Google

Árboles de decisión

Bosque aleatorio

Árboles potenciados por gradientes

Regresión de supervivencia

Este capítulo cubrirá los conceptos básicos de cada uno de estos modelos particulares proporcionando:

Una explicación simple del modelo y la intuición detrás del algoritmo.

Hiperparámetros del modelo (las diferentes formas en que podemos inicializar el modelo)

Parámetros de entrenamiento (parámetros que afectan cómo se entrena el modelo)

Parámetros de predicción (parámetros que afectan cómo se hacen las predicciones)

Puede buscar sobre los hiperparámetros y los parámetros de entrenamiento usando un ParamGrid, como vimos
en el Capítulo 24.

Escalabilidad del modelo


Todos los modelos de regresión en MLlib se escalan a grandes conjuntos de datos. La tabla 27­1 es un modelo simple
cuadro de mando de escalabilidad que le ayudará a elegir el mejor modelo para su tarea en particular (si
la escalabilidad es su principal consideración). Estos dependerán de su configuración, tamaño de la máquina,
y otros factores.

Tabla 27­1. Referencia de escalabilidad de regresión

Modelo Características numéricas Ejemplos de entrenamiento

regresión lineal 1 a 10 millones Sin límite

Regresión lineal generalizada 4.096 Sin límite

regresión isotónica N/A millones

Árboles de decisión 1,000s Sin límite

Bosque aleatorio 10,000s Sin límite

Árboles potenciados por gradientes 1,000s Sin límite

Regresión de supervivencia 1 a 10 millones Sin límite

NOTA

Al igual que nuestros otros capítulos de análisis avanzado, este no puede enseñarle los fundamentos matemáticos.
de cada modelo. Consulte el Capítulo 3 en ISL y ESL para obtener una revisión de la regresión.
Machine Translated by Google

Leamos algunos datos de muestra que usaremos a lo largo del capítulo:

// en Scala
val df = spark.read.load("/data/regression")

# en Python
df = chispa.read.load("/datos/regresión")

Regresión lineal
La regresión lineal asume que una combinación lineal de sus características de entrada (la suma de cada característica
multiplicada por un peso) resulta junto con una cantidad de error gaussiano en la salida. Esta suposición lineal (junto con el error
gaussiano) no siempre es cierta, pero crea un modelo simple e interpretable que es difícil de sobreajustar. Al igual que la regresión
logística, Spark implementa la regularización de ElasticNet para esto, lo que le permite combinar la regularización de L1 y L2.

Consulte ISL 3.2 y ESL 3.2 para obtener más información.

Hiperparámetros del modelo


La regresión lineal tiene los mismos hiperparámetros del modelo que la regresión logística. Consulte el Capítulo 26 para obtener
más información.

Parámetros de entrenamiento
La regresión lineal también comparte todos los mismos parámetros de entrenamiento de la regresión logística. Consulte el Capítulo
26 para obtener más información sobre este tema.

Ejemplo
Aquí hay un breve ejemplo del uso de la regresión lineal en nuestro conjunto de datos de muestra:

// en Scala
import org.apache.spark.ml.regression.LinearRegression val lr =
new LinearRegression().setMaxIter(10).setRegParam(0.3)\
.setElasticNetParam(0.8)
println(lr.explainParams()) val
lrModel = lr.fit(df)

# en Python
desde pyspark.ml.regression import LinearRegression lr =
LinearRegression().setMaxIter(10).setRegParam(0.3).setElasticNetParam(0.8) print
lr.explainParams() lrModel =
lr.fit(df)
Machine Translated by Google

Resumen de entrenamiento
Al igual que en la regresión logística, obtenemos información de entrenamiento detallada de nuestro modelo. El
método de fuente de código es una abreviatura simple para acceder a estas métricas. Informa varias
métricas convencionales para medir el éxito de un modelo de regresión, lo que le permite ver qué tan
bien su modelo realmente se ajusta a la línea.

El método de resumen devuelve un objeto de resumen con varios campos. Repasemos estos a su vez.
Los residuos son simplemente los pesos de cada una de las características que ingresamos en el modelo.
El historial objetivo muestra cómo va nuestro entrenamiento en cada iteración. El error cuadrático medio es
una medida de qué tan bien se ajusta nuestra línea a los datos, determinado al observar la distancia entre
cada valor predicho y el valor real en los datos. La variable R cuadrada es una medida de la proporción
de la varianza de la variable predicha que captura el modelo.

Hay una serie de métricas e información resumida que pueden ser relevantes para su caso de uso.
Esta sección demuestra la API, pero no cubre todas las métricas de manera exhaustiva (consulte la
documentación de la API para obtener más información).

Estos son algunos de los atributos del resumen del modelo para la regresión lineal:

// en Scala
val resumen = lrModel.summary
summary.residuals.show()
println(summary.objectiveHistory.toSeq.toDF.show())
println(summary.rootMeanSquaredError)
println(summary.r2)

# en Python
resumen = lrModel.summary
summary.residuals.show()
print summary.totalIterations print
summary.objectiveHistory print
summary.rootMeanSquaredError print
summary.r2

Regresión lineal generalizada


La regresión lineal estándar que vimos en este capítulo es en realidad parte de una familia de
algoritmos llamada regresión lineal generalizada. Spark tiene dos implementaciones de este
algoritmo. Uno está optimizado para trabajar con conjuntos muy grandes de funciones (la regresión
lineal simple que se trató anteriormente en este capítulo), mientras que el otro es más general, incluye soporte
para más algoritmos y actualmente no escala a un gran número de funciones.

La forma generalizada de regresión lineal le brinda un control más detallado sobre el tipo de modelo de
regresión que utiliza. Por ejemplo, estos le permiten seleccionar la distribución de ruido esperada de una
variedad de familias, que incluyen gaussiana (regresión lineal), binomial (regresión logística), poisson
(regresión de poisson) y gamma (regresión gamma). Los modelos generalizados también admiten el
establecimiento de una función de enlace que especifica la relación entre el predictor lineal y el
Machine Translated by Google

media de la función de distribución. La Tabla 27­2 muestra las funciones de enlace disponibles para cada familia.

Tabla 27­2. Familias de regresión, tipos de respuesta y funciones de


enlace

Familia Tipo de respuesta Enlaces compatibles

continuo gaussiano Identidad*, Registro, Inverso

Binomial Binario Logit*, Probit, CLogLog

Conde de veneno Registro*, Identidad, Sqrt

gamma continuo Inversa*, Identidad, Log

Tweedie Zero­inflated función Power link continuo

El asterisco representa la función de enlace canónico para cada familia.

Consulte ISL 3.2 y ESL 3.2 para obtener más información sobre modelos lineales generalizados.

ADVERTENCIA

Una limitación fundamental a partir de Spark 2.2 es que la regresión lineal generalizada solo acepta un máximo de 4096
características para las entradas. Es probable que esto cambie para versiones posteriores de Spark, así que asegúrese de
consultar la documentación.

Hiperparámetros del modelo


Estas son configuraciones que especificamos para determinar la estructura básica del propio modelo. Además de fitIntercept y

regParam (mencionados en "Regresión"), la regresión lineal generalizada incluye varios otros hiperparámetros:

familia

Una descripción de la distribución de error que se utilizará en el modelo. Las opciones admitidas son Poisson, binomial,
gamma, gaussiana y tweedie.

enlace

El nombre de la función de enlace que proporciona la relación entre el predictor lineal y la media de la función de
distribución. Las opciones admitidas son cloglog, probit, logit, inversa, sqrt, identidad y registro (predeterminado:
identidad).

solucionador

El algoritmo del solucionador que se utilizará para la optimización. El único solucionador admitido actualmente es irls (mínimos
cuadrados reponderados iterativamente).
Machine Translated by Google

varianzaPotencia

La potencia en la función de varianza de la distribución de Tweedie, que caracteriza la relación entre la


varianza y la media de la distribución. Solo aplicable a la familia Tweedie. Los valores admitidos son 0 y
[1, infinito). El valor predeterminado es 0.

linkPower

El índice en la función de enlace de potencia para la familia Tweedie.

Parámetros de entrenamiento
Los parámetros de entrenamiento son los mismos que encontrarás para la regresión logística. Consulte el
Capítulo 26 para obtener más información.

Parámetros de predicción
Este modelo agrega un parámetro de predicción:

enlacePredicciónCol

Un nombre de columna que contendrá la salida de nuestra función de enlace para cada predicción.

Ejemplo
Aquí hay un ejemplo del uso de GeneralizedLinearRegression:

// en Scala
import org.apache.spark.ml.regression.GeneralizedLinearRegression val glr =
new GeneralizedLinearRegression()

.setFamily("gaussiana") .setLink("identidad") .setMaxIter(10) .setRegParam(0.3) .setLinkPredictionCol("linkOut")


println(glr.explainParams()) val
glrModel = glr.fit(df)

# en Python
desde pyspark.ml.regression import GeneralizedLinearRegression glr =
GeneralizedLinearRegression()
\ .setFamily("gaussian")
\ .setLink("identity")

\ .setMaxIter(10)\ .setRegParam(0.3)\ .setLinkPredictionCol("linkOut ") imprimir glr.explainParams() glrModel = glr.fit(df)


Machine Translated by Google

Resumen de entrenamiento
En cuanto al modelo lineal simple de la sección anterior, el resumen de entrenamiento proporcionado por Spark para
el modelo lineal generalizado puede ayudarlo a asegurarse de que su modelo se ajuste bien a los datos que usó
como conjunto de entrenamiento. Es importante tener en cuenta que esto no reemplaza la ejecución de su algoritmo
contra un conjunto de prueba adecuado, pero puede proporcionar más información. Esta información incluye
varias métricas potenciales diferentes para analizar el ajuste de su algoritmo, incluidas algunas de las métricas de éxito
más comunes:

R­cuadrado

El coeficiente de determinación; una medida de ajuste.

los residuos

La diferencia entre la etiqueta y el valor predicho.

Asegúrese de inspeccionar el objeto de resumen en el modelo para ver todos los métodos disponibles.

Árboles de decisión
Los árboles de decisión aplicados a la regresión funcionan de manera bastante similar a los árboles de
decisión aplicados a la clasificación. La principal diferencia es que los árboles de decisión para la regresión generan un
solo número por nodo hoja en lugar de una etiqueta (como vimos con la clasificación). Se siguen aplicando las mismas
propiedades de interpretabilidad y estructura del modelo. En resumen, en lugar de intentar entrenar coeficientes
para modelar una función, la regresión del árbol de decisiones simplemente crea un árbol para predecir los resultados
numéricos. Esto tiene una consecuencia importante porque, a diferencia de la regresión lineal generalizada, podemos
predecir funciones no lineales en los datos de entrada. Esto también crea un riesgo significativo de sobreajuste de los
datos, por lo que debemos tener cuidado al ajustar y evaluar estos modelos.

También cubrimos los árboles de decisión en el Capítulo 26 (consulte “Árboles de decisión”). Para obtener más
información sobre este tema, consulte ISL 8.1 y ESL 9.2.

Hiperparámetros del modelo


Los hiperparámetros del modelo que aplican árboles de decisión para la regresión son los mismos que para la
clasificación excepto por un ligero cambio en el parámetro de impureza. Consulte el Capítulo 26 para obtener
más información sobre los otros hiperparámetros:

impureza

El parámetro de impureza representa la métrica (ganancia de información) de si el modelo debe o no dividirse en un


nodo de hoja particular con un valor particular o mantenerlo como está. La única métrica admitida actualmente
para los árboles de regresión es la "varianza".

Parámetros de entrenamiento
Además de los hiperparámetros, los árboles de clasificación y regresión también comparten el mismo entrenamiento
Machine Translated by Google

parámetros Consulte "Parámetros de entrenamiento" para estos parámetros.

Ejemplo
Aquí hay un breve ejemplo del uso de un regresor de árbol de decisión:

// en Scala
import org.apache.spark.ml.regression.DecisionTreeRegressor val dtr
= new DecisionTreeRegressor()
println(dtr.explainParams()) val
dtrModel = dtr.fit(df)

# en Python
desde pyspark.ml.regression import DecisionTreeRegressor dtr =
DecisionTreeRegressor() print
dtr.explainParams() dtrModel
= dtr.fit(df)

Bosques aleatorios y árboles potenciados por gradientes

Los modelos de bosque aleatorio y árbol potenciado por gradiente se pueden aplicar tanto a la clasificación como a la
regresión. Como revisión, ambos siguen el mismo concepto básico que el árbol de decisión, excepto que en lugar de
entrenar un árbol, se entrenan muchos árboles para realizar una regresión. En el modelo de bosque aleatorio, muchos árboles
descorrelacionados se entrenan y luego se promedian. Con árboles potenciados por gradiente, cada árbol hace una predicción
ponderada (de modo que algunos árboles tienen más poder predictivo para algunas clases que para otras). La regresión
de árbol potenciada por gradiente y de bosque aleatorio tiene los mismos hiperparámetros de modelo y parámetros de
entrenamiento que los modelos de clasificación correspondientes, excepto por la medida de pureza (como es el caso con
DecisionTreeRegressor).

Consulte ISL 8.2 y ESL 10.1 para obtener más información sobre conjuntos de árboles.

Hiperparámetros del modelo


Estos modelos comparten muchos de los mismos parámetros que vimos en el capítulo anterior, así como los árboles de
decisión de regresión. Consulte "Hiperparámetros del modelo" para obtener una explicación detallada de estos parámetros.
Sin embargo, en cuanto a un árbol de regresión único, la única métrica de impureza admitida actualmente es la varianza.

Parámetros de entrenamiento

Estos modelos admiten el mismo parámetro checkpointInterval que los árboles de clasificación, como se describe en
el Capítulo 26.

Ejemplo
Machine Translated by Google

Aquí hay un pequeño ejemplo de cómo usar estos dos modelos para realizar una regresión:

// en Scala
import org.apache.spark.ml.regression.RandomForestRegressor import
org.apache.spark.ml.regression.GBTRegressor val rf = new
RandomForestRegressor()
println(rf.explainParams()) val
rfModel = rf.fit( df) val gbt =
new GBTRegressor()
println(gbt.explainParams()) val
gbtModel = gbt.fit(df)

# en Python
desde pyspark.ml.regression import RandomForestRegressor from
pyspark.ml.regression import GBTRegressor rf =
RandomForestRegressor() print
rf.explainParams() rfModel =
rf.fit(df) gbt =
GBTRegressor() print
gbt.explainParams() gbtModel
= gbt.fit(df)

Métodos Avanzados
Los métodos anteriores son métodos muy generales para realizar una regresión. Los modelos no son exhaustivos,
pero proporcionan los tipos de regresión esenciales que mucha gente usa. La siguiente sección cubrirá algunos
de los modelos de regresión más especializados que incluye Spark. Omitimos ejemplos de código simplemente
porque siguen los mismos patrones que los otros algoritmos.

Regresión de supervivencia (tiempo de falla acelerado)


Los estadísticos utilizan el análisis de supervivencia para comprender la tasa de supervivencia de los
individuos, normalmente en experimentos controlados. Spark implementa el modelo de tiempo de falla acelerado
que, en lugar de describir el tiempo de supervivencia real, modela el registro del tiempo de supervivencia. Esta
variación de la regresión de supervivencia se implementa en Spark porque el modelo de riesgo proporcional
de Cox más conocido es semiparamétrico y no se adapta bien a grandes conjuntos de datos. Por el
contrario, el tiempo de falla acelerado sí lo hace porque cada instancia (fila) contribuye al modelo resultante de forma independiente.
El tiempo de falla acelerado tiene diferentes supuestos que el modelo de supervivencia de Cox y, por lo
tanto, uno no es necesariamente un reemplazo directo para el otro. Cubrir estas diferentes suposiciones está
fuera del alcance de este libro. Consulte el artículo de LJ Wei sobre el tiempo de falla acelerado para obtener
más información.

El requisito de entrada es bastante similar al de otras regresiones. Ajustaremos los coeficientes de acuerdo con
los valores de las características. Sin embargo, hay una desviación, y es la introducción de una columna
variable de censura. Un sujeto de prueba censura durante un estudio científico cuando ese individuo
abandona un estudio, ya que su estado al final del experimento puede ser desconocido. Esto es importante
porque no podemos asumir un resultado para un individuo que censura (no informa
Machine Translated by Google

ese estado a los investigadores) en algún punto intermedio de un estudio.

Vea más sobre la regresión de supervivencia con AFT en la documentación.

Regresión isotónica
La regresión isotónica es otro modelo de regresión especializado, con algunos requisitos únicos.
Esencialmente, la regresión isotónica especifica una función lineal por partes que siempre aumenta monótonamente.
No puede disminuir. Esto significa que si sus datos van hacia arriba y hacia la derecha en un gráfico determinado,
este es un modelo apropiado. Si varía en el transcurso de los valores de entrada, entonces esto no es
apropiado.

La ilustración del comportamiento de la regresión isotónica en la figura 27­1 hace que sea mucho más
fácil de entender.

Figura 27­1. Línea de regresión isotónica

Observe cómo esto se ajusta mejor que la regresión lineal simple. Obtenga más información sobre cómo usar este
modelo en la documentación de Spark.

Evaluadores y ajuste automático del modelo


La regresión tiene la misma funcionalidad de ajuste del modelo central que vimos con la clasificación. Podemos
especificar un evaluador, elegir una métrica para optimizar y luego entrenar nuestra canalización para realizar
ese ajuste de parámetros de nuestra parte. El evaluador de regresión, como era de esperar, se llama
RegressionEvaluator y nos permite optimizar para una serie de métricas de éxito de regresión comunes. Al igual
que el evaluador de clasificación, RegressionEvaluator espera dos columnas, una columna que representa la
predicción y otra que representa la etiqueta verdadera. Las métricas admitidas

2
Machine Translated by Google

para optimizar son el error cuadrático medio ("rmse"), el error cuadrático medio ("mse"), la métrica r ("r2") y el error 2

absoluto medio ("mae").

Para usar RegressionEvaluator, construimos nuestra canalización, especificamos los parámetros que nos gustaría probar
y luego la ejecutamos. Spark seleccionará automáticamente el modelo que funciona mejor y nos lo devolverá:

// en Scala
importar org.apache.spark.ml.evaluacion.RegressionEvaluator
importar org.apache.spark.ml.regression.GeneralizedLinearRegression importar
org.apache.spark.ml.Pipeline importar
org.apache.spark.ml.tuning.{ CrossValidator, ParamGridBuilder} val glr = new
GeneralizedLinearRegression()

.setFamily("gaussian") .setLink("identity") val pipeline = new


Pipeline().setStages(Array(glr)) val params = new ParamGridBuilder().addGrid(glr.regParam, Array(0, 0.5, 1 ))
.build()
val evaluador = nuevo RegressionEvaluator()

.setMetricName("rmse") .setPredictionCol("prediction") .setLabelCol("label") val cv = new CrossValidator() .setEstimator(pipelin


val modelo = cv.fit(df)

# en Python
desde pyspark.ml.e Evaluation import RegressionEvaluator from
pyspark.ml.regression import GeneralizedLinearRegression from
pyspark.ml import Pipeline from
pyspark.ml.tuning import CrossValidator, ParamGridBuilder glr =
GeneralizedLinearRegression().setFamily("gaussian").setLink( "identidad") canalización =
Pipeline().setStages([glr]) params =
ParamGridBuilder().addGrid(glr.regParam, [0, 0.5, 1]).build() evaluador =
RegressionEvaluator()\ .setMetricName("
rmse")
\ .setPredictionCol("predicción")
\ .setLabelCol("etiqueta")
cv = CrossValidator()\
.setEstimator(pipeline)
\ .setEvaluator(evaluator)
\ .setEstimatorParamMaps(params)
\ .setNumFolds(2) # siempre debe ser 3 o más, pero este conjunto de datos es pequeño
modelo = cv.fit(df)

Métrica
Los evaluadores nos permiten evaluar y ajustar un modelo de acuerdo con una métrica específica, pero también podemos
Machine Translated by Google

acceder a una serie de métricas de regresión a través del objeto RegressionMetrics. En cuanto a las métricas de

clasificación del capítulo anterior, RegressionMetrics opera en RDD de pares (predicción, etiqueta). Por ejemplo, veamos
cómo podemos inspeccionar los resultados del modelo previamente entrenado.

// en Scala
importar org.apache.spark.mllib.evaluacion.RegressionMetrics val out =
model.transform(df)
.select("predicción",
"etiqueta") .rdd.map(x => (x(0).asInstanceOf[Double], x(1).asInstanceOf[Double])) val
metrics = new RegressionMetrics(out) println
(s"MSE = ${metrics.meanSquaredError}") println(s"RMSE
= ${metrics.rootMeanSquaredError}") println(s"R­squared = $
{metrics.r2}") println(s"MAE = $
{metrics.meanAbsoluteError}") println(s"Varianza explicada
= ${metrics.explainedVariance}")

# en Python
desde pyspark.mllib.e Evaluation import RegressionMetrics out =
model.transform(df)\
.select("predicción", "etiqueta").rdd.map(lambda x: (float(x[0]), float(x[1])))
métricas = RegressionMetrics(out) print
"MSE: " + str(metrics.meanSquaredError) print "RMSE: "
+ str(metrics.rootMeanSquaredError) print "R­squared: " +
str(metrics.r2) print "MAE: " +
str(metrics.meanAbsoluteError) print "Varianza explicada:
"
+ str(metrics.explainedVariance)

Consulte la documentación de Spark para conocer los métodos más recientes.

Conclusión
En este capítulo, cubrimos los conceptos básicos de la regresión en Spark, incluido cómo entrenamos modelos y cómo medimos el
éxito. En el próximo capítulo, veremos los motores de recomendación, una de las aplicaciones más populares de MLlib.
Machine Translated by Google

Capítulo 28. Recomendación

La tarea de recomendación es una de las más intuitivas. Al estudiar las preferencias explícitas de las personas (a través de

calificaciones) o las preferencias implícitas (a través del comportamiento observado), puede hacer recomendaciones sobre lo que le puede

gustar a un usuario al establecer similitudes entre el usuario y otros usuarios, o entre los productos que les gustaron y otros productos.

Usando las similitudes subyacentes, los motores de recomendación pueden hacer nuevas recomendaciones a otros usuarios.

Casos de uso
Los motores de recomendación son uno de los mejores casos de uso para big data. Es bastante fácil recopilar datos de capacitación

sobre las preferencias anteriores de los usuarios a escala, y estos datos se pueden usar en muchos dominios para conectar a los usuarios

con contenido nuevo. Spark es una herramienta de código abierto de elección utilizada en una variedad de empresas para recomendaciones

a gran escala:

Recomendaciones de películas

Amazon, Netflix y HBO quieren ofrecer contenido relevante de cine y televisión a sus usuarios.

Netflix utiliza Spark para hacer recomendaciones de películas a gran escala a sus usuarios.

Recomendaciones de cursos

Una escuela puede querer recomendar cursos a los estudiantes estudiando qué cursos les han gustado o tomado a estudiantes

similares. Los datos de inscripción anteriores hacen que sea muy fácil recopilar un conjunto de datos de capacitación para esta
tarea.

En Spark, hay un algoritmo de recomendación de caballo de batalla, Alternating Least Squares (ALS).

Este algoritmo aprovecha una técnica llamada filtrado colaborativo, que hace recomendaciones basadas solo en los

elementos con los que los usuarios interactuaron en el pasado. Es decir, no requiere ni utiliza ninguna función adicional sobre los usuarios o

los elementos. Admite varias variantes de ALS (p. ej., comentarios explícitos o implícitos). Además de ALS, Spark proporciona

minería de patrones frecuentes para encontrar reglas de asociación en el análisis de la cesta de la compra. Finalmente, la API RDD de

Spark también incluye un método de factorización de matriz de nivel inferior que no se tratará en este libro.

Filtrado colaborativo con mínimos cuadrados alternos


ALS encuentra un vector de características bidimensional para cada usuario y elemento, de modo que el producto escalar del vector

de características de cada usuario con el vector de características de cada elemento se aproxima a la calificación del usuario para ese

elemento. Por lo tanto, esto solo requiere un conjunto de datos de entrada de calificaciones existentes entre pares de elementos de

usuario, con tres columnas: una columna de ID de usuario, una columna de ID de elemento (por ejemplo, una película) y una columna de calificación.

Las calificaciones pueden ser explícitas, una calificación numérica que pretendemos predecir directamente, o implícitas , en cuyo caso cada

calificación representa la fuerza de las interacciones observadas entre un usuario y un elemento (por ejemplo, la cantidad de visitas a una

página en particular), que mide nuestro nivel de confianza en el


Machine Translated by Google

preferencia del usuario por ese elemento. Dado este DataFrame de entrada, el modelo producirá vectores de características
que puede usar para predecir las calificaciones de los usuarios para los elementos que aún no han calificado.

Un problema a tener en cuenta en la práctica es que este algoritmo tiene preferencia por servir cosas que son muy comunes o
sobre las que tiene mucha información. Si está presentando un nuevo producto por el que ningún usuario ha expresado
preferencia, el algoritmo no lo recomendará a muchas personas. Además, si se incorporan nuevos usuarios a la plataforma,
es posible que no tengan ninguna calificación en el conjunto de capacitación. Por lo tanto, el algoritmo no sabrá qué
recomendarles. Estos son ejemplos de lo que llamamos el problema del arranque en frío, que trataremos más adelante en este
capítulo.

En términos de escalabilidad, una de las razones de la popularidad de Spark para esta tarea es que el algoritmo y la
implementación en MLlib pueden escalar a millones de usuarios, millones de elementos y miles de millones de calificaciones.

Hiperparámetros del modelo


Estas son configuraciones que podemos especificar para determinar la estructura del modelo así como el problema concreto de
filtrado colaborativo que queremos resolver:

rango

El término de rango determina la dimensión de los vectores de características aprendidos para usuarios y elementos.

Esto normalmente debe ajustarse a través de la experimentación. La compensación principal es que al especificar
un rango demasiado alto, el algoritmo puede sobreajustar los datos de entrenamiento; pero al especificar un rango bajo,
es posible que no haga las mejores predicciones posibles. El valor predeterminado es 10.

alfa

Cuando se entrena con retroalimentación implícita (observaciones de comportamiento), el alfa establece una confianza
de referencia para la preferencia. Esto tiene un valor predeterminado de 1.0 y debe impulsarse a través de la
experimentación.

regParam

Controla la regularización para evitar el sobreajuste. Debe probar diferentes valores para el parámetro de regularización
para encontrar el valor óptimo para su problema. El valor predeterminado es 0.1.

preferencias implícitas

Este valor booleano especifica si está entrenando en implícito (verdadero) o explícito (falso) (consulte la discusión anterior
para obtener una explicación de la diferencia entre explícito e implícito). Este valor debe establecerse en función de los
datos que está utilizando como entrada para el modelo. Si los datos se basan en la aprobación pasiva de un producto
(por ejemplo, a través de un clic o una visita a la página), entonces debe usar preferencias implícitas. Por el contrario,
si los datos son una calificación explícita (por ejemplo, el usuario le dio a este restaurante 4/5 estrellas), debe usar
preferencias explícitas. Las preferencias explícitas son las predeterminadas.

no negativo

Si se establece en verdadero, este parámetro configura el modelo para colocar restricciones no negativas en el
Machine Translated by Google

problema de mínimos cuadrados que resuelve y solo devuelve vectores de características no negativos. Esto
puede mejorar el rendimiento en algunas aplicaciones. El valor predeterminado es falso.

Parámetros de entrenamiento
Los parámetros de entrenamiento para la alternancia de mínimos cuadrados son un poco diferentes a los que hemos visto en
otros modelos. Esto se debe a que obtendremos más control de bajo nivel sobre cómo se distribuyen los datos en el clúster.
Los grupos de datos que se distribuyen alrededor del clúster se denominan bloques. Determinar cuántos datos colocar en cada
bloque puede tener un impacto significativo en el tiempo que lleva entrenar el algoritmo (pero no en el resultado final). Una
buena regla general es apuntar a aproximadamente de uno a cinco millones de calificaciones por bloque. Si tiene menos datos
que eso en cada bloque, más bloques no mejorarán el rendimiento del algoritmo.

numUserBlocks

Esto determina en cuántos bloques dividir a los usuarios. El valor predeterminado es 10.

numItemBlocks

Esto determina en cuántos bloques dividir los elementos. El valor predeterminado es 10.

maxIter

Número total de iteraciones sobre los datos antes de detenerse. Cambiar esto probablemente no cambie mucho sus
resultados, por lo que este no debería ser el primer parámetro que ajuste. El valor predeterminado es 10. Un ejemplo de

cuándo podría querer aumentar esto es después de inspeccionar su historial de objetivos y notar que no se
estanca después de una cierta cantidad de iteraciones de entrenamiento.

Intervalo de punto de control

Los puntos de control le permiten guardar el estado del modelo durante el entrenamiento para recuperarse más

rápidamente de las fallas de los nodos. Puede establecer un directorio de puntos de control mediante SparkContext.setCheckpointDir.

semilla

Especificar una semilla aleatoria puede ayudarlo a replicar sus resultados.

Parámetros de predicción
Los parámetros de predicción determinan cómo un modelo entrenado debe realizar predicciones. En nuestro caso, hay un

parámetro: la estrategia de arranque en frío (establecida a través de coldStartStrategy). Esta configuración determina qué
debe predecir el modelo para los usuarios o elementos que no aparecieron en el conjunto de entrenamiento.

El desafío del arranque en frío suele surgir cuando está sirviendo un modelo en producción, y los nuevos usuarios y/o
elementos no tienen un historial de calificaciones y, por lo tanto, el modelo no tiene ninguna recomendación que hacer.

También puede ocurrir cuando se usan divisiones aleatorias simples como en CrossValidator o TrainValidationSplit de

Spark, donde es muy común encontrar usuarios y/o elementos en el


Machine Translated by Google

conjunto de evaluación que no están en el conjunto de entrenamiento.

De forma predeterminada, Spark asignará valores de predicción de NaN cuando encuentre un usuario o elemento que no esté presente en el modelo

real. Esto puede ser útil porque diseña su sistema general para recurrir a alguna recomendación predeterminada cuando hay un nuevo usuario o elemento

en el sistema. Sin embargo, esto no es deseable durante el entrenamiento porque arruinará la capacidad de su evaluador para medir adecuadamente el

éxito de su modelo. Esto hace que la selección del modelo sea imposible. Spark permite a los usuarios configurar el parámetro coldStartStrategy para

eliminar cualquier fila en el DataFrame de predicciones que contengan valores de NaN. Luego, la métrica de evaluación se calculará sobre los datos

que no sean NaN y será válida. drop y nan (predeterminado) son las únicas estrategias de arranque en frío admitidas actualmente.

Ejemplo
Este ejemplo utilizará un conjunto de datos que no hemos utilizado hasta ahora en el libro, el conjunto de datos de clasificación de

películas de MovieLens. Este conjunto de datos, naturalmente, tiene información relevante para hacer recomendaciones de películas. Primero
usaremos este conjunto de datos para entrenar un modelo:

// en Scala
import org.apache.spark.ml.recommendation.ALS val
ratings = spark.read.textFile("/data/sample_movielens_ratings.txt")
.selectExpr("split(value , '::') as

col") .selectExpr( "cast(col[0] as int) as


userId", "cast(col[1] as int) as movieId",
"cast (col[2] como float) as rating",
"cast(col[3] as long) as timestamp") val
Array(entrenamiento, prueba) = ratings.randomSplit(Array(0.8, 0.2)) val als =
new ALS

() .setMaxIter(5) .setRegParam(0.01) .setUserCol("userId") .setItemCol("movieId") .setRatingCol("puntuación") println(als.expla

# en Python
desde pyspark.ml.recommendation import ALS
from pyspark.sql import
Clasificación de filas = spark.read.text("/data/sample_movielens_ratings.txt")\
.rdd.toDF()
\ .selectExpr("split(value , '::') as col")

\ .selectExpr( "cast(col[0] as int) as


userId", "cast(col[1] as int) como
movieId", "cast(col[2] as float) as rating",
"cast(col[3] as long) as timestamp")
Machine Translated by Google

entrenamiento, prueba = ratings.randomSplit([0.8, 0.2]) als =


ALS()

\ .setMaxIter(5)\ .setRegParam(0.01)\ .setUserCol("userId")\ .setItemCol("movieId")\ .setRatingCol ("calificación") print als.explainPara

Ahora podemos generar las mejores recomendaciones para cada usuario o película. El método de
recomendación para todos los usuarios del modelo devuelve un marco de datos de un ID de
usuario, una serie de recomendaciones, así como una calificación para cada una de esas películas. recomendarForAllItems
devuelve un DataFrame de un movieId, así como los principales usuarios de esa película:

// en Scala

alsModel.recommendForAllUsers(10) .selectExpr("userId",
"explotar(recomendaciones)").show()
alsModel.recommendForAllItems(10) .selectExpr("movieId", "explotar(recomendaciones)").show ()

# en Python

alsModel.recommendForAllUsers(10)\ .selectExpr("userId",
"explotar(recomendaciones)").show()
alsModel.recommendForAllItems(10)\ .selectExpr("movieId", "explotar(recomendaciones)"). espectáculo()

Evaluadores de Recomendación
Al cubrir la estrategia de arranque en frío, podemos configurar un evaluador de modelo automático cuando se
trabaja con ALS. Una cosa que puede no ser obvia de inmediato es que este problema de recomendación es realmente
solo una especie de problema de regresión. Como estamos prediciendo valores (calificaciones) para determinados
usuarios, queremos optimizar para reducir la diferencia total entre las calificaciones de nuestros usuarios y los valores
reales. Podemos hacer esto usando el mismo RegressionEvaluator que vimos en el Capítulo 27. Puede
colocar esto en una tubería para automatizar el proceso de entrenamiento. Al hacer esto, también debe configurar la
estrategia de arranque en frío para que se elimine en lugar de NaN y luego vuelva a cambiarla a NaN cuando llegue
el momento de hacer predicciones en su sistema de producción:

// en Scala
importar org.apache.spark.ml.evaluacion.RegressionEvaluator val
evaluador = new RegressionEvaluator()

.setMetricName("rmse") .setLabelCol("calificación") .setPredictionCol("predicción") val rmse = evaluador.evaluate(predicciones) println


Machine Translated by Google

# en Python
desde pyspark.ml.e Evaluation import RegressionEvaluator
evaluador = RegressionEvaluator()
\ .setMetricName("rmse")
\ .setLabelCol("rating")
\ .setPredictionCol("predicción") rmse
= evaluador.evaluate(predicciones) print( "Error
cuadrático medio = %f" % rmse)

Métrica
Los resultados de las recomendaciones se pueden medir utilizando las métricas de regresión estándar y algunas métricas
específicas de recomendaciones. No debería sorprender que existan formas más sofisticadas de medir el éxito de las
recomendaciones que simplemente evaluar en función de la regresión. Estas métricas son particularmente útiles para evaluar
su modelo final.

Métricas de regresión
Podemos reciclar las métricas de regresión para recomendación. Esto se debe a que simplemente podemos ver qué tan
cerca está cada predicción de la calificación real para ese usuario y elemento:

// en Scala
import org.apache.spark.mllib.e Evaluation.
{ RankingMetrics,
RegressionMetrics}
val regComparison = predicciones.select("puntuación", "predicción")
.rdd.map(x => (x.getFloat(0).toDouble,x.getFloat(1).toDouble))
val metrics = new RegressionMetrics(regComparison)

# en Python
desde pyspark.mllib.e Evaluation import RegressionMetrics
regComparison = predicciones.select("calificación", "predicción")\
.rdd.map(lambda x: (x(0), x(1)))
métricas = RegressionMetrics(regComparison)

Métricas de clasificación
Más interesante aún, también tenemos otra herramienta: métricas de clasificación. Un RankingMetric nos permite comparar
nuestras recomendaciones con un conjunto real de calificaciones (o preferencias) expresadas por un usuario determinado.
RankingMetric no se centra en el valor de la clasificación, sino en si nuestro algoritmo recomienda o no un elemento ya
clasificado nuevamente a un usuario. Esto requiere cierta preparación de datos de nuestra parte. Puede consultar la
Parte II para repasar algunos de los métodos.
Primero, debemos recopilar un conjunto de películas altamente clasificadas para un usuario determinado. En nuestro caso,
vamos a utilizar un umbral bastante bajo: películas clasificadas por encima de 2,5. Ajustar este valor será en gran
medida una decisión comercial:

// en escala
Machine Translated by Google

import org.apache.spark.mllib.e Evaluation.{RankingMetrics, RegressionMetrics} import


org.apache.spark.sql.functions.{col, expr} val perUserActual
= predicciones .where("rating >
2.5") .groupBy("userId

") .agg(expr("collect_set(movieId) como películas"))

# en Python
de pyspark.mllib.e Evaluation import RankingMetrics, RegressionMetrics de
pyspark.sql.functions import col, expr perUserActual
= predicciones\ .where("rating >
2.5")\ .groupBy("userId")

\ .agg(expr( "collect_set(movieId) como películas"))

En este punto, tenemos una colección de usuarios, junto con un conjunto real de películas clasificadas previamente
para cada usuario. Ahora obtendremos nuestras 10 recomendaciones principales de nuestro algoritmo por usuario.
Luego veremos si las 10 recomendaciones principales aparecen en nuestro conjunto de verdad. Si tenemos un
modelo bien entrenado, recomendará correctamente las películas que ya le gustaron a un usuario. Si no es así, es
posible que no haya aprendido lo suficiente sobre cada usuario en particular para reflejar con éxito sus preferencias:

// en Scala
val perUserPredictions =
predicciones .orderBy(col("userId"),

col("prediction").desc) .groupBy("userId") .agg(expr("collect_list(movieId) as movies"))

# en Python
perUserPredictions = predicciones\
.orderBy(col("userId"), expr("prediction DESC"))
\ .groupBy("userId")
\ .agg(expr("collect_list(movieId) como películas"))

Ahora tenemos dos DataFrames, uno de predicciones y otro de los elementos mejor clasificados para un
usuario en particular. Podemos pasarlos al objeto RankingMetrics. Este objeto acepta un RDD de estas combinaciones,
como puedes ver en la siguiente conversión de combinación y RDD:

// en Scala
val perUserActualvPred = perUserActual.join(perUserPredictions, Seq("userId"))
.map(fila =>
( fila(1).asInstanceOf[Seq[Integer]].toArray,
fila(2).asInstanceOf[Seq[Integer]].toArray.take(15) ))

val ranks = new RankingMetrics(perUserActualvPred.rdd)

# en Python
perUserActualvPred = perUserActual.join(perUserPredictions, ["userId"]).rdd\
.map(lambda fila: (fila[1], fila[2][:15])) rangos =
RankingMetrics(perUserActualvPred)
Machine Translated by Google

Ahora podemos ver las métricas de ese ranking. Por ejemplo, podemos ver qué tan preciso es nuestro
algoritmo con la precisión promedio media. También podemos obtener la precisión en ciertos puntos de clasificación,
por ejemplo, para ver dónde caen la mayoría de las recomendaciones positivas:

// en Scala
ranks.meanAveragePrecision
ranks.precisionAt(5)

# en Python
ranks.meanAveragePrecision
ranks.precisionAt(5)

Minería de patrones frecuentes


Además de ALS, otra herramienta que proporciona MLlib para crear recomendaciones es la extracción frecuente de
patrones. La minería de patrones frecuente, a veces denominada análisis de cesta de la compra, analiza datos sin
procesar y encuentra reglas de asociación. Por ejemplo, dada una gran cantidad de transacciones, podría
identificar que los usuarios que compran perros calientes casi siempre compran pan para perros calientes. Esta técnica
se puede aplicar en el contexto de la recomendación, especialmente cuando las personas están llenando carritos de
compras (ya sea en línea o fuera de línea). Spark implementa el algoritmo de crecimiento de FP para la minería de patrones frecuente.
Consulte la documentación de Spark y ESL 14.2 para obtener más información sobre este algoritmo.

Conclusión
En este capítulo, analizamos uno de los algoritmos de aprendizaje automático más populares de Spark en la
práctica: la alternancia de mínimos cuadrados para la recomendación. Vimos cómo podemos entrenar, ajustar y
evaluar este modelo. En el próximo capítulo, pasaremos al aprendizaje no supervisado y discutiremos el
agrupamiento.
Machine Translated by Google

Capítulo 29. Aprendizaje no supervisado

Este capítulo cubrirá los detalles de las herramientas disponibles de Spark para el aprendizaje no supervisado, centrándose
específicamente en la agrupación. El aprendizaje no supervisado, en términos generales, se usa con menos frecuencia
que el aprendizaje supervisado porque suele ser más difícil de aplicar y medir el éxito (desde la perspectiva del resultado
final). Estos desafíos pueden exacerbarse a escala. Por ejemplo, la agrupación en un espacio de alta dimensión puede
crear grupos extraños simplemente debido a las propiedades de los espacios de alta dimensión, algo que se conoce como
la maldición de la dimensionalidad. La maldición de la dimensionalidad describe el hecho de que a medida que un
espacio de características se expande en dimensionalidad, se vuelve cada vez más escaso. Esto significa que los
datos necesarios para llenar este espacio para obtener resultados estadísticamente significativos aumentan
rápidamente con cualquier aumento en la dimensionalidad. Además, con dimensiones altas viene más ruido en los datos.
Esto, a su vez, puede hacer que su modelo se concentre en el ruido en lugar de los verdaderos factores que causan un
resultado o agrupación en particular. Por lo tanto, en la tabla de escalabilidad del modelo, incluimos límites computacionales,
así como un conjunto de recomendaciones estadísticas. Estas son heurísticas y deberían ser guías útiles, no requisitos.

En esencia, el aprendizaje no supervisado está tratando de descubrir patrones o derivar una representación concisa de la
estructura subyacente de un conjunto de datos determinado.

Casos de uso
Estos son algunos casos de uso potenciales. En esencia, estos patrones pueden revelar temas, anomalías o agrupaciones
en nuestros datos que pueden no haber sido obvios de antemano:

Encontrar anomalías en los datos

Si la mayoría de los valores en un conjunto de datos se agrupan en un grupo más grande con varios grupos pequeños
en el exterior, esos grupos podrían justificar una mayor investigación.

Modelado de temas

Al observar grandes cuerpos de texto, es posible encontrar temas que existen en esos diferentes documentos.

Escalabilidad del modelo


Al igual que con nuestros otros modelos, es importante mencionar los requisitos básicos de escalabilidad del
modelo junto con las recomendaciones estadísticas.

Tabla 29­1. Referencia de escalabilidad del modelo de agrupación en clústeres

Modelo Límites de cálculo Ejemplos de entrenamiento


Recomendación estadística
Machine Translated by Google

k­significa 50 a 100 máximo Características x clústeres < 10 Sin límite


millón

k bisectriz Características x clústeres <


50 a 100 máximo Sin límite
medio 10 millones

Características x clústeres < 10


GMM 50 a 100 máximo Sin límite
millón

LDA Un número interpretable 1,000s de temas Sin límite

Comencemos cargando algunos datos numéricos de ejemplo:

// en Scala
importar org.apache.spark.ml.feature.VectorAssembler

val va = nuevo VectorAssembler()


.setInputCols(Array("Cantidad",
"PrecioUnitario")) .setOutputCol("características")

val sales = va.transform(spark.read.format("csv")


.option("header",
"true") .option("inferSchema",
"true") .load("/data/retail­data/by­day/

*.csv") .limit(50) .coalesce( 1) .where("La descripción NO ES NULA"))

ventas.cache()

# en Python
desde pyspark.ml.feature import VectorAssembler va =
VectorAssembler()
\ .setInputCols(["Cantidad", "PrecioUnitario"])
\ .setOutputCol("características")

ventas = va.transform(spark.read.format("csv")
.option("header",
"true") .option("inferSchema",
"true") .load("/data/retail­data/by­day/

*.csv") .limit(50) .coalesce( 1) .where("La descripción NO ES NULA"))

ventas.cache()

k­significa
­means es uno de los algoritmos de agrupamiento más populares. En este algoritmo, un número especificado
por el usuario de grupos () se asigna aleatoriamente a diferentes puntos en el conjunto de datos. los no asignados
Machine Translated by Google

Luego, los puntos se "asignan" a un grupo en función de su proximidad (medida en distancia euclidiana) al punto
previamente asignado. Una vez que ocurre esta asignación, se calcula el centro de este grupo (llamado centroide ) y el
proceso se repite. Todos los puntos se asignan a un centroide particular y se calcula un nuevo centroide.
Repetimos este proceso para un número finito de iteraciones o hasta la convergencia (es decir, cuando las ubicaciones
de nuestros centroides dejan de cambiar). Sin embargo, esto no significa que nuestros clústeres sean siempre
sensibles. Por ejemplo, un grupo de datos "lógico" dado podría dividirse por la mitad simplemente debido a los puntos
de partida de dos grupos distintos. Por lo tanto, a menudo es una buena idea realizar múltiples ejecuciones de ­means
comenzando con diferentes inicializaciones.

Elegir el valor correcto para es un aspecto extremadamente importante del uso exitoso de este algoritmo,
además de una tarea difícil. No hay una prescripción real para la cantidad de clústeres que necesita, por lo que
probablemente tendrá que experimentar con diferentes valores y considerar cuál le gustaría que fuera el resultado final.

Para obtener más información sobre los medios, consulte ISL 10.3 y ESL 14.3.

Hiperparámetros del modelo


Estas son configuraciones que especificamos para determinar la estructura básica del modelo:

Este es el número de clústeres con los que le gustaría terminar.

Parámetros de entrenamiento

initMode

El modo de inicialización es el algoritmo que determina las ubicaciones iniciales de los centroides. Las
opciones admitidas son aleatorias y ­means|| (el valor por defecto). Este último es una variante paralelizada de
­means|| método. Si bien los detalles no están dentro del alcance de este libro, el pensamiento detrás de este
último método es que, en lugar de simplemente elegir ubicaciones de inicialización aleatorias, el algoritmo elige
centros de grupos que ya están bien dispersos para generar un mejor agrupamiento.

pasos de inicio

El número de pasos para ­means|| modo de inicialización. Debe ser mayor que 0. (El valor predeterminado es 2).

maxIter

Número total de iteraciones sobre los datos antes de detenerse. Cambiar esto probablemente no cambie
mucho sus resultados, así que no haga de este el primer parámetro que busque ajustar. El valor
predeterminado es 20.

tol

Especifica un umbral por el cual los cambios en los centroides muestran que optimizamos nuestro modelo
Machine Translated by Google

suficiente, y puede dejar de iterar temprano, antes de las iteraciones de maxIter. El valor predeterminado es 0,0001.

Este algoritmo generalmente es robusto a estos parámetros, y la principal compensación es que ejecutar más pasos
de inicialización e iteraciones puede conducir a una mejor agrupación a expensas de un tiempo de entrenamiento más
prolongado:

Ejemplo
// en Scala
import org.apache.spark.ml.clustering.KMeans val
km = new KMeans().setK(5)
println(km.explainParams()) val
kmModel = km.fit(sales)

# en Python
desde pyspark.ml.clustering import KMeans km
= KMeans().setK(5) print
km.explainParams() kmModel
= km.fit(sales)

Resumen de métricas k­means


­means incluye una clase de resumen que podemos usar para evaluar nuestro modelo. Esta clase proporciona
algunas medidas comunes para el éxito de ­means (si se aplican a su conjunto de problemas es otra cuestión). El resumen
­means incluye información sobre los clústeres creados, así como sus tamaños relativos (número de ejemplos).

También podemos calcular la suma dentro del conjunto de errores cuadráticos, lo que puede ayudar a medir qué tan
cerca están nuestros valores de cada centroide de clúster, usando computeCost. El objetivo implícito en ­means es
que queremos minimizar la suma dentro del conjunto del error cuadrático, sujeto a la cantidad dada de grupos:

// en Scala
val resumen = kmModel.summary
summary.clusterSizes // número de puntos
kmModel.computeCost(sales)
println("Cluster Centers: ")
kmModel.clusterCenters.foreach(println)

# en Python
resumen = kmModel.summary
print summary.clusterSizes # número de puntos
kmModel.computeCost(sales)
centres = kmModel.clusterCenters()
print("Cluster Centers: ") for
center in centers:
print(center)
Machine Translated by Google

bisectriz k­medias
La bisección ­means es una variante de ­means. La principal diferencia es que, en lugar de agrupar puntos comenzando "de
abajo hacia arriba" y asignando un montón de grupos diferentes en los datos, este es un método de agrupamiento de arriba
hacia abajo. Esto significa que comenzará creando un solo grupo y luego dividirá ese grupo en grupos más pequeños para
terminar con la cantidad de clústeres especificados por el usuario.
Este suele ser un método más rápido que los medios y producirá resultados diferentes.

Hiperparámetros del modelo


Estas son configuraciones que especificamos para determinar la estructura básica del modelo:

Este es el número de clústeres con los que le gustaría terminar.

Parámetros de entrenamiento

minDivisibleClusterSize

El número mínimo de puntos (si es mayor o igual a 1.0) o la proporción mínima de puntos (si es menor a 1.0) de un
grupo divisible. El valor predeterminado es 1.0, lo que significa que debe haber al menos un punto en cada clúster.

maxIter

Número total de iteraciones sobre los datos antes de detenerse. Cambiar esto probablemente no cambie mucho
sus resultados, así que no haga de este el primer parámetro que busque ajustar. El valor predeterminado es 20.

La mayoría de los parámetros de este modelo deben ajustarse para encontrar el mejor resultado. No hay una regla que se
aplique a todos los conjuntos de datos.

Ejemplo

// en Scala
import org.apache.spark.ml.clustering.BisectingKMeans val bkm
= new BisectingKMeans().setK(5).setMaxIter(5)
println(bkm.explainParams()) val
bkmModel = bkm.fit(sales)

# en Python
desde pyspark.ml.clustering import BisectingKMeans bkm
= BisectingKMeans().setK(5).setMaxIter(5) bkmModel
= bkm.fit(sales)

Bisección k­medias Resumen


Machine Translated by Google

Bisecting ­means incluye una clase de resumen que podemos usar para evaluar nuestro modelo, que es en gran medida lo
mismo que el resumen de ­means. Esto incluye información sobre los clústeres creados, así como sus tamaños relativos (número
de ejemplos):

// en Scala
val resumen = bkmModel.summary
summary.clusterSizes // número de puntos
kmModel.computeCost(sales)
println("Cluster Centers: ")
kmModel.clusterCenters.foreach(println)

# en Python
resumen = bkmModel.summary
print summary.clusterSizes # número de puntos
kmModel.computeCost(sales)
centres = kmModel.clusterCenters()
print("Cluster Centers: ") for
center in centers:
print(center)

Modelos de mezcla gaussiana

Los modelos de mezcla gaussiana (GMM) son otro algoritmo de agrupamiento popular que hace suposiciones diferentes a las que
hace la bisección de medias o medias. Esos algoritmos intentan agrupar datos reduciendo la suma de las distancias al
cuadrado desde el centro del grupo. Los modelos de mezcla gaussiana, por otro lado, asumen que cada grupo produce datos
basados en sorteos aleatorios de una distribución gaussiana. Esto significa que es menos probable que los grupos de datos
tengan datos en el borde del grupo (reflejado en la distribución guassiana) y una probabilidad mucho mayor de tener datos en el
centro. Cada grupo gaussiano puede tener un tamaño arbitrario con su propia media y desviación estándar (y, por lo tanto, una
forma de elipsoide posiblemente diferente). Todavía hay clústeres especificados por el usuario que se crearán durante el
entrenamiento.

Una forma simplificada de pensar en los modelos de mezcla gaussiana es que son como una versión blanda de ­means. ­significa
que crea grupos muy rígidos: cada punto está solo dentro de un grupo. Los GMM permiten un grupo más matizado asociado
con probabilidades, en lugar de límites rígidos.

Para obtener más información, consulte ESL 14.3.

Hiperparámetros del modelo


Estas son configuraciones que especificamos para determinar la estructura básica del modelo:

Este es el número de clústeres con los que le gustaría terminar.

Parámetros de entrenamiento
Machine Translated by Google

maxIter

Número total de iteraciones sobre los datos antes de detenerse. Cambiar esto probablemente no cambie
mucho sus resultados, así que no haga de este el primer parámetro que busque ajustar. El valor
predeterminado es 100.

tol

Este valor simplemente nos ayuda a especificar un umbral por el cual los cambios en los parámetros muestran
que optimizamos nuestros pesos lo suficiente. Un valor más pequeño puede conducir a una mayor
precisión a costa de realizar más iteraciones (aunque nunca más que maxIter). El valor predeterminado es 0,01.

Al igual que con nuestro modelo de medias, es menos probable que estos parámetros de entrenamiento tengan un impacto que la
cantidad de conglomerados, .

Ejemplo

// en Scala
import org.apache.spark.ml.clustering.GaussianMixture val gmm
= new GaussianMixture().setK(5)
println(gmm.explainParams()) val
model = gmm.fit(sales)

# en Python
desde pyspark.ml.clustering import GaussianMixture gmm
= GaussianMixture().setK(5) print
gmm.explainParams() model
= gmm.fit(sales)

Resumen del modelo de mezcla gaussiana


Al igual que nuestros otros algoritmos de agrupamiento, los modelos de mezcla gaussiana incluyen una clase de
resumen para ayudar con la evaluación del modelo. Esto incluye información sobre los grupos creados, como los
pesos, las medias y la covarianza de la mezcla gaussiana, lo que puede ayudarnos a aprender más sobre la
estructura subyacente dentro de nuestros datos:

// en Scala
val resumen = modelo.resumen
modelo.pesos
modelo.gaussiansDF.show()
resumen.cluster.show()
resumen.clusterSizes
resumen.probabilidad.show()

# en Python
resumen = modelo.resumen
print modelo.pesos
modelo.gaussiansDF.show()
resumen.cluster.show()
Machine Translated by Google

resumen.clusterSizes
resumen.probabilidad.show()

Asignación latente de Dirichlet


La asignación de Dirichlet latente (LDA) es un modelo de agrupamiento jerárquico que generalmente se usa para
realizar el modelado de temas en documentos de texto. LDA intenta extraer temas de alto nivel de una serie
de documentos y palabras clave asociadas con esos temas. Luego interpreta que cada documento tiene un número
variable de contribuciones de múltiples temas de entrada. Hay dos implementaciones que puede usar: LDA en línea
y maximización de expectativas. En general, la LDA en línea funcionará mejor cuando haya más ejemplos, y el
optimizador de maximización de expectativas funcionará mejor cuando haya un vocabulario de entrada más
amplio. Este método también es capaz de escalar a cientos o miles de temas.

Para ingresar nuestros datos de texto en LDA, tendremos que convertirlos a un formato numérico. Puede usar
CountVectorizer para lograr esto.

Hiperparámetros del modelo


Estas son configuraciones que especificamos para determinar la estructura básica del modelo:

El número total de temas para inferir de los datos. El valor predeterminado es 10 y debe ser un número positivo.

docConcentración

Parámetro de concentración (comúnmente llamado “alfa”) para el anterior colocado en las distribuciones de
documentos sobre temas (“theta”). Este es el parámetro de una distribución de Dirichlet, donde los valores
más grandes significan más suavizado (más regularización).

Si el usuario no lo establece, docConcentration se establece automáticamente. Si se establece en vector singleton


[alfa], alfa se replica en un vector de longitud k en el ajuste. De lo contrario, el vector
docConcentration debe ser length .

temaConcentración

El parámetro de concentración (comúnmente llamado "beta" o "eta") para el anterior colocado en las
distribuciones de un tema sobre los términos. Este es el parámetro de una distribución de Dirichlet simétrica. Si no
lo establece el usuario, topicConcentration se establece automáticamente.

Parámetros de entrenamiento
Estas son configuraciones que especifican cómo realizamos el entrenamiento:

maxIter

Número total de iteraciones sobre los datos antes de detenerse. Cambiar esto probablemente no lo hará
Machine Translated by Google

cambie mucho sus resultados, así que no haga de este el primer parámetro que busque ajustar. El valor predeterminado
es 20.

optimizador

Esto determina si usar EM o optimización de entrenamiento en línea para determinar el modelo LDA. El valor
predeterminado es en línea.

learningDecay Tasa

de aprendizaje, establecida como una tasa de caída exponencial. Debe estar entre (0,5, 1,0] para garantizar la

convergencia asintótica. El valor predeterminado es 0,51 y solo se aplica al optimizador en línea.

aprendizajeOffset

Un parámetro de aprendizaje (positivo) que reduce el peso de las primeras iteraciones. Los valores más grandes hacen que

las primeras iteraciones cuenten menos. El valor predeterminado es 1024,0 y solo se aplica al optimizador en línea.

optimizarDocConcentración

Indica si la docConcentration (parámetro de Dirichlet para la distribución documento­tema) se optimizará durante

el entrenamiento. El valor predeterminado es verdadero, pero solo se aplica al optimizador en línea.

tasa de submuestreo

La fracción del corpus que se muestreará y utilizará en cada iteración de descenso de gradiente de minilotes, en el rango

(0, 1). El valor predeterminado es 0,5 y solo se aplica al optimizador en línea.

semilla

Este modelo también admite la especificación de una semilla aleatoria para la reproducibilidad.

Intervalo de punto de control

Esta es la misma función de punto de control que vimos en el Capítulo 26.

Parámetros de predicción

temaDistribuciónCol

La columna que contendrá el resultado de la distribución de mezcla de temas para cada documento.

Ejemplo

// en Scala
import org.apache.spark.ml.feature.{Tokenizer, CountVectorizer} val tkn =
new Tokenizer().setInputCol("Description").setOutputCol("DescOut") val tokenized =
tkn.transform(sales. drop("características")) val cv = new
CountVectorizer()
Machine Translated by Google

.setInputCol("DescOut") .setOutputCol("características") .setVocabSize(500) .setMinTF(0) .setMinDF(0) .setBinary(true)


val cvFitted = cv.fit(tokenizado) val
prepped = cvFitted.transform(tokenizado)

# en Python
desde pyspark.ml.feature import Tokenizer, CountVectorizer tkn =
Tokenizer().setInputCol("Description").setOutputCol("DescOut") tokenized =
tkn.transform(sales.drop("features")) cv = CountVectorizer
()\
.setInputCol("DescOut")
\ .setOutputCol("features")

\ .setVocabSize(500)\ .setMinTF(0)\ .setMinDF(0)\ .setBinary(True) cvFitted = cv.fit(tokenized) preparado = cvFitted .transform(

// en Scala
import org.apache.spark.ml.clustering.LDA val lda
= new LDA().setK(10).setMaxIter(5)
println(lda.explainParams()) val
model = lda.fit(prepped)

# en Python
desde pyspark.ml.clustering import LDA lda
= LDA().setK(10).setMaxIter(5) print
lda.explainParams() model =
lda.fit(prepped)

Después de entrenar al modelo, verá algunos de los temas principales. Esto devolverá los índices de
términos, y tendremos que buscarlos usando el CountVectorizerModel que entrenamos para encontrar
las palabras verdaderas. Por ejemplo, cuando entrenamos con los datos, nuestros 3 temas principales
fueron hot, home y brown después de buscarlos en nuestro vocabulario:

// en Scala
model.describeTopics(3).show()
cvFitted.vocabulary

# en Python
model.describeTopics(3).show()
cvFitted.vocabulary

Estos métodos dan como resultado información detallada sobre el vocabulario utilizado, así como el énfasis
en términos particulares. Estos pueden ser útiles para comprender mejor los temas subyacentes. Debido a
Machine Translated by Google

limitaciones de espacio, no podemos mostrar esta salida. Usando API similares, podemos obtener
algunas medidas más técnicas como la probabilidad de registro y la perplejidad. El objetivo de estas
herramientas es ayudarlo a optimizar la cantidad de temas, según sus datos. Cuando utilice la
perplejidad en sus criterios de éxito, debe aplicar estas métricas a un conjunto reservado para reducir la
perplejidad general del modelo. Otra opción es optimizar para aumentar el valor de probabilidad de registro
en el conjunto reservado. Podemos calcular cada uno de estos pasando un conjunto
de datos a las siguientes funciones: model.logLikelihood y model.logPerplexity.

Conclusión
Este capítulo cubrió los algoritmos más populares que Spark incluye para el aprendizaje no supervisado.
El próximo capítulo nos sacará de MLlib y hablará sobre algunos de los ecosistemas de análisis
avanzados que han crecido fuera de Spark.
Machine Translated by Google

Capítulo 30. Análisis gráfico

El capítulo anterior cubrió algunas técnicas convencionales no supervisadas. Este capítulo se sumergirá en un
conjunto de herramientas más especializado: el procesamiento de gráficos. Los gráficos son estructuras de datos
compuestas de nodos o vértices, que son objetos arbitrarios, y bordes que definen las relaciones entre estos
nodos. El análisis gráfico es el proceso de analizar estas relaciones. Un gráfico de ejemplo podría ser tu grupo
de amigos. En el contexto del análisis gráfico, cada vértice o nodo representaría a una persona y cada borde
representaría una relación. La figura 30­1 muestra un gráfico de muestra.

Figura 30­1. Un gráfico de muestra con siete nodos y siete aristas

Este gráfico en particular no está dirigido, en el sentido de que los bordes no tienen un vértice de "inicio" y
"final" específico. También hay gráficos dirigidos que especifican un inicio y un final. La figura 30­2 muestra un
gráfico dirigido donde los bordes son direccionales.
Machine Translated by Google

Figura 30­2. Un gráfico dirigido

Las aristas y los vértices de los gráficos también pueden tener datos asociados. En nuestro ejemplo de amigos, el peso
del borde podría representar la intimidad entre diferentes amigos; los conocidos tendrían aristas de poco peso entre ellos,
mientras que los casados tendrían aristas de gran peso. Podríamos establecer este valor observando la frecuencia de
comunicación entre los nodos y ponderando los bordes en consecuencia. Cada vértice (persona) también puede tener
datos como un nombre.

Los gráficos son una forma natural de describir relaciones y muchos conjuntos de problemas diferentes, y Spark ofrece
varias formas de trabajar en este paradigma analítico. Algunos casos de uso empresarial podrían ser la detección de
fraudes con tarjetas de crédito, la búsqueda de motivos, la determinación de la importancia de los documentos en las
redes bibliográficas (es decir, qué documentos son los más referenciados) y la clasificación de páginas web, como
Google utilizó el famoso algoritmo PageRank para hacer.

Spark ha contenido durante mucho tiempo una biblioteca basada en RDD para realizar el procesamiento de gráficos:
GraphX. Esto proporcionó una interfaz de muy bajo nivel que era extremadamente poderosa, pero al igual que los RDD, no
era fácil de usar ni de optimizar. GraphX sigue siendo una parte central de Spark. Las empresas continúan
construyendo aplicaciones de producción sobre él, y todavía ve un desarrollo de características menores. La API
de GraphX está bien documentada simplemente porque no ha cambiado mucho desde su creación.
Sin embargo, algunos de los desarrolladores de Spark (incluidos algunos de los autores originales de GraphX)
Machine Translated by Google

han creado recientemente una biblioteca de análisis de gráficos de próxima generación en Spark: GraphFrames.
GraphFrames amplía GraphX para proporcionar una API DataFrame y soporte para los diferentes enlaces de lenguaje
de Spark para que los usuarios de Python puedan aprovechar la escalabilidad de la herramienta. En este libro, nos
centraremos en GraphFrames.

GraphFrames está disponible actualmente como un paquete Spark, un paquete externo que debe cargar cuando inicia
su aplicación Spark, pero que puede fusionarse con el núcleo de Spark en el futuro. En su mayor parte, debería haber
poca diferencia en el rendimiento entre los dos (excepto por una gran mejora en la experiencia del usuario en
GraphFrames). Hay una pequeña sobrecarga cuando se usa GraphFrames, pero en su mayor parte intenta llamar a GraphX
cuando corresponde; y para la mayoría, las ganancias en la experiencia del usuario superan en gran medida
esta pequeña sobrecarga.

¿CÓMO SE COMPARA GRAPHFRAMES CON LAS BASES DE DATOS DE GRAPH?

Spark no es una base de datos. Spark es un motor de computación distribuido, pero no almacena datos a largo plazo
ni realiza transacciones. Puede crear un cálculo gráfico sobre Spark, pero eso es fundamentalmente diferente de
una base de datos. GraphFrames puede escalar a cargas de trabajo mucho más grandes que muchas bases
de datos de gráficos y funciona bien para el análisis, pero no es compatible con el procesamiento y el servicio
transaccionales.

El objetivo de este capítulo es mostrarle cómo usar GraphFrames para realizar análisis de gráficos en Spark. Vamos a
hacer esto con datos de bicicletas disponibles públicamente del portal Bay Area Bike Share.

CONSEJO

Durante el transcurso de la escritura de este libro, este mapa y los datos han cambiado drásticamente (¡incluso
el nombre!). Incluimos una copia del conjunto de datos dentro de la carpeta de datos del repositorio de este libro.
Asegúrese de usar ese conjunto de datos para replicar los siguientes resultados; y cuando se sienta aventurero, amplíe
a todo el conjunto de datos.

Para configurarlo, deberá señalar el paquete adecuado. Para hacer esto desde la línea de comando, ejecutará:

./bin/spark­shell ­­packages marcos gráficos: marcos gráficos: 0.5.0­spark2.2­s_2.11

// en Scala
val bikeStations = spark.read.option("header","true") .csv("/data/
bike­data/201508_station_data.csv") val tripData =
spark.read.option("header", "verdadero") .csv("/data/bike­
data/201508_trip_data.csv")

# en Python
bikeStations = spark.read.option("header","true")\ .csv("/data/
bike­data/201508_station_data.csv")
Machine Translated by Google

tripData = spark.read.option("header","true")\ .csv("/data/


bike­data/201508_trip_data.csv")

Construcción de un gráfico

El primer paso es construir el gráfico. Para hacer esto, necesitamos definir los vértices y los bordes, que son marcos de datos
con algunas columnas con nombres específicos. En nuestro caso, estamos creando un gráfico dirigido.
Este gráfico apuntará desde la fuente hasta la ubicación. En el contexto de los datos de este viaje en bicicleta, esto apuntará
desde la ubicación inicial de un viaje hasta la ubicación final de un viaje. Para definir el gráfico, usamos las convenciones de
nomenclatura para las columnas presentadas en la biblioteca GraphFrames. En la tabla de vértices, definimos nuestro
identificador como id (en nuestro caso, es de tipo cadena), y en la tabla de aristas, etiquetamos la ID de vértice de origen de
cada arista como src y la ID de destino como dst:

// en Scala
val stationVertices = bikeStations.withColumnRenamed("name", "id").distinct() val tripEdges =
tripData .withColumnRenamed("Start
Station", "src") .withColumnRenamed("End Station",
"dst" )

# en Python
stationVertices = bikeStations.withColumnRenamed("name", "id").distinct() tripEdges =
tripData\
.withColumnRenamed(" Estación de inicio", "origen")
\ .withColumnRenamed(" Estación final", "dst")

Ahora podemos construir un objeto GraphFrame, que representa nuestro gráfico, a partir de los DataFrames de vértice y
borde que tenemos hasta ahora. También aprovecharemos el almacenamiento en caché porque accederemos a estos datos
con frecuencia en consultas posteriores:

// en Scala
import org.graphframes.GraphFrame val
stationGraph = GraphFrame(stationVertices, tripEdges) stationGraph.cache()

# en Python
desde los marcos gráficos import
GraphFrame stationGraph = GraphFrame(stationVertices, tripEdges)
stationGraph.cache()

Ahora podemos ver las estadísticas básicas sobre el gráfico (y consultar nuestro DataFrame original para asegurarnos de ver los
resultados esperados):

// en Scala
println(s"Número total de estaciones: ${stationGraph.vertices.count()}") println(s" Número
total de viajes en el gráfico: ${stationGraph.edges.count()}") println( s"Número total de viajes
en datos originales: ${tripData.count()}")
Machine Translated by Google

# en Python
"
print "Número total de estaciones: print + str(gráficoEstación.vértices.cuenta())
"Número total de viajes en el gráfico: " + str(stationGraph.edges.count()) print "Número
"
total de viajes en los datos originales: + str(tripData.count())

Esto devuelve los siguientes resultados:

Número total de estaciones: 70


Número total de viajes en el gráfico: 354152
Número total de viajes en datos originales: 354152

Consultando el gráfico
La forma más básica de interactuar con el gráfico es simplemente consultarlo, realizar cosas como contar viajes
y filtrar por destinos determinados. GraphFrames proporciona un acceso simple tanto a los vértices como a los
bordes como DataFrames. Tenga en cuenta que nuestro gráfico retuvo todas las columnas adicionales en los
datos además de los ID, las fuentes y los destinos, por lo que también podemos consultarlos si es necesario:

// en Scala
import org.apache.spark.sql.functions.desc
stationGraph.edges.groupBy("src", "dst").count().orderBy(desc("count")).show(10)

# en Python
desde pyspark.sql.functions import desc
stationGraph.edges.groupBy("src", "dst").count().orderBy(desc("count")).show(10)

+­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­+­­­­­+
| origen| horario de verano|recuento|

+­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­+­­­­­+
|San Francisco California...| Townsend en la séptima | 3748|
|Harry Bridges Pla...|Embarcadero en Sa...| 3145|
...
| Townsend en 7th|San Francisco Cal...| 2192| |Transporte
Temporal...|San Francisco Cal...| 2184|
+­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­+­­­­­+

También podemos filtrar por cualquier expresión válida de DataFrame. En este caso, quiero ver una estación
específica y el conteo de viajes dentro y fuera de esa estación:

// en Scala

stationGraph.edges .where("src = 'Townsend at 7th' OR dst = 'Townsend at


7th'") .groupBy("src",

"dst").count() .orderBy(desc("count ")) .mostrar(10)

# en Python
stationGraph.edges\
.where("src = 'Townsend en 7th' O dst = 'Townsend en 7th'")\
Machine Translated by Google

.groupBy("src", "dst").count()
\ .orderBy(desc("count"))
\ .show(10)

+­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­+­­­­­+
| origen| horario de verano|recuento|

+­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­+­­­­­+
|San Francisco California...| Townsend en la séptima | 3748|
| Townsend en 7th|San Francisco Cal...| 2734|
...
| Steuart en el mercado| Townsend en la séptima | 746| |
Townsend en 7th|Transporte Temporal...| 740|
+­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­+­­­­­+

Subgrafos
Los subgrafos son simplemente gráficos más pequeños dentro de uno más grande. Vimos en la última sección cómo
podemos consultar un conjunto dado de aristas y vértices. Podemos usar esta capacidad de consulta para crear subgrafos:

// en Scala
val townAnd7thEdges = stationGraph.edges
.where("src = 'Townsend at 7th' OR dst = 'Townsend at 7th'") val subgraph
= GraphFrame(stationGraph.vertices, townAnd7thEdges)

# en Python
townAnd7thEdges =
stationGraph.edges\ .where("src = 'Townsend at 7th' OR dst = 'Townsend
at 7th'") subgraph = GraphFrame(stationGraph.vertices, townAnd7thEdges)

Luego podemos aplicar los siguientes algoritmos al gráfico original o al subgráfico.

Búsqueda de motivos

Los motivos son una forma de expresar patrones estructurales en un gráfico. Cuando especificamos un motivo, estamos
consultando patrones en los datos en lugar de datos reales. En GraphFrames, especificamos nuestra consulta en un lenguaje
específico de dominio similar al lenguaje Cypher de Neo4J. Este lenguaje nos permite especificar combinaciones de
vértices y aristas y asignarles nombres. Por ejemplo, si queremos especificar que un vértice a dado se conecta a otro
vértice b a través de una arista ab, especificaríamos (a)­[ab]­>(b). Los nombres entre paréntesis o corchetes no significan
valores, sino el nombre de las columnas para los vértices y bordes coincidentes en el DataFrame resultante. Podemos omitir
los nombres (p. ej., (a)­[]­>()) si no pretendemos consultar los valores resultantes.

Realicemos una consulta sobre los datos de nuestra bicicleta. En lenguaje sencillo, encontremos todos los juegos
que forman un patrón de "triángulo" entre tres estaciones. Expresamos esto con el siguiente motivo, utilizando el método
de búsqueda para consultar nuestro GraphFrame para ese patrón. (a) significa la estación inicial, y [ab] representa un
borde desde (a) hasta nuestra siguiente estación (b). Repetimos esto para las estaciones (b) a (c) y luego de (c) a (a):
Machine Translated by Google

// en Scala
val motivos = stationGraph.find("(a)­[ab]­>(b); (b)­[bc]­>(c); (c)­[ca]­>(a) ")

# en motivos
Python = stationGraph.find("(a)­[ab]­>(b); (b)­[bc]­>(c); (c)­[ca]­>(a)")

La Figura 30­3 presenta una representación visual de esta consulta.

Figura 30­3. Motivo de triángulo en nuestra consulta de triángulo

El DataFrame que obtenemos al ejecutar esta consulta contiene campos anidados para los vértices a, b y c, así
como los bordes respectivos. Ahora podemos consultar esto como lo haríamos con un DataFrame. Por ejemplo,
dada cierta bicicleta, ¿cuál es el viaje más corto que ha hecho la bicicleta desde la estación a, a la estación b,
a la estación c y de regreso a la estación a? La siguiente lógica analizará nuestras marcas de tiempo, en
marcas de tiempo de Spark y luego haremos comparaciones para asegurarnos de que es la misma bicicleta,
viajando de una estación a otra, y que las horas de inicio de cada viaje son correctas:

// en Scala
import org.apache.spark.sql.functions.expr
motifs.selectExpr("*",
"to_timestamp(ab.`Start Date`, 'MM/dd/yyyy HH:mm') as abStart", "
to_timestamp(bc.`Start Date`, 'MM/dd/aaaa HH:mm') as bcStart",
"to_timestamp(ca.`Start Date`, 'MM/dd/aaaa HH:mm') as caStart") .
where("ca.`Bike #` = bc.`Bike #`").where("ab.`Bike #` = bc.`Bike #`") .where("a.id !=
b.id" ).where("b.id != c.id") .where("abStart <
bcStart").where("bcStart < caStart") .orderBy(expr("cast(caStart
tan largo) ­ cast(abStart como long)")) .selectExpr("a.id", "b.id", "c.id",
"ab.`Start Date`", "ca.`End Date`") .limit(1). mostrar (falso)
Machine Translated by Google

# en Python
desde pyspark.sql.functions import expr
motifs.selectExpr("*",
"to_timestamp(ab.`Start Date`, 'MM/dd/yyyy HH:mm') as abStart",
"to_timestamp(bc.` Fecha de inicio`, 'MM/dd/aaaa HH:mm') como bcStart",
"to_timestamp(ca.`Start Date`, 'MM/dd/aaaa HH:mm') como caStart")
\ .where("ca .`Bike #` = bc.`Bike #`").where("ab.`Bike #` = bc.`Bike #`")\ .where("a.id !=
b.id").where ("b.id != c.id")\ .where("abStart <
bcStart").where("bcStart < caStart")\ .orderBy(expr("cast(caStart
tan largo) ­ cast(abStart tan largo )"))\ .selectExpr("a.id", "b.id", "c.id",
"ab.`Fecha de inicio`", "ca.`Fecha de finalización`") .limit(1). mostrar (1, falso)

Vemos que el viaje más rápido es de aproximadamente 20 minutos. ¡Bastante rápido para tres personas diferentes
(suponemos) usando la misma bicicleta!

Tenga en cuenta también que tuvimos que filtrar los triángulos devueltos por nuestra consulta de motivos en este
ejemplo. En general, los diferentes ID de vértice utilizados en la consulta no se verán obligados a coincidir con vértices
distintos, por lo que debe realizar este tipo de filtrado si desea vértices distintos. Una de las características más poderosas
de GraphFrames es que puede combinar la búsqueda de motivos con las consultas de DataFarme sobre las tablas
resultantes para reducir, ordenar o agregar aún más los patrones encontrados.

Algoritmos gráficos
Un gráfico es solo una representación lógica de datos. La teoría de gráficos proporciona numerosos algoritmos para
analizar datos en este formato, y GraphFrames nos permite aprovechar muchos algoritmos listos para usar. El desarrollo
continúa a medida que se agregan nuevos algoritmos a GraphFrames, por lo que lo más probable es que esta lista siga
creciendo.

Rango de página

Uno de los algoritmos gráficos más prolíficos es PageRank. Larry Page, cofundador de Google, creó PageRank
como un proyecto de investigación sobre cómo clasificar las páginas web. Desafortunadamente, una explicación
completa de cómo funciona PageRank está fuera del alcance de este libro. Sin embargo, para citar a Wikipedia,
la explicación de alto nivel es la siguiente:

PageRank funciona contando el número y la calidad de los enlaces a una página para determinar una estimación
aproximada de la importancia del sitio web. La suposición subyacente es que es probable que los sitios web más
importantes reciban más enlaces de otros sitios web.

PageRank se generaliza bastante bien fuera del dominio web. Podemos aplicar este derecho a nuestros propios datos y
tener una idea de las estaciones de bicicletas importantes (específicamente, aquellas que reciben mucho tráfico de
bicicletas). En este ejemplo, a las estaciones de bicicletas importantes se les asignarán valores altos de PageRank:

// en Scala
import org.apache.spark.sql.functions.desc val
ranks = stationGraph.pageRank.resetProbability(0.15).maxIter(10).run()
ranks.vertices.orderBy(desc("pagerank")). select("id", "pagerank").show(10)
Machine Translated by Google

# en Python
desde pyspark.sql.functions import desc ranks
= stationGraph.pageRank(resetProbability=0.15, maxIter=10)
ranks.vertices.orderBy(desc("pagerank")).select("id", "pagerank"). mostrar(10)

+­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­+
| identificación | rango de página|
+­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­+
|San José Diridon ...| 4.051504835989922| |San
Francisco Cal...|3.3511832964279518|
...
| Townsend en la séptima | 1.568456580534273| |
Embarcadero en Sa...|1.5414242087749768|
+­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­+

APIS DE ALGORITMO GRÁFICO: PARÁMETROS Y VALORES DEVUELTOS

Se accede a la mayoría de los algoritmos en GraphFrames como métodos que toman parámetros (p. ej.,
resetProbability en este ejemplo de PageRank). La mayoría de los algoritmos devuelven un nuevo
GraphFrame o un solo DataFrame. Los resultados del algoritmo se almacenan como una o más columnas en los
vértices y/o bordes de GraphFrame o DataFrame. Para PageRank, el algoritmo devuelve un GraphFrame y
podemos extraer los valores de PageRank estimados para cada vértice de la nueva columna de PageRank.

ADVERTENCIA

Dependiendo de los recursos disponibles en su máquina, esto puede llevar algún tiempo. Siempre puede probar un
conjunto de datos más pequeño antes de ejecutar esto para ver los resultados. En Databricks Community Edition,
tarda unos 20 segundos en ejecutarse, aunque algunos revisores encontraron que tardaba mucho más en sus máquinas.

Curiosamente, vemos que las estaciones de Caltrain tienen una clasificación bastante alta. Esto tiene sentido porque
estos son puntos de conexión naturales donde pueden terminar muchos viajes en bicicleta. Ya sea que los viajeros se
trasladen de su hogar a la estación de Caltrain para su viaje diario o de la estación de Caltrain a su hogar.

Métricas en grado y fuera de grado


Nuestro grafo es un grafo dirigido. Esto se debe a que los viajes en bicicleta son direccionales, comenzando en un
lugar y terminando en otro. Una tarea común es contar el número de viajes hacia o desde una estación determinada.
Para medir los viajes de entrada y salida de las estaciones, utilizaremos una métrica denominada grados de entrada y
de salida, respectivamente, como se ve en la figura 30­4.
Machine Translated by Google

Figura 30­4. Grado de entrada y de salida

Esto es particularmente aplicable en el contexto de las redes sociales porque ciertos usuarios pueden tener
muchas más conexiones entrantes (es decir, seguidores) que conexiones salientes (es decir, personas a las
que siguen). Con la siguiente consulta, puede encontrar personas interesantes en la red social que podrían
tener más influencia que otras. GraphFrames proporciona una forma sencilla de consultar nuestro gráfico para
obtener esta información:

// en Scala val
inDeg = stationGraph.inDegrees
inDeg.orderBy(desc("inDegree")).show(5, false)

# en Python
inDeg = stationGraph.inDegrees
inDeg.orderBy(desc("inDegree")).show(5, False)

El resultado de consultar las estaciones ordenadas por el mayor grado de entrada:

+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­ +
|id |enGrado|
+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­ +
Machine Translated by Google

|San Francisco Caltrain (Townsend en 4th)|34810 |


|San Francisco Caltrain 2 (330 Townsend) |22523 |
|Harry Bridges Plaza (Edificio Ferry) |17810 |
|2do en Townsend |15463 |
|Townsend en 7th |15422 |
+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­ +

Podemos consultar los grados de salida de la misma manera:

// en escala
val outDeg = stationGraph.outDegrees
outDeg.orderBy(desc("outDegree")).show(5, false)

# en pitón
outDeg = stationGraph.outDegrees
outDeg.orderBy(desc("outDegree")).show(5, False)

+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+­­­ ­­­­­­+
|id |fueraGrado|
+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+­­­ ­­­­­­+
|Caltrain de San Francisco (Townsend en 4th) |26304 |
|San Francisco Caltrain 2 (330 Townsend) |21758 |
|Harry Bridges Plaza (Edificio Ferry) |17255 |
|Terminal temporal de Transbay (Howard en Beale)|14436 |
|Embarcadero en Sansome |14158 |
+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+­­­ ­­­­­­+

La relación de estos dos valores es una métrica interesante para observar. Un valor de relación más alto nos dirá
donde termina una gran cantidad de viajes (pero rara vez comienzan), mientras que un valor más bajo nos dice dónde
a menudo comienzan (pero rara vez terminan):

// en escala
val gradosRatio = inDeg.join(outDeg, Seq("id"))
.selectExpr("id", "doble(enGrado)/doble(fueraDegrado) como proporción de grados")
gradoRatio.orderBy(desc("gradoRatio")).show(10, false)
gradoRatio.orderBy("gradoRatio").show(10, false)

# en pitón
relación de grados = inDeg.join(outDeg, "id")\
.selectExpr("id", "doble(enGrado)/doble(fueraDegrado) como proporción de grados")
gradoRatio.orderBy(desc("gradoRatio")).show(10, False)
gradoRatio.orderBy("gradoRatio").show(10, Falso)

Esas consultas dan como resultado los siguientes datos:

+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­ ­­­­­­­­­­+
|id |gradoRatio |
+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­ ­­­­­­­­­­+
|Centro médico de Redwood City |1.5333333333333334|
|Centro del Condado de San Mateo |1.4724409448818898|
Machine Translated by Google

...
|Embarcadero en Vallejo |1.2201707365495336|
|Mercado en Sansome |1.2173913043478262|
+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­ ­­­­­­­­­­+

+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­ ­+
|id |gradoRatio |
+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­ ­+
|Grant Avenue en Columbus Avenue|0.5180520570948782| |
2do en Folsom |0.5909488686085761|
...
|Ayuntamiento de San Francisco |0.7928849902534113|
|Estación de Caltrain de Palo Alto |0.8064516129032258|
+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­ ­+

Búsqueda primero en amplitud

La búsqueda primero en amplitud buscará en nuestro gráfico cómo conectar dos conjuntos de nodos, según
los bordes del gráfico. En nuestro contexto, podríamos querer hacer esto para encontrar las rutas más cortas a
diferentes estaciones, pero el algoritmo también funciona para conjuntos de nodos especificados a través de una
expresión SQL. Podemos especificar el máximo de bordes a seguir con maxPathLength, y también podemos
especificar un edgeFilter para filtrar los bordes que no cumplen con un requisito, como los viajes fuera del
horario comercial.

Elegiremos dos estaciones bastante cercanas para que esto no se alargue demasiado. Sin embargo, puede
realizar recorridos de gráficos interesantes cuando tiene gráficos dispersos que tienen conexiones distantes.
Siéntase libre de jugar con las estaciones (especialmente las de otras ciudades) para ver si puede conectar
estaciones distantes:

// en Scala
stationGraph.bfs.fromExpr("id = 'Townsend at 7th'") .toExpr("id
= 'Spear at Folsom'").maxPathLength(2).run().show(10)

# en Python
stationGraph.bfs(fromExpr="id = 'Townsend at 7th'",
toExpr="id = 'Lanza en Folsom'", maxPathLength=2).show(10)

+­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+­­­­­­­ ­­­­­­­­­­­­­+
| de| e0| a|
+­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+­­­­­­­ ­­­­­­­­­­­­­+
|[65,Townsend en 7...|[913371,663,8/31/...|[49,Lanza en Fols...| |[65,Townsend
en 7...|[913265,658,8/31/...|[49,Lanza en Fols...|
...
|[65,Townsend en 7...|[903375,850,8/24/...|[49,Lanza en Fols...| |[65,Townsend
en 7...|[899944,910,8/21/...|[49,Lanza en Fols...|
+­­­­­­­­­­­­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­+­­­­­­­ ­­­­­­­­­­­­­+

Componentes conectados
Machine Translated by Google

Un componente conexo define un subgrafo (no dirigido) que tiene conexiones consigo mismo pero no se
conecta con el grafo mayor, como se ilustra en la figura 30­5.

Figura 30­5. Un componente conectado

El algoritmo de componentes conectados no se relaciona directamente con nuestro problema actual


porque asumen un gráfico no dirigido. Sin embargo, aún podemos ejecutar el algoritmo, que solo asume
que no hay direccionalidad asociada con nuestros bordes. De hecho, si miramos el mapa de bicicletas
compartidas, asumimos que obtendríamos dos componentes distintos conectados (Figura 30­6).
Machine Translated by Google

Figura 30­6. Un mapa de las ubicaciones de bicicletas compartidas del Área de la Bahía

ADVERTENCIA

Para ejecutar este algoritmo, deberá establecer un directorio de punto de control que almacenará el estado
del trabajo en cada iteración. Esto le permite continuar donde lo dejó si el trabajo falla. Este es probablemente
uno de los algoritmos más caros actualmente en GraphFrames, así que espere retrasos.

Una cosa que probablemente tendrá que hacer para ejecutar este algoritmo en su máquina local es tomar una muestra de los datos,
tal como lo hacemos en el siguiente ejemplo de código (tomar una muestra puede ayudarlo a obtener un resultado sin bloquear la
aplicación Spark con problemas de recolección de basura):

// en Scala
spark.sparkContext.setCheckpointDir("/tmp/puntos de control")

# en Python
spark.sparkContext.setCheckpointDir("/tmp/puntos de control")

// en Scala val
minGraph = GraphFrame(stationVertices, tripEdges.sample(false, 0.1)) val cc =
minGraph.connectedComponents.run()

# en Python
minGraph = GraphFrame(stationVertices, tripEdges.sample(False, 0.1)) cc =
minGraph.connectedComponents()

De esta consulta obtenemos dos componentes conectados pero no necesariamente los que podríamos esperar.
Es posible que nuestra muestra no tenga todos los datos o la información correctos, por lo que probablemente necesitemos más
Machine Translated by Google

recursos informáticos para investigar más a fondo:

// en Scala
cc.where("componente != 0").show()

# en Python
cc.where("componente != 0").show()

+­­­­­­­­­­+­­­­­­­­­­­­­­­­­­+­­­­­­­­­+­­­­­­­­­ ­­+­­­­­­­­­+­­­­­­­­­­­­+­­­­­
|id_estación| identificación | lat| largo|recuento| hito|en...
+­­­­­­­­­­+­­­­­­­­­­­­­­­­­­+­­­­­­­­­+­­­­­­­­­ ­­+­­­­­­­­­+­­­­­­­­­­­­+­­­­­
47| Publicar en Kearney|37.788975|­122.403452| 46|Washington 19|San Francisco...| ...
|| en K...|37.795425|­122.404767| 15|San Francisco...| ...
+­­­­­­­­­­+­­­­­­­­­­­­­­­­­­+­­­­­­­­­+­­­­­­­­­ ­­+­­­­­­­­­+­­­­­­­­­­­­+­­­­­

Componentes fuertemente conectados


GraphFrames incluye otro algoritmo relacionado que se relaciona con gráficos dirigidos: componentes
fuertemente conectados, que tiene en cuenta la direccionalidad. Un componente fuertemente conectado es un subgrafo
que tiene caminos entre todos los pares de vértices dentro de él.

// en Scala val
scc = minGraph.stronglyConnectedComponents.maxIter(3).run()

# en Python scc
= minGraph.stronglyConnectedComponents(maxIter=3)

scc.groupBy("componente").count().show()

Tareas avanzadas
Esta es solo una breve selección de algunas de las características de GraphFrames. La biblioteca GraphFrames
también incluye características como escribir sus propios algoritmos a través de una interfaz de paso de
mensajes, conteo de triángulos y conversión hacia y desde GraphX. Puede encontrar más información en la
documentación de GraphFrames.

Conclusión
En este capítulo, hicimos un recorrido por GraphFrames, una biblioteca para realizar análisis de gráficos en
Apache Spark. Tomamos un enfoque más basado en tutoriales, ya que esta técnica de procesamiento no es
necesariamente la primera herramienta que las personas usan cuando realizan análisis avanzados. No obstante, es
una herramienta poderosa para analizar las relaciones entre diferentes objetos y es fundamental en muchos
dominios. El próximo capítulo hablará sobre una funcionalidad más avanzada, específicamente, el aprendizaje
profundo.
Machine Translated by Google

Capítulo 31. Aprendizaje profundo

El aprendizaje profundo es una de las áreas de desarrollo más emocionantes en torno a Spark debido a su capacidad
para resolver varios problemas de aprendizaje automático que antes eran difíciles, especialmente aquellos
que involucran datos no estructurados, como imágenes, audio y texto. Este capítulo cubrirá cómo funciona Spark en
conjunto con el aprendizaje profundo y algunos de los diferentes enfoques que puede usar para trabajar con Spark y el
aprendizaje profundo juntos.

Debido a que el aprendizaje profundo aún es un campo nuevo, muchas de las herramientas más nuevas se implementan
en bibliotecas externas. Este capítulo no se centrará en los paquetes que necesariamente son fundamentales para
Spark, sino en la enorme cantidad de innovación en las bibliotecas creadas sobre Spark. Comenzaremos con varias
formas de alto nivel de usar el aprendizaje profundo en Spark, discutiremos cuándo usar cada una y luego repasaremos
las bibliotecas disponibles para ellas. Como de costumbre, incluiremos ejemplos de principio a fin.

NOTA

Para aprovechar al máximo este capítulo, debe conocer al menos los conceptos básicos del aprendizaje
profundo, así como los conceptos básicos de Spark. Dicho esto, señalamos un excelente recurso al comienzo de
esta parte del libro llamado Deep Learning Book, de algunos de los mejores investigadores en esta área.

¿Qué es el aprendizaje profundo?


Para definir el aprendizaje profundo, primero debemos definir las redes neuronales. Una red neuronal es un gráfico
de nodos con pesos y funciones de activación. Estos nodos están organizados en capas que se apilan una
encima de la otra. Cada capa está conectada, ya sea parcial o completamente, a la capa anterior en la red. Al
apilar capas una tras otra, estas funciones simples pueden aprender a reconocer señales cada vez más complejas en
la entrada: líneas simples con una capa, círculos y cuadrados con la siguiente capa, texturas complejas en otra
y, finalmente, el objeto completo o la salida. espera identificar. El objetivo es entrenar a la red para asociar ciertas
entradas con ciertas salidas ajustando los pesos asociados con cada conexión y los valores de cada nodo en la
red. La figura 31­1 muestra la red neuronal simple.
Machine Translated by Google

Figura 31­1. Una red neuronal

El aprendizaje profundo, o redes neuronales profundas, apila muchas de estas capas en varias arquitecturas
diferentes. Las propias redes neuronales han existido durante décadas y han aumentado y disminuido en términos
de popularidad para diversos problemas de aprendizaje automático. Sin embargo, recientemente, una combinación
de conjuntos de datos mucho más grandes (p. ej., el corpus ImageNet para el reconocimiento de objetos), hardware
potente (clústeres y GPU) y nuevos algoritmos de entrenamiento han permitido entrenar redes neuronales mucho
más grandes que superan los enfoques anteriores en muchas tareas de aprendizaje automático. Las técnicas típicas
de aprendizaje automático normalmente no pueden seguir funcionando bien a medida que se agregan más datos;
su rendimiento toca un techo. El aprendizaje profundo puede beneficiarse de enormes cantidades de datos e
información, y no es raro que los conjuntos de datos de aprendizaje profundo sean órdenes de magnitud más grandes
que otros conjuntos de datos de aprendizaje automático. Las redes neuronales profundas ahora se han convertido
en el estándar en la visión por computadora, el procesamiento del habla y algunas tareas de lenguaje natural, donde
a menudo "aprenden" mejores funciones que los modelos anteriores ajustados a mano. También se están aplicando
activamente en otras áreas del aprendizaje automático. La fortaleza de Apache Spark como sistema de big data y
computación paralela lo convierte en un marco natural para usar con aprendizaje profundo.

Los investigadores e ingenieros se han esforzado mucho en acelerar estos cálculos similares a redes neuronales.
Hoy en día, la forma más popular de usar redes neuronales o aprendizaje profundo es usar un marco, implementado por
un instituto de investigación o una corporación. Los más populares en el momento de escribir este artículo son
TensorFlow, MXNet, Keras y PyTorch. Esta área está evolucionando rápidamente, por lo que siempre vale la pena
buscar otras.
Machine Translated by Google

Formas de usar el aprendizaje profundo en Spark


En su mayor parte, independientemente de la aplicación a la que se dirija, existen tres formas principales de utilizar el aprendizaje profundo en

Spark:

Inferencia

La forma más sencilla de usar el aprendizaje profundo es tomar un modelo previamente entrenado y aplicarlo a grandes conjuntos

de datos en paralelo usando Spark. Por ejemplo, podría usar un modelo de clasificación de imágenes, entrenado con un conjunto de

datos estándar como ImageNet, y aplicarlo a su propia colección de imágenes para identificar pandas, flores o automóviles. Muchas

organizaciones publican grandes modelos preentrenados en conjuntos de datos comunes (por ejemplo, Faster R­CNN y YOLO para

la detección de objetos), por lo que a menudo puede tomar un modelo de su marco de aprendizaje profundo favorito y aplicarlo en paralelo

mediante una función de Spark. Con PySpark, podría simplemente llamar a un marco como TensorFlow o PyTorch en una función de

mapa para obtener una inferencia distribuida, aunque algunas de las bibliotecas que discutimos hacen más optimizaciones más allá de

simplemente llamar a estas bibliotecas en una función de mapa.

Caracterización y transferencia de aprendizaje

El siguiente nivel de complejidad es utilizar un modelo existente como un destacador en lugar de tomar su resultado final. Muchos

modelos de aprendizaje profundo aprenden representaciones de características útiles en sus capas inferiores a medida que se entrenan

para una tarea de extremo a extremo. Por ejemplo, un clasificador entrenado en el conjunto de datos de ImageNet también

aprenderá características de bajo nivel presentes en todas las imágenes naturales, como bordes y texturas. Luego podemos usar

estas características para aprender modelos para un nuevo problema que no está cubierto por el conjunto de datos original. Este

método se denomina aprendizaje de transferencia y, por lo general, involucra las últimas capas de un modelo preentrenado y el

reentrenamiento con los datos de interés. El aprendizaje de transferencia también es especialmente útil si no tiene una gran

cantidad de datos de entrenamiento: entrenar una red completa desde cero requiere un conjunto de datos de cientos de miles de

imágenes, como ImageNet, para evitar el sobreajuste, que no estará disponible en muchos contextos empresariales. Por el contrario,

el aprendizaje por transferencia puede funcionar incluso con unos pocos miles de imágenes porque actualiza menos parámetros.

entrenamiento modelo

Spark también se puede usar para entrenar un nuevo modelo de aprendizaje profundo desde cero. Hay dos métodos comunes

aquí. Primero, puede usar un clúster de Spark para paralelizar el entrenamiento de un solo modelo en varios servidores,

comunicando actualizaciones entre ellos. Alternativamente, algunas bibliotecas permiten que el usuario entrene varias instancias de

modelos similares en paralelo para probar varias arquitecturas de modelos e hiperparámetros, lo que acelera el proceso de búsqueda y

ajuste del modelo.

En ambos casos, las bibliotecas de aprendizaje profundo de Spark simplifican el paso de datos de RDD y DataFrames a algoritmos

de aprendizaje profundo. Finalmente, incluso si no desea entrenar su modelo en paralelo, estas bibliotecas se pueden usar para extraer

datos de un clúster y exportarlos a un script de entrenamiento de una sola máquina usando el formato de datos nativo de marcos como

TensorFlow.

En los tres casos, el código de aprendizaje profundo generalmente se ejecuta como parte de una aplicación más grande que incluye

pasos de extracción, transformación y carga (ETL) para analizar los datos de entrada, E/S de varias fuentes y potencialmente inferencia

por lotes o transmisión. Para estas otras partes de la aplicación,


Machine Translated by Google

simplemente puede usar las API DataFrame, RDD y MLlib descritas anteriormente en este libro. Uno de los puntos
fuertes de Spark es la facilidad de combinar estos pasos en un único flujo de trabajo paralelo.

Bibliotecas de aprendizaje profundo


En esta sección, examinaremos algunas de las bibliotecas más populares disponibles para el aprendizaje profundo
en Spark. Describiremos los principales casos de uso de la biblioteca y los vincularemos a referencias o ejemplos cuando
sea posible. Esta lista no pretende ser exhaustiva, porque el campo está evolucionando rápidamente. Le recomendamos
que consulte el sitio web de cada biblioteca y la documentación de Spark para obtener las últimas
actualizaciones.

Soporte de red neuronal MLlib


MLlib de Spark actualmente tiene soporte nativo para un solo algoritmo de aprendizaje profundo: el
clasificador de perceptrón multicapa de la clase ml.classification.MultilayerPerceptronClassifier. Esta clase se
limita al entrenamiento de redes relativamente poco profundas que contienen capas completamente conectadas con la
función de activación sigmoidea y una capa de salida con una función de activación softmax. Esta clase es
más útil para entrenar las últimas capas de un modelo de clasificación cuando se usa el aprendizaje de transferencia
sobre un caracterizador basado en aprendizaje profundo existente. Por ejemplo, se puede agregar encima de la
biblioteca Deep Learning Pipelines que describimos más adelante en este capítulo para realizar rápidamente el
aprendizaje de transferencia sobre los modelos Keras y TensorFlow.

TensorFrames
TensorFrames es una biblioteca orientada al aprendizaje de inferencia y transferencia que facilita la transferencia
de datos entre Spark DataFrames y TensorFlow. Es compatible con las interfaces de Python y Scala y se enfoca en
proporcionar una interfaz simple pero optimizada para pasar datos de TensorFlow a Spark y viceversa. En particular,
usar TensorFrames para aplicar un modelo sobre Spark DataFrames generalmente es más eficiente que
llamar a una función de mapa de Python que invoca directamente el modelo de TensorFlow, debido a la transferencia
de datos más rápida y la amortización del costo inicial. TensorFrames es más útil para la inferencia, tanto en
configuraciones de transmisión como por lotes, y para el aprendizaje de transferencia, donde puede aplicar un
modelo existente sobre datos sin procesar para caracterizarlo, luego aprenda las últimas capas usando un
MultilayerPerceptronClassifier o incluso una regresión logística más simple o un bosque aleatorio. clasificador
sobre los datos.

BigDL
BigDL es un marco de aprendizaje profundo distribuido para Apache Spark desarrollado principalmente por Intel.
Su objetivo es admitir el entrenamiento distribuido de modelos grandes, así como aplicaciones rápidas de estos modelos
mediante inferencia. Una ventaja clave de BigDL sobre las otras bibliotecas descritas aquí es que está optimizada
principalmente para usar CPU en lugar de GPU, lo que hace que sea eficiente para ejecutarse en un clúster
existente basado en CPU (por ejemplo, una implementación de Apache Hadoop). BigDL proporciona API de alto
nivel para crear redes neuronales desde cero y distribuye automáticamente todas las operaciones de forma predeterminada. Puede
Machine Translated by Google

también entrenar modelos descritos con la biblioteca Keras DL.

TensorFlowOnSpark
TensorFlowOnSpark es una biblioteca ampliamente utilizada que puede entrenar modelos de TensorFlow de forma paralela
en clústeres de Spark. TensorFlow incluye algunas bases para realizar capacitación distribuida, pero aún debe depender de un
administrador de clústeres para administrar el hardware y las comunicaciones de datos. No viene con un administrador de clúster o
una capa de E/S distribuida lista para usar.
TensorFlowOnSpark inicia el modo distribuido existente de TensorFlow dentro de un trabajo de Spark y automáticamente alimenta
datos de Spark RDD o DataFrames al trabajo de TensorFlow. Si ya sabe cómo usar el modo distribuido de TensorFlow,
TensorFlowOnSpark facilita el lanzamiento de su trabajo dentro de un clúster de Spark y le pasa datos procesados con otras
bibliotecas de Spark (por ejemplo, transformaciones de DataFrame) desde cualquier fuente de entrada compatible con Spark.
TensorFlowOnSpark se desarrolló originalmente en Yahoo! y también se utiliza en la producción en otras grandes organizaciones.
El proyecto también se integra con la API de ML Pipelines de Spark.

DeepLearning4J
DeepLearning4j es un proyecto de aprendizaje profundo distribuido de código abierto en Java y Scala que proporciona
opciones de capacitación distribuidas y de un solo nodo. Una de sus ventajas sobre los marcos de aprendizaje profundo basados
en Python es que se diseñó principalmente para JVM, lo que lo hace más conveniente para los grupos que no desean agregar
Python a su proceso de desarrollo. Incluye una amplia variedad de algoritmos de entrenamiento y soporte para CPU y GPU.

Canalizaciones de aprendizaje profundo

Deep Learning Pipelines es un paquete de código abierto de Databricks que integra la funcionalidad de aprendizaje profundo en la
API de ML Pipelines de Spark. El paquete existente de marcos de aprendizaje profundo (TensorFlow y Keras en el momento de
escribir este artículo), pero se centra en dos objetivos:

La incorporación de estos marcos en las API de Spark estándar (como ML Pipelines y


Spark SQL) para que sean muy fáciles de usar

Distribuir todo el cálculo por defecto

Por ejemplo, Deep Learning Pipelines proporciona una clase DeepImageFeaturizer que actúa como un transformador en Spark
ML Pipeline API, lo que le permite crear una canalización de aprendizaje de transferencia en solo unas pocas líneas de código
(por ejemplo, agregando un perceptrón o un clasificador de regresión logística en la parte superior ).
Del mismo modo, la biblioteca admite la búsqueda de cuadrículas paralelas en varios parámetros del modelo mediante la búsqueda
de cuadrículas y la API de validación cruzada de MLlib. Finalmente, los usuarios pueden exportar un modelo de ML como una
función definida por el usuario de Spark SQL y ponerlo a disposición de los analistas que usan SQL o aplicaciones de transmisión.
En el momento de escribir este artículo (verano de 2017), Deep Learning Pipelines se encuentra en pleno desarrollo, por lo que le
recomendamos que consulte su sitio web para obtener las últimas actualizaciones.

La Tabla 31­1 resume las diversas bibliotecas de aprendizaje profundo y los principales casos de uso que admiten:
Machine Translated by Google

Cuadro 31­1. Bibliotecas de aprendizaje profundo

DL subyacente
Biblioteca casos de uso
estructura

BigDL BigDL Capacitación distribuida, inferencia, integración de ML Pipeline

DeepLearning4J DeepLearning4J Inferencia, transferencia de aprendizaje, entrenamiento distribuido

Aprendizaje profundo Inferencia, transferencia de aprendizaje, entrenamiento multimodelo, ML Pipeline


TensorFlow, Keras
Tuberías y la integración de Spark SQL

Perceptrón MLlib Chispa ­ chispear Capacitación distribuida, integración de ML Pipeline

TensorFlowOnSpark TensorFlow Capacitación distribuida, integración de ML Pipeline

TensorFrames TensorFlow Inferencia, transferencia de aprendizaje, integración de DataFrame

Si bien hay varios enfoques que diferentes empresas han adoptado para integrar Spark y deep
bibliotecas de aprendizaje, la que actualmente apunta a la integración más cercana con MLlib y
DataFrames es canalizaciones de aprendizaje profundo. Esta biblioteca tiene como objetivo mejorar el soporte de Spark para imágenes.
y datos de tensor (que se integrarán en la base de código central de Spark en Spark 2.3), y para hacer
toda la funcionalidad de aprendizaje profundo disponible en la API de ML Pipeline. Su amigable API lo convierte en el
la forma más sencilla de ejecutar el aprendizaje profundo en Spark hoy y será el enfoque de las secciones restantes
en este capítulo.

Un ejemplo simple con canalizaciones de aprendizaje profundo


Como describimos, Deep Learning Pipelines proporciona API de alto nivel para un aprendizaje profundo escalable
mediante la integración de marcos de aprendizaje profundo populares con ML Pipelines y Spark SQL.

Deep Learning Pipelines se basa en ML Pipelines de Spark para capacitación y en Spark DataFrames
y SQL para implementar modelos. Incluye API de alto nivel para aspectos comunes del aprendizaje profundo
para que se puedan hacer de manera eficiente en unas pocas líneas de código:

Trabajar con imágenes en Spark DataFrames;

Aplicar modelos de aprendizaje profundo a escala, ya sean propios o populares estándar


modelos, a imagen y datos de tensor;

Transfiera el aprendizaje utilizando modelos comunes de aprendizaje profundo preentrenados;

Exportación de modelos como funciones Spark SQL para que todo tipo de usuarios lo puedan tomar fácilmente
ventaja del aprendizaje profundo; y

Ajuste de hiperparámetros de aprendizaje profundo distribuido a través de ML Pipelines.

Deep Learning Pipelines actualmente solo ofrece una API en Python, que está diseñada para funcionar
Machine Translated by Google

estrechamente con los paquetes de aprendizaje profundo de Python existentes, como TensorFlow y Keras.

Configuración

Deep Learning Pipelines es un paquete Spark, por lo que lo cargaremos tal como cargamos GraphFrames.
Deep Learning Pipelines funciona en Spark 2.x y el paquete se puede encontrar aquí. Necesitará instalar algunas
dependencias de Python, incluidas TensorFrames, TensorFlow, Keras y h5py. Asegúrese de que estén instalados
tanto en su controlador como en las máquinas de trabajo.

Usaremos el conjunto de datos de flores del tutorial de actualización de TensorFlow. Ahora, si está ejecutando esto en un
grupo de máquinas, necesitará una forma de colocar estos archivos en un sistema de archivos distribuido una vez
que los descargue. Incluimos una muestra de estas imágenes en el repositorio de GitHub del libro.

Imágenes y marcos de datos

Uno de los desafíos históricos al trabajar con imágenes en Spark es que introducirlas en un DataFrame era difícil y
tedioso. Deep Learning Pipelines incluye funciones de utilidad que facilitan la carga y decodificación de imágenes de
forma distribuida. Esta es un área que está cambiando rápidamente. Actualmente, esto es parte de Deep Learning
Pipelines. La carga y representación de imágenes básicas se incluirán en Spark 2.3. Si bien aún no se ha
publicado, todos los ejemplos de este capítulo deberían ser compatibles con esta próxima versión de Spark:

from sparkdl import readImages img_dir


= '/data/deep­learning­images/' image_df =
readImages(img_dir)

El DataFrame resultante contiene la ruta y luego la imagen junto con algunos metadatos asociados:

imagen_df.printSchema()

raíz
|­­ filePath: cadena (anulable = falso) |­­ imagen:
estructura (anulable = verdadero) | | | | |
|­­ modo: cadena (anulable = falso) |­­ alto:
entero (anulable = falso) |­­ ancho: entero
(anulable = falso) |­­ nChannels: entero (anulable
= falso) |­­ datos: binario (anulable = falso)

Transferencia de aprendizaje

Ahora que tenemos algunos datos, podemos comenzar con un aprendizaje de transferencia simple. Recuerde, esto
significa aprovechar un modelo que otra persona creó y modificarlo para que se adapte mejor a nuestros propios
propósitos. Primero, cargaremos los datos para cada tipo de flor y crearemos un conjunto de entrenamiento y prueba:
Machine Translated by Google

from sparkdl import readImages from


pyspark.sql.functions import lit tulips_df =
readImages(img_dir + "/tulips").withColumn("label", lit(1)) daisy_df = readImages(img_dir +
"/daisy").withColumn(" etiqueta", lit(0)) tulipanes_tren, tulipanes_prueba =
tulipanes_df.randomSplit([0.6, 0.4]) daisy_tren, daisy_prueba =
daisy_df.randomSplit([0.6, 0.4]) tren_df = tulipanes_tren.unionAll(daisy_tren)
test_df = tulipanes_prueba.unionAll( prueba_daisy)

En el siguiente paso, aprovecharemos un transformador llamado DeepImageFeaturizer. Esto nos permitirá


aprovechar un modelo previamente entrenado llamado Inception, una poderosa red neuronal utilizada con éxito para
identificar patrones en imágenes. La versión que estamos usando está preentrenada para funcionar bien con
imágenes de varios objetos y animales comunes. Este es uno de los modelos preentrenados estándar que se
envían con la biblioteca de Keras. Sin embargo, esta red neuronal particular no está entrenada para reconocer
margaritas y rosas. Así que vamos a utilizar el aprendizaje por transferencia para convertirlo en algo útil para
nuestros propios propósitos: distinguir diferentes tipos de flores.

Tenga en cuenta que podemos usar los mismos conceptos de ML Pipeline que aprendimos a lo largo de esta parte del
libro y aprovecharlos con Deep Learning Pipelines: DeepImageFeaturizer es solo un transformador de ML. Además,
todo lo que hemos hecho para extender este modelo es agregar un modelo de regresión logística para facilitar el
entrenamiento de nuestro modelo final. Podríamos usar otro clasificador en su lugar. El siguiente fragmento de código
muestra la adición de este modelo (tenga en cuenta que esto puede llevar tiempo para completarse, ya que es un
proceso bastante intensivo en recursos):

from pyspark.ml.classification import LogisticRegression from pyspark.ml


import Pipeline from sparkdl import
DeepImageFeaturizer featurizer =
DeepImageFeaturizer(inputCol="image", outputCol="features",
nombre del modelo="InicioV3")
lr = LogisticRegression(maxIter=1, regParam=0.05, elasticNetParam=0.3,
labelCol="etiqueta")
p = Pipeline(etapas=[presentador, lr]) p_model
= p.fit(train_df)

Una vez que hayamos entrenado el modelo, podemos usar el mismo evaluador de clasificación que usamos
en el Capítulo 25. Podemos especificar la métrica que nos gustaría probar y luego evaluarla:

from pyspark.ml.e Evaluation import MulticlassClassificationEvaluator testing_df =


p_model.transform(test_df) evaluador =
MulticlassClassificationEvaluator(metricName="accuracy") print(" Exactitud del conjunto
"
de pruebas = + str(evaluator.evaluate(tested_df.select( "prediction", "label "))))

Con nuestro DataFrame de ejemplos, podemos inspeccionar las filas e imágenes en las que cometimos errores
en el entrenamiento anterior:

desde pyspark.sql.types importar DoubleType


desde pyspark.sql.functions importar expr
Machine Translated by Google

# un UDF simple para convertir el valor en una definición


doble _p1(v):
return float(v.array[1]) p1 =
udf(_p1, DoubleType()) df =
test_df.withColumn("p_1", p1(tested_df .probabilidad)) df_incorrecto =
df.orderBy(expr("abs(p_1 ­ etiqueta)"), ascending=False)
df_incorrecto.select("ruta del archivo", "p_1", "etiqueta").limit(10).show( )

Aplicación de modelos de aprendizaje profundo a escala

Spark DataFrames es una construcción natural para aplicar modelos de aprendizaje profundo a un conjunto de datos
a gran escala. Deep Learning Pipelines proporciona un conjunto de transformadores para aplicar gráficos de TensorFlow y
modelos Keras respaldados por TensorFlow a escala. Además, los modelos de imágenes populares se pueden aplicar de
forma inmediata, sin necesidad de ningún código TensorFlow o Keras. Los transformadores, respaldados por la biblioteca
Tensorframes, manejan de manera eficiente la distribución de modelos y datos a las tareas de Spark.

Aplicación de modelos populares


Hay muchos modelos estándar de aprendizaje profundo para imágenes. Si la tarea en cuestión es muy similar a la que
brindan los modelos (por ejemplo, reconocimiento de objetos con clases de ImageNet), o simplemente para
exploración, puede usar el transformador DeepImagePredictor simplemente especificando el nombre del modelo. Deep
Learning Pipelines admite una variedad de modelos estándar incluidos en Keras, que se enumeran en su sitio web. El
siguiente es un ejemplo del uso de DeepImagePredictor:

from sparkdl import readImages, DeepImagePredictor


image_df = readImages(img_dir)
predictor =

DeepImagePredictor( inputCol="image",
outputCol="predicted_labels",
modelName="InceptionV3",
decodePredictions=True, topK=10)
predicciones_df = predictor.transform(image_df)

Observe que la columna etiquetas_predichas muestra "margarita" como una clase de alta probabilidad para todas las
flores de muestra que usan este modelo base. Sin embargo, como puede verse por las diferencias en los valores de
probabilidad, la red neuronal tiene la información para discernir los dos tipos de flores. Como podemos ver, nuestro ejemplo
de aprendizaje por transferencia fue capaz de aprender correctamente las diferencias entre margaritas y tulipanes a partir
del modelo base:

df = p_modelo.transformar(imagen_df)

Aplicación de modelos personalizados de Keras

Deep Learning Pipelines también nos permite aplicar un modelo de Keras de manera distribuida usando Spark. Para
hacer esto, consulte la guía del usuario en KerasImageFileTransformer. Esto carga un modelo Keras y lo aplica a una
columna DataFrame.
Machine Translated by Google

Aplicar modelos de TensorFlow

Deep Learning Pipelines, a través de su integración con TensorFlow, se puede usar para crear transformadores personalizados
que manipulan imágenes usando TensorFlow. Por ejemplo, podría crear un transformador para cambiar el tamaño
de una imagen o modificar el espectro de colores. Para ello, utilice la clase TFImageTransformer.

Implementación de modelos como funciones SQL

Otra opción es implementar un modelo como una función SQL, lo que permite que cualquier usuario que sepa SQL pueda usar
un modelo de aprendizaje profundo. Una vez que se usa esta función, la función UDF resultante toma una columna y produce
la salida del modelo particular. Por ejemplo, podría aplicar Inception v3 a una variedad de imágenes usando la clase
registerKeraImageUDF:

desde keras.applications importar InceptionV3


desde sparkdl.udf.keras_image_model importar registerKerasImageUDF
desde keras.applications importar InceptionV3
registerKerasImageUDF("my_keras_inception_udf", InceptionV3(weights="imagenet"))

De esta forma, el poder del aprendizaje profundo está disponible para cualquier usuario de Spark, no solo para el especialista
que creó el modelo.

Conclusión
Este capítulo analizó varios enfoques comunes para usar el aprendizaje profundo en Spark. Cubrimos una variedad de
bibliotecas disponibles y luego trabajamos con algunos ejemplos básicos de tareas comunes.
Esta área de Spark está en desarrollo muy activo y continuará avanzando a medida que pase el tiempo, por lo que vale la pena
consultar las bibliotecas para obtener más información a medida que pasa el tiempo. Con el tiempo, los autores de este libro
esperan mantener este capítulo actualizado con los desarrollos actuales.
Machine Translated by Google

Parte VII. Ecosistema


Machine Translated by Google

Capítulo 32. Características del lenguaje:


Python (PySpark) y R (SparkR y sparklyr)

Este capítulo cubrirá algunas de las especificaciones de lenguaje más matizadas de Apache Spark. Hemos
visto una gran cantidad de ejemplos de PySpark a lo largo del libro. En el Capítulo 1, discutimos a alto nivel
cómo Spark ejecuta código de otros lenguajes. Hablemos de algunas de las integraciones más
específicas:

PySpark

SparkR

brillante

Como recordatorio, la Figura 32­1 muestra la arquitectura fundamental para estos lenguajes específicos.

Figura 32­1. El conductor de chispa

Ahora vamos a cubrir cada uno de estos en profundidad.

PySpark
Cubrimos una tonelada de PySpark a lo largo de este libro. De hecho, PySpark se incluye junto con Scala y
SQL en casi todos los capítulos de este libro. Por lo tanto, esta sección será breve y dulce, cubriendo
solo los detalles que son relevantes para Spark. Como discutimos en el Capítulo 1, Spark 2.2 incluía una
forma de instalar PySpark con pip. Simplemente, pip install pyspark lo pondrá a disposición como un
paquete en su máquina local. Esto es nuevo, por lo que puede haber algunos errores que corregir, pero es
algo que puede aprovechar en sus proyectos hoy.

Diferencias fundamentales de PySpark


Si está utilizando las API estructuradas, su código debería ejecutarse casi tan rápido como si hubiera escrito
Machine Translated by Google

en Scala, excepto si no está utilizando UDF en Python. Si está utilizando una UDF, puede tener un impacto en el
rendimiento. Consulte el Capítulo 6 para obtener más información sobre por qué este es el caso.

Si está utilizando API no estructuradas, específicamente RDD, entonces su rendimiento se verá afectado (a
costa de un poco más de flexibilidad). Tocamos este razonamiento en el Capítulo 12, pero la idea fundamental
es que Spark tendrá que trabajar mucho más para convertir información de algo que Spark y la JVM puedan
entender a Python y viceversa. Esto incluye tanto funciones como datos y es un proceso conocido como
serialización. No estamos diciendo que nunca tenga sentido usarlos; es sólo algo a tener en cuenta al hacerlo.

Integración de pandas
Uno de los poderes de PySpark es su capacidad para trabajar en todos los modelos de programación. Por ejemplo,
un patrón común es realizar un trabajo ETL a gran escala con Spark y luego recopilar el resultado (del tamaño de
una sola máquina) en el controlador y luego aprovechar Pandas para manipularlo aún más. Esto le permite utilizar
la mejor herramienta de su clase para la mejor tarea que tiene entre manos: Spark para big data y Pandas para
small data:

importar pandas como pd


df = pd.DataFrame({"primero":rango(200), "segundo":rango(50,250)})

chispaDF = chispa.createDataFrame(df)

nuevoPDF = sparkDF.toPandas()
nuevoPDF.cabeza()

Estas sutilezas facilitan el trabajo con datos grandes y pequeños con Spark. La comunidad de Spark continúa
enfocándose en mejorar esta interoperabilidad con varios otros proyectos, por lo que la integración entre
Spark y Python seguirá mejorando. Por ejemplo, en el momento de escribir este artículo, la comunidad está
trabajando activamente en UDF vectorizados (SPARK­21190), que agregan una API mapBatches para permitirle
procesar un Spark DataFrame como una serie de marcos de datos de Pandas en Python en lugar de convertir
cada fila individual. a un objeto Python. Esta función está destinada a aparecer en Spark 2.3.

R en chispa
El resto de este capítulo cubrirá R, el lenguaje compatible oficial más reciente de Spark. R es un lenguaje
y entorno para computación estadística y gráficos. Es similar al entorno y lenguaje S desarrollado en Bell
Laboratories por John Chambers (sin relación con uno de los autores de este libro) y sus colegas. El lenguaje R
ha existido durante décadas y es consistentemente popular entre los estadísticos y quienes realizan
investigaciones en computación numérica. R se está convirtiendo constantemente en un ciudadano de primera
clase en Spark y proporciona la interfaz de código abierto más simple para el cómputo distribuido en el lenguaje
R.

La popularidad de R para realizar análisis de datos de una sola máquina y análisis avanzados hace que sea
Machine Translated by Google

un excelente complemento para Spark. Hay dos iniciativas principales para hacer realidad esta asociación: SparkR y
sparklyr. Estos paquetes adoptan enfoques ligeramente diferentes para proporcionar una funcionalidad similar.
SparkR proporciona una API DataFrame similar a data.frame de R, mientras que sparklyr se basa en el popular
paquete dplyr para acceder a datos estructurados. Puede usar lo que prefiera en su código, pero con el tiempo
esperamos que la comunidad pueda converger hacia un único paquete integrado.

Cubriremos ambos paquetes aquí para que pueda elegir qué API prefiere. En su mayor parte, ambos proyectos están
maduros y cuentan con un buen apoyo, aunque por comunidades ligeramente diferentes.
Ambos son compatibles con las API estructuradas de Spark y permiten el aprendizaje automático. Desarrollaremos sus
diferencias en las siguientes secciones.

SparkR
SparkR es un paquete de R (que se originó como un proyecto de investigación colaborativo entre UC Berkeley,
Databricks y MIT CSAIL) que proporciona una interfaz para Apache Spark basada en API de R conocidas. SparkR
es conceptualmente similar a la API data.frame integrada de R, excepto por algunas desviaciones de la
semántica de la API, como la evaluación diferida. SparkR es parte del proyecto Spark oficial y es compatible como tal.
Consulte la documentación de SparkR para obtener más información.

Pros y contras de usar SparkR en lugar de otros lenguajes

Las razones por las que le recomendamos que use SparkR en lugar de PySpark son las siguientes.

Está familiarizado con R y quiere dar el paso más pequeño para aprovechar las capacidades de
Chispa ­ chispear:

Desea aprovechar la funcionalidad o las bibliotecas específicas de R (por ejemplo, la excelente biblioteca
ggplot2) y le gustaría trabajar con big data en el proceso.

R es un poderoso lenguaje de programación que brinda muchas ventajas sobre otros lenguajes cuando se trata de
ciertas tareas. Sin embargo, tiene su parte de deficiencias, como trabajar de forma nativa con datos distribuidos. SparkR
tiene como objetivo llenar este vacío y hace un gran trabajo al permitir que los usuarios tengan éxito con datos
pequeños y grandes, de una manera conceptual similar a PySpark y Pandas.

Configuración

Echemos un vistazo a cómo usar SparkR. Naturalmente, necesitará tener R instalado en su sistema para seguir este
capítulo. Para iniciar el shell, en su carpeta de inicio de Spark, ejecute ./bin/sparkR para iniciar SparkR. Esto creará
automáticamente una SparkSession para usted. Si tuviera que ejecutar SparkR desde RStudio, tendría que hacer
algo como lo siguiente:

biblioteca(SparkR)
chispa <­ chispaR.sesión()

Una vez que hayamos iniciado el shell, podemos ejecutar los comandos de Spark. Por ejemplo, podemos leer en un CSV
Machine Translated by Google

archivo como vimos en el Capítulo 9:

minorista.datos <­ read.df(


"/data/retail­data/all/", "csv",

header="true",
inferSchema="true")
print(str(retail.data))

Podemos tomar algunas filas de este SparkDataFrame y convertirlas a un tipo de marco de datos R
estándar:

local.retail.data <­ take(retail.data, 5)


print(str(local.retail.data))

Conceptos clave

Ahora que vimos un código muy básico, reiteremos los conceptos clave. Primero, SparkR sigue siendo Spark.
Básicamente, todas las herramientas que ha visto en todo el libro se aplican directamente a SparkR. Se ejecuta de
acuerdo con los mismos principios que PySpark y tiene casi todas las mismas funciones disponibles que
PySpark.

Como se muestra en la Figura 32­1, hay una puerta de enlace que conecta el proceso R con la JVM que contiene una
SparkSession, y SparkR convierte el código de usuario en manipulaciones estructuradas de Spark en todo el clúster.
Esto hace que su eficiencia esté a la par con Python y Scala cuando se usan las API estructuradas.
SparkR no tiene soporte para RDD u otras API de bajo nivel.

Si bien SparkR se usa menos que PySpark o Scala, sigue siendo popular y sigue creciendo. Para aquellos que
quieran saber lo suficiente sobre Spark para aprovechar SparkR de manera efectiva, recomendamos leer la siguiente
sección, junto con las Partes I y II de este libro. Cuando trabaje en esos otros capítulos, siéntase libre de probar y usar
SparkR en lugar de Python o Scala. Verás que una vez que lo domines, es fácil de traducir entre los distintos idiomas.

El resto de este capítulo explicará las diferencias más importantes entre SparkR y R "estándar" para que sea
más fácil ser productivo con SparkR más rápido.

Lo primero que debemos cubrir es la diferencia entre los tipos locales y los tipos Spark. La principal diferencia
de un tipo de marco de datos con la versión de Spark es que está disponible en la memoria y, por lo general, está
disponible directamente en ese proceso en particular. Un SparkDataFrame es solo una representación lógica
de una serie de manipulaciones. Por lo tanto, cuando manipulamos un marco de datos, veremos nuestros resultados de
inmediato. En un SparkDataFrame, vamos a manipular lógicamente los datos usando los mismos conceptos de
transformación y acción que vimos a lo largo del libro.

Una vez que tenemos un SparkDataFrame, podemos recopilarlo en un data.frame similar a cómo podemos leer datos
usando Spark. También podemos recopilarlo en un marco de datos local con el siguiente código (usando el
SparkDataFrame que creamos en "Configuración"):

# collect lo trae desde Spark a tu entorno local


Machine Translated by Google

collect(count(groupBy(retail.data, "country"))) #
createDataFrame convierte un data.frame # de su
entorno local a Spark

Esta diferencia es importante para los usuarios finales. Ciertas funciones o suposiciones que se aplican a los
marcos de datos locales no se aplican en Spark. Por ejemplo, no podemos indexar un SparkDataFrame según
una fila en particular. Además, no podemos cambiar los valores de los puntos en un SparkDataFrame, pero podemos
hacerlo en un data.frame local.

Enmascaramiento de funciones

Un problema frecuente cuando los usuarios acuden a SparkR es que SparkR enmascara ciertas funciones.
Cuando importé SparkR, recibí el siguiente mensaje:

Los siguientes objetos están enmascarados de 'paquete: estadísticas':

cov, filtrar, retrasar, na.omit, predecir, sd, var, ventana

Los siguientes objetos están enmascarados de 'paquete: base':

como.data.frame, colnames, ...

Esto significa que si deseamos llamar a estas funciones enmascaradas, debemos ser explícitos sobre el
paquete desde el que las estamos llamando o al menos entender qué función enmascara a otra. El ? puede ser útil
para determinar estos conflictos:

?na.omit # se refiere a SparkR debido al orden de carga del paquete ?


stats::na.omit # se refiere explícitamente a las
estadísticas ?SparkR::na.omit # se refiere explícitamente al filtrado de valores nulos de sparkR

Las funciones de SparkR solo se aplican a SparkDataFrames

Una implicación del enmascaramiento de funciones es que es posible que las funciones que funcionaban en objetos
anteriormente ya no funcionen en ellos después de traer el paquete SparkR. Esto se debe a que las funciones de
SparkR solo se aplican a los objetos de Spark. Por ejemplo, no podemos usar la función de muestra en un
marco de datos estándar porque Spark toma ese nombre de función:

muestra (mtcars) # falla

Lo que debe hacer en su lugar es usar explícitamente la función de muestra base. Además, las firmas de funciones
difieren entre las dos funciones, lo que significa que incluso si está familiarizado con la sintaxis y el orden de los
argumentos de una biblioteca en particular, no significa necesariamente que sea el mismo orden para SparkR:

base::sample(some.r.data.frame) # some.r.data.frame = R data.frame type

Manipulación de datos
Machine Translated by Google

La manipulación de datos en SparkR es conceptualmente igual que la API DataFrame de Spark en otros
idiomas. La principal diferencia está en la sintaxis, en gran parte debido a que ejecutamos código R y no otro idioma.
Las agregaciones, el filtrado y muchas de las funciones que puede encontrar en los otros capítulos a lo largo de
este libro también están disponibles en R. En su mayor parte, puede mirar los nombres de las funciones o
manipulaciones que encuentra a lo largo de este libro y descubrir si están disponibles en SparkR ejecutando ?<nombre­
función>. Esto debería funcionar la gran mayoría de las veces, ya que hay una buena cobertura de las funciones de
SQL estructurado:

?to_date # a la manipulación de la columna Data DataFrame

SQL es en gran medida lo mismo. Podemos especificar comandos SQL que luego podemos manipular como
DataFrames. Por ejemplo, podemos encontrar todas las tablas que contienen la palabra “producción” en ellas:

tbls <­ sql("MOSTRAR TABLAS")

recopilar

( seleccionar ( filtro (tbls, like (tbls$tableName, "%producción%")),


"tableName",
"isTemporary"))

También podemos usar el popular paquete magrittr para hacer que este código sea más legible, aprovechando el
operador de tubería para encadenar nuestras transformaciones en una sintaxis más funcional y legible:

biblioteca (magrittr)

tbls %>%
filter(like(tbls$tableName, "%production%")) %>%
select("tableName", "isTemporary") %>% collect()

Fuentes de datos

SparkR es compatible con todas las fuentes de datos compatibles con Spark, incluidos los paquetes de terceros.
Podemos ver en el siguiente fragmento que simplemente especificamos las opciones usando una sintaxis
ligeramente diferente:

minorista.datos <­ read.df(


"/data/retail­data/all/", "csv",

header="true",
inferSchema="true")
vuelo.datos <­ read.df(
"/data/flight­data/parquet/2010­summary.parquet", "parquet")

Consulte el Capítulo 9 para obtener más información.


Machine Translated by Google

Aprendizaje automático

El aprendizaje automático es una parte fundamental del lenguaje R, así como de Spark. Desde SparkR hay una
disponibilidad decente de algoritmos Spark MLlib. Por lo general, llegan a R una o dos versiones después de que se
introducen en Scala o Python. A partir de Spark 2.1, los siguientes algoritmos son compatibles con SparkR:

spark.glm o glm: modelo lineal generalizado

spark.survreg: modelo de regresión de supervivencia de tiempo de falla acelerado (AFT)

chispa.naiveBayes: modelo Naive Bayes

spark.kmeans: ­significa modelo

spark.logit: modelo de regresión logística

spark.isoreg: modelo de regresión isotónica

spark.gaussianMixture: modelo de mezcla gaussiana

spark.lda: modelo de asignación de Dirichlet latente (LDA)

spark.mlp: modelo de clasificación de perceptrón multicapa

spark.gbt: modelo de árbol potenciado por gradiente para regresión y clasificación

spark.randomForest: modelo de bosque aleatorio para regresión y clasificación

spark.als: modelo de factorización de matriz de mínimos cuadrados alternos (ALS)

chispa.kstest: prueba de Kolmogorov­Smirnov

Debajo del capó, SparkR usa MLlib para entrenar el modelo, lo que significa que casi todo lo que se cubre en la Parte VI

es relevante para los usuarios de SparkR. Los usuarios pueden llamar a summary para imprimir un resumen del modelo ajustado,
predecir para hacer predicciones sobre nuevos datos y write.ml/read.ml para guardar/cargar modelos ajustados. SparkR
admite un subconjunto de los operadores de fórmula R disponibles para el ajuste de modelos, incluidos ~, ., :, + y
­. Aquí hay un ejemplo de cómo ejecutar una regresión simple en el conjunto de datos minorista:

modelo <­ chispa.glm(minorista.datos, Cantidad ~ PrecioUnitario + País,


familia='gaussiana')
resumen(modelo)
predicción(modelo, minorista.datos)

write.ml(model, "/tmp/myModelOutput", overwrite=T)


newModel <­ read.ml("/tmp/myModelOutput")

La API es consistente en todos los modelos, aunque no todos los modelos admiten resultados de resumen detallados como
vimos con glm. Para obtener más información sobre modelos específicos o técnicas de preprocesamiento, consulte los capítulos
correspondientes en la Parte VI.
Machine Translated by Google

Si bien esto palidece en comparación con la extensa colección de algoritmos estadísticos y bibliotecas de análisis de R,
muchos usuarios no requieren la escala que proporciona Spark para el entrenamiento y el uso real de sus algoritmos
de aprendizaje automático. Los usuarios tienen la oportunidad de crear conjuntos de entrenamiento en grandes datos
usando Spark y luego recopilar ese conjunto de datos en su entorno local para entrenar en un data.frame local.

Funciones definidas por el usuario

En SparkR, hay varias formas de ejecutar funciones definidas por el usuario. Una función definida por el usuario es aquella
que se crea en el idioma nativo y se ejecuta en el servidor en ese mismo idioma nativo.
Estos se ejecutan, en su mayor parte, de la misma manera que se ejecuta una UDF de Python, mediante la
serialización dentro y fuera de la JVM de la función.

Los diferentes tipos de UDF que puede definir son los siguientes:

En primer lugar, spark.lapply le permite ejecutar varias instancias de una función en Spark en diferentes valores de parámetros
proporcionados en una colección de R. Esta es una excelente manera de realizar una búsqueda en cuadrícula y comparar
los resultados:

familias <­ c("gaussiana", "poisson") entrenar


<­ función(familia) { modelo <­
glm(Sepal.Length ~ Sepal.Width + Species, iris, family = family) resumen(modelo) }

# Devuelve una lista de resúmenes de


modelos model.summaries <­ spark.lapply(familias, tren)

# Imprime el resumen de cada modelo


print(model.summaries)

En segundo lugar, dapply y dapplyCollect le permiten procesar datos de SparkDataFrame mediante código personalizado.
En particular, estas funciones tomarán cada partición de SparkDataFrame, la convertirán en un marco de datos R dentro
de un ejecutor y luego llamarán a su código R sobre esa partición (representado como un marco de datos R). Luego
devolverán los resultados: un SparkDataFrame para dapply o un data.frame local para dapplyCollect.

Para usar dapply, que devuelve un SparkDataFrame, debe especificar el esquema de salida que resultará de la
transformación para que Spark comprenda qué tipo de datos devolverá. Por ejemplo, el siguiente código le permitirá
entrenar un modelo R local por partición en su SparkDataFrame, suponiendo que particione sus datos de acuerdo
con las claves correctas:

df <­ withColumnRenamed(createDataFrame(as.data.frame(1:100)), "1:100", "col") outputSchema


<­ structType(
structField("columna", "entero"),
structField("nuevaColumna", "doble"))

udfFunc <­ función (remoto.datos.marco)


{ remoto.datos.marco['nuevaColumna'] = remoto.datos.marco$col * 2
Machine Translated by Google

marco.de.datos.remoto

} # genera SparkDataFrame, por lo que requiere un esquema


take(dapply(df, udfFunc, outputSchema), 5) # recopila
todos los resultados en un, por lo que no se requiere esquema. # sin
embargo, esto fallará si el resultado es grande dapplyCollect(df,
udfFunc)

Finalmente, las funciones gapply y gapplyCollect aplican una UDF a un grupo de datos de manera
similar a dapply. De hecho, estos dos métodos son prácticamente iguales, excepto que uno opera en un
SparkDataFrame genérico y el otro se aplica a un DataFrame agrupado. La función gapply aplicará
esta función por grupo y pasando la clave como primer parámetro a la función que defina. De esta
manera, puede estar seguro de tener una función personalizada de acuerdo con cada grupo
en particular:

local <­ as.data.frame(1:100)


local['grupos'] <­ c("a", "b")

df <­ withColumnRenamed(createDataFrame(local), "1:100", "col")

esquemadesalida <­ structType(


structField("columna", "entero"),
structField("grupos", "cadena"),
structField("nuevaColumna", "doble"))

udfFunc <­ function (key, remote.data.frame) { if (key == "a")

{ remote.data.frame['newColumn'] = remote.data.frame$col * 2 } else if (key = =


"b") { remote.data.frame['newColumn']
= remote.data.frame$col * 3 } else if (key == "c") { remote.data.frame['newColumn']
= remoto .data.frame$col * 4

marco.de.datos.remoto

} # genera SparkDataFrame, por lo que requiere una toma de


esquema (gapply
(df, "groups",
udfFunc,
outputSchema), 50)

gapplyCollect(df,
"grupos",
udfFunc)

SparkR seguirá creciendo como parte de Spark; y si está familiarizado con R y un poco de Spark, esta
puede ser una herramienta muy poderosa.
Machine Translated by Google

brillante
sparklyr es un paquete más nuevo del equipo de RStudio basado en el popular paquete dplyr para datos
estructurados. Este paquete es fundamentalmente diferente de SparkR y sus autores adoptan una postura
más obstinada sobre lo que debería hacer la integración entre Spark y R. Esto significa que sparklyr se
deshace de algunos de los conceptos de Spark que están disponibles a lo largo de este libro, como
SparkSession, y utiliza sus propias ideas en su lugar. Para algunos, esto significa que sparklyr adopta un
enfoque R­first en lugar del enfoque de SparkR de coincidir estrechamente con las API de Python y
Scala. Ese enfoque habla de sus orígenes como marco; sparklyr fue creado dentro de la comunidad R por la
gente de RStudio (el popular IDE de R), en lugar de ser creado por la comunidad Spark.
Si el enfoque de sparklyr o SparkR es mejor o peor depende completamente de la preferencia del usuario
final.

En resumen, sparklyr brinda una experiencia mejorada para los usuarios de R familiarizados con
dplyr, con una funcionalidad general levemente menor que SparkR (que puede cambiar con el tiempo).
Específicamente, sparklyr proporciona un backend dplyr completo para Spark, lo que facilita tomar el
código dplyr que ejecuta hoy en su máquina local y distribuirlo. La implicación de la arquitectura de back­
end de dplyr es que las mismas funciones que usa en los objetos data.frame locales se aplican de manera
distribuida a Spark DataFrames distribuidos. En esencia, la ampliación no requiere cambios de código.
Dado que las funciones se aplican tanto a un solo nodo como a DataFrames distribuidos, esta arquitectura
aborda uno de los principales desafíos con SparkR en la actualidad, donde el enmascaramiento de
funciones puede conducir a extraños escenarios de depuración. Además, esta elección arquitectónica
hace que sparklyr sea una transición más fácil que simplemente usar SparkR. Al igual que SparkR,
sparklyr es un proyecto en evolución; y cuando se publique este libro, el proyecto sparklyr habrá
evolucionado aún más. Para obtener la referencia más actualizada, debe visitar el sitio web de sparklyr.
Las siguientes secciones proporcionan una comparación ligera y no profundizarán en este proyecto en
particular. Comencemos con algunos ejemplos prácticos de sparklyr. Lo primero que tenemos que hacer es instalar el paquete:

install.packages("sparklyr")
biblioteca(sparklyr)

Conceptos clave

sparklyr ignora algunos de los conceptos fundamentales que tiene Spark y que discutimos a lo largo
de este libro. Postulamos que esto se debe a que estos conceptos no son familiares (y potencialmente
irrelevantes) para el usuario típico de R. Por ejemplo, en lugar de SparkSession, simplemente existe
spark_connect, que le permite conectarse a un clúster de Spark:

sc <­ chispa_conectar(maestro = "local")

La variable devuelta es una fuente de datos dplyr remota. Esta conexión, aunque se parece a un SparkContext,
no es el mismo SparkContext que mencionamos en este libro. Este es un concepto puramente brillante
que representa una conexión de clúster Spark. Esta función es en gran parte la totalidad
Machine Translated by Google

interfaz sobre cómo definirá las configuraciones que le gustaría usar en su entorno Spark. A través de esta interfaz,
puede especificar configuraciones de inicialización para Spark Cluster como un todo:

spark_connect(maestro = "local", configuración = spark_config())

Esto funciona usando el paquete de configuración en R para especificar las configuraciones que le gustaría establecer en
su clúster de Spark. Estos detalles están cubiertos en la documentación de implementación de sparklyr.

Con esta variable, podemos manipular datos de Spark remotos desde un proceso R local, por lo que el resultado de
spark_connect realiza aproximadamente el mismo rol administrativo para los usuarios finales que un
SparkContext.

Sin tramas de datos

sparklyr ignora el concepto de un tipo SparkDataFrame único. En su lugar, aprovecha las tablas (que aún están
asignadas a DataFrames dentro de Spark) similares a otras fuentes de datos de dplyr y le permite manipularlas. Esto se
alinea más con el flujo de trabajo típico de R, que consiste en usar dplyr y magrittr para definir funcionalmente las
transformaciones desde una tabla de origen. Sin embargo, significa que es posible que no se pueda acceder a algunas
de las API y funciones integradas de Spark a menos que dplyr también las admita.

Manipulación de datos

Una vez que nos conectamos a nuestro clúster, podemos ejecutar todas las funciones y manipulaciones de dplyr disponibles
como si fueran un data.frame de dplyr local. Esta elección de arquitectura brinda a quienes están familiarizados con R la
capacidad de realizar las mismas transformaciones utilizando el mismo código, a escala. Esto significa que no hay nueva
sintaxis o conceptos que los usuarios de R deban aprender.

Si bien sparklyr mejora la experiencia del usuario final de R, tiene el costo de reducir la potencia general disponible
para los usuarios de sparklyr, ya que los conceptos son conceptos de R, no necesariamente conceptos de Spark. Por
ejemplo, sparklyr no admite funciones definidas por el usuario que pueda crear y aplicar en SparkR mediante dapply,
gapply y lapply. A medida que sparklyr continúa madurando, puede agregar este tipo de funcionalidad, pero en el
momento de escribir este artículo, esta capacidad no existe. sparklyr está en desarrollo muy activo y se están agregando
más funciones, así que consulte la página de inicio de sparklyr para obtener más información.

Ejecutando SQL

Si bien hay una integración de Spark menos directa, los usuarios pueden ejecutar código SQL arbitrario contra el clúster
usando la biblioteca DBI correspondiente a casi la misma interfaz SQL que hemos visto en capítulos anteriores:

biblioteca(DBI)
allTables <­ dbGetQuery(sc, "MOSTRAR TABLAS")
Machine Translated by Google

Esta interfaz SQL proporciona una interfaz conveniente de nivel inferior para SparkSession. Por ejemplo, los
usuarios pueden usar la interfaz de DBI para establecer propiedades específicas de Spark SQL en el clúster de Spark:

setShufflePartitions <­ dbGetQuery(sc, "SET spark.sql.shuffle.partitions=10")

Desafortunadamente, ni DBI ni spark_connect le brindan una interfaz para configurar propiedades específicas de
Spark, que tendrá que especificar cuando se conecte a su clúster.

Fuentes de datos

Los usuarios pueden aprovechar muchas de las mismas fuentes de datos disponibles en Spark usando
sparklyr. Por ejemplo, debería poder crear declaraciones de tabla utilizando fuentes de datos arbitrarias. Sin
embargo, solo los formatos CSV, JSON y Parquet son compatibles como ciudadanos de primera clase utilizando
las siguientes definiciones de función:

spark_write_csv(tbl_name, ubicación)
spark_write_json(tbl_name, ubicación)
spark_write_parquet(tbl_name, ubicación)

Aprendizaje automático

sparklyr también es compatible con algunos de los algoritmos básicos de aprendizaje automático que vimos en
capítulos anteriores. Una lista de los algoritmos admitidos (en el momento de escribir este artículo) incluye:

ml_kmeans: ­significa agrupación

ml_linear_regression: regresión lineal

ml_logistic_regression: regresión logística

ml_survival_regression: Regresión de supervivencia

ml_generalized_linear_regression: regresión lineal generalizada

ml_decision_tree: árboles de decisión

ml_random_forest: bosques aleatorios

ml_gradient_boosted_trees: árboles potenciados por degradado

ml_pca: análisis de componentes principales

ml_naive_bayes: Naive­Bayes

ml_multilayer_perceptron: perceptrón multicapa

ml_lda: Asignación latente de Dirichlet

ml_one_vs_rest: uno contra resto (lo que le permite convertir un clasificador binario en un clasificador
multiclase)
Machine Translated by Google

Sin embargo, el desarrollo continúa, así que consulte MLlib para obtener más información.

Conclusión
SparkR y sparklyr son áreas de rápido crecimiento en el proyecto Spark, así que visite sus sitios web para
conocer las últimas actualizaciones sobre cada uno. Además, todo el proyecto Spark continúa creciendo a
medida que nuevos miembros, herramientas, integraciones y paquetes se unen a la comunidad. El próximo
capítulo tratará sobre la comunidad de Spark y algunos de los otros recursos disponibles para usted.
Machine Translated by Google

Capítulo 33. Ecosistema y comunidad

Uno de los puntos de venta más importantes de Spark es el gran volumen de recursos, herramientas y colaboradores. En
el momento de escribir este artículo, hay más de 1000 colaboradores en el código base de Spark. Esto es mucho más
de lo que la mayoría de los otros proyectos sueñan con lograr y es un testimonio de la increíble comunidad de Spark,
tanto en términos de colaboradores como de administradores. El proyecto Spark no muestra signos de
desaceleración, ya que las empresas grandes y pequeñas buscan unirse a la comunidad. Este entorno ha estimulado
una gran cantidad de proyectos que complementan y amplían las funciones de Spark, incluidos los paquetes formales
de Spark y las extensiones informales que los usuarios pueden usar en Spark.

Paquetes de chispa
Spark tiene un repositorio de paquetes para paquetes específicos de Spark: Spark Packages. Estos paquetes se
analizaron en los Capítulos 9 y 24. Los paquetes Spark son bibliotecas para aplicaciones Spark que se pueden compartir
fácilmente con la comunidad. GraphFrames es un ejemplo perfecto; hace que el análisis de gráficos esté disponible en
las API estructuradas de Spark de una manera mucho más fácil de usar que el nivel inferior (GraphX)
API integrada en Spark. Hay muchos otros paquetes, incluidos muchos de aprendizaje automático y aprendizaje
profundo, que aprovechan Spark como núcleo y amplían su funcionalidad.

Más allá de estos paquetes de análisis avanzados, existen otros para resolver problemas en verticales particulares.
La atención médica y la genómica han visto un aumento en las oportunidades para las aplicaciones de big
data. Por ejemplo, el Proyecto ADAM aprovecha las optimizaciones internas únicas del motor Catalyst de Spark para
proporcionar una API y una CLI escalables para el procesamiento del genoma. Otro paquete, Hail, es un marco
escalable de código abierto para explorar y analizar datos genómicos. A partir de datos de secuenciación o microarrays
en VCF y otros formatos, Hail proporciona algoritmos escalables para permitir el análisis estadístico de datos a
escala de gigabytes en una computadora portátil o datos a escala de terabytes en un clúster.

En el momento de escribir este artículo, hay casi 400 paquetes diferentes para elegir. Como usuario, puede especificar
paquetes de Spark como dependencias en sus archivos de compilación (como se ve en el repositorio de GitHub del
libro de este libro). También puede descargar los archivos jar preconstruidos e incluirlos en su ruta de clase sin agregarlos
explícitamente a su archivo de compilación. Los paquetes de Spark también se pueden incluir en tiempo de
ejecución pasando un parámetro a las herramientas de línea de comandos spark­shell o spark­submit.

Una lista abreviada de paquetes populares


Como se mencionó, hay casi 400 paquetes Spark. Incluir todo esto no es relevante para usted como usuario porque
puede buscar paquetes específicos en el sitio web de paquetes de Spark. Sin embargo, vale la pena mencionar algunos
de los paquetes más populares:

Conector Spark Cassandra

Este conector lo ayuda a ingresar y sacar datos de la base de datos de Cassandra.


Machine Translated by Google

Conector Spark Redshift

Este conector lo ayuda a ingresar y sacar datos de la base de datos de Redshift.

Spark bigquery

Este conector lo ayuda a ingresar y sacar datos de BigQuery de Google.

chispa avro

Este paquete le permite leer y escribir archivos Avro.

Elasticsearch

Este paquete le permite ingresar y sacar datos de Elasticsearch.

Magallanes

Le permite realizar análisis de datos geoespaciales sobre Spark.

Marcos de gráficos

Le permite realizar análisis de gráficos con DataFrames.

Encienda el aprendizaje profundo

Le permite aprovechar Deep Learning y Spark juntos.

Uso de paquetes Spark


Hay dos formas principales de incluir Spark Packages en sus proyectos. En Scala o Java, puede incluirlo como una
dependencia de compilación, o también puede especificar sus paquetes en tiempo de ejecución (para Python o R).
Repasemos las formas en que puede incluir esta información.

en la escala

Incluir el siguiente solucionador en su archivo build.sbt le permitirá incluir paquetes de Spark como dependencias. Por
ejemplo, podemos agregar este resolver:

// nos permite incluir solucionadores de


paquetes Spark += "bintray­spark­packages" en
"https://dl.bintray.com/spark­packages/maven/"

Ahora que agregamos esta línea, podemos incluir una dependencia de biblioteca para nuestro paquete Spark:

bibliotecaDependencias ++= Seq(


...
// chispa paquetes
"graphframes" % "graphframes" % "0.4.0­spark2.1­s_2.11",

)
Machine Translated by Google

Esto es para incluir la biblioteca GraphFrames. Existen ligeras diferencias de versiones entre los paquetes, pero
siempre puede encontrar esta información en el sitio web de paquetes de Spark.

en pitón

Al momento de escribir este artículo, no hay una forma explícita de incluir un paquete Spark como una dependencia en
un paquete de Python. Este tipo de dependencias debe establecerse en tiempo de ejecución.

En tiempo de ejecución

Vimos cómo podemos especificar paquetes de Spark en paquetes de Scala, pero también podemos incluir estos
paquetes en tiempo de ejecución. Esto es tan simple como incluir un nuevo argumento en spark­shell y spark­submit
que usaría para ejecutar su código.

Por ejemplo, para incluir la biblioteca magallanes:

$SPARK_HOME/bin/spark­shell ­­paquetes áspero2010:magallanes:1.0.4­s_2.11

Paquetes Externos
Además de los paquetes Spark formales, hay una serie de paquetes informales que se basan en las capacidades de
Spark o las aprovechan. Un buen ejemplo es el popular marco de árbol de decisión potenciado por gradientes XGBoost,
que utiliza Spark para programar el entrenamiento distribuido en particiones individuales. Varios de estos son
proyectos públicos con licencia liberal disponibles en GitHub. Usar su motor de búsqueda favorito es una excelente
manera de descubrir proyectos que ya existen, en lugar de tener que escribir los suyos propios.

Comunidad
Spark tiene una comunidad grande y sólida. Es mucho más grande que los paquetes y las contribuciones
directas. El ecosistema de usuarios finales que integran Spark en sus productos y escriben tutoriales es un grupo en
constante crecimiento. Al momento de escribir este artículo, hay más de 1000 contribuyentes al repositorio en Github.

El sitio web oficial de Spark mantiene la información más actualizada de la comunidad, incluidas listas de correo,
propuestas de mejora y responsables de proyectos. Este sitio web también incluye muchos recursos sobre nuevas
versiones de Spark, documentación y notas de la versión para la comunidad.

cumbre chispa
Spark Summits son eventos que ocurren en todo el mundo en varias épocas del año. Este es el evento canónico
para las charlas relacionadas con Spark, donde miles de usuarios finales y desarrolladores asisten a estas cumbres para
conocer las últimas novedades de Spark y conocer los casos de uso. Hay cientos de pistas y cursos de formación a lo
largo de varios días. En 2016, hubo tres eventos: Nueva York (Spark Summit East), San Francisco (Spark Summit
West) y Amsterdam (Spark Summit Europe). En 2017, hubo Spark Summits en Boston, San Francisco y Dublín.
Machine Translated by Google

Próximamente en 2018, y más allá, habrá aún más eventos. Obtenga más información en el sitio web de Spark
Summit.

Hay cientos de videos de Spark Summit disponibles gratuitamente para aprender sobre casos de uso, el
desarrollo de Spark y estrategias y tácticas que puede usar para aprovechar Spark al máximo.
Puede buscar charlas y videos históricos de Spark Summit en el sitio web.

Reuniones locales
Hay muchos grupos de reunión relacionados con Spark en meetup.com. La Figura 33­1 muestra un mapa de
reuniones relacionadas con Spark en Meetup.com.

Figura 33­1. Mapa de reunión de Spark

El “grupo de reunión oficial” de Spark en el Área de la Bahía (fundado por uno de los autores de este libro) se puede
encontrar aquí. Sin embargo, hay más de 600 reuniones relacionadas con Spark en todo el mundo, con un total de casi
350 000 miembros. Estas reuniones continúan surgiendo y creciendo, así que asegúrese de encontrar una en su área.

Conclusión
Este capítulo vertiginoso analizó los recursos no técnicos que Spark pone a su disposición. Un hecho importante
es que uno de los mayores activos de Spark es la comunidad de Spark. Estamos muy orgullosos de la participación
de la comunidad en el desarrollo de Spark y nos encanta saber qué empresas, instituciones académicas e individuos
construyen con Spark.

¡Esperamos sinceramente que haya disfrutado de este libro y esperamos verlo en un Spark Summit!
Machine Translated by Google

Índice

simbolos

­­jars argumento de línea de comandos, envío de aplicaciones

./bin/pyspark, Iniciar la consola de Python, Iniciar Spark

./bin/spark­shell, Iniciar la consola de Scala, Iniciar Spark

Operador =!=, Concatenación y adición de filas (Unión), Trabajo con booleanos

== (igual a) expresión, Trabajar con booleanos


`
(comilla grave), caracteres reservados y palabras clave

Tiempo de falla acelerada (AFT), Regresión de supervivencia (Tiempo de falla acelerada)

acumuladores

ejemplo basico, ejemplo basico­ ejemplo basico

personalizado, acumuladores personalizados

descripción general de, variables compartidas distribuidas, acumuladores

reconocimientos, Agradecimientos

acciones, Acciones

Proyecto ADAM, Paquetes Spark

análisis avanzado (ver aprendizaje automático y análisis avanzado)

función agregada, agregado

Función agregada por clave, agregada por clave

AggregationBuffer, funciones de agregación definidas por el usuario

agregaciones

función agregada, agregado

Función agregada por clave, agregada por clave


Machine Translated by Google

funciones de agregación, funciones de agregación : agregación a tipos complejos

función CombineByKey, combineByKey

contar por clave, contar por clave

depuración, agregaciones lentas

función plegar por clave, plegar por clave

grupoPorClave, grupoPorClave

Agrupación, Agrupación­Agrupación con Mapas

conjuntos de agrupación, conjuntos de agrupación ­pivote

en tipos complejos, agregando a tipos complejos

resumen de, Agregaciones­Agregaciones

ajuste de rendimiento, agregaciones

en RDD, Agregaciones­foldByKey

reducirPorClave, reducirPorClave

en API de transmisión estructurada, agregaciones

Funciones de agregación definidas por el usuario (UDAF), Funciones de agregación definidas por el usuario

funciones de ventana, Funciones de ventana ­ Conjuntos de agrupación

alertas y notificaciones, Notificaciones y alertas, Alertas

Mínimos cuadrados alternos (ALS), métricas de clasificación de casos de uso

análisis (ver aprendizaje automático y análisis avanzado)

fase del analizador en Spark SQL, planificación lógica

Detección de anomalías

a través del análisis gráfico, Graph Analytics

a través del aprendizaje no supervisado, Aprendizaje no supervisado

anti uniones, Anti uniones izquierdas

Apache Hadoop, Filosofía de Apache Spark, Compresión y tipos de archivos divisibles, Hadoop
archivos
Machine Translated by Google

Apache Hive, Big Data y SQL: Apache Hive, The SparkContext, Varios
Consideraciones

Apache Maven, una aplicación simple basada en Scala

Apache Mesos, la arquitectura de una aplicación Spark, implementación de Spark, Spark en Mesos

Apache Spark (ver también Aplicaciones Spark; Spark SQL)

acciones, Acciones

Selección de API, ¿Qué API de Spark usar?

arquitectura de, arquitectura básica de Spark

beneficios de, ¿ Qué es Apache Spark?, Contexto: El problema de Big Data, Conclusión, Ecosistema y Comunidad

Construyendo Spark desde la fuente, Construyendo Spark desde la fuente

sensibilidad a mayúsculas y minúsculas, Sensibilidad a mayúsculas y minúsculas

implementación en la nube de Spark in the Cloud

(ver también implementación)

marcos de datos, marcos de datos

descargar, Descargar Spark localmente

ecosistema de paquetes y herramientas, Spark's Ecosystem and Packages, Spark Packages­External

Paquetes

centrarse en la computación, la filosofía de Apache Spark

modelo de programación funcional subyacente, Historia de Spark, un ejemplo de extremo a extremo

API fundamentales de las API de Spark

historia de, Prefacio, Historia de Spark

naturaleza interactiva de, Historia de Spark

representaciones de tipos internos, Spark Types­Spark Types

API de idioma, API de idioma de Spark

lanzamiento de consolas interactivas, lanzamiento de consolas interactivas de Spark

bibliotecas compatibles con la filosofía de Apache Spark


Machine Translated by Google

administración de versiones de Spark, consideraciones varias, actualización de su versión de Spark

filosofía de, la filosofía de Apache Spark

mejoras recientes al presente y futuro de Spark

caracteres reservados y palabras clave, caracteres reservados y palabras clave

ejecutando, Ejecutando Spark, Ejecutando Spark en la nube, Cómo se ejecuta Spark en un clúster
Conclusión

Interfaz de usuario de Spark, interfaz de usuario de Spark

chispa de arranque

componentes y bibliotecas del kit de herramientas, ¿ Qué es Apache Spark?, Un recorrido por el conjunto de herramientas de Spark : el

ecosistema y los paquetes de Spark

temas tratados, Prefacio

ejemplo de transformación de extremo a extremo, un ejemplo de extremo a extremo ­DataFrames y SQL

conceptos básicos de transformaciones, Transformaciones

naturaleza unificada de la filosofía de Apache Spark

agregar modo de salida, modo agregar

propiedades de la aplicación, Propiedades de la aplicación, Propiedades de la aplicación para YARN

aplicaciones (streaming estructurado) (consulte también aplicaciones de producción; aplicaciones Spark;

API de transmisión estructurada)

alertando, alertando

monitorización, métricas y monitorización­Spark UI

dimensionamiento y reescalado, dimensionamiento y reescalado de su aplicación

Supervisión de Stream Listener, supervisión avanzada con Streaming Listener

actualización, Actualización de su aplicación

aproximaciones, agregaciones

método approxQuantile, trabajar con números

función approx_count_distinct, approx_count_distinct

arreglos, arreglos­explotar
Machine Translated by Google

matriz_contiene, matriz_contiene

ejecución de trabajos asincrónicos, tiempos de espera

atomicidad, resiliencia en la salida y atomicidad

atribuciones, uso de ejemplos de código

ajuste automático de modelos, evaluadores para clasificación y ajuste automático de modelos, evaluadores y ajuste automático de

modelos

promedio, calcular, promediar

Avro, una lista abreviada de paquetes populares

carácter de acento grave (`), caracteres reservados y palabras clave

procesamiento por lotes, ¿ Qué es el procesamiento de flujo?, Duración del lote

Datos de bicicletas compartidas del área de la bahía, análisis gráfico

Gran DL, Gran DL

paquete bigquery, una lista abreviada de paquetes populares

clasificación binaria, clasificación, clasificación binaria

agrupamiento, agrupamiento

(ver también cubetas)

bisectriz ­means, bisectriz k­means Resumen

Booleanos, Trabajar con Booleanos

búsqueda primero en amplitud, búsqueda primero en amplitud

uniones de difusión, Estrategias de comunicación

variables de difusión, Variables compartidas distribuidas ­ Variables de difusión, Variables de difusión

agrupamiento, agrupamiento, agrupamiento, agrupamiento­ Técnicas avanzadas de agrupamiento

lógica de negocios, resiliencia y evolución de la lógica de negocios

ByKey, conceptos básicos de valor­clave (RDD de valor­clave)

C
Machine Translated by Google

almacenamiento en caché, almacenamiento temporal de datos (almacenamiento en caché) ­Almacenamiento temporal de datos (almacenamiento en caché)

fechas del calendario, Trabajar con fechas y marcas de tiempo­Trabajar con fechas y marcas de tiempo

mayúsculas, Trabajando con cadenas

Productos cartesianos, uniones cruzadas (cartesianas)

clases de casos, en Scala: clases de casos, conjuntos de datos y RDD de clases de casos

sensibilidad a mayúsculas y minúsculas, Sensibilidad a mayúsculas y minúsculas

case…when…then…end declaraciones de estilo, case…when…then Declaraciones

Cassandra Connector, conclusión, implementaciones de clústeres locales, una lista abreviada de

Paquetes Populares

casting, Cambiar el tipo de una columna (cast)

catálogo, Planificación Lógica, Consideraciones Misceláneas

Catálogo (Spark SQL), Catálogo, Consideraciones varias

Motor de cálculo de Catalyst, descripción general de los tipos de Spark estructurados, paquetes de Spark

características categóricas, preprocesamiento, Trabajar con características categóricas­ Transformadores de datos de texto

centroide, Machine Learning y Advanced Analytics, k­means

(ver también ­significa)

puntos de control, puntos de control, tolerancia a fallas y puntos de control, componentes conectados

Selector de chi­cuadrado, selección de funciones

clasificación

árboles de decisión, árboles de decisión : parámetros de predicción

evaluadores y ajuste del modelo de automatización, evaluadores para clasificación y modelo de automatización

Afinación

regresión logística, resumen del modelo de regresión logística

métricas, métricas de evaluación detalladas

modelos en MLlib, modelos de clasificación en MLlib­Model Scalability

clasificador de perceptrón multicapa, soporte de red neuronal MLlib

Bayesiano ingenuo, bayesiano ingenuo


Machine Translated by Google

Clasificador uno contra resto, uno contra resto

bosques aleatorios y árboles potenciados por degradado, bosques aleatorios y árboles potenciados por degradado
Parámetros de predicción

a través del análisis gráfico, Graph Analytics

tipos de, tipos de clasificación

casos de uso para, clasificación, casos de uso

modo cliente, modo cliente

solicitudes de clientes, solicitud de cliente

implementación en la nube, Spark en la nube

(ver también implementación)

administradores de clústeres (ver también clústeres)

administradores de clústeres, Spark en YARN: propiedades de la aplicación para YARN

Mesos, Chispa en Mesos

descripción general de la arquitectura de una aplicación Spark

propósito de la arquitectura básica de Spark

seleccionando, Implementando Spark

Administradores de clúster independientes : envío de aplicaciones

modo de grupo, modo de grupo

clústeres (ver también administradores de clústeres)

configuraciones de red de clúster, configuraciones de red de clúster

creación, Lanzamiento

definida, arquitectura básica de Spark

monitoreo, El panorama de monitoreo

clústeres locales, implementaciones de clústeres locales

ajuste de rendimiento, configuraciones de clúster

configuración de dimensionamiento y uso compartido, dimensionamiento y uso compartido de clústeres/aplicaciones


Machine Translated by Google

coalesce, Repartition and Coalesce, Coalesce, coalesce, Repartitioning and Coalescing

ejemplos de código, obtención y uso, uso de ejemplos de código, datos utilizados en este libro

Cogrupos, Cogrupos

función col, Columnas

problema de arranque en frío, filtrado colaborativo con mínimos cuadrados alternos, parámetros de predicción

filtrado colaborativo, Métricas de clasificación de casos de uso

método de recopilación, conjuntos de datos: API estructuradas con seguridad de tipos, recopilación de filas para el controlador, acciones

función collect_list, Listas

función collect_set, Listas

función de columna, Columnas

columnas

accediendo, Accediendo a las columnas de un DataFrame

agregando, agregando columnas

sensibilidad a mayúsculas y minúsculas, Sensibilidad a mayúsculas y minúsculas

cambiando el tipo (cast), cambiando el tipo de una columna (cast)

convertir filas a, Pivote

explotando, explotar

creación de instancias, tipos de chispa

métodos de localización, dónde buscar API

manipular con Select y SelectExpr, select y selectExpr

Metadatos de la columna MLlib, propiedades del transformador

resumen de, Columnas

eliminación, Eliminación de columnas

renombrar, renombrar columnas

caracteres reservados y palabras clave, caracteres reservados y palabras clave

trabajar con Spark, Columnas y Expresiones


Machine Translated by Google

función CombineByKey, combineByKey

comentarios y preguntas, Cómo contactarnos

palabras comunes, eliminación, Eliminación de palabras comunes

comunidad, ecosistema y paquetes de Spark

operador de comparación (=!=), concatenación y adición de filas (unión), trabajo con valores booleanos

modo de salida completo, modo completo

tipos complejos, columnas, escribir tipos complejos, tipos complejos ­listas

formatos de compresión, Tipos de archivos divisibles y compresión, Tipos de archivos divisibles y compresión

motores informáticos, ¿ Qué es Apache Spark?, Filosofía de Apache Spark

concatenación, Concatenación y Anexión de Filas (Unión)

archivo conf/slaves, secuencias de comandos de inicio de clúster

opciones de configuración

propiedades de la aplicación, Propiedades de la aplicación

variables ambientales, Variables Ambientales

propiedades de ejecución, Propiedades de ejecución

programación de trabajos, Programación de trabajos dentro de una aplicación

administración de memoria, Configuración de la administración de memoria

Descripción general de Configuración de aplicaciones

propiedades de tiempo de ejecución, propiedades de tiempo de ejecución

comportamiento aleatorio, Configuración del comportamiento aleatorio

SparkConf, La SparkConf

algoritmo de componentes conectados, componentes conectados

sumidero de consola, fuentes y sumideros para pruebas

consola, lanzamiento interactivo, Lanzamiento de consolas interactivas de Spark

aplicaciones continuas, ¿Qué es el procesamiento de transmisión?, Conceptos básicos de transmisión estructurada


Machine Translated by Google

funciones continuas, preprocesamiento de, Trabajar con funciones continuas­Normalizador

sistemas basados en procesamiento continuo, ejecución continua frente a microlotes

subconsultas predicadas correlacionadas, subconsultas predicadas correlacionadas

subconsultas correlacionadas, Subconsultas

correlación, computación, Trabajar con números, covarianza y correlación

optimizaciones basadas en costos, recopilación de estadísticas

(ver también ajuste de rendimiento)

contar acción, Agregaciones, Acciones, contar

ventanas basadas en conteo, mapGroupsWithState­mapGroupsWithState

recuento aproximado, recuento aproximado

recuentoAproximadoDistinto, recuentoAproximadoDistinto

countByKey, Agregaciones

contar por valor, contar por valor

recuento por valor aproximado, recuento por valor aproximado

cuentaDistinct función, cuentaDistinct

contar, trabajar con números

CountVectorizer, conversión de palabras en representaciones numéricas

covarianza, cálculo, covarianza y correlación

Sentencia CREATE EXTERNAL TABLE, Creación de tablas externas

uniones cruzadas (cartesianas), uniones cruzadas (cartesianas)

Archivos CSV (valores separados por comas)

Opciones del lector CSV, Archivos CSV ­Lectura de archivos CSV

leyendo, Lectura de archivos CSV

escritura, Escritura de archivos CSV

operador de cubo, Cubo

maldición de la dimensionalidad, aprendizaje no supervisado


Machine Translated by Google

localidad de datos, localidad de datos

fuentes de datos

Fuentes de datos creadas por la comunidad

Archivos CSV, Archivos CSV ­Escribir archivos CSV

estructura de las API de fuentes de datos, la estructura de las API de fuentes de datos: modos de guardado

descargar datos utilizados en este libro, Datos utilizados en este libro

Archivos JSON, Archivos JSON ­Archivos Parquet

administrar el tamaño del archivo, administrar el tamaño del archivo

archivos ORC, archivos ORC

Archivos de parquet, Archivos de parquet ­Escribir archivos de parquet

leyendo datos en paralelo, leyendo datos en paralelo

Núcleo de Spark, fuentes de datos

formatos de archivo divisibles, tipos de archivos divisibles y compresión

Bases de datos SQL, Bases de datos SQL­ Archivos de texto

en la API de transmisión estructurada, fuentes de entrada, dónde se leen y escriben los datos (fuentes y

Sumideros): lectura de Kafka Source, fuentes y sumideros para pruebas

archivos de texto, archivos de texto

escribir tipos complejos, escribir tipos complejos

escribir datos en paralelo, escribir datos en paralelo

datos, limpieza, limpieza de datos

datos, lectura

conceptos básicos de, conceptos básicos de la lectura de datos

estructura de la API principal, estructura de la API de lectura

Archivos CSV, Lectura de archivos CSV

depuración, lecturas y escrituras lentas


Machine Translated by Google

en paralelo, Lectura de datos en paralelo

modo de lectura, Modos de lectura

datos, almacenamiento

baldeando, baldeando

recopilación de estadísticas, recopilación de estadísticas

localidad de datos, localidad de datos

Almacenamiento de datos a largo plazo basado en archivos a largo plazo

número de archivos, el número de archivos

compresión y tipos de archivos divisibles, compresión y tipos de archivos divisibles

particionamiento de tablas, particionamiento de tablas

temporal (almacenamiento en caché), Almacenamiento temporal de datos (almacenamiento en caché)­Almacenamiento temporal de datos (almacenamiento en caché)

datos, tipos de

arreglos, arreglos­explotar

Booleanos, Trabajar con Booleanos

conversión a tipos Spark, conversión a tipos Spark

fechas y marcas de tiempo, Trabajar con fechas y marcas de tiempo­Trabajar con fechas y

Marcas de tiempo

Datos JSON, Trabajar con JSON

localización de transformaciones, dónde buscar API

mapas, mapas

valores nulos, Trabajo con valores nulos en la ordenación de datos, Signos y síntomas, Formateo de modelos

Según su caso de uso

números, Trabajando con Números­Trabajando con Números

ordenar valores nulos, ordenar

cadenas, Trabajando con Cadenas­ Expresiones Regulares

estructuras, estructuras

funciones definidas por el usuario (UDF), funciones definidas por el usuario ­funciones definidas por el usuario, agregación
Machine Translated by Google

a tipos complejos, funciones definidas por el usuario

datos, actualización en tiempo real, Actualizar datos para servir en tiempo real

datos, escritura

conceptos básicos de, conceptos básicos de escritura de datos

estructura de la API principal, estructura de la API de escritura

depuración, lecturas y escrituras lentas

en paralelo, Escritura de datos en paralelo

modo de guardado, Modos de guardado

bases de datos (Spark SQL) (ver también bases de datos SQL)

creando, Creando Bases de Datos

soltando, Soltando Bases de Datos

OLTP frente a OLAP, Big Data y SQL: Spark SQL

descripción general de, bases de datos

configuración, Configuración de la base de datos

Databricks Community Edition, Running Spark, Running Spark en la nube, El desarrollo

Proceso, Spark en la Nube

Submódulo DataFrameNaFunctions, dónde buscar las API

DataFrameReader, un ejemplo completo, conceptos básicos de la lectura de datos

marcos de datos

operaciones estructuradas básicas, operaciones estructuradas básicas ­conclusión

conceptos básicos de DataFrames, DataFrames y Datasets

componentes de Operaciones Estructuradas Básicas

creando, Creando marcos de datos

creación a partir de RDD, interoperabilidad entre marcos de datos, conjuntos de datos y RDD

frente a conjuntos de datos, tramas de datos frente a conjuntos de datos, tramas de datos frente a SQL frente a conjuntos de datos frente a

RDD

localizar métodos y funciones, Dónde buscar API


Machine Translated by Google

manipulando, Creando DataFrames­Conclusión

transmisión, transmisión estructurada en acción

Submódulo DataFrameStatFunctions, dónde buscar las API

DataFrameWriter, conceptos básicos de escritura de datos

conjuntos de datos

acciones, Acciones

beneficios de, Conjuntos de datos: API estructuradas con seguridad de tipos, Cuándo usar conjuntos de datos

creación, creación de conjuntos de datos

creando desde RDD, interoperando entre tramas de datos, conjuntos de datos y RDD

vs. DataFrames, DataFrames versus conjuntos de datos

filtrado, filtrado

Agrupaciones y Agregaciones, Agrupaciones y Agregaciones

se une, se une

métodos de localización, dónde buscar API

cartografía, cartografía

descripción general de Conjuntos de datos: API estructuradas con seguridad de tipos, Marcos de datos y conjuntos de datos, Conjuntos de datos

transmisión, API de conjunto de datos de transmisión

transformaciones, transformaciones

Cuándo usar, Cuándo usar conjuntos de datos, ¿ Qué API de Spark usar?, Marcos de datos frente a SQL frente a conjuntos de
datos frente a RDD

fechas y marcas de tiempo, Trabajar con fechas y marcas de tiempo­Trabajar con fechas y

Marcas de tiempo

deduplicación, eliminación de duplicados en un flujo

depuración (ver monitoreo y depuración)

toma de decisiones, en tiempo real, toma de decisiones en tiempo real

árboles de decisión
Machine Translated by Google

aplicado a la clasificación, árboles de decisión ­parámetros de predicción

aplicado a la regresión, árboles de decisión

ejemplo, Parámetros de predicción

hiperparámetros del modelo, hiperparámetros del modelo

descripción general de, árboles de decisión

parámetros de predicción, parámetros de predicción

parámetros de entrenamiento, parámetros de entrenamiento

API declarativas, Record­at­a­Time versus API declarativas

aprendizaje profundo

Gran DL, Gran DL

Canalizaciones de aprendizaje profundo, canalizaciones de aprendizaje profundo : conclusión

DeepLearning4j, DeepLearning4J

Compatibilidad con redes neuronales MLlib, compatibilidad con redes neuronales MLlib

descripción general de ¿ Qué es el aprendizaje profundo?

en Spark, Maneras de usar el aprendizaje profundo en Spark­Maneras de usar el aprendizaje profundo en Spark

TensorFlowOnSpark, TensorFlowOnSpark

TensorFrames, TensorFrames

Aprendizaje profundo (Goodfellow), breve introducción a la analítica avanzada

Canalizaciones de aprendizaje profundo, canalizaciones de aprendizaje profundo : conclusión

redes neuronales profundas, ¿Qué es el aprendizaje profundo?

DeepLearning4j, DeepLearning4J

palabras vacías predeterminadas, Eliminación de palabras comunes

dependencias, Transformaciones

despliegue

programación de aplicaciones, programación de aplicaciones

configuraciones de red de clúster, configuraciones de red de clúster


Machine Translated by Google

servicio de barajado externo, consideraciones varias

consideraciones de registro, Consideraciones varias, El panorama de monitoreo

administrar versiones de Spark, consideraciones varias

Mesos, Chispa en Mesos

Metastores, Consideraciones misceláneas

monitoreo, Consideraciones Misceláneas

número y tipo de solicitudes, Consideraciones Misceláneas

Descripción general de Implementación de Spark

configuraciones de implementación segura, Configuraciones de implementación segura

administrador de clúster independiente, administradores de clúster : envío de aplicaciones

dónde implementar, Dónde implementar su clúster para ejecutar Spark Applications­Spark en el


Nube

Framework YARN, Spark en YARN: propiedades de la aplicación para YARN

describir el método, Trabajando con Números

proceso de desarrollo, el proceso de desarrollo

plantilla de desarrollo, desarrollo de aplicaciones de Spark, registros de Spark

(ver también aplicaciones Spark)

dimensionalidad, maldición de, aprendizaje no supervisado

gráfico acíclico dirigido (DAG), marcos de datos y SQL, columnas como expresiones, la interfaz de usuario de Spark

gráficos dirigidos, análisis de gráficos, construcción de un gráfico

discos, errores de falta de espacio, signos y síntomas

método distinto, distinto

colecciones distribuidas, The SparkSession

variables compartidas distribuidas

acumuladores, Acumuladores­ Cumuladores personalizados

variables de difusión, Variables de difusión ­ Variables de difusión


Machine Translated by Google

procesamiento de flujo distribuido, conceptos básicos

DLB (consulte Aprendizaje profundo (Goodfellow))

fuentes de datos dplyr, sparklyr

procesos de controlador, Aplicaciones de Spark, Recopilación de filas para el controlador, La arquitectura de un Spark

Aplicación, solicitud de cliente, procesos de controlador y ejecutor, controlador OutOfMemoryError o

El controlador no responde, especificaciones del idioma: Python (PySpark) y R (SparkR y sparklyr)

soltar función, soltar

Biblioteca de métricas de Dropwizard, procesos de controlador y ejecución

Utilidad dstat, El panorama de monitoreo

API de DStreams, Fundamentos del procesamiento de transmisiones, API de DStream, Hora del evento, Manejo tardío
Datos con marcas de agua

duplicados, eliminación, Colocación de duplicados en una secuencia

asignación dinámica, programación de aplicaciones, asignación dinámica

mi

nodos perimetrales, modo cliente

bordes, análisis gráfico

Modelo ElasticNet, Regresión Lineal

Elasticsearch, una lista abreviada de paquetes populares

Elements of Statistical Learning (Hastie), breve introducción a la analítica avanzada

ElementwiseProduct, ElementwiseProduct

datos vacíos, Trabajar con valores nulos en los datos

aplicaciones de extremo a extremo, ¿ Qué es el procesamiento de transmisión?, Transmisión estructurada, Conceptos básicos

de transmisión estructurada

variables ambientales, Variables Ambientales

funciones epónimas, asimetría y curtosis

expresión igual a (==), Trabajar con booleanos

errores (ver también monitoreo y depuración)


Machine Translated by Google

antes de la ejecución, Errores antes de la ejecución

durante la ejecución, errores durante la ejecución

no queda espacio en errores de disco, signos y síntomas

OutOfMemoryError, Driver OutOfMemoryError o Driver Unresponsive­Posible tratamientos

errores de serialización, signos y síntomas

ESL (ver Elementos del aprendizaje estadístico (Hastie))

estimadores, Conceptos de MLlib de alto nivel, Estimadores, Estimadores para preprocesamiento

evaluadores, conceptos de MLlib de alto nivel, capacitación y evaluación­capacitación y evaluación,

Evaluadores para Clasificación y Automatización de Modelo Tuning, Evaluadores y Automatización de Modelo

Tuning, Evaluadores para Recomendación

registros de eventos, servidor de historial de interfaz de usuario de Spark

tiempo de evento, tiempo de evento versus tiempo de procesamiento

procesamiento de tiempo de evento

conceptos básicos de, conceptos básicos de tiempo de eventos

beneficios de Spark for, Event­Time y Stateful Processing

Procesamiento definido, de tiempo de evento y con estado

Soltar duplicados, Soltar duplicados en una secuencia

hora del evento definida, hora del evento

ejemplo, hora del evento

ideas clave, procesamiento de tiempo de eventos

manejo de datos tardíos, Manejo de datos tardíos con marcas de agua­Manejo de datos tardíos con
marcas de agua

tiempo de procesamiento definido, tiempo de evento

ventanas, ventanas en tiempo de eventos: manejo de datos tardíos con marcas de agua

modos de ejecución, modos de ejecución

plan de ejecución, DataFrames y SQL, Instrucciones Lógicas

propiedades de ejecución, Propiedades de ejecución


Machine Translated by Google

ejecución, de aplicaciones Spark, Ejecución

procesos ejecutores, aplicaciones Spark, la arquitectura de una aplicación Spark, controlador y

Procesos del ejecutor, Executor OutOfMemoryError o Executor no responde

explique el plan, un ejemplo de extremo a extremo

función explotar, explotar

análisis exploratorio de datos (EDA), limpieza de datos

función expr, Columnas como expresiones

expresiones

acceder a las columnas de DataFrame, Acceder a las columnas de un DataFrame

edificio, Trabajando con Diferentes Tipos de Datos­Conclusión

(ver también datos, tipos de)

columnas como expresiones, Columnas como expresiones

definido, Columnas y Expresiones, Expresiones

agrupar con, agrupar con expresiones

une, une expresiones­conclusión

paquetes externos, paquetes externos

servicio aleatorio externo, consideraciones varias, configuraciones aleatorias

tablas externas, Creación de tablas externas

extraer, transformar y cargar (ETL), esquemas, cuándo usar conjuntos de datos, Scala versus Java versus

Python versus R, ETL incremental, formas de usar el aprendizaje profundo en Spark

recuperación de fallas, tolerancia a fallas y puntos de control

programador justo, Programación de aplicaciones, Programación

tolerancia a fallas, tolerancia a fallas y puntos de control

ingeniería de funciones, ingeniería de funciones

generación de características, manipulación de características­ expansión polinomial

selección de funciones, selección de funciones


Machine Translated by Google

caracterización, formas de usar el aprendizaje profundo en Spark

tamaño de archivo, gestión, Gestión del tamaño del archivo

función de relleno, llenar

método de filtro, filtrado de filas, trabajo con valores booleanos, filtrado mejorado, selecciones y

Filtración

primera función, primera y última

primer método, primero

método de ajuste, ingeniería de características con transformadores

flatMap, flatMap, Mapeo sobre valores

flatMapGroupsWithState, ¿ Cuándo puede usar cada modo?, flatMapGroupsWithState flatMapGroupsWithState

función plegar por clave, plegar por clave

fregadero foreach, fregadero foreach

para cada partición, para cada partición

predicción de fraude, Graph Analytics

pares de elementos frecuentes, Trabajar con números

minería de patrones frecuentes, minería de patrones frecuentes

función (Spark SQL), Funciones

GRAMO

regresión gamma, regresión lineal generalizada

Ganglia, el paisaje de monitoreo

recolección de basura, presión de memoria y recolección de basura: ajuste de la recolección de basura

máquinas de puerta de enlace, modo cliente

Gaussiana (regresión lineal), Regresión lineal generalizada

Modelos de mezcla gaussiana (GMM), modelos de mezcla gaussiana

regresión lineal generalizada

ejemplo, ejemplo
Machine Translated by Google

hiperparámetros del modelo, hiperparámetros del modelo

descripción general de la regresión lineal generalizada

parámetros de predicción, parámetros de predicción

parámetros de entrenamiento, parámetros de entrenamiento

resumen de entrenamiento, resumen de entrenamiento

análisis de datos geoespaciales, una lista abreviada de paquetes populares

biblioteca ggplot, SparkR

función glom, glom

árboles potenciados por gradiente (GBT)

aplicado a la clasificación, Random Forest y Gradient­Boosted Trees­Parámetros de predicción

aplicado a regresión, Random Forests y Gradient­Boosted Trees

ejemplo, Parámetros de predicción

hiperparámetros del modelo, hiperparámetros del modelo

descripción general de Random Forest y Gradient­Boosted Trees

parámetros de predicción, parámetros de predicción

parámetros de entrenamiento, parámetros de entrenamiento

análisis gráfico

búsqueda primero en amplitud, búsqueda primero en amplitud

construyendo gráficos, Construyendo un gráfico

componentes conectados, componentes conectados

Algoritmos GraphFrames, Algoritmos gráficos

métricas en grado y fuera de grado, métricas en grado y fuera de grado, métricas en grado y fuera

Métricas de grado

búsqueda de motivos, búsqueda de motivos­ búsqueda de motivos

descripción general de Graph Analytics­Graph Analytics

Algoritmo PageRank, PageRank


Machine Translated by Google

consultando gráficos, consultando el gráfico

componentes fuertemente conectados, componentes fuertemente conectados

subgrafos, subgrafos

casos de uso para Graph Analytics

bases de datos gráficas, Graph Analytics

GraphFrames, Graph Analytics­Conclusion, Graph Algorithms, Spark Packages

GraphX, análisis gráfico

función de agrupación, tramas de datos y SQL, funciones de ventana

grupoPorClave, grupoPorClave

Agrupación, Agrupación­Agrupación con Mapas

conjuntos de agrupación, conjuntos de agrupación ­pivote

operador grouping_id, metadatos de agrupación

compresión gzip, tipos de archivos divisibles y compresión

Sistema de archivos distribuidos de Hadoop (HDFS), Filosofía de Apache Spark, Tipos de archivos divisibles y
Compresión, archivos de Hadoop, implementaciones de clústeres locales, configuraciones de Hadoop

Hadoop YARN, la arquitectura de una aplicación Spark, implementación de Spark, Spark en YARN
Propiedades de aplicación para YARN

Paquetes Granizo , Chispa

Conjunto de datos de reconocimiento de actividad humana de heterogeneidad, transmisión estructurada en acción

Servidor de historial, servidor de historial de interfaz de usuario de Spark

Hive, Big Data y SQL: Apache Hive

Hive metastore, The Hive metastore, Consideraciones varias

HiveContext, el SparkContext

HiveQL, creación de tablas externas

hiperparámetros, ajuste y evaluación de modelos, estimadores

I
Machine Translated by Google

función ifnull, ifnull, nullIf, nvl y nvl2

modelos de imagen, aplicando modelos populares

inmutabilidad, Transformaciones

actualizaciones de datos incrementales, actualizar datos para servir en tiempo real

ETL incremental, ETL incremental

IndexToString, conversión de valores indexados de nuevo a texto

inferencia, Formas de usar el aprendizaje profundo en Spark

paquetes informales, paquetes externos

función initcap, Trabajar con cadenas

uniones internas, uniones internas

resiliencia de datos de entrada, resiliencia de datos de entrada

entrada/salida (API de transmisión estructurada)

origen y sumidero de archivos, origen y sumidero de archivos

fregadero foreach, fregadero Foreach ­Fregadero Foreach

monitoreo de tasa de entrada, tasa de entrada y tasa de procesamiento

Fuente y sumidero Kafka, Fuente y sumidero Kafka

modos de salida, cómo se envían los datos (modos de salida)

lectura de la fuente Kafka, lectura de la fuente Kafka

fuentes y sumideros para pruebas, fuentes y sumideros para pruebas

desencadenadores, cuando se envían datos (disparadores)

escribiendo en el lavabo de Kafka, escribiendo en el lavabo de Kafka

Transformador de características de interacción, Interacción

consolas interactivas, lanzamiento, Lanzamiento de Consolas Interactivas de Spark

Utilidad iostat, El panorama de monitoreo

utilidad iotop, el panorama de monitoreo

regresión isotónica, regresión isotónica


Machine Translated by Google

Java

Codificadores, En Java: Codificadores

SimpleDateFormat, Trabajar con fechas y marcas de tiempo

Formato de zona horaria, trabajo con fechas y marcas de tiempo

referencia de tipo, tipos de chispa

escribir aplicaciones Spark en, escribir aplicaciones Java

Conectividad de bases de datos Java (JDBC), bases de datos SQL, servidor SparkSQL Thrift JDBC/ODBC

Java Virtual Machine (JVM), The Hive metastore, Conjuntos de datos, The Monitoring Landscape

Utilidad jconsole, El panorama de monitoreo

utilidad jmap, el panorama de monitoreo

Uniones

unión de difusión, Estrategias de comunicación

desafíos al usar, Desafíos al usar uniones: enfoque 3: cambiar el nombre de una columna antes de la unión

uniones cruzadas (cartesianas), uniones cruzadas (cartesianas)

depuración, uniones lentas

cómo Spark realiza las uniones, cómo Spark realiza las uniones: una tabla pequeña a una tabla pequeña

uniones internas, uniones internas, unión interna

izquierda anti uniones, izquierda anti uniones

uniones externas izquierdas, uniones externas izquierdas

Semiuniones izquierdas, Semiuniones izquierdas

uniones naturales, uniones naturales

uniones externas, uniones externas

descripción general de Unir expresiones

ajuste de rendimiento, uniones


Machine Translated by Google

en RDD, Uniones

uniones externas derechas, uniones externas derechas

shuffle join, Estrategias de comunicación

en API de transmisión estructurada, uniones

tipos disponibles, tipos de unión

cremalleras, cremalleras

método joinWith, Uniones

datos JSON

archivos JSON delimitados por líneas, archivos JSON

opciones disponibles, Opciones JSON

lectura de archivos JSON, lectura de archivos JSON

trabajando con, Trabajar con JSON

escribir archivos JSON, escribir archivos JSON

Utilidad jstack, El panorama de monitoreo

Utilidad jstat, El panorama de monitoreo

JUnit, Gestión de SparkSessions

Utilidad jvisualvm, El panorama de monitoreo

­algoritmo de medios, Machine Learning y Analítica Avanzada

ejemplo, parámetros de entrenamiento

hiperparámetros del modelo, hiperparámetros del modelo

descripción general de, k­medias

clase de resumen, k­means Resumen de métricas

parámetros de entrenamiento, parámetros de entrenamiento

Kafka

descripción general de fuente y sumidero de Kafka


Machine Translated by Google

lectura de, Lectura de la Fuente Kafka

escribiendo a, escribiendo al fregadero de Kafka

Keras, Aplicando Modelos Populares

RDD de valor­clave

función agregada, agregado

Función agregada por clave, agregada por clave

agregaciones, Agregaciones­foldByKey

función CombineByKey, combineByKey

creando, keyBy

extracción de claves y valores, extracción de claves y valores

función plegar por clave, plegar por clave

grupoPorClave, grupoPorClave

mapeo sobre valores, Mapeo sobre valores

reducirPorClave, reducirPorClave

cuándo usar, conceptos básicos de valor­clave (RDD de valor­clave), tramas de datos frente a SQL frente a conjuntos de datos
versus RDD

Serialización Kryo, Serialización personalizada

Serialización Kryo, Serialización de objetos en RDD

curtosis, cálculo, asimetría y curtosis

L­BFGS (memoria limitada Broyden­Fletcher­Goldfarb­Shanno), escalabilidad del modelo

API de idioma

Java, las API de lenguaje de Spark

descripción general de las API de lenguaje de Spark

Python, API de lenguaje de Spark, PySpark

R, API de lenguaje de Spark, R en Spark­Machine learning


Machine Translated by Google

Scala, las API de lenguaje de Spark

seleccionando, ¿Qué API de Spark usar?, Scala versus Java versus Python versus R

SQL, las API de lenguaje de Spark

última función, primera y última

datos tardíos, manejo, Manejo de datos tardíos con marcas de agua­Manejo de datos tardíos con marcas de agua

Asignación latente de Dirichlet (LDA)

ejemplo, parámetros de entrenamiento

hiperparámetros del modelo, hiperparámetros del modelo

Descripción general de la asignación de Dirichlet latente

parámetros de predicción, parámetros de predicción

parámetros de entrenamiento, parámetros de entrenamiento

evaluación perezosa, evaluación perezosa

izquierda anti uniones, izquierda anti uniones

uniones externas izquierdas, uniones externas izquierdas

Semiuniones izquierdas, Semiuniones izquierdas

bibliotecas, compatibles con Spark, la filosofía de Apache Spark

Formato de datos LIBSVM, MLlib en acción

método de límite, límite

archivos JSON delimitados por líneas, archivos JSON

regresión lineal, regresión lineal

listas, listas

función iluminada, conversión a tipos de chispa

literales, Convirtiendo a Tipos Spark (Literales)

modo local, aplicaciones Spark, modo local

registro, Consideraciones varias, El panorama de monitoreo, Spark Logs

plan lógico, un ejemplo de extremo a extremo, planificación lógica, instrucciones lógicas


Machine Translated by Google

Regresión logística

ejemplo, ejemplo

hiperparámetros del modelo, hiperparámetros del modelo

resumen del modelo, resumen del modelo

descripción general de la regresión logística

parámetros de predicción, parámetros de predicción

parámetros de entrenamiento, parámetros de entrenamiento

función de búsqueda, buscar

API de nivel inferior

definido, ¿Qué son las API de bajo nivel?

variables compartidas distribuidas, Variables compartidas distribuidas ­Conclusión

cómo usar, ¿Cómo usar las API de bajo nivel?

descripción general de las API de nivel inferior

Aplicaciones avanzadas de RDD, RDD avanzados ­Conclusión

Conceptos básicos de RDD, Acerca de RDD­Conclusión

Cuándo usar, ¿ Cuándo usar las API de bajo nivel?, ¿ Qué API de Spark usar?, Marcos de datos frente a SQL frente a conjuntos

de datos frente a RDD

minúsculas, trabajando con cadenas

METRO

aprendizaje automático y análisis avanzado

proceso de análisis avanzado, el proceso de análisis avanzado: aprovechar el modelo y/o los conocimientos

clasificación, Clasificación­Conclusión

limpieza de datos, limpieza de datos

recopilación de datos, recopilación de datos

aprendizaje profundo, aprendizaje profundo ­conclusión

patrones de implementación, patrones de implementación


Machine Translated by Google

ingeniería de funciones, ingeniería de funciones, manipulación de funciones ­ChiSqSelector

análisis gráfico, Graph Analytics, Graph Analytics­Conclusión

MLlib, kit de herramientas de análisis avanzado de Spark : persistencia y aplicación de modelos

entrenamiento de modelos, ingeniería de características

ajuste y evaluación de modelos, ajuste y evaluación de modelos

aprendizaje automático en línea, aprendizaje automático en línea

descripción general de Machine Learning y Advanced Analytics­Machine Learning and Advanced

Descripción general de análisis, análisis avanzado y aprendizaje automático: una introducción breve sobre el aprendizaje avanzado

Analítica

modelos persistentes y de aplicación, modelos persistentes y de aplicación

Concepto de canalización, Canalización de nuestro flujo de trabajo

preprocesamiento, preprocesamiento e ingeniería de características­Word2Vec

recomendación, recomendación, recomendación­conclusión

regresión, Regresión­Conclusión

aprendizaje supervisado, aprendizaje supervisado ­regresión

aprendizaje no supervisado, aprendizaje no supervisado, aprendizaje no supervisado ­conclusión

Magellan, una lista abreviada de paquetes populares

biblioteca Magrittr, SparkR

función main(), Aplicaciones Spark

mapGroupsWithState, ¿ Cuándo puede usar cada modo?, mapGroupsWithState mapGroupsWithState

particiones de mapa, particiones de mapa

MapPartitionsRDD, mapPartitions

mapas, Mapas, Agrupación con mapas

análisis de la cesta de la compra, minería de patrones frecuentes

Maven, una aplicación simple basada en Scala

función max, min y max, max y min


Machine Translated by Google

Escalador MaxAbs, Escalador MaxAbs

maxFilesPerTrigger, transmisión estructurada en acción, fuente y sumidero de archivos

grupos de encuentro, Meetups locales

gestión de la memoria

configurando, Configuración de la gestión de memoria

recolección de basura, presión de memoria y recolección de basura: ajuste de la recolección de basura

OutOfMemoryError, Driver OutOfMemoryError o Driver Unresponsive­Posible tratamientos

almacenamiento temporal de datos (caché), Almacenamiento temporal de datos (caché) ­Datos temporales

Almacenamiento (caché)

sumideros de memoria, transmisión estructurada en acción, fuentes y sumideros para pruebas

Mesos (ver Apache Mesos)

metadatos

describiendo, describiendo los metadatos de la tabla

agrupación, Agrupación de metadatos

Metastore de Hive, Consideraciones varias

Metadatos de la columna MLlib, propiedades del transformador

refrescante, Refrescando metadatos de tabla

tablas (Spark SQL), tablas administradas por Spark

Metastores, Consideraciones misceláneas

métricas, procesos de controlador y ejecutor, métricas y supervisión­ interfaz de usuario de Spark, evaluación detallada

Métricas, métricas, métricas, en grado y fuera de grado Métricas en grado y fuera de grado
Métrica

(ver también monitoreo y depuración)

sistemas de microlotes, ejecución continua frente a microlotes

función min, min y max, max y min

minDF, Conversión de palabras en representaciones numéricas

frecuencia de término mínimo (minTF), Conversión de palabras en representaciones numéricas


Machine Translated by Google

MinMaxScaler, MinMaxScaler

datos faltantes, trabajar con valores nulos en los datos

MLlib

beneficios de Machine Learning y Advanced Analytics, cuándo y por qué debería usar MLlib (frente a scikit­learn, TensorFlow

o foo package)

modelos de clasificación en, Modelos de clasificación en MLlib­Model Scalability

estimadores, conceptos de MLlib de alto nivel

evaluadores, conceptos de MLlib de alto nivel

tipos de datos de bajo nivel, tipos de datos de bajo nivel

compatibilidad con redes neuronales, compatibilidad con redes neuronales MLlib

descripción general de Machine Learning y Advanced Analytics­Machine Learning y Advanced Analytics, ¿ Qué es

MLlib?

paquetes incluidos en ¿Qué es MLlib?

modelos persistentes y de aplicación, modelos persistentes y de aplicación

ejemplo de canalización, MLlib en modelos de aplicación y persistencia de acciones

modelos de regresión en, Modelos de regresión en MLlib­Conclusión

transformadores, conceptos de MLlib de alto nivel

modelos (ver también modelos individuales; aprendizaje automático y análisis avanzado; MLlib)

ajuste automático de modelos, evaluadores para clasificación y ajuste automático de modelos,

Evaluadores y ajuste automático del modelo

modelos de clasificación, modelos de clasificación en MLlib­Conclusion

modelos de aprendizaje profundo, Deep Learning­Conclusión

formato según el caso de uso, formato de modelos según su caso de uso: formato

Modelos según su caso de uso

modelos de imagen, aplicando modelos populares

modelos de regresión, Regresión­Conclusión

escalabilidad de, Escalabilidad del modelo, Escalabilidad del modelo, Filtrado colaborativo con alternancia

Mínimos cuadrados, escalabilidad del modelo


Machine Translated by Google

formación, ingeniería de características

entrenar modelos de aprendizaje profundo, Formas de usar el aprendizaje profundo en Spark

sintonización y evaluación, sintonización y evaluación de modelos

monitoreo y depuración

metadatos de columna, propiedades del transformador

componentes para monitorear, El panorama de monitoreo­El panorama de monitoreo

decisiones de implementación, consideraciones varias

problemas con el controlador, Driver OutOfMemoryError o Driver Unresponsive

errores antes de la ejecución, Errores antes de la ejecución

errores durante la ejecución, Errores durante la ejecución

problemas con el ejecutor, Executor OutOfMemoryError o Executor no responde

no queda espacio en errores de disco, signos y síntomas

procesos a monitorear, Qué monitorear

papel en la optimización del rendimiento, Performance Tuning

errores de serialización, signos y síntomas

agregación lenta, agregaciones lentas

uniones lentas, uniones lentas

lecturas y escrituras lentas, lecturas y escrituras lentas

tareas lentas o rezagadas, tareas lentas o rezagadas

Los trabajos de Spark no se inician, los trabajos de Spark no se inician

Registros de chispa, registros de chispa

Spark UI, el servidor de historial de Spark UI­Spark UI

API de transmisión estructurada, métricas y supervisión­ interfaz de usuario de Spark

nulos inesperados en los resultados, signos y síntomas, formateo de modelos según su uso
Caso

función monotonically_increasing_id, trabajar con números


Machine Translated by Google

algoritmos de búsqueda de motivos, Motif Finding­Motif Finding

clasificación multiclase, clasificación multiclase

clasificación multilabel, clasificación multilabel

clasificador de perceptrón multicapa, soporte de red neuronal MLlib

modelos multinomiales, Naive Bayes

modelos multivariados de Bernoulli, Naive Bayes

norte

n­gramas, creación de combinaciones de palabras

Clasificadores Naive Bayes, Naive Bayes

dependencias estrechas, Transformaciones

uniones naturales, uniones naturales

Netflix, casos de uso

estrategia de comunicación de nodo a nodo, cómo Spark realiza uniones

nodos, análisis gráfico

normalización y escalado, Escalado y Normalización­Normalizador

notificaciones y alertas, Notificaciones y alertas

valores nulos, Columnas, Trabajar con valores nulos en la ordenación de datos, Signos y síntomas, Formateo

Modelos según su caso de uso

función nullIf, ifnull, nullIf, nvl y nvl2

números, Trabajando con Números­Trabajando con Números

función nvl, ifnull, nullIf, nvl y nvl2

función nvl2, ifnull, nullIf, nvl y nvl2

serialización de objetos, serialización personalizada, signos y síntomas, serialización de objetos en RDD

grupo de encuentro oficial, Meetups locales

clústeres locales, implementaciones de clústeres locales


Machine Translated by Google

disparador una vez, disparador una vez

Clasificador uno contra resto, uno contra resto

OneHotEncoder, aprendizaje automático y análisis avanzado, codificación One­Hot

procesamiento analítico en línea (OLAP), Big Data y SQL: Spark SQL

aprendizaje automático en línea, aprendizaje automático en línea

procesamiento de transacciones en línea (OLTP), Big Data y SQL: Spark SQL

optimización, basado en costos, Recopilación de estadísticas

(ver también ajuste de rendimiento)

archivos ORC, archivos ORC

Paquete org.apache.spark.sql.functions, creación de marcos de datos, dónde buscar API, funciones de agregación

Métrica fuera de grado, Métricas en grado y fuera de grado Métricas en grado y fuera de grado

uniones externas, uniones externas

Error de memoria insuficiente

drivers, Driver OutOfMemoryError o Driver no responde

ejecutores, Executor OutOfMemoryError o Executor no responde

modos de salida, modos de salida, cómo se emiten los datos (modos de salida), modos de salida

resolución del esquema de salida, resiliencia en la salida y atomicidad

sumideros de salida, sumideros

(ver también fregaderos)

PAG

Paquetes, Ecosistema de Spark y Paquetes, Paquetes Spark ­ Paquetes externos

Algoritmo PageRank, análisis gráfico, PageRank

Integración de Pandas, Pandas

paralelismo, paralelismo

método de paralelización, de una colección local


Machine Translated by Google

ParamGrid, Entrenamiento y Evaluación

Archivos de parquet

beneficios de, Archivos de parquet

opciones disponibles, opciones de parquet

lectura, lectura de archivos de parquet

escritura, Escritura de archivos de parquet

Particionador, Tipos de RDD, Particionamiento personalizado

particiones

basado en ventanas corredizas, Partición basada en una ventana corrediza

control con RDD, control de particiones: particionamiento personalizado

particionamiento personalizado, Particionamiento personalizado­ Particionamiento personalizado

definido, Particiones

esquemas de partición, Operaciones Estructuradas Básicas

ajuste de rendimiento, particionamiento de tablas, reparticionamiento y fusión

propósito de, partición

repartición, Repartición y Coalesce

papel en el ciclo de vida de la aplicación, Etapas

Coeficiente de correlación de Pearson, trabajo con números, covarianza y correlación

estrategia de cálculo por nodo, cómo Spark realiza las uniones

la optimización del rendimiento

agregaciones, agregaciones

ajuste automático de modelos, evaluadores para clasificación y ajuste automático de modelos,

Evaluadores y ajuste automático del modelo

variables de difusión, Variables de difusión

configuraciones de clúster, Configuraciones de clúster

configuraciones de red de clúster, configuraciones de red de clúster


Machine Translated by Google

datos en reposo, recopilación de datos en reposo­estadísticas

opciones de diseño, opciones de diseño

enfoques directos vs. indirectos, Performance Tuning

filtrado mejorado, filtrado mejorado

se une, se une

presión de memoria y recolección de basura, presión de memoria y recolección de basura: ajuste de la recolección de basura

serialización de objetos en RDD, serialización de objetos en RDD

descripción general de Ajuste de rendimiento

paralelismo, paralelismo

repartición y coalescencia, Repartición y coalescencia

rol de monitoreo en Performance Tuning

programación, programación

configuraciones aleatorias, Configuraciones aleatorias

almacenamiento temporal de datos (caché), Almacenamiento temporal de datos (caché) ­Datos temporales

Almacenamiento (caché)

Funciones definidas por el usuario (UDF), Funciones definidas por el usuario (UDF)

plan físico, evaluación perezosa, planificación física, instrucciones lógicas para la ejecución física,

Almacenamiento temporal de datos (caché)

pip install pyspark, descarga de Spark para un clúster de Hadoop

método de canalización, canalización de RDD a comandos del sistema

Concepto de canalización, Canalización de nuestro flujo de trabajo

canalización, Transformaciones, Canalización

pivotes, pivote

regresión de poisson, regresión lineal generalizada

expansión polinomial, expansión polinomial

empuje de predicado, evaluación perezosa


Machine Translated by Google

subconsultas de predicado, subconsultas de predicado no correlacionadas

preprocesamiento

cubetas, cubetas­ Técnicas avanzadas de cubetas

características categóricas, Trabajar con características categóricas­ Transformadores de datos de texto

funciones continuas, Trabajar con funciones continuas­Normalizador

convertir valores indexados de nuevo a texto, convertir valores indexados de nuevo a texto

conversión de palabras en números, conversión de palabras en representaciones numéricas­ frecuencia del término–frecuencia

inversa del documento

crear combinaciones de palabras, Crear combinaciones de palabras

estimadores, estimadores para preprocesamiento

generación de características, manipulación de características­ expansión polinomial

formateo de modelos según el caso de uso, formateo de modelos según su caso de uso

Formato de modelos según su caso de uso

transformadores de alto nivel, transformadores de alto nivel ­VectorAssembler

indexación en vectores, indexación en vectores

codificación one­hot, codificación One­Hot

eliminar palabras comunes, Eliminar palabras comunes

escalado y normalización, Escalado y Normalización

Transformadores SQL, Transformadores SQL

Indizador de cadenas, Indizador de cadenas

transformadores de datos de texto, transformadores de datos de texto­ frecuencia de término­frecuencia de documento inversa

tokenizando el texto, tokenizando el texto

transformadores, Transformadores, Transformadores persistentes: escritura de un transformador personalizado

Ensamblador de vectores, Ensamblador de vectores

Palabra2Vec, Palabra2Vec

Análisis de componentes principales (PCA), PCA

tiempo de procesamiento, tiempo de evento versus tiempo de procesamiento, tiempo de evento, tasa de entrada y tasa de procesamiento
Machine Translated by Google

disparador de tiempo de procesamiento, disparador de tiempo de procesamiento

aplicaciones de producción

beneficios de Spark para ejecutar aplicaciones de producción

implementando, Implementando Spark­Conclusión

desarrollo, Desarrollo de aplicaciones Spark­Conclusión

cómo se ejecuta Spark en clústeres, Cómo se ejecuta Spark en un clúster­Conclusión

monitoreo y depuración, Monitoreo y Depuración­Conclusión

ajuste de rendimiento, Ajuste de rendimiento ­Conclusión

API de transmisión estructurada, transmisión estructurada en producción­conclusión

PushedFilters, Query Pushdown

PySpark, Descarga de Spark para un clúster de Hadoop, PySpark

Pitón

lanzamiento de la consola, lanzamiento de la consola de Python

PySpark, PySpark

referencia de tipo, tipos de chispa

escribir aplicaciones Spark en, escribir aplicaciones Python

ejecución de consultas, monitoreo de, Qué monitorear, Estado de consultas : duración del lote

optimizador de consultas, recopilación de estadísticas

query pushdown, Query Pushdown­Partitioning basado en una ventana deslizante

preguntas y comentarios, Cómo contactarnos

descripción general de las API de lenguaje de Spark, R en Spark

sparklyr, sparklyr­ Aprendizaje automático

SparkR, SparkR­Funciones definidas por el usuario


Machine Translated by Google

bosques aleatorios

aplicado a la clasificación, Random Forest y Gradient­Boosted Trees­Parámetros de predicción

aplicado a regresión, Random Forests y Gradient­Boosted Trees

ejemplo, Parámetros de predicción

hiperparámetros del modelo, hiperparámetros del modelo

descripción general de Random Forest y Gradient­Boosted Trees

parámetros de predicción, parámetros de predicción

parámetros de entrenamiento, parámetros de entrenamiento

divisiones aleatorias, divisiones aleatorias, divisiones aleatorias

Método rdd, interoperabilidad entre marcos de datos, conjuntos de datos y RDD

atributo de lectura, Conceptos básicos de la lectura de datos

leyendo datos

conceptos básicos de, conceptos básicos de la lectura de datos

estructura de la API principal, estructura de la API de lectura

depuración, lecturas y escrituras lentas

modo de lectura, Modos de lectura

toma de decisiones en tiempo real, toma de decisiones en tiempo real

informes en tiempo real, informes en tiempo real

recomendación

filtrado colaborativo con alternancia de mínimos cuadrados, Filtrado colaborativo con alternancia

mínimos cuadrados

evaluadores, evaluadores de recomendación

ejemplo, ejemplo

minería de patrones frecuentes, minería de patrones frecuentes

métricas, métricas­ métricas de clasificación

hiperparámetros del modelo, hiperparámetros del modelo


Machine Translated by Google

parámetros de predicción, parámetros de predicción

a través del análisis gráfico, Graph Analytics

parámetros de entrenamiento, parámetros de entrenamiento

casos de uso para, recomendación, casos de uso

API de registro a la vez, API de registro a la vez versus API declarativas

registros (ver también columnas; filas)

muestras aleatorias de, muestras aleatorias

divisiones aleatorias de, divisiones aleatorias

repartir y fusionar, repartir y fusionar

restringir la extracción de, Limitar

frente a filas, registros y filas

recuperación, tolerancia a fallas y puntos de control

Redshift Connector, una lista abreviada de paquetes populares

reducir el método, reducir

reducirPorClave, reducirPorClave

REFRESH TABLE, actualización de metadatos de la tabla

RegexTokenizer, tokenización de texto

regresión

árboles de decisión, árboles de decisión

evaluadores y automatización del ajuste de modelos, evaluadores y automatización del ajuste de modelos

regresión lineal generalizada, resumen de entrenamiento de regresión lineal generalizada

regresión isotónica, regresión isotónica

regresión lineal, regresión lineal

métricas, Métricas

modelos en MLlib, modelos de regresión en MLlib­Model Scalability

bosques aleatorios y árboles potenciados por gradientes, bosques aleatorios y árboles potenciados por gradientes
Machine Translated by Google

regresión de supervivencia (tiempo de falla acelerado), regresión de supervivencia (tiempo de falla acelerado)

casos de uso para, regresión, casos de uso

Expresiones regulares (RegExes), Expresiones regulares­ Expresiones regulares, Tokenización de texto

RelationalGroupedDataset, DataFrames y SQL, Agregaciones

repartición, Repartición y Coalesce, repartición, Etapas, Repartición y Coalesce

repartitionAndSortWithinPartitions, repartitionAndSortWithinPartitions

reemplazar función, reemplazar

informes, en tiempo real, informes en tiempo real

solicitudes, solicitud del cliente

reescalar aplicaciones, dimensionar y reescalar su aplicación

caracteres reservados, caracteres reservados y palabras clave

resiliencia

de la lógica de negocios, resiliencia y evolución de la lógica de negocios

de datos de salida, resiliencia en la salida y atomicidad

resiliencia, de datos de entrada, Resiliencia de datos de entrada

Conjuntos de datos distribuidos resistentes (RDD)

accediendo a valores en, primero

acciones, Acciones­tomar

agregaciones, Agregaciones­foldByKey

almacenamiento en caché, almacenamiento en caché

punto de control, punto de control

Cogrupos, Cogrupos

contando, contar

creación, creación de RDD a partir de fuentes de datos

filtrado, filtrar

se une, se une
Machine Translated by Google

RDD de valor­clave, conceptos básicos de valor­clave (RDD de valor­clave )­foldByKey

manipular, manipular RDD

cartografía, mapa

serialización de objetos en, serialización de objetos en RDD

descripción general de, API de nivel inferior, Acerca de RDD

particiones, Control de particiones­ Particionamiento personalizado

canalizar RDD a comandos del sistema, canalizar RDD a comandos del sistema­Conclusión

divisiones aleatorias, divisiones aleatorias

RDD de clases de casos frente a conjuntos de datos, conjuntos de datos y RDD de clases de casos

reducir, reducir

eliminando duplicados de, distintos

guardar archivos, guardar archivos

serialización, serialización personalizada

clasificar, ordenar

transformaciones, Transformaciones­ Divisiones aleatorias

tipos de, Tipos de RDD

cuándo usar, cuándo usar RDD?, ¿ qué API de Spark usar?, DataFrames versus SQL versus
Conjuntos de datos frente a RDD

problema de utilización de recursos, implementaciones de clústeres locales, dimensionamiento y uso compartido de clústeres/

aplicaciones

Puntos finales de API REST, API REST de Spark

RFórmula

beneficios de, RFormula

etiquetas de columna, RFormula

ejemplo, fórmula R

operadores, Ingeniería de Características con Transformadores, RFormula

uniones externas derechas, uniones externas derechas


Machine Translated by Google

operador de acumulación, acumulaciones

redondear números, trabajar con números

Tipo de fila, tramas de datos frente a conjuntos de datos, registros y filas, conjuntos de datos

filas

acceder a datos en, Creación de filas

recogiendo al conductor, recogiendo filas al conductor

concatenación y adición, Concatenación y adición de filas (Unión)

conversión a columnas, pivote

creando, Creando filas

extrayendo filas únicas, obteniendo filas únicas

Filtrado, Filtrado de Filas

generar identificaciones únicas para trabajar con números

ordenar, Ordenar filas

propiedades de tiempo de ejecución, propiedades de tiempo de ejecución

método de muestra, muestras aleatorias

función muestraPorClave, muestraPorClave

modos de guardado, modos de guardado

guardar como archivo de texto, guardar como archivo de texto

Herramienta sbt, una aplicación simple basada en Scala

Scala

beneficios de una aplicación simple basada en Scala

clases de casos, en Scala: clases de casos

creación de columnas en, Columnas

operadores de comparación en, Trabajar con booleanos

lanzamiento de la consola, lanzamiento de la consola Scala


Machine Translated by Google

símbolos en, Columnas

referencia de tipo, tipos de chispa

escalabilidad, escalabilidad del modelo, escalabilidad del modelo, filtrado colaborativo con alternancia mínima

Cuadrados, escalabilidad del modelo

consultas escalares, consultas escalares no correlacionadas

ScalaTest, Gestión de SparkSessions

escalado y normalización, Escalado y Normalización­Normalizador

programación, Programación de trabajos dentro de una aplicación, Programación de aplicaciones, Programación

inferencia de esquema, un ejemplo de extremo a extremo, transmisión estructurada en acción

esquema en lectura, esquemas, esquemas

esquemas

componentes de, esquemas

definido, marcos de datos

definición, esquemas

hacer cumplir, esquemas

configuraciones de implementación segura, Configuraciones de implementación segura

Seleccionar método, seleccionar y seleccionar Expr, funciones de ventana, selecciones y filtrado

declaraciones selectas, declaraciones selectas

Método SelectExpr, select y selectExpr

semiuniones, semiuniones izquierdas

archivos de secuencia, archivos de secuencia

serialización, Serialización personalizada, Signos y síntomas, Serialización de objetos en RDD

sesionización, flatMapGroupsWithState­flatMapGroupsWithState

conjuntos, agrupación, Agrupación de conjuntos­pivote

instrucción SHOW FUNCTIONS, Funciones

barajar
Machine Translated by Google

configuración del comportamiento, Configuración del comportamiento aleatorio

definido, Transformaciones, Etapas

eficiencia de, Conclusión

servicio externo, Consideraciones Misceláneas

ajuste de rendimiento, configuraciones aleatorias

Combinaciones aleatorias, Estrategias de comunicación

Persistencia aleatoria, Persistencia aleatoria

tipos simples, Columnas

sumideros

sumidero de consola, fuentes y sumideros para pruebas

definido, Fregaderos

archivos, fuente de archivo y sumidero

para pruebas, fuentes y sumideros para pruebas

sumideros de memoria, transmisión estructurada en acción, fuentes y sumideros para pruebas

dimensionamiento de aplicaciones, dimensionamiento y cambio de escala de su aplicación

asimetría, cálculo, asimetría y curtosis

ventanas correderas, Ventanas correderas ­ Ventanas correderas

redes sociales, métricas de grado y fuera de grado

fuente de enchufe, fuentes y sumideros para pruebas

ordenar acción, un ejemplo de extremo a extremo

ordenar por método, ordenar

espacios, eliminar, Trabajar con cadenas

Chispa (ver Apache Spark)

Aplicaciones de chispa

propiedades de la aplicación, Propiedades de la aplicación

arquitectura y componentes de, La arquitectura de una aplicación Spark: finalización


Machine Translated by Google

conceptos básicos de aplicaciones Spark

configurando, Configuración de aplicaciones: programación de trabajos dentro de una aplicación

proceso de desarrollo, el proceso de desarrollo

plantilla de desarrollo, desarrollo de aplicaciones de Spark, registros de Spark

detalles de ejecución, Detalles de ejecución

modos de ejecución, modos de ejecución

lanzamiento, Lanzamiento de aplicaciones­ Ejemplos de lanzamiento de aplicaciones

ciclo de vida del interior de Spark, El ciclo de vida de una aplicación Spark (Inside Spark)­Tareas

ciclo de vida de fuera de Spark, el ciclo de vida de una aplicación de Spark (fuera de Spark)­

Terminación

monitoreo y depuración, Monitoreo y Depuración­Conclusión

ajuste de rendimiento, Ajuste de rendimiento ­Conclusión

programación, programación de aplicaciones, programación

(ver también implementación)

pruebas, Pruebas de aplicaciones Spark: conexión a fuentes de datos

escribir, escribir aplicaciones Spark: ejecutar la aplicación

Comunidad de Spark, ecosistema y paquetes de Spark, fuentes de datos, reuniones comunitarias locales

Spark Deep Learning, una lista abreviada de paquetes populares

Empleos de Spark

ejecución de trabajos asincrónicos, tiempos de espera

tratamientos de depuración, depuración y primeros auxilios Spark ­Potential

definida, interfaz de usuario de Spark

orden de ejecución, El ciclo de vida de una aplicación Spark (Dentro de Spark)

mejorando la velocidad y la ejecución de Agregaciones

seguimiento, Consultas, Trabajos, Etapas y Tareas

programación, Programación de trabajos dentro de una aplicación, Programación de aplicaciones, Programación


Machine Translated by Google

etapas y tareas en, Instrucciones lógicas para ejecución física­Tareas

Registros de chispa, registros de chispa

Paquetes Spark, Ecosistema y paquetes de Spark, Paquetes Spark ­ Paquetes externos

Plan Chispa, Planificación Física

(ver también plano físico)

API REST de Spark, API REST de Spark

Chispa SQL

configuraciones de aplicaciones, funciones diversas

Beneficios de Big Data y SQL: Spark SQL

Catálogo, Catálogo, Consideraciones Misceláneas

tipos complejos, Tipos complejos ­Listas

bases de datos, bases de datos

frente a marcos de datos y conjuntos de datos, marcos de datos frente a SQL frente a conjuntos de datos frente a RDD

funciones, Funciones

historia de Big Data y SQL: Apache Hive

lanzamiento de la consola, lanzamiento de la consola SQL

listas, listas

descripción general de Spark SQL

relación con Hive, relación de Spark con Hive

ejecución de consultas, Cómo ejecutar consultas Spark SQL­SparkSQL Thrift JDBC/ODBC Server

sentencia de selección, sentencias de selección

estructuras, estructuras

subconsultas, Subconsultas­Consultas escalares no correlacionadas

tablas, Tablas­Caching Tables

vistas, vistas­vistas eliminadas

Spark Streaming, fundamentos del procesamiento de transmisiones


Machine Translated by Google

Cumbres Spark, Cumbre Spark

Interfaz de usuario de Spark

beneficios de la interfaz de usuario de Spark

configuración, Configuración de la interfaz de usuario de Spark

Servidor de historial, servidor de historial de interfaz de usuario de Spark

investigación de ejecución de trabajos, la interfaz de usuario de Spark: la interfaz de usuario de Spark

descripción general de la interfaz de usuario de Spark

investigación de consultas, la interfaz de usuario de Spark: la interfaz de usuario de Spark

API REST de Spark, API REST de Spark

API de transmisión estructurada y Spark UI

pestañas en, La interfaz de usuario de Spark, Otras pestañas de la interfaz de usuario de Spark

Spark Unti Tests, resiliencia y evolución de la lógica de negocios

variable de chispa, SparkSession

spark­packages.org, Filosofía de Apache Spark

spark­shell, El proceso de desarrollo

spark­submit, Ejecución de aplicaciones de producción, El ciclo de vida de una aplicación Spark (fuera

Spark), una aplicación simple basada en Scala, lanzamiento de aplicaciones­lanzamiento de aplicaciones

función spark.sql, tramas de datos y SQL

spark.sql.hive.metastore.jars, la tienda de metadatos Hive

spark.sql.hive.metastore.version, la metatienda de Hive

spark.sql.shuffle.partitions, Etapas

SparkConf, La SparkConf

SparkContext, ¿Cómo usar las API de bajo nivel?, SparkSession­The SparkContext

brillante

manipulación de datos, manipulación de datos

fuentes de datos, fuentes de datos


Machine Translated by Google

ejecutando SQL, ejecutando SQL

conceptos clave, conceptos clave

falta de tramas de datos en, sin tramas de datos

aprendizaje automático, aprendizaje automático

descripción general de, brillante

SparkR

ventajas y desventajas, pros y contras de usar SparkR en lugar de otros lenguajes

manipulación de datos, manipulación de datos

fuentes de datos, fuentes de datos

enmascaramiento de funciones, enmascaramiento de funciones

conceptos clave, conceptos clave : funciones definidas por el usuario

aprendizaje automático, aprendizaje automático

descripción general de, SparkR, SparkR

configurar, Configurar

Funciones de SparkR, las funciones de SparkR solo se aplican a SparkDataFrames

funciones definidas por el usuario (UDF), funciones definidas por el usuario ­funciones definidas por el usuario

Instancias de SparkSession

creación, Iniciando Spark, La SparkSession

en Python, la SparkSession

en Scala, la SparkSession

administrar, Administrar SparkSessions

Propósito de SparkSession

papel en el proceso de lanzamiento, Lanzamiento

SparkContext y The SparkContext­The SparkContext

función dividida, dividir

formatos de archivo divisibles, tipos de archivos divisibles y compresión, tipos de archivos divisibles y
Machine Translated by Google

compresión

SQL (lenguaje de consulta estructurado), ¿ Qué es SQL?

(ver también Spark SQL; bases de datos SQL)

bases de datos SQL

Controlador Java Database Connectivity (JDBC) para bases de datos SQL

Opciones de origen de datos JDBC, bases de datos SQL

query pushdown, Query Pushdown­Partitioning basado en una ventana deslizante

lectura desde, Lectura desde bases de datos SQL­Lectura desde bases de datos SQL

lectura en paralelo, lectura de bases de datos en paralelo, lectura de datos en paralelo

SQLite, bases de datos SQL

sistemas disponibles, bases de datos SQL

escribir en, escribir en bases de datos SQL

SQLContext, la SparkSession

SQLite, bases de datos SQL

Transformadores SQL, Transformadores SQL

etapas, instrucciones lógicas para la ejecución física: tareas, consultas, trabajos, etapas y tareas

administrador de clúster independiente, administradores de clúster : envío de aplicaciones

desviación estándar, cálculo, varianza y desviación estándar

StandardScaler, estimadores para preprocesamiento, StandardScaler

procesamiento con estado

Procesamiento con estado arbitrario y arbitrario

beneficios de Spark for, Event­Time y Stateful Processing

Consideraciones para el procesamiento con estado arbitrario

flatMapGroupsWithState, flatMapGroupsWithState­flatMapGroupsWithState

mapGroupsWithState, mapGroupsWithState­mapGroupsWithState

modos de salida, modos de salida


Machine Translated by Google

descripción general de procesamiento con estado

tiempos muertos, tiempos muertos

Paquete StatFunctions, Trabajar con números

funciones estadísticas, Trabajando con Números

recopilación y mantenimiento de estadísticas, Recopilación de estadísticas

función stddev, Varianza y Desviación Estándar

descenso de gradiente estocástico, escalabilidad del modelo

detener palabras, eliminar, Eliminación de palabras comunes

ALMACENADO COMO, Creación de tablas

rezagados, tareas lentas o rezagados

procesamiento de flujo

ventajas de, Ventajas de Stream Processing

Selección de API, API de Streaming de Spark

conceptos básicos de ¿ Qué es el procesamiento de flujo?

desafíos de, Desafíos del procesamiento de flujo

ejecución continua frente a microlotes, ejecución continua frente a microlotes

puntos de diseño, procesamiento de secuencias Puntos de diseño: ejecución continua frente a microlotes

tiempo de evento frente a tiempo de procesamiento, tiempo de evento frente a tiempo de procesamiento

historia de los fundamentos del procesamiento de flujo

API de registro a la vez frente a API declarativas, API de registro a la vez frente a API declarativas

Casos de uso para el procesamiento de secuencias Casos de uso: aprendizaje automático en línea

Streaming Listener, monitorización avanzada con Streaming Listener

cadena, Trabajando con Cadenas­ Expresiones Regulares

StringIndexer, aprendizaje automático y análisis avanzado, StringIndexer

algoritmo de componentes fuertemente conectados, componentes fuertemente conectados

StructFields, Esquemas
Machine Translated by Google

estructuras, estructuras, estructuras

Tipo de estructura, esquemas

API estructuradas

operaciones estructuradas básicas, operaciones estructuradas básicas ­conclusión

ejecución de código, Descripción general de la ejecución de API estructurada ­Ejecución

Marcos de datos, marcos de datos y conjuntos de datos

Conjuntos de datos, conjuntos de datos: API estructuradas con seguridad de tipos, marcos de datos y conjuntos de datos

descripción general de API estructurada

esquemas y esquemas

seleccionando sobre RDD, API de nivel inferior

Conceptos fundamentales de Spark, descripción general de la API estructurada

tipos de Spark estructurados y, descripción general de tipos de Spark estructurados ­ tipos de Spark, conversión a

Tipos de chispas (literales)

API de transmisión estructurada

alertando, alertando

actualizaciones de aplicaciones, actualización del tamaño de su aplicación y cambio de escala de su aplicación

ejemplo aplicado, Streaming estructurado en acción­Streaming estructurado en acción

beneficios de, ¿Qué es el procesamiento de transmisión?, Transmisión estructurada, Conceptos básicos de transmisión estructurada

conceptos básicos, conceptos básicos ­marcas de agua

tolerancia a fallos y puntos de control, Tolerancia a fallos y puntos de control

historia de los fundamentos del procesamiento de flujo

entrada y salida, disparador de entrada y salida única

falta de ejecución asincrónica de trabajos, tiempos de espera

métricas y monitoreo, Métricas y Monitoreo­Spark UI

descripción general de Streaming estructurado ­Streaming estructurado, Conceptos básicos de streaming estructurado

como listo para producción, transmisión estructurada en producción


Machine Translated by Google

dimensionamiento y reescalado de aplicaciones, dimensionamiento y reescalado de su aplicación

API de conjunto de datos de transmisión, API de conjunto de datos de transmisión

Supervisión de Streaming Listener, supervisión avanzada con Streaming Listener

transformaciones en secuencias, Transformaciones en secuencias­Joins

subgrafos, subgrafos

subconsultas, subconsultas

subconsultas (Spark SQL), Subconsultas­Consultas escalares no correlacionadas

método de agregación sum, DataFrames y SQL

función de suma, suma

función sumDistinct, sumDistinct

estadísticas resumidas, computación, Trabajar con números

aprendizaje supervisado

clasificación, Clasificación, Clasificación­Conclusión

objetivo de, aprendizaje supervisado

regresión, regresión, regresión­conclusión

regresión de supervivencia (Tiempo de falla acelerada), Regresión de supervivencia (Tiempo de falla acelerada)

tablas (Spark SQL)

almacenamiento en caché, tablas de almacenamiento en caché

creando, Creando Tablas

creación de tablas externas, Creación de tablas externas

droping, Dropping Tables

insertar en, insertar en tablas

tablas administradas frente a no administradas, tablas administradas por Spark

metadatos, descripción de los metadatos de la tabla

resumen de, Tablas


Machine Translated by Google

tomar acción, un ejemplo integral, conjuntos de datos: API estructuradas con seguridad de tipos, acciones, tomar

tareas, instrucciones lógicas para la ejecución física: tareas, consultas, trabajos, etapas y tareas, lento

Tareas o Rezagados

plantilla, desarrollo de aplicaciones de Spark, registros de Spark

almacenamiento temporal de datos (caché), Almacenamiento temporal de datos (caché) ­Almacenamiento temporal de datos

(Almacenamiento en caché)

TensorFlowOnSpark, TensorFlowOnSpark

TensorFrames, TensorFrames

pruebas

conectarse a fuentes de datos, conectarse a fuentes de datos

conectarse a marcos de pruebas unitarias, conectarse a marcos de pruebas unitarias

principios y tácticas clave, Principios Estratégicos

administrar SparkSessions, Administrar SparkSessions

Selección de API de Spark, ¿Qué API de Spark usar?

consideraciones tácticas, conclusiones tácticas

transformadores de datos de texto, transformadores de datos de texto­ frecuencia de término­frecuencia de documento inversa

archivos de texto, archivos de texto

TF­IDF (frecuencia de término­frecuencia de documento inversa), frecuencia de término­frecuencia de documento inversa­ frecuencia

de término­frecuencia de documento inversa

la maldición de la dimensionalidad, aprendizaje no supervisado

Servidor Thrift JDBC/Open Database Connectivity (ODBC), SparkSQL Thrift JDBC/ODBC


Servidor

tiempos de espera, procesamiento con estado arbitrario, tiempos de espera

información relacionada con la hora, Trabajar con fechas y marcas de tiempo­Trabajar con fechas y

Marcas de tiempo

marcas de tiempo, procesamiento de tiempo de evento

Clase TimestampType, Trabajar con fechas y marcas de tiempo

Método toDF, interoperabilidad entre marcos de datos, conjuntos de datos y RDD


Machine Translated by Google

Transformador de tokenizador, Transformadores, Texto de tokenización

Método toLocalIterator, recopilación de filas para el controlador

componentes del juego de herramientas

API de conjuntos de datos, Conjuntos de datos: API estructuradas con seguridad de tipos

ecosistema de paquetes y herramientas, Spark's Ecosystem and Packages

API de nivel inferior, API de nivel inferior

aprendizaje automático y análisis avanzado, aprendizaje automático y análisis avanzado

Aprendizaje automático y análisis avanzado

descripción general de, ¿ Qué es Apache Spark?, Un recorrido por el conjunto de herramientas de Spark

ejecución de aplicaciones de producción, ejecución de aplicaciones de producción

ChispaR, ChispaR

Transmisión estructurada, transmisión estructurada

modelado de temas, aprendizaje no supervisado

función to_date, trabajar con fechas y marcas de tiempo

función to_timestamp, trabajar con fechas y marcas de tiempo

transferencia de aprendizaje, Formas de usar el aprendizaje profundo en Spark, Transferencia de aprendizaje

transformaciones

fundamentos de, Transformaciones

operaciones principales, transformaciones de tramas de datos

Funciones personalizadas definidas por el usuario ­Funciones definidas por el usuario

Creación de DataFrame, Creación de DataFrames

en conjuntos de datos, transformaciones

ejemplo de extremo a extremo, un ejemplo de extremo a extremo : marcos de datos y SQL

localización de API, dónde buscar API

en API de Streaming Estructurado, Transformaciones y Acciones, Transformaciones en Streams­Joins

trabajar con diferentes tipos de datos, Trabajar con funciones booleanas definidas por el usuario
Machine Translated by Google

transformadores

ejemplo, Ingeniería de características con transformadores­ Ingeniería de características con transformadores

formateo de modelos según el caso de uso, formateo de modelos según su caso de uso

Transformador de características de interacción, Interacción

localizar, formatear modelos según su caso de uso

Transformadores persistentes y persistentes

propiedades, Propiedades del transformador

propósito de, conceptos de MLlib de alto nivel, transformadores

RFórmula, RFórmula

transformadores de datos de texto, transformadores de datos de texto­Word2Vec

Transformador de tokenizador, Transformadores, Texto de tokenización

escribir personalizado, escribir un transformador personalizado

método treeAgregate, agregado

activadores, transmisión estructurada, activadores, cuando se emiten datos (activadores)

ventanas que caen, ventanas que caen ­ventanas que caen

ajuste (ver ajuste de rendimiento)

seguridad de tipos, Conjuntos de datos: API estructuradas con seguridad de tipos, ¿ Qué API de Spark usar?

convenciones tipográficas, convenciones utilizadas en este libro

tu

subconsultas de predicado no correlacionadas, subconsultas de predicado no correlacionadas

consultas escalares no correlacionadas, consultas escalares no correlacionadas

subconsultas no correlacionadas, Subconsultas

gráficos no dirigidos, análisis de gráficos

plataformas de software unificadas, filosofía de Apache Spark

uniones, Concatenación y Anexión de Filas (Unión)

pruebas unitarias, resiliencia y evolución de la lógica empresarial: conexión a marcos de pruebas unitarias
Machine Translated by Google

plan lógico no resuelto, planificación lógica

aprendizaje sin supervisión

bisectriz ­means, bisectriz k­means Resumen

Modelos de mezcla gaussiana, Modelos de mezcla gaussiana

Asignación latente de Dirichlet, Asignación latente de Dirichlet­Ejemplo

escalabilidad del modelo, escalabilidad del modelo

casos de uso para, aprendizaje no supervisado, casos de uso

­means, k­means­k­means Resumen de métricas

actualizar el modo de salida, modo de actualización

actualizaciones, en tiempo real, Actualizar datos para servir en tiempo real

actualizar aplicaciones, actualizar el tamaño de su aplicación y volver a escalar su aplicación

mayúsculas, Trabajar con cadenas

segmentación de usuarios, aprendizaje no supervisado

Funciones de agregación definidas por el usuario (UDAF), Funciones de agregación definidas por el usuario

Funciones definidas por el usuario (UDF), Funciones definidas por el usuario ­Funciones definidas por el usuario, Agregar a

Tipos complejos, funciones definidas por el usuario, tipos de RDD, funciones definidas por el usuario (UDF),

Actualización de su código de aplicación de transmisión, Funciones definidas por el usuario ­Funciones definidas por el usuario

USANDO, Creando Tablas

varianza, cálculo, varianza y desviación estándar

Tipo de datos vectoriales (MLlib), tipos de datos de bajo nivel, escalado y normalización

Ensamblador de vectores, Ensamblador de vectores

VectorIndexer, indexación en vectores

UDF vectorizado, funciones definidas por el usuario (UDF), integración de Pandas

versiones, actualización, Actualización de su versión de Spark

vértices, análisis gráfico

vistas (Spark SQL)


Machine Translated by Google

creando, Creando Vistas

soltando, Soltando Vistas

propósito de, Vistas

tamaño de vocabulario (vocabSize), Conversión de palabras en representaciones numéricas

marcas de agua, Marcas de agua, Manejo de datos tardíos con marcas de agua­Manejo de datos tardíos con
marcas de agua

método where, filas de filtrado

espacios en blanco, eliminar, trabajar con cadenas

amplias dependencias, Transformaciones

ventanas

basado en conteo, mapGroupsWithState­mapGroupsWithState

sobre columnas de series temporales, transmisión estructurada

partición basada en deslizamiento, partición basada en una ventana deslizante

ventanas correderas, Ventanas correderas ­ Ventanas correderas

conversión de marca de tiempo, Windows en tiempo de evento

ventanas que caen, ventanas que caen ­ventanas que caen

agregaciones únicas usando, conjuntos de agrupación de funciones de ventana

withColumnRenamed método, DataFrames y SQL

combinaciones de palabras, creación, Creación de combinaciones de palabras

Palabra2Vec, Palabra2Vec

palabras, convertir en números, Convertir palabras en representaciones numéricas­Término frecuencia–frecuencia

inversa del documento

método de escritura, transformadores persistentes

escribir datos

conceptos básicos de, conceptos básicos de escritura de datos

estructura de la API principal, estructura de la API de escritura


Machine Translated by Google

depuración, lecturas y escrituras lentas

modo de guardado, Modos de guardado

XGBoost, Random Forest y Gradient­Boosted Trees, paquetes externos

HILO (ver Hadoop HILO)

cremalleras, cremalleras
Machine Translated by Google

Sobre los autores

Bill Chambers es un gerente de producto en Databricks enfocado en ayudar a los clientes a tener éxito
con sus iniciativas de análisis y ciencia de datos a gran escala usando Spark y Databricks.

Bill también escribe regularmente en blogs sobre ciencia de datos y big data y presenta en
conferencias y reuniones. Tiene una maestría en Sistemas de Información de la Escuela de Información
de UC Berkeley, donde se centró en la ciencia de datos.

Matei Zaharia es profesor asistente de informática en la Universidad de Stanford y tecnólogo jefe en


Databricks. Comenzó el proyecto Spark en UC Berkeley en 2009, donde era estudiante de doctorado, y
continúa sirviendo como vicepresidente en Apache. Matei también co­inició el proyecto Apache Mesos y
es un committer en Apache Hadoop. El trabajo de investigación de Matei fue reconocido a través
del Premio de Disertación Doctoral ACM 2014 y el Premio de Investigación de Sistemas VMware.
Machine Translated by Google

Colofón

El animal de la portada de Spark: The Definitive Guide es el milano de cola bifurcada (Elanoides forficatus). Estas rapaces,
que se encuentran en lugares de bosques y humedales que van desde el sur de Brasil hasta el sureste de los Estados
Unidos, subsisten con pequeños reptiles, anfibios y mamíferos, así como con insectos grandes. Construyen sus nidos cerca
del agua.

Las cometas de cola de golondrina tienden a tener entre 20 y 27 pulgadas de largo y se deslizan por el aire con alas
que se extienden alrededor de 4 pies, usando sus colas bifurcadas para orientarse. Su plumaje crece en un
sorprendentemente contrastante blanco y negro, y pasan la mayor parte de su tiempo en el aire, incluso rozando la superficie
de los cuerpos de agua para beber en lugar de permanecer en el suelo.

Entre las especies de aves rapaces, Elanoides forficatus son animales sociales y, a menudo, anidan muy cerca o se
posan para pasar la noche en grandes grupos comunales. Durante la migración, pueden viajar en grupos de cientos o miles.

Muchos de los animales de las portadas de O'Reilly están en peligro de extinción; todos ellos son importantes para el mundo.
Para obtener más información sobre cómo puede ayudar, visite animals.oreilly.com.

La imagen de la portada es de The Royal Natural History de Lydekker . Las fuentes de la portada son URW Typewriter
y Guardian Sans. La fuente del texto es Adobe Minion Pro; la fuente del encabezado es Adobe Myriad Condensed; y la
fuente del código es Ubuntu Mono de Dalton Maag.

También podría gustarte