03 Spark The Definitive Guide ESP
03 Spark The Definitive Guide ESP
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: 8009989938 o [email protected].
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.
9781491912218
[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.
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
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.
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.
Muestra texto que debe reemplazarse con valores proporcionados por el usuario o por valores determinados por
contexto.
CONSEJO
NOTA
ADVERTENCIA
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 CDROM 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., 9781491912218.”
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, AddisonWesley 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, McGrawHill, Jones & Bartlett y
Course Tecnología, entre otros.
Cómo contactarnos
Dirija sus comentarios y preguntas sobre este libro a la editorial:
Sebastopol, CA 95472
7078290104 (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.
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.
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
Yin Huai
tim cazador
xiao li
cheng lian
Machine Translated by Google
Xiangrui Meng
kris mok
jose rosen
Srinath Shankar
Takuya Ueshin
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
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 11 ilustra todos los componentes y bibliotecas que Spark ofrece a los usuarios finales.
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.
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 clavevalor
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 sparkpackages.org.
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.
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.
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
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.
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 spark2.2.0binhadoop2.7.tgz cd
spark2.2.0binhadoop2.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.
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.
./bin/sparkshell
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.
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/sparksql
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.
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 21 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.
En la Figura 21, 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.
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.
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/sparkshell 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 sparksubmit, 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
<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 23 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
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 24.
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:
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.
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 26 muestra una IU de ejemplo para un trabajo de Spark donde se ejecutaron
dos etapas que contenían nueve tareas.
Machine Translated by Google
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.
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/flightdata/csv/2015summary.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 =
# en Python
flightData2015 =
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 27 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 27. 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)
¡Especifiquemos algunas transformaciones más! Ahora, ordenemos nuestros datos de acuerdo con la
columna de recuento, que es de tipo entero. La figura 28 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 28).
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()
¡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)
La Figura 29 ilustra esta operación. Tenga en cuenta que, además de las transformaciones lógicas, también
incluimos el recuento de particiones físicas.
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.
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 =
sqlWay.explain
dataFrameWay.explain
# en Python
sqlWay = chispa.sql("""
SELECCIONE DEST_COUNTRY_NAME, cuenta(1)
DESDE flight_data_2015
GRUPO POR DEST_COUNTRY_NAME
""")
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
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:
// 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 210 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 210
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.
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
# en Python
flightData2015\ .groupBy("DEST_COUNTRY_NAME")
\ .sum("count")
\ .withColumnRenamed("sum(count)", "destination_total")
\ .sort(desc("destination_total"))
== 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
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
Transmisión estructurada
Conjuntos de datos distribuidos resistentes (RDD): las API de bajo nivel de Spark
SparkR
Una vez que haya realizado el recorrido, podrá saltar a las partes correspondientes del libro para encontrar respuestas a sus
preguntas sobre temas particulares.
sparksubmit 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/sparkenviar \
class org.apache.spark.examples.SparkPi \
master local \ ./
examples/jars/sparkexamples_2.112.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 sparksubmit
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/sparkenviar \
Machine Translated by Google
maestro local \ ./
ejemplos/src/main/python/pi.py 10
Al cambiar el argumento maestro de sparksubmit, 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.
sparksubmit 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.
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
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
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.
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,20101201 08:26:00,2.55,17...
536365,71053,LINTERNA DE METAL BLANCO,6,20101201 08:26:00,3.39,17850.0,Reino Unido...
536365,84406B,PERCHA CORAZONES CUPIDO CREMA,8,20101201 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/retaildata/byday/*.csv")
staticDataFrame.createOrReplaceTempView("retail_data") val
staticSchema = staticDataFrame.schema
# en Python
staticDataFrame = spark.read.format("csv")
\ .option("header", "true")
\ .option("inferSchema", "true")\ .load("/
data/retaildata /pordí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}
.sum("coste_total") .show(5)
# en Python
desde pyspark.sql.functions import window, column, desc, col
Vale la pena mencionar que también puede ejecutar esto como código SQL, tal como vimos en el capítulo
anterior.
+++ +
|IdCliente| ventana| suma(coste_total)|
+++ +
| 17450.0|[20110920 00:00...| 71601.44|
...
| nulo|[20111208 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 =
# en Python
streamingDataFrame =
.format("csv")
\ .option("header", "true")\ .load("/
data/retaildata/byday/*.csv")
Configuremos la misma lógica comercial que la manipulación anterior de DataFrame. Realizaremos una suma en el proceso:
// en Scala
val compraPorClientePorHora = streamingDataFrame
# en Python
buyByCustomerPerHour =
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:
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.
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 KMEANS?
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 =
# en Python
desde pyspark.sql.functions import date_format, col preppedDataFrame
= staticDataFrame\ .na.fill(0)\ .withColumn("day_of_week",
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 < '20110701'")
val testDataFrame = preppedDataFrame
.where("FechaFactura >= '20110701'")
# en Python
trainDataFrame = preppedDataFrame\
.where("FechaFactura < '20110701'")
testDataFrame = preppedDataFrame\
.where("FechaFactura >= '20110701'")
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
# 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
# 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
# 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.
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
# 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.
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
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/flightdata/csv/2015summary.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.
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 31.
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
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
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.
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.
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.
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.
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()
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 de Java correctos, debe usar los métodos de fábrica en el siguiente paquete:
Los tipos de Python a veces tienen ciertos requisitos, que puede ver enumerados en la Tabla 41, al igual que Scala
y Java, que puede ver enumerados en las Tablas 42 y 43, respectivamente. Para trabajar con los tipos de Python
correctos, utilice lo siguiente:
Las siguientes tablas proporcionan la información de tipo detallada para cada uno de los enlaces de idioma de
Spark.
ArrayType(elementType,
[contiene Nulo]). Nota la
tipo de matriz lista, tupla o matriz
valor predeterminado de
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.
Tipo de datos Tipo de valor en Scala API para acceder o crear un tipo de datos
TipoEntero En t TipoEntero
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 datos Tipo de valor en Java API para acceder o crear un tipo de datos
Tipos de datos.createDecimalType()
DecimalType java.math.BigDecimal
DataTypes.createDecimalType(precisión, escala).
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)
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.
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:
3. Spark transforma este plan lógico en un plan físico, verificando las optimizaciones a lo largo
el camino.
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 41 muestra el proceso.
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 42 ilustra este proceso.
Machine Translated by Google
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 43. 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).
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
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
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.
// en Scala val
df = spark.read.format("json") .load("/data/
flightdata/json/2015summary.json")
# en Python df
= spark.read.format("json").load("/data/flightdata/json/2015summary.json")
Discutimos que un DataFame tendrá columnas y usamos un esquema para definirlas. Echemos un vistazo al esquema
en nuestro DataFrame actual:
df.imprimirEsquema()
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/flightdata/json/2015summary.json").schema
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/flightdata/json/2015summary.json").schema
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
# 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/flightdata/json/2015summary.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.
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
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 y las transformaciones de esas columnas se compilan en el mismo plan lógico que las
expresiones analizadas.
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.
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/flightdata/json/2015summary.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.
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.
Podemos cambiar el orden de las filas en función de los valores de las columnas.
Machine Translated by Google
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.
// en Scala val
df = chispa.read.format("json") .load("/data/
flightdata/json/2015summary.json")
df.createOrReplaceTempView("dfTable")
# en Python
df = spark.read.format("json").load("/data/flightdata/json/2015summary.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}
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) ])
++++
| 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
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
++
|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
+++
|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
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
++++ +
|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
+++
| promedio(recuento)|recuento(DISTINTO DEST_COUNTRY_NAME)|
+++
|1770.765625| 132|
+++
// 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
SELECCIONE *, 1 como uno DESDE dfTable LIMIT 2
+++++
|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
++++ +
|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
// en Scala
df.withColumnRenamed("DEST_COUNTRY_NAME", "dest").columns
# en Python
df.withColumnRenamed("DEST_COUNTRY_NAME", "dest").columns
// en Scala
importar org.apache.spark.sql.functions.expr
# 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 ColumnName`")).columns
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
dfWithLongColName.drop("ORIGIN_COUNTRY_NAME", "DEST_COUNTRY_NAME")
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:
en SQL
SELECCIONE * DESDE dfTable DONDE cuenta < 2 LÍMITE 2
++++
|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
++++
|DEST_COUNTRY_NAME|ORIGIN_COUNTRY_NAME|recuento|
++++
Estados Unidos| Singapur| 1|
|| Moldavia| Estados Unidos| 1|
++++
// 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()
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
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)
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)
# en Python
df.union(newDF)
\ .where("count = 1")
\ .where(col("ORIGIN_COUNTRY_NAME") != "Estados Unidos")
\ .show()
++++
|DEST_COUNTRY_NAME|ORIGIN_COUNTRY_NAME|recuento|
++++
| Estados Unidos| Croacia| 1|
...
Machine Translated by Google
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
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/flightdata/json/*
summary.json") .sortWithinPartitions("count")
# en Python
spark.read.format("json").load("/data/flightdata/json/*summary.json")
\ .sortWithinPartitions("count")
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.
// 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"))
// 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)
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
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,
Booleanos
Números
Instrumentos de cuerda
manejo nulo
tipos complejos
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:
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/retaildata/byday/20101201.csv")
df.imprimirEsquema()
df.createOrReplaceTempView("dfTable")
# en pitón
df = chispa.leer.formato("csv")\
.option("encabezado", "verdadero")\
.opción("inferirEsquema", "verdadero")\
.load("/data/retaildata/byday/20101201.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|20101201 08:26:00| 6| ...
|| 536365| 71053| LINTERNA METAL BLANCO| 20101201 08:26:00| ...
...
536367| 21755|BLOQUE CONSTRUYENDO 3|20101201 08:34:00| 4| ...
|| 536367| AMOR...| 21777|CAJA DE RECETAS CON M...| 20101201 08:34:00| ...
++++ ++...
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
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
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
# 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)
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
+++++ ++...
|Número de factura|Código de existencias| Descripción|Cantidad| Fecha de factura|Precio unitario|...
+++++ ++...
536544| PUNTO|FRANQUEO DOTCOM| 1|20101201 14:32:00| 569.77|... 1|20101201
|| 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)))
# 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)))
# 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.
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
# en Python
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.
// 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
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.
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 |
++
// en escala
Machine Translated by Google
# 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| 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"
en SQL
SELECCIONAR
+++
| 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)"
en SQL
Machine Translated by Google
+++
| 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) )
# 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
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.
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
# 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)|
+++
| 20170612| 20170622|
+++
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("20160101")).alias("inicio"),
# 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("20160101")).alias("inicio"),
to_date(lit("20170522")).alias("fin"))
\ .select( meses_entre(col("inicio"), col("fin"))).mostrar(1)
en SQL
SELECCIONE hasta_fecha('20160101'), meses_entre('20160101', '20170101'),
fechado('20160101', '20170101')
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("20170101")) .select(to_date(col ("fecha"))).mostrar(1)
# en Python
desde pyspark.sql.functions import to_date, lit
spark.range(5).withColumn("date", lit("20170101"))
\ .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
mesdía a añodíames. Spark no podrá analizar esta fecha y en su lugar devolverá un valor nulo:
dateDF.select(to_date(lit("20162012")),to_date(lit("20171211"))).show(1)
+++
|hasta la fecha(20162012)|hasta la_fecha(20171211)|
+++
| nulo| 20171211|
+++
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 = "yyyyddMM" val
cleanDateDF =
spark.range(1).select( to_date(lit("20171211") , formato de
fecha). alias ("fecha"), to_date (lit ("20172012"), formato de fecha). alias ("fecha 2"))
cleanDateDF.createOrReplaceTempView("dateTable2")
# en Python
desde pyspark.sql.functions import to_date
dateFormat = "yyyyddMM"
cleanDateDF =
spark.range(1).select( to_date(lit("20171211"), dateFormat).alias("
fecha"), to_date(lit("20172012"), formato de fecha).alias("fecha2"))
cleanDateDF.createOrReplaceTempView("dateTable2")
en SQL
SELECCIONE to_date(fecha, 'aaaaddMM'), to_date(fecha2, 'aaaaddMM'), to_date(fecha)
DESDE dateTable2
+++
| fecha| fecha2|
+++
|20171112|20171220|
+++
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, 'aaaaddMM'), to_timestamp(fecha2, 'aaaaddMM')
DESDE dateTable2
++
|to_timestamp(`fecha`, 'aaaaddMM')|
++
| 20171112 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("20170101", "yyyyddMM") 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 aaaaMMdd si estamos comparando una fecha:
Un punto menor es que también podemos establecer esto como una cadena, que Spark analiza en un literal:
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.
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()
en SQL
SELECCIONAR
+++++
| un| b| c| d|
+++++
|valor_devuelto|null|valor_devuelto|valor_devuelto|
+++++
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
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:
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.
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:
// 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
# 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
++
|matriz_col[0]|
++
BLANCO|
|| BLANCO|
++
Longitud de la matriz
// 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
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
++
|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 61 ilustra el proceso.
// en Scala
importar org.apache.spark.sql.functions.{dividir, explotar}
# en Python
desde pyspark.sql.functions importar dividir, explotar
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
++++
| 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 clavevalor. 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
++
| 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)
++
|complex_map[LINTERNA DE METAL BLANCO]|
++
nulo|
|| 536365|
++
// 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)
+++
| llave| valor|
+++
|CABEZA COLGANTE BLANCO...|
536365| | LINTERNA METAL BLANCO|536365|
+++
// 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
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}
# en Python
desde pyspark.sql.functions import get_json_object, json_tuple
jsonDF.selectExpr(
"json_tuple(jsonString, '$.myJSONKey.myJSONValue[1]') como columna").show(2)
+++
|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
# 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)
+++
|jsontostructs(nuevoJSON)| nuevoJSON|
+++
| [536365,WHITE HAN...|{"FacturaNo":"536...| |
[536365,WHITE MET...|{"FacturaNo":"536...|
+++
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
# 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 62 proporciona una descripción general del proceso.
Machine Translated by Google
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)
// en Scala
udfExampleDF.select(power3udf(col("num"))).show()
Machine Translated by Google
# en Python
desde pyspark.sql.functions import udf power3udf
= udf(power3)
# 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.
// 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.
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/datosminoristas/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/datosminoristas/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
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:
# 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
# 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.912186085635685E4| 1052.7280543902734| 1052.7...|
++ ++
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|
++++
// 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
++++
Machine Translated by Google
|FacturaNo|cantidad|recuento(Cantidad)|
++++
| 536596| 6| 6|
...
| C542604| 8| 8|
++++
// 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 71 ilustra cómo una fila dada puede caer en varios marcos.
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:
# 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,
+++++ ++
|IdCliente| fecha|Cantidad|quantityRank|quantityDenseRank|maxP...Cantidad|
+++++ ++
| 12346|20110118| 74215| 12346| 1| 1| 74215|
| 20110118| 74215| 12347|20101207| 2| 2| 74215|
| 36| 12347|20101207| 30| 1| 1| 36|
| 2| 2| 36|
...
| 12347|20101207| 12347| 12| 4| 4| 36|
| 20101207| 12347| 6| 17| 5| 36|
| 20101207| 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 groupby.
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|
++++
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 groupby 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:
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|
|20101201|Reino Unido| | 23949|
20101201| Alemania| 117|
|20101201| Francia| 449|
...
|20101203| | Francia| 239|
20101203| | Italia| 164|
20101203| 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()
++++
|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?
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:
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}
+++++
|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).
+++
| fecha|USA_sum(Cantidad)|
+++
|20111206| | nulo|
20111209| | nulo|
20111208| | 196|
20111207| 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.
Para crear un UDAF, debe heredar de la clase base UserDefinedAggregateFunction e implementar los siguientes
métodos:
determinista es un valor booleano que especifica si este UDAF devolverá el mismo resultado para una
entrada dada
actualizar describe cómo debe actualizar el búfer interno en función de una fila determinada
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) ::
}
Machine Translated by Google
}}
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._
+++
|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 SPARK19439.
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
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
Combinaciones naturales (realice una combinación haciendo coincidir implícitamente las columnas entre los dos conjuntos
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
// 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
// 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
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"
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"
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
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"
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|...
++++++ ++
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"
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
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"
+++++
| 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")
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|
Las antiuniones izquierdas son lo opuesto a las semiuniones 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
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.
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
++++ +++
|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|
++++ +++
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:
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()
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:
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()
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()
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.
Podemos evitar este problema por completo si cambiamos el nombre de una de nuestras columnas antes de la unión:
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.
Cuando une una mesa grande con otra mesa grande, termina con una combinación aleatoria, como la
que se ilustra en la Figura 81.
Machine Translated by Google
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 81, 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
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 82.
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:
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
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:
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
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
Como se mencionó, Spark tiene numerosas fuentes de datos creadas por la comunidad. He aquí sólo una pequeña muestra:
casandra
HBase
MongoDB
XML
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.
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 clavevalor
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.
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
el formato
el esquema
El modo de lectura
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.
chispa.read.format("csv")
.option("modo",
"FAILFAST") .option("inferSchema",
"true") .option("ruta", "ruta/al/
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 91 enumera los modos de lectura.
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
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.
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
// 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.
// en Scala
dataframe.write.format("csv") .option("modo",
"OVERWRITE") .option("dateFormat",
"yyyyMMdd") .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 92 enumera los modos de guardado.
adjuntar Agrega los archivos de salida a la lista de archivos que ya existen en esa ubicación
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 93 presenta las opciones disponibles en el lector CSV.
Potencial
Clave de lectura/escritura Por defecto Descripción
valores
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.
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 aaaaMMdd para cualquier columna que sea
java tipo de fecha
Formato de datos simple.
Cualquier cadena o
personaje que aaaaMM 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.
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.
ignorado
Especifica si todos
los valores deben estar encerrados
Escribir cotizartodo cierto, falso FALSO entre comillas, a diferencia de
simplemente escapando de valores que
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",
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/flightdata/csv/
2010summary.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.
// en Scala
val csvFile = spark.read.format("csv")
.option("encabezado", "verdadero").option("modo", "FAILFAST").schema(myManualSchema) .load("/
data/flightdata/csv/2010summary.csv")
# en Python
csvFile = spark.read.format("csv")
\ .option("header", "true")
\ .option("mode", "FAILFAST")
\ .option("inferSchema", "true" )\ .load("/
datos/datosdevuelo/csv/2010summary.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/mytsv
file.tsv")
# en Python
csvFile.write.format("csv").mode("overwrite").option("sep", "\t")\ .save("/tmp/mytsv
file.tsv")
Cuando enumera el directorio de destino, puede ver que mytsvfile es en realidad una carpeta con numerosos archivos
dentro:
$ ls /tmp/miarchivotsv.tsv/
/tmp/miarchivotsv.tsv/part0000035cf945319434a8c9c829f6ea9742b29.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 94 enumera las opciones disponibles para el objeto JSON, junto con sus descripciones.
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.
declara el
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
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")
// en Scala
spark.read.format("json").option("mode", "FAILFAST").schema(myManualSchema)
.load("/data/flightdata/json/2010summary.json").show(5)
# en Python
spark.read.format("json").option("modo", "FAILFAST")
\ .option("inferSchema", "true")\ .load("/
data/flightdata/json/ 2010resumen.json").show(5)
// en Scala
csvFile.write.format("json").mode("overwrite").save("/tmp/myjsonfile.json")
# en Python
csvFile.write.format("json").mode("overwrite").save("/tmp/myjsonfile.json")
$ ls /tmp/miarchivojson.json/
/tmp/miarchivojson.json/part00000tid543....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")
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).
chispa.read.format("parquet")
// en Scala
chispa.leer.formato("parquet")
.load("/data/flightdata/parquet/2010summary.parquet").show(5)
# en Python
spark.read.format("parquet")\
.load("/data/flightdata/parquet/2010summary.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 95 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.
rápido
// en Scala
csvFile.write.format("parquet").mode("overwrite") .save("/tmp/myparquet
file.parquet")
# en Python
csvFile.write.format("parquet").mode("overwrite")\ .save("/tmp/myparquet
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.
// en Scala
spark.read.format("orc").load("/data/flightdata/orc/2010summary.orc").show(5)
# en Python
spark.read.format("orc").load("/data/flightdata/orc/2010summary.orc").show(5)
Machine Translated by Google
// en Scala
csvFile.write.format("orc").mode("overwrite").save("/tmp/myjsonfile.orc")
# en Python
csvFile.write.format("orc").mode("overwrite").save("/tmp/myjsonfile.orc")
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.
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/sparkshell \ driverclass
path postgresql9.4.1207.jar \ jars postgresql9.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 96 enumera todas las opciones
que puede configurar cuando trabaja con bases de datos JDBC.
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.
// en Scala val
driver = "org.sqlite.JDBC" val path = "/data/flight
data/jdbc/mysqlite.db" val url = s"jdbc:sqlite:/${path}" val tablename =
"información_vuelo"
# en Python
driver = "org.sqlite.JDBC" ruta = "/data/
flightdata/jdbc/mysqlite.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
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 =
# 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", "micontraseñasecreta").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: ...
// 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
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
// 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
// en Scala
val newPath = "jdbc:sqlite://tmp/mysqlite.db"
csvFile.write.mode("overwrite").jdbc(newPath, tablename, props)
# en Python
newPath = "jdbc:sqlite://tmp/mysqlite.db"
csvFile.write.jdbc(newPath, tablename, mode="overwrite", properties=props)
Machine Translated by Google
// 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)
// 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.
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/flightdata/csv/2010summary.csv") .selectExpr("split(value,
',') as rows").show()
++
| filas|
++
|[DEST_COUNTRY_NAM...|
Machine Translated by Google
csvFile.select("DEST_COUNTRY_NAME").write.text("/tmp/simpletextfile.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/cincocsvfiles2.csv")
# en pitón
csvFile.limit(10).select("DEST_COUNTRY_NAME", "count")\
.write.partitionBy("recuento").text("/tmp/cincocsvfiles2py.csv")
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).
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/part00000767df509ec9747408e154e173d365a8b.csv /tmp/
multiple.csv/part00001767df509ec9747408e154e173d365a8b.csv /tmp/multiple .csv /
part00002767df509ec9747408e154e173d365a8b.csv /tmp/multiple.csv/
part00003767df509ec9747408e154e173d365a8b.csv /tmp/multiple.csv/part000
04767df509 ec9747408e154e173d365a8b.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/archivosparticionados.parquet")
# en Python
csvFile.limit(10).write.mode("overwrite").partitionBy("DEST_COUNTRY_NAME")\
.save("/tmp/archivosparticionados.parquet")
$ ls /tmp/archivosparticionados.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/partitionedfiles.parquet/DEST_COUNTRY_NAME=Senegal/
part00000tid.....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":
csvFile.write.format("parquet").mode("overwrite") .bucketBy(numberBuckets,
columnToBucketBy).saveAsTable("bucketedFiles")
$ ls /usuario/colmena/almacén/archivos en depósito/
part00000tid10205750976263326668....parquet part00000
tid10205750976263326668....parquet part00000
tid10205750976263326668....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.
Por ejemplo, los archivos CSV no admiten tipos complejos, mientras que Parquet y ORC sí.
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
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
ANSISQL 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 TPCDS.
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.
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
./bin/sparksql
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:
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/flightdata/json/2015
summary.json") .createOrReplaceTempView("some_sql_view") // DF => SQL
chispa.sql("""
Machine Translated by Google
# en Python
spark.read.json("/data/flightdata/json/2015summary.json")
\ .createOrReplaceTempView("some_sql_view") # DF => SQL
chispa.sql("""
SELECCIONE DEST_COUNTRY_NAME, suma (recuento)
DESDE some_sql_view GROUP BY
./sbin/startthriftserver.sh
Este script acepta todas las opciones de línea de comandos bin/sparksubmit. Para ver todas las opciones
disponibles para configurar este Thrift Server, ejecute ./sbin/startthriftserver.sh help. De forma
predeterminada, el servidor escucha en localhost:10000. Puede anular esto a través de variables
ambientales o propiedades del sistema.
export HIVE_SERVER2_THRIFT_PORT=<puerto de
escucha> export HIVE_SERVER2_THRIFT_BIND_HOST=<host
de escucha> ./sbin/startthriftserver.sh
\ master <uri maestro> \
...
./sbin/startthriftserver.sh \ hiveconf
hive.server2.thrift.port=<puerto de escucha> \ hiveconf
hive.server2.thrift.bind.host=<host de escucha> \ master < maestrouri>
Machine Translated by Google
...
./bin/línea recta
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.
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:
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:
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:
Finalmente, puede controlar el diseño de los datos escribiendo un conjunto de datos particionado, como vimos en
Capítulo 9:
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.
EXTERNAL TABLE.
Puede ver cualquier archivo que ya se haya definido ejecutando el siguiente comando:
También puede crear una tabla externa a partir de una cláusula de selección:
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
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:
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):
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:
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:
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:
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.
ADVERTENCIA
Esto elimina los datos de la tabla, así que tenga cuidado al hacer esto.
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.
Al igual que los marcos de datos, puede almacenar y recuperar tablas. Simplemente especifique qué tabla le gustaría
usar la siguiente sintaxis:
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:
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:
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:
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:
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:
.load("/data/flightdata/json/2015summary.json")
val just_usa_df = vuelos.where("dest_country_name = 'Estados Unidos'") just_usa_df.selectExpr("*").explain
O equivalente:
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í:
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.
La creación de bases de datos sigue los mismos patrones que ha visto anteriormente en este capítulo; sin embargo, aquí usas
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
Sin embargo, puede consultar diferentes bases de datos utilizando el prefijo correcto:
Puede ver qué base de datos está utilizando actualmente ejecutando el siguiente comando:
SELECCIONE base_de_datos_actual()
UTILIZAR predeterminado;
Descartar o eliminar bases de datos es igual de fácil: simplemente use la palabra clave DROP DATABASE:
Las consultas en Spark admiten los siguientes requisitos de ANSI SQL (aquí enumeramos el diseño del
expresión SELECCIONAR):
Machine Translated by Google
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
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:
Incluso puede consultar columnas individuales dentro de una estructura; todo lo que necesita hacer es usar la sintaxis de puntos:
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:
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:
Sin embargo, también puede crear una matriz manualmente dentro de una columna, como se muestra aquí:
También puede consultar listas por posición utilizando una sintaxis de consulta de matriz similar a Python:
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:
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:
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:
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):
Puede filtrar todos los comandos SHOW pasando una cadena con caracteres comodín (*). Aquí, podemos ver
todas las funciones que comienzan con "s":
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
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:
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.
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:
++
|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:
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.
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:
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!
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:
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 101. 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).
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
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.
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
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.
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;
Inmutable
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.
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.
Para comenzar a crear un conjunto de datos, definamos una clase de caso para uno de nuestros conjuntos de datos:
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:
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:
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
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:
El resultado es:
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:
El resultado es:
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:
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.
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:
Tenga en cuenta que terminamos con un conjunto de datos de una especie de par clavevalor, 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")
vuelos2.tomar(2)
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).
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:
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:
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:
== 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:
+++
| _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
Incluso podemos crear nuevas manipulaciones y definir cómo se deben reducir los grupos:
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
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!
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.
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.
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
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
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 clavevalor
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 clavevalor 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:
Opcionalmente, un particionador para RDD clavevalor (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 clavevalor.
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.
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).
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.
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. .
// 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
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()
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
// 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
# 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])
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
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:
recuentoAproximadoDistinto
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)
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()
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
palabras.primero()
máximo y mínimo
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.
Para guardar en un archivo de texto, simplemente especifique una ruta y, opcionalmente, un códec de compresión:
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 clavevalor. 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()
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.
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()
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()
} pw.cerrar() }
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 clavevalor.
Machine Translated by Google
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 clavevalor, 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:
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.
// 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)
Hay muchos métodos en RDD que requieren que coloque sus datos en un formato de clavevalor. Una pista de que esto
es necesario es que el método incluirá <algunaoperació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
clavevalor. 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])
// en Scala
palabra clave.mapValues(palabra => palabra.toUpperCase).collect()
# en Python
palabra clave.mapValues( palabra lambda: palabra.superior()).collect()
(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()
// 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")
// 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
# 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.
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()
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()
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.
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)
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
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
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:
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.
// en Scala val
df = spark.read.option("header", "true").option("inferSchema", "true")
.csv("/datos/datosminoristas/todos/")
val rdd = df.coalesce(10).rdd
# en Python df
= spark.read.option("header", "true").option("inferSchema", "true")\ .csv("/data/retaildata/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
}
}}
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:
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
}}
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
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
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 141.
Machine Translated by Google
// 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 clavevalor 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 142), 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
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
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/flightdata/parquet/2010
summary.parquet") .as[Vuelo]
# en Python
vuelos =
spark.read\ .parquet("/data/flightdata/parquet/2010summary.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 143 .
Machine Translated by Google
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
} def value():BigInt =
{ this.num
} def esCero():Booleano =
{ this.num == 0
// 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
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.
Lo que se necesita para ejecutar una aplicación Spark, como continuación al Capítulo 16.
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 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 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 151
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 151. 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.
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 151 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 152 muestra que el administrador de clústeres colocó nuestro
controlador en un nodo trabajador y los ejecutores en otros nodos trabajadores.
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 153, 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 153,
puede ver que el controlador se ejecuta en una máquina fuera del clúster pero que el
Machine Translated by Google
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.
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 154). 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.
./bin/sparkenviar \
class <clase principal> \
master <urlmaestra> \
deploymode cluster \ conf
<clave>=<valor> \ # otras
... opciones <jar
aplicación> \
[argumentosaplicación]
Lanzamiento
Ahora que el proceso del controlador se ha colocado en el clúster, comienza a ejecutar el código de
usuario (Figura 155). 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.
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 156. 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).
Terminación
Después de que se completa una aplicación Spark, el controlador finaliza con éxito o falla (Figura
157). 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
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:
.config("spark.sql.warehouse.dir", "/usuario/colmena/
almacén") .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
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
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.
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.
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
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.
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.)
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.1SNAPSHOT" scalaVersion :=
"2.11.8"
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:
// 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
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 "uberjar" o "fatjar" 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/sparksubmit \
class com.databricks.example.DataFrameExample \ master
local \ target/
scala2.11/example_2.110.1SNAPSHOT.jar "hola"
Machine Translated by Google
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 pyfiles de sparksubmit 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 sparksubmit:
# 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()
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 sparksubmit con esa información:
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>sparkcore_2.11</artifactId>
<versión>2.1.0</version> </
dependency>
<dependencia>
<groupId> org.apache.spark</groupId>
<artifactId>sparksql_2.11</artifactId>
<version>2.1.0</version> </
dependency>
<dependency>
<groupId>graphframes</groupId>
<artifactId>graphframes </artifactId>
<version>0.4.0spark2.1s_2.11</version> </
dependency>
</dependencies>
<repositorios>
<! lista de otros repositorios >
<repositorio>
<id>SparkPackagesRepo</id>
<url>http://dl.bintray.com/sparkpackages/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 =
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 sparksubmit:
$SPARK_HOME/bin/sparksubmit \
class com.databricks.example.SimpleExample \ master
local \ target/spark
example0.1SNAPSHOT.jar "hola"
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.
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.
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.
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.
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.
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, SparkShell 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 sparksubmit 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
Una vez que haya terminado su aplicación y haya creado un paquete o secuencia de comandos para ejecutar, sparksubmit
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 SparkSubmit. Anteriormente en este capítulo, le
mostramos cómo ejecutar sparksubmit; simplemente especifica tus opciones, el archivo JAR o script de la aplicación
y los argumentos relevantes:
./bin/sparkenviar \
class <clase principal> \
master <urlmaestra> \ deploy
mode <modoimplementación> \ conf
<clave>=<valor> \ # otras
... opciones <application
jaror 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 pyfiles.
Como referencia, la Tabla 161 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.
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
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
Lista separada por comas de repositorios remotos adicionales para buscar las coordenadas de Maven proporcionadas con
packages. repositorios
pyarchivos
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/sparkdefaults.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.
javaopciones
conductor
Entradas de ruta de biblioteca adicionales para pasar al
controlador. bibliotecaruta
conductor Entradas de rutas de clases adicionales para pasar al controlador. Tenga en cuenta que los archivos JAR agregados
ejecutor
Memoria por ejecutor (por ejemplo, 1000M, 2G) (Predeterminado: 1G).
memoria MEM
proxyusuario Usuario a suplantar al enviar la solicitud. Este argumento no funciona con principal / keytab.
NOMBRE
prolijo,
Imprimir salida de depuración adicional.
v
Grupo
Modos Conf. Descripción
Gerentes
conductor
Ser único Grupo Núcleos para el controlador (Predeterminado: 1).
núcleos NÚM
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
principal Principal que se usará para iniciar sesión en KDC, mientras se ejecuta
HILO Cualquiera
PRINCIPAL en HDFS seguro.
./bin/sparkenviar \
class org.apache.spark.examples.SparkPi \
master spark://207.184.161.138:7077 \
executormemory 20G \
totalexecutorcores 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/sparkenviar \
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
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
Las propiedades de Spark controlan la mayoría de los parámetros de la aplicación y se pueden configurar mediante un
objeto SparkConf
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/sparkenv.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:
Cabe destacar que al establecer propiedades basadas en la duración del tiempo, debe usar el siguiente formato:
25ms (milisegundos)
5s (segundos)
3h (horas)
5d (días)
1y (años)
Propiedades de la aplicación
Las propiedades de la aplicación son aquellas que establece desde Sparksubmit 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 163 presenta una lista de las propiedades de la aplicación actual.
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
OutOfMemoryErrors.
inicializa SparkContext . (por ejemplo , 1 g, 2 g). Nota: en el modo de cliente, esto no debe
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,
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")
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.
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
documentación de Spark. Las configuraciones más comunes para cambiar son spark.executor.cores (para controlar la cantidad de
Variables ambientales
Puede configurar ciertos ajustes de Spark a través de variables de entorno, que se leen desde el script conf/sparkenv.sh en el directorio
donde está instalado Spark (o conf/sparkenv.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/sparkenv.sh no existe de forma predeterminada cuando se instala Spark. Sin embargo, puede copiar conf/
sparkenv.sh.template para crearlo. Asegúrese de hacer la copia ejecutable.
JAVA_HOME
PYSPARK_PYTHON
Ejecutable binario de Python para usar con PySpark tanto en el controlador como en los trabajadores (el valor
PYSPARK_DRIVER_PYTHON
Ejecutable binario de Python para usar con PySpark solo en el controlador (el valor predeterminado es PYSPARK_PYTHON).
SPARKR_DRIVER_R
Ejecutable binario R para usar con el shell SparkR (el valor predeterminado es R). La
SPARK_LOCAL_IP
SPARK_PUBLIC_DNS
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 sparkenv.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
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/sparkdefaults.conf . Las variables de
entorno que se configuran en sparkenv.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.
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
Este capítulo explora la infraestructura que necesita para que usted y su equipo puedan ejecutar
Aplicaciones de chispa:
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.
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 clavevalor escalable. Esto incluye configurar la replicación geográfica y la recuperación ante
desastres si es necesario.
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 clavevalor, 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.
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/startmaster.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://
masteripaddress: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/startslave.sh <URIdechispamaestra>
¡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.
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/startmaster.sh
Machine Translated by Google
$SPARK_HOME/sbin/startslaves.sh
$SPARK_HOME/sbin/startslave.sh
$SPARK_HOME/sbin/startall.sh
Inicia tanto un maestro como varios esclavos como se describió anteriormente.
$SPARK_HOME/sbin/stopmaster.sh
$SPARK_HOME/sbin/stopslaves.sh
Detiene todas las instancias de esclavos en las máquinas especificadas en el archivo conf/slaves .
$SPARK_HOME/sbin/stopall.sh
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 SparkSubmit.
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 sparksubmit 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 SparkSubmit. 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.
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: hdfssite.xml, que proporciona comportamientos predeterminados para el cliente HDFS;
y coresite.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/sparkenv.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.
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 sparkenv.sh para
trabajar con Mesos.
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:
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://
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.
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 sparksubmit 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
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 181).
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.
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).
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.
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.
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 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 182 muestra todas las pestañas disponibles en la interfaz de usuario 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 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
# en Python
.csv("/data/retaildata/all/onlineretaildataset.csv")
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 183.
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 184.
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
185 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
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 186).
Machine Translated by Google
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 187 , nuestro trabajo se divide en tres etapas (lo que corresponde a lo que vimos en la
pestaña SQL).
Estas etapas tienen más o menos la misma información que la que se muestra en la Figura 186, 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 188.
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 188, 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.
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
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.
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.
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.
Signos y síntomas
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
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
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).
Signos y síntomas
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.
Signos y síntomas
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.
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.
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
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.
Tratamientos potenciales
Muchas uniones se pueden optimizar (manual o automáticamente) para otros tipos de uniones. Nosotros
Machine Translated by Google
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.
Signos y síntomas
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.
Signos y síntomas
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
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
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.
Tratamientos potenciales
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 clavevalor, intente aislar sus trabajos de Spark de otros trabajos.
Signos y síntomas
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.
Signos y síntomas
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
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
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:
Uniones
Agregaciones
Datos en vuelo
Nodos trabajadores
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
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.
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.
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.
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.
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 maxexecutor. 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 maxexecutorcores, 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.
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.
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
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.
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á
Para recopilar estadísticas a nivel de columna, puede nombrar las columnas específicas:
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 SPARK16026 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
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.
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.
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.
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.
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.
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
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
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
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.
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 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
Esto puede causar confusión porque a veces puede esperar acceder a datos sin procesar, pero debido a que otra persona ya almacenó los
Hay diferentes niveles de almacenamiento que puede usar para almacenar en caché sus datos, especificando qué tipo de almacenamiento usar.
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.
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 191 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.
# 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/flightdata/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
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.
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
clavevalor.
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.
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.
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.
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 clavevalor (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.
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.
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.
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:
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 2105 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)
Procesar cada evento exactamente una vez a pesar de las fallas de la máquina
Determinar cómo actualizar los sumideros de salida a medida que llegan nuevos eventos
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
desordenados, existen varias formas de diseñar un sistema de transmisión. Aquí describimos las opciones de diseño más comunes, antes de
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.
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.
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 202 .
Machine Translated by Google
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).
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.
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
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.
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
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 adhoc 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
Archivos en un sistema de archivos distribuido como HDFS o S3 (Spark leerá continuamente archivos nuevos en un
directorio)
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:
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
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.
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
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
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
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).
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.
// en Scala val
static = spark.read.json("/datos/datosdeactividad/") val dataSchema
= static.schema
# en Python
estático = chispa.read.json("/datos/datosdeactividad/")
dataSchema = estático.esquema
raíz
| Arrival_Time: largo (anulable = verdadero)
| Creation_Time: largo (anulable = verdadero)
Machine Translated by Google
+++++ ++++
| Hora_de_llegada| | Hora_de_la_creación| Dispositivo|Índice| Modelo|Usuario|_c...ord|. gt| nulo|estar|0... X
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/actividaddatos")
# en Python
streaming = spark.readStream.schema(dataSchema).option("maxFilesPerTrigger", 1)\
.json("/datos/datosdeactividad")
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.
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",
# en Python
desde pyspark.sql.functions import expr
Machine Translated by Google
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",
# 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()
++++ ++
| 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).
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
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.
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.
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:
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
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
También hay opciones para configurar los tiempos de espera del consumidor de Kafka, los reintentos de recuperación y los intervalos.
// en Scala //
Suscríbete a 1 tema val ds1
= spark.readStream.format("kafka")
.option("kafka.bootstrap.servers",
# en Python #
Suscríbete a 1 tema df1 =
spark.readStream.format("kafka")\
.option("kafka.bootstrap.servers", "host1:puerto1,host2:puerto2")
\ .option("suscribirse", "tema1")\ .load()
clave: binaria
valor: binario
tema: cadena
partición: int
desplazamiento: largo
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.
// en Scala
ds1.selectExpr("tema", "CAST(clave COMO CADENA)", "CAST(valor COMO CADENA)")
.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)")\
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.
El escritor debe ser serializable, como si fuera una UDF o una función de mapa de conjunto de datos.
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.
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
} })
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:
nclk 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.
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.
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.
Soportado
Tipo de consulta
Tipo de consulta Salida notas
(continuación)
Modos
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
Para el disparador de tiempo de procesamiento, simplemente especificamos una duración como una cadena (también puede usar
// 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()
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()
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/flightdata/parquet/2010summary.parquet/") .schema
.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
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.
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
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
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
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 221).
Machine Translated by Google
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.
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.
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.
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
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/actividaddatos") val streaming =
# en Python
spark.conf.set("spark.sql.shuffle.partitions", 5) static = spark.read.json("/
data/activitydata") streaming = spark\ .readStream\ .schema(static.
esquema)
\ .option("maxFilesPerTrigger",
10)\ .json("/datos/actividaddatos")
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.
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.
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 222), 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()
# 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()
Esto nos muestra algo como el siguiente resultado, dependiendo de la cantidad de datos procesados cuando ejecutó la consulta:
++ +
|ventana |contar|
++ +
|[20150223 10:40:00.0,20150223 10:50:00.0]|11035| |[20150224
11:50:00.0,20150224 12:00:00.0]|18854|
...
|[20150223 13:40:00.0,20150223 13:50:00.0]|20870| |[20150223
11:20:00.0,20150223 11:30:00.0]|9392 |
++ +
raíz
| ventana: estructura (anulable = falso) | |
| inicio: marca de tiempo (anulable = verdadero) |
final: marca de tiempo (anulable = verdadero)
Machine Translated by Google
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()
# en Python
desde la ventana de importación de
pyspark.sql.functions , col withEventTime.groupBy(window(col("event_time"), "10 minutes"), "User").count()\
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 223 ilustra lo que
significar.
Machine Translated by Google
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"))
# 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()
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|
++ +
|[20150223 14:15:00.0,20150223 14:25:00.0]|40375| |[20150224
11:50:00.0,20150224 12:00:00.0]|56549|
...
|[20150224 11:45:00.0,20150224 11:55:00.0]|51898| |[20150223
10:40:00.0,20150223 10:50:00.0]|33200|
++ +
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
224.
Machine Translated by Google
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
# en Python
desde la ventana de importación de pyspark.sql.functions , col
\ .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.
++ +
|ventana |contar|
++ +
|[20150223 14:15:00.0,20150223 14:25:00.0]|9505 | |[20150224
11:50:00.0,20150224 12:00:00.0]|13159|
...
|[20150224 11:45:00.0,20150224 11:55:00.0]|12021| |[20150223
10:40:00.0,20150223 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.
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
# en Python
desde pyspark.sql.functions import expr
Machine Translated by Google
\ .groupBy("Usuario")\ .count()
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|
+++
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
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
Asigne grupos en sus datos, opere en cada grupo de datos y genere una o más filas para cada grupo. La API relevante
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)
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 timeout 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 211 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
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).
Para facilitar la lectura, configure la función que define cómo actualizará su estado en función de un determinado
fila:
} 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:
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
++++ +
|usuario|actividad| empezar| fin|
Machine Translated by Google
++++ +
un| bicicleta|20150223 13:30:...|20150223 14:06:...|
|| un| bicicleta|20150223 13:30:...|20150223 14:06:...|
...
| d| bicicleta|20150224 13:07:...|20150224 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.
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).
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:
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:
} 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.
importar org.apache.spark.sql.streaming.GroupStateTimeout
GroupStateTimeout.NoTimeout)(actualizaciónAcrossEvents)
+++
| dispositivo| anteriorPromedio|
+++
|nexo4_1| 4.660034012E4| |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
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.
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:
estado
}
def updateAcrossEvents(uid:String,
entradas: Iterador[InputRow],
oldState: GroupState[UserSession]):Iterator[UserSessionOutput] = {
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
Consultar esta tabla le mostrará las filas de salida para cada usuario durante este período de tiempo:
++++
|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
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.
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/actividaddatos") val
streaming =
.json("/datos/datosde
# en Python
estático = chispa.read.json("/datos/actividaddatos")
streaming =
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.
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.
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:
"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.
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.
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.
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).
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:
servidores)
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
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:
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.
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.
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.
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.
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.
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.
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.
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
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.
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.
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.
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.
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.
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 scikitlearn, 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 scikitlearn 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.
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 243.
Machine Translated by Google
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 scikitlearn.
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/simpleml")
df.orderBy("value2").show()
# en Python
df = spark.read.json("/data/simpleml")
df.orderBy("value2").show()
+++++
|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")
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:
términos concatenados; “+ 0” significa eliminar el intercepto (esto significa que el intercepto en y de la línea
Machine Translated by Google
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"
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.
// en Scala
val fitRF = supervisado.fit(df)
Machine Translated by Google
# en Python
equipadoRF = supervisado.ajuste(df)
preparadoDF = equipadoRF.transform(df)
preparadoDF.show()
+++++ ++
|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
# 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()
+++
|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.
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 244 ilustra este proceso.
Machine Translated by Google
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:
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
# 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 Kfold 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
# en Python
desde pyspark.ml.tuning import TrainValidationSplit tvs =
TrainValidationSplit()
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)
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.
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 245 ilustra flujos de trabajo comunes.
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 245.
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 clavevalor). 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
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.
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/datosminoristas/pordía/*.csv")
.coalesce(5)
.where("La descripción NO ES NULA")
val fakeIntDF = chispa.read.parquet("/data/simplemlintegers")
var simpleDF = chispa.read.json("/datos/simpleml")
val scaleDF = chispa.read.parquet("/data/simplemlscaling")
# en pitón
ventas = spark.read.format("csv")\
.option("encabezado", "verdadero")\
.opción("inferirEsquema", "verdadero")\
.load("/data/retaildata/byday/*.csv")\
.coalesce(5)\
.where("La descripción NO ES NULA")
fakeIntDF = chispa.read.parquet("/data/simplemlintegers")
simpleDF = chispa.read.json("/datos/simpleml")
scaleDF = chispa.read.parquet("/data/simplemlscaling")
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|20111205 08:38:00| 1...
...
| 580539| 22375|BOLSA AIRLINE VINTA...| 4|20111205 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 251 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.
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] |
++ +
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.
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.
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 onehot. En resumen, la codificación onehot 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 onehot 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:
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)
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
.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()
+++
|suma(Cantidad)|recuento(1)|IDCliente|
++++
| 119| 62| 14452.0|
...
| 138| 18| 15776.0|
++++
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]|
++++ +
// 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
# 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|
...
+++
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.
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.
// 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()
+++ +
|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
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
# 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).
// 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...|
+++ +
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")
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
# 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 OneHot
Indexar variables categóricas es solo la mitad de la historia. La codificación onehot 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).
// 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
+++ +
|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])|
+++ +
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
# 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 |[ , , , , ]] |
++ +
// 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()
+++ +
| 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
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 ngramas, es decir , secuencias de palabras de longitud n. Un ngrama 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 ngramas, 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 ngramas 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
"Grandes datos"
"Procesamiento de datos"
“Tratamiento realizado”
"Simplificado"
“Procesamiento simplificado”
Con los ngramas, 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
// 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, ...
++
++
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...
++
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")
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]) |
++ +
Otra forma de abordar el problema de convertir texto en una representación numérica es utilizar el término
frecuenciafrecuencia inversa del documento (TFIDF). En términos más simples, las medidas TFIDF
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,
# 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
# 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 “skipgrams” 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 "ngram".
Word2Vec funciona mejor con texto continuo y de forma libre en forma de fichas
// 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
# 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"])
Texto: [I, wish, Java, could, use, case, classes] => Vector:
[0.043090314205203734,0.035048123182994974,0.023512658663094044]
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.
// 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.
// 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 ChiSquare, 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
# en Python
desde pyspark.ml.feature import ChiSqSelector, Tokenizer tkn =
Tokenizer().setInputCol("Description").setOutputCol("DescOut") tokenized =
.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
// 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")
// 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()
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}
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.
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
Una empresa de financiación puede considerar una serie de variables antes de ofrecer un préstamo a una empresa o individuo.
Clasificación de noticias
Se puede entrenar un algoritmo para predecir el tema de un artículo de noticias (deportes, política, negocios, etc.).
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.
Regresión logística
Árboles de decisión
Bosques aleatorios
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:
Hiperparámetros del modelo (las diferentes formas en que podemos inicializar el modelo)
Puede configurar los hiperparámetros y los parámetros de entrenamiento en un ParamGrid como vimos en
Capítulo 24.
Á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
LBFGS. 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
// en escala
val bInput = chispa.read.format("parquet").load("/data/binaryclassification")
.selectExpr("características", "cast(etiqueta como doble) como etiqueta")
# en pitón
Machine Translated by Google
bInput = chispa.read.format("parquet").load("/data/binaryclassification")\
.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.
familia
Puede ser multinomial (dos o más etiquetas distintas; clasificación multiclase) o binaria (solo dos etiquetas
distintas; clasificación binaria).
elasticNetParam
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.0E6. 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
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.
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:
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)
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
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.
numArboles
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.
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
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
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
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
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.
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.
// 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
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.
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.
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()
Onevsrest 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 onevsrest.
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
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:
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.
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.
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.
regresión lineal
regresión isotónica
Machine Translated by Google
Árboles de decisión
Bosque aleatorio
Regresión de supervivencia
Este capítulo cubrirá los conceptos básicos de cada uno de estos modelos particulares proporcionando:
Hiperparámetros del modelo (las diferentes formas en que podemos inicializar el modelo)
Puede buscar sobre los hiperparámetros y los parámetros de entrenamiento usando un ParamGrid, como vimos
en el Capítulo 24.
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
// 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.
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
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 272 muestra las funciones de enlace disponibles 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.
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
linkPower
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()
# en Python
desde pyspark.ml.regression import GeneralizedLinearRegression glr =
GeneralizedLinearRegression()
\ .setFamily("gaussian")
\ .setLink("identity")
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:
Rcuadrado
los residuos
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.
impureza
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
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)
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.
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.
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
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 271 hace que sea mucho más
fácil de entender.
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.
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
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()
# 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"Rsquared = $
{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 "Rsquared: " +
str(metrics.r2) print "MAE: " +
str(metrics.meanAbsoluteError) print "Varianza explicada:
"
+ str(metrics.explainedVariance)
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
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.
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
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.
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.
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
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
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
# 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")
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()
# 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
# 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")
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"),
# 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) ))
# 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)
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
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:
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.
// en Scala
importar org.apache.spark.ml.feature.VectorAssembler
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/retaildata/byday/
ventas.cache()
ksignifica
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.
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)
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 kmedias
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.
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)
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)
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.
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)
// 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()
Para ingresar nuestros datos de texto en LDA, tendremos que convertirlos a un formato numérico. Puede usar
CountVectorizer para lograr esto.
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).
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
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
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
semilla
Este modelo también admite la especificación de una semilla aleatoria para la reproducibilidad.
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
# 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")
// 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
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 301 muestra un gráfico de muestra.
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 302 muestra un
gráfico dirigido donde los bordes son direccionales.
Machine Translated by Google
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.
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á:
// en Scala
val bikeStations = spark.read.option("header","true") .csv("/data/
bikedata/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/
bikedata/201508_station_data.csv")
Machine Translated by Google
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())
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
# 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)
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)")
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|
+++
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.
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)
++ +
|id |enGrado|
++ +
Machine Translated by Google
// 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)
++ +
|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|
++ +
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 305.
Figura 306. 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
// 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...| ...
++++ +++
// 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
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.
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
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 RCNN 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
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
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
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.
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
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.
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:
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 311 resume las diversas bibliotecas de aprendizaje profundo y los principales casos de uso que admiten:
Machine Translated by Google
DL subyacente
Biblioteca casos de uso
estructura
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.
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:
Exportación de modelos como funciones Spark SQL para que todo tipo de usuarios lo puedan tomar fácilmente
ventaja del aprendizaje profundo; y
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.
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:
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
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):
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:
Con nuestro DataFrame de ejemplos, podemos inspeccionar las filas e imágenes en las que cometimos errores
en el entrenamiento anterior:
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.
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)
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
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.
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:
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
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 321 muestra la arquitectura fundamental para estos lenguajes específicos.
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.
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:
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 (SPARK21190), 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.
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
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:
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 321, 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(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:
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:
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:
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:
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:
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:
recopilar
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:
header="true",
inferSchema="true")
vuelo.datos < read.df(
"/data/flightdata/parquet/2010summary.parquet", "parquet")
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:
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:
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.
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:
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:
marco.de.datos.remoto
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:
marco.de.datos.remoto
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 Rfirst 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:
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:
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.
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:
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_naive_bayes: NaiveBayes
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
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 sparkshell o sparksubmit.
Spark bigquery
chispa avro
Elasticsearch
Magallanes
Marcos de gráficos
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:
Ahora que agregamos esta línea, podemos incluir una dependencia de biblioteca para nuestro paquete Spark:
)
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 sparkshell y sparksubmit
que usaría para ejecutar su código.
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 331 muestra un mapa de
reuniones relacionadas con Spark en Meetup.com.
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
acumuladores
reconocimientos, Agradecimientos
acciones, Acciones
agregaciones
grupoPorClave, grupoPorClave
en RDD, AgregacionesfoldByKey
reducirPorClave, reducirPorClave
Funciones de agregación definidas por el usuario (UDAF), Funciones de agregación definidas por el usuario
Detección de anomalías
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 Mesos, la arquitectura de una aplicación Spark, implementación de Spark, Spark en Mesos
acciones, Acciones
beneficios de, ¿ Qué es Apache Spark?, Contexto: El problema de Big Data, Conclusión, Ecosistema y Comunidad
Paquetes
ejecutando, Ejecutando Spark, Ejecutando Spark en la nube, Cómo se ejecuta Spark en un clúster
Conclusión
chispa de arranque
componentes y bibliotecas del kit de herramientas, ¿ Qué es Apache Spark?, Un recorrido por el conjunto de herramientas de Spark : el
alertando, alertando
aproximaciones, agregaciones
arreglos, arreglosexplotar
Machine Translated by Google
matriz_contiene, matriz_contiene
ajuste automático de modelos, evaluadores para clasificación y ajuste automático de modelos, evaluadores y ajuste automático de
modelos
agrupamiento, agrupamiento
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 tiempoTrabajar con fechas y marcas de tiempo
clases de casos, en Scala: clases de casos, conjuntos de datos y RDD de clases de casos
Paquetes Populares
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
puntos de control, puntos de control, tolerancia a fallas y puntos de control, componentes conectados
clasificación
evaluadores y ajuste del modelo de automatización, evaluadores para clasificación y modelo de automatización
Afinación
bosques aleatorios y árboles potenciados por degradado, bosques aleatorios y árboles potenciados por degradado
Parámetros de predicción
creación, Lanzamiento
ejemplos de código, obtención y uso, uso de ejemplos de código, datos utilizados en este libro
Cogrupos, Cogrupos
problema de arranque en frío, filtrado colaborativo con mínimos cuadrados alternos, parámetros de predicción
método de recopilación, conjuntos de datos: API estructuradas con seguridad de tipos, recopilación de filas para el controlador, acciones
columnas
explotando, explotar
operador de comparación (=!=), concatenación y adición de filas (unión), trabajo con valores booleanos
formatos de compresión, Tipos de archivos divisibles y compresión, Tipos de archivos divisibles y compresión
opciones de configuración
SparkConf, La SparkConf
recuentoAproximadoDistinto, recuentoAproximadoDistinto
countByKey, Agregaciones
fuentes de datos
estructura de las API de fuentes de datos, la estructura de las API de fuentes de datos: modos de guardado
en la API de transmisión estructurada, fuentes de entrada, dónde se leen y escriben los datos (fuentes y
datos, lectura
datos, almacenamiento
baldeando, baldeando
temporal (almacenamiento en caché), Almacenamiento temporal de datos (almacenamiento en caché)Almacenamiento temporal de datos (almacenamiento en caché)
datos, tipos de
arreglos, arreglosexplotar
fechas y marcas de tiempo, Trabajar con fechas y marcas de tiempoTrabajar con fechas y
Marcas de tiempo
mapas, mapas
valores nulos, Trabajo con valores nulos en la ordenación de datos, Signos y síntomas, Formateo de modelos
estructuras, estructuras
funciones definidas por el usuario (UDF), funciones definidas por el usuario funciones definidas por el usuario, agregación
Machine Translated by Google
datos, actualización en tiempo real, Actualizar datos para servir en tiempo real
datos, escritura
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
conjuntos de datos
acciones, Acciones
beneficios de, Conjuntos de datos: API estructuradas con seguridad de tipos, Cuándo usar conjuntos de datos
creando desde RDD, interoperando entre tramas de datos, conjuntos de datos y RDD
filtrado, filtrado
se une, se une
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
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 tiempoTrabajar con fechas y
Marcas de tiempo
árboles de decisión
Machine Translated by Google
aprendizaje profundo
DeepLearning4j, DeepLearning4J
Compatibilidad con redes neuronales MLlib, compatibilidad con redes neuronales MLlib
en Spark, Maneras de usar el aprendizaje profundo en SparkManeras de usar el aprendizaje profundo en Spark
TensorFlowOnSpark, TensorFlowOnSpark
TensorFrames, TensorFrames
DeepLearning4j, DeepLearning4J
dependencias, Transformaciones
despliegue
gráfico acíclico dirigido (DAG), marcos de datos y SQL, columnas como expresiones, la interfaz de usuario de Spark
procesos de controlador, Aplicaciones de Spark, Recopilación de filas para el controlador, La arquitectura de un Spark
API de DStreams, Fundamentos del procesamiento de transmisiones, API de DStream, Hora del evento, Manejo tardío
Datos con marcas de agua
mi
ElementwiseProduct, ElementwiseProduct
aplicaciones de extremo a extremo, ¿ Qué es el procesamiento de transmisión?, Transmisión estructurada, Conceptos básicos
de transmisión estructurada
manejo de datos tardíos, Manejo de datos tardíos con marcas de aguaManejo de datos tardíos con
marcas de agua
ventanas, ventanas en tiempo de eventos: manejo de datos tardíos con marcas de agua
expresiones
extraer, transformar y cargar (ETL), esquemas, cuándo usar conjuntos de datos, Scala versus Java versus
método de filtro, filtrado de filas, trabajo con valores booleanos, filtrado mejorado, selecciones y
Filtración
GRAMO
ejemplo, ejemplo
Machine Translated by Google
análisis gráfico
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
subgrafos, subgrafos
grupoPorClave, grupoPorClave
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
HiveContext, el SparkContext
I
Machine Translated by Google
inmutabilidad, Transformaciones
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
Uniones
desafíos al usar, Desafíos al usar uniones: enfoque 3: cambiar el nombre de una columna antes de la unión
cómo Spark realiza las uniones, cómo Spark realiza las uniones: una tabla pequeña a una tabla pequeña
en RDD, Uniones
cremalleras, cremalleras
datos JSON
Kafka
RDD de valorclave
agregaciones, AgregacionesfoldByKey
creando, keyBy
grupoPorClave, grupoPorClave
reducirPorClave, reducirPorClave
cuándo usar, conceptos básicos de valorclave (RDD de valorclave), tramas de datos frente a SQL frente a conjuntos de datos
versus RDD
API de idioma
seleccionando, ¿Qué API de Spark usar?, Scala versus Java versus Python versus R
datos tardíos, manejo, Manejo de datos tardíos con marcas de aguaManejo de datos tardíos con marcas de agua
listas, listas
Regresión logística
ejemplo, ejemplo
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
METRO
proceso de análisis avanzado, el proceso de análisis avanzado: aprovechar el modelo y/o los conocimientos
clasificación, ClasificaciónConclusión
Descripción general de análisis, análisis avanzado y aprendizaje automático: una introducción breve sobre el aprendizaje avanzado
Analítica
regresión, RegresiónConclusión
MapPartitionsRDD, mapPartitions
gestión de la memoria
almacenamiento temporal de datos (caché), Almacenamiento temporal de datos (caché) Datos temporales
Almacenamiento (caché)
metadatos
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
MinMaxScaler, MinMaxScaler
MLlib
beneficios de Machine Learning y Advanced Analytics, cuándo y por qué debería usar MLlib (frente a scikitlearn, TensorFlow
o foo package)
descripción general de Machine Learning y Advanced AnalyticsMachine Learning y Advanced Analytics, ¿ Qué es
MLlib?
modelos (ver también modelos individuales; aprendizaje automático y análisis avanzado; MLlib)
formato según el caso de uso, formato de modelos según su caso de uso: formato
escalabilidad de, Escalabilidad del modelo, Escalabilidad del modelo, Filtrado colaborativo con alternancia
monitoreo y depuración
nulos inesperados en los resultados, signos y síntomas, formateo de modelos según su uso
Caso
norte
valores nulos, Columnas, Trabajar con valores nulos en la ordenación de datos, Signos y síntomas, Formateo
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
modos de salida, modos de salida, cómo se emiten los datos (modos de salida), modos de salida
PAG
paralelismo, paralelismo
Archivos de parquet
particiones
definido, Particiones
agregaciones, agregaciones
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
paralelismo, paralelismo
programación, programación
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,
pivotes, pivote
preprocesamiento
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
formateo de modelos según el caso de uso, formateo de modelos según su caso de uso
transformadores de datos de texto, transformadores de datos de texto frecuencia de términofrecuencia de documento inversa
Palabra2Vec, Palabra2Vec
tiempo de procesamiento, tiempo de evento versus tiempo de procesamiento, tiempo de evento, tasa de entrada y tasa de procesamiento
Machine Translated by Google
aplicaciones de producción
Pitón
PySpark, PySpark
ejecución de consultas, monitoreo de, Qué monitorear, Estado de consultas : duración del lote
bosques aleatorios
leyendo datos
recomendación
filtrado colaborativo con alternancia de mínimos cuadrados, Filtrado colaborativo con alternancia
mínimos cuadrados
ejemplo, ejemplo
reducirPorClave, reducirPorClave
regresión
evaluadores y automatización del ajuste de modelos, evaluadores y automatización del ajuste de modelos
métricas, Métricas
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)
repartitionAndSortWithinPartitions, repartitionAndSortWithinPartitions
resiliencia
acciones, Accionestomar
agregaciones, AgregacionesfoldByKey
Cogrupos, Cogrupos
contando, contar
filtrado, filtrar
se une, se une
Machine Translated by Google
cartografía, mapa
canalizar RDD a comandos del sistema, canalizar RDD a comandos del sistemaConclusión
RDD de clases de casos frente a conjuntos de datos, conjuntos de datos y RDD de clases de casos
reducir, reducir
clasificar, ordenar
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
RFórmula
ejemplo, fórmula R
Tipo de fila, tramas de datos frente a conjuntos de datos, registros y filas, conjuntos de datos
filas
Scala
escalabilidad, escalabilidad del modelo, escalabilidad del modelo, filtrado colaborativo con alternancia mínima
esquemas
definición, esquemas
sesionización, flatMapGroupsWithStateflatMapGroupsWithState
barajar
Machine Translated by Google
sumideros
definido, Fregaderos
Aplicaciones de chispa
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
Comunidad de Spark, ecosistema y paquetes de Spark, fuentes de datos, reuniones comunitarias locales
Empleos de Spark
Chispa SQL
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
listas, listas
ejecución de consultas, Cómo ejecutar consultas Spark SQLSparkSQL Thrift JDBC/ODBC Server
estructuras, estructuras
pestañas en, La interfaz de usuario de Spark, Otras pestañas de la interfaz de usuario de Spark
sparksubmit, Ejecución de aplicaciones de producción, El ciclo de vida de una aplicación Spark (fuera
spark.sql.shuffle.partitions, Etapas
SparkConf, La SparkConf
brillante
SparkR
configurar, Configurar
funciones definidas por el usuario (UDF), funciones definidas por el usuario funciones definidas por el usuario
Instancias de SparkSession
en Python, la SparkSession
en Scala, la SparkSession
Propósito de SparkSession
formatos de archivo divisibles, tipos de archivos divisibles y compresión, tipos de archivos divisibles y
Machine Translated by Google
compresión
lectura desde, Lectura desde bases de datos SQLLectura desde bases de datos SQL
SQLContext, la SparkSession
etapas, instrucciones lógicas para la ejecución física: tareas, consultas, trabajos, etapas y tareas
flatMapGroupsWithState, flatMapGroupsWithStateflatMapGroupsWithState
mapGroupsWithState, mapGroupsWithStatemapGroupsWithState
procesamiento de flujo
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
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
StructFields, Esquemas
Machine Translated by Google
API estructuradas
Conjuntos de datos, conjuntos de datos: API estructuradas con seguridad de tipos, marcos de datos y conjuntos de datos
esquemas y esquemas
tipos de Spark estructurados y, descripción general de tipos de Spark estructurados tipos de Spark, conversión a
alertando, alertando
beneficios de, ¿Qué es el procesamiento de transmisión?, Transmisión estructurada, Conceptos básicos de transmisión estructurada
descripción general de Streaming estructurado Streaming estructurado, Conceptos básicos de streaming estructurado
subgrafos, subgrafos
subconsultas, subconsultas
aprendizaje supervisado
regresión de supervivencia (Tiempo de falla acelerada), Regresión de supervivencia (Tiempo de falla acelerada)
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
almacenamiento temporal de datos (caché), Almacenamiento temporal de datos (caché) Almacenamiento temporal de datos
(Almacenamiento en caché)
TensorFlowOnSpark, TensorFlowOnSpark
TensorFrames, TensorFrames
pruebas
transformadores de datos de texto, transformadores de datos de texto frecuencia de términofrecuencia de documento inversa
TFIDF (frecuencia de términofrecuencia de documento inversa), frecuencia de términofrecuencia de documento inversa frecuencia
información relacionada con la hora, Trabajar con fechas y marcas de tiempoTrabajar con fechas y
Marcas de tiempo
API de conjuntos de datos, Conjuntos de datos: API estructuradas con seguridad de tipos
descripción general de, ¿ Qué es Apache Spark?, Un recorrido por el conjunto de herramientas de Spark
ChispaR, ChispaR
transformaciones
trabajar con diferentes tipos de datos, Trabajar con funciones booleanas definidas por el usuario
Machine Translated by Google
transformadores
formateo de modelos según el caso de uso, formateo de modelos según su caso de uso
RFórmula, RFórmula
seguridad de tipos, Conjuntos de datos: API estructuradas con seguridad de tipos, ¿ Qué API de Spark usar?
tu
pruebas unitarias, resiliencia y evolución de la lógica empresarial: conexión a marcos de pruebas unitarias
Machine Translated by Google
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
Tipo de datos vectoriales (MLlib), tipos de datos de bajo nivel, escalado y normalización
marcas de agua, Marcas de agua, Manejo de datos tardíos con marcas de aguaManejo de datos tardíos con
marcas de agua
ventanas
Palabra2Vec, Palabra2Vec
escribir datos
cremalleras, cremalleras
Machine Translated by Google
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.
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.