Compare commits

...

2 Commits

Author SHA1 Message Date
2b2eb9b72d romanova_adelina_lab_2 is ready 2023-11-28 01:00:12 +04:00
a8c58683dd kutygin_andrey_lab_3_ready 2023-11-13 20:53:33 +04:00
7 changed files with 466 additions and 0 deletions

View File

@@ -0,0 +1,118 @@
**Задание**
***
Решите с помощью библиотечной реализации дерева решений задачу из лабораторной работы «Веб-сервис «Дерево решений» по предмету «Методы искусственного интеллекта»на 99% ваших данных. Проверьте работу модели на оставшемся проценте, сделайте вывод
**Как запустить лабораторную**
***
Запустить файл main.py
**Используемые технологии**
***
Библиотеки pandas, scikit-learn, matplotlib, их компоненты
**Описание лабораторной (программы)**
***
В данном коде мы создаем и обучаем модель дерева решений для прогнозирования инцидентов с НЛО на основе набора данных.
1. В первой строке кода мы загружаем данные из CSV-файла 'ufo_data_nuforc.csv' с помощью функции pd.read_csv(). Эти данные содержат информацию о различных инцидентах с НЛО.
2. Далее мы выбираем набор признаков, в данном случае, эти признаки - населенность и время, которые будут использоваться для обучения модели, и сохраняем их в переменную features.
3. Затем преобразуем категориальные признаки в числовой вид при помощи функции pd.get_dummies(). Это необходимо, так как модель дерева решений работает только с числовыми данными.
4. После этого мы разделяем данные на обучающую и тестовую выборки с помощью функции train_test_split(). Обучающая выборка будет использоваться для обучения модели, а тестовая - для проверки ее точности.
5. Создаем модель дерева решений с помощью класса DecisionTreeClassifier() из библиотеки sklearn.tree.
6. Обучаем модель на обучающей выборке с помощью метода fit(). В процессе обучения модель настраивает параметры дерева решений, чтобы лучше предсказывать целевой признак.
7. После обучения модели, мы производим прогнозы на тестовых данных с помощью метода predict().
8. Оцениваем точность модели на тестовой выборке с помощью метода accuracy_score() из библиотеки sklearn.metrics. Этот метод сравнивает фактические значения целевого признака с предсказанными и возвращает точность модели.
9. Наконец, выводим точность модели на тестовой выборке, чтобы оценить, насколько хорошо модель предсказывает инциденты с НЛО.
10. Также, код визуализирует данные в виде графика с помощью библиотеки matplotlib.pyplot, отображая фактические значения целевого признака и предсказания модели. Это помогает наглядно оценить, насколько близки предсказания модели к реальным значениям.
**Результат**
***
Точность модели на тестовой выборке: 0.1377245508982036
Прогнозы по оставшемуся проценту данных: 'cylinder' 'circle' 'sphere' 'disk' 'disk' 'fireball' 'disk' 'oval'
'circle' 'disk' 'disk' 'other' 'light' 'light' 'oval' 'fireball' 'light'
'rectangle' 'chevron' 'unknown' 'sphere' 'oval' 'light' 'circle'
'unknown' 'unknown' 'disk' 'triangle' 'triangle' 'unknown' 'formation'
'unknown' 'cigar' 'unknown' 'light' 'other' 'rectangle' 'light' 'other'
'light' 'cylinder' 'delta' 'sphere' 'other' 'changing' 'fireball'
'cylinder' 'cigar' 'circle' 'triangle' 'light' 'fireball' 'fireball'
'sphere' 'circle' 'light' 'chevron' 'oval' 'oval' 'light' 'unknown'
'triangle' 'other' 'rectangle' 'triangle' 'triangle' 'flash' 'unknown'
'sphere' 'unknown' 'other' 'circle' 'oval' 'light' 'oval' 'formation'
'sphere' 'triangle' 'changing' 'sphere' 'oval' 'unknown' 'circle'
'circle' 'flash' 'light' 'light' 'sphere' 'other' 'other' 'egg' 'unknown'
'other' 'light' 'light' 'disk' 'diamond' 'oval' 'unknown' 'light'
'triangle' 'other' 'light' 'disk' 'unknown' 'light' 'changing' 'sphere'
'triangle' 'circle' 'flash' 'sphere' 'light' 'unknown' 'oval' 'formation'
'light' 'circle' 'unknown' 'other' 'triangle' 'other' 'light' 'disk'
'formation' 'oval' 'triangle' 'triangle' 'light' 'formation' 'oval'
'light' 'light' 'oval' 'disk' 'sphere' 'egg' 'unknown' 'unknown'
'unknown' 'light' 'disk' 'changing' 'light' 'light' 'circle' 'circle'
'formation' 'light' 'light' 'cigar' 'light' 'triangle' 'oval' 'fireball'
'cylinder' 'other' 'circle' 'egg' 'changing' 'triangle' 'circle' 'other'
'oval' 'disk' 'light' 'flash' 'fireball' 'circle' 'circle' 'circle'
'circle' 'light' 'disk' 'fireball' 'other' 'sphere' 'light' 'changing'
'cigar' 'light' 'cylinder' 'rectangle' 'chevron' 'light' 'light' 'light'
'light' 'circle' 'circle' 'light' 'light' 'circle' 'sphere' 'triangle'
'light' 'egg' 'circle' 'fireball' 'sphere' 'sphere' 'triangle' 'light'
'other' 'cigar' 'sphere' 'sphere' 'fireball' 'light' 'light' 'disk'
'oval' 'oval' 'other' 'cigar' 'triangle' 'light' 'light' 'light' 'disk'
'light' 'light' 'light' 'light' 'other' 'light' 'teardrop' 'triangle'
'teardrop' 'fireball' 'sphere' 'cylinder' 'fireball' 'circle' 'egg'
'sphere' 'disk' 'chevron' 'triangle' 'light' 'other' 'light' 'circle'
'rectangle' 'fireball' 'formation' 'light' 'light' 'circle' 'light'
'light' 'formation' 'light' 'triangle' 'light' 'oval' 'light' 'unknown'
'fireball' 'diamond' 'light' 'circle' 'light' 'triangle' 'oval' 'oval'
'cylinder' 'circle' 'light' 'disk' 'light' 'sphere' 'circle' 'light'
'triangle' 'light' 'fireball' 'triangle' 'light' 'flash' 'triangle' 'egg'
'disk' 'oval' 'circle' 'flash' 'light' 'oval' 'sphere' 'light' 'triangle'
'other' 'chevron' 'other' 'circle' 'unknown' 'unknown' 'sphere' 'light'
'cigar' 'light' 'fireball' 'circle' 'diamond' 'fireball' 'triangle'
'diamond' 'sphere' 'circle' 'chevron' 'cylinder' 'light' 'circle'
'fireball' 'unknown' 'light' 'circle' 'fireball' 'light' 'fireball'
'fireball' 'fireball' 'light' 'sphere' 'light' 'sphere' 'sphere'
'formation' 'light' 'fireball' 'fireball' 'disk' 'disk' 'circle'
'rectangle' 'unknown' 'disk' 'unknown' 'disk' 'triangle' 'other' 'sphere'
'diamond' 'light' 'light' 'unknown' 'sphere' 'circle' 'disk' 'circle'
'oval' 'changing' 'other' 'other' 'disk' 'unknown' 'unknown' 'disk'
'rectangle' 'disk' 'light' 'oval' 'unknown' 'sphere' 'light' 'changing'
'disk' 'disk' 'other' 'other' 'disk' 'cylinder' 'disk' 'rectangle'
'light' 'disk' 'disk' 'light' 'fireball' 'formation' 'cigar' 'oval'
'fireball' 'unknown' 'disk' 'light' 'light' 'triangle' 'triangle' 'light'
'sphere' 'triangle' 'sphere' 'circle' 'light' 'oval' 'oval' 'circle'
'oval' 'rectangle' 'disk' 'oval' 'light' 'light' 'other' 'cigar'
'triangle' 'disk' 'cigar' 'other' 'triangle' 'egg' 'unknown' 'triangle'
'light' 'triangle' 'disk' 'changing' 'triangle' 'disk' 'disk' 'rectangle'
'other' 'triangle' 'triangle' 'formation' 'triangle' 'egg' 'sphere'
'fireball' 'triangle' 'rectangle' 'light' 'triangle' 'triangle' 'other'
'light' 'light' 'disk' 'fireball' 'light' 'disk' 'oval' 'triangle'
'other' 'fireball' 'light' 'light' 'triangle' 'unknown' 'cigar' 'light'
'unknown' 'chevron' 'formation' 'disk' 'cigar' 'light' 'sphere' 'cigar'
'unknown' 'triangle' 'other' 'light' 'light' 'triangle' 'diamond' 'light'
'triangle' 'oval' 'changing' 'light' 'flash' 'circle' 'oval' 'other'
'sphere' 'circle' 'triangle' 'unknown' 'teardrop' 'unknown' 'fireball'
'light' 'light' 'cigar' 'cigar' 'light' 'fireball' 'other' 'egg' 'light'
'other' 'unknown' 'unknown' 'changing' 'circle' 'light' 'other' 'unknown'
'unknown' 'light' 'other' 'light' 'unknown' 'cylinder' 'triangle'
'circle' 'light' 'circle' 'circle' 'circle' 'light' 'light' 'changing'
'changing' 'circle' 'circle' 'triangle' 'triangle' 'light' 'light'
'light' 'light' 'other' 'changing' 'triangle' 'cylinder' 'light'
'unknown' 'circle' 'disk' 'sphere' 'oval' 'formation' 'teardrop'
'triangle' 'chevron' 'light' 'unknown' 'unknown' 'other' 'egg' 'circle'
'oval' 'cigar' 'unknown' 'chevron' 'oval' 'cigar' 'fireball' 'circle'
'unknown' 'light' 'sphere' 'fireball' 'changing' 'light' 'circle'
'unknown' 'fireball' 'light' 'sphere' 'light' 'formation' 'circle'
'fireball' 'formation' 'formation' 'formation' 'light' 'other' 'light'
'light' 'circle' 'diamond' 'oval' 'circle' 'oval' 'triangle' 'light'
'disk' 'light' 'other' 'triangle' 'triangle' 'cylinder' 'disk' 'cylinder'
'light' 'oval' 'cigar' 'circle' 'disk' 'light' 'unknown' 'circle' 'other'
'light' 'light' 'light' 'unknown' 'triangle' 'other' 'disk' 'cylinder'
'triangle' 'oval' 'disk' 'light' 'triangle' 'circle' 'light' 'other'
'light' 'other' 'circle' 'disk' 'other' 'triangle' 'oval' 'unknown'
'light' 'triangle' 'unknown' 'circle' 'unknown' 'light' 'fireball'
'fireball' 'rectangle' 'light' 'formation' 'unknown' 'light' 'light'
'formation' 'fireball' 'light' 'light' 'other' 'unknown' 'light'
'triangle' 'fireball' 'triangle' 'triangle' 'flash' 'circle' 'triangle'
'disk' 'light' 'unknown' 'light' 'light' 'fireball' 'circle' 'unknown'
'unknown' 'circle' 'disk' 'chevron' 'disk' 'disk' 'triangle' 'light'
'light' 'disk'
***Вывод:*** Наша модель дерева решений показала низкую точность предсказаний (Точность модели на тестовой выборке: 0.1377245508982036), что означает, что она не очень хорошо предсказывает форму НЛО на основе выбранных признаков (население и время). Из-за чего можно сделать вывод, что возможно, эти признаки недостаточно информативны или недостаточно связаны с формой НЛО.

View File

@@ -0,0 +1,39 @@
import pandas as pd
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import accuracy_score
from sklearn.model_selection import train_test_split
# Загрузка данных
data = pd.read_csv('ufo_sighting_data.csv')
# Выбор признаков
features = [ 'length_of_encounter_seconds', 'latitude', 'longitude']
target = 'UFO_shape'
# Удаление строк содержащих NaN
data.dropna(inplace=True)
# Удаление столбцов содержащих NaN
data.dropna(axis='columns', inplace=True)
# Разделение данных на обучающую и тестовую выборки
train_data, test_data, train_labels, test_labels = train_test_split(data[features], data[target], test_size=0.2, random_state=42)
# Создание и обучение модели дерева решений
model = DecisionTreeClassifier()
model.fit(train_data, train_labels)
# Прогнозирование на тестовой выборке
predictions = model.predict(test_data)
# Оценка точности модели
accuracy = accuracy_score(test_labels, predictions)
print('Точность модели на тестовой выборке:', accuracy)
# Прогнозирование на оставшемся проценте данных
remaining_data = data.drop(test_data.index)
remaining_predictions = model.predict(remaining_data[features])
# Вывод результатов
print('Прогнозы по оставшемуся проценту данных:', remaining_predictions)
# Сделайте необходимые выводы

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,109 @@
# Лабораторная работа №2. Вариант 21
## Тема:
Ранжирование признаков
## Модели:
- LinearRegression
- RandomizedLasso
- Recursive Feature Elimination (RFE)
## Как запустить программу:
Установить *python, numpy, matplotlib, sklearn*
```
python main.py --top_k=6
```
## Какие технологии использовались:
Язык программирования Python, библиотеки numpy, matplotlib, sklearn
Среда разработки VSCode
# Что делает лабораторная работа:
Генерирует данные и обучает такие модели, как: ```LinearRegression, RandomizedLasso, Recursive Feature Elimination (RFE)```
Производиться ранжирование признаков с помощью моделей ```LinearRegression, RandomizedLasso, Recursive Feature Elimination (RFE)```
Отображение получившихся результатов: top_k самых важных признака по среднему значению, значения признаков для каждой модели
Функция ```get_arguments()``` использует модуль argparse для обработки аргументов командной строки. В данном случае, скрипт ожидает аргумент --top_k, который по умолчанию установлен в 4.
```
def get_arguments():
parser = argparse.ArgumentParser()
parser.add_argument('--top_k', type=int, default=4, help='Кол-во самых выжных признаков')
args = parser.parse_args()
return args
```
Функция ```data_gen()``` генерирует искусственные данные для регрессионной задачи. Здесь используется функция Фридмана для создания зависимости целевой переменной Y от признаков X.
```
def data_gen():
# --- генерируем исходные данные: 750 строк-наблюдений и 14 столбцов-признаков ---
np.random.seed(0)
size = 750
X = np.random.uniform(0, 1, (size, 14))
#Задаем функцию-выход: регрессионную проблему Фридмана
Y = (10 * np.sin(np.pi*X[:,0]*X[:,1]) + 20*(X[:,2] - .5)**2 +
10*X[:,3] + 5*X[:,4]**5 + np.random.normal(0,1))
#Добавляем зависимость признаков
X[:,10:] = X[:,:4] + np.random.normal(0, .025, (size,4))
return X, Y
```
Функция ```rank_to_dict()```, которая преобразует ранги признаков в словарь, нормализуя их значения от 0 до 1.
```
def rank_to_dict(ranks, names):
ranks = np.abs(ranks)
minmax = MinMaxScaler()
ranks = minmax.fit_transform(np.array(ranks).reshape(14,1)).ravel()
ranks = map(lambda x: round(x, 2), ranks)
return dict(zip(names, ranks))
```
Функция ```print_sorted_data()``` печатает отсортированные оценки признаков.
Функцию ```estimation()``` вычисляет средние оценки признаков на основе данных о рангах.
В блоке ```if __name__=="__main__":``` сначала получает аргументы командной строки, затем генерирует данные, обучает линейную регрессию, применяет рекурсивное сокращение признаков и случайное Лассо, а затем оценивает и выводит наиболее важные признаки с использованием средних оценок.
### Линейная регрессия (Linear Regression)
Это простой метод машинного обучения, который используется для прогнозирования непрерывной переменной на основе одной или нескольких других переменных. В нашем случае линейная регрессия используется для обучения модели на данных, а затем коэффициенты модели используются для ранжирования важности признаков
### Рекурсивное сокращение признаков (RFE)
Это метод выбора признаков, который работает путем итеративного удаления признаков и переобучения модели, чтобы определить, какие признаки наиболее важны для предсказания. В коде RFE используется для обучения модели на данных, а затем ранжирование признаков модели используется для ранжирования важности признаков
### Случайное Лассо (Randomized Lasso)
Это метод регуляризации, который добавляет штраф к коэффициентам модели в зависимости от их величины. Это делается для предотвращения переобучения модели. В коде случайное Лассо используется для обучения модели на данных, а затем коэффициенты модели используются для ранжирования важности признаков
## Оценка работы моделей
```
{'x1': 0.33, 'x4': 0.23, 'x2': 0.2, 'x11': 0.2, 'x3': 0.17, 'x13': 0.16, 'x5': 0.06, 'x12': 0.06, 'x14': 0.04, 'x6': 0.01, 'x8': 0.01, 'x7': 0.0, 'x9': 0.0, 'x10': 0.0}
---------------------------------------------------------------------------
Параметр - x1, значение - 0.33
Параметр - x4, значение - 0.23
Параметр - x2, значение - 0.2
Параметр - x11, значение - 0.2
---------------------------------------------------------------------------
Linear reg
[('x1', 1.0), ('x4', 0.69), ('x2', 0.61), ('x11', 0.59), ('x3', 0.51), ('x13', 0.48), ('x5', 0.19), ('x12', 0.19), ('x14', 0.12), ('x8', 0.03), ('x6', 0.02), ('x10', 0.01), ('x7', 0.0), ('x9', 0.0)]
RFE
[('x9', 1.0), ('x7', 0.86), ('x10', 0.71), ('x6', 0.57), ('x8', 0.43), ('x14', 0.29), ('x12', 0.14), ('x1', 0.0), ('x2', 0.0), ('x3', 0.0), ('x4', 0.0), ('x5', 0.0), ('x11', 0.0), ('x13', 0.0)]
RandomizedLasso
[('x4', 1.0), ('x2', 0.37), ('x1', 0.36), ('x5', 0.32), ('x6', 0.02), ('x8', 0.02), ('x3', 0.01), ('x7', 0.0), ('x9', 0.0), ('x10', 0.0), ('x11', 0.0), ('x12', 0.0), ('x13', 0.0), ('x14', 0.0)]
```
Хуже всех показала себя модель случайного Лассо, потеряв три значимые признака и добавив один лишний. Модели линейной регрессии и RFE допустили по одной ошибке, однако последняя не потеряла ни одного значимого признака. Значимость в среднем получилась неудовлетворительной и выдала три ошибки, как и первая модель.
Исходя из этого, можно сделать вывод, что для ранжирования признаков лучше использовать специально созданные для этого инструменты по типу RFE, а не использовать коэффициенты признаков регрессионных моделей.

View File

@@ -0,0 +1,76 @@
from sklearn.utils import check_X_y, check_random_state
from sklearn.linear_model import Lasso
from scipy.sparse import issparse
from scipy import sparse
def _rescale_data(x, weights):
if issparse(x):
size = weights.shape[0]
weight_dia = sparse.dia_matrix((1 - weights, 0), (size, size))
x_rescaled = x * weight_dia
else:
x_rescaled = x * (1 - weights)
return x_rescaled
class RandomizedLasso(Lasso):
"""
Randomized version of scikit-learns Lasso class.
Randomized LASSO is a generalization of the LASSO. The LASSO penalises
the absolute value of the coefficients with a penalty term proportional
to `alpha`, but the randomized LASSO changes the penalty to a randomly
chosen value in the range `[alpha, alpha/weakness]`.
Parameters
----------
weakness : float
Weakness value for randomized LASSO. Must be in (0, 1].
See also
--------
sklearn.linear_model.LogisticRegression : learns logistic regression models
using the same algorithm.
"""
def __init__(self, weakness=0.5, alpha=1.0, fit_intercept=True,
precompute=False, copy_X=True, max_iter=1000,
tol=1e-4, warm_start=False, positive=False,
random_state=None, selection='cyclic'):
self.weakness = weakness
super(RandomizedLasso, self).__init__(
alpha=alpha, fit_intercept=fit_intercept, precompute=precompute, copy_X=copy_X,
max_iter=max_iter, tol=tol, warm_start=warm_start,
positive=positive, random_state=random_state,
selection=selection)
def fit(self, X, y):
"""Fit the model according to the given training data.
Parameters
----------
X : {array-like, sparse matrix}, shape = [n_samples, n_features]
The training input samples.
y : array-like, shape = [n_samples]
The target values.
"""
if not isinstance(self.weakness, float) or not (0.0 < self.weakness <= 1.0):
raise ValueError('weakness should be a float in (0, 1], got %s' % self.weakness)
X, y = check_X_y(X, y, accept_sparse=True)
n_features = X.shape[1]
weakness = 1. - self.weakness
random_state = check_random_state(self.random_state)
weights = weakness * random_state.randint(0, 1 + 1, size=(n_features,))
# TODO: I am afraid this will do double normalization if set to true
#X, y, _, _ = _preprocess_data(X, y, self.fit_intercept, normalize=self.normalize, copy=False,
# sample_weight=None, return_mean=False)
# TODO: Check if this is a problem if it happens before standardization
X_rescaled = _rescale_data(X, weights)
return super(RandomizedLasso, self).fit(X_rescaled, y)

View File

@@ -0,0 +1,106 @@
from RandomizedLass import RandomizedLasso
import argparse
from sklearn.linear_model import LinearRegression, Ridge, Lasso
from sklearn.feature_selection import RFE
from sklearn.preprocessing import MinMaxScaler
import numpy as np
def get_arguments():
parser = argparse.ArgumentParser()
parser.add_argument('--top_k', type=int, default=4, help='Кол-во самых выжных признаков')
args = parser.parse_args()
return args
def data_gen():
# --- генерируем исходные данные: 750 строк-наблюдений и 14 столбцов-признаков ---
np.random.seed(0)
size = 750
X = np.random.uniform(0, 1, (size, 14))
#Задаем функцию-выход: регрессионную проблему Фридмана
Y = (10 * np.sin(np.pi*X[:,0]*X[:,1]) + 20*(X[:,2] - .5)**2 +
10*X[:,3] + 5*X[:,4]**5 + np.random.normal(0,1))
#Добавляем зависимость признаков
X[:,10:] = X[:,:4] + np.random.normal(0, .025, (size,4))
return X, Y
def rank_to_dict(ranks, names):
ranks = np.abs(ranks)
minmax = MinMaxScaler()
ranks = minmax.fit_transform(np.array(ranks).reshape(14,1)).ravel()
ranks = map(lambda x: round(x, 2), ranks)
return dict(zip(names, ranks))
def print_sorted_data(ranks: dict):
print()
for key, value in ranks.items():
ranks[key] = sorted(value.items(), key=lambda item: item[1], reverse=True)
for key, value in ranks.items():
print(key)
print(value)
def estimation(ranks: dict, top_k):
#Создаем пустой список для данных
mean = {}
#«Бежим» по списку ranks
for key, value in ranks.items():
#«Пробегаемся» по списку значений ranks, которые являются парой имя:оценка
for item in value.items():
#имя будет ключом для нашего mean
#если элемента с текущим ключем в mean нет - добавляем
if (item[0] not in mean):
mean[item[0]] = 0
#суммируем значения по каждому ключу-имени признака
mean[item[0]] += item[1]
#находим среднее по каждому признаку
for key, value in mean.items():
res=value/len(ranks)
mean[key] = round(res, 2)
#сортируем и распечатываем список
mean_sorted = dict(sorted(mean.items(), key=lambda x:x[1], reverse=True))
print("sorted MEAN")
print(mean_sorted, '---'*25, sep='\n')
for item in list(mean_sorted.items())[:top_k]:
print(f'Параметр - {item[0]}, значение - {item[1]}')
print('---'*25)
if __name__=="__main__":
args = get_arguments()
X,Y = data_gen()
# Линейная модель
lr = LinearRegression()
lr.fit(X, Y)
# Рекурсивное сокращение признаков
rfe = RFE(lr)
rfe.fit(X, Y)
# Случайное Лассо
randomized_lasso = RandomizedLasso(alpha=.01)
randomized_lasso.fit(X, Y)
names = ["x%s" % i for i in range(1,15)]
ranks = {}
ranks["Linear reg"] = rank_to_dict(lr.coef_, names)
ranks["RFE"] = rank_to_dict(rfe.ranking_, names)
ranks["RandomizedLasso"] = rank_to_dict(randomized_lasso.coef_, names)
estimation(ranks, args.top_k)
print_sorted_data(ranks)

View File

@@ -0,0 +1,17 @@
contourpy==1.2.0
cycler==0.12.1
fonttools==4.45.1
importlib-resources==6.1.1
joblib==1.3.2
kiwisolver==1.4.5
matplotlib==3.8.2
numpy==1.26.2
packaging==23.2
Pillow==10.1.0
pyparsing==3.1.1
python-dateutil==2.8.2
scikit-learn==1.3.2
scipy==1.11.4
six==1.16.0
threadpoolctl==3.2.0
zipp==3.17.0