# flake8: noqa

TASKS = [
    {
        "text": "Многопроцессорные архитектуры с общей и разделяемой памятью: специфика и сравнение.",
        "solution": """\
Под многопроцессорной архитектурой с общей памятью обычно понимают SMP и близкие варианты, где все процессоры адресуют единое пространство памяти. Программы опираются на общие переменные, кэширование и когерентность кэшей. Основная ценность общей памяти состоит в простоте модели: данные доступны всем вычислительным ядрам без явного обмена сообщениями. Цена этой простоты выражается в росте накладных расходов на когерентность, в конкуренции за пропускную способность памяти и в ограниченной масштабируемости. Типичный эффект при увеличении числа ядер это деградация производительности из-за ложного совместного использования кэш-линий и из-за синхронизации.

Архитектуры с разделяемой памятью в учебной терминологии часто описывают как NUMA, где память физически распределена по узлам, но остается логически общей. Адресное пространство едино, но время доступа зависит от того, локальная память или удаленная. Это меняет стратегию оптимизации: важным становится размещение потоков и данных на одном узле, выбор политики выделения памяти и контроль миграции страниц. На NUMA хорошо видны проблемы нерегулярного доступа и пропускной способности межсоединений, при этом масштабируемость обычно выше, чем у чистого SMP на одной шине.

Для сравнения удобно выделить три оси. Первая ось это модель программирования: общая память дает проще код, NUMA требует учета локальности. Вторая ось это синхронизация: в обеих моделях нужны блокировки, атомарные операции и барьеры, но цена ошибки на NUMA выше. Третья ось это масштабируемость: общая память упирается в подсистему памяти и когерентность, NUMA упирается в межузловые соединения и размещение данных.

В контексте Python различие проявляется через библиотеки, использующие потоки и нативные вычисления, а также через распределение процессов по NUMA узлам. Для задач обработки данных критичны пропускная способность памяти, локальность и устойчивость к contention, поэтому практическая оценка обязана опираться на профили доступа к памяти и на измерения на целевой машине.
""",
    },
    {
        "text": "Подходы к декомпозиции крупных вычислительных задач на подзадачи для параллельного исполнения.",
        "solution": """\
Декомпозиция крупных вычислений начинается с ответа на вопрос, что считается независимой единицей работы, и где проходят границы данных. На практике используют три базовых подхода: декомпозицию по данным, по задачам и смешанную декомпозицию.

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

Декомпозиция по задачам применима, когда вычисление естественно распадается на разные этапы. Примером служит конвейер ETL: чтение, очистка, обогащение, запись. Каждый этап может выполняться параллельно на разных порциях данных, формируя pipeline. Такой вариант повышает пропускную способность, но усложняет управление зависимостями, буферами и порядком выполнения.

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

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

Для Python важно учитывать формат данных и стоимость сериализации при переходе между процессами, поэтому эффективная декомпозиция часто использует партиции в виде NumPy массивов, Arrow таблиц или Parquet фрагментов, а также минимизирует число пересылок объектов уровня Python.
""",
    },
    {
        "text": "Модели параллельного программирования и их сочетаемость с архитектурами параллельных вычислительных систем.",
        "solution": """\
Модель параллельного программирования задает способ выражения параллелизма, а архитектура определяет физические ограничения: доступ к памяти, стоимость коммуникации и масштабируемость. Наиболее употребимы четыре модели: общая память с потоками, обмен сообщениями, модель задач и модель данных.

Потоковая модель общей памяти опирается на разделяемые структуры данных и синхронизацию через мьютексы, атомики и барьеры. Она хорошо согласуется с SMP и с NUMA, но на NUMA качество сильно зависит от локальности. В Python потоковая модель для CPU вычислений ограничена GIL, но остается полезной для I/O и для библиотек, выполняющих тяжелые вычисления в нативном коде.

Модель обмена сообщениями, традиционно ассоциируемая с MPI, лучше подходит для распределенной памяти, кластеров и систем, где узлы не делят RAM. Программист явно передает сообщения и управляет распределением данных. Эта модель масштабируется лучше, но требует аккуратного дизайна распределения данных и минимизации коммуникации. В Python используется mpi4py, а также высокоуровневые экосистемы, которые скрывают детали обмена.

Модель задач выражает вычисление как граф задач с зависимостями. Планировщик распределяет задачи по потокам или процессам. С архитектурной точки зрения модель задач универсальна: внутри узла задачи выполняются на потоках, между узлами задачи отправляются по сети. Примеры в Python это concurrent.futures, Ray, Dask. Сильная сторона это динамическое планирование и устойчивость к неравномерной нагрузке, слабая сторона это накладные расходы на диспетчеризацию и сериализацию.

Модель данных ориентируется на операции над коллекциями, таблицами и массивами, где параллелизм выводится из структуры данных. Векторизация в NumPy и DataFrame операции в системах наподобие Dask относятся к этой группе. Модель данных хорошо согласуется с архитектурами, где пропускная способность памяти и последовательный доступ доминируют над латентностью, а также с SIMD и многопоточными BLAS библиотеками.

Сочетаемость моделей с архитектурами оценивают по цене синхронизации и коммуникации. Общая память снижает цену обмена, но повышает цену когерентности и блокировок. Распределенная память повышает цену обмена, но убирает конкуренцию за общий контроллер памяти и улучшает масштабирование. Модель задач часто служит промежуточным слоем, который позволяет переносить одно и то же приложение с одной машины на кластер, меняя только исполнительную среду и формат данных.
""",
    },
    {
        "text": "Профилирование реализации алгоритмов на Python: принципы оптимизации производительности алгоритма.",
        "solution": """\
Профилирование в Python начинают с измерения, затем формируют гипотезы и проверяют их повторным измерением. Первичная цель это найти узкие места, а не ускорять код по ощущениям. На уровне CPU времени используют cProfile и pstats, которые показывают, какие функции потребляют время и сколько раз вызываются. Для локализации внутри функции применяют line_profiler, который дает построчную статистику. Для сценариев, где важно увидеть профиль без модификации кода, подходят py-spy и scalene, они работают через выборку стека и дают картину нагрузок в реальном времени.

После фиксации узких мест выбирают стратегию оптимизации. Если доминирует Python уровень, то главная проблема это накладные расходы интерпретатора, вызовы функций, создание объектов и работа сборщика мусора. В таких случаях помогают упрощение структур данных, снижение числа аллокаций, перенос горячих циклов в NumPy, использование list comprehension вместо многократных append с лишними проверками, кэширование результатов, а также замена Python циклов на векторные операции.

Если доминирует I/O, то профилирование дополняют трассировкой ожиданий и измерением времени чтения и записи. Здесь ускорение часто достигается через батчинг, чтение колонночных форматов, сжатие, параллельную предзагрузку и правильный выбор размера чанков.

Отдельное направление это профилирование памяти. memory_profiler показывает рост памяти по строкам, tracemalloc помогает найти источники аллокаций. Для обработки больших данных типичный прием это переход на типы меньшей разрядности, категориальные признаки, работа с Arrow или Parquet, а также потоковая обработка вместо загрузки всего набора в RAM.

Оптимизация обязана учитывать асимптотику алгоритма. Если сложность O(n^2), то микроподстройки дают ограниченный эффект. Поэтому после профилирования кода стоит оценить структуру вычислений, число проходов по данным, количество сортировок и join операций. Для подтверждения результата используют набор репрезентативных данных, фиксируют seed, прогревают окружение, повторяют замеры несколько раз и сравнивают медиану. Такой цикл измерение, изменение, повторное измерение дает устойчивый прирост и снижает риск деградации при реальной нагрузке.
""",
    },
    {
        "text": "Проблема Global Interpreter Lock в Python и способы обхода ее ограничений.",
        "solution": """\
Global Interpreter Lock, или GIL, это механизм в основной реализации Python, который гарантирует, что байткод выполняется одним потоком одновременно. Исторически он упростил управление памятью и сделал многие операции с объектами безопасными без тонкой блокировки на уровне каждого объекта. Следствие для производительности состоит в том, что многопоточность не ускоряет CPU ориентированные задачи, где основной объем работы выполняется в Python коде. Потоки при этом полезны для I/O, так как ожидание сети или диска освобождает интерпретатор и позволяет другому потоку продолжить работу.

Обход ограничений GIL строится вокруг переноса вычислений за пределы интерпретатора или вокруг параллелизма процессов. Самый прямой путь это multiprocessing и concurrent.futures.ProcessPoolExecutor. Каждый процесс имеет собственный интерпретатор и собственный GIL, поэтому CPU задачи масштабируются по числу ядер. Цена состоит в межпроцессной коммуникации и сериализации объектов. Для больших массивов полезны shared_memory, memmap и форматы, которые уменьшают копирование.

Второй путь это использование библиотек, которые выполняют тяжелые вычисления в нативном коде и освобождают GIL. NumPy, SciPy, pandas, многие реализации BLAS и FFT используют этот подход. В таких случаях потоковый параллелизм на уровне нативных библиотек дает ускорение без явного multiprocessing на уровне Python. При этом нужно контролировать число потоков в BLAS через переменные окружения, иначе появляется переподписка потоков и падение производительности.

Третий путь это компиляция горячих участков. Numba генерирует машинный код для численных функций и часто снимает ограничение GIL для скомпилированных участков, если оперировать массивами и примитивными типами. Cython и расширения на C или C++ дают более низкий уровень контроля, но требуют сборки.

Четвертый путь это альтернативные рантаймы. PyPy использует JIT, но совместимость с научной экосистемой ограничена. Jython и IronPython не имеют GIL, но не совместимы с CPython расширениями. Поэтому на практике для обработки больших данных наиболее употребимы процессы, векторизация и нативные вычисления, а также грамотный выбор формата данных, чтобы снизить цену обмена между процессами.
""",
    },
    {
        "text": "Технологический стек Python для обработки и анализа данных, Python как glue language, NumPy и его роль в экосистеме.",
        "solution": """\
Python в обработке данных часто выступает как язык, связывающий компоненты на разных уровнях. На нижнем уровне лежат библиотеки на C, C++ и Fortran, которые реализуют численные ядра и алгоритмы. Python предоставляет удобный синтаксис, интеграцию с файловыми форматами и инструментами визуализации, а также быстрый цикл разработки. Поэтому Python применяют для оркестрации вычислений, подготовки данных, экспериментов и построения конвейеров, где ключевые тяжелые операции передаются в оптимизированные нативные библиотеки.

Технологический стек обычно делят на несколько слоев. Для работы с массивами и линейной алгеброй используют NumPy и SciPy. Для табличных данных применяют pandas, для распределенной обработки Dask или PySpark, для машинного обучения scikit-learn, XGBoost, LightGBM, для глубокого обучения PyTorch или TensorFlow. Для хранения и обмена данными важны Apache Arrow, Parquet, ORC, HDF5, а также системы вроде PostgreSQL, ClickHouse и объектные хранилища. Для построения пайплайнов используют Airflow, Prefect, Dagster. Для ускорения критических участков применяют Numba, Cython и специализированные расширения.

NumPy занимает центральное место, так как задает стандарт представления плотных численных данных в виде ndarray и протоколов совместимости. Большая часть научных библиотек принимает и возвращает массивы NumPy или совместимые структуры. Важный эффект состоит в том, что массивы NumPy хранят данные компактно в непрерывной памяти, опираются на типизированные буферы и позволяют выполнять операции в векторном стиле. Векторизация переносит вычисления в нативный код, снижает накладные расходы интерпретатора и дает доступ к SIMD и оптимизациям компилятора.

Роль NumPy в экосистеме выражается и в интерфейсах. Протокол buffer, __array_interface__, поддержку dtype и strides используют разные библиотеки, включая pandas, SciPy, scikit-learn, Numba, а также мосты к Arrow. Поэтому знание устройства NumPy и принципов работы операций над массивами напрямую влияет на производительность и на корректность обработки больших данных.
""",
    },
    {
        "text": "Организация массивов в NumPy: хранение данных, создание массивов, принципы реализации операций с едиными исходными данными.",
        "solution": """\
NumPy массив ndarray это оболочка над непрерывным буфером памяти и метаданными, которые описывают форму, тип элементов, шаги strides и порядок хранения. Данные лежат в памяти как последовательность элементов фиксированного dtype, например int32 или float64. Массив не хранит каждый элемент как отдельный Python объект, поэтому снижаются накладные расходы и повышается плотность данных. Метаданные позволяют одному и тому же буферу интерпретироваться как многомерный массив, а strides задают, на сколько байт смещаться при переходе по каждой оси.

Создание массивов выполняют разными способами. np.array строит массив из последовательности Python, np.asarray избегает копирования, если вход уже ndarray. np.zeros, np.ones, np.empty создают буфер заданной формы и типа. np.arange, np.linspace формируют числовые сетки. Для работы с файлами используют np.fromfile, np.memmap, загрузку из np.load и чтение через сторонние библиотеки, которые возвращают буферы совместимые с NumPy.

Операции с едиными исходными данными опираются на представления, или views. Срезы и reshape в большинстве случаев не копируют данные, а создают новый ndarray с тем же буфером и другими метаданными. Это ускоряет преобразования формы и индексацию, но требует внимания к тому, что изменение view меняет исходный массив. Копирование выполняется явно через copy или неявно, когда операция требует непрерывного блока памяти или меняет порядок элементов.

Внутренний принцип реализации многих операций состоит в том, что NumPy строит итератор по массивам с учетом strides и выполняет цикл в C. Для унарных и бинарных операций задействуются ufunc, которые принимают входные буферы и производят выходной буфер. Если возможно, NumPy использует оптимизированные пути для непрерывных массивов и для типичных типов данных.

Для обработки больших данных важны два вывода. Первый вывод состоит в необходимости минимизировать лишние копии, особенно при reshape, transposition и конкатенации. Второй вывод состоит в контроле типов и порядка хранения, потому что операции над float32 и float64 имеют разный объем памяти, а порядок C и Fortran влияет на скорость прохода по данным и на эффективность кэширования.
""",
    },
    {
        "text": "Универсальные функции и применение функций по осям в NumPy.",
        "solution": """\
Универсальные функции, или ufunc, это объект NumPy, который инкапсулирует поэлементную операцию и реализован в нативном коде. Примеры это np.add, np.multiply, np.exp, np.sqrt, np.maximum. Ключевое свойство ufunc состоит в том, что она работает над массивами произвольной формы и типа, выбирая подходящую реализацию по dtype, а также поддерживает broadcasting. Благодаря исполнению в C поэлементные операции выполняются значительно быстрее, чем эквивалентные циклы на Python.

Ufunc поддерживают дополнительные режимы. Параметр out позволяет записать результат в заранее выделенный массив, снижая число аллокаций. Параметр where задает маску вычислений и помогает избегать ветвлений на Python уровне. Многие ufunc имеют методы reduce, accumulate, reduceat и outer. reduce выполняет свертку по указанной оси, например сумма или максимум. accumulate формирует префиксные суммы или другие накопления. outer строит внешнюю операцию над двумя векторами.

Применение функций по осям связано с агрегированием и преобразованием многомерных данных. В NumPy стандартные агрегаты, такие как sum, mean, std, min, max, имеют параметр axis, который задает ось, по которой выполняется свертка. При axis=0 агрегирование идет по строкам, при axis=1 по столбцам, если рассматривать матрицу. Также используется keepdims, чтобы сохранить размерность и упростить дальнейшее broadcasting.

Когда требуется применить пользовательскую функцию по оси, используют np.apply_along_axis или np.vectorize. Эти средства выполняют Python вызовы и подходят для небольших объемов, но для больших массивов производительность падает. Более эффективный подход состоит в выражении вычисления через комбинацию ufunc и агрегатов, либо в компиляции функции через Numba. В задачах обработки данных это означает, что нормализацию, стандартизацию, вычисление метрик и пороговые преобразования лучше формулировать как векторные выражения с явным axis, а не как циклы по строкам. Такой стиль дает устойчивую скорость и предсказуемое потребление памяти.
""",
    },
    {
        "text": "Принцип распространения значений при выполнении операций в NumPy: общий алгоритм и примеры.",
        "solution": """\
Распространение значений, или broadcasting, это правило NumPy, которое позволяет выполнять поэлементные операции над массивами разных форм без явного копирования меньшего массива до полной формы. Механизм основан на сравнении размеров по осям, начиная с последней оси. Для каждой пары размерностей возможны два допустимых случая: размеры равны, либо один из размеров равен 1. Если размер равен 1, то NumPy логически повторяет значения вдоль этой оси. Если ни одно условие не выполняется, операция завершается ошибкой несоответствия форм.

Алгоритм проверки можно описать так. Сначала выравнивают число осей, мысленно добавляя слева недостающие оси размером 1 у массива меньшей размерности. Затем по каждой оси сравнивают размеры, применяя правило равенства или единицы. После этого вычисляют результирующую форму как максимум размеров по каждой оси. На этапе выполнения NumPy не обязан физически раздувать массив, вместо этого итератор использует strides, где для распространенной оси stride равен нулю, то есть чтение идет из одного и того же элемента.

Пример с матрицей X формы (n, m) и вектором b формы (m,). При сложении X + b NumPy трактует b как форму (1, m) и распространяет по первой оси, добавляя один и тот же вектор к каждой строке. Аналогично, вектор a формы (n, 1) при сложении с X распространяется по второй оси и добавляет столбец к каждой колонке.

Еще один типичный пример это нормализация. Пусть X формы (n, m), а mean вычислен как X.mean(axis=0), получая форму (m,). Тогда X - mean использует broadcasting и вычитает среднее по каждой колонке. Для деления на стандартное отклонение применяют X / std, где std также имеет форму (m,).

Для больших данных важно помнить, что broadcasting снижает стоимость аллокаций, но увеличивает риск неожиданных временных массивов в составных выражениях. Поэтому выражения часто переписывают с использованием out и промежуточных буферов, чтобы контролировать память.
""",
    },
    {
        "text": "Маскирование и прихотливое индексирование в NumPy.",
        "solution": """\
В NumPy есть два существенно разных механизма выборки: маскирование и прихотливое индексирование, которое также называют fancy indexing. Маскирование использует булев массив той же формы, что и исходный массив, либо совместимой формы по правилам broadcasting. Элементы, где маска истинна, попадают в результат. Типичный сценарий это фильтрация по условию, например X[X > 0] или выборка строк, где выполнено несколько критериев через логические операции.

Важная особенность маскирования состоит в том, что результат чаще всего является копией данных, а не view. Поэтому изменение результата не меняет исходный массив. Это влияет на память при больших объемах, так как фильтрация создает новый массив. Для уменьшения аллокаций используют where в ufunc, либо работают с индексами, полученными через np.nonzero, чтобы контролировать дальнейшие операции.

Прихотливое индексирование использует массивы целых индексов. Например, X[[0, 2, 5]] выбирает строки с указанными номерами. В многомерном случае можно задавать массивы индексов по нескольким осям, получая выборку по набору координат. Этот механизм удобен для выборки батчей, перестановок и семплирования, например при обучении моделей. Как и булева индексация, fancy indexing возвращает копию, так как формирует новый массив по произвольным адресам памяти.

Есть тонкий момент с формой результата. Если по нескольким осям переданы индексные массивы, NumPy применяет их попарно и формирует результат формы, полученной от broadcast этих индексных массивов. Это иногда приводит к неожиданным формам. Для предсказуемого поведения используют np.ix_, который строит индексы для декартова произведения, например выбор подматрицы по списку строк и списку столбцов.

С точки зрения производительности произвольная выборка ухудшает локальность памяти, поэтому последовательные срезы обычно быстрее. При проектировании алгоритма для больших данных выгодно сохранять данные в порядке, который соответствует типичным шаблонам доступа, и минимизировать операции, создающие крупные копии. Маски и индексы стоит применять на этапах, где объем данных уже уменьшен, либо где копия неизбежна по смыслу задачи.
""",
    },

    {
        "text": "Векторизация в NumPy: ключевые параметры функции, примеры применения, использование обобщенной сигнатуры функции.",
        "solution": """\
Векторизация в NumPy означает перенос вычислений из циклов на уровне Python в операции над массивами, реализованные в C. На практике ядро векторизации это ufunc и выражения, которые опираются на broadcasting и на работу с непрерывными буферами памяти.

Ключевые параметры, которые напрямую влияют на производительность и контроль памяти, видны у многих ufunc. Параметр out задает массив для записи результата и снижает число временных выделений памяти. Параметр where задает булеву маску вычислений и помогает вычислять только нужные элементы, сохраняя остальные значения в out. Параметры dtype и casting управляют приведением типов, что важно при смешении int и float. Параметр order встречается у функций, которые создают новые массивы, он влияет на порядок хранения и на скорость последующих проходов по данным. Для операций редукции важен axis и keepdims, так как они определяют форму результата и дальнейшую совместимость при broadcasting.

Пример применения, нормализация матрицы X формы (n, m). Сначала вычисляется среднее по столбцам, mean = X.mean(axis=0). Затем выполняется центрирование: X_centered = X - mean. Broadcasting распространяет mean на все строки без явного размножения. Аналогично вычисляется std и выполняется деление. При больших объемах данных выгодно выделить буфер и использовать out. Например, np.subtract(X, mean, out=X) выполняет центрирование на месте, если исходные данные разрешено менять.

Обобщенная сигнатура удобна, когда функция работает не поэлементно, а по блокам фиксированной размерности, которые называют core dimensions. В Python уровне такой подход часто демонстрируют через np.vectorize с параметром signature. Пример, обработка набора векторов длины k: функция f принимает (k,) и возвращает (k,). Тогда задается signature="(k)->(k)", а входной массив имеет форму (n, k). np.vectorize с такой сигнатурой применяет f к каждому вектору по первой оси, при этом логика форм становится явной и предсказуемой. В задачах обработки данных сигнатура помогает формально описать преобразования блоков признаков, например нормализацию по группе признаков фиксированного размера или преобразование окон временного ряда.
""",
    },
    {
        "text": "Numba: принципы работы, базовые примеры использования.",
        "solution": """\
Numba ускоряет численные участки Python кода через JIT компиляцию. Типичный режим основан на компиляции функции, написанной в стиле численных вычислений, в машинный код через инфраструктуру LLVM. Для ускорения важны две идеи. Первая идея это специализация по типам, когда компилятор выводит типы аргументов и локальных переменных и строит отдельную версию функции под конкретные dtype. Вторая идея это отказ от объектов уровня Python в пользу примитивных численных типов и массивов NumPy, что снижает накладные расходы интерпретатора.

Базовый пример это ускорение цикла суммирования. Обычный Python цикл по массиву выполняет много операций на уровне интерпретатора. В Numba используется декоратор numba.njit. Функция принимает numpy.ndarray и возвращает скаляр. При первом вызове происходит компиляция под конкретный dtype, далее выполняется машинный код. Для устойчивого ускорения код внутри функции избегает списков Python, словарей и исключений, а также опирается на простые for циклы и арифметику.

Второй пример это вычисление попарных расстояний или скользящих метрик по временным рядам. Алгоритм часто имеет вложенные циклы и много повторяющихся операций. Numba в режиме nopython компилирует эти циклы и дает ускорение, близкое к C, особенно при работе с float32 и float64.

Параллелизм в Numba задается через njit(parallel=True) и использование prange вместо range. Компилятор распараллеливает независимые итерации. Такой режим подходит для задач, где каждая итерация пишет в отдельную ячейку результата и отсутствуют гонки данных. Пример это вычисление функции от каждого ряда матрицы или каждой строки таблицы признаков.

Отдельный набор возможностей связан с управлением памятью и с интеграцией с NumPy. Numba хорошо работает с заранее выделенными массивами, что снижает аллокации. Также поддерживаются элементарные операции индексирования и срезов, но наиболее предсказуемые результаты дает работа с непрерывными массивами и простыми формами. Для практики обработки больших данных Numba закрывает нишу, где векторизация выражениями NumPy становится громоздкой, а переписывание на C или Cython избыточно.
""",
    },
    {
        "text": "Организация Pandas DataFrame и организация индексации для DataFrame и Series.",
        "solution": """\
Pandas DataFrame представляет табличные данные как набор колонок, каждая колонка это Series с общим индексом строк. Внутри DataFrame хранит данные по колонкам и стремится группировать колонки одинаковых типов для эффективности, поэтому смешанные типы в одной таблице приводят к разным внутренним блокам. Колонки имеют имена, которые образуют ось columns. Строки адресуются индексом, который образует ось index. Индекс может быть RangeIndex для плотной нумерации, Index для произвольных меток, DatetimeIndex для временных рядов, а также MultiIndex для иерархических ключей.

Series это одномерная структура с массивом значений и индексом. Главный принцип Pandas состоит в выравнивании по меткам индекса при операциях между объектами. Если складываются две Series с разными индексами, результат строится по объединению меток, а отсутствующие значения переходят в пропуски. Этот принцип важен для корректности, но влияет на скорость, поэтому при тяжелых вычислениях часто используют .to_numpy и работают с чистыми массивами NumPy.

Индексация в Pandas разделяется на меточную и позиционную. Для меточной индексации используется .loc, где ключи интерпретируются как метки индекса и колонок. Для позиционной индексации используется .iloc, где ключи интерпретируются как целочисленные позиции. Для доступа к одному элементу применяют .at и .iat, это ускоряет точечные операции. Также используется булева индексация, когда передается маска True и False, и табличная выборка по массиву меток или позиций.

Критически важно различать срез по меткам и срез по позициям. В .loc срез по меткам включает правую границу для многих типов индекса, особенно для DatetimeIndex, тогда как .iloc работает как стандартный срез Python и не включает правую границу. Еще один важный момент это копии и представления. Pandas часто возвращает новый объект, а не view, поэтому присваивание в результат выборки ведет к предупреждениям и не гарантирует изменение исходных данных. Надежный путь изменения это явное присваивание через .loc с формой df.loc[mask, "col"] = value.

Организация индекса влияет на скорость. RangeIndex и отсортированный индекс ускоряют срезы и join операции. MultiIndex удобен для группировок и временных панелей, но усложняет понимание формы данных, поэтому его используют осознанно и фиксируют правила уровней индекса.
""",
    },
    {
        "text": "Применение универсальных функций и работа с пустыми значениями в Pandas.",
        "solution": """\
Pandas интегрируется с NumPy через ufunc и через протоколы преобразования массивов. Многие NumPy функции принимают Series и DataFrame и возвращают объекты с сохранением индекса и колонок. Например, np.log(series) вычисляет логарифм поэлементно и возвращает Series, сохраняя метки. Для DataFrame ufunc применяется к каждому элементу, а результат сохраняет структуру. Это удобно для вычисления признаков и трансформаций без ручных циклов.

Важная часть практики это работа с пропусками. В Pandas одновременно встречаются NaN для чисел с плавающей точкой, NaT для дат и времени, а также pd.NA для расширенных типов, например string и nullable integer. Эти представления ведут себя по-разному. NaN распространяется через большинство арифметических операций и ufunc, то есть NaN в одном элементе дает NaN в результате для этого элемента. Для агрегатов действует параметр skipna, по умолчанию многие агрегаты пропуски игнорируют, например sum и mean.

Для управления пропусками применяют fillna, где задается значение или стратегия, например заполнение вперед и назад для временных рядов. Для выборочного изменения используют where и mask. where сохраняет значения, где условие истинно, и заменяет остальные. mask делает обратное. Для приведения типов и устранения некорректных значений применяют pd.to_numeric с параметром errors="coerce", который преобразует ошибки в NaN.

При применении ufunc важно учитывать типы. Например, np.sqrt для Series с dtype object приводит к медленному пути и к неожиданным результатам. Правильная практика это привести колонку к числовому dtype, затем применять ufunc. Для DataFrame часто используют методы Pandas, такие как df.add, df.mul, df.pow, так как они поддерживают параметр fill_value и управляют выравниванием. Также применяют df.apply(np.func, axis=0) для поколоночных преобразований, но этот подход часто медленнее векторных операций, потому что создает вызовы Python для каждой колонки.

Для больших данных стратегия сводится к минимизации переходов к dtype object, явному управлению пропусками, а также к использованию векторных методов Series и DataFrame, которые опираются на NumPy и на специализированные реализации Pandas.
""",
    },
    {
        "text": "Объединение данных из нескольких Pandas DataFrame: общая логика и примеры.",
        "solution": """\
Объединение DataFrame в Pandas решает три разных задачи. Первая задача это конкатенация по строкам или по колонкам. Вторая задача это соединение по ключам, аналог SQL join. Третья задача это выравнивание и объединение по индексу.

Конкатенация выполняется через pd.concat. При axis=0 таблицы складываются по строкам, при axis=1 таблицы складываются по колонкам. При этом Pandas выравнивает колонки и индекс по меткам. Если у частей разный набор колонок, недостающие значения заполняются пропусками. Параметр ignore_index пересоздает индекс строк. Параметр keys добавляет внешний уровень MultiIndex, что удобно для маркировки источника.

Соединение по ключам выполняется через df.merge. В merge задают ключи через on, left_on и right_on. Тип соединения задает how, inner оставляет пересечение ключей, left сохраняет все строки левой таблицы, right сохраняет все строки правой таблицы, outer строит объединение ключей с пропусками. Для контроля конфликтующих имен колонок используют suffixes. Пример, есть таблица заказов с customer_id и таблица клиентов с customer_id и атрибутами. merge связывает строки заказов с атрибутами клиента. В обработке данных это базовый шаг обогащения.

Соединение по индексу выполняется через join или merge с параметром left_index и right_index. Этот вариант удобен, когда индекс хранит ключ, например дату для временных рядов. Также используется align, если требуется привести две Series или DataFrame к общему индексу перед вычислениями.

Для качества результата важны типы ключей и уникальность. Если ключи дублируются, merge порождает декартово размножение строк. Поэтому перед соединением проверяют кардинальность и ожидаемый тип связи, один к одному, один ко многим, многие ко многим. В Pandas есть параметр validate, который фиксирует ожидаемую кардинальность и поднимает ошибку при нарушении. Для производительности ключи приводят к категориальному типу при большом числе повторов, а также избегают merge по dtype object, где сравнение строк дороже.
""",
    },
    {
        "text": "Операция GroupBy в Pandas DataFrame и реализация в ней подхода «разбиение, применение и объединение».",
        "solution": """\
GroupBy в Pandas реализует схему split apply combine. На первом шаге данные разбиваются на группы по одному или нескольким ключам. Ключами служат колонки, уровень индекса, либо результат функции, которая возвращает метки групп. На втором шаге к каждой группе применяется функция агрегации, трансформации или произвольной обработки. На третьем шаге результаты объединяются в итоговую структуру с индексом групп и согласованными колонками.

Разбиение строится через факторизацию ключей. Pandas преобразует значения ключа в коды групп и таблицу уникальных ключей. Для больших данных это значит, что тип ключа влияет на скорость. Категориальные ключи и целые числа группируются быстрее, чем строки. Параметр sort управляет сортировкой ключей в результате. Параметр dropna определяет, попадают ли пропуски ключа в отдельную группу. Параметр observed важен при категориальных данных, он влияет на то, создаются ли группы для неиспользуемых категорий.

Применение делится на три основных семейства. agg выполняет агрегаты и возвращает сокращенный результат, например сумма, среднее, количество, минимум и максимум. transform возвращает результат той же длины, что и исходные данные, и подходит для вычисления нормализаций внутри группы, например вычитание среднего группы для каждой строки. apply принимает функцию, которая получает подтаблицу группы и возвращает объект произвольной формы. apply дает гибкость, но часто медленнее, потому что вызывает Python функцию для каждой группы и требует больше склейки результатов.

Объединение результатов зависит от as_index. При as_index=True ключи групп становятся индексом результата. При as_index=False ключи остаются колонками. Для многоколоночных агрегатов результат часто имеет MultiIndex по колонкам, поэтому после agg используют reset_index и переименование колонок.

Практический пример это расчет метрик по клиенту. df.groupby("customer_id").agg({"amount": "sum", "order_id": "count"}) дает сумму покупок и число заказов. Пример transform это нормализация суммы заказа внутри региона, df["amount_centered"] = df["amount"] - df.groupby("region")["amount"].transform("mean"). Такой подход сохраняет строковый уровень и дает возможность строить признаки для моделей без ручных циклов.
""",
    },
    {
        "text": "Специфика текстовых и бинарных файлов, форматы CSV и Pickle, представление данных в этих форматах и взаимодействие с ними в Python.",
        "solution": """\
Текстовые файлы хранят данные как последовательность символов в заданной кодировке. Их сильная сторона это читаемость и переносимость между языками и платформами. Их слабая сторона это объем, стоимость парсинга и неоднозначности, связанные с кодировкой, переносами строк и правилами экранирования. Бинарные файлы хранят байты в формате, который определяется спецификацией. Они обычно компактнее и быстрее читаются, но требуют строгого соблюдения схемы и инструментов для интерпретации.

CSV это текстовый табличный формат, где строки разделяются переводом строки, а поля разделяются разделителем, чаще запятой или точкой с запятой. В CSV отсутствует единая схема типов, поэтому числа, даты и булевы значения представлены строками. Нужны правила кавычек и экранирования, чтобы корректно хранить поля с разделителем или переводами строк. В Python чтение и запись CSV выполняют через модуль csv или через pandas.read_csv и DataFrame.to_csv. В Pandas важны параметры sep, encoding, decimal, thousands, quotechar, escapechar, na_values и dtype. От них зависит корректность распознавания чисел и пропусков. Для больших файлов решающими становятся chunksize и usecols, так как они уменьшают память и ускоряют чтение.

Pickle это бинарный формат сериализации объектов Python. Он сохраняет структуру объектов и связи между ними, включая пользовательские классы. Взаимодействие выполняют через pickle.dump и pickle.load, а в Pandas через to_pickle и read_pickle. Преимущество Pickle это точное сохранение объектов без потери типов. Недостатки это зависимость от версии Python и библиотек, слабая переносимость на другие языки и риск безопасности. Десериализация Pickle выполняет создание объектов, поэтому загрузка данных из недоверенного источника недопустима.

Для задач анализа данных CSV подходит для обмена и архивации простых таблиц, где важна совместимость. Pickle подходит для промежуточных артефактов внутри одного проекта, где важна скорость и сохранение сложных структур, например обученных моделей, словарей признаков или результатов расчетов в виде Python объектов.
""",
    },
    {
        "text": "Задача сериализации и десериализации, описание формата JSON и пример описания данных в этом формате и взаимодействия с ним в Python.",
        "solution": """\
Сериализация переводит структуру данных в последовательность байтов или символов для хранения или передачи. Десериализация выполняет обратное преобразование. В задачах обработки данных сериализация используется при записи результатов вычислений, при обмене между сервисами, при кэшировании и при построении пайплайнов, где шаги выполняются в разных процессах или на разных машинах. При выборе формата оценивают переносимость, размер, скорость и поддержку схемы типов.

JSON это текстовый формат представления данных, основанный на объектах и массивах. Объект в JSON это набор пар ключ значение, где ключ это строка. Массив это упорядоченный список значений. Поддерживаемые типы это строка, число, логическое значение, null, объект и массив. Формат не содержит явных дат, двоичных данных и произвольной точности чисел, поэтому такие типы кодируют строками или специальными структурами. JSON широко применяется в веб API и в конфигурациях, так как его легко читать и формировать в большинстве языков.

Пример JSON структуры для заказов:
{
  "customer_id": 10,
  "orders": [
    {"order_id": 1, "amount": 125.5, "currency": "EUR"},
    {"order_id": 2, "amount": 48.0, "currency": "EUR"}
  ]
}

В Python стандартная библиотека json предоставляет функции dump, dumps, load, loads. dumps сериализует объект в строку, dump пишет в файл. loads читает из строки, load читает из файла. При записи управляют кодировкой через открытие файла в UTF 8 и параметр ensure_ascii, который определяет, будут ли не ASCII символы экранироваться. Для красивого вывода используют indent. Для нестандартных типов, например datetime, используют параметр default, который преобразует объект в сериализуемое представление, например ISO строку.

При чтении JSON в анализе данных важно проверять схему. Данные часто содержат смешанные типы и отсутствующие поля. Поэтому после загрузки выполняют валидацию ключей, приведение типов и нормализацию вложенных структур. В Pandas для вложенных объектов применяют pandas.json_normalize, который разворачивает вложенные поля в плоскую таблицу, удобную для фильтрации и агрегаций.
""",
    },
    {
        "text": "Формат XML и модель DOM: общая характеристика, пример описания данных в XML и DOM, работа с ними с помощью BeautifulSoup.",
        "solution": """\
XML это текстовый формат разметки, который представляет данные в виде вложенных тегов с атрибутами. В отличие от JSON, XML чаще ориентирован на документы и на строгие схемы, но также применяется как транспорт для структурированных данных, особенно в интеграциях со старшими системами. XML поддерживает пространства имен, смешанный контент и раздельное описание схемы через XSD, что позволяет формально задавать допустимую структуру.

DOM это модель представления XML или HTML документа как дерева узлов в памяти. Узлы включают элементы, атрибуты, текстовые узлы и комментарии. DOM удобен для навигации по структуре и для выборок по тегам и атрибутам. Недостаток DOM это потребление памяти при больших файлах, так как дерево строится целиком. Для потоковой обработки крупных XML применяют SAX или iterparse, но для учебных и умеренных объемов DOM остается простым вариантом.

Пример XML для заказов:
<customer id="10">
  <orders>
    <order id="1" currency="EUR">125.5</order>
    <order id="2" currency="EUR">48.0</order>
  </orders>
</customer>

BeautifulSoup работает как парсер и как удобный интерфейс навигации по дереву. Для XML важно выбрать подходящий парсер, например "lxml-xml", чтобы корректно обрабатывать XML правила. После загрузки документа soup.find и soup.find_all выбирают элементы по имени тега. Атрибуты доступны через словарь, например tag["id"]. Текст элемента читается через tag.text. Для выборок по структуре применяют CSS селекторы через select, например "orders > order". В анализе данных задача часто сводится к извлечению набора однотипных элементов и атрибутов и к построению таблицы. Например, выбираются все теги order, для каждого берутся id, currency и текст суммы, затем формируется список словарей, после чего создается DataFrame.

Для качества обработки важно учитывать кодировку, пространства имен и неоднозначности. В XML имя тега в пространстве имен может включать префикс, поэтому селекторы и поиск часто настраиваются с учетом namespace. Также важно обрабатывать отсутствующие атрибуты и пустой текст, так как источники XML редко гарантируют идеальную чистоту данных.
""",
    },
    {
        "text": "Форматы файлов NPY и HDF: общая характеристика, пример взаимодействия с данными этих форматов в Python.",
        "solution": """\
NPY это бинарный формат NumPy для хранения одного массива ndarray. Файл содержит заголовок с описанием dtype, формы и порядка хранения, затем следует сырой бинарный буфер данных. Главные преимущества NPY это скорость чтения и записи, точное сохранение типов и отсутствие затрат на парсинг текста. Формат подходит для промежуточных артефактов вычислений, для кэширования признаков и для передачи массивов между этапами пайплайна на одной платформе.

В Python взаимодействие выполняется через np.save и np.load. np.save записывает массив в файл .npy. np.load читает и возвращает ndarray. Для больших массивов полезен режим mmap_mode, который открывает файл как memory map, что позволяет работать с данными без загрузки всего массива в RAM. Такой режим полезен при батчевой обработке и при построении моделей, где данные читаются блоками.

HDF обычно означает контейнер HDF5. Это формат для хранения массивов и таблиц в иерархической структуре групп и датасетов. HDF5 поддерживает чанки, сжатие, частичное чтение, атрибуты и хранение нескольких наборов данных в одном файле. Это делает формат удобным для больших численных данных и для долговременного хранения, особенно когда требуется выборочное чтение по срезам.

В Python HDF5 читают и пишут через библиотеку h5py, где файл открывается как объект, группы ведут себя как словари, а датасеты дают массивоподобный интерфейс. Примерная схема, создается файл, затем создается dataset "X" формы (n, m) с dtype float32 и включается gzip сжатие и chunking. Далее читается фрагмент X[0:1000, :], что уменьшает I O и память. В Pandas есть HDFStore и методы to_hdf и read_hdf, которые удобны для DataFrame, но требуют внимания к режимам таблицы и фиксированного формата, так как они влияют на возможность выборочного чтения.

С практической точки зрения NPY хорош для простого, быстрого хранения одного массива и для совместной работы с NumPy и Numba. HDF5 хорош для сложных проектов, где нужен набор датасетов, выборочное чтение и контроль над компрессией и структурой файла.
""",
    },
    {
        "text": "Взаимодействие с Excel из Python с помощью XLWings: принципы работы и примеры использования.",
        "solution": """\
XLWings связывает Python и Excel через автоматизацию приложения. На Windows связь обычно идет через COM, на macOS через AppleScript и соответствующие механизмы. Библиотека позволяет открывать книгу, читать и записывать диапазоны, управлять листами, формулами и вычислением, а также строить интеграцию в виде кнопок и пользовательских функций.

Базовая модель объектов включает App, Book, Sheet и Range. App управляет экземпляром Excel. Book представляет книгу. Sheet представляет лист. Range описывает прямоугольный диапазон ячеек. Типичный сценарий анализа данных, из Python открывается книга, читается таблица из диапазона в DataFrame, выполняются вычисления, затем результат записывается обратно в Excel. Для корректной передачи табличных данных важно управлять заголовками и индексом. XLWings умеет автоматически преобразовывать DataFrame в табличный вид, а также обратно, если задано, где находятся заголовки.

Пример практического применения это отчет по продажам. В Excel хранится исходный выгрузочный лист. Python через XLWings читает этот лист, применяет очистку данных, вычисляет агрегаты и записывает результаты на лист отчета. Далее Python обновляет сводные показатели, записывает формулы и инициирует пересчет. Такой подход полезен в организациях, где Excel остается интерфейсом для пользователей, а тяжелая логика переносится в Python.

Еще один вариант это создание пользовательских функций Excel на Python. XLWings предоставляет механизм, где функция на Python становится UDF и вызывается из ячейки как обычная функция. Это удобно для специализированных расчетов, но требует управления окружением и зависимостями, а также контроля времени выполнения, поскольку Excel ожидает быстрый ответ.

Для устойчивой работы важны три аспекта. Первый аспект это управление жизненным циклом Excel, закрытие книг и процессов, чтобы не оставлять зависшие экземпляры. Второй аспект это минимизация количества операций чтения и записи по ячейкам. Вместо циклов по строкам выполняется обмен блоками, так как каждая операция COM дорогая. Третий аспект это контроль типов и форматов, особенно дат и чисел, потому что Excel имеет собственные правила представления, а Python опирается на явные типы.
""",
    },

    {
        "text": "Основы работы с регулярными выражениями: базовый синтаксис, примеры использования модуля re в Python.",
        "solution": """\
Регулярные выражения описывают шаблоны строк и применяются для поиска, проверки формата, извлечения подстрок и замены. Шаблон строится из литералов и метасимволов. Точка означает любой символ, кроме перевода строки. Квантификаторы задают повторения. Звездочка означает ноль или больше повторов, плюс означает один или больше, вопросительный знак означает ноль или один. Фигурные скобки задают точное число повторов, например {3} или диапазон {2,5}. Класс символов задается квадратными скобками, например [A-Za-z0-9_]. Отрицание класса задается через ^ внутри скобок, например [^0-9]. Группы задаются круглыми скобками, они позволяют выделять части совпадения и применять квантификаторы к группе. Альтернатива задается символом |, например cat|dog. Якоря задают привязку к границам. ^ означает начало строки, $ означает конец строки. Границы слова задаются через \\b. Часто применяются предопределенные классы. \\d это цифра, \\w это буква, цифра или подчеркивание, \\s это пробельный символ.

В Python модуль re предоставляет функции re.search, re.match, re.fullmatch, re.findall, re.finditer, re.sub, re.split. re.search ищет первое совпадение в любой позиции, re.match проверяет совпадение только в начале строки, re.fullmatch проверяет совпадение всей строки. re.findall возвращает список найденных фрагментов, re.finditer возвращает итератор по объектам Match с координатами совпадений. re.sub выполняет замену, re.split выполняет разбиение строки по шаблону.

Пример проверки формата даты ISO:
import re
pattern = r"^\\d{4}-\\d{2}-\\d{2}$"
ok = re.fullmatch(pattern, "2026-01-18") is not None

Пример извлечения всех чисел с возможной десятичной частью:
nums = re.findall(r"\\d+(?:\\.\\d+)?", "x=10 y=3.14 z=7")

Флаги управляют режимом сопоставления. re.IGNORECASE отключает учет регистра, re.MULTILINE меняет поведение ^ и $ для многострочного текста, re.DOTALL разрешает точке матчить перевод строки, re.VERBOSE позволяет добавлять пробелы и комментарии в шаблон для читаемости. Для производительности крупные шаблоны компилируют через re.compile и переиспользуют объект Pattern при многократных вызовах.
""",
    },
    {
        "text": "Сегментация и токенезация текста на естественном языке, стемминг и лемматизация, примеры на Python.",
        "solution": """\
Сегментация текста включает разбиение на предложения и выделение смысловых блоков, а токенизация разбивает текст на токены, обычно слова, числа, знаки пунктуации. Эти шаги формируют основу для статистических моделей, поиска и извлечения признаков. Простая токенизация по пробелам плохо работает из-за пунктуации, сокращений, дефисов, чисел, ссылок и смешанных алфавитов. Поэтому используют правила и модели, которые учитывают контекст и стандарты языка.

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

Стемминг упрощает слово до основы через набор эвристик. Для русского языка распространен SnowballStemmer, который отбрасывает типичные суффиксы. Результат стемминга удобен для поиска и грубых признаков, но основа не обязана быть словарным словом. Лемматизация приводит слово к нормальной форме, опираясь на морфологический анализ и словари. Лемматизация точнее для семантики и статистики, но дороже по вычислениям.

Пример токенизации регулярным выражением для простого контура:
import re
tokens = re.findall(r"[A-Za-zА-Яа-яЁё]+|\\d+(?:\\.\\d+)?|[^\\s]", text)

Пример стемминга через nltk:
from nltk.stem.snowball import SnowballStemmer
stemmer = SnowballStemmer("russian")
stems = [stemmer.stem(t) for t in tokens if t.isalpha()]

Пример лемматизации через pymorphy2:
import pymorphy2
morph = pymorphy2.MorphAnalyzer()
lemmas = [morph.parse(t)[0].normal_form for t in tokens if t.isalpha()]

В потоке обработки данных важна согласованность. Один и тот же пайплайн токенизации и нормализации должен применяться при обучении и при инференсе. Для больших корпусов критичны скорость и память, поэтому часто сохраняют токены в виде списков строк, либо кодируют их в целые идентификаторы через словарь, а морфологические операции кэшируют по слову.
""",
    },
    {
        "text": "Расстояние Левенштейна: определение, алгоритм эффективного поиска оптимального редакционного предписания, пример поиска на Python.",
        "solution": """\
Расстояние Левенштейна между двумя строками определяется как минимальное число элементарных правок, которые преобразуют одну строку в другую. Классический набор правок включает вставку символа, удаление символа и замену символа. Каждая операция имеет стоимость 1, при необходимости модель расширяют взвешенными стоимостями. Расстояние применяется в поиске по опечаткам, нормализации имен, дедупликации и сопоставлении записей.

Стандартный алгоритм использует динамическое программирование. Строится матрица D размера (n+1) на (m+1), где n и m это длины строк s и t. Элемент D[i][j] задает минимальную стоимость преобразования префикса s[:i] в префикс t[:j]. Инициализация задает стоимость преобразования в пустую строку через удаления и вставки. Переход задается минимумом из трех вариантов: удаление, вставка, замена. Для замены стоимость равна D[i-1][j-1] плюс 0, если символы равны, иначе плюс 1. Итоговое расстояние равно D[n][m].

Поиск оптимального редакционного предписания означает восстановление последовательности операций. Для этого после заполнения матрицы выполняется обратный проход из D[n][m] к D[0][0]. На каждом шаге выбирается предшественник, который согласуется с выбранным минимумом. Если текущая ячейка равна D[i-1][j] + 1, то добавляется операция удаления. Если равна D[i][j-1] + 1, то добавляется вставка. Если равна D[i-1][j-1] + cost, то добавляется совпадение или замена в зависимости от cost. Обратный путь разворачивается, чтобы получить операции в прямом порядке.

Для экономии памяти при вычислении только расстояния достаточно двух строк матрицы, потому что переход зависит только от предыдущей строки и текущей строки. Для восстановления предписания требуется хранить всю матрицу, либо хранить указатели переходов, либо применять деление и властвование по Хиршбергу для компромисса между памятью и возможностью восстановления.

Пример на Python для расстояния и восстановления операций:
def levenshtein_ops(s, t):
    n, m = len(s), len(t)
    D = [[0] * (m + 1) for _ in range(n + 1)]
    for i in range(n + 1):
        D[i][0] = i
    for j in range(m + 1):
        D[0][j] = j
    for i in range(1, n + 1):
        for j in range(1, m + 1):
            cost = 0 if s[i - 1] == t[j - 1] else 1
            D[i][j] = min(
                D[i - 1][j] + 1,
                D[i][j - 1] + 1,
                D[i - 1][j - 1] + cost,
            )
    ops = []
    i, j = n, m
    while i > 0 or j > 0:
        if i > 0 and D[i][j] == D[i - 1][j] + 1:
            ops.append(("del", s[i - 1]))
            i -= 1
        elif j > 0 and D[i][j] == D[i][j - 1] + 1:
            ops.append(("ins", t[j - 1]))
            j -= 1
        else:
            cost = 0 if i > 0 and j > 0 and s[i - 1] == t[j - 1] else 1
            ops.append(("eq" if cost == 0 else "sub", s[i - 1], t[j - 1]))
            i -= 1
            j -= 1
    ops.reverse()
    return D[n][m], ops
""",
    },
    {
        "text": "Векторное представление текста на естественном языке: общий алгоритм подходов TF и TF-IDF.",
        "solution": """\
Векторное представление текста переводит документы в числовые признаки, пригодные для моделей классификации, кластеризации и поиска. Подходы TF и TF-IDF относятся к семейству мешка слов, где документ представляется частотами токенов без учета порядка слов. Алгоритм начинается с подготовки корпуса. Текст очищают, токенизируют, нормализуют регистр, удаляют стоп слова, выполняют лемматизацию или стемминг. Затем строят словарь признаков, то есть множество уникальных токенов. Часто вводят ограничения по частоте, например удаляют слишком редкие токены и слишком частые токены, которые не несут различительной информации.

TF, term frequency, задает вес токена в документе. Самый простой вариант это сырое число вхождений. Часто применяют нормировку, чтобы длинные документы не доминировали. Нормированная частота равна count(token, doc) делить на число токенов в документе. Также встречается логарифмическая шкала, где tf = 1 + log(count), если count больше нуля. Итогом становится разреженный вектор размера |V|, где V это словарь.

TF-IDF учитывает не только частоту в документе, но и распространенность токена по корпусу. IDF, inverse document frequency, понижает вес токенов, которые встречаются почти везде. Пусть N это число документов, а df(token) это число документов, где токен встречается хотя бы один раз. Тогда распространенная формула idf = log((N + 1) / (df + 1)) + 1. Вес признака равен tf * idf. После этого часто применяют нормировку вектора документа, например L2 нормировку, чтобы сравнение по косинусной близости работало устойчиво.

В Python типичный инструмент это sklearn.feature_extraction.text. В CountVectorizer получают матрицу счетчиков для TF, в TfidfVectorizer получают TF-IDF сразу. При настройке важны параметры tokenizer, ngram_range, min_df, max_df, max_features, stop_words, а также analyzer, который задает уровень, слова или символные n-граммы. Для больших корпусов критично хранение в разреженном формате CSR и пакетная обработка, так как плотная матрица размером N на |V| быстро переполняет память. При обновлении корпуса возникают вопросы согласованности словаря, поэтому словарь фиксируют на обучении и применяют тот же объект в инференсе.
""",
    },
    {
        "text": "Модуль multiprocessing: назначение и основные возможности, API multiprocessing.Pool.",
        "solution": """\
multiprocessing реализует параллельное исполнение через процессы операционной системы. Каждый процесс имеет отдельное адресное пространство и отдельный интерпретатор, что делает подход пригодным для CPU ориентированных вычислений. Библиотека поддерживает запуск процессов, очереди и каналы, разделяемую память, синхронизацию и пул рабочих процессов.

Основные сущности это Process, Queue, Pipe, Lock, Event, Semaphore, Manager и shared_memory. Process запускает целевую функцию в отдельном процессе. Queue и Pipe передают сообщения между процессами через сериализацию, обычно через pickle. Lock и связанные примитивы помогают синхронизировать доступ к общим ресурсам. Manager предоставляет прокси объекты для словарей и списков, но добавляет накладные расходы из-за сетевого протокола между процессами. shared_memory и Array, Value позволяют хранить данные в общей области памяти, уменьшая копирование, что критично при больших массивах.

multiprocessing.Pool предоставляет высокоуровневый API для карты задач на фиксированный набор процессов. Пул создается с числом процессов, обычно равным числу ядер. Основные методы это map, starmap, apply, apply_async, map_async, imap, imap_unordered. map принимает функцию и итерируемый набор аргументов и возвращает список результатов в исходном порядке. starmap принимает элементы, которые распаковываются как аргументы функции. apply вызывает функцию с одним набором аргументов и возвращает результат. apply_async и map_async возвращают объект AsyncResult, который позволяет ждать выполнения и получать результат позже. imap и imap_unordered возвращают итератор результатов, что позволяет обрабатывать поток результатов без накопления всего списка.

Важные практические моменты связаны со стоимостью сериализации и с размером задач. Если каждая задача слишком маленькая, накладные расходы на передачу аргументов и планирование перекрывают пользу параллелизма. Поэтому задачи группируют, увеличивают размер чанка через параметр chunksize в map, либо проектируют функцию так, чтобы внутри задачи было достаточно вычислений. Для больших NumPy массивов выгодно использовать shared_memory или memmap, чтобы процессы читали общие данные без копирования. Также важно закрывать пул через close и join, либо использовать контекстный менеджер, чтобы процессы не оставались живыми после завершения программы.
""",
    },
    {
        "text": "Различия между потоками и процессами, различие между различными планировщиками в Dask.",
        "solution": """\
Потоки и процессы отличаются уровнем изоляции и стоимостью переключения. Потоки разделяют адресное пространство процесса, поэтому обмен данными между потоками происходит через общую память и обычно дешевле. При этом требуется синхронизация, чтобы избежать гонок данных. Процессы имеют отдельные адресные пространства, что повышает изоляцию и снижает риск взаимного повреждения данных, но увеличивает стоимость обмена, так как данные передаются через сериализацию или через специально организованную разделяемую память.

В контексте Python важен GIL. Потоки не дают ускорения для CPU вычислений на уровне байткода, но подходят для I O задач и для нативных библиотек, которые освобождают интерпретатор. Процессы дают масштабирование CPU задач, но предъявляют требования к сериализуемости аргументов и результата. Также требуется учитывать старт метода, fork, spawn или forkserver, так как он влияет на перенос состояния и на совместимость на разных платформах.

Dask предоставляет несколько планировщиков, которые определяют, где и как исполняется граф задач. Однопоточный планировщик выполняет задачи последовательно в текущем процессе. Он полезен для отладки, так как поведение более детерминировано. Потоковый планировщик выполняет задачи в пуле потоков, что подходит для задач с нативными вычислениями и для I O, а также для операций над NumPy, где большая часть работы идет в C. Процессный планировщик выполняет задачи в пуле процессов, что подходит для Python уровня CPU нагрузки, но увеличивает накладные расходы на передачу данных.

Отдельно стоит распределенный планировщик Dask Distributed. Он вводит центральный scheduler и множество worker процессов, которые могут находиться на одной машине или в кластере. Distributed добавляет мониторинг, устойчивость, динамическое распределение задач и возможность хранить данные на воркерах. Выбор планировщика зависит от профиля задачи. Если данные большие и операции в основном NumPy, потоковый вариант снижает копирование. Если вычисления тяжелые на уровне Python, процессный или distributed вариант дает масштабирование. Если требуется кластер, distributed становится базовым выбором, так как локальные планировщики не решают задачу сети и размещения данных.
""",
    },
    {
        "text": "Граф зависимостей задач: суть структуры данных, ее построение и использование в Dask.",
        "solution": """\
Граф зависимостей задач описывает вычисление как набор вершин и ребер. Вершина представляет задачу, то есть вызов функции с аргументами. Ребро представляет зависимость, то есть необходимость получить результат одной задачи до выполнения другой. Такой граф является направленным ациклическим графом, так как каждая задача зависит только от ранее определенных результатов, а циклы приводят к невозможности вычисления без итерационной модели.

В Dask граф строится лениво. Операции над Dask коллекциями, например Dask.Array или Dask.Bag, не выполняют вычисления сразу. Вместо этого они добавляют узлы в граф, где каждый узел описывает минимальную порцию работы над чанком данных. Это позволяет оптимизировать вычисление до запуска, например объединять соседние операции, устранять повторные вычисления, переставлять независимые задачи и выбирать выгодный порядок выполнения. Для пользователя это выглядит как цепочка преобразований, а реальная работа начинается при вызове compute, persist или при выгрузке результата в файл.

Важная деталь состоит в том, что граф хранит ключи задач и их определения. Ключ обычно включает имя операции и координаты чанка. Определение задачи выражается как кортеж, где первый элемент это функция, а остальные элементы это аргументы, часть которых являются ссылками на другие ключи. Такой формат делает граф сериализуемым и пригодным для планирования.

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

Практическое использование графа в Dask включает диагностику и оптимизацию. Граф визуализируют через visualize, оценивают ширину, глубину и критический путь. Если граф слишком мелкозернистый, накладные расходы планирования растут, поэтому увеличивают размер чанков или объединяют операции. Если граф содержит широкие стадии, важна пропускная способность памяти и распределение по воркерам. Таким образом граф становится центральной моделью вычисления, которая связывает декларативное описание преобразований с конкретным параллельным исполнением.
""",
    },
    {
        "text": "Dask.Array: структура данных, специфика реализации и применения, процедура создания.",
        "solution": """\
Dask.Array представляет многомерные массивы, похожие по интерфейсу на NumPy ndarray, но предназначенные для данных, которые не помещаются в память или требуют параллельной обработки. Основная идея состоит в разбиении массива на чанки. Каждый чанк это блок фиксированной формы, который хранится как обычный NumPy массив или как ссылка на внешний источник. Над чанками строится ленивый граф задач, который описывает операции над блоками.

Структура Dask.Array включает метаданные формы, dtype и схему разбиения chunks. chunks задается как кортеж кортежей, где для каждой оси перечислены размеры блоков. Например, для матрицы формы (10000, 2000) можно задать chunks=((1000,)*10, (500,)*4). Это означает десять блоков по первой оси и четыре блока по второй оси. Выбор чанков влияет на параллелизм и на накладные расходы. Слишком маленькие чанки создают огромный граф, слишком большие чанки уменьшают параллелизм и увеличивают пик памяти.

Создание Dask.Array выполняют несколькими способами. Из NumPy массива создают через dask.array.from_array, задавая chunks, чтобы избегать копирования и формировать блоки как представления или как копии в зависимости от источника. Из файлов создают через функции чтения, например из Zarr или через dask.array.from_npy_stack для набора файлов .npy. Для генерации синтетических массивов используют dask.array.zeros, ones, random, arange, которые создают ленивые массивы без немедленного заполнения данными.

Применение Dask.Array типично для численных пайплайнов, линейной алгебры, обработки изображений, вычисления статистик и преобразований, где операции над блоками выражаются через NumPy подобные операции. При этом важно, что Dask.Array не хранит весь массив как единый объект, а распределяет вычисления по блокам. В результате compute выполняет чтение, преобразования и агрегации по блокам, что снижает требования к памяти. Для эффективности выбирают операции, которые хорошо раскладываются на блоки, например поэлементные функции, агрегаты по осям и свертки, и избегают глобальных перестановок, которые требуют сильного перемешивания данных между блоками.
""",
    },
    {
        "text": "Dask.Array: поддерживаемые операции и отличия от NumPy ndarray.",
        "solution": """\
Dask.Array поддерживает большой поднабор операций NumPy. Поддерживаются поэлементные операции, арифметика, сравнения, логические операции, многие ufunc, трансляция broadcasting, базовые преобразования формы, редукции по осям, такие как sum, mean, min, max, а также линейная алгебра ограниченного класса, например dot и matmul для совместимых блоковых разбиений. Поддерживаются операции индексации и срезов, причем многие срезы не требуют перемешивания блоков и выражаются как выбор подмножества задач.

Главное отличие от NumPy состоит в ленивости. Операции не возвращают готовый массив значений, они возвращают объект, который описывает будущий результат и хранит граф задач. Поэтому выражение вроде x + y формирует новые задачи, а не выполняет сложение. Выполнение происходит при compute или persist. Это меняет стиль разработки. Профилирование и оценка памяти строятся по графу и по параметрам чанков, а не по мгновенному состоянию массива.

Второе отличие связано с размером данных и переносом вычислений. NumPy ориентирован на данные в памяти одного процесса. Dask.Array ориентирован на блочное выполнение, поэтому операции, которые требуют глобальной перестановки данных, становятся дорогими. Примеры это sort по всей оси, некоторые варианты advanced indexing, а также операции, которые требуют доступа к соседним блокам сложной формы. В таких случаях Dask строит граф с операциями shuffle или rechunk, что увеличивает коммуникацию и память.

Третье отличие состоит в ограничениях по поддерживаемым функциям. Не все NumPy функции имеют реализацию для Dask.Array, особенно те, где требуется немедленное материализованное значение или сложная логика с произвольными Python объектами. Также важно, что многие операции возвращают приблизительные результаты, если используется вычисление на чанках и последующая агрегация, например численно нестабильные варианты суммирования могут давать небольшие расхождения из-за другого порядка сложения.

Практика работы с Dask.Array включает осознанный выбор чанков, минимизацию rechunk, избегание плотной материализации промежуточных результатов, использование persist для кэширования на воркерах, а также контроль планировщика. При корректной настройке Dask.Array дает масштабирование на многоядерных машинах и в кластере при сохранении привычного NumPy подобного интерфейса.
""",
    },
    {
        "text": "Dask.Bag: структура данных, специфика реализации и применения, процедура создания DaskBag.",
        "solution": """\
Dask.Bag предназначен для обработки коллекций Python объектов, где структура данных ближе к списку или потоку записей, чем к таблице или массиву. Bag хорошо подходит для полу-структурированных данных, например JSON строк, логов, событий, документов, а также для задач, где требуется применить последовательность функций map и filter к большому числу независимых элементов. Внутренне Bag делит данные на партиции. Каждая партиция это список элементов, который обрабатывается как единый блок задач.

Специфика Bag состоит в том, что он ориентирован на произвольные Python объекты, поэтому операции часто выполняются на уровне Python и имеют накладные расходы интерпретатора. Для тяжелых численных вычислений Bag уступает Dask.Array, а для табличных данных уступает Dask.DataFrame. При этом Bag удобен как входной слой, где данные читаются из файлов, очищаются и преобразуются в структуру, пригодную для дальнейшего анализа, например в список словарей, который затем превращается в DataFrame.

Создание DaskBag выполняют несколькими способами. Самый типичный способ это чтение текстовых файлов через dask.bag.read_text, которое создает Bag, где каждый элемент это строка, а партиции формируются по блокам файла. Для чтения набора файлов задают маску путей. Также используют dask.bag.from_sequence, чтобы построить Bag из существующей последовательности, задавая npartitions для параллелизма. Для JSON часто читают строки, затем применяют map с json.loads, получая элементы в виде словарей.

Bag поддерживает ленивость и строит граф задач. Каждая операция добавляет узлы в граф, а выполнение происходит при compute. Для устойчивой работы важно контролировать размер партиций. Слишком маленькие партиции увеличивают накладные расходы планирования, слишком большие партиции ухудшают балансировку и увеличивают пик памяти. В pipeline обработки логов часто делают несколько стадий. Сначала чтение и фильтрация строк, затем парсинг, затем нормализация полей, затем агрегации и группировки. Bag позволяет выразить эти стадии как цепочку преобразований, сохраняя возможность параллельного исполнения и отсрочки вычислений до финального шага.
""",
    },
    {
        "text": "Организация вычислений с помощью Map, Filter, Reduce: общий принцип и специфика параллельной реализации обработки данных в Dask.Bag.",
        "solution": """\
Map, Filter, Reduce это функциональная схема обработки коллекций. Map применяет функцию к каждому элементу и возвращает преобразованную коллекцию. Filter выбирает элементы, удовлетворяющие предикату. Reduce сворачивает коллекцию к одному результату через ассоциативную операцию, например суммирование или объединение. Эта схема удобна для параллельной обработки, так как map и filter не требуют обмена между элементами, а reduce допускает раздельную свертку частей с последующим объединением частичных результатов.

В Dask.Bag параллелизм строится через партиции. Map и filter применяются независимо к каждой партиции, что дает естественную параллельность. Планировщик запускает задачи обработки разных партиций на разных воркерах. После выполнения map и filter получаются новые партиции, возможно с измененным числом элементов. Эти операции хорошо масштабируются, если функция не зависит от глобального состояния и если время обработки элемента сопоставимо по всей коллекции. Если элементы сильно различаются по сложности, появляется дисбаланс, который снижают увеличением числа партиций и настройкой размера блоков чтения.

Reduce в Dask.Bag реализуется как дерево сверток. Сначала каждая партиция сворачивается локально, затем результаты партиций сворачиваются между собой. Такой подход снижает объем данных на стадии объединения и уменьшает нагрузку на сеть в распределенном режиме. Ключевое требование для корректности состоит в том, что операция reduce должна быть ассоциативной, а для детерминированности желательно коммутативной. Если операция не ассоциативна, результат зависит от порядка объединения, который в параллельном исполнении меняется.

Специфика параллельной реализации связана с сериализацией функций и данных. Dask отправляет функции на воркеры, поэтому функции должны быть сериализуемы, а также не должны держать неявные ссылки на ресурсы, которые не доступны на воркерах. Для эффективности функции делают чистыми, с минимальными замыканиями, а тяжелые внешние ресурсы инициализируют на воркере через механизмы клиента distributed, если используется кластер.

В обработке больших данных эта схема применяется для логов и событий. Map парсит строку в структуру, filter отбрасывает шум, reduce считает агрегаты. Такой контур остается читаемым и хорошо согласуется с моделью Dask.Bag.
""",
    },
    {
        "text": "API Dask.Bag: функции мэппинга, фильтрации и преобразования.",
        "solution": """\
Dask.Bag предоставляет набор операций для преобразования элементов и партиций. Базовая операция map применяет функцию к каждому элементу. Вариант map_partitions применяет функцию к целой партиции, передавая итератор элементов. Это позволяет реализовать более эффективные преобразования, где выгодно обрабатывать блоком, например пакетный парсинг, агрегация в пределах партиции, преобразование в список словарей с последующим возвратом.

Операция filter принимает предикат и оставляет элементы, для которых предикат возвращает истинное значение. Для задач очистки данных filter используется после этапа нормализации, чтобы исключить пустые или некорректные записи. При этом предикат должен быть простым и стабильным, так как он вызывается много раз.

Операция pluck извлекает значение по ключу из элементов, которые являются словарями или объектами с атрибутами. Например, после json.loads элементы часто становятся словарями, и pluck("user_id") выделяет поток идентификаторов. Операция flatten разворачивает элементы, если каждый элемент является итерируемым набором, например список токенов. Это полезно для перехода от документов к токенам.

Операции map и filter часто комбинируют с remove, которая исключает элементы по предикату, и с random_sample, которая выбирает подмножество элементов для быстрых проверок. Для приведения структуры Bag к табличному виду используют to_dataframe, задавая meta, то есть описание колонок и типов. Это ускоряет построение и снижает ошибки при выводе типов.

Для контроля выполнения применяют persist, чтобы зафиксировать промежуточный Bag в памяти воркеров, и compute для материализации результата. Также используется take для извлечения первых k элементов, что удобно для проверки пайплайна без полного вычисления. take запускает вычисление только необходимых партиций, поэтому ускоряет итеративную разработку.

Практический пример контура API выглядит так. Чтение строк из файлов, очистка через map, фильтрация через filter, парсинг JSON через map, извлечение полей через pluck, разворачивание вложенных списков через flatten, перевод к DataFrame через to_dataframe. Такой набор операций покрывает большую часть типовых задач предобработки полу-структурированных данных.
""",
    },
    {
        "text": "API Dask.Bag: функции группировки и свертки.",
        "solution": """\
Группировка и свертка в Dask.Bag решают задачу агрегации по ключу и получения итоговых метрик. Основная операция это fold и foldby. fold выполняет свертку всей коллекции по ассоциативной функции с заданным начальным значением. Сначала выполняются локальные свертки по партициям, затем выполняется объединение частичных результатов. Этот механизм подходит для суммирования чисел, объединения множеств, конкатенации строк при контролируемых объемах, а также для подсчета простых статистик при аккуратном определении нейтрального элемента.

foldby выполняет свертку по ключу. Пользователь задает функцию key, которая извлекает ключ из элемента, функцию binop, которая обновляет аккумулятор для ключа одним элементом, и функцию combine, которая объединяет два аккумулятора. Такой контракт позволяет выполнять агрегацию по ключу сначала внутри партиций, затем объединять результаты между партициями. Пример, подсчет количества событий по user_id. key возвращает user_id, binop увеличивает счетчик на 1, combine суммирует частичные счетчики. Этот подход масштабируется лучше, чем наивное группирование всех элементов по ключу в память одного процесса.

Операция groupby также существует, но она обычно требует собрать элементы одной группы вместе, что приводит к большому перемещению данных и увеличению памяти. Поэтому groupby применяют на умеренных данных или после сильной фильтрации, а для больших потоков предпочитают foldby, где агрегаты накапливаются постепенно.

Для статистик есть специализированные методы, такие как frequencies, который возвращает частоты элементов, и count, который считает элементы. Для получения top k по частоте часто комбинируют frequencies с последующей сортировкой на уровне результата. Для сложных агрегатов используют map_partitions, где внутри партиции строится локальный словарь агрегатов, затем применяется fold с объединением словарей. Такой подход уменьшает объем промежуточных данных.

При проектировании свертки важно обеспечивать корректность нейтрального элемента и ассоциативность combine. Также важен контроль размера аккумуляторов, например словари частот растут с числом уникальных ключей. В таких случаях применяют предварительное хеширование ключей на корзины, пороговую фильтрацию редких ключей, либо переход на Dask.DataFrame, где группировки реализованы на колонночных структурах и лучше используют оптимизации и типы данных.
""",
    },

    {
        "text": "Напишите функцию, которая парсит одну страницу с книгой. Функция принимает на вход URL, возвращает словарь со всей информацией о книге (включая количество звездочек, ссылку на изображение, доступное количество и жанр). В случае 404 ошибки возбудите ValueError",
        "solution": """\
import re
from urllib.parse import urljoin

import requests
from bs4 import BeautifulSoup


def parse_book_page(url, timeout=30):
    r = requests.get(url, timeout=timeout)
    if r.status_code == 404:
        raise ValueError(url)
    r.raise_for_status()
    soup = BeautifulSoup(r.text, "html.parser")

    def txt(sel):
        el = soup.select_one(sel)
        return el.get_text(strip=True) if el else ""

    def table_value(label):
        for row in soup.select("table.table.table-striped tr"):
            th = row.find("th")
            td = row.find("td")
            if not th or not td:
                continue
            if th.get_text(strip=True) == label:
                return td.get_text(strip=True)
        return ""

    title = txt("div.product_main h1")
    price_text = table_value("Price (incl. tax)") or txt("p.price_color")
    price = float(price_text.replace("£", "").strip()) if price_text else float("nan")

    availability_text = table_value("Availability") or txt("p.availability")
    m = re.search(r"(\\d+)\\s+available", availability_text)
    availability = int(m.group(1)) if m else 0

    star_el = soup.select_one("p.star-rating")
    stars = 0
    if star_el and star_el.has_attr("class"):
        classes = [c for c in star_el["class"] if c != "star-rating"]
        if classes:
            mapping = {"One": 1, "Two": 2, "Three": 3, "Four": 4, "Five": 5}
            stars = mapping.get(classes[0], 0)

    img_el = soup.select_one("div.item.active img")
    image_url = urljoin(url, img_el["src"]) if img_el and img_el.has_attr("src") else ""

    bc = [a.get_text(strip=True) for a in soup.select("ul.breadcrumb li a")]
    genre = bc[-1] if bc else ""

    description = ""
    desc_header = soup.select_one("#product_description")
    if desc_header:
        p = desc_header.find_next_sibling("p")
        if p:
            description = p.get_text(strip=True)

    data = {
        "url": url,
        "title": title,
        "genre": genre,
        "stars": stars,
        "image_url": image_url,
        "price_gbp": price,
        "availability_text": availability_text.strip(),
        "availability": availability,
        "upc": table_value("UPC"),
        "product_type": table_value("Product Type"),
        "price_excl_tax": table_value("Price (excl. tax)"),
        "price_incl_tax": table_value("Price (incl. tax)"),
        "tax": table_value("Tax"),
        "reviews": table_value("Number of reviews"),
        "description": description,
    }
    return data
""",
    },
    {
        "text": "Напишите функцию, которая обрабатывает одну страницу из каталога книг (возвращает список словарей по каждой из книг).",
        "solution": """\
from urllib.parse import urljoin

import requests
from bs4 import BeautifulSoup

from re import search


def parse_book_page(url, timeout=30):
    import re
    from urllib.parse import urljoin

    import requests
    from bs4 import BeautifulSoup

    r = requests.get(url, timeout=timeout)
    if r.status_code == 404:
        raise ValueError(url)
    r.raise_for_status()
    soup = BeautifulSoup(r.text, "html.parser")

    def txt(sel):
        el = soup.select_one(sel)
        return el.get_text(strip=True) if el else ""

    def table_value(label):
        for row in soup.select("table.table.table-striped tr"):
            th = row.find("th")
            td = row.find("td")
            if not th or not td:
                continue
            if th.get_text(strip=True) == label:
                return td.get_text(strip=True)
        return ""

    title = txt("div.product_main h1")
    price_text = table_value("Price (incl. tax)") or txt("p.price_color")
    price = float(price_text.replace("£", "").strip()) if price_text else float("nan")

    availability_text = table_value("Availability") or txt("p.availability")
    m = re.search(r"(\\d+)\\s+available", availability_text)
    availability = int(m.group(1)) if m else 0

    star_el = soup.select_one("p.star-rating")
    stars = 0
    if star_el and star_el.has_attr("class"):
        classes = [c for c in star_el["class"] if c != "star-rating"]
        if classes:
            mapping = {"One": 1, "Two": 2, "Three": 3, "Four": 4, "Five": 5}
            stars = mapping.get(classes[0], 0)

    img_el = soup.select_one("div.item.active img")
    image_url = urljoin(url, img_el["src"]) if img_el and img_el.has_attr("src") else ""

    bc = [a.get_text(strip=True) for a in soup.select("ul.breadcrumb li a")]
    genre = bc[-1] if bc else ""

    description = ""
    desc_header = soup.select_one("#product_description")
    if desc_header:
        p = desc_header.find_next_sibling("p")
        if p:
            description = p.get_text(strip=True)

    data = {
        "url": url,
        "title": title,
        "genre": genre,
        "stars": stars,
        "image_url": image_url,
        "price_gbp": price,
        "availability_text": availability_text.strip(),
        "availability": availability,
        "upc": table_value("UPC"),
        "product_type": table_value("Product Type"),
        "price_excl_tax": table_value("Price (excl. tax)"),
        "price_incl_tax": table_value("Price (incl. tax)"),
        "tax": table_value("Tax"),
        "reviews": table_value("Number of reviews"),
        "description": description,
    }
    return data


def parse_catalogue_page(url, timeout=30):
    r = requests.get(url, timeout=timeout)
    if r.status_code == 404:
        raise ValueError(url)
    r.raise_for_status()
    soup = BeautifulSoup(r.text, "html.parser")
    books = []
    for art in soup.select("article.product_pod"):
        a = art.select_one("h3 a")
        if not a or not a.has_attr("href"):
            continue
        book_url = urljoin(url, a["href"])
        books.append(parse_book_page(book_url, timeout=timeout))
    return books
""",
    },
    {
        "text": "Создайте массив NumPy, содержащий информацию о ценах книг. Магазин вводит скидку 15% на все книги. Найдите минимальную цену книги после введения скидки",
        "solution": """\
import numpy as np
import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin
import re


def parse_book_page(url, timeout=30):
    r = requests.get(url, timeout=timeout)
    if r.status_code == 404:
        raise ValueError(url)
    r.raise_for_status()
    soup = BeautifulSoup(r.text, "html.parser")

    def txt(sel):
        el = soup.select_one(sel)
        return el.get_text(strip=True) if el else ""

    def table_value(label):
        for row in soup.select("table.table.table-striped tr"):
            th = row.find("th")
            td = row.find("td")
            if not th or not td:
                continue
            if th.get_text(strip=True) == label:
                return td.get_text(strip=True)
        return ""

    price_text = table_value("Price (incl. tax)") or txt("p.price_color")
    price = float(price_text.replace("£", "").strip()) if price_text else float("nan")
    return {"url": url, "price_gbp": price}


def parse_catalogue_page(url, timeout=30):
    r = requests.get(url, timeout=timeout)
    if r.status_code == 404:
        raise ValueError(url)
    r.raise_for_status()
    soup = BeautifulSoup(r.text, "html.parser")
    books = []
    for art in soup.select("article.product_pod"):
        a = art.select_one("h3 a")
        if not a or not a.has_attr("href"):
            continue
        book_url = urljoin(url, a["href"])
        books.append(parse_book_page(book_url, timeout=timeout))
    return books


books = parse_catalogue_page("https://books.toscrape.com/catalogue/page-1.html")
prices = np.array([b["price_gbp"] for b in books], dtype=np.float64)
prices_after = prices * 0.85
min_price_after = float(np.min(prices_after))
min_price_after
""",
    },
    {
        "text": "Создайте массив NumPy, содержащий информацию о количестве книг. Посчитайте общую выручку, которую магазин получит, продав все книги",
        "solution": """\
import numpy as np
import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin
import re


def parse_book_page(url, timeout=30):
    r = requests.get(url, timeout=timeout)
    if r.status_code == 404:
        raise ValueError(url)
    r.raise_for_status()
    soup = BeautifulSoup(r.text, "html.parser")

    def txt(sel):
        el = soup.select_one(sel)
        return el.get_text(strip=True) if el else ""

    def table_value(label):
        for row in soup.select("table.table.table-striped tr"):
            th = row.find("th")
            td = row.find("td")
            if not th or not td:
                continue
            if th.get_text(strip=True) == label:
                return td.get_text(strip=True)
        return ""

    price_text = table_value("Price (incl. tax)") or txt("p.price_color")
    price = float(price_text.replace("£", "").strip()) if price_text else float("nan")

    availability_text = table_value("Availability") or txt("p.availability")
    m = re.search(r"(\\d+)\\s+available", availability_text)
    availability = int(m.group(1)) if m else 0

    return {"url": url, "price_gbp": price, "availability": availability}


def parse_catalogue_page(url, timeout=30):
    r = requests.get(url, timeout=timeout)
    if r.status_code == 404:
        raise ValueError(url)
    r.raise_for_status()
    soup = BeautifulSoup(r.text, "html.parser")
    books = []
    for art in soup.select("article.product_pod"):
        a = art.select_one("h3 a")
        if not a or not a.has_attr("href"):
            continue
        book_url = urljoin(url, a["href"])
        books.append(parse_book_page(book_url, timeout=timeout))
    return books


books = parse_catalogue_page("https://books.toscrape.com/catalogue/page-1.html")
prices = np.array([b["price_gbp"] for b in books], dtype=np.float64)
qty = np.array([b["availability"] for b in books], dtype=np.int64)
revenue = float(np.sum(prices * qty))
revenue
""",
    },
    {
        "text": "Создайте массив NumPy с закодированными значениями жанра. Магазин вводит скидку на каждый жанр по отдельной. Создайте массив NumPy со значениями скидок (размер массива зависит от количества жанров в выборке). Примените скидку к каждой книге в зависимости от жанра.",
        "solution": """\
import numpy as np
import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin
import re


def parse_book_page(url, timeout=30):
    r = requests.get(url, timeout=timeout)
    if r.status_code == 404:
        raise ValueError(url)
    r.raise_for_status()
    soup = BeautifulSoup(r.text, "html.parser")

    def txt(sel):
        el = soup.select_one(sel)
        return el.get_text(strip=True) if el else ""

    def table_value(label):
        for row in soup.select("table.table.table-striped tr"):
            th = row.find("th")
            td = row.find("td")
            if not th or not td:
                continue
            if th.get_text(strip=True) == label:
                return td.get_text(strip=True)
        return ""

    price_text = table_value("Price (incl. tax)") or txt("p.price_color")
    price = float(price_text.replace("£", "").strip()) if price_text else float("nan")

    bc = [a.get_text(strip=True) for a in soup.select("ul.breadcrumb li a")]
    genre = bc[-1] if bc else ""

    return {"url": url, "price_gbp": price, "genre": genre}


def parse_catalogue_page(url, timeout=30):
    r = requests.get(url, timeout=timeout)
    if r.status_code == 404:
        raise ValueError(url)
    r.raise_for_status()
    soup = BeautifulSoup(r.text, "html.parser")
    books = []
    for art in soup.select("article.product_pod"):
        a = art.select_one("h3 a")
        if not a or not a.has_attr("href"):
            continue
        book_url = urljoin(url, a["href"])
        books.append(parse_book_page(book_url, timeout=timeout))
    return books


books = parse_catalogue_page("https://books.toscrape.com/catalogue/page-1.html")
prices = np.array([b["price_gbp"] for b in books], dtype=np.float64)
genres = np.array([b["genre"] for b in books], dtype=object)

uniq_genres, genre_codes = np.unique(genres, return_inverse=True)
k = uniq_genres.shape[0]
discounts = np.minimum(0.05 + 0.02 * np.arange(k, dtype=np.float64), 0.30)
prices_after = prices * (1.0 - discounts[genre_codes])

genre_codes, discounts, prices_after
""",
    },
    {
        "text": "Соберите словарь вида {жанр: список книг жанра}. Сохраните словарь в формат JSON. Прочитайте файл.",
        "solution": """\
import json
from collections import defaultdict
from urllib.parse import urljoin
import requests
from bs4 import BeautifulSoup
import re


def parse_book_page(url, timeout=30):
    r = requests.get(url, timeout=timeout)
    if r.status_code == 404:
        raise ValueError(url)
    r.raise_for_status()
    soup = BeautifulSoup(r.text, "html.parser")

    def txt(sel):
        el = soup.select_one(sel)
        return el.get_text(strip=True) if el else ""

    def table_value(label):
        for row in soup.select("table.table.table-striped tr"):
            th = row.find("th")
            td = row.find("td")
            if not th or not td:
                continue
            if th.get_text(strip=True) == label:
                return td.get_text(strip=True)
        return ""

    title = txt("div.product_main h1")
    price_text = table_value("Price (incl. tax)") or txt("p.price_color")
    price = float(price_text.replace("£", "").strip()) if price_text else None

    availability_text = table_value("Availability") or txt("p.availability")
    m = re.search(r"(\\d+)\\s+available", availability_text)
    availability = int(m.group(1)) if m else 0

    bc = [a.get_text(strip=True) for a in soup.select("ul.breadcrumb li a")]
    genre = bc[-1] if bc else ""

    return {
        "url": url,
        "title": title,
        "genre": genre,
        "price_gbp": price,
        "availability": availability,
    }


def parse_catalogue_page(url, timeout=30):
    r = requests.get(url, timeout=timeout)
    if r.status_code == 404:
        raise ValueError(url)
    r.raise_for_status()
    soup = BeautifulSoup(r.text, "html.parser")
    books = []
    for art in soup.select("article.product_pod"):
        a = art.select_one("h3 a")
        if not a or not a.has_attr("href"):
            continue
        book_url = urljoin(url, a["href"])
        books.append(parse_book_page(book_url, timeout=timeout))
    return books


books = parse_catalogue_page("https://books.toscrape.com/catalogue/page-1.html")
by_genre = defaultdict(list)
for b in books:
    by_genre[b["genre"]].append(b)

by_genre = dict(by_genre)
path = "books_by_genre.json"
with open(path, "w", encoding="utf-8") as f:
    json.dump(by_genre, f, ensure_ascii=False, indent=2)

with open(path, "r", encoding="utf-8") as f:
    loaded = json.load(f)

loaded
""",
    },
    {
        "text": "Соберите кортеж из двух элементов (список книг, облагаемых налогами; список книг, не облагаемых налогами). Сохраните кортеж в формат Pickle. Считайте файл.",
        "solution": """\
import pickle
from urllib.parse import urljoin
import requests
from bs4 import BeautifulSoup
import re


def parse_book_page(url, timeout=30):
    r = requests.get(url, timeout=timeout)
    if r.status_code == 404:
        raise ValueError(url)
    r.raise_for_status()
    soup = BeautifulSoup(r.text, "html.parser")

    def txt(sel):
        el = soup.select_one(sel)
        return el.get_text(strip=True) if el else ""

    def table_value(label):
        for row in soup.select("table.table.table-striped tr"):
            th = row.find("th")
            td = row.find("td")
            if not th or not td:
                continue
            if th.get_text(strip=True) == label:
                return td.get_text(strip=True)
        return ""

    title = txt("div.product_main h1")
    price_text = table_value("Price (incl. tax)") or txt("p.price_color")
    price = float(price_text.replace("£", "").strip()) if price_text else None

    availability_text = table_value("Availability") or txt("p.availability")
    m = re.search(r"(\\d+)\\s+available", availability_text)
    availability = int(m.group(1)) if m else 0

    bc = [a.get_text(strip=True) for a in soup.select("ul.breadcrumb li a")]
    genre = bc[-1] if bc else ""

    return {
        "url": url,
        "title": title,
        "genre": genre,
        "price_gbp": price,
        "availability": availability,
    }


def parse_catalogue_page(url, timeout=30):
    r = requests.get(url, timeout=timeout)
    if r.status_code == 404:
        raise ValueError(url)
    r.raise_for_status()
    soup = BeautifulSoup(r.text, "html.parser")
    books = []
    for art in soup.select("article.product_pod"):
        a = art.select_one("h3 a")
        if not a or not a.has_attr("href"):
            continue
        book_url = urljoin(url, a["href"])
        books.append(parse_book_page(book_url, timeout=timeout))
    return books


books = parse_catalogue_page("https://books.toscrape.com/catalogue/page-1.html")
prices = sorted([b["price_gbp"] for b in books if b["price_gbp"] is not None])
median = prices[len(prices) // 2] if prices else 0.0

taxable = [b for b in books if (b["price_gbp"] or 0.0) > median]
non_taxable = [b for b in books if (b["price_gbp"] or 0.0) <= median]

payload = (taxable, non_taxable)
path = "tax_split.pkl"
with open(path, "wb") as f:
    pickle.dump(payload, f, protocol=pickle.HIGHEST_PROTOCOL)

with open(path, "rb") as f:
    loaded = pickle.load(f)

loaded
""",
    },
    {
        "text": "Создайте pd.DataFrame, содержащий всю полученную информацию о книгах. Сохраните его в файл xlsx.",
        "solution": """\
import pandas as pd
import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin
import re


def parse_book_page(url, timeout=30):
    r = requests.get(url, timeout=timeout)
    if r.status_code == 404:
        raise ValueError(url)
    r.raise_for_status()
    soup = BeautifulSoup(r.text, "html.parser")

    def txt(sel):
        el = soup.select_one(sel)
        return el.get_text(strip=True) if el else ""

    def table_value(label):
        for row in soup.select("table.table.table-striped tr"):
            th = row.find("th")
            td = row.find("td")
            if not th or not td:
                continue
            if th.get_text(strip=True) == label:
                return td.get_text(strip=True)
        return ""

    title = txt("div.product_main h1")

    price_text = table_value("Price (incl. tax)") or txt("p.price_color")
    price = float(price_text.replace("£", "").strip()) if price_text else None

    availability_text = table_value("Availability") or txt("p.availability")
    m = re.search(r"(\\d+)\\s+available", availability_text)
    availability = int(m.group(1)) if m else 0

    star_el = soup.select_one("p.star-rating")
    stars = 0
    if star_el and star_el.has_attr("class"):
        classes = [c for c in star_el["class"] if c != "star-rating"]
        if classes:
            mapping = {"One": 1, "Two": 2, "Three": 3, "Four": 4, "Five": 5}
            stars = mapping.get(classes[0], 0)

    img_el = soup.select_one("div.item.active img")
    image_url = urljoin(url, img_el["src"]) if img_el and img_el.has_attr("src") else ""

    bc = [a.get_text(strip=True) for a in soup.select("ul.breadcrumb li a")]
    genre = bc[-1] if bc else ""

    description = ""
    desc_header = soup.select_one("#product_description")
    if desc_header:
        p = desc_header.find_next_sibling("p")
        if p:
            description = p.get_text(strip=True)

    return {
        "url": url,
        "title": title,
        "genre": genre,
        "stars": stars,
        "image_url": image_url,
        "price_gbp": price,
        "availability": availability,
        "upc": table_value("UPC"),
        "product_type": table_value("Product Type"),
        "price_excl_tax": table_value("Price (excl. tax)"),
        "price_incl_tax": table_value("Price (incl. tax)"),
        "tax": table_value("Tax"),
        "reviews": table_value("Number of reviews"),
        "description": description,
    }


def parse_catalogue_page(url, timeout=30):
    r = requests.get(url, timeout=timeout)
    if r.status_code == 404:
        raise ValueError(url)
    r.raise_for_status()
    soup = BeautifulSoup(r.text, "html.parser")
    books = []
    for art in soup.select("article.product_pod"):
        a = art.select_one("h3 a")
        if not a or not a.has_attr("href"):
            continue
        book_url = urljoin(url, a["href"])
        books.append(parse_book_page(book_url, timeout=timeout))
    return books


books = parse_catalogue_page("https://books.toscrape.com/catalogue/page-1.html")
df = pd.DataFrame(books)
path = "books.xlsx"
df.to_excel(path, index=False, sheet_name="Books")
path
""",
    },
    {
        "text": "При помощи xlwings добавьте в xlsx файл столбец с ценой в рублях. Используйте протягиваемые формулы. Курс зафиксируйте на отдельном листе.",
        "solution": """\
import xlwings as xw

path = "books.xlsx"
gbp_rub = 120.0

app = xw.App(visible=False)
app.display_alerts = False
app.screen_updating = False
book = app.books.open(path)

if "Rates" in [s.name for s in book.sheets]:
    sh_rates = book.sheets["Rates"]
else:
    sh_rates = book.sheets.add("Rates", after=book.sheets[-1])

sh_rates.range("A1").value = "GBP_RUB"
sh_rates.range("B1").value = gbp_rub

sh = book.sheets["Books"]
last_col = sh.range("A1").end("right").column
last_row = sh.range("A1").end("down").row

headers = sh.range((1, 1), (1, last_col)).value
price_col = headers.index("price_gbp") + 1

new_col = last_col + 1
sh.range((1, new_col)).value = "price_rub"

first_formula_cell = sh.range((2, new_col))
price_cell_ref = sh.range((2, price_col)).address(False, False)
rate_cell_ref = sh_rates.range("B1").address(True, True, external=True)
first_formula_cell.formula = f"={price_cell_ref}*{rate_cell_ref}"

fill_range = sh.range((2, new_col), (last_row, new_col))
first_formula_cell.api.AutoFill(Destination=fill_range.api)

book.save()
book.close()
app.quit()
path
""",
    },
    {
        "text": "При помощи xlwings раскрасьте столбец с названием книги. Если в наличии более 20 книг, сделайте заливку зеленым, иначе - желтым.",
        "solution": """\
import xlwings as xw

path = "books.xlsx"

app = xw.App(visible=False)
app.display_alerts = False
app.screen_updating = False
book = app.books.open(path)
sh = book.sheets["Books"]

last_col = sh.range("A1").end("right").column
last_row = sh.range("A1").end("down").row

headers = sh.range((1, 1), (1, last_col)).value
title_col = headers.index("title") + 1
avail_col = headers.index("availability") + 1

titles_rng = sh.range((2, title_col), (last_row, title_col))
avail_rng = sh.range((2, avail_col), (last_row, avail_col))

avail_values = avail_rng.value
if not isinstance(avail_values, list):
    avail_values = [avail_values]
if avail_values and isinstance(avail_values[0], list):
    avail_values = [v[0] for v in avail_values]

for i, v in enumerate(avail_values, start=2):
    cell = sh.range((i, title_col))
    if (v or 0) > 20:
        cell.color = (0, 255, 0)
    else:
        cell.color = (255, 255, 0)

book.save()
book.close()
app.quit()
path
""",
    },

    {
        "text": "Напишите функцию, которая извлекает из текстов описаний имена собственные. Имя собственное это слово, начинающееся с заглавной буквы и за которым следует одна или несколько строчных букв.",
        "solution": """\
import re


def extract_proper_nouns(text):
    if text is None:
        return []
    return re.findall(r"\\b[A-Z][a-z]+\\b", str(text))
""",
    },
    {
        "text": "Представьте каждое описание в виде вектора при помощи TfidfVectorizer. Для каждой пары описаний посчитайте косинусную близость между ними. Визуализируйте результат в виде heatmap.",
        "solution": """\
import os
import re
from urllib.parse import urljoin

import matplotlib.pyplot as plt
import numpy as np
import requests
from bs4 import BeautifulSoup
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity


def parse_book_page(url, timeout=30):
    r = requests.get(url, timeout=timeout)
    if r.status_code == 404:
        raise ValueError(url)
    r.raise_for_status()
    soup = BeautifulSoup(r.text, "html.parser")

    def txt(sel):
        el = soup.select_one(sel)
        return el.get_text(strip=True) if el else ""

    def table_value(label):
        for row in soup.select("table.table.table-striped tr"):
            th = row.find("th")
            td = row.find("td")
            if not th or not td:
                continue
            if th.get_text(strip=True) == label:
                return td.get_text(strip=True)
        return ""

    title = txt("div.product_main h1")
    price_text = table_value("Price (incl. tax)") or txt("p.price_color")
    price = float(price_text.replace("£", "").strip()) if price_text else float("nan")

    availability_text = table_value("Availability") or txt("p.availability")
    m = re.search(r"(\\d+)\\s+available", availability_text)
    availability = int(m.group(1)) if m else 0

    star_el = soup.select_one("p.star-rating")
    stars = 0
    if star_el and star_el.has_attr("class"):
        classes = [c for c in star_el["class"] if c != "star-rating"]
        if classes:
            mapping = {"One": 1, "Two": 2, "Three": 3, "Four": 4, "Five": 5}
            stars = mapping.get(classes[0], 0)

    img_el = soup.select_one("div.item.active img")
    image_url = urljoin(url, img_el["src"]) if img_el and img_el.has_attr("src") else ""

    bc = [a.get_text(strip=True) for a in soup.select("ul.breadcrumb li a")]
    genre = bc[-1] if bc else ""

    description = ""
    desc_header = soup.select_one("#product_description")
    if desc_header:
        p = desc_header.find_next_sibling("p")
        if p:
            description = p.get_text(strip=True)

    return {
        "url": url,
        "title": title,
        "genre": genre,
        "stars": stars,
        "image_url": image_url,
        "price_gbp": price,
        "availability": availability,
        "description": description,
    }


def parse_catalogue_page(url, timeout=30):
    r = requests.get(url, timeout=timeout)
    if r.status_code == 404:
        raise ValueError(url)
    r.raise_for_status()
    soup = BeautifulSoup(r.text, "html.parser")
    books = []
    for art in soup.select("article.product_pod"):
        a = art.select_one("h3 a")
        if not a or not a.has_attr("href"):
            continue
        book_url = urljoin(url, a["href"])
        books.append(parse_book_page(book_url, timeout=timeout))
    return books


books = parse_catalogue_page("https://books.toscrape.com/catalogue/page-1.html")
descriptions = [b.get("description", "") or "" for b in books]

vectorizer = TfidfVectorizer(stop_words="english")
X = vectorizer.fit_transform(descriptions)
S = cosine_similarity(X)

fig_path = "cosine_similarity_heatmap.png"
plt.figure()
plt.imshow(S, interpolation="nearest")
plt.colorbar()
plt.title("Cosine similarity")
plt.xlabel("Document index")
plt.ylabel("Document index")
plt.tight_layout()
plt.savefig(fig_path, dpi=150)
plt.close()

S, fig_path
""",
    },
    {
        "text": "Найдите собственные числа матрицы косинусной близости. Найдите разность между максимальным и минимальным собственным значением.",
        "solution": """\
import re
from urllib.parse import urljoin

import numpy as np
import requests
from bs4 import BeautifulSoup
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity


def parse_book_page(url, timeout=30):
    r = requests.get(url, timeout=timeout)
    if r.status_code == 404:
        raise ValueError(url)
    r.raise_for_status()
    soup = BeautifulSoup(r.text, "html.parser")

    description = ""
    desc_header = soup.select_one("#product_description")
    if desc_header:
        p = desc_header.find_next_sibling("p")
        if p:
            description = p.get_text(strip=True)

    return {"url": url, "description": description}


def parse_catalogue_page(url, timeout=30):
    r = requests.get(url, timeout=timeout)
    if r.status_code == 404:
        raise ValueError(url)
    r.raise_for_status()
    soup = BeautifulSoup(r.text, "html.parser")
    books = []
    for art in soup.select("article.product_pod"):
        a = art.select_one("h3 a")
        if not a or not a.has_attr("href"):
            continue
        book_url = urljoin(url, a["href"])
        books.append(parse_book_page(book_url, timeout=timeout))
    return books


books = parse_catalogue_page("https://books.toscrape.com/catalogue/page-1.html")
descriptions = [b.get("description", "") or "" for b in books]

vectorizer = TfidfVectorizer(stop_words="english")
X = vectorizer.fit_transform(descriptions)
S = cosine_similarity(X)

eigvals = np.linalg.eigvalsh(S)
diff = float(np.max(eigvals) - np.min(eigvals))
diff
""",
    },
    {
        "text": "Используя pandas, найдите самую дорогую книгу в каждом из жанров.",
        "solution": """\
import re
from urllib.parse import urljoin

import pandas as pd
import requests
from bs4 import BeautifulSoup


def parse_book_page(url, timeout=30):
    r = requests.get(url, timeout=timeout)
    if r.status_code == 404:
        raise ValueError(url)
    r.raise_for_status()
    soup = BeautifulSoup(r.text, "html.parser")

    def txt(sel):
        el = soup.select_one(sel)
        return el.get_text(strip=True) if el else ""

    def table_value(label):
        for row in soup.select("table.table.table-striped tr"):
            th = row.find("th")
            td = row.find("td")
            if not th or not td:
                continue
            if th.get_text(strip=True) == label:
                return td.get_text(strip=True)
        return ""

    title = txt("div.product_main h1")
    price_text = table_value("Price (incl. tax)") or txt("p.price_color")
    price = float(price_text.replace("£", "").strip()) if price_text else float("nan")

    bc = [a.get_text(strip=True) for a in soup.select("ul.breadcrumb li a")]
    genre = bc[-1] if bc else ""

    return {"url": url, "title": title, "genre": genre, "price_gbp": price}


def parse_catalogue_page(url, timeout=30):
    r = requests.get(url, timeout=timeout)
    if r.status_code == 404:
        raise ValueError(url)
    r.raise_for_status()
    soup = BeautifulSoup(r.text, "html.parser")
    books = []
    for art in soup.select("article.product_pod"):
        a = art.select_one("h3 a")
        if not a or not a.has_attr("href"):
            continue
        book_url = urljoin(url, a["href"])
        books.append(parse_book_page(book_url, timeout=timeout))
    return books


books = parse_catalogue_page("https://books.toscrape.com/catalogue/page-1.html")
df = pd.DataFrame(books)

idx = df.groupby("genre")["price_gbp"].idxmax()
most_expensive_by_genre = df.loc[idx].sort_values(["genre"]).reset_index(drop=True)
most_expensive_by_genre
""",
    },
    {
        "text": "Используя pandas, разбейте книги на 3 ценовые категории. Для каждой категории посчитайте количество книг с разбивкой по рейтингам.",
        "solution": """\
import re
from urllib.parse import urljoin

import pandas as pd
import requests
from bs4 import BeautifulSoup


def parse_book_page(url, timeout=30):
    r = requests.get(url, timeout=timeout)
    if r.status_code == 404:
        raise ValueError(url)
    r.raise_for_status()
    soup = BeautifulSoup(r.text, "html.parser")

    def txt(sel):
        el = soup.select_one(sel)
        return el.get_text(strip=True) if el else ""

    def table_value(label):
        for row in soup.select("table.table.table-striped tr"):
            th = row.find("th")
            td = row.find("td")
            if not th or not td:
                continue
            if th.get_text(strip=True) == label:
                return td.get_text(strip=True)
        return ""

    title = txt("div.product_main h1")
    price_text = table_value("Price (incl. tax)") or txt("p.price_color")
    price = float(price_text.replace("£", "").strip()) if price_text else float("nan")

    star_el = soup.select_one("p.star-rating")
    stars = 0
    if star_el and star_el.has_attr("class"):
        classes = [c for c in star_el["class"] if c != "star-rating"]
        if classes:
            mapping = {"One": 1, "Two": 2, "Three": 3, "Four": 4, "Five": 5}
            stars = mapping.get(classes[0], 0)

    bc = [a.get_text(strip=True) for a in soup.select("ul.breadcrumb li a")]
    genre = bc[-1] if bc else ""

    return {"url": url, "title": title, "genre": genre, "price_gbp": price, "stars": stars}


def parse_catalogue_page(url, timeout=30):
    r = requests.get(url, timeout=timeout)
    if r.status_code == 404:
        raise ValueError(url)
    r.raise_for_status()
    soup = BeautifulSoup(r.text, "html.parser")
    books = []
    for art in soup.select("article.product_pod"):
        a = art.select_one("h3 a")
        if not a or not a.has_attr("href"):
            continue
        book_url = urljoin(url, a["href"])
        books.append(parse_book_page(book_url, timeout=timeout))
    return books


books = parse_catalogue_page("https://books.toscrape.com/catalogue/page-1.html")
df = pd.DataFrame(books)

df["price_category"] = pd.qcut(df["price_gbp"], 3, labels=["low", "mid", "high"])
table = df.groupby(["price_category", "stars"]).size().unstack(fill_value=0).sort_index()
table
""",
    },
    {
        "text": "Сохраните данные о книгах в БД sqlite3. Напишите функцию, которая по введенному пользователем названию жанра возвращает кол-во книг в этом жанре.",
        "solution": """\
import re
import sqlite3
from urllib.parse import urljoin

import requests
from bs4 import BeautifulSoup


def parse_book_page(url, timeout=30):
    r = requests.get(url, timeout=timeout)
    if r.status_code == 404:
        raise ValueError(url)
    r.raise_for_status()
    soup = BeautifulSoup(r.text, "html.parser")

    def txt(sel):
        el = soup.select_one(sel)
        return el.get_text(strip=True) if el else ""

    def table_value(label):
        for row in soup.select("table.table.table-striped tr"):
            th = row.find("th")
            td = row.find("td")
            if not th or not td:
                continue
            if th.get_text(strip=True) == label:
                return td.get_text(strip=True)
        return ""

    title = txt("div.product_main h1")
    price_text = table_value("Price (incl. tax)") or txt("p.price_color")
    price = float(price_text.replace("£", "").strip()) if price_text else None

    star_el = soup.select_one("p.star-rating")
    stars = 0
    if star_el and star_el.has_attr("class"):
        classes = [c for c in star_el["class"] if c != "star-rating"]
        if classes:
            mapping = {"One": 1, "Two": 2, "Three": 3, "Four": 4, "Five": 5}
            stars = mapping.get(classes[0], 0)

    bc = [a.get_text(strip=True) for a in soup.select("ul.breadcrumb li a")]
    genre = bc[-1] if bc else ""

    availability_text = table_value("Availability") or txt("p.availability")
    m = re.search(r"(\\d+)\\s+available", availability_text)
    availability = int(m.group(1)) if m else 0

    return {"url": url, "title": title, "genre": genre, "price_gbp": price, "stars": stars, "availability": availability}


def parse_catalogue_page(url, timeout=30):
    r = requests.get(url, timeout=timeout)
    if r.status_code == 404:
        raise ValueError(url)
    r.raise_for_status()
    soup = BeautifulSoup(r.text, "html.parser")
    books = []
    for art in soup.select("article.product_pod"):
        a = art.select_one("h3 a")
        if not a or not a.has_attr("href"):
            continue
        book_url = urljoin(url, a["href"])
        books.append(parse_book_page(book_url, timeout=timeout))
    return books


def init_db(conn):
    conn.execute(
        "CREATE TABLE IF NOT EXISTS books ("
        "url TEXT PRIMARY KEY,"
        "title TEXT,"
        "genre TEXT,"
        "price_gbp REAL,"
        "stars INTEGER,"
        "availability INTEGER"
        ")"
    )
    conn.commit()


def upsert_books(conn, books):
    conn.executemany(
        "INSERT OR REPLACE INTO books (url, title, genre, price_gbp, stars, availability) VALUES (?, ?, ?, ?, ?, ?)",
        [(b["url"], b["title"], b["genre"], b["price_gbp"], b["stars"], b["availability"]) for b in books],
    )
    conn.commit()


def count_books_by_genre(conn, genre):
    cur = conn.execute("SELECT COUNT(*) FROM books WHERE genre = ?", (genre,))
    row = cur.fetchone()
    return int(row[0]) if row else 0


books = parse_catalogue_page("https://books.toscrape.com/catalogue/page-1.html")
db_path = "books.sqlite3"
conn = sqlite3.connect(db_path)
init_db(conn)
upsert_books(conn, books)

example_genre = books[0]["genre"] if books else ""
count = count_books_by_genre(conn, example_genre)
conn.close()
count
""",
    },
    {
        "text": "Напишите функцию, которая добавляет новую запись в таблицу. Продемонстрируйте результат.",
        "solution": """\
import sqlite3


def init_db(conn):
    conn.execute(
        "CREATE TABLE IF NOT EXISTS books ("
        "url TEXT PRIMARY KEY,"
        "title TEXT,"
        "genre TEXT,"
        "price_gbp REAL,"
        "stars INTEGER,"
        "availability INTEGER"
        ")"
    )
    conn.commit()


def insert_book(conn, book):
    conn.execute(
        "INSERT OR REPLACE INTO books (url, title, genre, price_gbp, stars, availability) VALUES (?, ?, ?, ?, ?, ?)",
        (book["url"], book["title"], book["genre"], book["price_gbp"], book["stars"], book["availability"]),
    )
    conn.commit()


def fetch_by_url(conn, url):
    cur = conn.execute("SELECT url, title, genre, price_gbp, stars, availability FROM books WHERE url = ?", (url,))
    row = cur.fetchone()
    if not row:
        return None
    return {"url": row[0], "title": row[1], "genre": row[2], "price_gbp": row[3], "stars": row[4], "availability": row[5]}


db_path = "books.sqlite3"
conn = sqlite3.connect(db_path)
init_db(conn)

new_book = {
    "url": "https://books.toscrape.com/catalogue/demo-record_9999/index.html",
    "title": "Demo Record",
    "genre": "Demo",
    "price_gbp": 99.99,
    "stars": 5,
    "availability": 100,
}
insert_book(conn, new_book)
result = fetch_by_url(conn, new_book["url"])
conn.close()
result
""",
    },
    {
        "text": "Воспользовавшись модулем multiprocessing, соберите информацию о всех книгах с сайта (распаралелльте вычисления по страницам каталога).",
        "solution": """\
import re
from multiprocessing import Pool, cpu_count
from urllib.parse import urljoin

import requests
from bs4 import BeautifulSoup


BASE = "https://books.toscrape.com/"


def parse_book_page(url, timeout=30):
    r = requests.get(url, timeout=timeout)
    if r.status_code == 404:
        raise ValueError(url)
    r.raise_for_status()
    soup = BeautifulSoup(r.text, "html.parser")

    def txt(sel):
        el = soup.select_one(sel)
        return el.get_text(strip=True) if el else ""

    def table_value(label):
        for row in soup.select("table.table.table-striped tr"):
            th = row.find("th")
            td = row.find("td")
            if not th or not td:
                continue
            if th.get_text(strip=True) == label:
                return td.get_text(strip=True)
        return ""

    title = txt("div.product_main h1")
    price_text = table_value("Price (incl. tax)") or txt("p.price_color")
    price = float(price_text.replace("£", "").strip()) if price_text else None

    availability_text = table_value("Availability") or txt("p.availability")
    m = re.search(r"(\\d+)\\s+available", availability_text)
    availability = int(m.group(1)) if m else 0

    star_el = soup.select_one("p.star-rating")
    stars = 0
    if star_el and star_el.has_attr("class"):
        classes = [c for c in star_el["class"] if c != "star-rating"]
        if classes:
            mapping = {"One": 1, "Two": 2, "Three": 3, "Four": 4, "Five": 5}
            stars = mapping.get(classes[0], 0)

    img_el = soup.select_one("div.item.active img")
    image_url = urljoin(url, img_el["src"]) if img_el and img_el.has_attr("src") else ""

    bc = [a.get_text(strip=True) for a in soup.select("ul.breadcrumb li a")]
    genre = bc[-1] if bc else ""

    description = ""
    desc_header = soup.select_one("#product_description")
    if desc_header:
        p = desc_header.find_next_sibling("p")
        if p:
            description = p.get_text(strip=True)

    return {
        "url": url,
        "title": title,
        "genre": genre,
        "stars": stars,
        "image_url": image_url,
        "price_gbp": price,
        "availability": availability,
        "description": description,
    }


def get_catalogue_page_urls(base=BASE, timeout=30):
    r = requests.get(urljoin(base, "catalogue/page-1.html"), timeout=timeout)
    if r.status_code == 404:
        raise ValueError(urljoin(base, "catalogue/page-1.html"))
    r.raise_for_status()
    soup = BeautifulSoup(r.text, "html.parser")
    current = soup.select_one("li.current")
    if not current:
        return [urljoin(base, "catalogue/page-1.html")]
    txt = current.get_text(" ", strip=True)
    m = re.search(r"of\\s+(\\d+)", txt)
    pages = int(m.group(1)) if m else 1
    return [urljoin(base, f"catalogue/page-{i}.html") for i in range(1, pages + 1)]


def parse_catalogue_page(url, timeout=30):
    r = requests.get(url, timeout=timeout)
    if r.status_code == 404:
        raise ValueError(url)
    r.raise_for_status()
    soup = BeautifulSoup(r.text, "html.parser")
    book_urls = []
    for art in soup.select("article.product_pod"):
        a = art.select_one("h3 a")
        if not a or not a.has_attr("href"):
            continue
        book_urls.append(urljoin(url, a["href"]))
    books = [parse_book_page(u, timeout=timeout) for u in book_urls]
    return books


page_urls = get_catalogue_page_urls()
workers = max(1, min(cpu_count(), len(page_urls)))
with Pool(processes=workers) as pool:
    parts = pool.map(parse_catalogue_page, page_urls)

all_books = [b for part in parts for b in part]
len(all_books), all_books[0] if all_books else None
""",
    },
    {
        "text": "Воспользовавшись dask.delayed, скачайте изображения книг с сайта (распараллельте вычисления по страницам каталога) (использование Dask должно приводить к истинной параллельной обработке данных).",
        "solution": """\
import os
import re
from urllib.parse import urljoin, urlparse

import requests
from bs4 import BeautifulSoup
from dask import delayed, compute


BASE = "https://books.toscrape.com/"


def get_catalogue_page_urls(base=BASE, timeout=30):
    r = requests.get(urljoin(base, "catalogue/page-1.html"), timeout=timeout)
    if r.status_code == 404:
        raise ValueError(urljoin(base, "catalogue/page-1.html"))
    r.raise_for_status()
    soup = BeautifulSoup(r.text, "html.parser")
    current = soup.select_one("li.current")
    if not current:
        return [urljoin(base, "catalogue/page-1.html")]
    txt = current.get_text(" ", strip=True)
    m = re.search(r"of\\s+(\\d+)", txt)
    pages = int(m.group(1)) if m else 1
    return [urljoin(base, f"catalogue/page-{i}.html") for i in range(1, pages + 1)]


def book_image_urls_from_catalogue_page(url, timeout=30):
    r = requests.get(url, timeout=timeout)
    if r.status_code == 404:
        raise ValueError(url)
    r.raise_for_status()
    soup = BeautifulSoup(r.text, "html.parser")
    image_urls = []
    for art in soup.select("article.product_pod"):
        img = art.select_one("div.image_container img")
        if not img or not img.has_attr("src"):
            continue
        image_urls.append(urljoin(url, img["src"]))
    return image_urls


def download_image(url, out_dir="images", timeout=60):
    os.makedirs(out_dir, exist_ok=True)
    p = urlparse(url)
    name = os.path.basename(p.path)
    if not name:
        name = "image"
    path = os.path.join(out_dir, name)
    r = requests.get(url, timeout=timeout, stream=True)
    if r.status_code == 404:
        raise ValueError(url)
    r.raise_for_status()
    with open(path, "wb") as f:
        for chunk in r.iter_content(chunk_size=1024 * 64):
            if chunk:
                f.write(chunk)
    return path


page_urls = get_catalogue_page_urls()
tasks = []
for page_url in page_urls:
    page_imgs = delayed(book_image_urls_from_catalogue_page)(page_url)
    tasks.append(page_imgs)

img_lists = compute(*tasks, scheduler="processes")
img_urls = [u for sub in img_lists for u in sub]

download_tasks = [delayed(download_image)(u, out_dir="images") for u in img_urls]
paths = compute(*download_tasks, scheduler="processes")
len(paths), paths[0] if paths else None
""",
    },
    {
        "text": "Сохраните информацию о книгах в формате JSONl с разбивкой на файлы по жанрам.",
        "solution": """\
import json
import os
import re
from collections import defaultdict
from urllib.parse import urljoin

import requests
from bs4 import BeautifulSoup


BASE = "https://books.toscrape.com/"


def parse_book_page(url, timeout=30):
    r = requests.get(url, timeout=timeout)
    if r.status_code == 404:
        raise ValueError(url)
    r.raise_for_status()
    soup = BeautifulSoup(r.text, "html.parser")

    def txt(sel):
        el = soup.select_one(sel)
        return el.get_text(strip=True) if el else ""

    def table_value(label):
        for row in soup.select("table.table.table-striped tr"):
            th = row.find("th")
            td = row.find("td")
            if not th or not td:
                continue
            if th.get_text(strip=True) == label:
                return td.get_text(strip=True)
        return ""

    title = txt("div.product_main h1")
    price_text = table_value("Price (incl. tax)") or txt("p.price_color")
    price = float(price_text.replace("£", "").strip()) if price_text else None

    availability_text = table_value("Availability") or txt("p.availability")
    m = re.search(r"(\\d+)\\s+available", availability_text)
    availability = int(m.group(1)) if m else 0

    star_el = soup.select_one("p.star-rating")
    stars = 0
    if star_el and star_el.has_attr("class"):
        classes = [c for c in star_el["class"] if c != "star-rating"]
        if classes:
            mapping = {"One": 1, "Two": 2, "Three": 3, "Four": 4, "Five": 5}
            stars = mapping.get(classes[0], 0)

    img_el = soup.select_one("div.item.active img")
    image_url = urljoin(url, img_el["src"]) if img_el and img_el.has_attr("src") else ""

    bc = [a.get_text(strip=True) for a in soup.select("ul.breadcrumb li a")]
    genre = bc[-1] if bc else ""

    description = ""
    desc_header = soup.select_one("#product_description")
    if desc_header:
        p = desc_header.find_next_sibling("p")
        if p:
            description = p.get_text(strip=True)

    return {
        "url": url,
        "title": title,
        "genre": genre,
        "stars": stars,
        "image_url": image_url,
        "price_gbp": price,
        "availability": availability,
        "description": description,
    }


def get_catalogue_page_urls(base=BASE, timeout=30):
    r = requests.get(urljoin(base, "catalogue/page-1.html"), timeout=timeout)
    if r.status_code == 404:
        raise ValueError(urljoin(base, "catalogue/page-1.html"))
    r.raise_for_status()
    soup = BeautifulSoup(r.text, "html.parser")
    current = soup.select_one("li.current")
    if not current:
        return [urljoin(base, "catalogue/page-1.html")]
    txt = current.get_text(" ", strip=True)
    m = re.search(r"of\\s+(\\d+)", txt)
    pages = int(m.group(1)) if m else 1
    return [urljoin(base, f"catalogue/page-{i}.html") for i in range(1, pages + 1)]


def parse_catalogue_page(url, timeout=30):
    r = requests.get(url, timeout=timeout)
    if r.status_code == 404:
        raise ValueError(url)
    r.raise_for_status()
    soup = BeautifulSoup(r.text, "html.parser")
    book_urls = []
    for art in soup.select("article.product_pod"):
        a = art.select_one("h3 a")
        if not a or not a.has_attr("href"):
            continue
        book_urls.append(urljoin(url, a["href"]))
    return [parse_book_page(u, timeout=timeout) for u in book_urls]


page_urls = get_catalogue_page_urls()
all_books = []
for page_url in page_urls:
    all_books.extend(parse_catalogue_page(page_url))

by_genre = defaultdict(list)
for b in all_books:
    by_genre[b.get("genre", "")].append(b)

out_dir = "jsonl_by_genre"
os.makedirs(out_dir, exist_ok=True)

paths = []
for genre, items in by_genre.items():
    safe = re.sub(r"[^A-Za-z0-9_\\-]+", "_", genre.strip()) or "unknown"
    path = os.path.join(out_dir, f"{safe}.jsonl")
    with open(path, "w", encoding="utf-8") as f:
        for obj in items:
            f.write(json.dumps(obj, ensure_ascii=False) + "\\n")
    paths.append(path)

paths
""",
    },

    {
        "text": "Считайте данные в виде Dask Bag. Посчитайте, для скольких книг описание имеет больше 10 предложений. Выполните задание с использованием dask.bag, распараллелив процесс обработки данных (использование Dask должно приводить к истинной параллельной обработке данных).",
        "solution": """\
import json
import re

import dask.bag as db


def sentence_count(text):
    if text is None:
        return 0
    t = str(text).strip()
    if not t:
        return 0
    parts = re.split(r"[.!?]+", t)
    parts = [p.strip() for p in parts if p.strip()]
    return len(parts)


bag = db.read_text("jsonl_by_genre/*.jsonl")
count = (
    bag.map(json.loads)
    .map(lambda x: x.get("description", "") or "")
    .map(sentence_count)
    .filter(lambda n: n > 10)
    .count()
    .compute(scheduler="processes")
)
count
""",
    },
    {
        "text": "Сохраните данные о книгах в виде нескольких csv-файлов. Считайте их в виде Dask DataFrame. Выясните, есть ли в датасете книги, на которые оставили хоть один отзыв",
        "solution": """\
import os
import re
from urllib.parse import urljoin

import dask.dataframe as dd
import pandas as pd
import requests
from bs4 import BeautifulSoup


BASE = "https://books.toscrape.com/"


def get_catalogue_page_urls(base=BASE, timeout=30):
    r = requests.get(urljoin(base, "catalogue/page-1.html"), timeout=timeout)
    if r.status_code == 404:
        raise ValueError(urljoin(base, "catalogue/page-1.html"))
    r.raise_for_status()
    soup = BeautifulSoup(r.text, "html.parser")
    current = soup.select_one("li.current")
    if not current:
        return [urljoin(base, "catalogue/page-1.html")]
    txt = current.get_text(" ", strip=True)
    m = re.search(r"of\\s+(\\d+)", txt)
    pages = int(m.group(1)) if m else 1
    return [urljoin(base, f"catalogue/page-{i}.html") for i in range(1, pages + 1)]


def parse_book_page(url, timeout=30):
    r = requests.get(url, timeout=timeout)
    if r.status_code == 404:
        raise ValueError(url)
    r.raise_for_status()
    soup = BeautifulSoup(r.text, "html.parser")

    def txt(sel):
        el = soup.select_one(sel)
        return el.get_text(strip=True) if el else ""

    def table_value(label):
        for row in soup.select("table.table.table-striped tr"):
            th = row.find("th")
            td = row.find("td")
            if not th or not td:
                continue
            if th.get_text(strip=True) == label:
                return td.get_text(strip=True)
        return ""

    title = txt("div.product_main h1")
    price_text = table_value("Price (incl. tax)") or txt("p.price_color")
    price = float(price_text.replace("£", "").strip()) if price_text else None

    star_el = soup.select_one("p.star-rating")
    stars = 0
    if star_el and star_el.has_attr("class"):
        classes = [c for c in star_el["class"] if c != "star-rating"]
        if classes:
            mapping = {"One": 1, "Two": 2, "Three": 3, "Four": 4, "Five": 5}
            stars = mapping.get(classes[0], 0)

    bc = [a.get_text(strip=True) for a in soup.select("ul.breadcrumb li a")]
    genre = bc[-1] if bc else ""

    availability_text = table_value("Availability") or txt("p.availability")
    m = re.search(r"(\\d+)\\s+available", availability_text)
    availability = int(m.group(1)) if m else 0

    reviews_text = table_value("Number of reviews")
    reviews = int(reviews_text) if reviews_text.isdigit() else 0

    return {
        "url": url,
        "title": title,
        "genre": genre,
        "price_gbp": price,
        "stars": stars,
        "availability": availability,
        "reviews": reviews,
    }


def parse_catalogue_page(url, timeout=30):
    r = requests.get(url, timeout=timeout)
    if r.status_code == 404:
        raise ValueError(url)
    r.raise_for_status()
    soup = BeautifulSoup(r.text, "html.parser")
    book_urls = []
    for art in soup.select("article.product_pod"):
        a = art.select_one("h3 a")
        if not a or not a.has_attr("href"):
            continue
        book_urls.append(urljoin(url, a["href"]))
    return [parse_book_page(u, timeout=timeout) for u in book_urls]


page_urls = get_catalogue_page_urls()
all_books = []
for page_url in page_urls:
    all_books.extend(parse_catalogue_page(page_url))

df = pd.DataFrame(all_books)

out_dir = "books_csv_parts"
os.makedirs(out_dir, exist_ok=True)

n_parts = 8
parts = []
for i, part in enumerate(pd.np.array_split(df, n_parts)):
    p = os.path.join(out_dir, f"books_part_{i:02d}.csv")
    part.to_csv(p, index=False)
    parts.append(p)

ddf = dd.read_csv(os.path.join(out_dir, "books_part_*.csv"))
has_reviews = bool((ddf["reviews"].fillna(0).astype("int64") > 0).any().compute(scheduler="processes"))
has_reviews
""",
    },
    {
        "text": "Посчитайте кол-во обложек книг, которые по ширине больше, чем по длине. Разбейте весь набор файлов на 4 группы и выполните обработку в 4 процесса.",
        "solution": """\
import os
from multiprocessing import Pool

from PIL import Image


def chunk_list(items, k):
    n = len(items)
    base = n // k
    rem = n % k
    out = []
    start = 0
    for i in range(k):
        size = base + (1 if i < rem else 0)
        out.append(items[start : start + size])
        start += size
    return out


def count_wider(paths):
    c = 0
    for p in paths:
        try:
            with Image.open(p) as im:
                w, h = im.size
            if w > h:
                c += 1
        except Exception:
            pass
    return c


img_dir = "images"
files = []
if os.path.isdir(img_dir):
    for name in os.listdir(img_dir):
        p = os.path.join(img_dir, name)
        if os.path.isfile(p) and name.lower().endswith((".png", ".jpg", ".jpeg", ".webp")):
            files.append(p)

groups = chunk_list(sorted(files), 4)

with Pool(processes=4) as pool:
    counts = pool.map(count_wider, groups)

total = int(sum(counts))
total
""",
    }
]