├── DE_course ├── Data │ ├── README.md │ ├── countries-of-the-world.zip │ ├── country-data.zip │ └── nobel-laureates.zip ├── README.md └── Spark │ ├── 01_intro.ipynb │ ├── 02_setup.ipynb │ ├── 03_io.ipynb │ ├── 04_transf.ipynb │ ├── 05_joins.ipynb │ ├── 06_aggr.ipynb │ ├── 07_sql.ipynb │ ├── 08_rdd.ipynb │ ├── 09_stream.ipynb │ ├── README.md │ └── spark_sql_db.ipynb ├── README.md ├── airflow ├── README.txt └── auto_start.md ├── data_doc ├── README.md └── habr_spark_lazy.html ├── fileutils ├── README.md ├── et2dict.py └── fieldnamer.py ├── livy_tools ├── README.md ├── livy_admin.py ├── livy_test.py └── livy_tools.py ├── orc ├── README.md ├── my-file.orc ├── orc_demo.ipynb ├── orc_format_notes.md └── orc_proto_pb2.py └── spark_code ├── README.md └── graph_life.ipynb /DE_course/Data/README.md: -------------------------------------------------------------------------------- 1 | В этой директории лежат файлы с данными (архивы), с которыми мы работаем во время курса. 2 | 3 | Они взяты с kaggle.com, никаких изменений не вносилось 4 | 5 | Еще мы работаем с данными о городах, но он сюда не может быть загружен (ограничения Github), оригинал находится здесь: https://www.kaggle.com/max-mind/world-cities-database 6 | -------------------------------------------------------------------------------- /DE_course/Data/countries-of-the-world.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/korolmi/dataeng/41bdfd850345cf2eac3ec885d6a62c0c4948cd33/DE_course/Data/countries-of-the-world.zip -------------------------------------------------------------------------------- /DE_course/Data/country-data.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/korolmi/dataeng/41bdfd850345cf2eac3ec885d6a62c0c4948cd33/DE_course/Data/country-data.zip -------------------------------------------------------------------------------- /DE_course/Data/nobel-laureates.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/korolmi/dataeng/41bdfd850345cf2eac3ec885d6a62c0c4948cd33/DE_course/Data/nobel-laureates.zip -------------------------------------------------------------------------------- /DE_course/README.md: -------------------------------------------------------------------------------- 1 | Материалы для курса "Инженерия данных" от SkillFactory 2 | -------------------------------------------------------------------------------- /DE_course/Spark/01_intro.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Введение в Spark" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "### Что такое Spark\n", 15 | "\n", 16 | "`Apache Spark is a unified computing engine and a set of libraries for parallel data processing on\n", 17 | "computer clusters.`\n", 18 | "\n", 19 | "* способ параллельного программирования (как Map Reduce)\n", 20 | "* набор библиотек (Scala) и выполняемых файлов (spark-submit)\n", 21 | "* гораздо более функциональный, чем Map Reduce\n", 22 | "* похож на pandas\n", 23 | "* активно развивается (только что вышле версия 3.0)\n", 24 | "* мы будем использовать версию 2.4\n", 25 | "* нет привязки к Hadoop, но мы будем рассматривать spark именно в связке с Hadoop\n", 26 | "\n", 27 | "Хорошая книга от автора Spark - https://github.com/databricks/Spark-The-Definitive-Guide\n", 28 | "\n", 29 | "Активно ей пользуюсь сам, большинство материала взято оттуда." 30 | ] 31 | }, 32 | { 33 | "cell_type": "markdown", 34 | "metadata": {}, 35 | "source": [ 36 | "### Что входит в Spark\n", 37 | "\n", 38 | "![](spark_toolkit.png)\n" 39 | ] 40 | }, 41 | { 42 | "cell_type": "markdown", 43 | "metadata": {}, 44 | "source": [ 45 | "### Что такое pyspark\n", 46 | "\n", 47 | "* spark предоставляет API на нескольких языках\n", 48 | "* наиболее популярное python API - pyspark\n", 49 | " * Scala, Java, SQL, R\n", 50 | "* производительность не страдает (разберем ниже)\n", 51 | "\n", 52 | "`If you use just the Structured APIs, you can expect all languages to have similar performance characteristics`\n" 53 | ] 54 | }, 55 | { 56 | "cell_type": "markdown", 57 | "metadata": {}, 58 | "source": [ 59 | "### Как работают Spark приложения\n", 60 | "\n", 61 | "![](spark_apps.png)\n", 62 | "\n", 63 | "* `driver`: исполняет программу\n", 64 | " * поддерживает инфорацию о приложении\n", 65 | " * исполняет код программы \n", 66 | " * планирует и запускает работу executor-ов\n", 67 | "* `executor`: выполняет вычисления\n", 68 | " * исполняет код, переданный driver-ом (JAR)\n", 69 | " * отчитывается перед driver-ом о состоянии процесса вычисления\n", 70 | "* `cluster manager`: управляет физическими машинами и выделяет ресурсы spark приложениям\n", 71 | " * standalone, YARN, Mesos" 72 | ] 73 | }, 74 | { 75 | "cell_type": "markdown", 76 | "metadata": {}, 77 | "source": [ 78 | "### Запуск приложения и режимы исполнения \n", 79 | "\n", 80 | "* приложения могут быть\n", 81 | " * интерактивными (shell) - python, scala, SQL\n", 82 | " * скомпилированными (JAR)\n", 83 | "* `execution mode`: возможность управления размещением процессов (driver, executors)\n", 84 | " * `cluster mode`: все в кластере \n", 85 | " * `client mode`: driver работает вне кластера, executor-ы - в кластере\n", 86 | " * `local mode`: все работает локально (потоки)\n", 87 | "* `spark session`: объект, через который происходит работа со spark-ом (driver-ом) \n" 88 | ] 89 | }, 90 | { 91 | "cell_type": "markdown", 92 | "metadata": {}, 93 | "source": [ 94 | "### Основные абстракции\n", 95 | "\n", 96 | "![](rdd_dataframe.png)\n", 97 | "\n", 98 | "* `RDD (Resilient Distributed Dataset)`: набор объектов, разбитых на разделы (`partitions`)\n", 99 | "* `Dataset`: набор типизированных записей, разбитых на разделы \n", 100 | "* `Dataframe`: набор записей типа `Row`, разбитых на разделы\n", 101 | "\n", 102 | "* `Low level API` (RDD)\n", 103 | "* `Structured API` (Dataset, Dataframe)\n", 104 | "* Dataset: только в Scala и Java API\n" 105 | ] 106 | }, 107 | { 108 | "cell_type": "markdown", 109 | "metadata": {}, 110 | "source": [ 111 | "### Dataframe \n", 112 | "\n", 113 | "![](dataframe.png)\n", 114 | "\n", 115 | "* таблица, состоящая из строк и столбцов\n", 116 | " * похожа на датафреймы в pandas и R\n", 117 | "* `schema`: список, определяющий структуру строки (имена и типы колонок)\n", 118 | "* `partition`: набор строк датафрейма, расположенных на одном узле кластера\n", 119 | " * определяют возможность параллелизма (executors + partitions)\n", 120 | "* `Row`: строка датафрейма (объект)\n", 121 | "* `Column`: элемент строки (объект)\n", 122 | "* spark работает со своими типами данных (ByteType, IntegerType, StringType, ...)\n" 123 | ] 124 | }, 125 | { 126 | "cell_type": "markdown", 127 | "metadata": {}, 128 | "source": [ 129 | "### Трансформации и действия\n", 130 | "\n", 131 | "* методы spark делятся на трансформации (`transformation`) и действия (`action`)\n", 132 | "* все объекты (Dataframe, RDD, ...) неизменяемы (immutable)\n", 133 | "* чтобы изменить объект нужно задать инструкцию его изменения (transformation)\n", 134 | "* результат трансформации - объект (например, Dataframe)\n", 135 | "* `lazy evaluation`: трансформации выполняются только во время действия (`action`)\n", 136 | " * используется оптимизатор (`Catalyst`)\n", 137 | " * план исполнения можно посмотреть\n", 138 | "* `action`: вычисление, возвращающее результат\n", 139 | " * подразумевает перемещение данных между экзекьюторами и драйвером\n", 140 | " * посмотреть данные на консоли\n", 141 | " * собрать данные на драйвере (с преобразованием в `native` тип данных)\n", 142 | " * сохранение данных в источник" 143 | ] 144 | }, 145 | { 146 | "cell_type": "markdown", 147 | "metadata": {}, 148 | "source": [ 149 | "### Движение данных\n", 150 | "\n", 151 | "![](ideal.png)" 152 | ] 153 | }, 154 | { 155 | "cell_type": "markdown", 156 | "metadata": {}, 157 | "source": [ 158 | "### Основные действия\n", 159 | "\n", 160 | "(для материалов, не слайд)\n", 161 | "\n", 162 | "Основными действиями (`action`), которыми мы будем далее активно пользоваться, являются:\n", 163 | "\n", 164 | "**collect()**\n", 165 | "\n", 166 | "Возвращает dataframe как список строк (list of Row)\n", 167 | "\n", 168 | "**take()**\n", 169 | "\n", 170 | "Аналогично collect(), только первые N строк\n", 171 | "\n", 172 | "**first()**\n", 173 | "\n", 174 | "Возвращает первую строку, как Row\n", 175 | "\n", 176 | "**count()**\n", 177 | "\n", 178 | "Возвращает количество строк в датафрейме\n", 179 | "\n", 180 | "\n", 181 | "**saveAs()**\n", 182 | "\n", 183 | "Сохраняет датафрейм в файл (в HDFS).\n", 184 | "\n", 185 | "**saveAsTable()**\n", 186 | "\n", 187 | "Сохраняет датафрейм в таблицу (Hive)" 188 | ] 189 | } 190 | ], 191 | "metadata": { 192 | "kernelspec": { 193 | "display_name": "Python 3", 194 | "language": "python", 195 | "name": "python3" 196 | }, 197 | "language_info": { 198 | "codemirror_mode": { 199 | "name": "ipython", 200 | "version": 3 201 | }, 202 | "file_extension": ".py", 203 | "mimetype": "text/x-python", 204 | "name": "python", 205 | "nbconvert_exporter": "python", 206 | "pygments_lexer": "ipython3", 207 | "version": "3.5.2" 208 | } 209 | }, 210 | "nbformat": 4, 211 | "nbformat_minor": 2 212 | } 213 | -------------------------------------------------------------------------------- /DE_course/Spark/02_setup.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Установка и настройка Spark" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "### Настройка кластера\n", 15 | "\n", 16 | "* добавляем сервис Spark\n", 17 | "* (можно остановить Hue, если мало памяти)\n", 18 | "* spark готов к работе" 19 | ] 20 | }, 21 | { 22 | "cell_type": "markdown", 23 | "metadata": {}, 24 | "source": [ 25 | "### Как увеличить объем используемой памяти\n", 26 | "\n", 27 | "После запуска одноименного сервиса, Spark готов к работе. Поскольку на нашем кластере все процессы работают на одном узле, этому узлу может не хватить оперативной памяти. Увеличим объем с 2ГБ до 3ГБ, этого достаточно для того, чтобы работали все наши примеры.\n", 28 | "\n", 29 | "* в кластер менеджере заходим в сервис Yarn\n", 30 | "* вкладка Configuration\n", 31 | "* находим параметр \"container memory\"\n", 32 | "\n", 33 | "![](hadoop_yarn_memory.PNG)\n", 34 | "\n", 35 | "* устанавливаем новое значение 3\n", 36 | "* сохраняем изменения\n", 37 | "* (через минуту) появляется иконка \"перезагрузить\" \n", 38 | "\n", 39 | "![](yarn_restart.PNG)\n", 40 | "\n", 41 | "* кликаем ее, дожидаемся перезагрузки Yarn\n", 42 | "* в WEB интерфейсе менеджера ресурсов должно появиться значение \"3GB\" в верхней строке (\"Memory Total\")" 43 | ] 44 | }, 45 | { 46 | "cell_type": "markdown", 47 | "metadata": {}, 48 | "source": [ 49 | "### Запуск Spark приложений\n", 50 | "\n", 51 | "* запуск интерактивного shell (scala или python)\n", 52 | " * spark-shell (scala)\n", 53 | " * pyspark (python)\n", 54 | "* запуск скомпилированных приложений (JAR)\n", 55 | " * spark-submit\n", 56 | "* мы будем использовать pyspark, а точнее - jupyter notebook" 57 | ] 58 | }, 59 | { 60 | "cell_type": "markdown", 61 | "metadata": {}, 62 | "source": [ 63 | "### Параметры запуска Spark приложений\n", 64 | "\n", 65 | "Для того, чтобы на нашем маленьком кластере работали Spark приложения, необходимо задать следующие параметры\n", 66 | "\n", 67 | "* --driver-memory 1G: объем памяти, выделяемый драйверу на узле кластера\n", 68 | "* --executor-memory 600M: объем памяти, выделяемый экзекьютору\n", 69 | "\n", 70 | "Эти параметры нужно задать\n", 71 | "\n", 72 | "* в командной строке (при запуске интерактивного spark shell-а)\n", 73 | "\n", 74 | "`pyspark --driver-memory 1G --executor-memory 600M`\n", 75 | "\n", 76 | "* в качестве значения переменной окружения `PYSPARK_SUBMIT_ARGS` (при работе со spark из jupyter notebook)\n", 77 | "\n", 78 | "`os.environ[\"PYSPARK_SUBMIT_ARGS\"] = --driver-memory 1G --executor-memory 600M pyspark-shell`\n" 79 | ] 80 | }, 81 | { 82 | "cell_type": "markdown", 83 | "metadata": {}, 84 | "source": [ 85 | "### Настройки jupyter notebook\n", 86 | "\n", 87 | "* стандартная \"шапка\" \n", 88 | "* создание сессии\n", 89 | "* интерактивная работа (как обычно в jupyter)\n", 90 | "* остановка сессии\n", 91 | "* перезапуск ядра (если что-то нужно поменять в конфигурации)" 92 | ] 93 | }, 94 | { 95 | "cell_type": "markdown", 96 | "metadata": {}, 97 | "source": [ 98 | "К материалам курса приложен jupyter notebook с примером, продемонстрированным в скринкасте." 99 | ] 100 | }, 101 | { 102 | "cell_type": "markdown", 103 | "metadata": {}, 104 | "source": [ 105 | "### Web интерфейс\n", 106 | "\n", 107 | "* доступен на порту 8088 (в кластер менеджере) или на порту 4040 (локальная установка)\n", 108 | "* в кластере - через Resource Manager-а (с возможностью принудительной остановки)\n", 109 | "* можно увидеть процесс выполнения job-а\n", 110 | "* использование кэша (разберем чуть позже)\n", 111 | "* информацию о процессах (драйвере и экзекьюторах)" 112 | ] 113 | }, 114 | { 115 | "cell_type": "markdown", 116 | "metadata": {}, 117 | "source": [ 118 | "### Локальная установка Spark\n", 119 | "\n", 120 | "Spark может работать локально (т.е. может быть установлен на рабочем месте, в нашем случае - не в виртуальной машине. Это часто используется для тестирования). В модуле мы будем использовать как кластерный вариант Spark, так и локальный.\n", 121 | "\n", 122 | "Как установить и настроить spark для локальной работы:\n", 123 | "\n", 124 | "1. Должна быть предустановлена java (установить, если это не так)\n", 125 | "2. Установить spark (см. инструкции на сайте проекта), он уже может быть установлен (с другими продуктами - у меня был установлен вместе со scala)\n", 126 | "3. pip3 install pyspark\n", 127 | "4. pip3 install ipyparallel\n", 128 | "\n", 129 | "Далее в ноутбуке\n", 130 | "\n", 131 | "Стандартная шапка:\n", 132 | "\n", 133 | " import os\n", 134 | " from pyspark.sql import SparkSession\n", 135 | " \n", 136 | " os.environ[\"SPARK_HOME\"] = \"<путь куда был установлен pyspark>\"\n", 137 | " os.environ[\"PYSPARK_PYTHON\"] = \"/usr/bin/python3\"\n", 138 | " os.environ[\"PYSPARK_DRIVER_PYTHON\"] = \"python3\"\n", 139 | " os.environ[\"PYSPARK_SUBMIT_ARGS\"] = \"pyspark-shell\"\n", 140 | "\n", 141 | "Чтобы стартануть сессию делаем\n", 142 | "\n", 143 | " sp = SparkSession.builder.master(\"local\").appName(\"имя_приложения\").getOrCreate()\n", 144 | "\n", 145 | "После этого сессию можно видеть здесь:\n", 146 | "\n", 147 | " http://localhost:4040\n", 148 | "\n", 149 | "Чтобы остановить сессию делаем\n", 150 | "\n", 151 | " sp.stop()\n", 152 | "\n", 153 | "После этого сессия пропадает..." 154 | ] 155 | }, 156 | { 157 | "cell_type": "markdown", 158 | "metadata": {}, 159 | "source": [ 160 | "### Настройка Spark для работы с JDBC источниками\n", 161 | "\n", 162 | "Для работы с реляционными базами данных (например, для чтения данных из них) необходимо установить JDBC драйвер и указать Spark два параметра\n", 163 | "\n", 164 | "* --driver-class-path <путь к JDBC драйверу>\n", 165 | "* --jars <путь к JDBC драйверу>\n", 166 | "\n", 167 | "(один и тот же пусть указываем два раза). **ВАЖНО** файл с драйвером должен находиться в одном и том же месте (директории) на всех узлах кластера, на которых будут работать executor-ы, его использующие.\n", 168 | "\n", 169 | "Например, для работы с PostgreSQL в нашем кластере нужно будет указать такую комбинацию параметров (см. способы задания параметров выше):\n", 170 | "\n", 171 | "`--driver-class-path /usr/share/java/postgresql.jar --jars /usr/share/java/postgresql.jar\n", 172 | "\n", 173 | "см. более подробно https://spark.apache.org/docs/latest/sql-data-sources-jdbc.html\n", 174 | "\n", 175 | "Мы разберем примеры работы с базами данных в Spark более подробно на следующем шаге этого модуля.\n" 176 | ] 177 | }, 178 | { 179 | "cell_type": "markdown", 180 | "metadata": {}, 181 | "source": [ 182 | "### Основные параметры параллелизма Spark\n", 183 | "\n", 184 | "Spark подразумевает параллельную обработку, как мы уже говорили ранее, dataframe состоит из разделов (`partitions`), каждый из которых хранится и обрабатывается на своем узле кластера. Как правило, количество разделов можно задать в момент создания dataframe. В работе инженера данных наиболее частым способом создания dataframe является его загрузка (из источника - файла или таблицы). Задать количество разделов можно с помощью опции `numPartitions` (более подробно с опциями мы познакомимся на следующем шаге).\n", 185 | "\n", 186 | "С другой стороны для параллельной обработки нужны \"обработчики\" (`executors` в терминологии spark). Их количество задается в параметре `--num-executors` при запуске spark приложения. \n", 187 | "\n", 188 | "Другими параметрами, связанными с параллельной обработкой, являются\n", 189 | "\n", 190 | "* --executor-cores: количество ядер, выделяемых одному executor-у\n", 191 | "* --executor-memory: объем оперативной памяти, выделяемый одному executor-у\n", 192 | "\n", 193 | "**ВАЖНО** некорретное задание этих параметров может привести к одному из следующих исходов (решает логика Spark)\n", 194 | "\n", 195 | "* приложение не будет запущено \n", 196 | "* приложение будет запущено с другими значениями параметров параллелизма (их установит spark)\n", 197 | "\n", 198 | "Хорошая статья про partitions: https://medium.com/parrot-prediction/partitioning-in-apache-spark-8134ad840b0" 199 | ] 200 | } 201 | ], 202 | "metadata": { 203 | "jupytext": { 204 | "encoding": "# -*- coding: utf-8 -*-", 205 | "formats": "ipynb,py:light" 206 | }, 207 | "kernelspec": { 208 | "display_name": "Python 3", 209 | "language": "python", 210 | "name": "python3" 211 | }, 212 | "language_info": { 213 | "codemirror_mode": { 214 | "name": "ipython", 215 | "version": 3 216 | }, 217 | "file_extension": ".py", 218 | "mimetype": "text/x-python", 219 | "name": "python", 220 | "nbconvert_exporter": "python", 221 | "pygments_lexer": "ipython3", 222 | "version": "3.5.2" 223 | } 224 | }, 225 | "nbformat": 4, 226 | "nbformat_minor": 2 227 | } 228 | -------------------------------------------------------------------------------- /DE_course/Spark/04_transf.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Базовые трансформации в Spark" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "### Из чего состоят dataframe\n", 15 | "\n", 16 | "* датафрейм состоит из строк (объект Row)\n", 17 | "* строка состоит из колонок (объект Column)\n", 18 | "* Row не имеет методов, Column имеет множество методов\n", 19 | "* значение в колонке всегда имеет тип и определяется схемой (Schema)\n", 20 | "* spark работает с собственным набором типов данных (см. документацию)" 21 | ] 22 | }, 23 | { 24 | "cell_type": "markdown", 25 | "metadata": {}, 26 | "source": [ 27 | "### Выражения и колонки\n", 28 | "\n", 29 | "* выражение (`expression`) - это функция, которая преобразует множество колонок в значение для каждой строки dataframe\n", 30 | "* в простейшем случае - само значение колонки датафрейма (`col(\"a\")`)\n", 31 | "* функция expr() - создает выражение из строки (`expr(\"a - 5\")`)" 32 | ] 33 | }, 34 | { 35 | "cell_type": "markdown", 36 | "metadata": {}, 37 | "source": [ 38 | "### Операции с dataframe\n", 39 | "\n", 40 | "![](df_transforms.PNG)\n", 41 | "\n", 42 | "* добавляем или удаляем колонки\n", 43 | "* добавляем или удаляем строки\n", 44 | "* трансформируем колонку в строку или наоборот\n", 45 | "* меняем порядок строк\n", 46 | "* dataframe изменить нельзя, можно лишь создать новый" 47 | ] 48 | }, 49 | { 50 | "cell_type": "markdown", 51 | "metadata": {}, 52 | "source": [ 53 | "### Информация о dataframe\n", 54 | "\n", 55 | "* schema - атрибут dataframe, содержащий StructType со схемой\n", 56 | "* printSchema() - печать схемы в виде дерева\n", 57 | "* columns - атрибут dataframe, содержащий список колонок\n", 58 | "* describe() - вычисляет count, mean, standard deviation, min и max для всех цифровых колонок dataframe" 59 | ] 60 | }, 61 | { 62 | "cell_type": "markdown", 63 | "metadata": {}, 64 | "source": [ 65 | "### Удаление колонок\n", 66 | "\n", 67 | "* select() - выбор (набора) колонок dataframe\n", 68 | "* selectExpr() - аналогично, но с использованием выражений\n", 69 | "* drop() - удаление колонок dataframe" 70 | ] 71 | }, 72 | { 73 | "cell_type": "markdown", 74 | "metadata": {}, 75 | "source": [ 76 | "### Добавление колонок\n", 77 | "\n", 78 | "* withColumn() - добавление колонки\n", 79 | "* withColumnRenamed() - переименование колонки\n", 80 | "* функция lit() - литерал (используется для добавления колонок с константными значениями)" 81 | ] 82 | }, 83 | { 84 | "cell_type": "markdown", 85 | "metadata": {}, 86 | "source": [ 87 | "### Удаление строк\n", 88 | "\n", 89 | "* filter() - отбор строк dataframe\n", 90 | "* where() - синоним " 91 | ] 92 | }, 93 | { 94 | "cell_type": "markdown", 95 | "metadata": {}, 96 | "source": [ 97 | "### Остальные виды операций\n", 98 | "\n", 99 | "* добавление строк - это join() и union()\n", 100 | "* сортировку мы рассмотрим в отдельном шаге вместе с агрегатными функциями " 101 | ] 102 | }, 103 | { 104 | "cell_type": "markdown", 105 | "metadata": {}, 106 | "source": [ 107 | "### Практика\n", 108 | "\n", 109 | "Давайте попробуем основные трансформации на наших данных (данные стран мира).\n", 110 | "\n", 111 | "Тренироваться будем в локальном режиме (на рабочем месте) - см. создание spark session." 112 | ] 113 | }, 114 | { 115 | "cell_type": "code", 116 | "execution_count": 3, 117 | "metadata": {}, 118 | "outputs": [], 119 | "source": [ 120 | "import os\n", 121 | "from pyspark.sql import SparkSession\n", 122 | "from pyspark.sql import functions as f\n", 123 | "os.environ[\"SPARK_HOME\"] = \"/home/mk/mk_win/projects/SparkEdu/lib/python3.5/site-packages/pyspark\"\n", 124 | "os.environ[\"PYSPARK_PYTHON\"] = \"/usr/bin/python3\"\n", 125 | "os.environ[\"PYSPARK_DRIVER_PYTHON\"] = \"python3\"\n", 126 | "os.environ[\"PYSPARK_SUBMIT_ARGS\"] = \"pyspark-shell\"" 127 | ] 128 | }, 129 | { 130 | "cell_type": "code", 131 | "execution_count": 4, 132 | "metadata": {}, 133 | "outputs": [], 134 | "source": [ 135 | "spark = SparkSession.builder.master(\"local\").appName(\"transf_test\").getOrCreate()" 136 | ] 137 | }, 138 | { 139 | "cell_type": "markdown", 140 | "metadata": {}, 141 | "source": [ 142 | "Загрузим наши страны" 143 | ] 144 | }, 145 | { 146 | "cell_type": "code", 147 | "execution_count": 5, 148 | "metadata": {}, 149 | "outputs": [], 150 | "source": [ 151 | "cdf = spark.read.format(\"csv\") \\\n", 152 | " .option(\"mode\", \"FAILFAST\") \\\n", 153 | " .option(\"inferSchema\", \"true\") \\\n", 154 | " .option(\"header\",\"true\") \\\n", 155 | " .option(\"path\", \"data/countries of the world.csv\") \\\n", 156 | " .load()" 157 | ] 158 | }, 159 | { 160 | "cell_type": "markdown", 161 | "metadata": {}, 162 | "source": [ 163 | "**Схема**\n", 164 | "\n", 165 | "ее лучше всего смотреть в виде дерева" 166 | ] 167 | }, 168 | { 169 | "cell_type": "code", 170 | "execution_count": 6, 171 | "metadata": {}, 172 | "outputs": [ 173 | { 174 | "name": "stdout", 175 | "output_type": "stream", 176 | "text": [ 177 | "root\n", 178 | " |-- Country: string (nullable = true)\n", 179 | " |-- Region: string (nullable = true)\n", 180 | " |-- Population: integer (nullable = true)\n", 181 | " |-- Area (sq. mi.): integer (nullable = true)\n", 182 | " |-- Pop. Density (per sq. mi.): string (nullable = true)\n", 183 | " |-- Coastline (coast/area ratio): string (nullable = true)\n", 184 | " |-- Net migration: string (nullable = true)\n", 185 | " |-- Infant mortality (per 1000 births): string (nullable = true)\n", 186 | " |-- GDP ($ per capita): integer (nullable = true)\n", 187 | " |-- Literacy (%): string (nullable = true)\n", 188 | " |-- Phones (per 1000): string (nullable = true)\n", 189 | " |-- Arable (%): string (nullable = true)\n", 190 | " |-- Crops (%): string (nullable = true)\n", 191 | " |-- Other (%): string (nullable = true)\n", 192 | " |-- Climate: string (nullable = true)\n", 193 | " |-- Birthrate: string (nullable = true)\n", 194 | " |-- Deathrate: string (nullable = true)\n", 195 | " |-- Agriculture: string (nullable = true)\n", 196 | " |-- Industry: string (nullable = true)\n", 197 | " |-- Service: string (nullable = true)\n", 198 | "\n" 199 | ] 200 | } 201 | ], 202 | "source": [ 203 | "cdf.printSchema()" 204 | ] 205 | }, 206 | { 207 | "cell_type": "markdown", 208 | "metadata": {}, 209 | "source": [ 210 | "**Колонки**\n", 211 | "\n", 212 | "Список колонок - атрибут, отсюда удобно копировать (колонки названы длинно)" 213 | ] 214 | }, 215 | { 216 | "cell_type": "code", 217 | "execution_count": 7, 218 | "metadata": {}, 219 | "outputs": [ 220 | { 221 | "data": { 222 | "text/plain": [ 223 | "['Country',\n", 224 | " 'Region',\n", 225 | " 'Population',\n", 226 | " 'Area (sq. mi.)',\n", 227 | " 'Pop. Density (per sq. mi.)',\n", 228 | " 'Coastline (coast/area ratio)',\n", 229 | " 'Net migration',\n", 230 | " 'Infant mortality (per 1000 births)',\n", 231 | " 'GDP ($ per capita)',\n", 232 | " 'Literacy (%)',\n", 233 | " 'Phones (per 1000)',\n", 234 | " 'Arable (%)',\n", 235 | " 'Crops (%)',\n", 236 | " 'Other (%)',\n", 237 | " 'Climate',\n", 238 | " 'Birthrate',\n", 239 | " 'Deathrate',\n", 240 | " 'Agriculture',\n", 241 | " 'Industry',\n", 242 | " 'Service']" 243 | ] 244 | }, 245 | "execution_count": 7, 246 | "metadata": {}, 247 | "output_type": "execute_result" 248 | } 249 | ], 250 | "source": [ 251 | "cdf.columns" 252 | ] 253 | }, 254 | { 255 | "cell_type": "markdown", 256 | "metadata": {}, 257 | "source": [ 258 | "**describe()**\n", 259 | "\n", 260 | "немного неожиданный результат - посчиталось для всех колонок... Не только числовых." 261 | ] 262 | }, 263 | { 264 | "cell_type": "code", 265 | "execution_count": 8, 266 | "metadata": {}, 267 | "outputs": [ 268 | { 269 | "name": "stdout", 270 | "output_type": "stream", 271 | "text": [ 272 | "+-------+------------+--------------------+--------------------+------------------+--------------------------+----------------------------+--------------------+----------------------------------+------------------+------------+-----------------+------------------+------------------+------------------+------------------+------------------+---------+-----------+--------+-------+\n", 273 | "|summary| Country| Region| Population| Area (sq. mi.)|Pop. Density (per sq. mi.)|Coastline (coast/area ratio)| Net migration|Infant mortality (per 1000 births)|GDP ($ per capita)|Literacy (%)|Phones (per 1000)| Arable (%)| Crops (%)| Other (%)| Climate| Birthrate|Deathrate|Agriculture|Industry|Service|\n", 274 | "+-------+------------+--------------------+--------------------+------------------+--------------------------+----------------------------+--------------------+----------------------------------+------------------+------------+-----------------+------------------+------------------+------------------+------------------+------------------+---------+-----------+--------+-------+\n", 275 | "| count| 227| 227| 227| 227| 227| 227| 224| 224| 226| 209| 223| 225| 225| 225| 205| 224| 223| 212| 211| 212|\n", 276 | "| mean| null| null|2.8740284365638766E7| 598226.9559471365| null| null|0.031746031746031744| 56.5| 9689.823008849558| null| null| 8.842105263157896|1.9090909090909092| 86.57894736842105|2.1597938144329896| 21.0| 22.0| 0.0| null| null|\n", 277 | "| stddev| null| null|1.1789132654347657E8|1790282.2437336047| null| null| 0.2519763153394848| 36.78314831549904|10049.138513197226| null| null|10.128415242358372| 5.954849055410831|14.633734958321792|0.7054429092420468|11.853269591129697| NaN| NaN| null| null|\n", 278 | "| min|Afghanistan |ASIA (EX. NEAR EA...| 7026| 2| 0,0| 0,00| -0,02| 10,03| 500| 100,0| 0,2| 0| 0| 100| 1| 10| 10,01| 0| 0,02| 0,062|\n", 279 | "| max| Zimbabwe |WESTERN EUROPE ...| 1313973713| 17075200| 99,9| 92,31| 9,61| 98,8| 55100| 99,9| 97,7| 9,75| 9,87| 99,98| 4| 9,95| 9,89| 0,769| 0,906| 0,954|\n", 280 | "+-------+------------+--------------------+--------------------+------------------+--------------------------+----------------------------+--------------------+----------------------------------+------------------+------------+-----------------+------------------+------------------+------------------+------------------+------------------+---------+-----------+--------+-------+\n", 281 | "\n" 282 | ] 283 | } 284 | ], 285 | "source": [ 286 | "cdf.describe().show()" 287 | ] 288 | }, 289 | { 290 | "cell_type": "markdown", 291 | "metadata": {}, 292 | "source": [ 293 | "**Отбор колонок**\n", 294 | "\n", 295 | "С помощью `select`-а оставим только нужные нам (обратите внимание на результат - схема результирующего датафрейма)" 296 | ] 297 | }, 298 | { 299 | "cell_type": "code", 300 | "execution_count": 9, 301 | "metadata": {}, 302 | "outputs": [ 303 | { 304 | "data": { 305 | "text/plain": [ 306 | "DataFrame[Country: string, Region: string, Population: int]" 307 | ] 308 | }, 309 | "execution_count": 9, 310 | "metadata": {}, 311 | "output_type": "execute_result" 312 | } 313 | ], 314 | "source": [ 315 | "cdf.select('Country','Region','Population')" 316 | ] 317 | }, 318 | { 319 | "cell_type": "markdown", 320 | "metadata": {}, 321 | "source": [ 322 | "**selectExpr()**\n", 323 | "\n", 324 | "могучая функция: мы знаем, что в модуле `pyspark.sql.functions` есть функция конкатенации, смело вызываем ее для создания такого названия страны. Заодно воспользуемся возможностью и переименуем колонку (`as`).\n", 325 | "\n", 326 | "Если мы хотим, чтобы `show` не обрезал вывод - добавляем параметр `False`." 327 | ] 328 | }, 329 | { 330 | "cell_type": "code", 331 | "execution_count": 10, 332 | "metadata": {}, 333 | "outputs": [ 334 | { 335 | "name": "stdout", 336 | "output_type": "stream", 337 | "text": [ 338 | "+----------------------------------------------+\n", 339 | "|CountryIn |\n", 340 | "+----------------------------------------------+\n", 341 | "|Afghanistan in ASIA (EX. NEAR EAST) |\n", 342 | "|Albania in EASTERN EUROPE |\n", 343 | "|Algeria in NORTHERN AFRICA |\n", 344 | "+----------------------------------------------+\n", 345 | "only showing top 3 rows\n", 346 | "\n" 347 | ] 348 | } 349 | ], 350 | "source": [ 351 | "cdf.selectExpr('concat(Country,\"in \",Region) as CountryIn').show(3, False)" 352 | ] 353 | }, 354 | { 355 | "cell_type": "markdown", 356 | "metadata": {}, 357 | "source": [ 358 | "**Удаление колонок**\n", 359 | "\n", 360 | "Удалим так понравившиеся нам колонки и посмотрим - что осталось..." 361 | ] 362 | }, 363 | { 364 | "cell_type": "code", 365 | "execution_count": 11, 366 | "metadata": {}, 367 | "outputs": [ 368 | { 369 | "data": { 370 | "text/plain": [ 371 | "['Area (sq. mi.)',\n", 372 | " 'Pop. Density (per sq. mi.)',\n", 373 | " 'Coastline (coast/area ratio)',\n", 374 | " 'Net migration',\n", 375 | " 'Infant mortality (per 1000 births)',\n", 376 | " 'GDP ($ per capita)',\n", 377 | " 'Literacy (%)',\n", 378 | " 'Phones (per 1000)',\n", 379 | " 'Arable (%)',\n", 380 | " 'Crops (%)',\n", 381 | " 'Other (%)',\n", 382 | " 'Climate',\n", 383 | " 'Birthrate',\n", 384 | " 'Deathrate',\n", 385 | " 'Agriculture',\n", 386 | " 'Industry',\n", 387 | " 'Service']" 388 | ] 389 | }, 390 | "execution_count": 11, 391 | "metadata": {}, 392 | "output_type": "execute_result" 393 | } 394 | ], 395 | "source": [ 396 | "cdf.drop('Country','Region','Population').columns" 397 | ] 398 | }, 399 | { 400 | "cell_type": "markdown", 401 | "metadata": {}, 402 | "source": [ 403 | "**Добавим литеральную колонку**\n", 404 | "\n", 405 | "В новом датафрейме будет колонка `ONE` со значением 1." 406 | ] 407 | }, 408 | { 409 | "cell_type": "code", 410 | "execution_count": 12, 411 | "metadata": {}, 412 | "outputs": [ 413 | { 414 | "data": { 415 | "text/plain": [ 416 | "['Country', 'Region', 'Population', 'ONE']" 417 | ] 418 | }, 419 | "execution_count": 12, 420 | "metadata": {}, 421 | "output_type": "execute_result" 422 | } 423 | ], 424 | "source": [ 425 | "cdf.select('Country','Region','Population').withColumn(\"ONE\",f.lit(1)).columns" 426 | ] 427 | }, 428 | { 429 | "cell_type": "markdown", 430 | "metadata": {}, 431 | "source": [ 432 | "**Переименуем колонку**\n", 433 | "\n", 434 | "Уж очень длинно звучит название колонки - давайте укоротим..." 435 | ] 436 | }, 437 | { 438 | "cell_type": "code", 439 | "execution_count": 13, 440 | "metadata": {}, 441 | "outputs": [ 442 | { 443 | "data": { 444 | "text/plain": [ 445 | "['Country',\n", 446 | " 'Region',\n", 447 | " 'Population',\n", 448 | " 'AREA',\n", 449 | " 'Pop. Density (per sq. mi.)',\n", 450 | " 'Coastline (coast/area ratio)',\n", 451 | " 'Net migration',\n", 452 | " 'Infant mortality (per 1000 births)',\n", 453 | " 'GDP ($ per capita)',\n", 454 | " 'Literacy (%)',\n", 455 | " 'Phones (per 1000)',\n", 456 | " 'Arable (%)',\n", 457 | " 'Crops (%)',\n", 458 | " 'Other (%)',\n", 459 | " 'Climate',\n", 460 | " 'Birthrate',\n", 461 | " 'Deathrate',\n", 462 | " 'Agriculture',\n", 463 | " 'Industry',\n", 464 | " 'Service']" 465 | ] 466 | }, 467 | "execution_count": 13, 468 | "metadata": {}, 469 | "output_type": "execute_result" 470 | } 471 | ], 472 | "source": [ 473 | "cdf.withColumnRenamed('Area (sq. mi.)','AREA').columns" 474 | ] 475 | }, 476 | { 477 | "cell_type": "markdown", 478 | "metadata": {}, 479 | "source": [ 480 | "**Фильтрация**\n", 481 | "\n", 482 | "Посмотрим, к какому региону приписали в этой табличке нас (пришлось немного поупражняться, чтобы " 483 | ] 484 | }, 485 | { 486 | "cell_type": "code", 487 | "execution_count": 14, 488 | "metadata": {}, 489 | "outputs": [ 490 | { 491 | "name": "stdout", 492 | "output_type": "stream", 493 | "text": [ 494 | "+-------+--------------------+\n", 495 | "|Country| Region|\n", 496 | "+-------+--------------------+\n", 497 | "|Russia |C.W. OF IND. STATES |\n", 498 | "+-------+--------------------+\n", 499 | "\n" 500 | ] 501 | } 502 | ], 503 | "source": [ 504 | "cdf.select('Country','Region').filter(f.col('Country').startswith('Russia')).show()" 505 | ] 506 | }, 507 | { 508 | "cell_type": "markdown", 509 | "metadata": {}, 510 | "source": [ 511 | "### Еще немного практики" 512 | ] 513 | }, 514 | { 515 | "cell_type": "markdown", 516 | "metadata": {}, 517 | "source": [ 518 | "**range()**\n", 519 | "\n", 520 | "Простейший способ создаиние датафрейма - создает датафрейм с одной числовой колонкой `id` и заполяет его строками указанного в параметре диапазона. Дата инженерам не нужно создавать датафреймы - они их обычно загружают. Но если нужно - вот способ." 521 | ] 522 | }, 523 | { 524 | "cell_type": "code", 525 | "execution_count": 15, 526 | "metadata": {}, 527 | "outputs": [ 528 | { 529 | "name": "stdout", 530 | "output_type": "stream", 531 | "text": [ 532 | "+---+\n", 533 | "| id|\n", 534 | "+---+\n", 535 | "| 0|\n", 536 | "| 1|\n", 537 | "| 2|\n", 538 | "| 3|\n", 539 | "| 4|\n", 540 | "| 5|\n", 541 | "| 6|\n", 542 | "| 7|\n", 543 | "| 8|\n", 544 | "| 9|\n", 545 | "+---+\n", 546 | "\n" 547 | ] 548 | } 549 | ], 550 | "source": [ 551 | "df = spark.range(10).show()" 552 | ] 553 | }, 554 | { 555 | "cell_type": "markdown", 556 | "metadata": {}, 557 | "source": [ 558 | "**distinct()**\n", 559 | "\n", 560 | "С помощью `distinct()` посмотрим - какие вообще регионы имеются в нашем датафрейме. Выглядит как-то подозрительно - что там, пробелы в конце что-ли?" 561 | ] 562 | }, 563 | { 564 | "cell_type": "code", 565 | "execution_count": 16, 566 | "metadata": {}, 567 | "outputs": [ 568 | { 569 | "name": "stdout", 570 | "output_type": "stream", 571 | "text": [ 572 | "+-----------------------------------+\n", 573 | "|Region |\n", 574 | "+-----------------------------------+\n", 575 | "|BALTICS |\n", 576 | "|C.W. OF IND. STATES |\n", 577 | "|ASIA (EX. NEAR EAST) |\n", 578 | "|WESTERN EUROPE |\n", 579 | "|NORTHERN AMERICA |\n", 580 | "|NEAR EAST |\n", 581 | "|EASTERN EUROPE |\n", 582 | "|OCEANIA |\n", 583 | "|SUB-SAHARAN AFRICA |\n", 584 | "|NORTHERN AFRICA |\n", 585 | "|LATIN AMER. & CARIB |\n", 586 | "+-----------------------------------+\n", 587 | "\n" 588 | ] 589 | } 590 | ], 591 | "source": [ 592 | "cdf.select('Region').distinct().show(truncate=False)" 593 | ] 594 | }, 595 | { 596 | "cell_type": "markdown", 597 | "metadata": {}, 598 | "source": [ 599 | "**length()**\n", 600 | "\n", 601 | "Добавим к имени региона колонку, которая покажет длину строки... Действительно, пробелы" 602 | ] 603 | }, 604 | { 605 | "cell_type": "code", 606 | "execution_count": 17, 607 | "metadata": {}, 608 | "outputs": [ 609 | { 610 | "name": "stdout", 611 | "output_type": "stream", 612 | "text": [ 613 | "+-----------------------------------+---+\n", 614 | "|Region |len|\n", 615 | "+-----------------------------------+---+\n", 616 | "|BALTICS |35 |\n", 617 | "|C.W. OF IND. STATES |20 |\n", 618 | "|ASIA (EX. NEAR EAST) |29 |\n", 619 | "|WESTERN EUROPE |35 |\n", 620 | "|NORTHERN AMERICA |35 |\n", 621 | "|NEAR EAST |35 |\n", 622 | "|EASTERN EUROPE |35 |\n", 623 | "|OCEANIA |35 |\n", 624 | "|SUB-SAHARAN AFRICA |35 |\n", 625 | "|NORTHERN AFRICA |35 |\n", 626 | "|LATIN AMER. & CARIB |23 |\n", 627 | "+-----------------------------------+---+\n", 628 | "\n" 629 | ] 630 | } 631 | ], 632 | "source": [ 633 | "cdf.select('Region').distinct().withColumn('len',f.length('Region')).show(100,truncate=False)" 634 | ] 635 | }, 636 | { 637 | "cell_type": "markdown", 638 | "metadata": {}, 639 | "source": [ 640 | "**monotonically_increasing_id(), alias(), replace()**\n", 641 | "\n", 642 | "Ну и напоследок \"завернем\" такое преобразование:\n", 643 | "\n", 644 | "* добавим `monotonically_increasing_id()` - обратите внимание: это на простом примере так красиво получилось, в жизни туда еще добавится номер партиции, будет не очень красиво, но монотонно возрастающе...\n", 645 | "* переименуем колонку с помощью `alias()`\n", 646 | "* через `expr()` \"укоротим\" название региона и переименуем его сразу в код\n", 647 | "* словарной заменой заменим часть регионов на их код" 648 | ] 649 | }, 650 | { 651 | "cell_type": "code", 652 | "execution_count": 18, 653 | "metadata": {}, 654 | "outputs": [ 655 | { 656 | "name": "stdout", 657 | "output_type": "stream", 658 | "text": [ 659 | "+---+------------------+-------------------+\n", 660 | "| ID| Country| RegCode|\n", 661 | "+---+------------------+-------------------+\n", 662 | "| 0| Afghanistan | ASA|\n", 663 | "| 1| Albania | EASTERN EUROPE|\n", 664 | "| 2| Algeria | NORTHERN AFRICA|\n", 665 | "| 3| American Samoa | OCN|\n", 666 | "| 4| Andorra | WESTERN EUROPE|\n", 667 | "| 5| Angola | SUB-SAHARAN AFRICA|\n", 668 | "| 6| Anguilla |LATIN AMER. & CARIB|\n", 669 | "| 7|Antigua & Barbuda |LATIN AMER. & CARIB|\n", 670 | "| 8| Argentina |LATIN AMER. & CARIB|\n", 671 | "| 9| Armenia |C.W. OF IND. STATES|\n", 672 | "| 10| Aruba |LATIN AMER. & CARIB|\n", 673 | "| 11| Australia | OCN|\n", 674 | "| 12| Austria | WESTERN EUROPE|\n", 675 | "| 13| Azerbaijan |C.W. OF IND. STATES|\n", 676 | "| 14| Bahamas, The |LATIN AMER. & CARIB|\n", 677 | "| 15| Bahrain | NEAR EAST|\n", 678 | "| 16| Bangladesh | ASA|\n", 679 | "| 17| Barbados |LATIN AMER. & CARIB|\n", 680 | "| 18| Belarus |C.W. OF IND. STATES|\n", 681 | "| 19| Belgium | WESTERN EUROPE|\n", 682 | "+---+------------------+-------------------+\n", 683 | "only showing top 20 rows\n", 684 | "\n" 685 | ] 686 | } 687 | ], 688 | "source": [ 689 | "regRepl = { 'OCEANIA': 'OCN', 'ASIA (EX. NEAR EAST)': 'ASA' }\n", 690 | "cdf.select(f.monotonically_increasing_id().alias('ID'),'Country',f.expr('rtrim(Region) as RegCode')) \\\n", 691 | " .replace(regRepl,None,'RegCode') \\\n", 692 | " .show()" 693 | ] 694 | }, 695 | { 696 | "cell_type": "markdown", 697 | "metadata": {}, 698 | "source": [ 699 | "**Итого**\n", 700 | "\n", 701 | "Возможности базовых трансформации поистинне неисчерпаемы. \n", 702 | "\n", 703 | "Изучайте документацию, пробуйте - в этом ноутбуке, создавайте свои.\n", 704 | "\n", 705 | "В этом модуле вам нужно будет преобразовать \"наши\" данные - в материалах выше содержатся необходимые методы. Джойны и объединения мы рассмотрим в следующем шаге." 706 | ] 707 | }, 708 | { 709 | "cell_type": "code", 710 | "execution_count": 19, 711 | "metadata": {}, 712 | "outputs": [], 713 | "source": [ 714 | "spark.stop()" 715 | ] 716 | } 717 | ], 718 | "metadata": { 719 | "jupytext": { 720 | "encoding": "# -*- coding: utf-8 -*-", 721 | "formats": "ipynb,py:light" 722 | }, 723 | "kernelspec": { 724 | "display_name": "Python 3", 725 | "language": "python", 726 | "name": "python3" 727 | }, 728 | "language_info": { 729 | "codemirror_mode": { 730 | "name": "ipython", 731 | "version": 3 732 | }, 733 | "file_extension": ".py", 734 | "mimetype": "text/x-python", 735 | "name": "python", 736 | "nbconvert_exporter": "python", 737 | "pygments_lexer": "ipython3", 738 | "version": "3.5.2" 739 | } 740 | }, 741 | "nbformat": 4, 742 | "nbformat_minor": 2 743 | } 744 | -------------------------------------------------------------------------------- /DE_course/Spark/05_joins.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Объединение dataframe" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "### Добавление строк и/или колонок\n", 15 | "\n", 16 | "* всегда два участника\n", 17 | "* join() - \"джойнит\" один датафрейм с другим\n", 18 | "* union() - конкатенирует один датафрейм с другим" 19 | ] 20 | }, 21 | { 22 | "cell_type": "markdown", 23 | "metadata": {}, 24 | "source": [ 25 | "### Join() и их виды\n", 26 | "\n", 27 | "* Inner joins: остаются строки, которые есть в левом и правом датафреймах\n", 28 | "* Outer joins: остаются строки, ключи которых есть в левом или правом датафрейме\n", 29 | "* Left outer joins: остаются строки, ключи которых есть в левом датафрейме\n", 30 | "* Right outer joins: остаются строки, ключи которых есть в правом датафрейме\n", 31 | "* Left semi joins: остаются только строки левого датафрейма, ключи которых есть в правом датафрейме\n", 32 | "* Left anti joins: остаются только строки левого датафрейма, ключей которых нет в правом датафрейме\n", 33 | "* Natural joins: выполняет объединение с помощью неявного сопоставления колонок в двух датафреймах (сопоставление по именам)\n", 34 | "* Cross (or Cartesian) joins: каждая строка левого датафрейма объединяется с каждой строкой правого датафрейма" 35 | ] 36 | }, 37 | { 38 | "cell_type": "markdown", 39 | "metadata": {}, 40 | "source": [ 41 | "### Метод join() и его параметры\n", 42 | "\n", 43 | "* leftDf.join(rightDf,joinExpression,how)\n", 44 | "* how: строка (inner, cross, outer, full, full_outer, left, left_outer, right, right_outer, left_semi, left_anti)\n", 45 | "* joinExpression: выражение (например `person[\"graduate_program\"]==graduateProgram['id']`)\n", 46 | "\n", 47 | "В материалах разбере виды join на примерах." 48 | ] 49 | }, 50 | { 51 | "cell_type": "markdown", 52 | "metadata": {}, 53 | "source": [ 54 | "### Union() и его использование\n", 55 | "\n", 56 | "* firstDf.union(secondDf)\n", 57 | "* конкатенация данных двух датафреймов\n", 58 | "* работает по \"локации\" (не по схеме)\n", 59 | "* максимальная аккуратность, расположение колонок в нужном порядке\n", 60 | "* unionByName() работает по именам колонок" 61 | ] 62 | }, 63 | { 64 | "cell_type": "markdown", 65 | "metadata": {}, 66 | "source": [ 67 | "### Lazy evaluation и оптимизация\n", 68 | "\n", 69 | "* трансформации - это правило формирования dataframe\n", 70 | "* \"промежуточные\" датафреймы в цепочке преобразований не снижают ее эффективность\n", 71 | "* \"многостраничные\" select-ы появляются из-за стремления к повышению эффективности SQL запроса\n", 72 | "* используя spark мы можем \"многостраничный\" select выразить последовательностью простых транформаций\n", 73 | "* повышается \"читабельность\" и наглядность\n", 74 | "* inner, cross, outer, full, full_outer, left, left_outer, right, right_outer, left_semi, and left_antiвозможность более \"гибкого\" управления кэшированием промежуточных результатов\n", 75 | "\n", 76 | "(см. примеры в материалах)" 77 | ] 78 | }, 79 | { 80 | "cell_type": "code", 81 | "execution_count": 1, 82 | "metadata": {}, 83 | "outputs": [], 84 | "source": [ 85 | "import os\n", 86 | "from pyspark.sql import SparkSession\n", 87 | "\n", 88 | "os.environ[\"SPARK_HOME\"] = \"/home/mk/mk_win/projects/SparkEdu/lib/python3.5/site-packages/pyspark\"\n", 89 | "os.environ[\"PYSPARK_PYTHON\"] = \"/usr/bin/python3\"\n", 90 | "os.environ[\"PYSPARK_DRIVER_PYTHON\"] = \"python3\"\n", 91 | "os.environ[\"PYSPARK_SUBMIT_ARGS\"] = \"pyspark-shell\"" 92 | ] 93 | }, 94 | { 95 | "cell_type": "code", 96 | "execution_count": 2, 97 | "metadata": {}, 98 | "outputs": [], 99 | "source": [ 100 | "master = \"local\"\n", 101 | "#master = \"yarn\"\n", 102 | "spark = SparkSession.builder.master(master).appName(\"spark_test\").getOrCreate()" 103 | ] 104 | }, 105 | { 106 | "cell_type": "markdown", 107 | "metadata": {}, 108 | "source": [ 109 | "**Специально подобранные данные**\n", 110 | "\n", 111 | "Для того, чтобы увидеть разницу, нужно воспользоваться специально подобранными данными (иначе просто потеряться). \n", 112 | "\n", 113 | "Данные взяты из книги - авторы постарались..." 114 | ] 115 | }, 116 | { 117 | "cell_type": "code", 118 | "execution_count": 20, 119 | "metadata": {}, 120 | "outputs": [], 121 | "source": [ 122 | "person = spark.createDataFrame([\n", 123 | "(0, \"Bill Chambers\", 0, [100]),\n", 124 | "(1, \"Matei Zaharia\", 1, [500, 250, 100]),\n", 125 | "(2, \"Michael Armbrust\", 1, [250, 100])])\\\n", 126 | ".toDF(\"id\", \"name\", \"graduate_program\", \"spark_status\")\n", 127 | "graduateProgram = spark.createDataFrame([\n", 128 | "(0, \"Masters\", \"School of Information\", \"UC Berkeley\"),\n", 129 | "(2, \"Masters\", \"EECS\", \"UC Berkeley\"),\n", 130 | "(1, \"Ph.D.\", \"EECS\", \"UC Berkeley\")])\\\n", 131 | ".toDF(\"id\", \"degree\", \"department\", \"school\")" 132 | ] 133 | }, 134 | { 135 | "cell_type": "code", 136 | "execution_count": 22, 137 | "metadata": {}, 138 | "outputs": [ 139 | { 140 | "name": "stdout", 141 | "output_type": "stream", 142 | "text": [ 143 | "+---+----------------+----------------+---------------+\n", 144 | "| id| name|graduate_program| spark_status|\n", 145 | "+---+----------------+----------------+---------------+\n", 146 | "| 0| Bill Chambers| 0| [100]|\n", 147 | "| 1| Matei Zaharia| 1|[500, 250, 100]|\n", 148 | "| 2|Michael Armbrust| 1| [250, 100]|\n", 149 | "+---+----------------+----------------+---------------+\n", 150 | "\n", 151 | "+---+-------+--------------------+-----------+\n", 152 | "| id| degree| department| school|\n", 153 | "+---+-------+--------------------+-----------+\n", 154 | "| 0|Masters|School of Informa...|UC Berkeley|\n", 155 | "| 2|Masters| EECS|UC Berkeley|\n", 156 | "| 1| Ph.D.| EECS|UC Berkeley|\n", 157 | "+---+-------+--------------------+-----------+\n", 158 | "\n" 159 | ] 160 | } 161 | ], 162 | "source": [ 163 | "person.show()\n", 164 | "graduateProgram.show()" 165 | ] 166 | }, 167 | { 168 | "cell_type": "markdown", 169 | "metadata": {}, 170 | "source": [ 171 | "### Примеры join()" 172 | ] 173 | }, 174 | { 175 | "cell_type": "markdown", 176 | "metadata": {}, 177 | "source": [ 178 | "Выражение, по которому будет происходить join, всегда одинаково." 179 | ] 180 | }, 181 | { 182 | "cell_type": "code", 183 | "execution_count": 7, 184 | "metadata": {}, 185 | "outputs": [], 186 | "source": [ 187 | "joinExpression = person[\"graduate_program\"] == graduateProgram['id']" 188 | ] 189 | }, 190 | { 191 | "cell_type": "markdown", 192 | "metadata": {}, 193 | "source": [ 194 | "**Inner**" 195 | ] 196 | }, 197 | { 198 | "cell_type": "code", 199 | "execution_count": 8, 200 | "metadata": {}, 201 | "outputs": [ 202 | { 203 | "name": "stdout", 204 | "output_type": "stream", 205 | "text": [ 206 | "+---+----------------+----------------+---------------+---+-------+--------------------+-----------+\n", 207 | "| id| name|graduate_program| spark_status| id| degree| department| school|\n", 208 | "+---+----------------+----------------+---------------+---+-------+--------------------+-----------+\n", 209 | "| 0| Bill Chambers| 0| [100]| 0|Masters|School of Informa...|UC Berkeley|\n", 210 | "| 1| Matei Zaharia| 1|[500, 250, 100]| 1| Ph.D.| EECS|UC Berkeley|\n", 211 | "| 2|Michael Armbrust| 1| [250, 100]| 1| Ph.D.| EECS|UC Berkeley|\n", 212 | "+---+----------------+----------------+---------------+---+-------+--------------------+-----------+\n", 213 | "\n" 214 | ] 215 | } 216 | ], 217 | "source": [ 218 | "joinType = \"inner\"\n", 219 | "person.join(graduateProgram, joinExpression, joinType).show()" 220 | ] 221 | }, 222 | { 223 | "cell_type": "markdown", 224 | "metadata": {}, 225 | "source": [ 226 | "**Outer**\n", 227 | "\n", 228 | "появляются строки из правого фрейма, для которых нет ключей в левом" 229 | ] 230 | }, 231 | { 232 | "cell_type": "code", 233 | "execution_count": 9, 234 | "metadata": {}, 235 | "outputs": [ 236 | { 237 | "name": "stdout", 238 | "output_type": "stream", 239 | "text": [ 240 | "+----+----------------+----------------+---------------+---+-------+--------------------+-----------+\n", 241 | "| id| name|graduate_program| spark_status| id| degree| department| school|\n", 242 | "+----+----------------+----------------+---------------+---+-------+--------------------+-----------+\n", 243 | "| 0| Bill Chambers| 0| [100]| 0|Masters|School of Informa...|UC Berkeley|\n", 244 | "| 1| Matei Zaharia| 1|[500, 250, 100]| 1| Ph.D.| EECS|UC Berkeley|\n", 245 | "| 2|Michael Armbrust| 1| [250, 100]| 1| Ph.D.| EECS|UC Berkeley|\n", 246 | "|null| null| null| null| 2|Masters| EECS|UC Berkeley|\n", 247 | "+----+----------------+----------------+---------------+---+-------+--------------------+-----------+\n", 248 | "\n" 249 | ] 250 | } 251 | ], 252 | "source": [ 253 | "joinType = \"outer\"\n", 254 | "person.join(graduateProgram, joinExpression, joinType).show()" 255 | ] 256 | }, 257 | { 258 | "cell_type": "markdown", 259 | "metadata": {}, 260 | "source": [ 261 | "**Left outer**\n", 262 | "\n", 263 | "поменяли местами датафреймы, теперь все строки из левого датафрейма показаны, не для всех нашлись ключи в правом" 264 | ] 265 | }, 266 | { 267 | "cell_type": "code", 268 | "execution_count": 11, 269 | "metadata": {}, 270 | "outputs": [ 271 | { 272 | "name": "stdout", 273 | "output_type": "stream", 274 | "text": [ 275 | "+---+-------+--------------------+-----------+----+----------------+----------------+---------------+\n", 276 | "| id| degree| department| school| id| name|graduate_program| spark_status|\n", 277 | "+---+-------+--------------------+-----------+----+----------------+----------------+---------------+\n", 278 | "| 0|Masters|School of Informa...|UC Berkeley| 0| Bill Chambers| 0| [100]|\n", 279 | "| 1| Ph.D.| EECS|UC Berkeley| 1| Matei Zaharia| 1|[500, 250, 100]|\n", 280 | "| 1| Ph.D.| EECS|UC Berkeley| 2|Michael Armbrust| 1| [250, 100]|\n", 281 | "| 2|Masters| EECS|UC Berkeley|null| null| null| null|\n", 282 | "+---+-------+--------------------+-----------+----+----------------+----------------+---------------+\n", 283 | "\n" 284 | ] 285 | } 286 | ], 287 | "source": [ 288 | "joinType = \"left_outer\"\n", 289 | "graduateProgram.join(person, joinExpression, joinType).show()" 290 | ] 291 | }, 292 | { 293 | "cell_type": "markdown", 294 | "metadata": {}, 295 | "source": [ 296 | "**Right outer**\n", 297 | "\n", 298 | "ключи из правого датафрейма, не для всех нашлось соответствие в левом" 299 | ] 300 | }, 301 | { 302 | "cell_type": "code", 303 | "execution_count": 12, 304 | "metadata": {}, 305 | "outputs": [ 306 | { 307 | "name": "stdout", 308 | "output_type": "stream", 309 | "text": [ 310 | "+----+----------------+----------------+---------------+---+-------+--------------------+-----------+\n", 311 | "| id| name|graduate_program| spark_status| id| degree| department| school|\n", 312 | "+----+----------------+----------------+---------------+---+-------+--------------------+-----------+\n", 313 | "| 0| Bill Chambers| 0| [100]| 0|Masters|School of Informa...|UC Berkeley|\n", 314 | "| 1| Matei Zaharia| 1|[500, 250, 100]| 1| Ph.D.| EECS|UC Berkeley|\n", 315 | "| 2|Michael Armbrust| 1| [250, 100]| 1| Ph.D.| EECS|UC Berkeley|\n", 316 | "|null| null| null| null| 2|Masters| EECS|UC Berkeley|\n", 317 | "+----+----------------+----------------+---------------+---+-------+--------------------+-----------+\n", 318 | "\n" 319 | ] 320 | } 321 | ], 322 | "source": [ 323 | "joinType = \"right_outer\"\n", 324 | "person.join(graduateProgram, joinExpression, joinType).show()" 325 | ] 326 | }, 327 | { 328 | "cell_type": "markdown", 329 | "metadata": {}, 330 | "source": [ 331 | "**Left semi**\n", 332 | "\n", 333 | "опять поменяли местами датафреймы. Остались только те программы (левый датафрейм), для которых были соответствия в правом. Правого датафрейма нет (т.е. чисто работа со строками одного датафрейма)." 334 | ] 335 | }, 336 | { 337 | "cell_type": "code", 338 | "execution_count": 13, 339 | "metadata": {}, 340 | "outputs": [ 341 | { 342 | "name": "stdout", 343 | "output_type": "stream", 344 | "text": [ 345 | "+---+-------+--------------------+-----------+\n", 346 | "| id| degree| department| school|\n", 347 | "+---+-------+--------------------+-----------+\n", 348 | "| 0|Masters|School of Informa...|UC Berkeley|\n", 349 | "| 1| Ph.D.| EECS|UC Berkeley|\n", 350 | "+---+-------+--------------------+-----------+\n", 351 | "\n" 352 | ] 353 | } 354 | ], 355 | "source": [ 356 | "joinType = \"left_semi\"\n", 357 | "graduateProgram.join(person, joinExpression, joinType).show()" 358 | ] 359 | }, 360 | { 361 | "cell_type": "markdown", 362 | "metadata": {}, 363 | "source": [ 364 | "**Left anti**\n", 365 | "\n", 366 | "опять поменяли местами датафреймы. Остались только те программы (левый датафрейм), для которых не было соответствия в правом. Правого датафрейма нет (т.е. чисто работа со строками одного датафрейма)." 367 | ] 368 | }, 369 | { 370 | "cell_type": "code", 371 | "execution_count": 14, 372 | "metadata": {}, 373 | "outputs": [ 374 | { 375 | "name": "stdout", 376 | "output_type": "stream", 377 | "text": [ 378 | "+---+-------+----------+-----------+\n", 379 | "| id| degree|department| school|\n", 380 | "+---+-------+----------+-----------+\n", 381 | "| 2|Masters| EECS|UC Berkeley|\n", 382 | "+---+-------+----------+-----------+\n", 383 | "\n" 384 | ] 385 | } 386 | ], 387 | "source": [ 388 | "joinType = \"left_anti\"\n", 389 | "graduateProgram.join(person, joinExpression, joinType).show()" 390 | ] 391 | }, 392 | { 393 | "cell_type": "markdown", 394 | "metadata": {}, 395 | "source": [ 396 | "### Примеры union()" 397 | ] 398 | }, 399 | { 400 | "cell_type": "markdown", 401 | "metadata": {}, 402 | "source": [ 403 | "Просто добавим датафрейм в \"хвост\" себе же" 404 | ] 405 | }, 406 | { 407 | "cell_type": "code", 408 | "execution_count": 16, 409 | "metadata": {}, 410 | "outputs": [ 411 | { 412 | "name": "stdout", 413 | "output_type": "stream", 414 | "text": [ 415 | "+---+-------+--------------------+-----------+\n", 416 | "| id| degree| department| school|\n", 417 | "+---+-------+--------------------+-----------+\n", 418 | "| 0|Masters|School of Informa...|UC Berkeley|\n", 419 | "| 2|Masters| EECS|UC Berkeley|\n", 420 | "| 1| Ph.D.| EECS|UC Berkeley|\n", 421 | "| 0|Masters|School of Informa...|UC Berkeley|\n", 422 | "| 2|Masters| EECS|UC Berkeley|\n", 423 | "| 1| Ph.D.| EECS|UC Berkeley|\n", 424 | "+---+-------+--------------------+-----------+\n", 425 | "\n" 426 | ] 427 | } 428 | ], 429 | "source": [ 430 | "graduateProgram.union(graduateProgram).show()" 431 | ] 432 | }, 433 | { 434 | "cell_type": "markdown", 435 | "metadata": {}, 436 | "source": [ 437 | "Поменяем местами колонки в добавляемом датафрейме - увидим, какая \"каша\" получилась." 438 | ] 439 | }, 440 | { 441 | "cell_type": "code", 442 | "execution_count": 18, 443 | "metadata": {}, 444 | "outputs": [ 445 | { 446 | "name": "stdout", 447 | "output_type": "stream", 448 | "text": [ 449 | "+-------+-------+--------------------+-----------+\n", 450 | "| id| degree| department| school|\n", 451 | "+-------+-------+--------------------+-----------+\n", 452 | "| 0|Masters|School of Informa...|UC Berkeley|\n", 453 | "| 2|Masters| EECS|UC Berkeley|\n", 454 | "| 1| Ph.D.| EECS|UC Berkeley|\n", 455 | "|Masters| 0|School of Informa...|UC Berkeley|\n", 456 | "|Masters| 2| EECS|UC Berkeley|\n", 457 | "| Ph.D.| 1| EECS|UC Berkeley|\n", 458 | "+-------+-------+--------------------+-----------+\n", 459 | "\n" 460 | ] 461 | } 462 | ], 463 | "source": [ 464 | "graduateProgram.union(graduateProgram.select(\"degree\",\"id\",\"department\",\"school\")).show()" 465 | ] 466 | }, 467 | { 468 | "cell_type": "markdown", 469 | "metadata": {}, 470 | "source": [ 471 | "Если воспользоваться методом `unionByName`, то все будет корректно даже с переставленными колонками." 472 | ] 473 | }, 474 | { 475 | "cell_type": "code", 476 | "execution_count": 19, 477 | "metadata": {}, 478 | "outputs": [ 479 | { 480 | "name": "stdout", 481 | "output_type": "stream", 482 | "text": [ 483 | "+---+-------+--------------------+-----------+\n", 484 | "| id| degree| department| school|\n", 485 | "+---+-------+--------------------+-----------+\n", 486 | "| 0|Masters|School of Informa...|UC Berkeley|\n", 487 | "| 2|Masters| EECS|UC Berkeley|\n", 488 | "| 1| Ph.D.| EECS|UC Berkeley|\n", 489 | "| 0|Masters|School of Informa...|UC Berkeley|\n", 490 | "| 2|Masters| EECS|UC Berkeley|\n", 491 | "| 1| Ph.D.| EECS|UC Berkeley|\n", 492 | "+---+-------+--------------------+-----------+\n", 493 | "\n" 494 | ] 495 | } 496 | ], 497 | "source": [ 498 | "graduateProgram.unionByName(graduateProgram.select(\"degree\",\"id\",\"department\",\"school\")).show()" 499 | ] 500 | }, 501 | { 502 | "cell_type": "markdown", 503 | "metadata": {}, 504 | "source": [ 505 | "### Многостраничный select и spark\n", 506 | "\n", 507 | "В этой части разберем на примере - как использование spark снижает требования к SQL и позволяет создавать более модульные, \"читабельные\" и потенциально более эффективные программы.\n", 508 | "\n", 509 | "Код, приведенный ниже, нельзя исполнить (нет таблиц), его можно просто посмотреть (поверив, что он работает).\n", 510 | "\n", 511 | "Возьмем относительно простой select: \n", 512 | "\n", 513 | "`select\n", 514 | " con.contract_id as contract_id,\n", 515 | " con.date_insert as date_insert,\n", 516 | " con.date_sign as date_sign,\n", 517 | " con.begin_date as begin_date,\n", 518 | " con.end_date as end_date,\n", 519 | " con.product_id as product_id,\n", 520 | " con.channel_sale_id as channel_sale_id,\n", 521 | " con.contract_prev_id as contract_prev_id,\n", 522 | " con.contract_option_id as contract_option_id,\n", 523 | " con.owner_subject_id as owner_subject_id,\n", 524 | " con.contract_type_id as contract_type_id,\n", 525 | " cst.contract_status_type_id as status_type_id,\n", 526 | " sbj.subject_type_id as subject_type_id,\n", 527 | " sbj.subject_name as subject_name,\n", 528 | " pp.birth_date as birth_date,\n", 529 | " uv.hid_party as hid_party\n", 530 | "from\n", 531 | " kasko.contract con\n", 532 | " join kasko.contract_status cst on cst.contract_status_id = con.contract_status_id\n", 533 | " join kasko.subject sbj on sbj.subject_id = con.owner_subject_id\n", 534 | " left join kasko.physical_person pp on pp.subject_id = con.owner_subject_id\n", 535 | " left join kasko.united_view uv on uv.source_id = sbj.subject_id and uv.source_name = 'UNI'\n", 536 | "where\n", 537 | " department_id=3075`\n", 538 | "\n", 539 | "И посмотрим, на какие \"кирпичики\" его можно разложить в spark.\n", 540 | "\n", 541 | "Видно, что участвуют 5 таблиц. Начнем с того, что сформулируем, что мы хотим получить от каждой таблицы (воспользуемся синтаксисом SQL для простоты - про него чуть позже в этом модуле)." 542 | ] 543 | }, 544 | { 545 | "cell_type": "code", 546 | "execution_count": null, 547 | "metadata": {}, 548 | "outputs": [], 549 | "source": [ 550 | "# таблица contract\n", 551 | "qCon = \"\"\"\n", 552 | "select\n", 553 | " contract_id as con_contract_id,\n", 554 | " date_insert as con_date_insert,\n", 555 | " date_sign as con_date_sign,\n", 556 | " begin_date as con_begin_date,\n", 557 | " end_date as con_end_date,\n", 558 | " product_id as con_product_id,\n", 559 | " channel_sale_id as con_channel_sale_id,\n", 560 | " contract_prev_id as con_contract_prev_id,\n", 561 | " contract_option_id as con_contract_option_id,\n", 562 | " owner_subject_id as con_owner_subject_id,\n", 563 | " contract_status_id as con_contract_status_id,\n", 564 | " contract_type_id as con_contract_type_id\n", 565 | "from\n", 566 | " kasko.contract\n", 567 | "where\n", 568 | " department_id = 3075\n", 569 | "\"\"\"\n", 570 | "dfCon = sp.sql(qCon)" 571 | ] 572 | }, 573 | { 574 | "cell_type": "code", 575 | "execution_count": null, 576 | "metadata": {}, 577 | "outputs": [], 578 | "source": [ 579 | "# таблица contract_status\n", 580 | "qCStat = \"\"\"\n", 581 | "select \n", 582 | " contract_status_id as cst_status_id,\n", 583 | " contract_status_type_id as cst_status_type_id\n", 584 | "from\n", 585 | " kasko.contract_status\n", 586 | "\"\"\"\n", 587 | "dfCStat = sp.sql(qCStat)" 588 | ] 589 | }, 590 | { 591 | "cell_type": "code", 592 | "execution_count": null, 593 | "metadata": {}, 594 | "outputs": [], 595 | "source": [ 596 | "# таблица subject\n", 597 | "qSubj = \"\"\"\n", 598 | "select\n", 599 | " subject_id as sbj_subject_id,\n", 600 | " subject_type_id as sbj_subject_type_id,\n", 601 | " subject_name as sbj_subject_name\n", 602 | "from\n", 603 | " kasko.subject\n", 604 | "\"\"\"\n", 605 | "dfSubj = sp.sql(qSubj)" 606 | ] 607 | }, 608 | { 609 | "cell_type": "code", 610 | "execution_count": null, 611 | "metadata": {}, 612 | "outputs": [], 613 | "source": [ 614 | "# таблица physical_person\n", 615 | "qPPers = \"\"\"\n", 616 | "select\n", 617 | " subject_id as pp_subject_id,\n", 618 | " birth_date as pp_birth_date\n", 619 | "from\n", 620 | " kasko.physical_person\n", 621 | "\"\"\"\n", 622 | "dfPPers = sp.sql(qPPers)" 623 | ] 624 | }, 625 | { 626 | "cell_type": "code", 627 | "execution_count": null, 628 | "metadata": {}, 629 | "outputs": [], 630 | "source": [ 631 | "# таблица united_view\n", 632 | "qUView = \"\"\"\n", 633 | "select\n", 634 | " source_id as uv_source_id,\n", 635 | " hid_party as uv_hid_party\n", 636 | "from\n", 637 | " kasko.united_view\n", 638 | "where\n", 639 | " source_name = 'UNI'\n", 640 | "\"\"\"\n", 641 | "dfUView = sp.sql(qUView)" 642 | ] 643 | }, 644 | { 645 | "cell_type": "markdown", 646 | "metadata": {}, 647 | "source": [ 648 | "Все эти датафреймы нам могут понадобиться многократно (в нашем конвейере преобразований данных), можно включить в них побольше колонок (на все случаи жизни). Получим \"универсальные\" кирпичики, из которых потом будем складывать \"запросы\" (трансформации).\n", 649 | "\n", 650 | "Далее формулируем правила связывания датафреймов, это всего лишь выражения, которые показывают - как связаны между собой таблицы. Эти знания универсальны, могут быть унаследованы из схемы реляционной базы данных. Тоже универсальные \"кирпичики\" другого рода - связи между таблицами." 651 | ] 652 | }, 653 | { 654 | "cell_type": "code", 655 | "execution_count": null, 656 | "metadata": {}, 657 | "outputs": [], 658 | "source": [ 659 | "con_stat = f.col(\"cst_status_id\")==f.col(\"con_contract_status_id\")\n", 660 | "con_subj_own = f.col(\"con_owner_subject_id\")==f.col(\"sbj_subject_id\")\n", 661 | "con_ppers_own = f.col(\"con_owner_subject_id\")==f.col(\"pp_subject_id\")\n", 662 | "subj_uview = f.col(\"sbj_subject_id\")==f.col(\"uv_source_id\")" 663 | ] 664 | }, 665 | { 666 | "cell_type": "markdown", 667 | "metadata": {}, 668 | "source": [ 669 | "И, наконец, формулируем наш \"многостраничный\" запрос" 670 | ] 671 | }, 672 | { 673 | "cell_type": "code", 674 | "execution_count": null, 675 | "metadata": {}, 676 | "outputs": [], 677 | "source": [ 678 | "resDf = dfCon.join(dfSubj,con_subj_own,\"inner\")\\\n", 679 | " .join(dfCStat,con_stat, \"inner\")\\\n", 680 | " .join(dfPPers,con_ppers_own, \"left\")\\\n", 681 | " .join(dfUView,subj_uview,\"left\")" 682 | ] 683 | }, 684 | { 685 | "cell_type": "markdown", 686 | "metadata": {}, 687 | "source": [ 688 | "Он эквивалентен исходному `select`-у, но... проще (может быть, непривычнее, но проще). Он состоит из кирпичиков, которые объединяются в простую цепочку. Оптимизатор spark сделает свое дело - итоговый запрос будет выполнен оптимально (также, как оптимально будет выполнен и исходный SQL запрос).\n", 689 | "\n", 690 | "А теперь представим себе, что таблица `contract` у нас участвует в половине преобразований. Понять из набора select-ов, какой набор колонок нужно выбирать, какую таблицу кэшировать сложнее, чем в цепочках spark, потому что они набираются из \"кирпичиков\", каждый из которых фактически представляет собой подмножество полей исходных таблиц." 691 | ] 692 | }, 693 | { 694 | "cell_type": "code", 695 | "execution_count": 4, 696 | "metadata": {}, 697 | "outputs": [], 698 | "source": [ 699 | "spark.stop()" 700 | ] 701 | } 702 | ], 703 | "metadata": { 704 | "jupytext": { 705 | "encoding": "# -*- coding: utf-8 -*-", 706 | "formats": "ipynb,py:light" 707 | }, 708 | "kernelspec": { 709 | "display_name": "Python 3", 710 | "language": "python", 711 | "name": "python3" 712 | }, 713 | "language_info": { 714 | "codemirror_mode": { 715 | "name": "ipython", 716 | "version": 3 717 | }, 718 | "file_extension": ".py", 719 | "mimetype": "text/x-python", 720 | "name": "python", 721 | "nbconvert_exporter": "python", 722 | "pygments_lexer": "ipython3", 723 | "version": "3.5.2" 724 | } 725 | }, 726 | "nbformat": 4, 727 | "nbformat_minor": 2 728 | } 729 | -------------------------------------------------------------------------------- /DE_course/Spark/06_aggr.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Агрегаты и оконные функции, сортировка" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "### Агрегаты\n", 15 | "\n", 16 | "* агрегирование - это получение итогового значения по некой группе\n", 17 | "* \"тип\" итогового значения обычно определяется агрегирующей функцией (sum, max, ...)\n", 18 | "* группировать можно разными способами\n", 19 | " * по всему датафрейму (нет группировки)\n", 20 | " * по его колонкам датафрейма\n", 21 | " * с использованием \"оконных\" функций" 22 | ] 23 | }, 24 | { 25 | "cell_type": "markdown", 26 | "metadata": {}, 27 | "source": [ 28 | "### Агрегирующие функции\n", 29 | "\n", 30 | "Их много, см. документацию, основные - min(), max(), sum(), count(), countDistinct().\n", 31 | "\n", 32 | "Агрегирующие функции - это трансформации." 33 | ] 34 | }, 35 | { 36 | "cell_type": "markdown", 37 | "metadata": {}, 38 | "source": [ 39 | "### Группировка\n", 40 | "\n", 41 | "* `df.select(sum(\"column\"))` - по всему датафрейма\n", 42 | "* `groupBy()` - создает `RelationalGroupedDataset`, который потом агрегируем\n", 43 | "* оконная функция задает `window`, которое потом используется агрегирующей функцией\n", 44 | "\n", 45 | "Оконные функции - мощный и универсальный механизм, см. материалы, там приведеные примеры его применения." 46 | ] 47 | }, 48 | { 49 | "cell_type": "markdown", 50 | "metadata": {}, 51 | "source": [ 52 | "### Сортировка\n", 53 | "\n", 54 | "* два метода (синонимы) `sort()` и `orderBy()`\n", 55 | "* сортируем по значению колонки/колонок\n", 56 | "* задавать колонки можно именами или строковым выражением\n", 57 | "* сортировать можно по возрастанию или убыванию\n" 58 | ] 59 | }, 60 | { 61 | "cell_type": "markdown", 62 | "metadata": {}, 63 | "source": [ 64 | "### Практикуемся" 65 | ] 66 | }, 67 | { 68 | "cell_type": "code", 69 | "execution_count": 5, 70 | "metadata": {}, 71 | "outputs": [], 72 | "source": [ 73 | "import os\n", 74 | "from pyspark.sql import SparkSession\n", 75 | "from pyspark.sql import functions as f\n", 76 | "os.environ[\"SPARK_HOME\"] = \"/home/mk/mk_win/projects/SparkEdu/lib/python3.5/site-packages/pyspark\"\n", 77 | "os.environ[\"PYSPARK_PYTHON\"] = \"/usr/bin/python3\"\n", 78 | "os.environ[\"PYSPARK_DRIVER_PYTHON\"] = \"python3\"\n", 79 | "os.environ[\"PYSPARK_SUBMIT_ARGS\"] = \"pyspark-shell\"" 80 | ] 81 | }, 82 | { 83 | "cell_type": "code", 84 | "execution_count": 2, 85 | "metadata": {}, 86 | "outputs": [], 87 | "source": [ 88 | "spark = SparkSession.builder.master(\"local\").appName(\"spark_aggr\").getOrCreate()" 89 | ] 90 | }, 91 | { 92 | "cell_type": "code", 93 | "execution_count": 3, 94 | "metadata": {}, 95 | "outputs": [], 96 | "source": [ 97 | "cdf = spark.read.format(\"csv\") \\\n", 98 | " .option(\"mode\", \"FAILFAST\") \\\n", 99 | " .option(\"inferSchema\", \"true\") \\\n", 100 | " .option(\"header\",\"true\") \\\n", 101 | " .option(\"path\", \"data/countries of the world.csv\") \\\n", 102 | " .load()" 103 | ] 104 | }, 105 | { 106 | "cell_type": "markdown", 107 | "metadata": {}, 108 | "source": [ 109 | "**Без группировки**\n", 110 | "\n", 111 | "Агрегируем по всему датафрейму" 112 | ] 113 | }, 114 | { 115 | "cell_type": "code", 116 | "execution_count": 7, 117 | "metadata": {}, 118 | "outputs": [ 119 | { 120 | "name": "stdout", 121 | "output_type": "stream", 122 | "text": [ 123 | "+---------------+\n", 124 | "|max(Population)|\n", 125 | "+---------------+\n", 126 | "| 1313973713|\n", 127 | "+---------------+\n", 128 | "\n" 129 | ] 130 | } 131 | ], 132 | "source": [ 133 | "cdf.select(f.max(\"Population\")).show()" 134 | ] 135 | }, 136 | { 137 | "cell_type": "markdown", 138 | "metadata": {}, 139 | "source": [ 140 | "**groupBy**\n", 141 | "\n", 142 | "Посмотрим агрегат в разбивке по колонке (используем groupBy())" 143 | ] 144 | }, 145 | { 146 | "cell_type": "code", 147 | "execution_count": 9, 148 | "metadata": {}, 149 | "outputs": [ 150 | { 151 | "name": "stdout", 152 | "output_type": "stream", 153 | "text": [ 154 | "+--------------------+---------------+\n", 155 | "| Region|max(Population)|\n", 156 | "+--------------------+---------------+\n", 157 | "|BALTICS ...| 3585906|\n", 158 | "|C.W. OF IND. STATES | 142893540|\n", 159 | "|ASIA (EX. NEAR EA...| 1313973713|\n", 160 | "|WESTERN EUROPE ...| 82422299|\n", 161 | "|NORTHERN AMERICA ...| 298444215|\n", 162 | "|NEAR EAST ...| 70413958|\n", 163 | "|EASTERN EUROPE ...| 38536869|\n", 164 | "|OCEANIA ...| 20264082|\n", 165 | "|SUB-SAHARAN AFRIC...| 131859731|\n", 166 | "|NORTHERN AFRICA ...| 78887007|\n", 167 | "|LATIN AMER. & CAR...| 188078227|\n", 168 | "+--------------------+---------------+\n", 169 | "\n" 170 | ] 171 | } 172 | ], 173 | "source": [ 174 | "cdf.groupBy(\"Region\").max(\"Population\").show()" 175 | ] 176 | }, 177 | { 178 | "cell_type": "markdown", 179 | "metadata": {}, 180 | "source": [ 181 | "**оконная функция**\n", 182 | "\n", 183 | "Сделаем то же самое, но с использованием оконной функции." 184 | ] 185 | }, 186 | { 187 | "cell_type": "code", 188 | "execution_count": 22, 189 | "metadata": {}, 190 | "outputs": [ 191 | { 192 | "name": "stdout", 193 | "output_type": "stream", 194 | "text": [ 195 | "+--------------------+-------------+\n", 196 | "| Region|MaxPopulation|\n", 197 | "+--------------------+-------------+\n", 198 | "|BALTICS ...| 3585906|\n", 199 | "|C.W. OF IND. STATES | 142893540|\n", 200 | "|ASIA (EX. NEAR EA...| 1313973713|\n", 201 | "|WESTERN EUROPE ...| 82422299|\n", 202 | "|NORTHERN AMERICA ...| 298444215|\n", 203 | "|NEAR EAST ...| 70413958|\n", 204 | "|EASTERN EUROPE ...| 38536869|\n", 205 | "|OCEANIA ...| 20264082|\n", 206 | "|SUB-SAHARAN AFRIC...| 131859731|\n", 207 | "|NORTHERN AFRICA ...| 78887007|\n", 208 | "|LATIN AMER. & CAR...| 188078227|\n", 209 | "+--------------------+-------------+\n", 210 | "\n" 211 | ] 212 | } 213 | ], 214 | "source": [ 215 | "from pyspark.sql.window import Window\n", 216 | "windowSpec = Window.partitionBy(\"Region\")\n", 217 | "cdf.select(\"Region\",f.max(\"Population\").over(windowSpec).alias(\"MaxPopulation\")).distinct().show()" 218 | ] 219 | }, 220 | { 221 | "cell_type": "markdown", 222 | "metadata": {}, 223 | "source": [ 224 | "**Сортировка**\n", 225 | "\n", 226 | "Упорядочим предыдущий результат по убыванию максимального населения" 227 | ] 228 | }, 229 | { 230 | "cell_type": "code", 231 | "execution_count": 23, 232 | "metadata": {}, 233 | "outputs": [ 234 | { 235 | "name": "stdout", 236 | "output_type": "stream", 237 | "text": [ 238 | "+--------------------+-------------+\n", 239 | "| Region|MaxPopulation|\n", 240 | "+--------------------+-------------+\n", 241 | "|ASIA (EX. NEAR EA...| 1313973713|\n", 242 | "|NORTHERN AMERICA ...| 298444215|\n", 243 | "|LATIN AMER. & CAR...| 188078227|\n", 244 | "|C.W. OF IND. STATES | 142893540|\n", 245 | "|SUB-SAHARAN AFRIC...| 131859731|\n", 246 | "|WESTERN EUROPE ...| 82422299|\n", 247 | "|NORTHERN AFRICA ...| 78887007|\n", 248 | "|NEAR EAST ...| 70413958|\n", 249 | "|EASTERN EUROPE ...| 38536869|\n", 250 | "|OCEANIA ...| 20264082|\n", 251 | "|BALTICS ...| 3585906|\n", 252 | "+--------------------+-------------+\n", 253 | "\n" 254 | ] 255 | } 256 | ], 257 | "source": [ 258 | "cdf.select(\"Region\",f.max(\"Population\").over(windowSpec).alias(\"MaxPopulation\")).distinct().sort(f.desc(\"MaxPopulation\")).show()" 259 | ] 260 | }, 261 | { 262 | "cell_type": "markdown", 263 | "metadata": {}, 264 | "source": [ 265 | "**Минимум или максимум**\n", 266 | "\n", 267 | "Завершающий пример - чуть усложним: найдем страны с минимальным и максимальным населением в регионах\n", 268 | "\n", 269 | "Комментарии:\n", 270 | "\n", 271 | "* оконная функция просто группирует по региону\n", 272 | "* добавляем колонки с минимальным и максимальным населением по региону (`minp, maxp`)\n", 273 | "* оставляем в датафрейме только страны, население которых самое маленькое или большое (используем expr)\n", 274 | "* добавляем колонку `which`, в которую записываем - какая это страна (с минимальным или максимальным населением, используем функции `when().otherwise()`\n", 275 | "* удалим неинтересные нам колонки (`select()`)\n", 276 | "* упорядочим - покажем сначала самые большие страны, потом - самые маленькие (по убыванию населения)" 277 | ] 278 | }, 279 | { 280 | "cell_type": "code", 281 | "execution_count": 54, 282 | "metadata": {}, 283 | "outputs": [ 284 | { 285 | "name": "stdout", 286 | "output_type": "stream", 287 | "text": [ 288 | "+--------------------+--------------------+----------+-----+\n", 289 | "| Country| Region|Population|which|\n", 290 | "+--------------------+--------------------+----------+-----+\n", 291 | "| China |ASIA (EX. NEAR EA...|1313973713| MAX|\n", 292 | "| United States |NORTHERN AMERICA ...| 298444215| MAX|\n", 293 | "| Brazil |LATIN AMER. & CAR...| 188078227| MAX|\n", 294 | "| Russia |C.W. OF IND. STATES | 142893540| MAX|\n", 295 | "| Nigeria |SUB-SAHARAN AFRIC...| 131859731| MAX|\n", 296 | "| Germany |WESTERN EUROPE ...| 82422299| MAX|\n", 297 | "| Egypt |NORTHERN AFRICA ...| 78887007| MAX|\n", 298 | "| Turkey |NEAR EAST ...| 70413958| MAX|\n", 299 | "| Poland |EASTERN EUROPE ...| 38536869| MAX|\n", 300 | "| Australia |OCEANIA ...| 20264082| MAX|\n", 301 | "| Lithuania |BALTICS ...| 3585906| MAX|\n", 302 | "| Armenia |C.W. OF IND. STATES | 2976372| MIN|\n", 303 | "| Slovenia |EASTERN EUROPE ...| 2010347| MIN|\n", 304 | "| Estonia |BALTICS ...| 1324333| MIN|\n", 305 | "| Bahrain |NEAR EAST ...| 698585| MIN|\n", 306 | "| Maldives |ASIA (EX. NEAR EA...| 359008| MIN|\n", 307 | "| Western Sahara |NORTHERN AFRICA ...| 273008| MIN|\n", 308 | "| Gibraltar |WESTERN EUROPE ...| 27928| MIN|\n", 309 | "| Tuvalu |OCEANIA ...| 11810| MIN|\n", 310 | "| Montserrat |LATIN AMER. & CAR...| 9439| MIN|\n", 311 | "| Saint Helena |SUB-SAHARAN AFRIC...| 7502| MIN|\n", 312 | "|St Pierre & Mique...|NORTHERN AMERICA ...| 7026| MIN|\n", 313 | "+--------------------+--------------------+----------+-----+\n", 314 | "\n" 315 | ] 316 | } 317 | ], 318 | "source": [ 319 | "wr = Window.partitionBy('Region')\n", 320 | "cdf.withColumn('minp', f.min('Population').over(wr))\\\n", 321 | " .withColumn('maxp', f.max('Population').over(wr)) \\\n", 322 | " .where((f.expr('Population==minp or Population==maxp') ) ) \\\n", 323 | " .withColumn(\"which\",f.when(f.expr(\"Population==minp\"),'MIN').otherwise('MAX')) \\\n", 324 | " .select(\"Country\",\"Region\",\"Population\",\"which\") \\\n", 325 | " .sort(\"which\",f.desc(\"Population\")) \\\n", 326 | " .show(100)\n" 327 | ] 328 | }, 329 | { 330 | "cell_type": "code", 331 | "execution_count": null, 332 | "metadata": {}, 333 | "outputs": [], 334 | "source": [ 335 | "spark.stop()" 336 | ] 337 | } 338 | ], 339 | "metadata": { 340 | "jupytext": { 341 | "formats": "ipynb,py:light" 342 | }, 343 | "kernelspec": { 344 | "display_name": "Python 3", 345 | "language": "python", 346 | "name": "python3" 347 | }, 348 | "language_info": { 349 | "codemirror_mode": { 350 | "name": "ipython", 351 | "version": 3 352 | }, 353 | "file_extension": ".py", 354 | "mimetype": "text/x-python", 355 | "name": "python", 356 | "nbconvert_exporter": "python", 357 | "pygments_lexer": "ipython3", 358 | "version": "3.5.2" 359 | } 360 | }, 361 | "nbformat": 4, 362 | "nbformat_minor": 2 363 | } 364 | -------------------------------------------------------------------------------- /DE_course/Spark/07_sql.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Spark SQL" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "### Что такое Spark SQL\n", 15 | "\n", 16 | "* свой собственный SQL движок\n", 17 | "* может использоваться \"вперемешку\" с датафреймами\n", 18 | "* технически может работать (и работает) с Hive Metastore\n", 19 | "* поддерживает подмножество ANSI SQL:2003\n", 20 | "* есть интерактивная консоль (`spark-sql`)" 21 | ] 22 | }, 23 | { 24 | "cell_type": "markdown", 25 | "metadata": {}, 26 | "source": [ 27 | "### Как использовать\n", 28 | "\n", 29 | "* основной метод - `sql()` (метод объекта SparkSession)\n", 30 | "* возвращает dataframe\n", 31 | "* lazy evaluation\n", 32 | "* может использоваться для доступа к источникам\n", 33 | " * не уверен в параллелизме\n", 34 | "* хорошо работает с Hive (эффективно, параллельно)" 35 | ] 36 | }, 37 | { 38 | "cell_type": "markdown", 39 | "metadata": {}, 40 | "source": [ 41 | "### Какие операции поддерживаются\n", 42 | "\n", 43 | "* создание таблиц и view\n", 44 | " * включая внешние (поддерживается синтаксис Hive QL)\n", 45 | "* создание баз\n", 46 | "* `insert` \n", 47 | "* основное - `select`" 48 | ] 49 | }, 50 | { 51 | "cell_type": "markdown", 52 | "metadata": {}, 53 | "source": [ 54 | "### Использование SQL и Structured API\n", 55 | "\n", 56 | "* dataframe можно \"зарегистрировать\" как view (`.createOrReplaceTempView(\"some_sql_view\")`)\n", 57 | "* все трансформации - lazy\n", 58 | "* в итоге все будет скомпилировано в операции над RDD\n" 59 | ] 60 | }, 61 | { 62 | "cell_type": "markdown", 63 | "metadata": {}, 64 | "source": [ 65 | "### Logical plan\n", 66 | "\n", 67 | "* трансформации формируют логический план (алгоритм)\n", 68 | "* его можно посмотреть (`.explain()`)\n", 69 | "* планы запросов SQL и dataframe эквивалентны" 70 | ] 71 | }, 72 | { 73 | "cell_type": "markdown", 74 | "metadata": {}, 75 | "source": [ 76 | "### Кэширование таблиц\n", 77 | "\n", 78 | "* `CACHE TABLE` - кэширует таблицу\n", 79 | "* `UNCACHE TABLE` - чистит кэш" 80 | ] 81 | }, 82 | { 83 | "cell_type": "markdown", 84 | "metadata": {}, 85 | "source": [ 86 | "### Попрактикуемся\n", 87 | "\n", 88 | "Для простоты будем все делать локально - зарегистрируем таблицу со странами, как `view` и будем с ней работать с помощью SQL." 89 | ] 90 | }, 91 | { 92 | "cell_type": "code", 93 | "execution_count": 1, 94 | "metadata": {}, 95 | "outputs": [], 96 | "source": [ 97 | "import os\n", 98 | "from pyspark.sql import SparkSession\n", 99 | "from pyspark.sql import functions as f\n", 100 | "os.environ[\"SPARK_HOME\"] = \"/home/mk/mk_win/projects/SparkEdu/lib/python3.5/site-packages/pyspark\"\n", 101 | "os.environ[\"PYSPARK_PYTHON\"] = \"/usr/bin/python3\"\n", 102 | "os.environ[\"PYSPARK_DRIVER_PYTHON\"] = \"python3\"\n", 103 | "os.environ[\"PYSPARK_SUBMIT_ARGS\"] = \"pyspark-shell\"" 104 | ] 105 | }, 106 | { 107 | "cell_type": "code", 108 | "execution_count": 2, 109 | "metadata": {}, 110 | "outputs": [], 111 | "source": [ 112 | "spark = SparkSession.builder.master(\"local\").appName(\"spark_sql\").getOrCreate()" 113 | ] 114 | }, 115 | { 116 | "cell_type": "code", 117 | "execution_count": 6, 118 | "metadata": {}, 119 | "outputs": [], 120 | "source": [ 121 | "cdf = spark.read.format(\"csv\") \\\n", 122 | " .option(\"mode\", \"FAILFAST\") \\\n", 123 | " .option(\"inferSchema\", \"true\") \\\n", 124 | " .option(\"header\",\"true\") \\\n", 125 | " .option(\"path\", \"data/countries of the world.csv\") \\\n", 126 | " .load()\n", 127 | "cdf.createOrReplaceTempView(\"countries\")" 128 | ] 129 | }, 130 | { 131 | "cell_type": "markdown", 132 | "metadata": {}, 133 | "source": [ 134 | "**Простой select**" 135 | ] 136 | }, 137 | { 138 | "cell_type": "code", 139 | "execution_count": 4, 140 | "metadata": {}, 141 | "outputs": [ 142 | { 143 | "name": "stdout", 144 | "output_type": "stream", 145 | "text": [ 146 | "+--------+\n", 147 | "|count(1)|\n", 148 | "+--------+\n", 149 | "| 227|\n", 150 | "+--------+\n", 151 | "\n" 152 | ] 153 | } 154 | ], 155 | "source": [ 156 | "spark.sql(\"select count(*) from countries\").show()" 157 | ] 158 | }, 159 | { 160 | "cell_type": "markdown", 161 | "metadata": {}, 162 | "source": [ 163 | "**Информация о таблице**" 164 | ] 165 | }, 166 | { 167 | "cell_type": "code", 168 | "execution_count": 5, 169 | "metadata": {}, 170 | "outputs": [ 171 | { 172 | "name": "stdout", 173 | "output_type": "stream", 174 | "text": [ 175 | "+--------------------+---------+-------+\n", 176 | "| col_name|data_type|comment|\n", 177 | "+--------------------+---------+-------+\n", 178 | "| Country| string| null|\n", 179 | "| Region| string| null|\n", 180 | "| Population| int| null|\n", 181 | "| Area (sq. mi.)| int| null|\n", 182 | "|Pop. Density (per...| string| null|\n", 183 | "|Coastline (coast/...| string| null|\n", 184 | "| Net migration| string| null|\n", 185 | "|Infant mortality ...| string| null|\n", 186 | "| GDP ($ per capita)| int| null|\n", 187 | "| Literacy (%)| string| null|\n", 188 | "| Phones (per 1000)| string| null|\n", 189 | "| Arable (%)| string| null|\n", 190 | "| Crops (%)| string| null|\n", 191 | "| Other (%)| string| null|\n", 192 | "| Climate| string| null|\n", 193 | "| Birthrate| string| null|\n", 194 | "| Deathrate| string| null|\n", 195 | "| Agriculture| string| null|\n", 196 | "| Industry| string| null|\n", 197 | "| Service| string| null|\n", 198 | "+--------------------+---------+-------+\n", 199 | "\n" 200 | ] 201 | } 202 | ], 203 | "source": [ 204 | "spark.sql(\"describe table countries\").show()" 205 | ] 206 | }, 207 | { 208 | "cell_type": "markdown", 209 | "metadata": {}, 210 | "source": [ 211 | "**Что с пробелами**\n", 212 | "\n", 213 | "Посмотрим - как спробелами в названиях стран и регионов" 214 | ] 215 | }, 216 | { 217 | "cell_type": "code", 218 | "execution_count": 11, 219 | "metadata": {}, 220 | "outputs": [ 221 | { 222 | "name": "stdout", 223 | "output_type": "stream", 224 | "text": [ 225 | "+-----------------------------------+--------------+\n", 226 | "|region |length(region)|\n", 227 | "+-----------------------------------+--------------+\n", 228 | "|EASTERN EUROPE |35 |\n", 229 | "|OCEANIA |35 |\n", 230 | "|SUB-SAHARAN AFRICA |35 |\n", 231 | "|NORTHERN AMERICA |35 |\n", 232 | "|NEAR EAST |35 |\n", 233 | "|WESTERN EUROPE |35 |\n", 234 | "|BALTICS |35 |\n", 235 | "|ASIA (EX. NEAR EAST) |29 |\n", 236 | "|NORTHERN AFRICA |35 |\n", 237 | "|C.W. OF IND. STATES |20 |\n", 238 | "|LATIN AMER. & CARIB |23 |\n", 239 | "+-----------------------------------+--------------+\n", 240 | "\n" 241 | ] 242 | } 243 | ], 244 | "source": [ 245 | "spark.sql(\"select distinct region,length(region) from countries\").show(100,False)" 246 | ] 247 | }, 248 | { 249 | "cell_type": "markdown", 250 | "metadata": {}, 251 | "source": [ 252 | "**Поработаем смешанно - SQL + dataframe**" 253 | ] 254 | }, 255 | { 256 | "cell_type": "code", 257 | "execution_count": 12, 258 | "metadata": {}, 259 | "outputs": [ 260 | { 261 | "name": "stdout", 262 | "output_type": "stream", 263 | "text": [ 264 | "+-------+--------------------+----------+\n", 265 | "|country| region|population|\n", 266 | "+-------+--------------------+----------+\n", 267 | "|Russia |C.W. OF IND. STATES | 142893540|\n", 268 | "+-------+--------------------+----------+\n", 269 | "\n" 270 | ] 271 | } 272 | ], 273 | "source": [ 274 | "spark.sql(\"select country,region,population from countries\") \\\n", 275 | " .filter(f.col('country').startswith('Russia')) \\\n", 276 | " .show()" 277 | ] 278 | }, 279 | { 280 | "cell_type": "markdown", 281 | "metadata": {}, 282 | "source": [ 283 | "**Logical plans**\n", 284 | "\n", 285 | "Посмотрим на логические планы выполнения SQL запроса и dataframe и убедимся в их \"похожести\" (найти различия, конечно, можно, но они - косметические). " 286 | ] 287 | }, 288 | { 289 | "cell_type": "code", 290 | "execution_count": 21, 291 | "metadata": {}, 292 | "outputs": [ 293 | { 294 | "name": "stdout", 295 | "output_type": "stream", 296 | "text": [ 297 | "+--------------------+----------+\n", 298 | "| Region|ncountries|\n", 299 | "+--------------------+----------+\n", 300 | "|SUB-SAHARAN AFRIC...| 51|\n", 301 | "|LATIN AMER. & CAR...| 45|\n", 302 | "|ASIA (EX. NEAR EA...| 28|\n", 303 | "|WESTERN EUROPE ...| 28|\n", 304 | "|OCEANIA ...| 21|\n", 305 | "|NEAR EAST ...| 16|\n", 306 | "|EASTERN EUROPE ...| 12|\n", 307 | "|C.W. OF IND. STATES | 12|\n", 308 | "|NORTHERN AFRICA ...| 6|\n", 309 | "|NORTHERN AMERICA ...| 5|\n", 310 | "|BALTICS ...| 3|\n", 311 | "+--------------------+----------+\n", 312 | "\n", 313 | "+--------------------+----------+\n", 314 | "| region|ncountries|\n", 315 | "+--------------------+----------+\n", 316 | "|SUB-SAHARAN AFRIC...| 51|\n", 317 | "|LATIN AMER. & CAR...| 45|\n", 318 | "|ASIA (EX. NEAR EA...| 28|\n", 319 | "|WESTERN EUROPE ...| 28|\n", 320 | "|OCEANIA ...| 21|\n", 321 | "|NEAR EAST ...| 16|\n", 322 | "|EASTERN EUROPE ...| 12|\n", 323 | "|C.W. OF IND. STATES | 12|\n", 324 | "|NORTHERN AFRICA ...| 6|\n", 325 | "|NORTHERN AMERICA ...| 5|\n", 326 | "|BALTICS ...| 3|\n", 327 | "+--------------------+----------+\n", 328 | "\n", 329 | "== Physical Plan ==\n", 330 | "*(3) Sort [ncountries#375L DESC NULLS LAST], true, 0\n", 331 | "+- Exchange rangepartitioning(ncountries#375L DESC NULLS LAST, 200)\n", 332 | " +- *(2) HashAggregate(keys=[Region#86], functions=[count(1)])\n", 333 | " +- Exchange hashpartitioning(Region#86, 200)\n", 334 | " +- *(1) HashAggregate(keys=[Region#86], functions=[partial_count(1)])\n", 335 | " +- *(1) FileScan csv [Region#86] Batched: false, Format: CSV, Location: InMemoryFileIndex[file:/mnt/c/Users/mk/projects/SkillFactory/module5_spark/data/countries of the ..., PartitionFilters: [], PushedFilters: [], ReadSchema: struct\n", 336 | "== Physical Plan ==\n", 337 | "*(3) Sort [ncountries#346L DESC NULLS LAST], true, 0\n", 338 | "+- Exchange rangepartitioning(ncountries#346L DESC NULLS LAST, 200)\n", 339 | " +- *(2) HashAggregate(keys=[region#86], functions=[count(1)])\n", 340 | " +- Exchange hashpartitioning(region#86, 200)\n", 341 | " +- *(1) HashAggregate(keys=[region#86], functions=[partial_count(1)])\n", 342 | " +- *(1) FileScan csv [Region#86] Batched: false, Format: CSV, Location: InMemoryFileIndex[file:/mnt/c/Users/mk/projects/SkillFactory/module5_spark/data/countries of the ..., PartitionFilters: [], PushedFilters: [], ReadSchema: struct\n" 343 | ] 344 | } 345 | ], 346 | "source": [ 347 | "sqlQ = spark.sql(\"select region,count(*) as ncountries from countries group by region order by ncountries desc\")\n", 348 | "dfQ = cdf.groupBy('Region').count().withColumnRenamed(\"count\",\"ncountries\").sort(f.desc('ncountries'))\n", 349 | "dfQ.show()\n", 350 | "sqlQ.show()\n", 351 | "dfQ.explain()\n", 352 | "sqlQ.explain()" 353 | ] 354 | }, 355 | { 356 | "cell_type": "code", 357 | "execution_count": 22, 358 | "metadata": {}, 359 | "outputs": [], 360 | "source": [ 361 | "spark.stop()" 362 | ] 363 | } 364 | ], 365 | "metadata": { 366 | "kernelspec": { 367 | "display_name": "Python 3", 368 | "language": "python", 369 | "name": "python3" 370 | }, 371 | "language_info": { 372 | "codemirror_mode": { 373 | "name": "ipython", 374 | "version": 3 375 | }, 376 | "file_extension": ".py", 377 | "mimetype": "text/x-python", 378 | "name": "python", 379 | "nbconvert_exporter": "python", 380 | "pygments_lexer": "ipython3", 381 | "version": "3.5.2" 382 | } 383 | }, 384 | "nbformat": 4, 385 | "nbformat_minor": 2 386 | } 387 | -------------------------------------------------------------------------------- /DE_course/Spark/08_rdd.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# RDD: Resilient Distributed Dataset" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "### Что такое RDD\n", 15 | "\n", 16 | "* набор объектов, разбитых на разделы (`partitions`)\n", 17 | "* часть Low Level API\n", 18 | "* dataframe \"компилируются\" в RDD\n", 19 | "* менее функционально полная абстракция spark" 20 | ] 21 | }, 22 | { 23 | "cell_type": "markdown", 24 | "metadata": {}, 25 | "source": [ 26 | "### Зачем нам нужны \n", 27 | "\n", 28 | "* для понимания концепции (как SQL)\n", 29 | "* работы с legacy кодом\n", 30 | "* если нужна функциональность, которой нет в Structured API (например, управление размещением данных по разделам)\n", 31 | "* фокус все равно на Structured API" 32 | ] 33 | }, 34 | { 35 | "cell_type": "markdown", 36 | "metadata": {}, 37 | "source": [ 38 | "### Формальное определение RDD\n", 39 | "\n", 40 | "Процитирую из книги - RDD внутренне характеризуется следующим набором атрибутов:\n", 41 | "\n", 42 | "* списком разделов (`partitions`)\n", 43 | "* фукнцией для вычисления каждого `split`\n", 44 | "* списком зависимостей от других RDD\n", 45 | "* (опционально) функцией `Partitioner` для `key-value RDD`\n", 46 | "* (опционально) списком предпочтений узлов, на которых обрабатывать каждый `split` (например, список расположений блоков файла HDFS)" 47 | ] 48 | }, 49 | { 50 | "cell_type": "markdown", 51 | "metadata": {}, 52 | "source": [ 53 | "### Трансформации и действия\n", 54 | "\n", 55 | "* та же модель, что и с dataframe\n", 56 | "* набор трансформаций меньше (и он - другой)\n", 57 | "* используется lazy evaluation " 58 | ] 59 | }, 60 | { 61 | "cell_type": "markdown", 62 | "metadata": {}, 63 | "source": [ 64 | "### Создание RDD\n", 65 | "\n", 66 | "* `spark.sparkConect` - точка входа в Low Level API\n", 67 | "* `parallelize()` - создание RDD\n", 68 | "* `textFile()` - создание RDD из файлов (каждая строка - элемент RDD)\n", 69 | "* `wholeTextFiles()` - каждый файл - элемент RDD (например, наши сложные XML или JSON)" 70 | ] 71 | }, 72 | { 73 | "cell_type": "markdown", 74 | "metadata": {}, 75 | "source": [ 76 | "### Основные трансформации\n", 77 | "\n", 78 | "* `distinct()` - оставить только уникальные элементы RDD\n", 79 | "* `filter()` - отфильтровать элементы с помощью заданной функции\n", 80 | "* `map()` - преобразовать элементы с помощью заданной функции\n", 81 | "* `flatMap()` - преобразовать с добавлением элементов RDD\n", 82 | "* `sort()` - упорядочить элементы RDD" 83 | ] 84 | }, 85 | { 86 | "cell_type": "markdown", 87 | "metadata": {}, 88 | "source": [ 89 | "### Основные действия\n", 90 | "\n", 91 | "* `reduce()` - сворачивает элементы RDD, используя коммутативную и ассоциативную функцию (с двумя параметрами)\n", 92 | "* `count()` - посчитать количество элементов в RDD\n", 93 | "* `first()` - собрать первые N элементов RDD на драйвер\n", 94 | "* `min()/max()` - найти максимум и минимум \n", 95 | "* `collect()/take()` - собрать/собрать N элементов RDD на драйвер\n", 96 | "* `saveAsTextFile()` - сохранить RDD в виде текстового файла" 97 | ] 98 | }, 99 | { 100 | "cell_type": "markdown", 101 | "metadata": {}, 102 | "source": [ 103 | "### python и RDD\n", 104 | "\n", 105 | "При использовании python-а и RDD происходит потеря производительности (в отличие от RDD + java или использования Structured API): RDD - это как использование python UDF для каждого элемента RDD\n", 106 | "\n", 107 | "* сначала данные сериализуются в python структуры данных\n", 108 | "* обрабатываем их на python-е\n", 109 | "* сериализуем данные в формат JVM" 110 | ] 111 | }, 112 | { 113 | "cell_type": "markdown", 114 | "metadata": {}, 115 | "source": [ 116 | "### Практикуемся" 117 | ] 118 | }, 119 | { 120 | "cell_type": "code", 121 | "execution_count": 51, 122 | "metadata": {}, 123 | "outputs": [], 124 | "source": [ 125 | "import os\n", 126 | "from pyspark.sql import SparkSession\n", 127 | "\n", 128 | "os.environ[\"SPARK_HOME\"] = \"/home/mk/mk_win/projects/SparkEdu/lib/python3.5/site-packages/pyspark\"\n", 129 | "os.environ[\"PYSPARK_PYTHON\"] = \"/usr/bin/python3\"\n", 130 | "os.environ[\"PYSPARK_DRIVER_PYTHON\"] = \"python3\"\n", 131 | "os.environ[\"PYSPARK_SUBMIT_ARGS\"] = \"pyspark-shell\"" 132 | ] 133 | }, 134 | { 135 | "cell_type": "code", 136 | "execution_count": 52, 137 | "metadata": {}, 138 | "outputs": [], 139 | "source": [ 140 | "spark = SparkSession.builder.master(\"local\").appName(\"spark_app\").getOrCreate()" 141 | ] 142 | }, 143 | { 144 | "cell_type": "markdown", 145 | "metadata": {}, 146 | "source": [ 147 | "**Создание RDD**\n", 148 | "\n", 149 | "Давайте создадим простейший RDD - слова файла (как строки)" 150 | ] 151 | }, 152 | { 153 | "cell_type": "code", 154 | "execution_count": 53, 155 | "metadata": {}, 156 | "outputs": [], 157 | "source": [ 158 | "wrds = spark.sparkContext.parallelize(\"this is spark and it is great\".split(),3)" 159 | ] 160 | }, 161 | { 162 | "cell_type": "markdown", 163 | "metadata": {}, 164 | "source": [ 165 | "**Из файла**\n", 166 | "\n", 167 | "Прочитаем файл и создадим RDD со строками файла" 168 | ] 169 | }, 170 | { 171 | "cell_type": "code", 172 | "execution_count": 54, 173 | "metadata": {}, 174 | "outputs": [ 175 | { 176 | "data": { 177 | "text/plain": [ 178 | "'Country,Region,Population,Area (sq. mi.),Pop. Density (per sq. mi.),Coastline (coast/area ratio),Net migration,Infant mortality (per 1000 births),GDP ($ per capita),Literacy (%),Phones (per 1000),Arable (%),Crops (%),Other (%),Climate,Birthrate,Deathrate,Agriculture,Industry,Service'" 179 | ] 180 | }, 181 | "execution_count": 54, 182 | "metadata": {}, 183 | "output_type": "execute_result" 184 | } 185 | ], 186 | "source": [ 187 | "lines = spark.sparkContext.textFile(\"data/countries of the world.csv\")\n", 188 | "lines.first()" 189 | ] 190 | }, 191 | { 192 | "cell_type": "markdown", 193 | "metadata": {}, 194 | "source": [ 195 | "**Трансформации**\n", 196 | "\n", 197 | "Начнем с фильтрации, давайте сделаем что-то со словами, ниже - разберем, что получилось и почему RDD не эффективны" 198 | ] 199 | }, 200 | { 201 | "cell_type": "code", 202 | "execution_count": 55, 203 | "metadata": {}, 204 | "outputs": [ 205 | { 206 | "data": { 207 | "text/plain": [ 208 | "['spark']" 209 | ] 210 | }, 211 | "execution_count": 55, 212 | "metadata": {}, 213 | "output_type": "execute_result" 214 | } 215 | ], 216 | "source": [ 217 | "def startsWithS(individual):\n", 218 | " return individual.startswith(\"s\")\n", 219 | "\n", 220 | "wrds.filter(lambda word: startsWithS(word)).collect()" 221 | ] 222 | }, 223 | { 224 | "cell_type": "markdown", 225 | "metadata": {}, 226 | "source": [ 227 | "Обсуждение сделанного.\n", 228 | "\n", 229 | "Что произошло:\n", 230 | "\n", 231 | "* мы написали функцию на python (startsWithS)\n", 232 | "* она была spark-ом как-то переведена на java (executor-ы \"понимают\" только java)\n", 233 | "* на каждом узле кластера (где \"живут\" разделы нашего RDD) для каждого элемента раздела RDD была вызвана эта функция\n", 234 | "* на вход был подан \"переведенный\" в python строку (десериализация) элемент раздела RDD с этого узла\n", 235 | "* функция отработала, вернула True или False\n", 236 | "* в раздел нового RDD попала часть элементов старого (возможно, произошла еще одна сериализация)\n", 237 | "* все разделы собираются на драйвер (`collect`) и происходит еще одна десериализация (в типы python)\n", 238 | "\n", 239 | "Обратите внимание - результат `collect()` - список строк (чисто python объект), т.е. никакого преобразования в объекты spark не происходит. В функции `startsWithS()` мы работаем со строками. Spark делает за нас все преобразования, но это снижает эффективность, нужно иметь это в виду" 240 | ] 241 | }, 242 | { 243 | "cell_type": "markdown", 244 | "metadata": {}, 245 | "source": [ 246 | "**dictinct()**\n", 247 | "\n", 248 | "У нас есть повторяющиеся слова - удалим их" 249 | ] 250 | }, 251 | { 252 | "cell_type": "code", 253 | "execution_count": 56, 254 | "metadata": {}, 255 | "outputs": [ 256 | { 257 | "data": { 258 | "text/plain": [ 259 | "['it', 'and', 'is', 'spark', 'this', 'great']" 260 | ] 261 | }, 262 | "execution_count": 56, 263 | "metadata": {}, 264 | "output_type": "execute_result" 265 | } 266 | ], 267 | "source": [ 268 | "wrds.distinct().collect()" 269 | ] 270 | }, 271 | { 272 | "cell_type": "markdown", 273 | "metadata": {}, 274 | "source": [ 275 | "**map()**\n", 276 | "\n", 277 | "Преобразуем наши слова в пары - слово, длина" 278 | ] 279 | }, 280 | { 281 | "cell_type": "code", 282 | "execution_count": 57, 283 | "metadata": {}, 284 | "outputs": [ 285 | { 286 | "data": { 287 | "text/plain": [ 288 | "[('this', 4),\n", 289 | " ('is', 2),\n", 290 | " ('spark', 5),\n", 291 | " ('and', 3),\n", 292 | " ('it', 2),\n", 293 | " ('is', 2),\n", 294 | " ('great', 5)]" 295 | ] 296 | }, 297 | "execution_count": 57, 298 | "metadata": {}, 299 | "output_type": "execute_result" 300 | } 301 | ], 302 | "source": [ 303 | "def getLen(individual):\n", 304 | " return (individual,len(individual))\n", 305 | "\n", 306 | "wrds.map(getLen).collect()" 307 | ] 308 | }, 309 | { 310 | "cell_type": "markdown", 311 | "metadata": {}, 312 | "source": [ 313 | "**flatMap()**\n", 314 | "\n", 315 | "Разобъем слова по символам (т.е. увеличим количество разделов - каждый символ = раздел) и соберем на `driver` первые 6 разделов" 316 | ] 317 | }, 318 | { 319 | "cell_type": "code", 320 | "execution_count": 58, 321 | "metadata": {}, 322 | "outputs": [ 323 | { 324 | "data": { 325 | "text/plain": [ 326 | "['t', 'h', 'i', 's', 'i', 's']" 327 | ] 328 | }, 329 | "execution_count": 58, 330 | "metadata": {}, 331 | "output_type": "execute_result" 332 | } 333 | ], 334 | "source": [ 335 | "def toChars(individual):\n", 336 | " return list(individual)\n", 337 | "\n", 338 | "wrds.flatMap(toChars).take(6)" 339 | ] 340 | }, 341 | { 342 | "cell_type": "markdown", 343 | "metadata": {}, 344 | "source": [ 345 | "**sort()**\n", 346 | "\n", 347 | "Давайте упорядочим слова по их длине (по убыванию)" 348 | ] 349 | }, 350 | { 351 | "cell_type": "code", 352 | "execution_count": 59, 353 | "metadata": {}, 354 | "outputs": [ 355 | { 356 | "data": { 357 | "text/plain": [ 358 | "['spark', 'great', 'this', 'and', 'is', 'it', 'is']" 359 | ] 360 | }, 361 | "execution_count": 59, 362 | "metadata": {}, 363 | "output_type": "execute_result" 364 | } 365 | ], 366 | "source": [ 367 | "wrds.sortBy(lambda word: len(word) * -1).collect()" 368 | ] 369 | }, 370 | { 371 | "cell_type": "markdown", 372 | "metadata": {}, 373 | "source": [ 374 | "**Действия**\n", 375 | "\n", 376 | "мы уже видели `collect()/take()`, давайте посмотрим `reduce()`: оставим самое длинное слово из нашего набора" 377 | ] 378 | }, 379 | { 380 | "cell_type": "code", 381 | "execution_count": 60, 382 | "metadata": {}, 383 | "outputs": [ 384 | { 385 | "data": { 386 | "text/plain": [ 387 | "'great'" 388 | ] 389 | }, 390 | "execution_count": 60, 391 | "metadata": {}, 392 | "output_type": "execute_result" 393 | } 394 | ], 395 | "source": [ 396 | "def wordLengthReducer(leftWord, rightWord):\n", 397 | " if len(leftWord) > len(rightWord):\n", 398 | " return leftWord\n", 399 | " else:\n", 400 | " return rightWord\n", 401 | "\n", 402 | "wrds.reduce(wordLengthReducer)" 403 | ] 404 | }, 405 | { 406 | "cell_type": "markdown", 407 | "metadata": {}, 408 | "source": [ 409 | "**count()**" 410 | ] 411 | }, 412 | { 413 | "cell_type": "code", 414 | "execution_count": 61, 415 | "metadata": {}, 416 | "outputs": [ 417 | { 418 | "data": { 419 | "text/plain": [ 420 | "7" 421 | ] 422 | }, 423 | "execution_count": 61, 424 | "metadata": {}, 425 | "output_type": "execute_result" 426 | } 427 | ], 428 | "source": [ 429 | "wrds.count()" 430 | ] 431 | }, 432 | { 433 | "cell_type": "markdown", 434 | "metadata": {}, 435 | "source": [ 436 | "**saveAsTextFile()**\n", 437 | "\n", 438 | "Сохраним наши слова в виде \"файла\" - как обычно для Spark: будет создана директория, в которую будет записан RDD, каждый раздел - отдельный файл. Количество разделов мы тоже выведем, хотя это - 3 (мы именно так создавали RDD - см. выше)." 439 | ] 440 | }, 441 | { 442 | "cell_type": "code", 443 | "execution_count": 62, 444 | "metadata": {}, 445 | "outputs": [ 446 | { 447 | "data": { 448 | "text/plain": [ 449 | "3" 450 | ] 451 | }, 452 | "execution_count": 62, 453 | "metadata": {}, 454 | "output_type": "execute_result" 455 | } 456 | ], 457 | "source": [ 458 | "!rm -rf data/words.txt\n", 459 | "wrds.saveAsTextFile(\"data/words.txt\")\n", 460 | "wrds.getNumPartitions()" 461 | ] 462 | }, 463 | { 464 | "cell_type": "code", 465 | "execution_count": 63, 466 | "metadata": {}, 467 | "outputs": [ 468 | { 469 | "name": "stdout", 470 | "output_type": "stream", 471 | "text": [ 472 | "43.0 as numbers\n", 473 | "5.0 as strings\n" 474 | ] 475 | } 476 | ], 477 | "source": [ 478 | "numbs = spark.sparkContext.parallelize([1.0, 5.0, 43.0, 10.0])\n", 479 | "print(numbs.max(),\" as numbers\")\n", 480 | "print(numbs.max(key=str),\" as strings\")" 481 | ] 482 | }, 483 | { 484 | "cell_type": "markdown", 485 | "metadata": {}, 486 | "source": [ 487 | "### Еще немного практики" 488 | ] 489 | }, 490 | { 491 | "cell_type": "markdown", 492 | "metadata": {}, 493 | "source": [ 494 | "трансформация `glom()`\n", 495 | "\n", 496 | "Собирает все элементы раздела в список." 497 | ] 498 | }, 499 | { 500 | "cell_type": "code", 501 | "execution_count": 64, 502 | "metadata": {}, 503 | "outputs": [ 504 | { 505 | "data": { 506 | "text/plain": [ 507 | "[['this', 'is'], ['spark', 'and'], ['it', 'is', 'great']]" 508 | ] 509 | }, 510 | "execution_count": 64, 511 | "metadata": {}, 512 | "output_type": "execute_result" 513 | } 514 | ], 515 | "source": [ 516 | "wrds.glom().collect()" 517 | ] 518 | }, 519 | { 520 | "cell_type": "markdown", 521 | "metadata": {}, 522 | "source": [ 523 | "**Кэширование**\n", 524 | "\n", 525 | "Как и с датафреймами - не сможем это эффективно использовать, но упомянуть стоит.\n", 526 | "\n", 527 | "`cache()` - кэширует RDD (после выполнения любого следующего действия), после этого RDD уже не будет каждый раз исполняться, значения будут браться из кэша. \n", 528 | "\n", 529 | "После выполнения этого узла можно будет увидеть статус RDD здесь - localhost:4040/storage/\n" 530 | ] 531 | }, 532 | { 533 | "cell_type": "code", 534 | "execution_count": 65, 535 | "metadata": {}, 536 | "outputs": [ 537 | { 538 | "data": { 539 | "text/plain": [ 540 | "7" 541 | ] 542 | }, 543 | "execution_count": 65, 544 | "metadata": {}, 545 | "output_type": "execute_result" 546 | } 547 | ], 548 | "source": [ 549 | "wrds.cache()\n", 550 | "wrds.count()" 551 | ] 552 | }, 553 | { 554 | "cell_type": "markdown", 555 | "metadata": {}, 556 | "source": [ 557 | "Обратная операция - `unpersist()`\n", 558 | "\n", 559 | "После ее выполнения кэш пропадет (см. localhost:4040/storage/)" 560 | ] 561 | }, 562 | { 563 | "cell_type": "code", 564 | "execution_count": 66, 565 | "metadata": {}, 566 | "outputs": [ 567 | { 568 | "data": { 569 | "text/plain": [ 570 | "ParallelCollectionRDD[0] at parallelize at PythonRDD.scala:195" 571 | ] 572 | }, 573 | "execution_count": 66, 574 | "metadata": {}, 575 | "output_type": "execute_result" 576 | } 577 | ], 578 | "source": [ 579 | "wrds.unpersist()" 580 | ] 581 | }, 582 | { 583 | "cell_type": "markdown", 584 | "metadata": {}, 585 | "source": [ 586 | "**Использование RDD для небольших XML файлов**\n", 587 | "\n", 588 | "Как один из вариантов практического использования RDD - работа с мелкими XML файлами (помните, мы разбирали проблематику в модуле про работу с источниками?).\n", 589 | "\n", 590 | "Можно считать XML (в преобразованном, конечно, виде) в RDD, а дальше с помощью `map()` \"вытащить\" нужные поля в \"плоскую\" часть, оставив весь исходный XML в виде одного из полей. Потом эту \"регулярную\" структуру сохранить в реляционной таблице, например.\n", 591 | "\n", 592 | "Это будет не очень эффективно - см выше - но для обработки относительно небольшого потока XML может и хватить. \n", 593 | "\n", 594 | "Как мысль." 595 | ] 596 | }, 597 | { 598 | "cell_type": "code", 599 | "execution_count": 67, 600 | "metadata": {}, 601 | "outputs": [ 602 | { 603 | "data": { 604 | "text/plain": [ 605 | "[(1, 'pete', {'details': {'name': 'pete', 'phone': 123}, 'id': 1}),\n", 606 | " (2, 'mike', {'details': {'name': 'mike', 'phone': 999}, 'id': 2})]" 607 | ] 608 | }, 609 | "execution_count": 67, 610 | "metadata": {}, 611 | "output_type": "execute_result" 612 | } 613 | ], 614 | "source": [ 615 | "dList = [ \n", 616 | " { \"id\": 1, \"details\": { \"name\": \"pete\", \"phone\": 123 } },\n", 617 | " { \"id\": 2, \"details\": { \"name\": \"mike\", \"phone\": 999 } },\n", 618 | "]\n", 619 | "dictRdd = spark.sparkContext.parallelize(dList)\n", 620 | "\n", 621 | "def mkRecord(el):\n", 622 | " return ( el[\"id\"], el[\"details\"][\"name\"], el)\n", 623 | "\n", 624 | "dictRdd.map(mkRecord).collect()" 625 | ] 626 | }, 627 | { 628 | "cell_type": "markdown", 629 | "metadata": {}, 630 | "source": [ 631 | "**Пример вычисления числа Пи**\n", 632 | "\n", 633 | "Давайте, наконец, разберем - что же происходило в том примере, который мы использовали для вычисления числа Пи (в начале - когда тестировали работоспособность Spark), код приведен ниже.\n", 634 | "\n", 635 | "Теперь мы все знаем и можем объяснить - собственно, один из \"смыслов\" изучения RDD:\n", 636 | "\n", 637 | "* создается простейщий RDD (один раздел, список чисел нужного диапазона)\n", 638 | "* функция `filter()` позволяет нам выполнить код на питоне (вообще говоря - любой), в этом примере параметр (элемент RDD, для которого происходит вызов) не используется\n", 639 | "* функция `inside()` написана на python, использует его инструментарий\n", 640 | "* возвращает true или false, в зависимости от того, попали ли случайные числа в круг диаметром 2 или нет\n", 641 | "* фильтруя мы оставляем в RDD только \"попавшие\" в круг его элементы\n", 642 | "* `count()` посчитает число попавших в круг элементов\n", 643 | "* далее - простая математика (которая геометрия)\n", 644 | "\n", 645 | "Все просто! (\"как хорошо уметь читать...\")" 646 | ] 647 | }, 648 | { 649 | "cell_type": "code", 650 | "execution_count": 68, 651 | "metadata": {}, 652 | "outputs": [ 653 | { 654 | "name": "stdout", 655 | "output_type": "stream", 656 | "text": [ 657 | "3.1388\n" 658 | ] 659 | } 660 | ], 661 | "source": [ 662 | "import random\n", 663 | "num_samples = 10000\n", 664 | "\n", 665 | "def inside(p): \n", 666 | " x, y = random.random(), random.random()\n", 667 | " return x*x + y*y < 1\n", 668 | "\n", 669 | "count = spark.sparkContext.parallelize(range(0, num_samples)).filter(inside).count()\n", 670 | "\n", 671 | "pi = 4 * count / num_samples\n", 672 | "print(pi)" 673 | ] 674 | }, 675 | { 676 | "cell_type": "code", 677 | "execution_count": 69, 678 | "metadata": {}, 679 | "outputs": [], 680 | "source": [ 681 | "spark.stop()" 682 | ] 683 | } 684 | ], 685 | "metadata": { 686 | "kernelspec": { 687 | "display_name": "Python 3", 688 | "language": "python", 689 | "name": "python3" 690 | }, 691 | "language_info": { 692 | "codemirror_mode": { 693 | "name": "ipython", 694 | "version": 3 695 | }, 696 | "file_extension": ".py", 697 | "mimetype": "text/x-python", 698 | "name": "python", 699 | "nbconvert_exporter": "python", 700 | "pygments_lexer": "ipython3", 701 | "version": "3.5.2" 702 | } 703 | }, 704 | "nbformat": 4, 705 | "nbformat_minor": 2 706 | } 707 | -------------------------------------------------------------------------------- /DE_course/Spark/09_stream.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Введение в Spark Structured Streaming " 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "### Streaming и batch обработки\n", 15 | "\n", 16 | "* `batch` - обработка всех данных\n", 17 | "* `streaming` - обработка постоянно поступающих данных\n", 18 | "* постоянно поступать могут как структурированные, так и не структурированные данные\n", 19 | "* желательно обрабатывать единообразно, минимальным количеством инструментов" 20 | ] 21 | }, 22 | { 23 | "cell_type": "markdown", 24 | "metadata": {}, 25 | "source": [ 26 | "### Как можно обрабатывать потоковые данные\n", 27 | "\n", 28 | "(например, с помощью NiFi)\n", 29 | "\n", 30 | "Настраиваем граф обработки, который\n", 31 | "\n", 32 | "* периодически читает данные (файлы, таблицы)\n", 33 | "* обрабатывает каждый элемент\n", 34 | "* собирает в \"batch\" (где-то временно храня)\n", 35 | "* записывает batch в хранилище (например, Hive)\n", 36 | "\n", 37 | "Все это мы делаем\n", 38 | "\n", 39 | "* другим инструментом\n", 40 | "* на каком-то оборудовании\n", 41 | "* каким-то образом обеспечиваем мониторинг\n", 42 | "* каким-то образом обеспечиваем lineage" 43 | ] 44 | }, 45 | { 46 | "cell_type": "markdown", 47 | "metadata": {}, 48 | "source": [ 49 | "### Что предлагает Spark Structured Streaming\n", 50 | "\n", 51 | "(начнем с конца)\n", 52 | "\n", 53 | "* такую же обстракцию (`dataframe`) для преобразования данных\n", 54 | "* такой же механизм получения потоковых данных (например, чтение из файлов)\n", 55 | "* новый механизм действия (`action`): трансформация запускается при обновлении данных\n", 56 | "* возможность работы с актуальными данными (batch + streaming) " 57 | ] 58 | }, 59 | { 60 | "cell_type": "markdown", 61 | "metadata": {}, 62 | "source": [ 63 | "### Как это работает\n", 64 | "\n", 65 | "(на примере файлов)\n", 66 | "\n", 67 | "* создаем датафрейм из файла (также, как и в обычном случае, только `readStream`)\n", 68 | "* настраиваем необходимые преобразования\n", 69 | "* настраиваем параметры стриминга\n", 70 | " * куда записывать (файлы или kafka)\n", 71 | " * способ запуска алгоритмов (количество файлов или промежуток времени)\n", 72 | "* запускаем стриминг (`start`)\n" 73 | ] 74 | }, 75 | { 76 | "cell_type": "markdown", 77 | "metadata": {}, 78 | "source": [ 79 | "### Простой пример\n", 80 | "\n", 81 | "Шаг 1. Создаем \"стриминговый\" датафрейм (`readStream`), задаем - как часто выполнять (через каждые 1000 файлов)\n", 82 | "\n", 83 | " streamingDataFrame = spark.readStream\\\n", 84 | " .schema(staticSchema)\\\n", 85 | " .option(\"maxFilesPerTrigger\", 1000)\\\n", 86 | " .format(\"csv\")\\\n", 87 | " .option(\"header\", \"true\")\\\n", 88 | " .load(\"/data/retail-data/by-day/*.csv\")\n", 89 | "\n", 90 | "Шаг 2. Обрабатываем его так, как нам нужно (например, агрегируем)\n", 91 | "\n", 92 | " purchaseByCustomerPerHour = streamingDataFrame\\\n", 93 | " .selectExpr( \"CustomerId\", \"(UnitPrice * Quantity) as total_cost\", \"InvoiceDate\")\\\n", 94 | " .groupBy(col(\"CustomerId\"), window(col(\"InvoiceDate\"), \"1 day\"))\\\n", 95 | " .sum(\"total_cost\")\n", 96 | "\n", 97 | "Шаг 3. Запускаем стриминг (в данном примере будет формироваться таблица в памяти, но это могли бы быть файлы или топики Kafka)\n", 98 | "\n", 99 | " purchaseByCustomerPerHour.writeStream\\\n", 100 | " .format(\"memory\")\\\n", 101 | " .queryName(\"customer_purchases\")\\\n", 102 | " .outputMode(\"complete\")\\\n", 103 | " .start()" 104 | ] 105 | } 106 | ], 107 | "metadata": { 108 | "kernelspec": { 109 | "display_name": "Python 3", 110 | "language": "python", 111 | "name": "python3" 112 | }, 113 | "language_info": { 114 | "codemirror_mode": { 115 | "name": "ipython", 116 | "version": 3 117 | }, 118 | "file_extension": ".py", 119 | "mimetype": "text/x-python", 120 | "name": "python", 121 | "nbconvert_exporter": "python", 122 | "pygments_lexer": "ipython3", 123 | "version": "3.5.2" 124 | } 125 | }, 126 | "nbformat": 4, 127 | "nbformat_minor": 2 128 | } 129 | -------------------------------------------------------------------------------- /DE_course/Spark/README.md: -------------------------------------------------------------------------------- 1 | Jupyter Notebook, которые использовались в процессе обучения 2 | 3 | * 01-09 - ноутбуки с рабочими материалами шага курса 4 | * spqrk_sql_db - ноутбук, по которому был записан скринкаст (работа с базами данных в кластере) 5 | -------------------------------------------------------------------------------- /DE_course/Spark/spark_sql_db.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "# настраиваем окружение для работы spark\n", 10 | "import os\n", 11 | "\n", 12 | "os.environ[\"SPARK_HOME\"] = \"/opt/cloudera/parcels/CDH-6.3.1-1.cdh6.3.1.p0.1470567/lib/spark\"\n", 13 | "os.environ[\"PYSPARK_PYTHON\"] = \"/usr/bin/python3\"\n", 14 | "os.environ[\"PYSPARK_DRIVER_PYTHON\"] = \"python3\"\n", 15 | "os.environ[\"HADOOP_CONF_DIR\"] = \"/etc/hadoop/conf\"\n", 16 | "#os.environ[\"PYSPARK_SUBMIT_ARGS\"] = \"--executor-memory 2G --num-executors 16 --executor-cores 2 pyspark-shell\"\n", 17 | "os.environ[\"PYSPARK_SUBMIT_ARGS\"] = \"\"\"--driver-class-path /usr/share/java/postgresql.jar \n", 18 | "--jars /usr/share/java/postgresql.jar --executor-memory 600M --driver-memory 1G --num-executors 1 pyspark-shell\"\"\"\n" 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": 2, 24 | "metadata": {}, 25 | "outputs": [], 26 | "source": [ 27 | "# добавляем модуль в путь\n", 28 | "import sys\n", 29 | "sys.path.append('/opt/cloudera/parcels/CDH-6.3.1-1.cdh6.3.1.p0.1470567/lib/spark/python')" 30 | ] 31 | }, 32 | { 33 | "cell_type": "code", 34 | "execution_count": 3, 35 | "metadata": {}, 36 | "outputs": [], 37 | "source": [ 38 | "from pyspark.sql import SparkSession\n", 39 | "\n", 40 | "#master = \"local\"\n", 41 | "master = \"yarn\"\n", 42 | "spark = SparkSession.builder.master(master).appName(\"spark_test\").enableHiveSupport().getOrCreate()" 43 | ] 44 | }, 45 | { 46 | "cell_type": "code", 47 | "execution_count": 4, 48 | "metadata": {}, 49 | "outputs": [], 50 | "source": [ 51 | "df = spark.read.format(\"jdbc\") \\\n", 52 | " .option(\"url\",\"jdbc:postgresql://localhost:7432/scm\") \\\n", 53 | " .option(\"dbtable\",\"hosts\") \\\n", 54 | " .option(\"user\",\"cloudera-scm\") \\\n", 55 | " .option(\"password\",\"2MhalIGcSp\") \\\n", 56 | " .load()" 57 | ] 58 | }, 59 | { 60 | "cell_type": "code", 61 | "execution_count": 5, 62 | "metadata": {}, 63 | "outputs": [ 64 | { 65 | "name": "stdout", 66 | "output_type": "stream", 67 | "text": [ 68 | "+-------+-----------------------+--------------------+---------+----------+--------+------+-------------------+-----------------+------------------+----------+---------+--------------------+-----------+-----------------+--------------+\n", 69 | "|host_id|optimistic_lock_version| host_identifier| name|ip_address| rack_id|status|config_container_id|maintenance_count|decommission_count|cluster_id|num_cores|total_phys_mem_bytes|public_name|public_ip_address|cloud_provider|\n", 70 | "+-------+-----------------------+--------------------+---------+----------+--------+------+-------------------+-----------------+------------------+----------+---------+--------------------+-----------+-----------------+--------------+\n", 71 | "| 1| 34|a47cfc74-1775-496...|localhost| 127.0.0.1|/default| NA| 1| 0| 0| 1| 2| 10514747392| null| null| null|\n", 72 | "+-------+-----------------------+--------------------+---------+----------+--------+------+-------------------+-----------------+------------------+----------+---------+--------------------+-----------+-----------------+--------------+\n", 73 | "\n" 74 | ] 75 | } 76 | ], 77 | "source": [ 78 | "df.show()" 79 | ] 80 | }, 81 | { 82 | "cell_type": "code", 83 | "execution_count": 6, 84 | "metadata": {}, 85 | "outputs": [], 86 | "source": [ 87 | "df.write.format(\"parquet\") \\\n", 88 | " .mode(\"append\") \\\n", 89 | " .saveAsTable(\"sp_hosts\")" 90 | ] 91 | }, 92 | { 93 | "cell_type": "code", 94 | "execution_count": 7, 95 | "metadata": {}, 96 | "outputs": [], 97 | "source": [ 98 | "spark.stop()" 99 | ] 100 | } 101 | ], 102 | "metadata": { 103 | "kernelspec": { 104 | "display_name": "Python 3", 105 | "language": "python", 106 | "name": "python3" 107 | }, 108 | "language_info": { 109 | "codemirror_mode": { 110 | "name": "ipython", 111 | "version": 3 112 | }, 113 | "file_extension": ".py", 114 | "mimetype": "text/x-python", 115 | "name": "python", 116 | "nbconvert_exporter": "python", 117 | "pygments_lexer": "ipython3", 118 | "version": "3.5.2" 119 | } 120 | }, 121 | "nbformat": 4, 122 | "nbformat_minor": 2 123 | } 124 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # В помощь дата инженеру 2 | 3 | В этом репозитории находятся материалы (исходный код), которые могут "скрасить" жизнь дата инженера. 4 | 5 | Структура 6 | 7 | * airflow: полезные материалы для Apache Airflow (пока маловато, в процессе) 8 | * data_doc: документирование данных и связанный код 9 | * fileutils: код, помогающий в работе с файлами (данных) 10 | * livy_tools: примеры использования Apache Livy совместно с Apache Airflow 11 | * orc: материалы по формату ORC 12 | -------------------------------------------------------------------------------- /airflow/README.txt: -------------------------------------------------------------------------------- 1 | Здесь собраны полезные материалы по Apache Airflow 2 | 3 | * auto_start.md: описание того, как реализовать автоматический запуск Airflow при старте линукса (через systemd) 4 | -------------------------------------------------------------------------------- /airflow/auto_start.md: -------------------------------------------------------------------------------- 1 | # Кратко 2 | 3 | Записки о том, что и как настраивалось для автоматического запуска нужных сервисов при перезагрузке 4 | 5 | ## Источники информации 6 | 7 | Кратко это описано в документации 8 | 9 | * systemd: https://airflow.readthedocs.io/en/stable/howto/run-with-systemd.html 10 | * (warning) не работает с Cloudera (warning) upstart: https://airflow.readthedocs.io/en/stable/howto/run-with-upstart.html 11 | * скрипты лежат в гитхабе (https://github.com/apache/airflow/tree/master/scripts/systemd) 12 | 13 | ## Systemd 14 | 15 | Вот что получается: 16 | 17 | * есть место, где лежат описания юнитов (в нашем случае - сервисов: airflow-scheduler и airflow-webserver). Это - /usr/lib/systemd/system (если такой папки нет - создаем) 18 | * есть место, где лежит конфигурация (один файл), в нашем случае пусть это будет /etc/airflow/airflow. Этот пусть прописан в сервисах (которые выше) 19 | * есть место, где лежит информация о том, с какими полномочиями должен создаваться run файл, в нашем случае пусть это будет /usr/lib/tmpfiles.d/airflow.conf (такая директория обычно уже есть) 20 | * нужно определить пользователя, от имени которого будут работать сервисы Airflow (в нашем примере - mluser) 21 | 22 | Что делаем 23 | 24 | * правим описания сервисов и копируем в /usr/lib/systemd/system (конкретный работающий на Ubuntu 16.04 пример приведен ниже) 25 | * правим конфигурацию и копируем в /etc/airflow (пример - ниже) 26 | * правим файл с полномочиями и копируем в /usr/lib/tmpfiles.d 27 | * говорим sudo systemctl enable airflow-scheduler (и то же для вебсервера) 28 | * создать директорию /run/airflow, сделать хозяином mluser 29 | 30 | Для перезапуска выполняем 31 | 32 | sudo systemctl restart # например airflow-scheduler 33 | 34 | ## Upstart 35 | 36 | Пробовал настраивать для upstart - у меня поломало Cloudera (не стартовал datanode, вообще стало все плохо...) Нигде не нашел, что upstart противоречит clouder-e. Может быть, это было потому, что upstart-a не было на момент установки cloudera. 37 | 38 | Поэтому эту веточку здесь не развиваю. 39 | 40 | ## Примеры файлов 41 | 42 | В примерах предполагается, что Airflow использует mySQL. 43 | 44 | ### /usr/lib/systemd/system/airflow-scheduler.service 45 | 46 | [Unit] 47 | Description=Airflow scheduler daemon 48 | After=network.target mysql.service 49 | Wants=mysql.service 50 | 51 | [Service] 52 | EnvironmentFile=/etc/airflow/airflow 53 | User=mluser 54 | Group=mluser 55 | Type=simple 56 | ExecStart=/usr/local/bin/airflow scheduler 57 | Restart=always 58 | RestartSec=5s 59 | 60 | [Install] 61 | WantedBy=multi-user.target 62 | 63 | ### /usr/lib/systemd/system/airflow-webserver.service 64 | 65 | [Unit] 66 | Description=Airflow webserver daemon 67 | After=network.target mysql.service 68 | Wants=mysql.service 69 | 70 | [Service] 71 | EnvironmentFile=/etc/airflow/airflow 72 | User=mluser 73 | Group=mluser 74 | Type=simple 75 | ExecStart=/usr/local/bin/airflow webserver --pid /run/airflow/webserver.pid 76 | Restart=on-failure 77 | RestartSec=5s 78 | PrivateTmp=true 79 | 80 | [Install] 81 | WantedBy=multi-user.target 82 | 83 | ### /etc/airflow/airflow 84 | 85 | # This file is the environment file for Airflow. Put this file in /etc/sysconfig/airflow per default 86 | # configuration of the systemd unit files. 87 | # 88 | AIRFLOW_CONFIG=/home/mluser/airflow/airflow.cfg 89 | AIRFLOW_HOME=/home/mluser/airflow 90 | 91 | ### /usr/lib/tmpfiles.d/airflow.conf 92 | 93 | d /run/airflow 0755 mluser mluser 94 | 95 | -------------------------------------------------------------------------------- /data_doc/README.md: -------------------------------------------------------------------------------- 1 | Здесь собраны материалы, связанные с документированием данных 2 | 3 | * habr_spark_lazy.html: копия jupyter книжки, в которой сравниваются атомарные и многостраничные SQL запросы (смотреть ее лучше так - http://htmlpreview.github.io/?https://github.com/korolmi/dataeng/blob/master/data_doc/habr_spark_lazy.html - только нужно немного подождать...) 4 | 5 | -------------------------------------------------------------------------------- /fileutils/README.md: -------------------------------------------------------------------------------- 1 | # Работа с файлами данных 2 | 3 | В этой директории собраны фрагменты кода, облегчающие жизнь дата инженера при работе с файлами 4 | 5 | * et2dict.py: фрагменты кода, конвертирующие ElementTree в словарь, а также позволяющие работать с элементами словаря в "точечной" нотации 6 | * fieldnamer.py: набор кода (слабо оформленного), "переводящие" русские заголовки таблицы на английский язык (код в формате light, получен с помощью jupytext из jupyter notebook) 7 | -------------------------------------------------------------------------------- /fileutils/et2dict.py: -------------------------------------------------------------------------------- 1 | def hasAttribs(t): 2 | """ проверяет наличие атрибутов в узле и 3 | исключает вырожденные атрибуты типа {http://www.w3.org/2001/XMLSchema-instance}nil """ 4 | 5 | if not t.attrib: 6 | return False 7 | 8 | for a in t.attrib: 9 | if a.find('{http://www.w3.org/2001/XMLSchema-instance}nil')>=0: 10 | return False 11 | 12 | return True 13 | 14 | def etree_to_dict(t): 15 | """ конвертилка распарсенного XML в словарь """ 16 | 17 | # удалим наймспейс из тэга 18 | if t.tag.find('{')>=0: 19 | t.tag = t.tag.split('}')[1] 20 | 21 | # учтем здесь {http://www.w3.org/2001/XMLSchema-instance}nil 22 | d = {t.tag: {} if hasAttribs(t) else None} 23 | children = list(t) 24 | if children: 25 | dd = defaultdict(list) 26 | for dc in map(etree_to_dict, children): 27 | for k, v in dc.items(): 28 | dd[k].append(v) 29 | d = {t.tag: {k:v[0] if len(v) == 1 else v for k, v in dd.items()}} 30 | if hasAttribs(t): 31 | d[t.tag].update(('@' + k, v) for k, v in t.attrib.items()) 32 | if t.text: 33 | text = t.text.strip() 34 | if children or t.attrib: 35 | if text: 36 | d[t.tag]['#text'] = text 37 | else: 38 | d[t.tag] = text 39 | return d 40 | 41 | def getByDot(d,ss): 42 | """ выбирает из словаря d элемент по строке ss в точечной нотации """ 43 | 44 | for e in ss.split("."): 45 | if not isinstance(d, (dict,)): 46 | return None 47 | if e in d.keys(): 48 | d = d[e] 49 | else: 50 | return None 51 | 52 | return d 53 | -------------------------------------------------------------------------------- /fileutils/fieldnamer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # --- 3 | # jupyter: 4 | # jupytext: 5 | # formats: ipynb,py:light 6 | # text_representation: 7 | # extension: .py 8 | # format_name: light 9 | # format_version: '1.4' 10 | # jupytext_version: 1.2.4 11 | # kernelspec: 12 | # display_name: Python 3 13 | # language: python 14 | # name: python3 15 | # --- 16 | 17 | # # Именователь полей в таблице 18 | 19 | # Книжка помогает решить стандартную проблему - есть файл с колонками, которые названы по-русски. В базе данных мы хотим их видеть по-английски. 20 | # 21 | # Делать это каждый раз творчески надоело, попытка реализовать канонический путь. 22 | # 23 | # Как работает (идеологически) - идем от конца к началу (то есть выполняться будет в обратном порядке): 24 | # 25 | # * английские слова в имени конкатенируются через "_", если слово содержит "*", оно ставится в конец 26 | # * каждое слово имени поля переводится на английский (или транслитерируется) 27 | # * из имени поля выкидываются одиночные символы 28 | # * из имени поля выкидываются исключения ("лишние слова") 29 | # * слова в имени поля приводятся к нормальной форме (в смысле pymorphe2, из приведения исключаются слова "аббревиатуры") 30 | # * слова в имени поля переводится в нижний регистр 31 | # * спец. символы заменяются или удаляются в имени поля 32 | # 33 | # Для перевода используем словарь tranDict: он содержит соответствие нормальной формы русского слова английскому. Если в словаре нет слова, то оно будет транслитерировано. В словарь можно добавлять вручную. 34 | # 35 | # Пока непонятно (нет статистики) - как решать обратную задачу (есть поле в базе - как это поле называлось в оригинале), решим, когда будет более понятна потребность. 36 | 37 | # здесь список колонок для инкрементов по регистрации типа _РЕГИСТРАЦИЯ_01_2019_ИНН.xlsx 38 | rusHdr = """Федеральный округ,Экономический район,Регион,Марка,Модель+модификация,Модель,Модификация, 39 | Год выпуска,Год регистрации,Квартал регистрации,Месяц регистрации,День регистрации,Количество, 40 | Новые/подержанные,Vin код,Номер двигателя,Номер кузова,Номер шасси,Тип регистрации,Тип владения, 41 | Округ (для Москвы),Район в области,Район в городе,Город,Населенный пункт,ОКАТО,OKTMO, 42 | Региональный центр/область,Индекс (почтовый),Тип двигателя,Тип топлива,Объем двигателя, 43 | Объем двигателя в диапазоне,Мощность двигателя (л/с),Тип руля,Тип кузова,Точная масса, 44 | Снаряженная масса,Масса в сегменте 1,Масса в сегменте 2,Страна происхождения марки, 45 | Часть света,Отечественные/зарубежные,Мировой производитель,Ценовой сегмент, 46 | Страна производства (Vin код),Отечественные/зарубежные (Vin код),Производитель (Vin код), 47 | ИНН,Название компании (налоговая инспекция),Первый код ОКВЭД (налоговая инспекция), 48 | Расшифровка кода ОКВЭД (налоговая инспекция),ID,Класс для автомобиля""" 49 | 50 | # это был список колонок для регистрации на 01.01.2019 51 | rusHdr = """Тип ТС,Федеральный Округ,Регион,Областной район,Город,Прочие населенные пункты, 52 | ОКАТО,OKTMO,ОКАТО региона,ОКТМО региона,Региональный центр/область,Марка,Модель + Модификация,Модель,Год выпуска, 53 | Дата регистрации,Год регистрации,Месяц регистрации,Количество,VIN код,Номер двигателя,Номер кузова,Номер шасси, 54 | Тип регистрации,Тип владения,Точная масса,Снаряженная масса,Масса в сегменте 1,Масса в сегменте 2, 55 | Тип двигателя,Объем двигателя в диапазоне,Мощность двигателя (л/с),Тип топлива,Тип руля,Тип кузова, 56 | Ценовой сегмент,Класс для автомобиля,Страна происхождения марки,Отечественные/зарубежные, 57 | Производитель (по VIN коду),Страна производства (по VIN коду),Отечественные/зарубежные (по Vin коду), 58 | ИНН компании,Название компании,Первый код ОКВЭД,Расшифровка кода ОКВЭД,Группа ОКВЭД, 59 | Холдинг,ИНН Холдинга,Субхолдинг,ИНН Субхолдинга,Причина связи,Комментрий к причине связи, 60 | Признак лизингового автомобиля,Лизинг статус по регистрации,ИНН лизингодателя,Лизингодатель наименование, 61 | Холдинг лизингодателя,ИНН Холдинга лизингодателя,Лизингополучатель наименование,Адрес лизингополучателя, 62 | ИНН лизингополучателя,Телефон лизингополучателя,E mail лизингополучателя,Первый код ОКВЭД лизингополучателя, 63 | Расшифровка кода ОКВЭД лизингополучателя,Руководитель лизингополучателя,Должность лизингополучателя,ID""" 64 | 65 | import pymorphy2 66 | morph = pymorphy2.MorphAnalyzer() 67 | 68 | # настройки 69 | SEPR = "," 70 | 71 | # слова аббревиатуры: они исключаются из приведения в нормальную форму 72 | abbrList = [ 73 | "окато", "октмо", "vin", "инн", "оквэд", "id", "mail", "email" 74 | ] 75 | 76 | # лишние слова: эти слова просто выкидываются из имени полей 77 | exclList = [ 78 | "для", "по" 79 | ] 80 | 81 | # получение нормальной формы слова (для вставки в словарь, например) 82 | morph.parse("актуальность")[0].normal_form 83 | 84 | # перевод слов - сюда можно и нужно добавлять, но нужно добавлять НОРМАЛЬНУЮ форму слова 85 | # см. ячейку выше - в ней можно получить нормальную форму слова 86 | # звездочка в конце английского варианта означает, что слово будет поставлено в конец фразы 87 | # если таких слов (со звездочками) много - получится как-то (как работает алгоритм ниже) 88 | tranDict = { 89 | "тип": "type*", 90 | "инн": "inn*", # можно было бы транслитить, но тогда оно будет вначале, а не в конце 91 | "тс": "ts", 92 | "федеральный": "federal", 93 | "регион": "region", 94 | "район": "district", 95 | "город": "city", 96 | "прочий": "other", 97 | "населить": "inhabited", # населенный 98 | "пункт": "location", # потому что это от населенный пункт 99 | "региональный": "regional", 100 | "центр": "center", 101 | "область": "area", 102 | "марка": "made", 103 | "модель": "model", 104 | "модификация": "modification", 105 | "год": "year*", 106 | "выпуск": "made", 107 | "дата": "date*", 108 | "регистрация": "registrtation", 109 | "месяц": "month*", 110 | "количество": "quantity", 111 | "код": "code", 112 | "номер": "number*", 113 | "двигатель": "engine", 114 | "кузов": "body", 115 | "шасси": "chassis", 116 | "владение": "ownership", 117 | "точный": "exact", 118 | "масса": "weight*", 119 | "снарядить": "running_state", # снаряженная 120 | "сегмент": "segment", 121 | "объесть": "volume", # объем 122 | "диапазон": "range", 123 | "мощность": "power*", 124 | "топливо": "fuel", 125 | "руль": "wheel", 126 | "ценовый": "price", 127 | "класс": "class*", 128 | "автомобиль": "vehicle", 129 | "страна": "country", 130 | "происхождение": "origin", 131 | "отечественный": "russian", 132 | "зарубежный": "foreign", 133 | "производитель": "maker", 134 | "производство": "production", 135 | "компания": "company", 136 | "название": "name*", 137 | "наименование": "name*", 138 | "один": "first", # первый 139 | "расшифровка": "description*", 140 | "группа": "group*", 141 | "холдинг": "holding", 142 | "субхолдинг": "subholding", 143 | "причина": "reason", 144 | "связь": "link", 145 | "комментарий": "comment", 146 | "признак": "flag*", 147 | "лизинговый": "leased", 148 | "лизинг": "leasing", 149 | "статус": "status*", 150 | "лизингодатель": "leasing_company", 151 | "лизингополучатель": "leasee", 152 | "адрес": "address", 153 | "телефон": "phone", 154 | "руководитель": "director*", 155 | "должность": "role*", 156 | "новое": "new", 157 | "новый": "new", 158 | "владелец": "owner", 159 | "лицензия": "license", 160 | "разрешение": "permit", 161 | "начало": "start*", 162 | "окончание": "end*", 163 | "срок": "period", 164 | "действие": "validity", 165 | "прекращение": "termination", 166 | "актуальность": "actuality", 167 | "грз": "plate", 168 | "подержать": "used" 169 | } 170 | 171 | 172 | def creEngHdr ( rusHdr, debug=False ): 173 | """ 174 | подготовка слов для перевода 175 | на вход подается строка русского заголовка rusHdr 176 | в результате получаем строку английского заголовка (в том же порядке) 177 | """ 178 | fields = [ f.strip().lower() for f in rusHdr.split(SEPR)] 179 | engFields = [] 180 | for fld in fields: 181 | fldList = [] 182 | endList = [] 183 | for wrd in fld.replace("(","").replace(")","").replace("+"," ").replace("/"," ").split(): 184 | 185 | # исключим односимвольные слова 186 | if len(wrd)==1: continue 187 | 188 | # приводим к нормальной форме (кроме аббревиатур) 189 | normWrd = wrd if wrd in abbrList else morph.parse(wrd)[0].normal_form 190 | 191 | # исключаем лишние слова 192 | if normWrd in exclList: 193 | continue 194 | 195 | # переводим или транслитерируем 196 | if normWrd in tranDict.keys(): 197 | engWord = tranDict[normWrd] 198 | else: 199 | engWord = transliterate(normWrd) 200 | 201 | # убираем слова со звездочкой в конец 202 | if engWord.find("*")>=0: 203 | endList.append(engWord.replace("*","")) 204 | else: 205 | fldList.append(engWord) 206 | 207 | fldList.extend(endList) 208 | newField = "_".join(fldList) 209 | while newField in engFields: 210 | newField += "1" 211 | if debug: print(fld, ":", newField) 212 | engFields.append(newField) 213 | if len(engFields)!=len(fields): 214 | print("ЧТО-ТО ПОШЛО НЕ ТАК - ДЛИНЫ ЗАГОЛОВКОВ ПОЛУЧИЛИСЬ РАЗНЫЕ... {} -> {} английских".format(len(fields),len(engFields))) 215 | engHdr = SEPR.join(engFields) 216 | return engHdr 217 | 218 | 219 | # name: это строка которую транслитим 220 | def transliterate(name): 221 | """ 222 | Автор: LarsKort 223 | Дата: 16/07/2011; 1:05 GMT-4; 224 | Не претендую на "хорошесть" словарика. В моем случае и такой пойдет, 225 | вы всегда сможете добавить свои символы и даже слова. Только 226 | это нужно делать в обоих списках, иначе будет ошибка. 227 | """ 228 | # Слоаврь с заменами 229 | slovar = {'а':'a','б':'b','в':'v','г':'g','д':'d','е':'e','ё':'e', 230 | 'ж':'zh','з':'z','и':'i','й':'i','к':'k','л':'l','м':'m','н':'n', 231 | 'о':'o','п':'p','р':'r','с':'s','т':'t','у':'u','ф':'f','х':'h', 232 | 'ц':'c','ч':'cz','ш':'sh','щ':'scz','ъ':'','ы':'y','ь':'','э':'e', 233 | 'ю':'u','я':'ja', 'А':'a','Б':'b','В':'v','Г':'g','Д':'d','Е':'e','Ё':'e', 234 | 'Ж':'zh','З':'z','И':'i','Й':'i','К':'k','Л':'l','М':'m','Н':'n', 235 | 'О':'o','П':'p','Р':'r','С':'s','Т':'t','У':'u','Ф':'Х','х':'h', 236 | 'Ц':'c','Ч':'cz','Ш':'sh','Щ':'scz','Ъ':'','Ы':'y','Ь':'','Э':'e', 237 | 'Ю':'u','Я':'ja',',':'','?':'',' ':'_','~':'','!':'','@':'','#':'', 238 | '$':'','%':'','^':'','&':'','*':'','(':'',')':'','-':'','=':'','+':'', 239 | ':':'',';':'','<':'','>':'','\'':'','"':'','\\':'','/':'','№':'', 240 | '[':'',']':'','{':'','}':'','ґ':'','ї':'', 'є':'','Ґ':'g','Ї':'i', 241 | 'Є':'e'} 242 | 243 | # Циклически заменяем все буквы в строке 244 | for key in slovar: 245 | name = name.replace(key, slovar[key]) 246 | return name 247 | -------------------------------------------------------------------------------- /livy_tools/README.md: -------------------------------------------------------------------------------- 1 | Здесь примеры использования Apache Livy совместно с Airflow 2 | 3 | * livy_tools.py: набор универсальных методов 4 | * livy_test.py: пример использования этих методов с Airflow 5 | * livy_admin.py: некоторые админ действия, выполняемые периодически в jupyter notebook (jupytext копия) 6 | 7 | В реальной жизни был построен еще один слой - etl_tools - который позволяет декларативно работать со spark-ом, 8 | этот слой пока находится в стадии разработки и сюда не выложен... 9 | 10 | Статья про "смысл происходящего" - https://habr.com/ru/company/alfastrah/blog/466017 11 | 12 | Следите за обновлениями! 13 | -------------------------------------------------------------------------------- /livy_tools/livy_admin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # --- 3 | # jupyter: 4 | # jupytext: 5 | # formats: ipynb,py:light 6 | # text_representation: 7 | # extension: .py 8 | # format_name: light 9 | # format_version: '1.4' 10 | # jupytext_version: 1.2.4 11 | # kernelspec: 12 | # display_name: Python 3 13 | # language: python 14 | # name: python3 15 | # --- 16 | 17 | # # Админ действия по livy 18 | 19 | import json, requests, os 20 | 21 | # **Настройки** 22 | 23 | headers = {'Content-Type': 'application/json'} 24 | host = 'http://z14-1379-dl:8998' 25 | data = { 26 | 'name': 'livy_test', 27 | 'kind': 'pyspark' 28 | } 29 | sessCmd = "/sessions" 30 | stmtCmd = "/statements" 31 | os.environ["http_proxy"] = "" 32 | 33 | # **Базовая информация о сессиях** 34 | 35 | res = requests.get(host+sessCmd, headers=headers) 36 | if res: 37 | for sess in res.json()['sessions']: 38 | print(sess['id'],': name',sess['name'],', state',sess['state']) 39 | if len(res.json()['sessions'])==0: 40 | print("No sessions...") 41 | else: 42 | print("ERROR:") 43 | print(res.text) 44 | 45 | # **Выполняющийся оператор** 46 | # 47 | # Не забыть поменять ID сессии 48 | 49 | res = requests.get(host+sessCmd+"/54"+stmtCmd) 50 | if res: 51 | for stmt in res.json()['statements']: 52 | print(stmt['id'],'>> code:\n',stmt['code']) 53 | print('state:',stmt['state']) 54 | if stmt['state'] in ['available']: 55 | print('OUTPUT:',stmt['output']) 56 | print('----------------') 57 | if len(res.json()['statements'])==0: 58 | print("No statements...") 59 | else: 60 | print("ERROR:") 61 | print(res.text) 62 | 63 | # **Убиение сессии** 64 | # 65 | # Иногда так бывает, что сессия спарка кончается, а сессия livy остается. Если ее не убить, то следующая сессия с тем же именем не запустится... 66 | # 67 | # Не забыть поменять ID сессии 68 | 69 | requests.delete(host+sessCmd+"/50", headers=headers) 70 | -------------------------------------------------------------------------------- /livy_tools/livy_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | Тестовый граф использования спарка через Livy - простая загрузка таблицы 3 | """ 4 | 5 | from airflow.models import DAG 6 | from airflow.models import Variable 7 | #from airflow.models.xcom import XCom # так и не смог найти всех концов - как этим пользоваться... 8 | from airflow import AirflowException 9 | from airflow.operators.python_operator import PythonOperator 10 | 11 | import requests, json, subprocess, datetime, time 12 | 13 | import livy_tools 14 | 15 | # настройки 16 | 17 | DAG_NAME = "livy_test" 18 | SESS_VAR = DAG_NAME + "_sess" 19 | 20 | dag = DAG( dag_id=DAG_NAME, schedule_interval=None, start_date=datetime.datetime(2019, 7, 23) ) 21 | 22 | createSess = PythonOperator( task_id='createSess', python_callable=livy_tools.createSess, op_args = [SESS_VAR], dag=dag) 23 | createSparkSess = PythonOperator( task_id='createSparkSess', python_callable=livy_tools.createSparkSession, op_args = [SESS_VAR], dag=dag) 24 | closeSess = PythonOperator( task_id='closeSess', python_callable=livy_tools.closeSess, op_args = [SESS_VAR], dag=dag) 25 | 26 | loadTab = PythonOperator( task_id='loadTable', python_callable=livy_tools.loadTable, op_args = [SESS_VAR,"address_area"], dag=dag) 27 | storeTab = PythonOperator( task_id='storeTable', python_callable=livy_tools.storeTable, op_args = [SESS_VAR,"addr_ar"], dag=dag) 28 | 29 | # создаем DAG - последовательность операторов, подумаем, как лучше обобщить 30 | createSess >> createSparkSess >> loadTab >> storeTab >> closeSess 31 | -------------------------------------------------------------------------------- /livy_tools/livy_tools.py: -------------------------------------------------------------------------------- 1 | """ 2 | Модуль с основными функциями для работы с данными в стиле 3 | jupyter notebook с использованием spark. 4 | 5 | Функции по максимуму не имеют состояния, для обмена информацией 6 | используем переменные Airflow 7 | """ 8 | 9 | import requests, json, time, textwrap 10 | 11 | from airflow import AirflowException 12 | 13 | headers = {'Content-Type': 'application/json'} 14 | host = 'http://z14-1379-dl:8998' 15 | sessCmd = "/sessions" 16 | stmtCmd = "/statements" 17 | 18 | def getSessId(appName, doRaise=True): 19 | """ возвращает ID сессии по имени приложения в формате /ID или райзит эксепшн (можно отключить) """ 20 | 21 | res = requests.get(host+sessCmd, headers=headers) 22 | if res: 23 | for sess in res.json()['sessions']: 24 | if sess['name']==appName: 25 | return "/" + str(sess['id']) 26 | 27 | # если пришли сюда - нет такого приложения... 28 | if doRaise: 29 | print("**getSessId ERROR**: no such app",appName) 30 | raise AirflowException 31 | else: 32 | return None 33 | 34 | def createSess(name, executorMemory=None, numExecutors=None, executorCores=None): 35 | """ создает сессию с заданными параметрами или райзит ексепшн """ 36 | 37 | args = locals() 38 | 39 | data = { 40 | 'kind': 'pyspark' 41 | } 42 | for k,v in args.items(): 43 | if v is not None: 44 | data[k] = v 45 | 46 | res = requests.post(host+sessCmd, data=json.dumps(data), headers=headers) 47 | if res: 48 | sessDict = res.json() 49 | sessId = "/"+str(sessDict['id']) 50 | while sessDict["state"]!="idle": # ждем до этого состояния... 51 | time.sleep(1) 52 | sessDict = requests.get(host+sessCmd+sessId, headers=headers).json() 53 | else: 54 | print("**createSess ERROR**: status_code",res.status_code, ", text:",res.text) 55 | raise AirflowException 56 | 57 | def execStmt(appName,stmt,doFail=False): 58 | """ выполняет python код в сессии, дожидаясь его завершения 59 | райзит ексепшн, если код дал ошибку и в параметрах не задано игнорирование ошибки 60 | """ 61 | 62 | ssid = getSessId(appName) 63 | 64 | r = requests.post(host+sessCmd+ssid+stmtCmd, data=json.dumps({'code': stmt }), headers=headers) 65 | if r.ok: 66 | stmtDict = r.json() 67 | stmtId = "/"+str(stmtDict['id']) 68 | while stmtDict["state"]!="available": # ждем до этого статуса 69 | time.sleep(1) 70 | r = requests.get(host+sessCmd+ssid+stmtCmd+stmtId, headers=headers) 71 | if r.ok: 72 | stmtDict = r.json() 73 | else: 74 | print("**execStmt request failed**: r ",r) 75 | raise AirflowException 76 | # проверяем результат выполнения - райзим ошибку если не ОК 77 | print("**execStmt finished**: output ",str(stmtDict["output"])) 78 | if stmtDict["output"]["status"]!="ok" and doFail: 79 | raise AirflowException 80 | else: 81 | print("**execStmt creation ERROR**: status_code",r.status_code, ", text:",r.text) 82 | if doFail: 83 | raise AirflowException 84 | 85 | def closeSess(appName): 86 | """ закрывает сессию """ 87 | 88 | ssid = getSessId(appName,False) 89 | if ssid is not None: 90 | requests.delete(host+sessCmd+ssid, headers=headers) # хотим что-то проверить? 91 | 92 | def createSparkSession(appName): 93 | 94 | stmt = textwrap.dedent("""\ 95 | from pyspark.sql import SparkSession 96 | from pyspark.sql.functions import lit 97 | sp = SparkSession.builder.enableHiveSupport().getOrCreate() 98 | """) 99 | 100 | execStmt(appName,stmt) 101 | 102 | def loadTable(appName,tabName, pcol=None, np=None, lb=None, ub=None): 103 | """ если будем пользоваться, то нужно убрать имена в константы или параметры """ 104 | 105 | stmt = textwrap.dedent("""\ 106 | rdr = sp.read \ 107 | .format("jdbc") \ 108 | .option("url", "jdbc:oracle:thin:@p260unc4:1566:uncunc") \ 109 | .option("dbtable", "{}") \ 110 | .option("user", "staff") \ 111 | .option("password", "staff") \ 112 | .option("driver", "oracle.jdbc.driver.OracleDriver") \ 113 | .option("fetchsize", 5000) 114 | """.format(tabName)) 115 | 116 | if pcol is not None: # большая таблица - добавляем опции для параллелизма 117 | stmt += textwrap.dedent("""\ 118 | ldf.option("lowerBound", {}) \ 119 | .option("upperBound", {}) \ 120 | .option("partitionColumn", "{}") \ 121 | .option("numPartitions", {})""".format(lb,ub,pcol,np)) 122 | stmt += "ldf = rdr.load()" 123 | 124 | execStmt(appName,stmt) 125 | 126 | def storeTable(appName,tabName): 127 | """ если будем пользоваться, то нужно убрать параметры сохранения в константы или параметры """ 128 | 129 | stmt = """ldf.write.option("compression","gzip").format("parquet").mode("overwrite").saveAsTable("{}")""".format(tabName) 130 | execStmt(appName,stmt) 131 | -------------------------------------------------------------------------------- /orc/README.md: -------------------------------------------------------------------------------- 1 | В этой директории собраны материалы про формат ORC 2 | 3 | * orc_demo.ipynb: книжка с разбором конкретного файла 4 | * orc_format_notes.md: описание формата (дополняющее книжку) 5 | * my-file.orc: ORC файл, на котором производился разбор 6 | * orc_proto_pb2.py: сгенерированный proto buf (чтобы не перегенирировать) 7 | -------------------------------------------------------------------------------- /orc/my-file.orc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/korolmi/dataeng/41bdfd850345cf2eac3ec885d6a62c0c4948cd33/orc/my-file.orc -------------------------------------------------------------------------------- /orc/orc_demo.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "**Let us read ORC**\n", 8 | "\n", 9 | "Книжка с разбором ORC файла, предыстория и смысл - см блог (последняя ссылка ниже).\n", 10 | "\n", 11 | "Ссылки:\n", 12 | "\n", 13 | "* spec: https://orc.apache.org/specification/ORCv1/\n", 14 | "* protocol buffers: https://developers.google.com/protocol-buffers/docs/pythontutorial\n", 15 | "* orc proto: https://github.com/apache/hive/blob/trunk/ql/src/protobuf/org/apache/hadoop/hive/ql/io/orc/orc_proto.proto\n", 16 | "* apache orc: https://github.com/apache/orc\n", 17 | "* блог про это: http://wiki.alfastrah.ru/pages/viewpage.action?pageId=97983312" 18 | ] 19 | }, 20 | { 21 | "cell_type": "code", 22 | "execution_count": 243, 23 | "metadata": {}, 24 | "outputs": [], 25 | "source": [ 26 | "import zlib # части файла могут быть сжаты..." 27 | ] 28 | }, 29 | { 30 | "cell_type": "code", 31 | "execution_count": 244, 32 | "metadata": {}, 33 | "outputs": [], 34 | "source": [ 35 | "import orc_proto_pb2 # без этого - никуда, все метаданные хранятся в protobuf... пришлось сгенерить по github-у выше" 36 | ] 37 | }, 38 | { 39 | "cell_type": "markdown", 40 | "metadata": {}, 41 | "source": [ 42 | "В качестве примера взят код с сайта проекта (https://orc.apache.org/docs/core-cpp.html), прямо из примера была создана \"табличка\" \n", 43 | "\n", 44 | "`struct`\n", 45 | "\n", 46 | "и заполнена таким нехитрым образом\n", 47 | "\n", 48 | "`\n", 49 | "for (i = 0; i < 10000; ++i) { \n", 50 | " x->data[rows] = i; \n", 51 | " y->data[rows] = i * 3;\n", 52 | "}`\n", 53 | "\n", 54 | "Результат просто (`writer->add(*batch)`) засунули в батчи размером 1024 строк и записали в файл, который дальше будет читать и изучать." 55 | ] 56 | }, 57 | { 58 | "cell_type": "code", 59 | "execution_count": 245, 60 | "metadata": {}, 61 | "outputs": [ 62 | { 63 | "data": { 64 | "text/plain": [ 65 | "628" 66 | ] 67 | }, 68 | "execution_count": 245, 69 | "metadata": {}, 70 | "output_type": "execute_result" 71 | } 72 | ], 73 | "source": [ 74 | "with open(\"my-file.orc\",\"rb\") as f:\n", 75 | " buf = f.read()\n", 76 | "len(buf)" 77 | ] 78 | }, 79 | { 80 | "cell_type": "markdown", 81 | "metadata": {}, 82 | "source": [ 83 | "Пока все бьется - размер файла 628 байт (как и обещали в блоге). И это - забегая вперед - безо всякой компрессии!\n", 84 | "\n", 85 | "Что дальше: как известно из спецификации, чтение начинается с хвоста: читаем PostScript. \n", 86 | "\n", 87 | "Для этого сначала считаем последний байт файла - он содержит размер PostScript-а, который (единственный из всех частей файла) никогда не сжимается." 88 | ] 89 | }, 90 | { 91 | "cell_type": "markdown", 92 | "metadata": {}, 93 | "source": [ 94 | "**Часть 1: PostScript и Footer**\n", 95 | "\n", 96 | "считаем и распечатаем (protobuf предоставляет такую замечательную возможность - супер просто...)" 97 | ] 98 | }, 99 | { 100 | "cell_type": "markdown", 101 | "metadata": {}, 102 | "source": [ 103 | "**Шаг 1: Postdcript**" 104 | ] 105 | }, 106 | { 107 | "cell_type": "code", 108 | "execution_count": 246, 109 | "metadata": {}, 110 | "outputs": [ 111 | { 112 | "name": "stdout", 113 | "output_type": "stream", 114 | "text": [ 115 | "psLen 23\n" 116 | ] 117 | } 118 | ], 119 | "source": [ 120 | "psLen = buf[-1] # psLen\n", 121 | "print(\"psLen\",psLen)" 122 | ] 123 | }, 124 | { 125 | "cell_type": "code", 126 | "execution_count": 247, 127 | "metadata": {}, 128 | "outputs": [], 129 | "source": [ 130 | "psBuf = buf[-psLen-1:-1]" 131 | ] 132 | }, 133 | { 134 | "cell_type": "code", 135 | "execution_count": 248, 136 | "metadata": {}, 137 | "outputs": [ 138 | { 139 | "name": "stdout", 140 | "output_type": "stream", 141 | "text": [ 142 | "footerLength: 115\n", 143 | "compression: NONE\n", 144 | "compressionBlockSize: 65536\n", 145 | "version: 0\n", 146 | "version: 12\n", 147 | "metadataLength: 50\n", 148 | "writerVersion: 4\n", 149 | "magic: \"ORC\"\n", 150 | "\n" 151 | ] 152 | } 153 | ], 154 | "source": [ 155 | "pScript = orc_proto_pb2.PostScript()\n", 156 | "pScript.ParseFromString(psBuf)\n", 157 | "print(pScript)\n", 158 | "if pScript.compression not in [0,1]: # [NONE,ZLIB]\n", 159 | " print(\"Compression unknown\")\n", 160 | "comprOffs = 3 if pScript.compression==1 else 0" 161 | ] 162 | }, 163 | { 164 | "cell_type": "markdown", 165 | "metadata": {}, 166 | "source": [ 167 | "Что мы здесь видим:\n", 168 | "\n", 169 | "* файл не сжат (ZLIB я отключил в опциях создания файла)\n", 170 | "* размер футера и метаданных (будем использовать ниже)\n", 171 | "* версия \"писателя\": важно (!). в библиотеке стояло 6 (текущая на момент ее клонирования), в результате все падало: HIVE не знал, что \"бывают версии старше 4\". Пришлось код поправить...\n", 172 | "\n", 173 | "Остальное - просто так, для информации. Про сжатие - см. комментарии в блоге." 174 | ] 175 | }, 176 | { 177 | "cell_type": "markdown", 178 | "metadata": {}, 179 | "source": [ 180 | "**Шаг 2: Footer**\n", 181 | "\n", 182 | "дальше читаем footer (обратите внимание на обработку сжатых файлов, хоть у нас и не сжат, но код рассчитан на все случаи)" 183 | ] 184 | }, 185 | { 186 | "cell_type": "code", 187 | "execution_count": 186, 188 | "metadata": {}, 189 | "outputs": [], 190 | "source": [ 191 | "ftBuf = buf[-psLen-pScript.footerLength-1+comprOffs:-psLen-1]" 192 | ] 193 | }, 194 | { 195 | "cell_type": "code", 196 | "execution_count": 187, 197 | "metadata": {}, 198 | "outputs": [ 199 | { 200 | "name": "stdout", 201 | "output_type": "stream", 202 | "text": [ 203 | "headerLength: 3\n", 204 | "contentLength: 436\n", 205 | "stripes {\n", 206 | " offset: 3\n", 207 | " indexLength: 73\n", 208 | " dataLength: 276\n", 209 | " footerLength: 87\n", 210 | " numberOfRows: 10000\n", 211 | "}\n", 212 | "types {\n", 213 | " kind: STRUCT\n", 214 | " subtypes: 1\n", 215 | " subtypes: 2\n", 216 | " fieldNames: \"x\"\n", 217 | " fieldNames: \"y\"\n", 218 | " maximumLength: 0\n", 219 | " precision: 0\n", 220 | " scale: 0\n", 221 | "}\n", 222 | "types {\n", 223 | " kind: INT\n", 224 | " maximumLength: 0\n", 225 | " precision: 0\n", 226 | " scale: 0\n", 227 | "}\n", 228 | "types {\n", 229 | " kind: INT\n", 230 | " maximumLength: 0\n", 231 | " precision: 0\n", 232 | " scale: 0\n", 233 | "}\n", 234 | "numberOfRows: 10000\n", 235 | "statistics {\n", 236 | " numberOfValues: 10000\n", 237 | " hasNull: false\n", 238 | "}\n", 239 | "statistics {\n", 240 | " numberOfValues: 10000\n", 241 | " intStatistics {\n", 242 | " minimum: 0\n", 243 | " maximum: 9999\n", 244 | " sum: 49995000\n", 245 | " }\n", 246 | " hasNull: false\n", 247 | "}\n", 248 | "statistics {\n", 249 | " numberOfValues: 10000\n", 250 | " intStatistics {\n", 251 | " minimum: 0\n", 252 | " maximum: 29997\n", 253 | " sum: 149985000\n", 254 | " }\n", 255 | " hasNull: false\n", 256 | "}\n", 257 | "rowIndexStride: 10000\n", 258 | "\n" 259 | ] 260 | } 261 | ], 262 | "source": [ 263 | "footer = orc_proto_pb2.Footer()\n", 264 | "if pScript.compression==1: ftBuf = zlib.decompress(ftBuf,-zlib.MAX_WBITS)# inflate version!\n", 265 | "footer.ParseFromString(ftBuf)\n", 266 | "print(footer)" 267 | ] 268 | }, 269 | { 270 | "cell_type": "markdown", 271 | "metadata": {}, 272 | "source": [ 273 | "Что интересного видим здесь\n", 274 | "\n", 275 | "* есть размеры всех частей (лучше смотреть код ниже и описание структуры в блоге)\n", 276 | "* страйп у нас получился 1\n", 277 | "* структура таблицы\n", 278 | " * на верхнем уровне всегда struct\n", 279 | " * далее - две колонки (x и y) целого типа\n", 280 | "* файловая статистика\n", 281 | " * 10 000 строк\n", 282 | " * по колоночно (нет нулей, мин, макс и сумма для каждой колонки)\n", 283 | "* последнее - размер блока строк, на которые в страйпе будет создан индекс (10 тыс, значение по умолчанию)" 284 | ] 285 | }, 286 | { 287 | "cell_type": "markdown", 288 | "metadata": {}, 289 | "source": [ 290 | "**Шаг 3: Metadata**\n", 291 | "\n", 292 | "далее читаем метаданные" 293 | ] 294 | }, 295 | { 296 | "cell_type": "code", 297 | "execution_count": 188, 298 | "metadata": {}, 299 | "outputs": [], 300 | "source": [ 301 | "metaBuf = buf[-psLen-pScript.footerLength-pScript.metadataLength-1+comprOffs:-psLen-pScript.footerLength-1]" 302 | ] 303 | }, 304 | { 305 | "cell_type": "code", 306 | "execution_count": 189, 307 | "metadata": {}, 308 | "outputs": [ 309 | { 310 | "name": "stdout", 311 | "output_type": "stream", 312 | "text": [ 313 | "stripeStats {\n", 314 | " colStats {\n", 315 | " numberOfValues: 10000\n", 316 | " hasNull: false\n", 317 | " }\n", 318 | " colStats {\n", 319 | " numberOfValues: 10000\n", 320 | " intStatistics {\n", 321 | " minimum: 0\n", 322 | " maximum: 9999\n", 323 | " sum: 49995000\n", 324 | " }\n", 325 | " hasNull: false\n", 326 | " }\n", 327 | " colStats {\n", 328 | " numberOfValues: 10000\n", 329 | " intStatistics {\n", 330 | " minimum: 0\n", 331 | " maximum: 29997\n", 332 | " sum: 149985000\n", 333 | " }\n", 334 | " hasNull: false\n", 335 | " }\n", 336 | "}\n", 337 | "\n" 338 | ] 339 | } 340 | ], 341 | "source": [ 342 | "meta = orc_proto_pb2.Metadata()\n", 343 | "if pScript.compression==1: metaBuf = zlib.decompress(metaBuf,-zlib.MAX_WBITS)# inflate version!\n", 344 | "meta.ParseFromString(metaBuf)\n", 345 | "print(meta)" 346 | ] 347 | }, 348 | { 349 | "cell_type": "markdown", 350 | "metadata": {}, 351 | "source": [ 352 | "Что интересного в метаданных\n", 353 | "\n", 354 | "статистика по каждому страйпу - у нас он один, поэтому только одна секция, значения совпадают со статистикой по файлу (а тип данных для статистик один - ColumnStatistics)." 355 | ] 356 | }, 357 | { 358 | "cell_type": "markdown", 359 | "metadata": {}, 360 | "source": [ 361 | "**Часть 2: данные**" 362 | ] 363 | }, 364 | { 365 | "cell_type": "markdown", 366 | "metadata": {}, 367 | "source": [ 368 | "**Шаг 1: Stripe Footer**\n", 369 | "\n", 370 | "Он уже содержится в страйпе (который у нас один)" 371 | ] 372 | }, 373 | { 374 | "cell_type": "code", 375 | "execution_count": 190, 376 | "metadata": {}, 377 | "outputs": [], 378 | "source": [ 379 | "st = footer.stripes[0].offset+footer.stripes[0].indexLength+footer.stripes[0].dataLength\n", 380 | "fn = st + footer.stripes[0].footerLength\n", 381 | "stripeFtBuf = buf[st:fn]" 382 | ] 383 | }, 384 | { 385 | "cell_type": "code", 386 | "execution_count": 191, 387 | "metadata": {}, 388 | "outputs": [ 389 | { 390 | "name": "stdout", 391 | "output_type": "stream", 392 | "text": [ 393 | "streams {\n", 394 | " kind: ROW_INDEX\n", 395 | " column: 0\n", 396 | " length: 14\n", 397 | "}\n", 398 | "streams {\n", 399 | " kind: ROW_INDEX\n", 400 | " column: 1\n", 401 | " length: 29\n", 402 | "}\n", 403 | "streams {\n", 404 | " kind: ROW_INDEX\n", 405 | " column: 2\n", 406 | " length: 30\n", 407 | "}\n", 408 | "streams {\n", 409 | " kind: PRESENT\n", 410 | " column: 0\n", 411 | " length: 20\n", 412 | "}\n", 413 | "streams {\n", 414 | " kind: PRESENT\n", 415 | " column: 1\n", 416 | " length: 20\n", 417 | "}\n", 418 | "streams {\n", 419 | " kind: DATA\n", 420 | " column: 1\n", 421 | " length: 103\n", 422 | "}\n", 423 | "streams {\n", 424 | " kind: PRESENT\n", 425 | " column: 2\n", 426 | " length: 20\n", 427 | "}\n", 428 | "streams {\n", 429 | " kind: DATA\n", 430 | " column: 2\n", 431 | " length: 113\n", 432 | "}\n", 433 | "columns {\n", 434 | " kind: DIRECT\n", 435 | " dictionarySize: 0\n", 436 | "}\n", 437 | "columns {\n", 438 | " kind: DIRECT_V2\n", 439 | " dictionarySize: 0\n", 440 | "}\n", 441 | "columns {\n", 442 | " kind: DIRECT_V2\n", 443 | " dictionarySize: 0\n", 444 | "}\n", 445 | "writerTimezone: \"GMT\"\n", 446 | "\n" 447 | ] 448 | } 449 | ], 450 | "source": [ 451 | "stripeFooter = orc_proto_pb2.StripeFooter()\n", 452 | "stripeFooter.ParseFromString(stripeFtBuf)\n", 453 | "print(stripeFooter)" 454 | ] 455 | }, 456 | { 457 | "cell_type": "markdown", 458 | "metadata": {}, 459 | "source": [ 460 | "Что мы здесь видим:\n", 461 | "\n", 462 | "* сначала в страйпе идут три стрима с индексами\n", 463 | "* далее идет PRESENT стрим для \"псевдо колонки\" (структуры)\n", 464 | "* по PRESENT + DATA стримы для реальных колонок\n", 465 | "* информация о кодировании колонок (DIRECT для структуры и DIRECT_V2 для реальных колонок)\n", 466 | "\n", 467 | "Из непонятного пока - почему появились PRESENT стримы - вроде вверху видно, что \"нулей\" итак нет..." 468 | ] 469 | }, 470 | { 471 | "cell_type": "markdown", 472 | "metadata": {}, 473 | "source": [ 474 | "**Шаг 2: Индексы**\n", 475 | "\n", 476 | "посмотрим на наиболее интересные - по псевдо колонке (он первый)" 477 | ] 478 | }, 479 | { 480 | "cell_type": "code", 481 | "execution_count": 192, 482 | "metadata": {}, 483 | "outputs": [], 484 | "source": [ 485 | "st = footer.stripes[0].offset\n", 486 | "fn = st + stripeFooter.streams[0].length\n", 487 | "stripeFtBuf = buf[st:fn]" 488 | ] 489 | }, 490 | { 491 | "cell_type": "code", 492 | "execution_count": 193, 493 | "metadata": {}, 494 | "outputs": [ 495 | { 496 | "name": "stdout", 497 | "output_type": "stream", 498 | "text": [ 499 | "entry {\n", 500 | " positions: 0\n", 501 | " positions: 0\n", 502 | " positions: 0\n", 503 | " statistics {\n", 504 | " numberOfValues: 10000\n", 505 | " hasNull: false\n", 506 | " }\n", 507 | "}\n", 508 | "\n" 509 | ] 510 | } 511 | ], 512 | "source": [ 513 | "rowIndex = orc_proto_pb2.RowIndex()\n", 514 | "rowIndex.ParseFromString(stripeFtBuf)\n", 515 | "print(rowIndex)" 516 | ] 517 | }, 518 | { 519 | "cell_type": "markdown", 520 | "metadata": {}, 521 | "source": [ 522 | "Пока непонятно... там и данных-то нет, попробуем посмотреть на реальную колонку" 523 | ] 524 | }, 525 | { 526 | "cell_type": "code", 527 | "execution_count": 196, 528 | "metadata": {}, 529 | "outputs": [], 530 | "source": [ 531 | "st = footer.stripes[0].offset + stripeFooter.streams[0].length\n", 532 | "fn = st + stripeFooter.streams[1].length\n", 533 | "stripeFtBuf = buf[st:fn]" 534 | ] 535 | }, 536 | { 537 | "cell_type": "code", 538 | "execution_count": 197, 539 | "metadata": {}, 540 | "outputs": [ 541 | { 542 | "name": "stdout", 543 | "output_type": "stream", 544 | "text": [ 545 | "entry {\n", 546 | " positions: 0\n", 547 | " positions: 0\n", 548 | " positions: 0\n", 549 | " positions: 0\n", 550 | " positions: 0\n", 551 | " statistics {\n", 552 | " numberOfValues: 10000\n", 553 | " intStatistics {\n", 554 | " minimum: 0\n", 555 | " maximum: 9999\n", 556 | " sum: 49995000\n", 557 | " }\n", 558 | " hasNull: false\n", 559 | " }\n", 560 | "}\n", 561 | "\n" 562 | ] 563 | } 564 | ], 565 | "source": [ 566 | "rowIndex = orc_proto_pb2.RowIndex()\n", 567 | "rowIndex.ParseFromString(stripeFtBuf)\n", 568 | "print(rowIndex)" 569 | ] 570 | }, 571 | { 572 | "cell_type": "markdown", 573 | "metadata": {}, 574 | "source": [ 575 | "Понятнее не стало... Смотрим данные" 576 | ] 577 | }, 578 | { 579 | "cell_type": "markdown", 580 | "metadata": {}, 581 | "source": [ 582 | "**Шаг 3: Data**" 583 | ] 584 | }, 585 | { 586 | "cell_type": "markdown", 587 | "metadata": {}, 588 | "source": [ 589 | "Смотрим PRESENT stream для структуры (по идее должен быть из единичек - все не нули)" 590 | ] 591 | }, 592 | { 593 | "cell_type": "code", 594 | "execution_count": 200, 595 | "metadata": {}, 596 | "outputs": [], 597 | "source": [ 598 | "st = footer.stripes[0].offset + footer.stripes[0].indexLength\n", 599 | "fn = st + stripeFooter.streams[3].length\n", 600 | "dataBuf = buf[st:fn]" 601 | ] 602 | }, 603 | { 604 | "cell_type": "code", 605 | "execution_count": 209, 606 | "metadata": {}, 607 | "outputs": [ 608 | { 609 | "name": "stdout", 610 | "output_type": "stream", 611 | "text": [ 612 | "HDR: 127 , DATA: 255\n", 613 | "HDR: 127 , DATA: 255\n", 614 | "HDR: 127 , DATA: 255\n", 615 | "HDR: 127 , DATA: 255\n", 616 | "HDR: 127 , DATA: 255\n", 617 | "HDR: 127 , DATA: 255\n", 618 | "HDR: 127 , DATA: 255\n", 619 | "HDR: 127 , DATA: 255\n", 620 | "HDR: 127 , DATA: 255\n", 621 | "HDR: 77 , DATA: 255\n", 622 | "Values count: 1220\n" 623 | ] 624 | } 625 | ], 626 | "source": [ 627 | "i = 0\n", 628 | "valCnt = 0\n", 629 | "while True:\n", 630 | " hdr = dataBuf[i]\n", 631 | " valCnt += hdr\n", 632 | " if hdr<=127:\n", 633 | " data = dataBuf[i+1]\n", 634 | " print(\"HDR:\",hdr,\", DATA:\", data)\n", 635 | " i += 2\n", 636 | " if i==20:\n", 637 | " break\n", 638 | "print (\"Values count:\", valCnt)" 639 | ] 640 | }, 641 | { 642 | "cell_type": "markdown", 643 | "metadata": {}, 644 | "source": [ 645 | "у нас 10тыс строк, они бьются на run-ы по 127: 10000/127 = 78 и еще останется 94\n", 646 | "\n", 647 | "Но, судя по данным (в стриме - 20 байт), что-то со значениями не совсем так.. Получилось 1220 значений\n", 648 | "\n", 649 | "Не будем заморачиваться другими PRESENT стримами - похоже, они такие же, надо поскорее на данные посмотреть" 650 | ] 651 | }, 652 | { 653 | "cell_type": "markdown", 654 | "metadata": {}, 655 | "source": [ 656 | "**Колонка 1** данные" 657 | ] 658 | }, 659 | { 660 | "cell_type": "code", 661 | "execution_count": 223, 662 | "metadata": {}, 663 | "outputs": [], 664 | "source": [ 665 | "st = footer.stripes[0].offset + footer.stripes[0].indexLength + stripeFooter.streams[3].length + stripeFooter.streams[4].length\n", 666 | "fn = st + stripeFooter.streams[5].length\n", 667 | "dataBuf = buf[st:fn]" 668 | ] 669 | }, 670 | { 671 | "cell_type": "markdown", 672 | "metadata": {}, 673 | "source": [ 674 | "Первая колонка закодирована при помощи DIRECT_V2 для целых - это RLE v2\n", 675 | "\n", 676 | "первые два бита - тип кодировки, дальше - зависит от типа\n", 677 | "\n", 678 | "* 0=short: заголовок 1 байт\n", 679 | "* 1=direct: 2 байта\n", 680 | "* 2=patched base: 4 байта\n", 681 | "* 3=delta: 2 байта\n", 682 | "\n", 683 | "смотрим данные" 684 | ] 685 | }, 686 | { 687 | "cell_type": "code", 688 | "execution_count": 233, 689 | "metadata": {}, 690 | "outputs": [ 691 | { 692 | "name": "stdout", 693 | "output_type": "stream", 694 | "text": [ 695 | "HDR: 0b11000001 0b11111111\n", 696 | "0\n", 697 | "2\n", 698 | "HDR: 0b11000001 0b11111111\n", 699 | "0b10000000 0b1000\n", 700 | "2\n", 701 | "HDR: 0b11000001 0b11111111\n", 702 | "0b10000000 0b10000\n", 703 | "2\n" 704 | ] 705 | } 706 | ], 707 | "source": [ 708 | "print(\"HDR:\",bin(dataBuf[0]),bin(dataBuf[1]))\n", 709 | "print(dataBuf[2])\n", 710 | "print(dataBuf[3])\n", 711 | "print(\"HDR:\",bin(dataBuf[4]),bin(dataBuf[5]))\n", 712 | "print(bin(dataBuf[6]),bin(dataBuf[7]))\n", 713 | "print(dataBuf[8])\n", 714 | "print(\"HDR:\",bin(dataBuf[9]),bin(dataBuf[10]))\n", 715 | "print(bin(dataBuf[11]),bin(dataBuf[12]))\n", 716 | "print(dataBuf[13])" 717 | ] 718 | }, 719 | { 720 | "cell_type": "markdown", 721 | "metadata": {}, 722 | "source": [ 723 | "**Разбираем**\n", 724 | "\n", 725 | "у нас получается delta кодировка (логично - последовательность)\n", 726 | "\n", 727 | "первая пачка:\n", 728 | "* 511 значений ширины 0 (т.е. никаких больше дельт не будет!, только первая)\n", 729 | "* base = 0 (с базой получается как раз 512 значений)\n", 730 | "* delta = 1\n", 731 | "предполагаю, что должно обозначать 512 значений, начиная с нуля, увеличивающееся на 1 до 511 включительно...\n", 732 | "\n", 733 | "вторая пачка (аналогично):\n", 734 | "* 511 значений\n", 735 | "* base = 1024 (?) предполагаю, что это есть 512 из-за зигзага...\n", 736 | "* delta = 1\n", 737 | "третья пачка (аналогично):\n", 738 | "* 511 значений\n", 739 | "* base = 1024 \n", 740 | "* delta = 1\n", 741 | "...\n", 742 | "\n", 743 | "что получается - растет длина базы (за счет varint - кодируем целые). Остальное - постоянно. \n", 744 | "\n", 745 | "Посмотрим общую длину\n", 746 | "\n", 747 | "всего 103 байта:\n", 748 | "* 4 первая\n", 749 | "* 25 еще пять (по 5 байт каждая)\n", 750 | "* 84 еще 14 (по шесть байт каждая)\n", 751 | "сошлось..." 752 | ] 753 | }, 754 | { 755 | "cell_type": "markdown", 756 | "metadata": {}, 757 | "source": [ 758 | "**Колонка 2** данные" 759 | ] 760 | }, 761 | { 762 | "cell_type": "code", 763 | "execution_count": 241, 764 | "metadata": {}, 765 | "outputs": [], 766 | "source": [ 767 | "st = footer.stripes[0].offset + footer.stripes[0].indexLength + stripeFooter.streams[3].length + \\\n", 768 | " stripeFooter.streams[4].length + stripeFooter.streams[5].length + stripeFooter.streams[6].length\n", 769 | "fn = st + stripeFooter.streams[7].length\n", 770 | "dataBuf = buf[st:fn]" 771 | ] 772 | }, 773 | { 774 | "cell_type": "code", 775 | "execution_count": 242, 776 | "metadata": {}, 777 | "outputs": [ 778 | { 779 | "name": "stdout", 780 | "output_type": "stream", 781 | "text": [ 782 | "HDR: 0b11000001 0b11111111\n", 783 | "0\n", 784 | "6\n", 785 | "HDR: 0b11000001 0b11111111\n", 786 | "0b10000000 0b11000\n", 787 | "6\n", 788 | "HDR: 0b11000001 0b11111111\n", 789 | "0b10000000 0b110000\n", 790 | "6\n" 791 | ] 792 | } 793 | ], 794 | "source": [ 795 | "print(\"HDR:\",bin(dataBuf[0]),bin(dataBuf[1]))\n", 796 | "print(dataBuf[2])\n", 797 | "print(dataBuf[3])\n", 798 | "print(\"HDR:\",bin(dataBuf[4]),bin(dataBuf[5]))\n", 799 | "print(bin(dataBuf[6]),bin(dataBuf[7]))\n", 800 | "print(dataBuf[8])\n", 801 | "print(\"HDR:\",bin(dataBuf[9]),bin(dataBuf[10]))\n", 802 | "print(bin(dataBuf[11]),bin(dataBuf[12]))\n", 803 | "print(dataBuf[13])" 804 | ] 805 | }, 806 | { 807 | "cell_type": "markdown", 808 | "metadata": {}, 809 | "source": [ 810 | "**Разбираем**\n", 811 | "\n", 812 | "у нас получается delta кодировка (логично - последовательность)\n", 813 | "\n", 814 | "первая пачка:\n", 815 | "* 511 значений ширины 0 (т.е. никаких больше дельт не будет!, только первая)\n", 816 | "* base = 0 (с базой получается как раз 512 значений)\n", 817 | "* delta = 3\n", 818 | "предполагаю, что должно обозначать 512 значений, начиная с нуля, увеличивающееся на 3 до ... включительно...\n", 819 | "\n", 820 | "в-общем все похоже на предыдущий вариант, только разница будет в длинах базы (она быстрее растет - макс 29997)" 821 | ] 822 | }, 823 | { 824 | "cell_type": "markdown", 825 | "metadata": {}, 826 | "source": [ 827 | "---------------------------------------------------------------------дальше всякая отладка и другие разности..." 828 | ] 829 | }, 830 | { 831 | "cell_type": "code", 832 | "execution_count": 239, 833 | "metadata": {}, 834 | "outputs": [ 835 | { 836 | "data": { 837 | "text/plain": [ 838 | "84" 839 | ] 840 | }, 841 | "execution_count": 239, 842 | "metadata": {}, 843 | "output_type": "execute_result" 844 | } 845 | ], 846 | "source": [ 847 | "14*6\n" 848 | ] 849 | }, 850 | { 851 | "cell_type": "code", 852 | "execution_count": 69, 853 | "metadata": {}, 854 | "outputs": [ 855 | { 856 | "data": { 857 | "text/plain": [ 858 | "'34'" 859 | ] 860 | }, 861 | "execution_count": 69, 862 | "metadata": {}, 863 | "output_type": "execute_result" 864 | } 865 | ], 866 | "source": [ 867 | "#with open(\"ft.gz\",\"wb\") as ff:\n", 868 | "## ff.write(ftBuf)\n", 869 | "#len(ftBuf)\n", 870 | "st = 2\n", 871 | "ln = 2\n", 872 | "\"123456\"[st:st+ln]" 873 | ] 874 | }, 875 | { 876 | "cell_type": "code", 877 | "execution_count": null, 878 | "metadata": {}, 879 | "outputs": [], 880 | "source": [ 881 | "#buf[-psLen-pScript.footerLength-1+2]\n", 882 | "#f = open(\"footer.dat\",\"rb\")\n", 883 | "#tBuf = f.read()\n", 884 | "#.close()\n", 885 | "#len(ftBuf)" 886 | ] 887 | } 888 | ], 889 | "metadata": { 890 | "kernelspec": { 891 | "display_name": "Python 3", 892 | "language": "python", 893 | "name": "python3" 894 | }, 895 | "language_info": { 896 | "codemirror_mode": { 897 | "name": "ipython", 898 | "version": 3 899 | }, 900 | "file_extension": ".py", 901 | "mimetype": "text/x-python", 902 | "name": "python", 903 | "nbconvert_exporter": "python", 904 | "pygments_lexer": "ipython3", 905 | "version": "3.5.2" 906 | } 907 | }, 908 | "nbformat": 4, 909 | "nbformat_minor": 2 910 | } 911 | -------------------------------------------------------------------------------- /orc/orc_format_notes.md: -------------------------------------------------------------------------------- 1 | # История формата 2 | 3 | ORC: Optimized RC Format (где RC = Record Columnar), собственно базируется на своем предшественнике (RC Format-е) и 4 | всего-лишь 5 | 6 | `the smallest, fastest columnar storage for Hadoop workloads` 7 | 8 | (это цитата с главной страницы проекта - https://orc.apache.org/. Немного погрузившись - я ей верю...) 9 | 10 | Формат уже не юн - создан в 2013 году, сейчас стабильная версия - v1, в работе - v2. 11 | Достаточно хорошо документирован - все, о чем я здесь пишу, базируется на спецификации 12 | (https://orc.apache.org/specification/) и проверено питновским кодом (который здесь же). 13 | 14 | # Верхнеуровневое описание формата 15 | 16 | ORC файл полностью самодостаточен (как и любой другой формат Hadoop - иначе как читать, никаких отдельных метаданных нет...). 17 | Файл состоит из достаточно больших "страйпов" (stripes) порядка 64МБ каждый и "хвостовичка", содержащего общую для всего файла метаинформацию. Страйп является также самодостаточным (в плане данных) и содержит подмножество строк исходной таблицы. Для чтения данных (подмножества строк) достаточно прочитать хвостовик файла (мета информацию) и страйп(ы), который содержит данные нужных строк. 18 | 19 | Формат ориентирован на хранение колонок - в пределах страйпа можно прочитать данные каждой из колонок независимо. 20 | 21 | Каждая часть файла (про "части" - ниже) может быть сжата (ZLIB, например), при этом опять же сжатие не должно мешать возможности считать части файла по отдельности. 22 | 23 | Файл содержит индексы (на трех уровнях: файл, страйп и блок строк, про блоки - ниже). Что дополнительно ускоряет чтение данных из файла. 24 | 25 | Значения колонок хранятся в encoded виде (например, RLE), причем виды кодировки для одной колонки в пределах одного блока строк могут быть разными - все зависит от данных. Кстати, именно этим объясняется тот размер (628 байт), который был приведен выше: данные "легли" так удачно, что RLE кодировка их ну совсем сжала... См. детали ниже. 26 | 27 | Ну и в завершение - метаданные хранятся с использованием Protocol Buffers (https://developers.google.com/protocol-buffers/docs/encoding), что тоже прикольно. 28 | 29 | # Какая от этого конкретная польза и как ее получить 30 | 31 | Формат очень хорошо поддерживается Hadoop-ом, HIVE-у достаточно просто указать, что таблица будет храниться как ORC. Тогда данные таблицы будут храниться в ORC файлах (с ZLIB сжатием по умолчанию). 32 | 33 | HIVE имеет утилиту просмотра метаданных ORC файлов, т.е. их можно поизучать (я изучал прямо на питоне по спецификации). 34 | 35 | SQOOP пока не поддерживает ORC (говорят, что будет). Поэтому для создания ORC таблиц приходится делать 2 шага 36 | 37 | * sqoop таблицу из внешней базы в текстовый файл 38 | * hive создает таблицу с данными из текстового файла 39 | 40 | Абсолютные времена последней операции ("перегона" данных из текста в ORC) вполне приемлемы. 41 | 42 | Кроме того, можно воспользоваться C++ библиотекой (см. приведенные выше сайт проекта - там есть описание, как собраться С++ библиотеку, я ее собрал - работает) и создавать или читать файлы напрямую из C++. 43 | 44 | Сравнение форматов (parquet vs orc) - в процессе. Пока по результатам изучения "ставка" сделана на ORC (хотя в планах изучить parquet также, как и ORC - в байтах...) 45 | 46 | # Собственно описание деталей 47 | 48 | Вот тут я чуть призадумался - не повторять же здесь спецификацию, в ней все хорошо написано (просто надо внимательно читать). 49 | 50 | Наверное вот как лучше сделать: дальнейшее чтение лучше делать в "юпитеровской книжке". 51 | 52 | А здесь (ниже) я постараюсь описать то, что "не влезло" непосредственно в книжку (даже интересно - что это будет...). 53 | 54 | # Что не влезло в книжку 55 | 56 | ## Сжатие 57 | 58 | `If the ORC file writer selects a generic compression codec (zlib or snappy), every part of the ORC file except for the Postscript is compressed with that codec. However, one of the requirements for ORC is that the reader be able to skip over compressed bytes without decompressing the entire stream. To manage this, ORC writes compressed streams in chunks with headers as in the figure below. To handle uncompressable data, if the compressed data is larger than the original, the original is stored and the isOriginal flag is set. Each header is 3 bytes long with (compressedLength * 2 + isOriginal) stored as a little endian value. For example, the header for a chunk that compressed to 100,000 bytes would be [0x40, 0x0d, 0x03]. The header for 5 bytes that did not compress would be [0x0b, 0x00, 0x00]. Each compression chunk is compressed independently so that as long as a decompressor starts at the top of a header, it can start decompressing without the previous bytes.` 59 | 60 | В-общем это надо понимать так: 61 | 62 | * если в файле применяется сжатие, то все метаданные (см. структуру ниже) будут сжаты. Про сами данные не понял до конца - сжаты они или нет (в этом примере я сжатие отключил - может быть, попозже обновлю пост, либо напишу отдельно: есть мысль посмотреть, как устроены реальные данные - не из тестового примера, а из базы) 63 | * про каждый блок метаданных известна его длина (и смещение, которое получается обратным счетом - см. книжку) 64 | * тогда нужно отступить 3 байта от начала сжатых данных секции (именно это и хранится в переменной comprOffs - смещение относительно начала секции. Для несжатых файлов это всегда 0) 65 | * считанные "длина секции - 3" байта нужно "декомпрессить" так, как это написано в книжке (zlib deflate) 66 | 67 | Не сложно... Но пока я подобрал "все ключи" - ручек в стены полетало... 68 | 69 | ## Общая структура файла 70 | 71 | Все же она недостаточно просто описана в спецификации, ниже приведу свою "версию" описания структуры. 72 | 73 | ORC файл состоит из двух логических частей 74 | 75 | * повторяющиеся страйпы 76 | * хвостовик (который я упоминал выше) 77 | 78 | ORC файл читается 79 | 80 | * снизу-вверх (хвостовик) 81 | * сверху вниз (страйпы), точнее - они адресуются по абсолютному смещению в файле и имеют размер (все это хранится в хвостовике) 82 | 83 | * футер страйпа находится в его конце, сначала читается он 84 | 85 | Попробую обойтись без рисунка - списком секций (в скобках - длина). Не расписывал все поля - только те, которые позволяют читать файл. 86 | 87 | Повтояющиеся страйпы 88 | Страйп(ы) 89 | их размеры известны из хвостовика 90 | структура страйпа описана в книжке 91 | Хвостовик 92 | MetaData, может быть сжать (PostScript.metaDataLength байт) 93 | содержит информацию о страйпах (сколько строк в каждом, мин макс и сумма каждой колонки), это не позволяет адресоваться по файлу, но позволяет понять, какие страйпы нужно читать 94 | Footer, может быть сжат (PostScript.footerLength байт) 95 | содержит количество страйпов (неявно - количество секций) 96 | про каждый страйп - смещение и длина (длина в разбивке по составным страйпа) 97 | PostScript, никогда не сжат, гарантированно не может быть длиннее 256 байт (ps байт) 98 | содержит длину Footer (поле footerLength) 99 | содержит длину Metadata (поле metadataLength) 100 | ps: длина PostScript (1 байт) 101 | 102 | ## Все же данные - как же так коротко получается? 103 | 104 | В юпитеровской книжке все про данные написано, но кратко все же стоит срезюмировать здесь: как удается так сжать данные? 105 | 106 | Данные хранятся в страйпах поколоночно. Каждая колонка - отдельный поток (пока забудем про разбиение внутри страйпа). Внутри потока (=колонки) данные хранятся кодированным образом, но единообразно. Кодировка указывается в стайп футере (см. книжку). 107 | 108 | Кодировок достаточно много, давайте остановимся на числовом типе - целом, например: 109 | 110 | Short Repeat - used for short sequences with repeated values 111 | Direct - used for random sequences with a fixed bit width 112 | Patched Base - used for random sequences with a variable bit width 113 | Delta - used for monotonically increasing or decreasing sequences 114 | 115 | Внутри одной и той же колонки эти кодировки могут меняться, в-зависимости от данных (колонки). Грубо говоря 116 | 117 | * по первым двум байтам можно понять - какая кодировка используется в "фрагменте" (термин мой) 118 | * сколько байт этот фрагмент занимает 119 | 120 | Таким образом можно "расшифровать" (раскодировать, наверное, более политкорректно) хранящиеся данные. 121 | 122 | Вернемся к нашему примеру - у нас табличка создавалась таким кусочком кода (оттуда же - из первоисточника, апач). 123 | 124 | struct 125 | for (i = 0; i < 10000; ++i) { 126 | x->data[rows] = i; 127 | y->data[rows] = i * 3; 128 | } 129 | 130 | Что имеем в страйпе в потоке, соответствущем первой колонке ("x") 131 | 132 | * кодировка Delta (логично - у нас точная дельта) 133 | * первый фрагмент - 4 байта: 134 | 135 | * заголовок 2 байта, он же содержит количество значений, которые должны раскодироваться из этого фрагмента - 511 136 | * начальное значение (0) 137 | * дельта (1) 138 | 139 | * второй фрагмент - 5 байт: 140 | 141 | * заголовок 2 байта, он же содержит количество значений, которые должны раскодироваться из этого фрагмента - 511 142 | * начальное значение (512), в "зигзаг" кодироке занимает 2 байта 143 | * дельта (1) 144 | 145 | * и т.д. 146 | 147 | Что получается "крупноблочно" 148 | 149 | * один 4 байтовый фрагмент 150 | * пять пятибайтовых фрагментов (размер растет за счет зигзаг кодировки начального значения) 151 | * четырнадцать шестибайтовых фрагментов 152 | 153 | Итого - 103 байта (! на 10тыс чисел) 154 | 155 | Вторая колонка - аналогично, только там шаг - 3. 156 | 157 | # Вместо итого 158 | 159 | ORC - весьма продвинутый формат (я еще не смотрел паркет, но, мне кажется, там чуть по-проще. Статья про паркет последует...). 160 | 161 | Если Оракл (и Клик) скрывают то, как они хранят данные внутри, то в случае ORC это достаточно хорошо описано и каждый может разобраться. Что (как минимум) прикольно (см. также ниже). 162 | 163 | Что интересно и подлежит обдумыванию: 164 | Сам формат (ORC) предоставляет кучу (пардон за словечко) возможностей по оптимизации 165 | 166 | * хранения данных 167 | * доступа к данным 168 | 169 | Суметь ими воспользоваться, это 170 | 171 | * задача reader-а 172 | * задача writer-а 173 | 174 | Причем читатель и писатель могут быть от разных "авторов"... 175 | 176 | Т.е. я, как "писатель" могу "наплевать" на всякие там индексы (как в этом примере, кстати, и сделали с индексами - хотя могу ошибаться...), кодировки и т.п. и тупо записать все 10 тысяч значений "как есть". И это будет корректно (с точки зрения формата). А могу "заморочиться" и получится так, как описано выше. 177 | 178 | Точно также "читатель" - может использовать индексы, а может читать все подряд. Может эффективно "раскодировать" вышеизложенное, а может это делать мега неэффективно. 179 | 180 | Отсюда 181 | 182 | * бенчмарки форматов - бред: сравниваются реализации, не форматы (как таковые) 183 | * если хочется "сжать" - не уверен, что получится сжать лучше, чем ORC + ZLib 184 | * надо понять - можем ли мы (как потребители) что-то "поиметь", зная внутренности того или иного формата. 185 | -------------------------------------------------------------------------------- /spark_code/README.md: -------------------------------------------------------------------------------- 1 | В этой директории собраны некоторые примеры кода 2 | 3 | * симулятор игры "Жизнь" 4 | 5 | (будет добавляться...) 6 | -------------------------------------------------------------------------------- /spark_code/graph_life.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Life simulation with Spark and GraphFrames " 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "## Настройка" 15 | ] 16 | }, 17 | { 18 | "cell_type": "markdown", 19 | "metadata": {}, 20 | "source": [ 21 | "Настройка на Spark\n", 22 | "\n", 23 | "это можно делать множеством разных способов - например, так." 24 | ] 25 | }, 26 | { 27 | "cell_type": "code", 28 | "execution_count": 1, 29 | "metadata": {}, 30 | "outputs": [], 31 | "source": [ 32 | "import sys\n", 33 | "sys.path.append('/home/mk/local/spark-2.4.7-bin-hadoop2.7/python/lib/py4j-0.10.7-src.zip')\n", 34 | "sys.path.append('/home/mk/local/spark-2.4.7-bin-hadoop2.7/python')" 35 | ] 36 | }, 37 | { 38 | "cell_type": "markdown", 39 | "metadata": {}, 40 | "source": [ 41 | "Подключаем Graphframes..." 42 | ] 43 | }, 44 | { 45 | "cell_type": "code", 46 | "execution_count": 2, 47 | "metadata": {}, 48 | "outputs": [], 49 | "source": [ 50 | "import os\n", 51 | "os.environ[\"PYSPARK_SUBMIT_ARGS\"] = \"--packages graphframes:graphframes:0.8.1-spark2.4-s_2.11 pyspark-shell\"" 52 | ] 53 | }, 54 | { 55 | "cell_type": "code", 56 | "execution_count": 3, 57 | "metadata": {}, 58 | "outputs": [], 59 | "source": [ 60 | "from pyspark.sql import SparkSession\n", 61 | "import pyspark.sql.functions as f" 62 | ] 63 | }, 64 | { 65 | "cell_type": "code", 66 | "execution_count": 4, 67 | "metadata": {}, 68 | "outputs": [], 69 | "source": [ 70 | "spark = SparkSession.builder.master(\"local\").appName(\"life_simu\").getOrCreate()" 71 | ] 72 | }, 73 | { 74 | "cell_type": "code", 75 | "execution_count": null, 76 | "metadata": {}, 77 | "outputs": [], 78 | "source": [ 79 | "spark.sparkContext.setCheckpointDir('graphframes_cps')" 80 | ] 81 | }, 82 | { 83 | "cell_type": "code", 84 | "execution_count": 5, 85 | "metadata": {}, 86 | "outputs": [], 87 | "source": [ 88 | "from graphframes import GraphFrame\n", 89 | "from graphframes.lib import AggregateMessages as AM" 90 | ] 91 | }, 92 | { 93 | "cell_type": "markdown", 94 | "metadata": {}, 95 | "source": [ 96 | "## Функции" 97 | ] 98 | }, 99 | { 100 | "cell_type": "markdown", 101 | "metadata": {}, 102 | "source": [ 103 | "Создадим немного функций (борьба со сложностью)...." 104 | ] 105 | }, 106 | { 107 | "cell_type": "code", 108 | "execution_count": 6, 109 | "metadata": {}, 110 | "outputs": [], 111 | "source": [ 112 | "def creDeltaDf():\n", 113 | " \"\"\" создает специальный датафрейм для ребер и соседей \"\"\"\n", 114 | " \n", 115 | " return spark.createDataFrame(\n", 116 | " [\n", 117 | " [ -1, -1 ],\n", 118 | " [ -1, 0 ],\n", 119 | " [ -1, 1 ],\n", 120 | " [ 0, 1 ],\n", 121 | " [ 1, 1 ],\n", 122 | " [ 1, 0 ],\n", 123 | " [ 1, -1 ],\n", 124 | " [ 0, -1 ],\n", 125 | " ],\n", 126 | " [\"dx\", \"dy\"]\n", 127 | " )" 128 | ] 129 | }, 130 | { 131 | "cell_type": "code", 132 | "execution_count": 116, 133 | "metadata": {}, 134 | "outputs": [], 135 | "source": [ 136 | "def creSurrDF(df):\n", 137 | " \"\"\" возвращает датафрейм с колонками nv,nx,ny, содержащими координаты окружающих исходный \n", 138 | " датафрейм точками\n", 139 | " \"\"\"\n", 140 | "\n", 141 | " delt = creDeltaDf()\n", 142 | " \n", 143 | " res = (\n", 144 | " df.crossJoin(delt) # добавляем для каждой точки ее окружение\n", 145 | " .selectExpr(\"0 as nv\", \"x+dx as nx\", \"y+dy as ny\") # вычисляем координаты точек окружения\n", 146 | " .distinct() # убираем дубликаты\n", 147 | " .join( # убираем новые точки, которые попали на существующие точки\n", 148 | " df,(df[\"x\"]==f.col(\"nx\")) & (df[\"y\"]==f.col(\"ny\")),\"left_anti\"\n", 149 | " )\n", 150 | " )\n", 151 | " return df.union(res) # конкатенируем исходный и окружающий датафреймы" 152 | ] 153 | }, 154 | { 155 | "cell_type": "code", 156 | "execution_count": 59, 157 | "metadata": {}, 158 | "outputs": [], 159 | "source": [ 160 | "def creEdges(df):\n", 161 | " \"\"\" создает датафрейм с ребрами для графа \"окружаюещего датафрейма\" \n", 162 | " ребра соединяют каждую ячейку с 8 соседями (граничные - только с существующими)\n", 163 | " \"\"\"\n", 164 | " \n", 165 | " # выражения, определяющие - попала ли новая вершина в наш граф\n", 166 | " rx = ((f.col(\"x\")+f.col(\"dx\"))<=f.col(\"maxx\"))&(f.col(\"minx\")<=(f.col(\"x\")+f.col(\"dx\")))\n", 167 | " ry = ((f.col(\"y\")+f.col(\"dy\"))<=f.col(\"maxy\"))&(f.col(\"miny\")<=(f.col(\"y\")+f.col(\"dy\")))\n", 168 | " \n", 169 | " # получение id для dst вершины\n", 170 | " dstCol = f.concat_ws(\":\",(f.col(\"x\")+f.col(\"dx\")).cast(\"string\"),(f.col(\"y\")+f.col(\"dy\")).cast(\"string\"))\n", 171 | " \n", 172 | " # добавим к каждой строке колонки макс и мин координат\n", 173 | " dfn = df.crossJoin(df.selectExpr(\"max(x) as maxx\",\"min(x) as minx\",\"max(y) as maxy\",\"min(y) as miny\"))\n", 174 | "\n", 175 | " # создаем дуги - формируем id вершин как строку \"x:y\"\n", 176 | " delt = creDeltaDf()\n", 177 | " eDf = (\n", 178 | " dfn.crossJoin(delt)\n", 179 | " .withColumn(\"src\",f.concat_ws(\":\",f.col(\"x\").cast(\"string\"),f.col(\"y\").cast(\"string\")))\n", 180 | " .withColumn(\"dst\",f.when(rx & ry,dstCol))\n", 181 | " .filter(\"dst is not NULL\")\n", 182 | " .select(\"src\",\"dst\")\n", 183 | " )\n", 184 | " return eDf" 185 | ] 186 | }, 187 | { 188 | "cell_type": "code", 189 | "execution_count": 9, 190 | "metadata": {}, 191 | "outputs": [], 192 | "source": [ 193 | "def creVertices(df):\n", 194 | " \"\"\" создает вершины по датафрейму с координатами точек \n", 195 | " вершина получает id = строке \"x:y\"\n", 196 | " \"\"\"\n", 197 | " \n", 198 | " vDf = (\n", 199 | " df\n", 200 | " .withColumn(\"id\",f.concat_ws(\":\",f.col(\"x\").cast(\"string\"),f.col(\"y\").cast(\"string\")))\n", 201 | " .select(\"id\",\"v\")\n", 202 | " )\n", 203 | " return vDf" 204 | ] 205 | }, 206 | { 207 | "cell_type": "code", 208 | "execution_count": 10, 209 | "metadata": {}, 210 | "outputs": [], 211 | "source": [ 212 | "def sendMsg(gr):\n", 213 | " \"\"\" рассылает сообщения от живых клеток, возвращает датафрейм (id,totn) \"\"\"\n", 214 | " res = (\n", 215 | " gr.aggregateMessages(\n", 216 | " f.sum(\"msg\").alias(\"totn\"),\n", 217 | " None,\n", 218 | " f.when(AM.src[\"v\"]==1,f.lit(1)).otherwise(f.lit(0)) \n", 219 | " )\n", 220 | " )\n", 221 | " return res" 222 | ] 223 | }, 224 | { 225 | "cell_type": "code", 226 | "execution_count": 92, 227 | "metadata": {}, 228 | "outputs": [], 229 | "source": [ 230 | "def creNewDF(df,gr):\n", 231 | " \"\"\" создает новый датафрейм с выжившими ячейками \"\"\"\n", 232 | "\n", 233 | " # правила \"выживания\" для заполненной и пустой ячеек\n", 234 | " eRule = ((f.col(\"totn\")==2)|(f.col(\"totn\")==3))\n", 235 | " nRule = (f.col(\"totn\")==3)\n", 236 | " \n", 237 | " # формирование новой ячейки по правилам выживания\n", 238 | " newCell = f.when(f.col(\"v\")==1,f.when(eRule,f.lit(1)).otherwise(f.lit(0))).otherwise(f.when(nRule,f.lit(1)).otherwise(f.lit(0)))\n", 239 | " \n", 240 | " res = (\n", 241 | " df\n", 242 | " .join(gr.vertices,\"id\")\n", 243 | " .withColumn(\"nv\", newCell)\n", 244 | " .filter(\"nv=1\")\n", 245 | " .select(\"id\",\"nv\")\n", 246 | " .withColumn(\"x\",f.split(f.col(\"id\"),\":\").getItem(0).cast(\"int\"))\n", 247 | " .withColumn(\"y\",f.split(f.col(\"id\"),\":\").getItem(1).cast(\"int\"))\n", 248 | " .select(\"nv\",\"x\",\"y\")\n", 249 | " .withColumnRenamed(\"nv\",\"v\")\n", 250 | " )\n", 251 | " return res" 252 | ] 253 | }, 254 | { 255 | "cell_type": "code", 256 | "execution_count": 12, 257 | "metadata": {}, 258 | "outputs": [], 259 | "source": [ 260 | "def printField(df):\n", 261 | " \"\"\" печатает поле, заданное в датафрейме \"\"\"\n", 262 | " \n", 263 | " # коллектим диапазоны и координаты точек\n", 264 | " maxx,minx,maxy,miny = df.selectExpr(\"max(x)\",\"min(x)\",\"max(y)\",\"min(y)\").collect()[0]\n", 265 | " res = df.collect()\n", 266 | "\n", 267 | " # создаем пустое поле\n", 268 | " rows = []\n", 269 | " for i in range(maxy-miny+1):\n", 270 | " rows.append([None]*(maxx-minx+1))\n", 271 | " \n", 272 | " # заполняем точками (пустая=*, заполненная=-)\n", 273 | " for r in res:\n", 274 | " rows[r[2]-miny][r[1]-minx] = \"*\" if r[0]==1 else \"-\"\n", 275 | "\n", 276 | " # печатаем, добавляя отсутствующие в датафрейме ячейки в виде .\n", 277 | " for r in rows:\n", 278 | " pLine = []\n", 279 | " for el in r:\n", 280 | " if el is None:\n", 281 | " pLine.append(\".\")\n", 282 | " else:\n", 283 | " pLine.append(el)\n", 284 | " print(\"\".join(pLine))" 285 | ] 286 | }, 287 | { 288 | "cell_type": "markdown", 289 | "metadata": {}, 290 | "source": [ 291 | "## Начальные данные" 292 | ] 293 | }, 294 | { 295 | "cell_type": "markdown", 296 | "metadata": {}, 297 | "source": [ 298 | "Несколько примеров начальных колоний клеток (для отладки и вообще)" 299 | ] 300 | }, 301 | { 302 | "cell_type": "code", 303 | "execution_count": 50, 304 | "metadata": {}, 305 | "outputs": [], 306 | "source": [ 307 | "# пульсар с самого начала\n", 308 | "dfo = spark.createDataFrame(\n", 309 | " [\n", 310 | " [ 1, 0, 0 ],\n", 311 | " [ 1, 0, 1 ],\n", 312 | " [ 1, 0, 2 ],\n", 313 | " ],\n", 314 | " [\"v\",\"x\",\"y\"]\n", 315 | ")" 316 | ] 317 | }, 318 | { 319 | "cell_type": "code", 320 | "execution_count": 118, 321 | "metadata": {}, 322 | "outputs": [], 323 | "source": [ 324 | "# пульсар через небольшое количесто итераций\n", 325 | "dfo = spark.createDataFrame(\n", 326 | " [\n", 327 | " [ 1, 1, 2 ],\n", 328 | " [ 1, 2, 3 ],\n", 329 | " [ 1, 2, 2 ],\n", 330 | " [ 1, 2, 1 ],\n", 331 | " [ 1, 3, 2 ],\n", 332 | " ],\n", 333 | " [\"v\",\"x\",\"y\"]\n", 334 | ")" 335 | ] 336 | }, 337 | { 338 | "cell_type": "markdown", 339 | "metadata": {}, 340 | "source": [ 341 | "## Код: его исполняем" 342 | ] 343 | }, 344 | { 345 | "cell_type": "markdown", 346 | "metadata": {}, 347 | "source": [ 348 | "Итерация \"жизни\"" 349 | ] 350 | }, 351 | { 352 | "cell_type": "code", 353 | "execution_count": 119, 354 | "metadata": {}, 355 | "outputs": [], 356 | "source": [ 357 | "dfo.write.format(\"orc\").mode(\"overwrite\").save(\"curdf\")" 358 | ] 359 | }, 360 | { 361 | "cell_type": "markdown", 362 | "metadata": {}, 363 | "source": [ 364 | "Ячейку ниже нужно выполнять - каждое выполнение = очередная итерация \"жизни\"" 365 | ] 366 | }, 367 | { 368 | "cell_type": "code", 369 | "execution_count": 122, 370 | "metadata": {}, 371 | "outputs": [ 372 | { 373 | "name": "stdout", 374 | "output_type": "stream", 375 | "text": [ 376 | "..---..\n", 377 | ".--*--.\n", 378 | "--*-*--\n", 379 | "-*---*-\n", 380 | "--*-*--\n", 381 | ".--*--.\n", 382 | "..---..\n" 383 | ] 384 | } 385 | ], 386 | "source": [ 387 | "cDf = spark.read.format(\"orc\").load(\"curdf\")\n", 388 | "surDf = creSurrDF(cDf) # окружаем колонию пустыми клетками\n", 389 | "surDf.cache()\n", 390 | "printField(surDf)\n", 391 | "\n", 392 | "# создаем граф (т.е. ребра и вершины) включая пустые клетки\n", 393 | "eDf = creEdges(surDf)\n", 394 | "vDf = creVertices(surDf)\n", 395 | "nGr = GraphFrame(vDf,eDf)\n", 396 | "\n", 397 | "resDf = sendMsg(nGr) # рассылаем сообщения из \"живых\" клеток\n", 398 | "nDf = creNewDF(resDf,nGr) # получаем итоговый датафрейм для следующей итерации\n", 399 | "nDf.write.format(\"orc\").mode(\"overwrite\").save(\"curdf\")\n", 400 | "\n", 401 | "none = surDf.unpersist()" 402 | ] 403 | }, 404 | { 405 | "cell_type": "code", 406 | "execution_count": 123, 407 | "metadata": {}, 408 | "outputs": [], 409 | "source": [ 410 | "spark.stop()" 411 | ] 412 | } 413 | ], 414 | "metadata": { 415 | "kernelspec": { 416 | "display_name": "Python 3", 417 | "language": "python", 418 | "name": "python3" 419 | }, 420 | "language_info": { 421 | "codemirror_mode": { 422 | "name": "ipython", 423 | "version": 3 424 | }, 425 | "file_extension": ".py", 426 | "mimetype": "text/x-python", 427 | "name": "python", 428 | "nbconvert_exporter": "python", 429 | "pygments_lexer": "ipython3", 430 | "version": "3.7.9" 431 | } 432 | }, 433 | "nbformat": 4, 434 | "nbformat_minor": 4 435 | } 436 | --------------------------------------------------------------------------------