1. Большие данные – определение и причины возникновения задач обработки больших данных.

	Определение больших данных
	Большие данные (Big Data) – это массивы данных, которые характеризуются высокими объемами, скоростью поступления и разнообразием. Основные характеристики больших данных представлены через концепцию 4V:
	1. Объем (Volume) – значительный рост объемов данных (включая экзабайты и зеттабайты).
	2. Скорость (Velocity) – высокая скорость генерации данных.
	3. Разнообразие (Variety) – данные поступают в структурированном, слабо структурированном и неструктурированном виде.
	4. Достоверность (Veracity) – важность качества и точности данных для анализа.

	Причины возникновения задач обработки больших данных
	1. Рост объемов данных. Нелинейный рост объемов данных вызван развитием цифровых технологий и увеличением использования интернета, социальных сетей, мобильных устройств и IoT.
	2. Синергия данных. Современные IT-системы собирают и концентрируют данные в рамках одной системы, создавая единые платформы для анализа.
	3. Доминация неструктурированных данных. Доля структурированных данных уменьшилась, а слабо структурированные (например, текстовые, мультимедийные) и неструктурированные данные (например, видео, лог-файлы) стали преобладать.
	4. Разнообразие источников. Источники данных включают IoT, мобильные устройства, соцсети, веб-сервисы, что повышает сложность интеграции и анализа.

2. Специфика современного аппаратного обеспечения для обработки больших данных и проблема масштабируемости параллельных вычислений.

	Современное аппаратное обеспечение для обработки больших данных
	1. Процессоры и кластеры. Рост вычислительных возможностей процессоров достиг предела в 2005 году, что привело к переходу от одноядерных к многоядерным архитектурам и использованию кластерных систем.
	2. Распределенные вычисления. Большие данные требуют масштабирования через распределенные системы, например, использование систем с массовым параллелизмом (Hadoop, Spark).
	3. NoSQL-системы. Для работы с большими объемами слабоструктурированных данных применяются системы хранения данных, такие как NoSQL, ориентированные на высокую производительность и масштабируемость.

	Проблема масштабируемости параллельных вычислений
	1. Закон Амдала. Ускорение выполнения задач при добавлении вычислительных узлов ограничено долей последовательных операций в программе, что усложняет масштабируемость.
	2. Узкие места аппаратного обеспечения. Ограничения по пропускной способности сети, задержки доступа к памяти и высокая стоимость синхронизации данных между узлами создают барьеры для масштабирования.
	3. Требования к программированию. Массовый параллелизм требует изменений в подходах к разработке программного обеспечения, включая алгоритмы и архитектуры, способные эффективно распределять нагрузку.

	Решение проблемы масштабируемости:

	1. Распределенные системы: Использование таких технологий, как Hadoop и Apache Spark, для разделения задач на мелкие части и параллельного выполнения.  
	2. Оптимизация алгоритмов: Уменьшение объема последовательных операций и минимизация синхронизации между узлами.  
	3. Горизонтальное масштабирование: Добавление новых узлов для увеличения мощности, вместо повышения характеристик отдельных серверов.  
	4. Распределенные файловые системы: Хранение данных в HDFS и других системах, поддерживающих высокую доступность и отказоустойчивость.  
	5. Облачные технологии: Использование облачных платформ для гибкого управления вычислительными ресурсами.  

3. Выбор типичных средств обработки данных, адекватных различным объемам данных; принцип обработки данных на базе операций map / filter / reduce.

	1. Малые объемы данных:  
	   - Реляционные СУБД: Используются традиционные реляционные базы данных (например, MySQL, PostgreSQL, Microsoft SQL Server).  
	   - Подход основан на ACID-принципах (атомарность, согласованность, изолированность, долговечность).  
	   - Применяется нормализация данных и стандартизованные языки запросов (SQL).  

	2. Средние объемы данных:  
	   - NoSQL базы данных: Подходы с высокой производительностью и масштабируемостью, например:  
	     - Key-Value базы (Redis, DynamoDB) для простых операций с большим количеством записей.  
	     - Документо-ориентированные базы (MongoDB, CouchDB) для работы с полуструктурированными данными.  
	     - Колонковые базы данных (Cassandra, HBase) для аналитических запросов.  
	   - Эти системы обеспечивают гибкость в работе с большими объемами разнородных данных.  

	3. Большие объемы данных (Big Data):  
	   - Распределенные файловые системы: HDFS, позволяющая хранить большие массивы данных, распределенных между узлами.  
	   - Распределенные вычисления: Платформы, такие как Hadoop (MapReduce) и Apache Spark, обеспечивают параллельную обработку данных.  
	   - Инструменты потоковой обработки: Apache Kafka, Apache Flink, которые подходят для данных в реальном времени.  

	4. Очень большие объемы данных (экзабайты и выше):  
	   - Интеграция облачных технологий, таких как AWS, Google Cloud, Microsoft Azure, с их распределенными хранилищами и вычислительными возможностями.  
	   - Использование специализированных систем, таких как Snowflake, для обработки данных масштаба предприятия.  

	Принцип обработки данных на базе операций Map / Filter / Reduce

	1. Операция `Map`:  
	Операция Map применяется для трансформации каждого элемента входного набора данных с использованием указанной функции. Результатом является новый набор данных с преобразованными элементами.

	Пример (Python):  
	```python
	data = [1, 2, 3, 4]
	result = list(map(lambda x: x**2, data))
	print(result)  # Результат: [1, 4, 9, 16]
	```

	Применение в больших данных:  
	- Преобразование данных, таких как извлечение определенного поля из записей или создание промежуточных пар ключ-значение (например, `(слово, 1)` для подсчета слов).

	---

	2. Операция `Filter`:  
	Операция Filter используется для отбора элементов, удовлетворяющих заданному условию. Только те элементы, для которых функция-предикат возвращает `True`, включаются в результат.

	Пример (Python):  
	```python
	data = [1, 2, 3, 4, 5]
	result = list(filter(lambda x: x % 2 == 0, data))
	print(result)  # Результат: [2, 4]
	```

	Применение в больших данных:  
	- Очистка данных (например, удаление некорректных записей или фильтрация по категории).  

	---

	3. Операция `Reduce`:  
	Операция Reduce сводит весь набор данных к одному значению с использованием заданной функции. Функция агрегации применяется к элементам набора последовательно.

	Пример (Python):  
	```python
	from functools import reduce
	data = [1, 2, 3, 4]
	result = reduce(lambda x, y: x + y, data)
	print(result)  # Результат: 10
	```

	Применение в больших данных:  
	- Суммирование, вычисление среднего значения, подсчет итоговых результатов (например, общего количества встреч слова).  

	Как работает в контексте MapReduce:  
	1. Данные разбиваются на части и распределяются между узлами кластера.  
	2. На каждом узле выполняется операция `Map`, преобразующая локальные данные.  
	3. После завершения Map происходит группировка данных по ключам.  
	4. Операция `Reduce` агрегирует данные с одинаковыми ключами, создавая итоговые результаты.  

	Преимущества подхода Map / Filter / Reduce:  
	- Простота реализации параллельных вычислений.  
	- Высокая масштабируемость за счет распараллеливания.  
	- Отказоустойчивость через репликацию данных.  
	- Универсальность: применимо для текстового анализа, обработки логов, машинного обучения и других задач.

4. Многопроцессорные архитектуры с общей и разделяемой памятью – специфика и сравнение.
	Архитектура с общей памятью

	Принципы многопроцессорной архитектуры с общей памятью (shared memory):
	- несколько процессоров работают независимо, но совместно используют общую память
	- изменения в памяти, осуществляемые одним процессором, видны всем другим процессорам

	Типы реализации архитектуры с общей памятью:
	- однородный доступ к памяти (Uniform Memory Access, UMA) – равный (в том числе по времени) доступ всех процессоров ко всем областям основной памяти (процессоры могут иметь свой когерентный кэш). Типично для архитектуры с симметричной мультипроцессорностью (Symmetric Multiprocessing, SMP).
	- неоднородный доступ к памяти (Non-Uniform Memory Access, NUMA) – время доступа к памяти определяется её расположением по отношению к процессору. Обычно реализуется как соединение нескольких SMP узлов.

	Преимущества и недостатки:
	Преимущества:
	- Привычная модель программирования за счёт единого адресного пространства
	- Высокая скорость и низкая латентность обмена данными между параллельными задачами

	Недостатки:
	- Низкая масштабируемость (обычно до 16 процессоров) из-за геометрического роста нагрузки на шину CPU-RAM
	- Проблема поддержания когерентности кэшей
	- Трудоёмкая организация эффективного использования памяти в NUMA-системах
	- Необходимость синхронизации при доступе к общим данным (критические секции)

	Архитектура с распределённой памятью и гибридная архитектура

	Принципы многопроцессорной архитектуры с распределённой памятью (distributed memory):
	- несколько процессоров работают с собственной памятью, недоступной напрямую для других процессоров (отсутствует общая адресация памяти)
	- обмен данными между процессорами производится через коммуникационную сеть и явно определяется исполняемой программой

	Реализации архитектуры с распределённой памятью:
	- возможно большое количество вариантов организации коммуникационной сети между узлами архитектуры с распределённой памятью
	- на практике часто узлами систем с распределённой памятью являются многопроцессорные узлы с общей памятью (гибридная архитектура)

	Преимущества и недостатки:
	Преимущества:
	- Высокая масштабируемость
	- Объём памяти растёт пропорционально количеству ядер
	- Возможность использовать недорогие массовые компоненты

	Недостатки:
	- Специальные подходы к программированию: необходимость использования передачи сообщений (message passing)
	- Сложность реализации некоторых структур данных и алгоритмов
	- Высокая латентность и низкая скорость обмена данными между узлами
	- Неоднородность, отказы узлов

	Гибридная архитектура

	Гибридная архитектура сочетает в себе элементы архитектур с общей и распределённой памятью, обеспечивая баланс между масштабируемостью и эффективностью обмена данными.

5. Подходы к декомпозиции крупных вычислительных задач на подзадачи для параллельного исполнения.
	Параллельное программирование требует:
	- Выделения подзадач, которые могут выполняться параллельно.
	- Определения данных, которые должны разделяться/пересылаться между подзадачами.
	- Синхронизации подзадач.

	В зависимости от задач можно проводить крупнозернистую и мелкозернистую декомпозицию:
	- Крупнозернистая – задача разбивается на небольшое количество крупных блоков.
	  - Преимущества: снижается обмен информацией и затраты на синхронизацию.
	  - Недостатки: несбалансированность нагрузки, ограничение по степени параллелизма.
	- Мелкозернистая – задача разбивается на большое количество небольших блоков.
	  - Выявление мелкозернистой декомпозиции часто выполняется компилятором.

	Подходы к декомпозиции на подзадачи

	1. Функциональная декомпозиция (Task/Functional decomposition):
	   - Распределение вычислений по подзадачам.

	2. Декомпозиция по данным (Domain/Data decomposition):
	   - Распределение данных по подзадачам.
	   - Преимущества: высокая масштабируемость (многие тысячи ядер), возможность использовать недорогие массовые компоненты (CPU, RAM, сети).

	3. Конвейерная обработка данных:
	   - Регулярный поток блоков данных, каждый из которых проходит несколько стадий обработки, выполняемых на этапах конвейера.

	4. Геометрическая декомпозиция:
	   - Данные задачи разбиваются на области (желательно равного размера) по "геометрическому" принципу (например, n-мерная решетка с регулярным шагом).
	   - Каждая область данных обрабатывается отдельным обработчиком, который при необходимости обменивается данными с обработчиками соседних областей.

	5. Рекурсивный параллелизм (разделяй и властвуй) (Divide and Conquer):
	   - Операции Split и Merge могут стать узким местом, так как выполняются последовательно.
	   - Задания порождаются динамически, обеспечивая балансировку загрузки потоков.
	   - Степень параллелизма изменяется в ходе выполнения алгоритма.
	   
6. Модели параллельного программирования и их сочетаемость с архитектурами параллельных вычислительных систем.
	Разделяемая память (shared memory):
	- Аналогия: доска объявлений.
	- Подзадачи используют общее адресное пространство (оперативной памяти).
	- Подзадачи взаимодействуют асинхронно, читая и записывая информацию в общем пространстве.
	- Реализация: многопоточные приложения, OpenMP.

	Передача сообщений (message passing):
	- Аналогия: отправка писем с явным указанием отправителя и получателя.
	- Каждая подзадача работает с собственными локальными данными.
	- Подзадачи взаимодействуют за счет обмена сообщениями.
	- Реализация: MPI (message passing interface).

	Параллельная обработка данных (data parallelization):
	- Строго описанные глобальные операции над данными.
	- Может обозначаться как чрезвычайная параллельность (embarrassingly parallel) – очень хорошо распараллеливаемые вычисления.
	- Обычно данные равномерно разделяются по подзадачам.
	- Подзадачи выполняются как набор независимых операций.
	- Реализация может быть сделана как с помощью разделяемой памяти, так и с помощью передачи сообщений.

	Модель параллельного программирования на основе передачи сообщений:

	Основные характеристики модели на основе передачи сообщений:
	- Набор задач, имеющих свою собственную локальную память во время вычислений.
	- Задачи могут находиться как на одной машине (в том числе с разделяемой памятью), так и на разных машинах.
	- Задачи обмениваются данными с помощью отсылки и приема сообщений, явно описываемых в программном коде.
	- Передача данных подразумевает их сериализацию/десериализацию, что требует накладных расходов.
	- Передача данных требует совместной работы задачи-отправителя и задачи-получателя.

	Программирование для модели на основе передачи сообщений:
	- Модель выглядит как внедрение вызовов специализированной библиотеки в программный код.
	- За реализацию параллелизма отвечает программист, а не компилятор.
	- Общепринятый стандарт для модели передачи сообщений – библиотека MPI (Message Passing Interface).

	- При отправке определяется процесс-отправитель и буфер, содержащий передаваемую информацию.
	- При получении определяется процесс-получатель и место для сохранения результата.
	- Возможны дополнительные атрибуты, обуславливающие передачу сообщения.
	- Передача сообщения несет значительные накладные расходы.
	- Возможны реализации не двухточечной коммуникации с использованием дополнительных структур данных, например, очередей сообщений.

	Модель параллельного программирования на основе параллельной обработки данных:

	Основные характеристики:
	- Основные параллельные задачи сфокусированы на выполнении операций над массивом данных.
	- Массив данных обычно организован в виде однородной структуры, например, массива или гиперкуба.
	- Задачи выполняют аналогичные операции над выделенными им фрагментами массива данных.
	- В архитектурах без разделяемой памяти массив данных делится на фрагменты, находящиеся в распоряжении отдельных задач.
	- Программирование включает написание программы, использующей конструкции для параллельной обработки данных, например, вызовы специализированной библиотеки.

7. Профилирование реализации алгоритмов на Python, принципы решения задачи оптимизации производительности алгоритма 
	Профилирование

	Профилирование — сбор характеристик работы программы, таких как:
	- Время выполнения отдельных фрагментов (например, функций).
	- Число верно предсказанных условных переходов.
	- Число кэш-промахов.
	- Объем используемой оперативной памяти.
	- И другие параметры.

	Инструмент, используемый для анализа работы, называют профайлером (profiler). Обычно профилирование выполняется в процессе оптимизации программы.

	Магические функции IPython для профилирования:
	- `%time` - длительность выполнения отдельного оператора.
	- `%timeit` - длительность выполнения отдельного оператора при неоднократном повторе (может использоваться для обеспечения большей точности оценки).
	- `%prun` - выполнение кода с использованием профилировщика.
	- `%lprun` - пошаговое выполнение кода с применением профилировщика.
	- `%memit` - оценка использования оперативной памяти для отдельного оператора.
	- `%mprun` - пошаговое выполнение кода с применением профилировщика памяти.

	Для работы с одной строкой кода используются строчные магические команды (например, `%time`), для работы с целой ячейкой их блочные аналоги (например, `%%time`).

	Профилирование помогает определить "бутылочные горлышки" — участки кода, которые потребляют наибольшее количество времени или ресурсов, и которые необходимо оптимизировать.

	Принципы оптимизации производительности алгоритма:
	1. Идентификация узких мест: Использование профилировщиков для определения частей кода, требующих оптимизации.
	2. Анализ алгоритмов: Оценка временной и пространственной сложности используемых алгоритмов и поиск более эффективных альтернатив.
	3. Оптимизация кода:
	   - Улучшение структуры кода для уменьшения количества вычислений.
	   - Использование более эффективных структур данных.
	   - Минимизация количества вызовов ресурсоемких функций.
	4. Использование специализированных инструментов и библиотек:
	   - Numba — JIT-компилятор для ускорения численных вычислений. JIT-компиляция (Just-in-time compilation, компиляция «на лету») — динамическая компиляция, увеличивающая производительность программных систем, использующих байт-код, путём компиляции байт-кода в машинный код непосредственно во время работы программы.
	   - Векторизация — применение операций над массивами данных вместо использования циклов.
	5. Переоценка подходов: Иногда изменение общего подхода к решению задачи может привести к значительным улучшениям производительности.
	6. Тестирование и повторное профилирование: После внесения изменений необходимо повторно профилировать программу, чтобы оценить эффективность оптимизаций и выявить новые узкие места.
	
8. Проблема Global Interpreter Lock в Python и способы обхода ее ограничений.
	Проблема Global Interpreter Lock (GIL)

	Global Interpreter Lock – способ синхронизации потоков используемый в рефернсной реализации Python (CPython) и в реализациях некоторых других интерпретируемых языков программирования.

	Интерпретатор CPython НЕ является потоково-безопасным т.к. некоторые ключевые структуры данных могут быть одновременно доступны только одному потку.
	GIL является самым простым и быстрым при исполнении однопоточных приложений способом обеспечения потоковой безопасности при одновременном обращении разных потоков к одним и тем же участкам памяти.
	Наличие GIL не является требованием языка программирования Python, а только спецификой реализации самого популярного интерпретатора CPython, существуют другие интерпретаторы Python не имеющие GIL.
	Для обхода проблемы GIL для реализации параллельных вычислений в Python вместо многопоточного подхода с разделяемой памятью используется более тяжеловесная конструкция:

	- Множество процессов, в каждом из которых работает собственный интерпретатор с собственным GIL и имеется собственная копия данных и кода
	- Обмен данными между процессами обычно производится не через разделяемую память (это иногда возможно, но чревато ошибками), а через передачу данных и кода с помощью сериализации
	По сути это вариация на тему модели параллельного программирования на основе передачи сообщений (реализуемой в т.ч. при вычислении на компьютере с разделяемой памятью)

9. Модуль multiprocessing – назначение и основные возможности, API multiprocessing.Pool.
	Модуль multiprocessing включен в стандартную библиотеку Python. Он позволяет организовывать параллельные вычисления, избегая ограничений, связанных с Global Interpreter Lock (GIL) в CPython. Основная идея модуля заключается в создании множества процессов, где каждый процесс работает со своим интерпретатором Python, собственной копией кода и данных. Каждый процесс имеет собственное адресное пространство памяти.
	Класс Pool:
	- Используется для упрощения работы с большим количеством процессов.
	- Предоставляет следующие методы:
	- `apply(func, args)`: Выполняет функцию `func` с аргументами `args`.
	- `map(func, iterable)`: Применяет функцию `func` к каждому элементу из `iterable`.
	- `apply_async(func, args)`: Асинхронный вариант метода `apply`.
	- `map_async(func, iterable)`: Асинхронный вариант метода `map`.
	`Pool.map` и `Pool.apply` блокируют основную программу до тех пор, пока все процессы не будут завершены, что очень полезно, если мы хотим получить результаты в определенном порядке для определенных приложений (потребителей).
	Напротив, варианты `async` стартуют все процессы сразу и получат результаты, как только они будут готовы. Еще одно отличие состоит в том, что нам нужно использовать метод `get` после вызова `apply_async()`, чтобы получить возвращаемые значения завершенных процессов. 
	
	%%file cube_.py

	def cube(x):
	    return x**3
	--Overwriting cube_.py

	import cube_
	pool = mp.Pool(processes=4)
	results = [pool.apply(cube_.cube, args=(x,)) for x in range(1,7)]
	print(results)
	--[1, 8, 27, 64, 125, 216]

	pool = mp.Pool(processes=4)
	results = pool.map(cube_.cube, range(1,7))
	print(type(results[0]))
	print(results)
	--<class 'int'>
	--[1, 8, 27, 64, 125, 216]

	pool = mp.Pool(processes=4)
	results = [pool.apply_async(cube_.cube, args=(x,)) for x in range(1,7)]
	print(type(results[0]))
	output = [p.get() for p in results]
	print(output)
	--<class 'multiprocessing.pool.ApplyResult'>
	--[1, 8, 27, 64, 125, 216]
	
10. Различия между потоками и процессами, различие между различными планировщиками в Dask.
	Процесс (process)
	- Полноценная программа (использует большое количество системных ресурсов)
	- Разные процессы имеют изолированное адресное пространство
	- Процессы взаимодействуют через системные механизмы межпроцессной коммуникации

	Поток (thread)
	- Часть процесса (создается существенно проще)
	- Потоки одного процесса разделяют общую память (и другие ресурсы)
	- Потоки взаимодействуют через разделяемую память.

	Dask предоставляет несколько вариантов планировщиков для выполнения задач, каждый из которых имеет свои особенности и предназначение. Вот основные различия между ними:

	1. Single-Threaded ("synchronous") Scheduler:
	   - Выполняет задачи последовательно в одном потоке.
	   - Используется для отладки и тестирования, так как позволяет легко отслеживать выполнение.
	   - Подходит для простых или небольших задач, где многопоточность не требуется.
	   - Пример вызова: dask.compute(scheduler='synchronous').

	2. Threaded Scheduler:
	   - Использует пул потоков для выполнения задач.
	   - Подходит для задач, которые ограничены ожиданием ввода-вывода (I/O-bound), например, загрузка данных из сети или файловой системы.
	   - Неэффективен для задач, интенсивно использующих CPU, из-за глобальной блокировки интерпретатора (GIL) в Python.
	   - Пример вызова: dask.compute(scheduler='threads').

	3. Multiprocessing Scheduler:
	   - Использует процессы вместо потоков для выполнения задач.
	   - Подходит для вычислительно интенсивных задач (CPU-bound), поскольку процессы работают независимо и не подвержены GIL.
	   - Может вызывать накладные расходы на межпроцессное взаимодействие.
	   - Пример вызова: dask.compute(scheduler='processes').

	4. Distributed Scheduler:
	   - Это распределённый планировщик, который позволяет выполнять задачи на кластере машин или нескольких ядрах одной машины.
	   - Поддерживает мониторинг выполнения через веб-интерфейс.
	   - Идеален для работы с большими данными и сложными задачами, требующими распределённых вычислений.
	   - Требует предварительного запуска Dask Scheduler (dask-scheduler) и Workers (dask-worker).
	   - Пример вызова: dask.compute(scheduler='distributed').

	Ключевые различия:
	- Single-threaded: для отладки и последовательного выполнения.
	- Threaded: для задач с интенсивным вводом-выводом.
	- Multiprocessing: для задач с высокой нагрузкой на процессор.
	- Distributed: для масштабируемых распределённых вычислений.

11. Граф зависимостей задач – суть структуры данных, ее построение и использование в Dask
Граф зависимостей задач (Task Dependency Graph) в Dask — это ориентированный ациклический граф (DAG), где:
- Суть структуры данных: Граф отображает взаимосвязь задач, их порядок выполнения и зависимости друг от друга. Он позволяет эффективно организовать выполнение задач, минимизируя накладные расходы.
- Узлы — отдельные задачи или операции.
- Ребра — зависимости между задачами.

Построение:
Dask автоматически строит граф, анализируя вызовы функций и операции над структурами данных (например, Dask.Array, Dask.DataFrame).

Использование:
Граф используется для:
1. Оптимального распределения задач между процессами или потоками.
2. Устранения дублирующихся вычислений.
3. Параллельного выполнения задач.

12. Три ключевые структуры данных Dask: их специфика и принцип выбора структуры данных при решении задач
1. Dask.Array:
   - Аналог NumPy, работает с многомерными массивами.
   - Делит массивы на чанки, что позволяет обрабатывать данные, которые не помещаются в память.

2. Dask.DataFrame:
   - Аналог Pandas, работает с табличными данными.
   - Делит DataFrame на чанки и применяет операции к ним параллельно.

3. Dask.Bag:
   - Для работы с несбалансированными или неструктурированными данными (списки, JSON).

Принцип выбора:
- Используйте Dask.Array, если данные представляют собой числовые массивы и необходимы численные операции или линейная алгебра.
- Dask.DataFrame подходит для табличных данных, особенно когда необходимо проводить операции, аналогичные Pandas.
- Dask.Bag применяйте для неструктурированных данных, таких как текстовые файлы или данные JSON, которые нужно преобразовать и обработать.

13. Dask.Array – структура данных, специфика реализации и применения, процедура создания
Специфика реализации:
Dask.Array работает как распределенный аналог NumPy ndarray. Вместо одного большого массива данные разбиваются на чанки — небольшие блоки, которые могут обрабатываться независимо. Эти чанки выполняются параллельно или на распределенных системах. Операции над массивами в Dask ленивы, то есть они создают граф задач, но не выполняются до вызова .compute().

Применение:
- Вычислительная математика и статистика на больших массивах.
- Работа с данными, которые превышают объем оперативной памяти.

Процедура создания:
Dask.Array можно создать из существующих массивов (например, NumPy), файлов (Zarr, HDF5) или других источников данных. При создании необходимо указать размер чанков, чтобы определить, как массив будет разделен.

14. Dask.Array – поддерживаемые операции и отличия от NumPy ndarray
Поддерживаемые операции:
- Арифметические операции: сложение, вычитание, умножение, деление.
- Статистические функции: среднее, сумма, стандартное отклонение.
- Линейная алгебра: матричные операции, сингулярное разложение.

Отличия от NumPy:
- Операции в Dask ленивы: они формируют граф задач, а не выполняются немедленно.
- Dask поддерживает работу с данными, которые не помещаются в память, разбивая их на чанки.
- Требуется вызов .compute() для выполнения операций.
- Некоторые специфические операции NumPy могут быть недоступны в Dask.

15. Распараллеливание алгоритмов с помощью dask.delayed – принцип и примеры использования
Принцип работы:
dask.delayed — это инструмент для создания ленивых задач, которые добавляются в граф зависимостей. Задачи не выполняются немедленно, вместо этого создается структура данных, описывающая порядок выполнения. Только при вызове .compute() Dask исполняет граф.

Что такое ленивые задачи?
Ленивая задача — это отложенное выполнение функции. Вместо немедленного выполнения задача записывается в граф зависимостей, позволяя Dask оптимизировать порядок выполнения и распределение ресурсов.

16. Дополнительные параметры декоратора dask.delayed – назначение и примеры использования
pure=True: Указывает, что функция детерминирована (результат зависит только от входных данных и всегда одинаков). Это позволяет Dask кэшировать результаты вызовов и избегать повторных вычислений.
pure=False: Используется для недетерминированных функций (например, функции с побочными эффектами).
name: Позволяет задать пользовательское имя для задачи в графе зависимостей, что упрощает отладку и визуализацию.

Что такое детерминированность?
Детерминированная функция — это функция, которая при одинаковых входных данных всегда возвращает один и тот же результат. Например, сложение двух чисел детерминировано, а получение текущего времени — нет.

17. Использование dask.delayed для объектов и операции над объектами dask.delayed, включая ограничения их использования
Использование:
dask.delayed позволяет работать с любыми Python-объектами, откладывая выполнение операций над ними.
Например, можно отложить выполнение сложных вычислений или операций над большими данными.

Пример работы с объектами:
1. Преобразование сложных структур данных (например, списков или словарей).
2. Обработка пользовательских классов с использованием методов, помеченных как @delayed.

Ограничения:
- Некоторые операции над объектами могут быть несовместимы с Dask, если они зависят от состояния среды.
- Ограниченная поддержка многопоточности из-за GIL (Global Interpreter Lock).
- Требуется явное управление графом задач для сложных операций.

18. Dask.DataFrame - структура данных, специфика реализации и применения, процедура создания Dask.DataFrame
Структура данных:
Dask.DataFrame — это распределенная версия Pandas DataFrame. Табличные данные делятся на чанки (по строкам), которые обрабатываются независимо. Каждый чанк — это Pandas DataFrame, и операции выполняются над ними параллельно.

Специфика реализации:
- Dask.DataFrame поддерживает большинство операций Pandas (группировка, фильтрация, объединение).
- Методы ленивы: создают граф задач, который выполняется при вызове .compute().
- Подходит для обработки данных, превышающих объем памяти.

Применение:
- Обработка больших наборов данных.
- Интеграция с источниками данных (CSV, Parquet).
- Подготовка данных для моделирования и анализа.

Ограничения:
- Не все методы Pandas поддерживаются.
- Некоторые операции (например, сортировка) могут быть менее эффективными.

19. Ограничения использования Dask.DataFrame и операции мэппинга в Dask.DataFrame
Ограничения использования Dask.DataFrame:
1. Совместимость с Pandas: Dask поддерживает только подмножество функций Pandas. Например, сложные операции с многоуровневыми индексами недоступны.
2. Объем чанков: Размеры чанков должны быть настроены так, чтобы обеспечивать баланс между эффективностью вычислений и затратами на управление задачами.
3. Сортировка: Полная сортировка данных требует значительных вычислительных ресурсов и ограничена распределенной средой.
4. Операции, требующие глобального состояния: Некоторые методы, такие как .corr() или .cov(), требуют агрегации всех данных и менее эффективны в Dask.

Операции мэппинга:
Dask позволяет применять пользовательские функции к каждому чанку данных с помощью .map_partitions(). Это удобно для трансформации данных, создания новых колонок или фильтрации.

Пример применения:
- Фильтрация: df.map_partitions(lambda df: df[df['col'] > 0])
- Создание новой колонки: df.map_partitions(lambda df: df.assign(new_col=df['col'] * 2))

20. Поддержка Dask.DataFrame операций работающих со скользящим окном
Dask поддерживает операции со скользящим окном через метод .rolling(). Это полезно для временных рядов, где необходимо вычислять агрегаты, такие как среднее или сумма, на основе фиксированного окна.

Особенности реализации:
1. Ограничение чанков: Размер окна не должен превышать размер чанка. Если окно пересекает границы чанков, данные из соседних чанков не учитываются автоматически.
2. Поддерживаемые функции: .mean(), .sum(), .min(), .max() и другие.
3. Применение:
   - Временные ряды: вычисление скользящего среднего.
   - Анализ финансовых данных: расчеты индикаторов на основе скользящего окна.

Пример использования:
rolling_avg = df.rolling(window=3).mean()
result = rolling_avg.compute()

Ограничения:
- Распределенные вычисления усложняют обработку окон, пересекающих границы чанков.
- Требуется тщательная настройка размеров окон и чанков для корректных результатов.

21. Совместное использование промежуточных результатов в Dask  
Dask оптимизирует выполнение вычислений, формируя общий граф задач (DAG), который отображает зависимости между операциями. Промежуточные результаты сохраняются в кэше, что позволяет повторно использовать их при выполнении нескольких задач, обращающихся к одним и тем же данным. Это снижает количество повторных вычислений и ускоряет обработку, так как данные не нужно считывать или вычислять заново. Например, если несколько аналитических операций требуют предварительной обработки одного и того же набора данных, этот этап выполняется только один раз и результат используется всеми операциями.

22. Dask.Bag: структура данных и создание  
Dask.Bag представляет собой структуру данных, предназначенную для обработки неструктурированных или полуструктурированных данных, аналогичную RDD в Apache Spark. Она эффективно работает с коллекциями элементов, таких как логи, JSON-файлы или текстовые данные, разбивая их на несколько партиций для параллельной обработки. Создать Dask.Bag можно с помощью методов `from_sequence`, который принимает последовательность элементов, или `read_text` для чтения данных из текстовых файлов. После создания данные распределяются по партициям, что позволяет выполнять операции параллельно на разных узлах кластера.

```
import dask.bag as db

# Создание из последовательности
b1 = db.from_sequence([1, 2, 3, 4, 5])

# Чтение из текстовых файлов
b2 = db.read_text('logs/*.txt')
```

23. Map / Filter / Reduce в Dask.Bag  
В Dask.Bag операции Map, Filter и Reduce позволяют эффективно обрабатывать большие объемы данных параллельно. Map применяет заданную функцию к каждому элементу Bag, распределяя вычисления по партициям. Filter отбирает элементы, удовлетворяющие определенному условию, тем самым уменьшая объем данных для последующей обработки. Reduce сворачивает все элементы Bag в одно итоговое значение, выполняя сначала локальную свертку в каждой партиции, а затем объединяя результаты глобально. Такая последовательность операций позволяет гибко и эффективно обрабатывать данные в распределенной среде.

24. API Dask.Bag: мэппинг, фильтрация, преобразования  
API Dask.Bag предоставляет различные функции для манипуляции данными, такие как `map(func)` для применения функции к каждому элементу, `filter(func)` для отбора элементов по условию, и `map_partitions(func)` для выполнения операций на уровне целых партиций, что может повысить эффективность. Функция `flatmap(func)` позволяет разворачивать вложенные структуры данных, превращая списки списков в один плоский список. Кроме того, `repartition(npartitions)` позволяет изменять количество партиций, что помогает балансировать нагрузку между узлами кластера и оптимизировать производительность вычислений.

25. API Dask.Bag: группировка и свертка  
Для группировки и свертки данных Dask.Bag предлагает функции `fold(binop, combine, initial)` и `groupby(keyfunc)`. Функция `fold` позволяет выполнять сложные операции свертки, используя бинарную операцию и функцию комбинирования, что особенно полезно для распределенных вычислений. `groupby` группирует элементы Bag по ключу, определенному функцией `keyfunc`, создавая пары ключ-список значений. Эти инструменты позволяют эффективно агрегировать и анализировать большие объемы данных, распределяя обработку по нескольким узлам и объединяя результаты.

26. Принципы работы Apache Hadoop  
Apache Hadoop основан на модульной архитектуре, включающей HDFS для распределенного хранения данных, YARN для управления ресурсами и MapReduce для обработки данных. HDFS разбивает большие файлы на блоки и хранит их с репликацией для обеспечения надежности. MapReduce выполняет обработку данных в два этапа: этап Map, где данные преобразуются в пары ключ-значение, и этап Reduce, где эти пары агрегируются по ключам. Hadoop подходит для обработки больших объемов данных, обеспечивая масштабируемость и отказоустойчивость, однако из-за частых операций чтения и записи на диск может быть медленнее современных решений, таких как Apache Spark.

27. Принципы работы Apache Spark  
Apache Spark использует концепцию RDD (Resilient Distributed Dataset) для представления распределенных коллекций данных, которые могут быть обработаны параллельно. Одной из ключевых особенностей Spark являются ленивые вычисления: операции над данными не выполняются сразу, а формируют DAG (Directed Acyclic Graph) задач, который затем оптимизируется и выполняется. Spark поддерживает in-memory вычисления, что значительно ускоряет обработку по сравнению с системами, основанными на дисковом хранении, такими как Hadoop MapReduce. Благодаря этим особенностям Spark обеспечивает высокую производительность и гибкость для различных типов аналитических задач и потоковых данных.

28. Сценарии использования Faiss  
Faiss — это библиотека от Facebook AI Research, предназначенная для быстрого поиска и сопоставления векторов, особенно в задачах nearest neighbors. Она широко используется для поиска похожих объектов, таких как изображения, тексты или другие виды векторных представлений, а также в системах рекомендаций. Faiss поддерживает использование GPU для ускорения вычислений и масштабируется для обработки больших коллекций векторов, что делает её подходящей для задач, связанных с machine learning и искусственным интеллектом. Кроме того, Faiss предоставляет различные алгоритмы индексирования, позволяющие балансировать между скоростью поиска и точностью результатов.

29. Сценарии использования Redis  
Redis — это высокопроизводительная in-memory база данных, поддерживающая различные структуры данных, включая строки, хэши, списки, множества и упорядоченные множества. Она широко используется для кэширования данных, что позволяет значительно ускорить доступ к часто запрашиваемой информации и снизить нагрузку на основные базы данных. Кроме того, Redis применяется для реализации очередей задач, хранения сессий пользователей в веб-приложениях и проведения аналитики в реальном времени благодаря своей высокой скорости обработки операций. Поддержка механизмов Pub/Sub, транзакций и скриптов на Lua делает Redis универсальным инструментом для построения масштабируемых и эффективных систем.