Files
2022_oknardia/oknardia/web/management/commands/make_rating.py

1107 lines
56 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""
Django management command: make_rating
Вычисляет (каждый раз заново) рейтинги оконных профилей и стеклопакетов.
Вычисление базируется на ренкинге методом Манна-Уитни (Mann-Whitney U test шаг).
В базовом ренкинге участвуют только профили и стеклопакеты, присутствующие
в коммерческих предложениях размещённых в ОКНАРДИИ.
АЛГОРИТМ РАНЖИРОВАНИЯ (Mann-Whitney U Step Rank):
====================================================
1. Для каждой характеристики (параметра):
- Сортируем список объектов по значению параметра (от МЕНЬШЕГО к БОЛЬШЕМУ)
- Начинаем с начального ранга = 0
- Проходим по отсортированному списку и увеличиваем ранг на вес характеристики
ТОЛьКО когда значение МЕНЯЕТСЯ на новое уникальное значение
2. Логика определения "улучшения":
- revers=False (ПО ВОЗРАСТАНИЮ):
* Начинаем с МИНИМУМА, повышаем ранг при УВЕЛИЧЕНИИ значения
* Используется для параметров где БОЛЬШЕ = ЛУЧШЕ
* Примеры: HeatTransfer (Ro), Soundproofing (дБ), LightTransmission (%)
- revers=True (ПО УБЫВАНИЮ):
* Начинаем с МАКСИМУМА, повышаем ранг при УМЕНЬШЕНИИ значения
* Используется для параметров где МЕНЬШЕ = ЛУЧШЕ
* Примеры: PassingSun (%), Height (для фальца)
3. После ранжирования по всем параметрам:
- Суммируем ранги по каждому объекту
- Нормализуем итоговый ранг: (значение - минимум) / максимум
- Переводим в финальный рейтинг: нормализованный * 5.0 звёзд
ДИАПАЗОНЫ РЕЙТИНГА:
===================
- ТемпоральныйРейтинг (TmpRating): от 0 до максимальной суммы всех рангов
- Нормализованный (RatingConsist): 0.0 … 1.0 (для каждого параметра)
- Финальный рейтинг профиля/стеклопакета (fProfileRating/fGlazingRating): 0.0 … 5.0 звёзд
ПАРАМЕТРЫ РАНЖИРОВАНИЯ И ИХ НАПРАВЛЕНИЯ:
==========================================
ПРОФИЛИ (PVCprofiles):
- fProfileSoundproofing (дБ) → БОЛЬШЕ дБ = ЛУЧШЕ звукоизоляция → revers=False
- fProfileHeatTransf (м²×°C/Вт) → БОЛЬШЕ сопротивление = ЛУЧШЕ теплоизоляция → revers=False
- iProfileHeight (см) → МЕНЬШЕ высота в проёме = ЛУЧШЕ (экономия) → revers=True
- iProfileRabbet (мм) → БОЛЬШЕ высота фальца = ЛУЧШЕ (надежнее) → revers=False
- iProfileGlazingThickness (мм) → БОЛЬШЕ толщина = ЛУЧШЕ → revers=False
- iProfileThickness (мм) → БОЛЬШЕ толщина профиля = ЛУЧШЕ → revers=False
- fProfileSeals (контуры) → БОЛЬШЕ контуров = ЛУЧШЕ уплотнение → revers=False
- iProfileCamerasN (камер) → БОЛЬШЕ камер = ЛУЧШЕ теплоизоляция → revers=False
- NumOffer (кол-во предложений) → БОЛЬШЕ предложений = популярнее → revers=False
СТЕКЛОПАКЕТЫ (Glazing):
- fGlazingHeatTransfer (м²×°C/Вт) → БОЛЬШЕ сопротивление = ЛУЧШЕ теплоизоляция → revers=False
- fGlazingSoundproofing (дБ) → БОЛЬШЕ дБ = ЛУЧШЕ звукоизоляция → revers=False
- fGlazingLightTransmission (%) → БОЛЬШЕ светопропускание = ЛУЧШЕ видимость → revers=False ⚠️ ИСПРАВЛЕНО
- fGlazingPassingSun (%) → МЕНЬШЕ солнцепропускания = ЛУЧШЕ (летнее охлаждение) → revers=True
- iGlazingThickness (мм) → БОЛЬШЕ толщина = ЛУЧШЕ → revers=False
- iGlazingCamerasN (камер) → БОЛЬШЕ камер = ЛУЧШЕ теплоизоляция → revers=False
НАБОРЫ УСЛУГ (SetKit) - КОМБИНИРОВАННЫЙ РЕЙТИНГ:
Рейтинг набора = k1 (услуги) + k2 (стеклопакет) + k3 (профиль)
Параметры услуг ранжирования:
- dModify (дата последнего обновления) → МЕНЬШЕ дата = ЛУЧШЕ (свежее) → revers=True
- bSetDelivery (доставка включена) → БОЛЬШЕ = ЛУЧШЕ → revers=False
- bSetUninstallInstall (монтаж/демонтаж) → БОЛЬШЕ = ЛУЧШЕ → revers=False
- sSetSill (подоконник) → БОЛЬШЕ = ЛУЧШЕ → revers=False
- sSetPanes (водоотлив) → БОЛЬШЕ = ЛУЧШЕ → revers=False
- sSetSlope (откос) → БОЛЬШЕ = ЛУЧШЕ → revers=False
- sSetClimateControl (климат-контроль) → БОЛЬШЕ = ЛУЧШЕ → revers=False
- NumOffer (кол-во предложений) → БОЛЬШЕ = ЛУЧШЕ (популярность) → revers=False
- iDiscountVariantsCount (гибкость скидок) → БОЛЬШЕ вариантов = ЛУЧШЕ → revers=False
- fDiscountMax (размер скидок) → БОЛЬШЕ скидка = ЛУЧШЕ → revers=False
Веса компонентов набора в итоговом рейтинге:
- k1 (услуги): RARING_SET_MAX - RARING_WEIGHT_PVC_PROFILE_IN_SET - RARING_WEIGHT_GLAZING_IN_SET (остаток)
- k2 (стеклопакет): RARING_WEIGHT_GLAZING_IN_SET (обычно 1.5)
- k3 (профиль): RARING_WEIGHT_PVC_PROFILE_IN_SET (обычно 1.5)
- Итого: от 0.0 до 5.0 звёзд
ВАЖНО:
- sGlazingLightReflectance (строка "30/20") - НЕ РАНЖИРУЕТСЯ, это только информация
- В России отопление независимое, поэтому низкий SHGC (PassingSun) лучше для летнего охлаждения
- Рейтинг наборов зависит от качества входящих компонентов (профиль + стеклопакет) И услуг (доставка, монтаж и т.д.)
ТЕХНИЧЕСКАЯ ИНФОРМАЦИЯ:
=======================
- Функция GetRank() реализует алгоритм Mann-Whitney U test
- Prepare_PVC_Dictionary() исправляет данные перед ранжированием
* Конвертирует iProfileCamerasN из строки (например "3/3") в число суммированием цифр
* Для деревянных окон (камер = 0) ставит 1000 (максимум, т.к. дерево лучше)
* Для профилей с высотой < 10см ставит 1000 (не валидные данные)
- Prepare_GLAZ_Dictionary() исправляет данные стеклопакетов
* Для светопропускания/солнцепропускания < 1% ставит 10000 (нет данных = минимум)
"""
from django.core.management.base import BaseCommand
import json
import time
from oknardia.models import PVCprofiles, Glazing, SetKit
from oknardia.settings import (
RANK_PVCP_SOUNDPROOFING, RANK_PVCP_HEAT_TRANSFER,
RANK_PVCP_HEIGHT, RANK_PVCP_RABBET, RANK_PVCP_G_THICKNESS,
RANK_PVCP_THICKNESS, RANK_PVCP_SEALS, RANK_PVCP_CAMERAS_NUM,
RANK_PVCP_SOUNDPROOFING_NAME, RANK_PVCP_HEAT_TRANSFER_NAME,
RANK_PVCP_HEIGHT_NAME, RANK_PVCP_RABBET_NAME, RANK_PVCP_G_THICKNESS_NAME,
RANK_PVCP_THICKNESS_NAME, RANK_PVCP_SEALS_NAME, RANK_PVCP_CAMERAS_NUM_NAME,
RANK_PVCP_CAMERAS_POPULARITY_NAME,
RANK_STEP_SET_NUM_OFFER, RANK_STEP_SET_MODIFY, RANK_STEP_SET_DELIVERY,
RANK_STEP_SET_UNINSTALL_INSTALL, RANK_STEP_SET_SILL, RANK_STEP_SET_PANES,
RANK_STEP_SET_SLOPE, RANK_STEP_SET_CLIMATE_CONTROL,
RANK_STEP_DISCOUNT_FLEX, RANK_STEP_DISCOUNT_MAX,
RANK_GLAZ_SOUNDPROOFING, RANK_GLAZ_HEAT_TRANSFER,
RANK_GLAZ_LIGHT_TRANSMISSION, RANK_GLAZ_PASSING_SUN,
RANK_GLAZ_THICKNESS, RANK_GLAZ_CAMERAS_NUM,
RARING_PVC_PROFILE_MIN, RARING_PVC_PROFILE_MAX,
RARING_GLAZING_MIN, RARING_GLAZING_MAX,
RARING_SET_MAX, RARING_SET_MIN,
RARING_WEIGHT_PVC_PROFILE_IN_SET, RARING_WEIGHT_GLAZING_IN_SET,
KEY_RATING, KEY_RATING_VIRTUAL, KEY_DICSOUNT
)
from web.add_func import sum_through, normalize
def get_rank(set_dictionary, key, key_weight: float = 1, rank_name="", revers=False):
"""
Реализует алгоритм ранжирования Манна-Уитни (Mann-Whitney U Step Rank).
Алгоритм:
1. Сортируем список по значению ключа (от МЕНЬШЕГО к БОЛЬШЕМУ)
2. Если revers=True, разворачиваем список (от БОЛЬШЕГО к МЕНЬШЕМУ)
3. Начинаем с начального значения (минимум или максимум)
4. Проходим по списку, ПОВЫШАЕМ ранг на key_weight когда значение МЕНЯЕТСЯ
Параметры:
- set_dictionary: список словарей для ранжирования
- key: название поля/ключа для ранжирования
- key_weight: вес этого параметра в итоговом рейтинге
- rank_name: подпись названия параметра (для логирования/хранения)
- revers: True если МЕНЬШЕ = ЛУЧШЕ, False если БОЛЬШЕ = ЛУЧШЕ
Возвращает:
- (словарь с ранжированными объектами, максимальный ранг для параметра)
"""
new_set_dictionary = sorted(set_dictionary, key=lambda item: float(item[key]))
rank = 0
# Выбираем стартовое значение
if revers:
# ПО УБЫВАНИЮ: начинаем с МАКСИМУМА, повышаем ранг при УМЕНЬШЕНИИ
low_value = new_set_dictionary[-1][key]
new_set_dictionary = new_set_dictionary[::-1] # разворачиваем список
else:
# ПО ВОЗРАСТАНИЮ: начинаем с МИНИМУМА, повышаем ранг при УВЕЛИЧЕНИИ
low_value = new_set_dictionary[0][key]
# Проходим по отсортированному списку и присваиваем ранги
for i in new_set_dictionary:
# Инициализируем поля если их ещё нет
if "RatingConsist" not in i:
i.update({"RatingConsist": {}})
if "TmpRating" not in i:
i.update({"TmpRating": 0})
# Логика повышения ранга: при УЛУЧШЕНИИ параметра
if revers:
# МЕНЬШЕ = ЛУЧШЕ → повышаем ранг когда значение ПАДАЕТ
if i[key] < low_value:
low_value = i[key]
rank += key_weight
else:
# БОЛЬШЕ = ЛУЧШЕ → повышаем ранг когда значение РАСТЕТ
if i[key] > low_value:
low_value = i[key]
rank += key_weight
# Аккумулируем ранг в общий TmpRating
i["TmpRating"] += rank
i["RatingConsist"].update({rank_name: rank})
# Нормализуем ранги для этого параметра (0.0 … 1.0)
for i in new_set_dictionary:
# Если rank == 0, значит все значения одинаковые (ноль изменений)
if rank > 0:
i["RatingConsist"].update({rank_name: round(normalize(i["RatingConsist"][rank_name], val_max=rank), 3)})
else:
# Если нет различий, то все имеют одинаковый ранг (0.5 по нормализации)
i["RatingConsist"].update({rank_name: 0.5})
return new_set_dictionary, rank
def prepare_pvc_dictionary(profile_use_list):
"""
Конвертирует RawQuerySet в список словарей и исправляет данные для ранжирования.
Коррекции:
1. iProfileCameras: конвертирует строку "3/3" в число суммированием цифр
- Деревянные окна (число камер = 0) → ставим 1000 (максимум, т.к. дерево лучше)
2. iProfileHeight: для значений < 10 см → ставим 1000 (невалидные данные)
3. NumOffer: добавляем поле если его нет (количество коммерческих предложений)
:param profile_use_list: RawQuerySet с объектами профилей
:return: список словарей с исправленными данными
"""
pvc_dictionary = []
for profile in profile_use_list:
profile_dict = profile.__dict__.copy()
# Коррекция: количество камер из строки в число
if 'iProfileCameras' in profile_dict:
profile_dict['iProfileCameras'] = sum_through(str(profile_dict['iProfileCameras']))
# Деревянные окна (ноль камер) → ставим 1000 как максимум (они лучше для ранжирования)
if profile_dict['iProfileCameras'] == 0:
profile_dict['iProfileCameras'] = 1000
# Коррекция: невалидная высота в световом проёме
if 'iProfileHeight' in profile_dict and profile_dict['iProfileHeight'] < 10:
profile_dict['iProfileHeight'] = 1000
# Инициализация: количество коммерческих предложений
if 'NumOffer' not in profile_dict:
profile_dict['NumOffer'] = 0
pvc_dictionary.append(profile_dict)
return pvc_dictionary
def prepare_glaz_dictionary(glazing_use_list):
"""
Конвертирует RawQuerySet в список словарей и исправляет данные для ранжирования.
Коррекции:
1. fGlazingLightTransmission: если < 1% → ставим 10000 (нет данных о светопропускании)
2. fGlazingPassingSun: если < 1% → ставим 10000 (нет данных о солнцепропускании)
:param glazing_use_list: RawQuerySet с объектами стеклопакетов
:return: список словарей с исправленными данными
"""
glaz_dictionary = []
for glazing in glazing_use_list:
glazing_dict = glazing.__dict__.copy()
# Коррекция: светопропускание < 1% означает нет данных
if glazing_dict.get("fGlazingLightTransmission", 0) < 1:
glazing_dict["fGlazingLightTransmission"] = 10000
# Коррекция: солнцепропускание < 1% означает нет данных
if glazing_dict.get("fGlazingPassingSun", 0) < 1:
glazing_dict["fGlazingPassingSun"] = 10000
glaz_dictionary.append(glazing_dict)
return glaz_dictionary
def ranking_pvc(pvc_dictionary):
"""
Ранжирует профили по всем параметрам используя алгоритм Манна-Уитни.
Параметры ранжирования (в порядке применения):
1. Звукоизоляция (fProfileSoundproofing) - БОЛЬШЕ = ЛУЧШЕ
2. Сопротивление теплопередаче (fProfileHeatTransf) - БОЛЬШЕ = ЛУЧШЕ
3. Высота в световом проёме (iProfileHeight) - МЕНЬШЕ = ЛУЧШЕ
4. Высота фальца (iProfileRabbet) - БОЛЬШЕ = ЛУЧШЕ
5. Максимальная толщина стеклопакета (iProfileGlazingThickness) - БОЛЬШЕ = ЛУЧШЕ
6. Толщина профиля (iProfileThickness) - БОЛЬШЕ = ЛУЧШЕ
7. Контуры уплотнения (fProfileSeals) - БОЛЬШЕ = ЛУЧШЕ
8. Количество камер (iProfileCamerasN) - БОЛЬШЕ = ЛУЧШЕ
9. Популярность/кол-во предложений (NumOffer) - БОЛЬШЕ = ЛУЧШЕ
:param pvc_dictionary: список словарей с профилями
:return: (словарь со статистикой, ранжированный список словарей)
"""
dimension_to_template = {}
dimension_to_template.update({'NUM_PROFILE': len(pvc_dictionary)})
# 1. Звукоизоляция (дБ) - БОЛЬШЕ = ЛУЧШЕ → revers=False
pvc_dictionary, max_rank = get_rank(
pvc_dictionary,
"fProfileSoundproofing",
key_weight=RANK_PVCP_SOUNDPROOFING,
rank_name=RANK_PVCP_SOUNDPROOFING_NAME,
revers=False
)
# 2. Теплопередача (м²×°C/Вт) - БОЛЬШЕ = ЛУЧШЕ → revers=False
pvc_dictionary, max_rank = get_rank(
pvc_dictionary,
"fProfileHeatTransf",
key_weight=RANK_PVCP_HEAT_TRANSFER,
rank_name=RANK_PVCP_HEAT_TRANSFER_NAME,
revers=False
)
# 3. Высота в световом проёме (см) - МЕНЬШЕ = ЛУЧШЕ → revers=True
pvc_dictionary, max_rank = get_rank(
pvc_dictionary,
"iProfileHeight",
key_weight=RANK_PVCP_HEIGHT,
rank_name=RANK_PVCP_HEIGHT_NAME,
revers=True
)
# 4. Высота фальца (мм) - БОЛЬШЕ = ЛУЧШЕ → revers=False
pvc_dictionary, max_rank = get_rank(
pvc_dictionary,
"iProfileRabbet",
key_weight=RANK_PVCP_RABBET,
rank_name=RANK_PVCP_RABBET_NAME,
revers=False
)
# 5. Максимальная толщина стеклопакета (мм) - БОЛЬШЕ = ЛУЧШЕ → revers=False
pvc_dictionary, max_rank = get_rank(
pvc_dictionary,
"iProfileGlazingThickness",
key_weight=RANK_PVCP_G_THICKNESS,
rank_name=RANK_PVCP_G_THICKNESS_NAME,
revers=False
)
# 6. Монтажная ширина профиля (мм) - БОЛЬШЕ = ЛУЧШЕ → revers=False
pvc_dictionary, max_rank = get_rank(
pvc_dictionary,
"iProfileThickness",
key_weight=RANK_PVCP_THICKNESS,
rank_name=RANK_PVCP_THICKNESS_NAME,
revers=False
)
# 7. Контуры уплотнения - БОЛЬШЕ = ЛУЧШЕ → revers=False
pvc_dictionary, max_rank = get_rank(
pvc_dictionary,
"fProfileSeals",
key_weight=RANK_PVCP_SEALS,
rank_name=RANK_PVCP_SEALS_NAME,
revers=False
)
# 8. Количество камер - БОЛЬШЕ = ЛУЧШЕ → revers=False
pvc_dictionary, max_rank = get_rank(
pvc_dictionary,
"iProfileCameras",
key_weight=RANK_PVCP_CAMERAS_NUM,
rank_name=RANK_PVCP_CAMERAS_NUM_NAME,
revers=False
)
# 9. Популярность (кол-во коммерческих предложений) - БОЛЬШЕ = ЛУЧШЕ → revers=False
pvc_dictionary, max_rank = get_rank(
pvc_dictionary,
"NumOffer",
key_weight=RANK_STEP_SET_NUM_OFFER,
rank_name=RANK_PVCP_CAMERAS_POPULARITY_NAME,
revers=False
)
return dimension_to_template, pvc_dictionary
def prepare_setkit_dictionary(setkit_use_list):
"""
Конвертирует RawQuerySet в список словарей и исправляет данные для ранжирования.
Коррекции:
1. Строковые поля (sSetSill, sSetPanes, sSetSlope, sSetClimateControl) преобразуются в 0/1
2. sOfficeDiscountMetaFormula парсится для максимальной скидки и количества вариантов
3. dModify (datetime) преобразуется в timestamp (число) для ранжирования
4. Логические поля должны быть числами для алгоритма ранжирования
:param setkit_use_list: RawQuerySet с объектами наборов
:return: список словарей с исправленными данными
"""
import datetime
setkit_dictionary = []
for setkit in setkit_use_list:
setkit_dict = setkit.__dict__.copy()
# Коррекция: преобразуем дату в timestamp для ранжирования
# Свежие данные (большие timestamp) должны иметь более высокий ранг
if 'dModify' in setkit_dict and setkit_dict['dModify']:
# Если это datetime-объект, конвертируем в timestamp
if isinstance(setkit_dict['dModify'], datetime.datetime):
setkit_dict['dModify'] = setkit_dict['dModify'].timestamp()
elif isinstance(setkit_dict['dModify'], datetime.date):
# Если это просто date, конвертируем в datetime сначала
dt = datetime.datetime.combine(setkit_dict['dModify'], datetime.time())
setkit_dict['dModify'] = dt.timestamp()
else:
# Если это строка, пытаемся распарсить
try:
dt = datetime.datetime.fromisoformat(str(setkit_dict['dModify']))
setkit_dict['dModify'] = dt.timestamp()
except:
setkit_dict['dModify'] = 0 # Если не смогли распарсить, ставим 0
else:
setkit_dict['dModify'] = 0 # Если нет даты, ставим 0
# Коррекция: преобразуем строковые поля в 0/1
# Подоконник: если нет, — то 0, иначе 1
if setkit_dict.get("sSetSill", "").lower() in ["нет", "-", "", "", " "]:
setkit_dict["sSetSill"] = 0
else:
setkit_dict["sSetSill"] = 1
# Водоотлив: если нет, — то 0, иначе 1
if setkit_dict.get("sSetPanes", "").lower() in ["нет", "-", "", "", " "]:
setkit_dict["sSetPanes"] = 0
else:
setkit_dict["sSetPanes"] = 1
# Откос: если нет, — то 0, иначе 1
if setkit_dict.get("sSetSlope", "").lower() in ["нет", "-", "", "", " "]:
setkit_dict["sSetSlope"] = 0
else:
setkit_dict["sSetSlope"] = 1
# Климат-контроль: если нет, — то 0, иначе 1
if setkit_dict.get("sSetClimateControl", "").lower() in ["нет", "-", "", "", " "]:
setkit_dict["sSetClimateControl"] = 0
else:
setkit_dict["sSetClimateControl"] = 1
# Парсим формулу скидок офиса и извлекаем информацию о скидках
try:
import json as json_module
discount_formula = json_module.loads(setkit_dict.get("sOfficeDiscountMetaFormula", "{}"))
discount_values = discount_formula.get(KEY_DICSOUNT, {})
# Количество вариантов скидок
setkit_dict["iDiscountVariantsCount"] = len(discount_values.keys()) if discount_values else 0
# Максимальная скидка из всех вариантов
if discount_values:
setkit_dict["fDiscountMax"] = float(max(discount_values.values()))
else:
setkit_dict["fDiscountMax"] = 0.0
except (ValueError, KeyError, AttributeError):
# Если парс неудался, то скидок нет
setkit_dict["iDiscountVariantsCount"] = 0
setkit_dict["fDiscountMax"] = 0.0
# Инициализация: количество коммерческих предложений
if 'NumOffer' not in setkit_dict:
setkit_dict['NumOffer'] = 0
setkit_dictionary.append(setkit_dict)
return setkit_dictionary
def ranking_setkit(setkit_dictionary):
"""
Ранжирует наборы (SetKit) по всем параметрам используя алгоритм Манна-Уитни.
Параметры ранжирования (в порядке применения):
1. Актуальность — дата последнего обновления (dModify) ← МЕНЬШЕ=ЛУЧШЕ (свежее) → revers=True
2. Доставка (bSetDelivery) — включена ли в стоимость → БОЛЬШЕ=ЛУЧШЕ
3. Монтаж/демонтаж (bSetUninstallInstall) — включён ли → БОЛЬШЕ=ЛУЧШЕ
4. Подоконник (sSetSill) — включён ли → БОЛЬШЕ=ЛУЧШЕ
5. Водоотлив (sSetPanes) — включён ли → БОЛЬШЕ=ЛУЧШЕ
6. Откос (sSetSlope) — включён ли → БОЛЬШЕ=ЛУЧШЕ
7. Климат-контроль (sSetClimateControl) — включён ли → БОЛЬШЕ=ЛУЧШЕ
8. Число предложений (NumOffer) — популярность → БОЛЬШЕ=ЛУЧШЕ
9. Гибкость скидок (iDiscountVariantsCount) — кол-во вариантов → БОЛЬШЕ=ЛУЧШЕ
10. Размер скидок (fDiscountMax) — максимальная скидка → БОЛЬШЕ=ЛУЧШЕ
:param setkit_dictionary: список словарей с наборами
:return: ранжированный список словарей
"""
# 1. Актуальность (дата последнего обновления) - МЕНЬШЕ = ЛУЧШЕ → revers=True
setkit_dictionary, _ = get_rank(
setkit_dictionary,
"dModify",
key_weight=RANK_STEP_SET_MODIFY,
rank_name="Актуальность",
revers=True
)
# 2. Доставка включена в стоимость - БОЛЬШЕ = ЛУЧШЕ → revers=False
setkit_dictionary, _ = get_rank(
setkit_dictionary,
"bSetDelivery",
key_weight=RANK_STEP_SET_DELIVERY,
rank_name="Доставка",
revers=False
)
# 3. Монтаж/демонтаж включен - БОЛЬШЕ = ЛУЧШЕ → revers=False
setkit_dictionary, _ = get_rank(
setkit_dictionary,
"bSetUninstallInstall",
key_weight=RANK_STEP_SET_UNINSTALL_INSTALL,
rank_name="Монтаж",
revers=False
)
# 4. Подоконник включен - БОЛЬШЕ = ЛУЧШЕ → revers=False
setkit_dictionary, _ = get_rank(
setkit_dictionary,
"sSetSill",
key_weight=RANK_STEP_SET_SILL,
rank_name="Подоконник",
revers=False
)
# 5. Водоотлив включен - БОЛЬШЕ = ЛУЧШЕ → revers=False
setkit_dictionary, _ = get_rank(
setkit_dictionary,
"sSetPanes",
key_weight=RANK_STEP_SET_PANES,
rank_name="Водоотлив",
revers=False
)
# 6. Откос включен - БОЛЬШЕ = ЛУЧШЕ → revers=False
setkit_dictionary, _ = get_rank(
setkit_dictionary,
"sSetSlope",
key_weight=RANK_STEP_SET_SLOPE,
rank_name="Откос",
revers=False
)
# 7. Климат-контроль включен - БОЛЬШЕ = ЛУЧШЕ → revers=False
setkit_dictionary, _ = get_rank(
setkit_dictionary,
"sSetClimateControl",
key_weight=RANK_STEP_SET_CLIMATE_CONTROL,
rank_name="Климат-контроль",
revers=False
)
# 8. Число предложений (популярность) - БОЛЬШЕ = ЛУЧШЕ → revers=False
setkit_dictionary, _ = get_rank(
setkit_dictionary,
"NumOffer",
key_weight=RANK_STEP_SET_NUM_OFFER,
rank_name="Число предложений",
revers=False
)
# 9. Гибкость скидок (количество вариантов) - БОЛЬШЕ = ЛУЧШЕ → revers=False
setkit_dictionary, _ = get_rank(
setkit_dictionary,
"iDiscountVariantsCount",
key_weight=RANK_STEP_DISCOUNT_FLEX,
rank_name="Гибкость скидок",
revers=False
)
# 10. Размер скидок (максимальная скидка) - БОЛЬШЕ = ЛУЧШЕ → revers=False
setkit_dictionary, _ = get_rank(
setkit_dictionary,
"fDiscountMax",
key_weight=RANK_STEP_DISCOUNT_MAX,
rank_name="Размер скидок",
revers=False
)
return setkit_dictionary
def ranking_glazing(glaz_dictionary):
"""
Ранжирует стеклопакеты по всем параметрам используя алгоритм Манна-Уитни.
Параметры ранжирования (в порядке применения):
1. Звукоизоляция (fGlazingSoundproofing) - БОЛЬШЕ = ЛУЧШЕ
2. Сопротивление теплопередаче (fGlazingHeatTransfer) - БОЛЬШЕ = ЛУЧШЕ
3. Светопропускание (fGlazingLightTransmission) - БОЛЬШЕ = ЛУЧШЕ ⚠️ ИСПРАВЛЕНО
4. Солнцепропускание (fGlazingPassingSun) - МЕНЬШЕ = ЛУЧШЕ (летнее охлаждение в РФ)
5. Толщина стеклопакета (iGlazingThickness) - БОЛЬШЕ = ЛУЧШЕ
6. Количество камер (iGlazingCamerasN) - БОЛЬШЕ = ЛУЧШЕ
ВАЖНО: sGlazingLightReflectance (строка "30/20") НЕ ранжируется - только информация!
:param glaz_dictionary: список словарей со стеклопакетами
:return: (словарь со статистикой, ранжированный список словарей)
"""
# ... existing code ...
# 1. Звукоизоляция (дБ) - БОЛЬШЕ = ЛУЧШЕ → revers=False
glaz_dictionary, max_rank = get_rank(
glaz_dictionary,
"fGlazingSoundproofing",
key_weight=RANK_GLAZ_SOUNDPROOFING,
rank_name=RANK_PVCP_SOUNDPROOFING_NAME,
revers=False
)
# 2. Теплопередача (м²×°C/Вт) - БОЛЬШЕ = ЛУЧШЕ → revers=False
glaz_dictionary, max_rank = get_rank(
glaz_dictionary,
"fGlazingHeatTransfer",
key_weight=RANK_GLAZ_HEAT_TRANSFER,
rank_name=RANK_PVCP_HEAT_TRANSFER_NAME,
revers=False
)
# 3. Светопропускание (%) - БОЛЬШЕ = ЛУЧШЕ → revers=False
# БОЛЬШЕ света в помещение = ЛУЧШЕ освещенность!
glaz_dictionary, max_rank = get_rank(
glaz_dictionary,
"fGlazingLightTransmission",
key_weight=RANK_GLAZ_LIGHT_TRANSMISSION,
rank_name="Светопропускание",
revers=False
)
# 4. Солнцепропускание (%) - МЕНЬШЕ = ЛУЧШЕ → revers=True
# В России отопление независимое, поэтому меньше солнечного нагрева = лучше для лета
# На внутреннем стекле напыление отражает тепло, не пуская его в квартиру
glaz_dictionary, max_rank = get_rank(
glaz_dictionary,
"fGlazingPassingSun",
key_weight=RANK_GLAZ_PASSING_SUN,
rank_name="Солнцепропускание",
revers=True
)
# 5. Толщина стеклопакета (мм) - БОЛЬШЕ = ЛУЧШЕ → revers=False
glaz_dictionary, max_rank = get_rank(
glaz_dictionary,
"iGlazingThickness",
key_weight=RANK_GLAZ_THICKNESS,
rank_name="Толщина",
revers=False
)
# 6. Количество камер - БОЛЬШЕ = ЛУЧШЕ → revers=False
glaz_dictionary, max_rank = get_rank(
glaz_dictionary,
"iGlazingCamerasN",
key_weight=RANK_GLAZ_CAMERAS_NUM,
rank_name="Камеры",
revers=False
)
# ПРОПУСКАЕМ: sGlazingLightReflectance это СТРОКА ("30/20"), не число!
# Не ранжируем, только информация об отражении света
return {}, glaz_dictionary
class Command(BaseCommand):
help = 'Пересчитывает рейтинги профилей и стеклопакетов используя Mann-Whitney метод'
def add_arguments(self, parser):
# Django уже добавляет --verbosity автоматически
# 0 = минимум, 1 = нормально, 2 = подробно, 3 = очень подробно
pass
def format_profile_table(self, profile_item, profile_obj):
"""Форматирует таблицу характеристик профиля для подробного вывода"""
output = []
output.append(f"\n {'='*100}")
output.append(f" ПРОФИЛЬ: {profile_obj.sProfileName} (ID: {profile_item['id']})")
output.append(f" {'='*100}")
# Основные параметры
if 'RatingConsist' in profile_item:
output.append(f" {'Характеристика':<30} {'Значение':<15} {'Ранг (0..1)':<15} {'Вклад':<15}")
output.append(f" {'-'*100}")
for param_name, rank_value in sorted(profile_item['RatingConsist'].items()):
# Получаем значение параметра из словаря
if param_name == RANK_PVCP_SOUNDPROOFING_NAME and 'fProfileSoundproofing' in profile_item:
value = f"{profile_item['fProfileSoundproofing']:.2f} дБ"
elif param_name == RANK_PVCP_HEAT_TRANSFER_NAME and 'fProfileHeatTransf' in profile_item:
value = f"{profile_item['fProfileHeatTransf']:.2f} Ro"
elif param_name == RANK_PVCP_HEIGHT_NAME and 'iProfileHeight' in profile_item:
value = f"{profile_item['iProfileHeight']} мм"
elif param_name == RANK_PVCP_RABBET_NAME and 'iProfileRabbet' in profile_item:
value = f"{profile_item['iProfileRabbet']} мм"
elif param_name == RANK_PVCP_G_THICKNESS_NAME and 'iProfileGlazingThickness' in profile_item:
value = f"{profile_item['iProfileGlazingThickness']} мм"
elif param_name == RANK_PVCP_THICKNESS_NAME and 'iProfileThickness' in profile_item:
value = f"{profile_item['iProfileThickness']} мм"
elif param_name == RANK_PVCP_SEALS_NAME and 'fProfileSeals' in profile_item:
value = f"{profile_item['fProfileSeals']} контуров"
elif param_name == RANK_PVCP_CAMERAS_NUM_NAME and 'iProfileCameras' in profile_item:
value = f"{profile_item['iProfileCameras']} шт"
elif param_name == RANK_PVCP_CAMERAS_POPULARITY_NAME and 'NumOffer' in profile_item:
value = f"{profile_item['NumOffer']} предл."
else:
value = ""
rank_str = f"{rank_value:.3f}" if isinstance(rank_value, float) else str(rank_value)
output.append(f" {param_name:<30} {value:<15} {rank_value:.3f} {'*' * int(float(rank_str)*5):<15}")
output.append(f" {'-'*100}")
output.append(f" ИТОГО: Рейтинг = {profile_obj.fProfileRating:.2f}/5.0 "
f"{'*' * int(profile_obj.fProfileRating)}")
return "\n".join(output)
def format_glazing_table(self, glazing_item, glazing_obj):
"""Форматирует таблицу характеристик стеклопакета для подробного вывода"""
output = []
output.append(f"\n {'='*100}")
output.append(f" СТЕКЛОПАКЕТ: {glazing_obj.sGlazingName} (ID: {glazing_item['id']}) | Марка:"
f"{glazing_obj.sGlazingMark}")
output.append(f" {'='*100}")
# Основные параметры
if 'RatingConsist' in glazing_item:
output.append(f" {'Характеристика':<35} {'Значение':<20} {'Ранг (0..1)':<15} {'Вклад':<15}")
output.append(f" {'-'*100}")
for param_name, rank_value in sorted(glazing_item['RatingConsist'].items()):
# Получаем значение параметра из словаря
if param_name == "Звукоизоляция" and 'fGlazingSoundproofing' in glazing_item:
value = f"{glazing_item['fGlazingSoundproofing']:.2f} дБ"
elif param_name == "Теплопередача" and 'fGlazingHeatTransfer' in glazing_item:
value = f"{glazing_item['fGlazingHeatTransfer']:.2f} Ro"
elif param_name == "Светопропускание" and 'fGlazingLightTransmission' in glazing_item:
value = f"{glazing_item['fGlazingLightTransmission']:.2f}%"
elif param_name == "Солнцепропускание" and 'fGlazingPassingSun' in glazing_item:
value = f"{glazing_item['fGlazingPassingSun']:.2f}%"
elif param_name == "Толщина" and 'iGlazingThickness' in glazing_item:
value = f"{glazing_item['iGlazingThickness']} мм"
elif param_name == "Число камер" and 'iGlazingCamerasN' in glazing_item:
value = f"{glazing_item['iGlazingCamerasN']} камер"
else:
value = ""
rank_str = f"{rank_value:.3f}" if isinstance(rank_value, float) else str(rank_value)
output.append(f" {param_name:<35} {value:<20} {rank_value:.3f} {'*' * int(float(rank_str)*5):<15}")
output.append(f" {'-'*100}")
output.append(f" ИТОГО: Рейтинг = {glazing_obj.fGlazingRating:.2f}/5.0 "
f"{'*' * int(glazing_obj.fGlazingRating)}")
return "\n".join(output)
def format_setkit_table(self, setkit_item, setkit_obj):
"""Форматирует таблицу характеристик набора для подробного вывода"""
output = []
output.append(f"\n {'='*120}")
output.append(f" НАБОР: {setkit_obj.sSetName} (ID: {setkit_item['id']})")
output.append(f" {'='*120}")
# Основные параметры из RatingConsist
if 'RatingConsist' in setkit_item:
output.append(f" {'Параметр':<35} {'Значение':<20} {'Ранг (0..1)':<15} {'Вклад':<15}")
output.append(f" {'-'*120}")
for param_name, rank_value in sorted(setkit_item['RatingConsist'].items()):
# Извлекаем значение параметра
if param_name == "Актуальность":
# dModify кодируется в специальный формат, просто показываем дату
value = "свежий" if setkit_item.get("dModify") else "старый"
elif param_name == "Доставка" and 'bSetDelivery' in setkit_item:
value = "✓ Да" if setkit_item['bSetDelivery'] else "✗ Нет"
elif param_name == "Монтаж" and 'bSetUninstallInstall' in setkit_item:
value = "✓ Да" if setkit_item['bSetUninstallInstall'] else "✗ Нет"
elif param_name == "Подоконник" and 'sSetSill' in setkit_item:
value = "✓ Да" if setkit_item['sSetSill'] else "✗ Нет"
elif param_name == "Водоотлив" and 'sSetPanes' in setkit_item:
value = "✓ Да" if setkit_item['sSetPanes'] else "✗ Нет"
elif param_name == "Откос" and 'sSetSlope' in setkit_item:
value = "✓ Да" if setkit_item['sSetSlope'] else "✗ Нет"
elif param_name == "Климат-контроль" and 'sSetClimateControl' in setkit_item:
value = "✓ Да" if setkit_item['sSetClimateControl'] else "✗ Нет"
elif param_name == "Число предложений" and 'NumOffer' in setkit_item:
value = f"{setkit_item['NumOffer']} шт"
elif param_name == "Гибкость скидок" and 'iDiscountVariantsCount' in setkit_item:
value = f"{setkit_item['iDiscountVariantsCount']} вариантов"
elif param_name == "Размер скидок" and 'fDiscountMax' in setkit_item:
value = f"{setkit_item['fDiscountMax']:.1f}%"
else:
value = ""
rank_str = f"{rank_value:.3f}" if isinstance(rank_value, float) else str(rank_value)
output.append(f" {param_name:<35} {value:<20} {rank_value:.3f} {'*' * int(float(rank_str)*5):<15}")
output.append(f" {'-'*120}")
output.append(f" ИТОГО: Рейтинг = {setkit_obj.fSetRating:.2f}/5.0 {'*' * int(setkit_obj.fSetRating)}")
return "\n".join(output)
def handle(self, *args, **options):
verbose = int(options.get('verbosity', 1))
time_start = time.perf_counter()
self.stdout.write(self.style.SUCCESS('=== НАЧАЛИ ПЕРЕСЧЁТ РЕЙТИНГОВ ==='))
# ========== РАНЖИРОВАНИЕ ПРОФИЛЕЙ ==========
self.stdout.write('\n========================================')
self.stdout.write('[ЭТАП 1]: Пересчёт рейтингов ПРОФИЛЕЙ...')
self.stdout.write('========================================\n')
# Обнуляем все рейтинги профилей
profile_all_num = PVCprofiles.objects.all().update(fProfileRating=0.0)
self.stdout.write(f' ✓ Обнулены рейтинги у {profile_all_num} профилей')
# Извлекаем профили которые используются в коммерческих предложениях
q = PVCprofiles.objects.raw(
"SELECT oknardia_pvcprofiles.*, COUNT(oknardia_priceoffer.id) AS NumOffer "
"FROM oknardia_setkit "
"INNER JOIN oknardia_priceoffer "
" ON oknardia_setkit.id = oknardia_priceoffer.kOffer2SetKit_id "
"RIGHT OUTER JOIN oknardia_pvcprofiles "
" ON oknardia_setkit.kSet2PVCprofiles_id = oknardia_pvcprofiles.id "
"GROUP BY oknardia_pvcprofiles.id"
)
profile_use_list = list(q)
self.stdout.write(f' ✓ Найдено {len(profile_use_list)} профилей для ранжирования')
# Подготавливаем словарь профилей с коррекциями
pvc_dictionary = prepare_pvc_dictionary(profile_use_list)
# Ранжируем профили
dim, pvc_dictionary = ranking_pvc(pvc_dictionary)
# Сортируем по рейтингу
pvc_dictionary = sorted(pvc_dictionary, key=lambda item: item["TmpRating"])
# Нахождение минимального и максимального рейтинга
num_max_rank = 0
num_min_rank = -1
for j, i in enumerate(pvc_dictionary):
if i["NumOffer"] != 0:
num_max_rank = j
if num_min_rank == -1:
num_min_rank = j
# Сохраняем финальные рейтинги в БД
for j, i in enumerate(pvc_dictionary):
obj = PVCprofiles.objects.get(id=i["id"])
# Загружаем JSON описание профиля
try:
getted_json = json.loads(obj.sProfileDescription)
except:
getted_json = {}
# Обновляем JSON рейтингом
if i["NumOffer"] != 0:
try:
del getted_json[KEY_RATING_VIRTUAL]
except:
pass
getted_json[KEY_RATING] = i["RatingConsist"]
else:
try:
del getted_json[KEY_RATING]
except:
pass
getted_json[KEY_RATING_VIRTUAL] = i["RatingConsist"]
obj.sProfileDescription = json.dumps(
getted_json,
separators=(",", ":"),
sort_keys=True,
ensure_ascii=False
)
# Вычисляем финальный рейтинг (0.0 … 5.0 звёзд)
if j <= num_max_rank:
obj.fProfileRating = (
normalize(i["TmpRating"], pvc_dictionary[num_max_rank]["TmpRating"]) *
(RARING_PVC_PROFILE_MAX - RARING_PVC_PROFILE_MIN) +
RARING_PVC_PROFILE_MIN
)
else:
obj.fProfileRating = 5.0
obj.save()
# Подробный вывод в режиме verbosity >= 3
if verbose >= 3:
self.stdout.write(self.format_profile_table(i, obj))
self.stdout.write(f' ✓ Сохранено {len(pvc_dictionary)} профилей с финальными рейтингами')
# ========== РАНЖИРОВАНИЕ СТЕКЛОПАКЕТОВ ==========
self.stdout.write('\n=============================================')
self.stdout.write('[ЭТАП 2]: Пересчёт рейтингов СТЕКЛОПАКЕТОВ...')
self.stdout.write('=============================================\n')
# Обнуляем все рейтинги стеклопакетов
glazing_all_num = Glazing.objects.all().update(fGlazingRating=0.0)
self.stdout.write(f' ✓ Обнулены рейтинги у {glazing_all_num} стеклопакетов')
# Извлекаем стеклопакеты которые используются в коммерческих предложениях
q = Glazing.objects.raw(
"SELECT oknardia_glazing.*, COUNT(oknardia_priceoffer.id) AS NumOffer "
"FROM oknardia_setkit "
"INNER JOIN oknardia_priceoffer "
" ON oknardia_setkit.id = oknardia_priceoffer.kOffer2SetKit_id "
"RIGHT OUTER JOIN oknardia_glazing "
" ON oknardia_setkit.kSet2Glazing_id = oknardia_glazing.id "
"GROUP BY oknardia_glazing.id"
)
glazing_use_list = list(q)
self.stdout.write(f' ✓ Найдено {len(glazing_use_list)} стеклопакетов для ранжирования')
# Подготавливаем словарь стеклопакетов с коррекциями
glaz_dictionary = prepare_glaz_dictionary(glazing_use_list)
# Ранжируем стеклопакеты
dim, glaz_dictionary = ranking_glazing(glaz_dictionary)
# Сортируем по рейтингу
glaz_dictionary = sorted(glaz_dictionary, key=lambda item: item["TmpRating"])
# Нахождение минимального и максимального рейтинга
num_max_rank = 0
num_min_rank = -1
for j, i in enumerate(glaz_dictionary):
if i["NumOffer"] != 0:
num_max_rank = j
if num_min_rank == -1:
num_min_rank = j
# Сохраняем финальные рейтинги в БД
for j, i in enumerate(glaz_dictionary):
obj = Glazing.objects.get(id=i["id"])
# Загружаем JSON описание стеклопакета
try:
getted_json = json.loads(obj.sGlazingDescription)
except:
getted_json = {}
# Обновляем JSON рейтингом
if i["NumOffer"] != 0:
try:
del getted_json[KEY_RATING_VIRTUAL]
except:
pass
getted_json[KEY_RATING] = i["RatingConsist"]
else:
try:
del getted_json[KEY_RATING]
except:
pass
getted_json[KEY_RATING_VIRTUAL] = i["RatingConsist"]
obj.sGlazingDescription = json.dumps(
getted_json,
separators=(",", ":"),
sort_keys=True,
ensure_ascii=False
)
# Вычисляем финальный рейтинг (0.0 … 5.0 звёзд)
if j <= num_max_rank:
obj.fGlazingRating = (
normalize(i["TmpRating"], glaz_dictionary[num_max_rank]["TmpRating"]) *
(RARING_GLAZING_MAX - RARING_GLAZING_MIN) +
RARING_GLAZING_MIN
)
else:
obj.fGlazingRating = 5.0
obj.save()
# Подробный вывод в режиме verbosity >= 3
if verbose >= 3:
self.stdout.write(self.format_glazing_table(i, obj))
self.stdout.write(f' ✓ Сохранено {len(glaz_dictionary)} стеклопакетов с финальными рейтингами')
# ========== РАНЖИРОВАНИЕ НАБОРОВ ==========
self.stdout.write('\n================================================')
self.stdout.write('[ЭТАП 3]: Пересчёт рейтингов НАБОРОВ (SetKit)...')
self.stdout.write('================================================\n')
# Обнуляем все рейтинги наборов
setkit_all_num = SetKit.objects.all().update(fSetRating=0.0)
self.stdout.write(f' ✓ Обнулены рейтинги у {setkit_all_num} наборов')
# Извлекаем наборы которые используются в коммерческих предложениях
q = SetKit.objects.raw(
"SELECT oknardia_setkit.*, COUNT(oknardia_priceoffer.id) AS NumOffer, "
"MAX(oknardia_priceoffer.dOfferModify) AS dModify, "
"oknardia_pvcprofiles.fProfileRating, "
"oknardia_glazing.fGlazingRating, "
"oknardia_merchantoffice.sOfficeDiscountMetaFormula "
"FROM oknardia_setkit "
"INNER JOIN oknardia_priceoffer "
" ON oknardia_priceoffer.kOffer2SetKit_id = oknardia_setkit.id "
"INNER JOIN oknardia_glazing "
" ON oknardia_setkit.kSet2Glazing_id = oknardia_glazing.id "
"INNER JOIN oknardia_pvcprofiles "
" ON oknardia_pvcprofiles.id = oknardia_setkit.kSet2PVCprofiles_id "
"INNER JOIN oknardia_ouruser "
" ON oknardia_setkit.kSet2User_id = oknardia_ouruser.id "
"INNER JOIN oknardia_merchantoffice "
" ON oknardia_ouruser.kMerchantOffice_id = oknardia_merchantoffice.id "
"WHERE oknardia_setkit.sSetActive = TRUE "
"GROUP BY oknardia_pvcprofiles.fProfileRating, "
" oknardia_glazing.fGlazingRating, "
" oknardia_merchantoffice.sOfficeDiscountMetaFormula, "
" oknardia_setkit.sSetActive, oknardia_setkit.id "
"ORDER BY MAX(oknardia_priceoffer.dOfferModify)"
)
setkit_use_list = list(q)
self.stdout.write(f' ✓ Найдено {len(setkit_use_list)} наборов для ранжирования')
# Подготавливаем словарь наборов с коррекциями
setkit_dictionary = prepare_setkit_dictionary(setkit_use_list)
# Ранжируем наборы
setkit_dictionary = ranking_setkit(setkit_dictionary)
# Сортируем по рейтингу
setkit_dictionary = sorted(setkit_dictionary, key=lambda item: item["TmpRating"])
# Нахождение минимального и максимального рейтинга
num_max_rank = 0
num_min_rank = -1
for j, i in enumerate(setkit_dictionary):
if i["NumOffer"] != 0:
num_max_rank = j
if num_min_rank == -1:
num_min_rank = j
# Сохраняем финальные рейтинги в БД
for j, i in enumerate(setkit_dictionary):
obj = SetKit.objects.get(id=i["id"])
# Загружаем JSON описание набора
try:
getted_json = json.loads(obj.sSetDescription)
except:
getted_json = {}
# Обновляем JSON рейтингом
if i["NumOffer"] != 0:
try:
del getted_json[KEY_RATING_VIRTUAL]
except:
pass
getted_json[KEY_RATING] = i["RatingConsist"]
else:
try:
del getted_json[KEY_RATING]
except:
pass
getted_json[KEY_RATING_VIRTUAL] = i["RatingConsist"]
obj.sSetDescription = json.dumps(
getted_json,
separators=(",", ":"),
sort_keys=True,
ensure_ascii=False
)
# Вычисляем финальный рейтинг набора как комбинацию:
# k1 = нормализованный TmpRating (параметры сервиса) * вес сервиса
# k2 = рейтинг стеклопакета * вес стеклопакета в наборе
# k3 = рейтинг профиля * вес профиля в наборе
# fSetRating = k1 + k2 + k3
if j <= num_max_rank:
# Нормализуем TmpRating (от 0 до максимума)
norm_tmp_rating = normalize(
i["TmpRating"],
setkit_dictionary[num_max_rank]["TmpRating"]
)
# вес для TmpRating (всё что не профиль и не стеклопакет)
service_rating_weight = (
RARING_SET_MAX - RARING_WEIGHT_PVC_PROFILE_IN_SET - RARING_WEIGHT_GLAZING_IN_SET - RARING_SET_MIN
)
k1 = norm_tmp_rating * service_rating_weight
k2 = normalize(i["fGlazingRating"], RARING_GLAZING_MAX) * RARING_WEIGHT_GLAZING_IN_SET
k3 = normalize(i["fProfileRating"], RARING_PVC_PROFILE_MAX) * RARING_WEIGHT_PVC_PROFILE_IN_SET
obj.fSetRating = k1 + k2 + k3
else:
obj.fSetRating = 5.0
obj.save()
# Подробный вывод в режиме verbosity >= 3
if verbose >= 3:
self.stdout.write(self.format_setkit_table(i, obj))
self.stdout.write(f' ✓ Сохранено {len(setkit_dictionary)} наборов с финальными рейтингами')
self.stdout.write(self.style.SUCCESS('\n[OK!] ПЕРЕСЧЁТ РЕЙТИНГОВ ЗАВЕРШЁН УСПЕШНО!'))
self.stdout.write(f' • Обновлено профилей: {len(pvc_dictionary)}')
self.stdout.write(f' • Обновлено стеклопакетов: {len(glaz_dictionary)}')
self.stdout.write(f' • Обновлено наборов: {len(setkit_dictionary)}')