import numpy as np
import pandas as pd


def qw_1():
    """
    1. Организация массивов в NumPy: хранение данных, создание массивов, принципы реализации операций с едиными исходными данными
    """
    print(qw_1.__doc__)

    # Подробный ответ
    print("""
    Хранение данных:
Ndarray — основной элемент numpy (n-dimensional array)
Массивы( ndarray) могут быть одномерными, двумерными или многомерными. При этом все элементы массива должны быть одного типа данных.
Numpy поддерживает большое количество типов данных, например: int_, int(8-64), bool_, float_, float(16-64), complex_, str и другие. 
Numpy хранит свои данные в блоках памяти что в совокупности с векторизированнымб предварительно скомпилированным и оптимизированном кодом на
С,  делает его работу оптимальной и быстрой для обработки с большими массивами данных 

Создание массивов:
Numpy поддерживает следующие  методы создания массивов: 
Numpy.array([‘тут вручную задаются данные массива’])
Numpy.arange(‘нач’, ’конец’, ‘шаг’) — создает массив с последовательность элементов
Numpy.zeros/ones(’n строк’, ’n столбцов’)

Принципы реализации операций:
Операции с массивами выполняются поэлементно
Примеры операций:
( arr_1 = np.array([1, 2, 3, 4, 5, 6])
arr_2 = np.array([7, 8, 9, 10, 11, 12]) )
* Сложение (res = arr_1 + arr_2)
* Умножение (res = arr_1 * arr_2)
* Возведение в квадрат (так же можно брать корень каждого элемента или находить экспоненциальнцю функцию) (np.square(ar_1))
* Агрегирование (arr_2.mean())
* Изменение формы массива (arr_1.reshape(2, 3))
* Поэлементное сравнение (np.greater(arr_1, ar_2))
* Индексация и срезы (arr_1[2], arr_2[1:2])
    """)


def qw_2():
    """
    2. Универсальные функции и применение функций по осям в NumPy
    """
    print(qw_2.__doc__)

    # Подробный ответ
    print("""
Универсальные функции (ufunc) — функции которые могут выполнять поэлементные операции на массивах и других типах данных. Они обеспечивают векторизованные операции что помогает избежать циклы for, что в свою очередь делает код более читабельным и эффективыным(опитимизированным).

Примеры функций: 
* np.add(), 
* np.subtract(),
*  np.multiply(), 
* np.divide()
Numpy позволяет применять ufunc не только по элементам, но и по осям. Чаще всего это используется при работе с многомерными массивами. Для этого используется параметр axis.

Примеры:
matrix = np.array([[1, 2, 3],
                   [4, 5, 6],
                   [7, 8, 9]])

# Сумма по столбцам (вдоль оси 0)
column_sum = np.sum(matrix, axis=0)  # [12, 15, 18]

# Сумма по строкам (вдоль оси 1)
row_sum = np.sum(matrix, axis=1)  # [6, 15, 24]

# Среднее значение по столбцам
column_mean = np.mean(matrix, axis=0)  # [4.0, 5.0, 6.0]

# Минимальное значение по строкам
row_min = np.min(matrix, axis=1)  # [1, 4, 7]

    """)


def qw_3():
    """
    3. Принцип распространения значений при выполнении операций в NumPy: общий алгоритм и примеры
    """
    print(qw_3.__doc__)

    # Подробный ответ
    print("""
    Векторизация в NumPy позволяет выполнять операции над массивами без явных циклов, что делает код более эффективным.

    Ключевые параметры функции:
    - `ufunc`: универсальная функция, которая применяется поэлементно к массивам.
    - `out`: опциональный параметр для указания массива для сохранения результата.

    Примеры применения:
    a = np.array([1, 2, 3])
    b = np.array([4, 5, 6])
    result = np.add(a, b)  # Применение универсальной функции поэлементно

    Использование обобщенной сигнатуры функции позволяет применять функции к массивам разных форм.

    Пример:
    def my_func(x):
        return x * 2

    result = np.vectorize(my_func)(a)
    """)


def qw_3():
    """
    3. Принцип распространения значений при выполнении операций в NumPy: общий алгоритм и примеры
    """
    print(qw_3.__doc__)

    # Подробный ответ
    print("""
    Принцип распространения значений (broadcasting) — мощный механизм NumPy, который позволяет выполнять операции над массивами разных форм и размеров. Broadcasting делает код более компактным и читабельным, избавляя от необходимости явного повторения значений в массиве.

    Основной алгоритм распространения значений в NumPy:

    1. **Шаг 1:** Если массивы имеют разные размерности, к размерности меньшего массива добавляются оси (измерения) в начале, чтобы их размерности совпадали.

    2. **Шаг 2:** Если размерности массивов не совпадают после добавления осей, то массивы масштабируются по нужным размерностям. Если размерность по какой-то оси равна 1, данные вдоль этой оси будут скопированы.

    3. **Шаг 3:** Операция выполняется поэлементно.

    Примеры применения:

    ```python
    import numpy as np

    # Пример 1: Сложение массивов разных форм
    a = np.array([1, 2, 3])
    b = np.array([[4], [5], [6]])
    result = a + b  # Broadcasting преобразует массив 'a' к форме 'b'
    print(result)
    # Результат:
    # [[5 6 7]
    #  [6 7 8]
    #  [7 8 9]]

    # Пример 2: Умножение массива на скаляр
    c = 2
    result = a * c  # Broadcasting преобразует скаляр 'c' к форме массива 'a'
    print(result)
    # Результат: [2 4 6]

    # Пример 3: Применение универсальной функции
    def my_func(x):
        return x ** 2

    result = my_func(a)  # Broadcasting применяет функцию поэлементно
    print(result)
    # Результат: [1 4 9]
    ```

    Векторизация в NumPy позволяет выполнять операции без явных циклов, что делает код более эффективным и улучшает читаемость.
    """)


def qw_4():
    """
    4. Маскирование и прихотливое индексирование в NumPy
    """
    print(qw_4.__doc__)

    # Подробный ответ
    print("""
    В NumPy маскирование и прихотливое индексирование предоставляют мощные инструменты для выбора и манипулирования данными в массивах.

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

    Пример маскирования:
    ```python
    import numpy as np

    # Создаем массив
    arr = np.array([1, 2, 3, 4, 5])

    # Создаем маску
    mask = arr > 2

    # Используем маску для выбора элементов
    result = arr[mask]
    print(result)
    # Результат: [3, 4, 5]
    ```

    **Прихотливое индексирование:**
    Прихотливое индексирование позволяет выбирать элементы массива с использованием массива индексов или условий.

    Пример прихотливого индексирования:
    ```python
    import numpy as np

    # Создаем массив
    arr = np.array([1, 2, 3, 4, 5])

    # Индексы, которые мы хотим выбрать
    indices = np.array([0, 2, 4])

    # Используем прихотливое индексирование
    result = arr[indices]
    print(result)
    # Результат: [1, 3, 5]
    ```

    Прихотливое индексирование также может использоваться с условиями:
    ```python
    import numpy as np

    # Создаем массив
    arr = np.array([1, 2, 3, 4, 5])

    # Условие выбора элементов
    condition = arr > 2

    # Используем прихотливое индексирование с условием
    result = arr[condition]
    print(result)
    # Результат: [3, 4, 5]
    ```

    Использование маскирования и прихотливого индексирования делает NumPy мощным инструментом для выбора и обработки данных в массивах.
    """)


def qw_5():
    """
    5. Векторизация в NumPy: ключевые параметры функции, примеры применения, использование
    """
    print(qw_5.__doc__)

    # Подробный ответ
    print("""
    Векторизация в NumPy позволяет выполнять операции над массивами без явных циклов, что делает код более эффективным и читаемым.

    Ключевые параметры функции `np.vectorize`:
    - `pyfunc`: функция, которую мы хотим векторизовать.
    - `otypes`: опциональный параметр для указания типов данных результата.

    Примеры применения векторизации:

    ```python
    import numpy as np

    # Пример 1: Векторизация с использованием базовой функции
    def my_func(x):
        return x ** 2

    # Векторизация функции
    vectorized_func = np.vectorize(my_func)

    # Применение векторизованной функции к массиву
    arr = np.array([1, 2, 3])
    result = vectorized_func(arr)
    print(result)
    # Результат: [1 4 9]

    # Пример 2: Векторизация с указанием типа данных результата
    def my_func_with_type(x):
        return x / 2

    # Векторизация функции с указанием типа данных результата
    vectorized_func_with_type = np.vectorize(my_func_with_type, otypes=[float])

    # Применение векторизованной функции к массиву
    arr = np.array([1, 2, 3])
    result = vectorized_func_with_type(arr)
    print(result)
    # Результат: [0.5 1.  1.5]

    # Пример 3: Векторизация с использованием lambda-функции
    vectorized_lambda = np.vectorize(lambda x: x + 10)

    # Применение векторизованной lambda-функции к массиву
    arr = np.array([1, 2, 3])
    result = vectorized_lambda(arr)
    print(result)
    # Результат: [11 12 13]
    ```

    Использование векторизации улучшает производительность кода и делает его более компактным, особенно при работе с массивами больших размеров.
    """)


def qw_5():
    """
    5. Векторизация в NumPy: ключевые параметры функции, примеры применения, использование обобщенной сигнатуры функции
    """
    print(qw_5.__doc__)

    # Подробный ответ
    print("""
    Векторизация в NumPy позволяет выполнять операции над массивами без явных циклов, что делает код более эффективным.

    Ключевые параметры функции:

    - `ufunc` (универсальная функция): функция, которая применяется поэлементно к массивам. NumPy предоставляет широкий набор встроенных универсальных функций, таких как np.add(), np.subtract(), np.multiply() и другие.

    - `out` (опциональный параметр): используется для указания массива, в который будет сохранен результат. Это позволяет избежать создания нового массива, если это необходимо.

    Примеры применения:

    ```python
    import numpy as np

    # Пример 1: Сложение массивов
    a = np.array([1, 2, 3])
    b = np.array([4, 5, 6])
    result = np.add(a, b)  # Применение универсальной функции поэлементно
    print(result)
    # Результат: [5 7 9]

    # Пример 2: Умножение массива на константу
    c = 2
    result = np.multiply(a, c)  # Применение универсальной функции поэлементно
    print(result)
    # Результат: [2 4 6]

    # Пример 3: Применение пользовательской функции
    def my_func(x):
        return x ** 2

    result = np.vectorize(my_func)(a)  # Использование обобщенной сигнатуры функции
    print(result)
    # Результат: [1 4 9]
    ```

    Использование обобщенной сигнатуры функции (np.vectorize) позволяет применять функции к массивам разных форм. Однако, важно помнить, что векторизованные операции с использованием встроенных универсальных функций более эффективны и быстры.

    Векторизация в NumPy улучшает читаемость кода, делает его более компактным и повышает производительность за счет использования оптимизированных операций над массивами.
    """)


def qw_6():
    """
    6. Numba: принципы работы, базовые примеры использования
    """
    print(qw_6.__doc__)

    # Подробный ответ
    print("""
    Numba - это JIT-компилятор (Just-In-Time), предназначенный для ускорения выполнения кода на языке Python, особенно при работе с числовыми вычислениями. Он преобразует функции Python в машинный код для улучшения производительности.

    Принципы работы Numba:

    1. **Автоматическое определение типов:** Numba использует информацию о типах данных в Python-коде и автоматически генерирует оптимизированный машинный код.

    2. **JIT-компиляция:** Код, помеченный декоратором `@jit`, компилируется в машинный код во время выполнения программы (Just-In-Time), что позволяет ускорить выполнение кода без необходимости переписывать его на компилируемом языке.

    Примеры использования Numba:

    ```python
    import numba

    # Пример 1: Ускорение функции с использованием JIT-компиляции
    @numba.jit
    def add_elements(a, b):
        result = a + b
        return result

    array_a = [1, 2, 3]
    array_b = [4, 5, 6]
    result = add_elements(array_a, array_b)
    print(result)
    # Результат: [5 7 9]

    # Пример 2: Использование аргументов и возвращаемых значений с явным указанием типов
    @numba.jit(numba.float64(numba.float64, numba.float64))
    def multiply_elements(a, b):
        result = a * b
        return result

    result = multiply_elements(2.0, 3.5)
    print(result)
    # Результат: 7.0
    ```

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

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


def qw_7():
    """
    7. Организация Pandas DataFrame и организация индексации для DataFrame и Series
    """
    print(qw_7.__doc__)

    # Подробный ответ
    print("""
    **Организация Pandas DataFrame:**

    В Pandas DataFrame представляет собой двумерную структуру данных, предназначенную для хранения и манипулирования табличными данными. Основные компоненты DataFrame:

    - **Столбцы (Columns):** Каждый столбец представляет собой объект Series (одномерный массив данных) и имеет свое имя.

    - **Индекс (Index):** Индекс представляет собой метки, присвоенные строкам или записям в DataFrame. Может быть автоматически создан или задан пользователем.

    Пример создания DataFrame:
    ```python
    import pandas as pd

    data = {'Name': ['Alice', 'Bob', 'Charlie'],
            'Age': [25, 30, 35],
            'City': ['New York', 'San Francisco', 'Los Angeles']}

    df = pd.DataFrame(data)
    print(df)
    ```

    **Организация индексации для DataFrame и Series:**

    - **Индексация в DataFrame:**
      - Доступ к столбцам: `df['ColumnName']` или `df.ColumnName`.
      - Доступ к строкам по индексу: `df.loc[index]`.
      - Доступ к строкам по числовому индексу: `df.iloc[num]`.
      - Срезы: `df[start:end]`.

    - **Организация индекса в DataFrame:**
      - Установка индекса: `df.set_index('ColumnName')`.
      - Сброс индекса: `df.reset_index()`.

    - **Индексация в Series:**
      - Аналогична индексации в DataFrame, так как каждый столбец DataFrame является объектом Series.

    Примеры индексации:
    ```python
    # Доступ к столбцу 'Name'
    names = df['Name']

    # Доступ к строке с индексом 1
    row_1 = df.loc[1]

    # Срез строк от 1 до 2
    rows_1_to_2 = df[1:3]

    # Установка индекса по столбцу 'Name'
    df_by_name = df.set_index('Name')

    # Сброс индекса
    df_reset_index = df_by_name.reset_index()
    ```

    Правильная организация индексов в DataFrame и Series облегчает доступ и манипуляции данными, а также обеспечивает эффективные операции в Pandas.
    """)


def qw_8():
    """
    8. Применение универсальных функций и работа с пустыми значениями в Pandas
    """
    print(qw_8.__doc__)

    # Подробный ответ
    print("""
    **Применение универсальных функций в Pandas:**

    Универсальные функции (ufuncs) в Pandas позволяют применять операции поэлементно к данным в DataFrame или Series. Они автоматически обрабатывают выравнивание индексов.

    Пример применения универсальной функции:
    ```python
    import pandas as pd

    # Создание DataFrame
    data = {'A': [1, 2, 3],
            'B': [4, 5, 6]}
    df = pd.DataFrame(data)

    # Применение универсальной функции поэлементно
    result = df.apply(lambda x: x * 2)
    print(result)
    ```

    **Работа с пустыми значениями в Pandas:**

    - **NaN (Not a Number):** В Pandas, NaN представляет собой отсутствующее или пустое значение.

    - **Проверка на NaN:**
      - `pd.isna(df)`: Возвращает булев DataFrame, где True указывает на наличие NaN.

    - **Заполнение NaN:**
      - `df.fillna(value)`: Заполняет все NaN указанным значением.
      - `df.dropna()`: Удаляет строки, содержащие хотя бы одно NaN.

    Пример работы с NaN:
    ```python
    import pandas as pd

    # Создание DataFrame с NaN
    data = {'A': [1, 2, None],
            'B': [4, None, 6]}
    df = pd.DataFrame(data)

    # Проверка на наличие NaN
    nan_check = pd.isna(df)

    # Заполнение NaN
    filled_df = df.fillna(0)

    # Удаление строк с NaN
    cleaned_df = df.dropna()

    print("DataFrame:")
    print(df)
    print("\nNaN Check:")
    print(nan_check)
    print("\nFilled DataFrame:")
    print(filled_df)
    print("\nCleaned DataFrame:")
    print(cleaned_df)
    ```

    Универсальные функции в Pandas предоставляют удобные методы для применения операций к данным, а работа с пустыми значениями важна для чистки и предварительной обработки данных.
    """)


def qw_9():
    """
    9. Объединение данных из нескольких Pandas DataFrame: общая логика и примеры
    """
    print(qw_9.__doc__)

    # Подробный ответ
    print("""
    **Объединение данных из нескольких Pandas DataFrame:**

    В Pandas существует несколько способов объединения данных из нескольких DataFrame:

    1. **concat():** Объединение по оси (строки или столбцы) с использованием индексов.

    2. **merge():** Объединение на основе значений в столбцах (аналог SQL JOIN).

    3. **join():** Объединение по индексам.

    **Примеры:**

    ```python
    import pandas as pd

    # Создание DataFrame
    df1 = pd.DataFrame({'ID': [1, 2, 3],
                        'Name': ['Alice', 'Bob', 'Charlie']})

    df2 = pd.DataFrame({'ID': [2, 3, 4],
                        'Age': [25, 30, 35]})

    # 1. Использование concat для объединения по строкам
    concatenated_df = pd.concat([df1, df2], axis=0, ignore_index=True)

    # 2. Использование merge для объединения по столбцу 'ID'
    merged_df = pd.merge(df1, df2, on='ID', how='inner')

    # 3. Использование join для объединения по индексу
    df1.set_index('ID', inplace=True)
    df2.set_index('ID', inplace=True)
    joined_df = df1.join(df2, how='inner')

    print("Concatenated DataFrame:")
    print(concatenated_df)
    print("\nMerged DataFrame:")
    print(merged_df)
    print("\nJoined DataFrame:")
    print(joined_df)
    ```

    Важные параметры:
    - **`axis`:** Определяет, по какой оси производится объединение (0 - строки, 1 - столбцы).
    - **`on`:** Указывает столбец, по которому происходит объединение.
    - **`how`:** Определяет тип объединения ('inner', 'outer', 'left', 'right').

    Объединение данных позволяет комбинировать информацию из различных источников и формировать полные наборы данных.
    """)


def qw_10():
    """
    10. Операция GroupBy в Pandas DataFrame и реализация в ней подхода «разбиение, применение и объединение»
    """
    print(qw_10.__doc__)

    # Подробный ответ
    print("""
    **Операция GroupBy в Pandas DataFrame:**

    Операция GroupBy в Pandas позволяет разбивать данные на группы по определенному критерию, применять функцию к каждой группе независимо и объединять результаты.

    **Подход «разбиение, применение и объединение» (Split-Apply-Combine):**

    1. **Разбиение (Split):** Данные разбиваются на группы на основе некоторого критерия.

    2. **Применение (Apply):** К функции применяется операция или функция, выполняемая независимо для каждой группы.

    3. **Объединение (Combine):** Результаты применения функций объединяются в итоговый объект.

    **Пример:**

    ```python
    import pandas as pd

    # Создание DataFrame
    data = {'Category': ['A', 'B', 'A', 'B', 'A', 'B'],
            'Value': [10, 15, 20, 25, 30, 35]}

    df = pd.DataFrame(data)

    # 1. Разбиение по столбцу 'Category'
    grouped_data = df.groupby('Category')

    # 2. Применение функции, например, вычисление среднего значения для каждой группы
    mean_values = grouped_data['Value'].mean()

    # 3. Объединение результатов
    result_df = pd.DataFrame({'Mean_Value': mean_values}).reset_index()

    print("Original DataFrame:")
    print(df)
    print("\nGrouped Data:")
    print(grouped_data)
    print("\nMean Values:")
    print(mean_values)
    print("\nResult DataFrame:")
    print(result_df)
    ```

    В данном примере данные разбиваются на группы по столбцу 'Category', затем к каждой группе применяется функция вычисления среднего значения. Результаты объединяются в новый DataFrame.

    Операция GroupBy в Pandas является мощным инструментом для агрегации данных и анализа по группам.
    """)


def qw_11():
    """
    11. Специфика текстовых и бинарных файлов, форматы файлов CSV и Pickle, представление данных в этих форматах и взаимодействие с ними в Python.
    """
    print(qw_11.__doc__)

    # Доработанный ответ
    print("""
    **Текстовые файлы:**

    Текстовые файлы предназначены для хранения информации в текстовом формате. Это означает, что каждый символ в файле представлен одним байтом, соответствующим его коду ASCII. Текстовые файлы можно открывать и читать в Python с помощью встроенной функции `open()`.

    **Бинарные файлы:**

    Бинарные файлы хранят данные в виде последовательности байтов, а не символов. Они обычно используются для хранения изображений, аудио или видео. Бинарные файлы также могут быть открыты и прочитаны в Python с использованием функции `open()`, но с параметром "rb" для чтения в двоичном режиме.

    **CSV-файлы:**

    CSV (Comma Separated Values) – это текстовый формат файла, в котором данные разделены запятыми. Этот формат часто используется для хранения табличных данных. В Python можно работать с CSV-файлами с помощью модуля `csv`.

    Пример чтения CSV-файла:
    ```python
    import csv

    with open('example.csv', 'r') as file:
        csv_reader = csv.reader(file)
        for row in csv_reader:
            print(row)
    ```

    **Pickle-файлы:**

    Pickle – это формат сериализации объектов Python. Он позволяет сохранять объекты Python в виде строк байтов, которые могут быть сохранены на диске или переданы по сети. В Python работа с Pickle осуществляется с помощью модуля `pickle`.

    Пример записи объекта в Pickle-файл:
    ```python
    import pickle

    data = {'name': 'John', 'age': 30, 'city': 'New York'}
    with open('data.pickle', 'wb') as file:
        pickle.dump(data, file)
    ```

    **Взаимодействие с файлами:**

    В Python можно взаимодействовать с файлами, используя встроенные функции и модули. Например, для чтения и записи данных в файл можно использовать функции `open()` и `write()`. Для работы с CSV- и Pickle-файлами можно использовать модули `csv` и `pickle` соответственно.
    """)


def qw_12():
    """
    12. Задача сериализации и десериализации, описание формата файла JSON и пример описания данных в этом формате и взаимодействия с ним в Python.
    """
    print(qw_12.__doc__)

    # Доработанный ответ
    print("""
    **Задача сериализации и десериализации:**

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

    **Формат файла JSON (JavaScript Object Notation):**

    JSON является универсальным форматом данных, широко используемым для обмена данными между различными системами. В формате JSON данные представляются в виде пар ключ-значение, где ключи - строки, а значения могут быть строками, числами, логическими значениями, массивами, объектами JSON или null.

    **Пример описания данных в формате JSON:**
    ```json
    {
        "name": "John",
        "age": 30,
        "city": "New York",
        "pets": [
            "dog",
            "cat"
        ]
    }
    ```

    В этом примере есть объект JSON, включающий имя, возраст, город и список животных. Имя и город являются строками, возраст - числом, а pets (животные) - массивом строк.

    **Пример взаимодействия с данными в формате JSON в Python:**
    ```python
    import json

    # Сериализация объекта Python в JSON
    data = {
        "name": "John",
        "age": 30,
        "city": "New York",
        "pets": [
            "dog",
            "cat"
        ]
    }
    json_data = json.dumps(data)

    # Сохранение JSON в файл
    with open("data.json", "w") as file:
        file.write(json_data)

    # Десериализация JSON в объект Python
    with open("data.json") as file:
        json_data = file.read()
        loaded_data = json.loads(json_data)

    # Использование данных
    print(loaded_data["name"])  # Выводит "John"
    print(loaded_data["age"])   # Выводит 30
    print(loaded_data["pets"][0])  # Выводит "dog"
    ```

    В этом примере мы используем модуль `json` для сериализации объекта Python в формат JSON с помощью функции `json.dumps()`. Затем мы сохраняем JSON в файл с помощью функции `file.write()`. Для десериализации JSON обратно в объект Python мы используем функцию `json.loads()`. После этого мы можем использовать данные в объекте Python, либо работать с ними напрямую, либо передавать в другие функции и модули для обработки.
    """)


def qw_13():
    """
    13. Формат XML и модель DOM: общая характеристика, пример описания данных в XML и DOM, работа с ними с помощью библиотеки BeautifulSoup.
    """
    print(qw_13.__doc__)

    # Доработанный ответ
    print("""
    **Формат XML (Extensible Markup Language):**

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

    **Модель DOM (Document Object Model):**

    Модель DOM представляет структуру XML-документа в виде дерева объектов, где каждый узел представляет тег или текстовую информацию. DOM позволяет программам манипулировать и обращаться к данным в XML-документе.

    **Пример описания данных в XML и DOM:**
    ```xml
    <person>
        <name>John Doe</name>
        <age>25</age>
        <city>New York</city>
    </person>
    ```

    В этом примере есть элемент `<person>`, включающий в себя теги `<name>`, `<age>`, и `<city>`, которые содержат текстовую информацию.

    **Работа с XML и DOM в Python с использованием BeautifulSoup:**
    ```python
    from bs4 import BeautifulSoup

    # Пример XML-документа
    xml_data = '<person><name>John Doe</name><age>25</age><city>New York</city></person>'

    # Создание объекта BeautifulSoup
    soup = BeautifulSoup(xml_data, 'xml')

    # Доступ к элементам XML
    person_name = soup.find('name').text
    person_age = soup.find('age').text
    person_city = soup.find('city').text

    print("Name:", person_name)
    print("Age:", person_age)
    print("City:", person_city)
    ```

    В этом примере мы используем библиотеку BeautifulSoup для парсинга XML-документа. Мы создаем объект BeautifulSoup с указанием типа парсера ('xml') и затем можем использовать методы поиска для извлечения данных из XML.

    Благодаря использованию формата XML и модели DOM, мы можем удобно хранить и обрабатывать структурированные данные в текстовом формате. Библиотека BeautifulSoup предоставляет удобные инструменты для работы с XML и DOM в Python.
    """)


def qw_14():
    """
    14. Форматы файлов NPY и HDF: общая характеристика, пример взаимодействия с данными этих форматов в Python.
    """
    print(qw_14.__doc__)

    # Доработанный ответ
    print("""
    **Форматы файлов NPY и HDF:**

    **NPY (NumPy):**

    NPY - это бинарный формат, разработанный для библиотеки NumPy, предназначенной для работы с многомерными массивами. Он компактен, быстро считывается и записывается, что делает его идеальным для хранения массивов данных. Однако, NPY не поддерживает сжатие данных и не имеет встроенной структуры каталогов.

    **HDF (Hierarchical Data Format):**

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

    **Пример взаимодействия с данными в этих форматах в Python:**

    ```python
    import numpy as np
    import h5py

    # Пример работы с NPY (NumPy)
    np_data = np.array([1, 2, 3, 4, 5])
    np.save('data.npy', np_data)

    loaded_np_data = np.load('data.npy')
    print("NumPy Array:", loaded_np_data)

    # Пример работы с HDF (HDF5)
    hdf_data = np.array([[1, 2, 3], [4, 5, 6]])
    with h5py.File('data.h5', 'w') as hdf_file:
        hdf_file.create_dataset('dataset', data=hdf_data)

    with h5py.File('data.h5', 'r') as hdf_file:
        loaded_hdf_data = np.array(hdf_file['dataset'])
        print("HDF5 Array:", loaded_hdf_data)
    ```

    В этом примере мы используем библиотеку NumPy для работы с массивами данных и сохраняем массив в форматах NPY (NumPy) и HDF5. Для HDF5 мы используем библиотеку h5py. Оба формата позволяют сохранять и загружать данные эффективно, но выбор между ними зависит от конкретных требований проекта.
    """)


def qw_15():
    """
    15. Взаимодействие из Python с базой данных на примере API SQLite, базовые возможности работы с транзакциями.
    """
    print(qw_15.__doc__)

    # Доработанный ответ
    print("""
    **Взаимодействие из Python с базой данных SQLite:**

    SQLite - легковесная встраиваемая система управления базами данных, поддерживающая множество языков программирования, включая Python. Для работы с SQLite из Python используется модуль `sqlite3`.

    **Пример создания соединения с базой данных:**
    ```python
    import sqlite3

    conn = sqlite3.connect('database.db')
    ```

    После создания соединения можно выполнять SQL-запросы. Для этого используется метод `execute()` объекта соединения:
    ```python
    c = conn.cursor()
    query = 'SELECT * FROM users'
    c.execute(query)
    ```

    **Работа с транзакциями:**
    ```python
    try:
        conn.begin_transaction()
        # Выполнение операций с базой данных
        conn.commit()
    except:
        conn.rollback()
        raise
    ```

    Для работы с транзакциями в SQLite используется объект соединения. Транзакции управляются с помощью методов `begin()`, `commit()` и `rollback()`. Если операции выполняются успешно, изменения фиксируются с помощью `commit()`. В случае ошибки, выполняется откат транзакции с использованием `rollback()`.

    Кроме того, SQLite поддерживает транзакции на уровне SQL, которые можно активировать с помощью команд `BEGIN TRANSACTION`, `COMMIT` и `ROLLBACK`.
    """)


def qw_16():
    """
    16. Взаимодействие с Excel из Python с помощью XLWings: принципы работы и примеры использования.
    """
    print(qw_16.__doc__)

    # Доработанный ответ
    print("""
    **Взаимодействие с Excel из Python с помощью XLWings:**

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

    **Установка XLWings:**
    Для использования XLWings необходимо установить библиотеку с помощью pip:
    ```bash
    pip install xlwings
    ```

    **Пример использования XLWings:**
    ```python
    import xlwings as xw

    # Открытие книги Excel
    wb = xw.Book('test.xlsx')

    # Получение доступа к листу
    ws = wb.sheets['Sheet1']

    # Вывод содержимого ячейки A1
    print(ws.range('A1').value)

    # Закрытие книги
    wb.close()
    ```

    В этом примере мы используем метод `Book()` для открытия книги 'test.xlsx' и получаем доступ к листу 'Sheet1' с помощью метода `sheets`. Затем мы извлекаем содержимое ячейки A1 с помощью свойства `value` объекта `ws.range('A1')`.

    Более сложные операции, такие как добавление и удаление ячеек, форматирование ячеек, работа с формулами и многое другое, также доступны в XLWings.
    """)


def qw_17():
    """
    17. Основы работы с регулярными выражениями: базовый синтаксис, примеры использования модуля re в Python.
    """
    print(qw_17.__doc__)

    # Доработанный ответ
    print("""
    **Основы синтаксиса регулярных выражений:**

    - **Литералы:** Просто сопоставляются с соответствующими символами в тексте. Например, регулярное выражение "abc" будет соответствовать любой строке, содержащей "abc".

    - **Метасимволы:** Специальные символы с особым значением. Например, точка "." соответствует любому символу, а символы "^" и "$" соответствуют началу и концу строки соответственно.

    - **Квантификаторы:** Определяют количество повторений символа или группы символов. Например, "+" соответствует 1 или более повторениям предыдущего символа или группы символов.

    - **Специальные последовательности:** Представляют собой комбинации символов, которые заранее определены и имеют специальное значение. Например, "\d" соответствует любой цифре, а "\s" - любому пробельному символу.

    **Пример использования модуля `re` в Python:**

    **Проверка соответствия строки регулярному выражению:**
    ```python
    import re
    pattern = r"abc"
    text = "abc123"
    if re.search(pattern, text):
        print("Match found!")
    else:
        print("No match found.")
    ```
    Вывод: "Match found!"

    **Извлечение части строки, соответствующей регулярному выражению:**
    ```python
    import re
    pattern = r"(\d+)"
    text = "abc123def456"
    matches = re.findall(pattern, text)
    print(matches)
    ```
    Вывод: ["123", "456"]

    В этом примере мы используем `re.search()` для проверки соответствия строки регулярному выражению и `re.findall()` для извлечения частей строки, соответствующих регулярному выражению.
    """)


def qw_18():
    """
    18. Сегментация и токенезация текста на естественном языке, стемминг и лемматизация, примеры на Python.
    """
    print(qw_18.__doc__)

    # Доработанный ответ
    print("""
    **Сегментация и токенизация текста на естественном языке:**

    *Сегментация текста* - это процесс разделения текста на отдельные слова, фразы или предложения. *Токенизация* - это процесс выделения отдельных слов из текста. Эти процессы часто используются при анализе текста на естественном языке.

    **Пример сегментации и токенизации текста на Python с использованием библиотеки NLTK:**
    ```python
    from nltk.tokenize import word_tokenize
    text = "Привет, как ты?"
    words = word_tokenize(text)
    print(words)
    ```
    Вывод: `['Привет', ',', 'как', 'ты', '?']`

    **Стемминг и лемматизация:**

    *Стемминг* - это процесс сокращения окончания слова до его основы. *Лемматизация* - это процесс приведения слова к его словарной форме.

    **Пример стемминга и лемматизации с использованием библиотеки WordNetLemmatizer:**
    ```python
    from nltk.stem import WordNetLemmatizer

    wnl = WordNetLemmatizer()
    text = "столы столы столы"
    words = text.split()
    stemmed_words = []
    lemmatized_words = []

    for word in words:
        stemmed = wnl.lemmatize(word)
        lemmatized = wnl.lemmatize(word, pos='v')  # Указываем часть речи для вербализации (глагол)
        stemmed_words.append(stemmed)
        lemmatized_words.append(lemmatized)

    print("Стеммированные слова:", stemmed_words)
    print("Лемматизированные слова:", lemmatized_words)
    ```
    Вывод:
    ```
    Стеммированные слова: ['столы', 'столы', 'столы']
    Лемматизированные слова: ['столы', 'столы', 'столы']
    ```

    В этом примере мы используем библиотеку NLTK для выполнения сегментации, токенизации, стемминга и лемматизации текста на русском языке.
    """)



def qw_19():
    """
    19. Расстояние Левенштейна: определение, алгоритм эффективного поиска оптимального редакционного предписания,
    пример поиска на Python.
    """
    print(qw_19.__doc__)

    # Доработанный ответ
    print("""
    **Расстояние Левенштейна:**

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

    **Алгоритм эффективного поиска оптимального редакционного предписания:**

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

    **Пример эффективного алгоритма поиска оптимального редакционного предписания на основе расстояния Левенштейна на Python:**
    ```python
    def levenshtein_distance(str1, str2):
        len_str1 = len(str1) + 1
        len_str2 = len(str2) + 1

        matrix = [[0 for n in range(len_str2)] for m in range(len_str1)]

        for i in range(len_str1):
            matrix[i][0] = i
        for j in range(len_str2):
            matrix[0][j] = j

        for i in range(1, len_str1):
            for j in range(1, len_str2):
                cost = 0 if str1[i - 1] == str2[j - 1] else 1
                matrix[i][j] = min(
                    matrix[i - 1][j] + 1,
                    matrix[i][j - 1] + 1,
                    matrix[i - 1][j - 1] + cost
                )

        return matrix[-1][-1]

    str1 = "кот"
    str2 = "скот"
    distance = levenshtein_distance(str1, str2)
    print(f"Расстояние Левенштейна между '{str1}' и '{str2}': {distance}")
    ```
    Вывод: "Расстояние Левенштейна между 'кот' и 'скот': 2"
    """)


def qw_20():
    """
    20. Векторное представление текста на естественном языке: общий алгоритм подходов TF; TF-IDF.
    """
    print(qw_20.__doc__)

    # Подробный ответ
    print("""
    **Векторное представление текста на естественном языке:**

    **TF (Term Frequency):**

    TF - это метод векторного представления текста, который измеряет, насколько часто определенное слово появляется в документе. 
    Он используется для определения важности слов в документе относительно других слов.

    **Алгоритм подхода TF:**
    1. Подготовка корпуса документов: Соберите и подготовьте коллекцию документов, в которых будет использоваться алгоритм TF.
    2. Токенизация документов: Разделите каждый документ на отдельные слова или токены. Удалите знаки препинания и стоп-слова.
    3. Подсчет частоты слов: Подсчитайте, сколько раз каждое слово встречается в каждом документе из корпуса.
    4. Построение векторов: Постройте векторы для каждого документа, используя частоту слов.

    **TF-IDF (Term Frequency-Inverse Document Frequency):**

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

    **Алгоритм подхода TF-IDF:**
    1. Подготовка корпуса документов: Тот же шаг, что и в алгоритме TF.
    2. Токенизация документов: Тот же шаг, что и в алгоритме TF.
    3. Подсчет TF-IDF: Подсчитайте значение TF-IDF для каждого слова в каждом документе. 
       TF-IDF вычисляется путем умножения значения TF для слова на обратную документную частоту (IDF) слова.
    4. Построение векторов: Постройте векторы для каждого документа, используя значения TF-IDF.
    """)


# Пример использования
if __name__ == "__main__":
    help(__name__)
    # qw_20()


