Skip to content

Commit 45fe2fb

Browse files
committed
The project has been brought to the running stage.
Model prediction and training algorithms are built. The architecture has been changed.
1 parent 076fa1e commit 45fe2fb

23 files changed

+275625
-189
lines changed

.env.example

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
WEIGHTS_USE=True
22

33
KNN_WEIGHT=0.8
4-
LOGISTICS_REGRESSION_WEIGHT=0.7
5-
XGBOOST_WEIGHT=1
4+
TREECLASSIFIER_WEIGHT=0.7
5+
RANDOMFORESTCLASSIFIER_WEIGHT=1

README.md

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,20 @@
2424
1. Клонируем проект с GitHub: `git clone https://github.com/DIMFLIX-OFFICIAL/disk-destiny.git`
2525
2. Переходим в папку с проектом: `cd disk-destiny`
2626
3. Устанавливаем зависимости: `poetry install`
27-
4. Запускаем программу: `poetry run python disk_destiny/app.py`
28-
29-
Программа будет запускаться в терминале и предложить выбрать действие для выполнения.
27+
4. В папку `data/prediction` копируем нужные датасеты
28+
5. Запускаем программу `poetry run python disk_destiny/app.py` и следуем дальнейшим инструкциям.
3029

3130
> [!important]
3231
> Для выбора нескольких значений в списке нужно нажать клавишу 'Space'.
3332
> Если возможно выбрать только одно значение из списка, нужно просто нажать клавишу 'Enter'.
3433
3534
# Разработчикам
35+
## Обучение моделей
36+
Для обучения моделей требуется скопировать тренировочные датасеты в папку `data/train`.
37+
Далее запустить программу `poetry run python disk_destiny/app.py` и выбрать опцию `Дообучение модели`.
38+
Выбираем нужную модель датасеты для обучения.
39+
40+
## Добавление новых моделей
3641
Если вы хотите добавить новые модели, то вам нужно будет создать файл с именем `models/your_model.py` в корневой папке проекта.
3742

3843
Класс вашей модели должен наследоваться от класса `BaseModel` и реализовывать методы `train` и `predict`. Так-же у вашего класса должен быть атрибут `name` со значением типа `str`.
@@ -53,28 +58,28 @@
5358
<img src="https://github.com/DIMFLIX-OFFICIAL.png?size=100" width="100px;" alt=""/><br/>
5459
<b>Пронин Дмитрий</b>
5560
</a><br/>
56-
<sub>Построение и оптимизация алгоритмов.<br>Разработка TUI</sub>
61+
<sub>Algorithmic Designer<br>System Architect</sub>
5762
</td>
5863
<td align="center">
5964
<a href="https://github.com/K1rsn7">
6065
<img src="https://github.com/K1rsn7.png?size=100" width="100px;" alt=""/><br />
6166
<b>Сухоруков Кирилл</b>
6267
</a><br />
63-
<sub>Построение алгоритмов.<br>Обучение моделей</sub>
68+
<sub>ML Engineer</sub>
6469
</td>
6570
<td align="center">
6671
<a href="https://github.com/AsDo001">
6772
<img src="https://github.com/AsDo001.png?size=100" width="100px;" alt=""/><br />
6873
<b>Донсков Арсений</b>
6974
</a><br />
70-
<sub>Построение алгоритмов.<br>Обучение моделей</sub>
75+
<sub>ML Engineer</sub>
7176
</td>
7277
<td align="center">
7378
<a href="https://github.com/Sweepyd1">
7479
<img src="https://github.com/Sweepyd1.png?size=100" width="100px;" alt=""/><br />
7580
<b>Яшин Дмитрий</b>
7681
</a><br />
77-
<sub>Построение алгоритмов.<br>Обучение моделей</sub>
82+
<sub>Data Engineer</sub>
7883
</td>
7984
</tr>
8085
</table>

disk_destiny/app.py

Lines changed: 109 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,27 @@
1-
import os
21
import inquirer
32
import numpy as np
3+
import pandas as pd
44
from dotenv import load_dotenv
5-
from typing import Dict, List
5+
from typing import Dict, List, Tuple
66

77
import models
88
from models.base import BaseModel
9-
from utils.other import print_pure_banner
9+
from utils.data_normalizing import Normalizer
10+
from utils.other import print_pure_banner, get_csv_files, ask_list, ask_checkbox
11+
from config import WEIGHTS, WEIGHTS_USE, PATH_TO_PREDICT_DATASETS, PATH_TO_PREDICT_NORMALIZED_DATASETS, PATH_TO_TRAIN_DATASETS
1012

1113

12-
class App:
14+
class Application:
15+
weights_use: bool
1316
allowed_models: Dict[str, BaseModel]
1417

1518
def __init__(self, weights: Dict[str, float], weights_use: bool = True) -> None:
19+
self.weights_use = weights_use
1620
self.allowed_models = {}
21+
22+
PATH_TO_PREDICT_DATASETS.mkdir(exist_ok=True, parents=True)
23+
PATH_TO_PREDICT_NORMALIZED_DATASETS.mkdir(exist_ok=True, parents=True)
24+
PATH_TO_TRAIN_DATASETS.mkdir(exist_ok=True, parents=True)
1725

1826
for model in BaseModel.__subclasses__():
1927
weight = weights.get(model.name.upper(), 1)
@@ -30,58 +38,117 @@ def __init__(self, weights: Dict[str, float], weights_use: bool = True) -> None:
3038

3139
def run(self) -> None:
3240
print_pure_banner()
33-
self.ask_questions()
41+
self.main_menu()
3442

35-
def ask_questions(self) -> None:
36-
main_menu_answer = inquirer.prompt([
37-
inquirer.List('answer',
38-
message="Что будем делать?",
39-
choices=['Дообучение модели', 'Получить предсказание', 'Выход'],
40-
)
41-
])['answer']
42-
43+
def main_menu(self) -> None:
44+
main_menu_answer = ask_list("Что будем делать?", ['Дообучение модели', 'Получить предсказание', 'Выход'])
4345
print_pure_banner()
4446

4547
match main_menu_answer:
4648
case 'Дообучение модели':
47-
model = inquirer.prompt([
48-
inquirer.List('answer',
49-
message="Выберите модель для дообучения",
50-
choices=self.allowed_models,
51-
)
52-
])['answer']
53-
self.allowed_models[model].train()
49+
self.train()
5450

5551
case 'Получить предсказание':
56-
models = inquirer.prompt([
57-
inquirer.Checkbox('answer',
58-
message="Выберите модели для предсказания.",
59-
choices=self.allowed_models,
60-
)
61-
])['answer']
62-
print(models)
63-
self.predict(list_models=models)
52+
self.predict()
6453

6554
case 'Выход':
6655
exit()
6756

68-
def predict(self, list_models: List[BaseModel], use_weights: bool = True) -> float:
69-
results = {i.weight: i.predict() for i in list_models}
70-
71-
if use_weights:
72-
return np.average(results.values(), axis=0, weights=results.keys())
57+
def train(self) -> None:
58+
model = ask_list("Выберите модель для дообучения", self.allowed_models)
59+
print_pure_banner()
60+
61+
csv_files = get_csv_files(path=PATH_TO_TRAIN_DATASETS)
62+
if not csv_files:
63+
print("Нет датасетов для обучения!")
64+
exit()
65+
66+
dataset_filename = ask_list("Выберите тренировочный датасет.", csv_files)
67+
print(dataset_filename)
68+
69+
70+
data = Normalizer.get_df_for_train(PATH_TO_TRAIN_DATASETS / dataset_filename)
71+
self.allowed_models[model].train(data)
72+
73+
def predict(self) -> float:
74+
list_models: List[BaseModel] = ask_checkbox("Выберите модели для предсказания.",self.allowed_models, default=list(self.allowed_models.keys()))
75+
list_models = [self.allowed_models[i] for i in list_models]
76+
print_pure_banner()
77+
78+
csv_files = get_csv_files(path=PATH_TO_PREDICT_DATASETS)
79+
if not csv_files:
80+
print("Нет датасетов для предсказания!")
81+
exit()
82+
83+
dataset_filename = ask_list("Выберите датасет.", csv_files)
84+
85+
##==> Нормализация данных
86+
##########################################################
87+
csv_path = PATH_TO_PREDICT_DATASETS/ dataset_filename
88+
csv_normalized_path = PATH_TO_PREDICT_NORMALIZED_DATASETS / dataset_filename
89+
90+
if csv_normalized_path.exists() and inquirer.confirm("Данные уже преобразованы. Хотите продолжить без повторной обработки?", default=True):
91+
print("Продолжаем с преобразованными данными...")
92+
X = pd.read_csv(csv_normalized_path)
7393
else:
74-
return np.mean(results.values(), axis=0)
94+
print("Преобразуем данные... Это может занять некоторое время.")
95+
X = Normalizer.get_df_for_predict(csv_path)
7596

97+
print_pure_banner()
98+
print("Начинаем предсказывание отказоустойчивости дисков...")
99+
results: Dict[float, np.ndarray] = {i.weight: i.predict(X=X) for i in list_models}
76100

77-
if __name__ == "__main__":
78-
load_dotenv()
79-
weights_use = os.environ.get('WEIGHTS_USE', 'true').lower() == 'true'
101+
##==> Подсчёт среднего отказа дисков по моделям
102+
##########################################################
103+
def calculate_destroyed_devices(shans: Dict[str, List[float]], count_models: int) -> Tuple[int, List[str]]:
104+
count_destroy = 0
105+
destroy_devices = []
106+
for key, values in shans.items():
107+
for month in range(4):
108+
if values[month] / count_models > 0.7:
109+
count_destroy += 1
110+
destroy_devices.append(key)
111+
return count_destroy, destroy_devices
80112

81-
weights = {
82-
key.replace("_WEIGHT", ""): value
83-
for key, value in os.environ.items() if key.endswith('_WEIGHT')
84-
}
113+
if self.weights_use:
114+
shans = {}
115+
count_models = len(results.keys())
116+
117+
for weight, pred in results.items():
118+
for key in pred.keys():
119+
if key not in shans:
120+
shans[key] = [0, 0, 0, 0]
121+
for month in range(4):
122+
shans[key][month] += pred[key][month] * weight
85123

86-
app = App(weights=weights, weights_use=weights_use)
124+
count_destroy_3month, destroy_devices3 = calculate_destroyed_devices(shans, count_models)
125+
count_destroy_6month, destroy_devices6 = calculate_destroyed_devices(shans, count_models)
126+
count_destroy_9month, destroy_devices9 = calculate_destroyed_devices(shans, count_models)
127+
count_destroy_12month, destroy_devices12 = calculate_destroyed_devices(shans, count_models)
128+
129+
else:
130+
shans_no_weight = {}
131+
count_models = len(results.keys())
132+
133+
for pred in results.values():
134+
for key in pred.keys():
135+
if key not in shans_no_weight:
136+
shans_no_weight[key] = [0, 0, 0, 0]
137+
for month in range(4):
138+
shans_no_weight[key][month] += pred[key][month]
139+
140+
count_destroy_3month, destroy_devices3 = calculate_destroyed_devices(shans_no_weight, count_models)
141+
count_destroy_6month, destroy_devices6 = calculate_destroyed_devices(shans_no_weight, count_models)
142+
count_destroy_9month, destroy_devices9 = calculate_destroyed_devices(shans_no_weight, count_models)
143+
count_destroy_12month, destroy_devices12 = calculate_destroyed_devices(shans_no_weight, count_models)
144+
145+
print(f"\n\n[{count_destroy_3month}] Устройств выйдет из строя в течение 3 месяцев: {destroy_devices3}")
146+
print(f"[{count_destroy_6month}] Устройств выйдет из строя в течение 6 месяцев: {destroy_devices6}")
147+
print(f"[{count_destroy_9month}] Устройств выйдет из строя в течение 9 месяцев: {destroy_devices9}")
148+
print(f"[{count_destroy_12month}] Устройств выйдет из строя в течение 12 месяцев: {destroy_devices12}")
149+
150+
151+
if __name__ == "__main__":
152+
load_dotenv()
153+
app = Application(weights=WEIGHTS, weights_use=WEIGHTS_USE)
87154
app.run()

disk_destiny/config.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import os
2+
from pathlib import Path
3+
from dotenv import load_dotenv
4+
5+
load_dotenv()
6+
7+
WEIGHTS_USE = os.environ.get('WEIGHTS_USE', 'true').lower() == 'true'
8+
WEIGHTS = {
9+
key.replace("_WEIGHT", ""): value
10+
for key, value in os.environ.items() if key.endswith('_WEIGHT')
11+
}
12+
13+
PROJECT_ROOT = Path(__file__).parent.parent
14+
PROJECT_SRC = Path(__file__).parent
15+
16+
17+
PATH_TO_DATA = PROJECT_ROOT / "data"
18+
PATH_TO_PARAMS = PROJECT_SRC / "params"
19+
20+
PATH_TO_PREDICT_DATASETS = PROJECT_ROOT / "data" / "prediction"
21+
PATH_TO_PREDICT_NORMALIZED_DATASETS = PROJECT_ROOT / "data" / "prediction" / "normalized"
22+
PATH_TO_TRAIN_DATASETS = PROJECT_ROOT / "data" / "train"
23+
24+
PATH_TO_WEIGHTS = PROJECT_SRC / "models" / "weights"
25+
PATH_TO_COLUMNS = PATH_TO_PARAMS / "column_good.json"
26+
PATH_TO_MEDIANS = PATH_TO_PARAMS / "medians_value.json"
27+
PATH_TO_MODELS_CODING = PATH_TO_PARAMS / "model_coding.json"
28+
PATH_TO_START_HDD = PATH_TO_PARAMS / "start_date_HDD.csv"

disk_destiny/models/KNN.py

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,67 @@
1+
from pandas import DataFrame
2+
import numpy as np
3+
from sklearn.neighbors import KNeighborsClassifier
4+
from sklearn.model_selection import GridSearchCV
5+
from sklearn.metrics import accuracy_score, f1_score
6+
import pickle
7+
from typing import List
8+
19
from .base import BaseModel
10+
from schemes import TrainData
11+
from config import PATH_TO_WEIGHTS
212

313

414
class KNN(BaseModel):
515
name = 'KNN'
616

7-
def train(self) -> None:
8-
pass
17+
def __init__(self, weight: float = 1) -> None:
18+
super().__init__(weight=weight)
19+
self.model = KNeighborsClassifier()
20+
21+
def _train(self, data: TrainData) -> None:
22+
def get_matrix(model, data: TrainData) -> dict:
23+
pred = model.predict(data.X_test)
24+
metrics = {}
25+
for i, column in enumerate(data.y_test.columns):
26+
accuracy = accuracy_score(data.y_test[column], pred[:, i])
27+
f1 = f1_score(data.y_test[column], pred[:, i])
28+
29+
metrics[column] = {
30+
'Accuracy':accuracy,
31+
'F1 Score': f1
32+
}
33+
return metrics
34+
35+
with open(PATH_TO_WEIGHTS / "KNN.pkl", 'rb') as f:
36+
self.model = pickle.load(f)
37+
38+
# Считаем метрики старой модели
39+
old_metrics = get_matrix(self.model, data)
40+
mean_old_f1 = np.mean([v['F1 Score'] for v in old_metrics.values()])
41+
mean_old_accuracy = np.mean([v['Accuracy'] for v in old_metrics.values()])
42+
43+
# Обучение модели
44+
param_grid = {'n_neighbors': [3, 5, 7, 9, 11], 'metric': ['euclidean', 'manhattan']}
45+
grid_search = GridSearchCV(self.model, param_grid, cv=5, verbose=2)
46+
grid_search.fit(data.X_train, data.y_train)
47+
self.model = grid_search.best_estimator_
48+
49+
# Считаем метрики новой модели
50+
new_metrics = get_matrix(self.model, data)
51+
mean_new_f1 = np.mean([v['F1 Score'] for v in new_metrics.values()])
52+
mean_new_accuracy = np.mean([v['Accuracy'] for v in new_metrics.values()])
53+
54+
# Сравниваем, стала-ли лучше модель или нет
55+
if mean_new_f1 > mean_old_f1 and mean_new_accuracy > mean_old_accuracy:
56+
with open(PATH_TO_WEIGHTS / "KNN.pkl", 'wb') as f:
57+
pickle.dump(self.model, f)
58+
print(f"Модель улучшилась после дообучения! Accuracy: {mean_old_accuracy} -> {mean_new_accuracy}, F1 Score: {mean_old_f1} -> {mean_new_f1}!")
59+
else:
60+
print("Модель не улучшилась после дообучения!")
961

10-
def predict(self) -> float:
11-
pass
62+
def _predict(self, X: DataFrame) -> List[list]:
63+
with open(PATH_TO_WEIGHTS / "KNN.pkl", 'rb') as f:
64+
self.model = pickle.load(f)
65+
66+
predictions = self.model.predict(X[:10])
67+
return {key: value for key, value in zip(X.index.values, predictions)}

0 commit comments

Comments
 (0)