From d074b00c07b6a02a2c5aa9fc5a30bb60c9c834ee Mon Sep 17 00:00:00 2001 From: Erokhin Evgenii Date: Sun, 23 Jun 2024 14:54:18 +0300 Subject: [PATCH 1/2] Create remainings_plot.py new file to show remainings --- remainings_plot.py | 75 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 remainings_plot.py diff --git a/remainings_plot.py b/remainings_plot.py new file mode 100644 index 0000000..ee21597 --- /dev/null +++ b/remainings_plot.py @@ -0,0 +1,75 @@ +import argparse +import os +import sys +from time import time_ns +from matplotlib import pyplot as plt +from dotenv import load_dotenv +import matplotlib.font_manager as font_manager +import seaborn as sns + +# Получаем текущий каталог и корень проекта для установки правильного пути системы. +current_dir = os.path.dirname(os.path.abspath(__file__)) +project_root = os.path.abspath(os.path.join(current_dir, '..')) +dotenv_path = os.path.join(project_root, 'variables.env') +sys.path.append(project_root) + +load_dotenv(dotenv_path) + + +from utils.data_scripts import get_mongo_collection, fetch_data, aggregate_data, create_dataframe, \ + add_missing_dates + + +def make_plot_of_remainings(name): + """ + Генерирует график остатков товара по времени для заданного названия продукта и отображает его. + + Аргументы: + name (str): Название продукта, для которого необходимо сгенерировать график. + + Возвращает: + dict: Содержит последнюю дату и соответствующий 'остаток'. + """ + collection = get_mongo_collection('Оборотная ведомость') + data, dates = fetch_data(collection, name) + aggregated_data = aggregate_data(data, "единиц после") + df = create_dataframe(aggregated_data, ['остаток', 'дата']) + + if df.empty: + return "Извините, данные оборотной ведомости не найдены для данного товара, невозможно предсказать потребление." + + + df = add_missing_dates(df, dates, 'остаток') + df.sort_values(by='дата', inplace=True) + + plt.figure(figsize=(8, 7)) + bars = sns.barplot(x='дата', y='остаток', hue='дата', palette='Reds', data=df, legend=False) + + font = font_manager.FontProperties(family='serif', style='italic') + font1 = font_manager.FontProperties(family='serif',style='italic', size=16) + + # Add labels to the bars, using the x-axis values and y-axis values + for p in bars.patches: + plt.text(p.get_x() + p.get_width()/2., p.get_height(), f'{p.get_height():.2f}', + ha='center', va='bottom', fontsize=10, fontproperties=font) + + plt.xlabel('Дата', fontsize=14, fontproperties=font) + plt.ylabel('Остатки', ha= 'center', fontsize=14, fontproperties=font) + plt.title('Остатки за период в конце квартала', fontproperties=font1) + + plt.xticks(rotation=45, ha='center', fontsize=10, fontproperties=font) + plt.yticks(fontsize=12, fontstyle= 'italic',fontproperties=font) + + + file_name = "/backend/src/main/java/ru/hackaton/python_scripts/images/" + str(time_ns()) + '.png' + plt.savefig(file_name) + + return (f"По имеющимся данным на {df.iloc[-1]['дата'].strftime('%d.%m.%Y')} осталось " + f"{int(df.iloc[-1]['остаток'])} ед. товара.\n") + file_name + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='Process some arguments.') + parser.add_argument('item_name', type=str) + args = parser.parse_args() + print(make_plot_of_remainings(args.item_name)) From 65a5b7c8e29cebfb315946b527febc19e7db851e Mon Sep 17 00:00:00 2001 From: Erokhin Evgenii Date: Mon, 24 Jun 2024 00:31:15 +0300 Subject: [PATCH 2/2] Create forecaster.py with new visual --- forecaster.py | 180 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 forecaster.py diff --git a/forecaster.py b/forecaster.py new file mode 100644 index 0000000..a11171c --- /dev/null +++ b/forecaster.py @@ -0,0 +1,180 @@ +import argparse + +import pandas as pd + +from math import ceil +from time import time_ns +from datetime import datetime +from matplotlib import pyplot as plt +from pmdarima import auto_arima +import matplotlib.font_manager as font_manager +import seaborn as sns + +from utils.data_scripts import get_mongo_collection, fetch_data, aggregate_data, create_dataframe, add_missing_dates, \ + mongo_find + +ABSOLUTE_PATH = "/backend/src/main/java/ru/hackaton/python_scripts/" + +def purchase(months, forecast): + """ + Рассчитывает сумму закупок за заданное количество месяцев. + + :param months: Количество месяцев + :param forecast: Прогноз потребления + :return: Сумма закупок + """ + sum_of_purchase = 0. + for quarter in range(months // 3): + sum_of_purchase += float(forecast.iloc[quarter]) + if months > (months // 3) * 3: + sum_of_purchase += float(forecast.iloc[months // 3]) * (months - (months // 3) * 3) / 3 + return sum_of_purchase + + +def plot_forecast(df_forecast, filename): + """ + Строит график прогноза потребления. + + :param df_forecast: DataFrame с прогнозом потребления + """ + + plt.figure(figsize=(8, 7)) + if len(df_forecast) == 1: + plt.xlim(-2, 2) + bars = sns.barplot(x='дата', y='прогноз', hue='дата', palette='Reds', data=df_forecast, legend=False) + + font = font_manager.FontProperties(family='serif', style='italic') + font1 = font_manager.FontProperties(family='serif',style='italic', size=16) + + # Add labels to the bars, using the x-axis values and y-axis values + for p in bars.patches: + plt.text(p.get_x() + p.get_width()/2., p.get_height(), f'{p.get_height():.2f}', + ha='center', va='bottom', fontsize=10, fontproperties=font) + + plt.xlabel('Дата', fontsize=14, fontproperties=font) + plt.ylabel('Прогноз потребления', ha= 'center', fontsize=14, fontproperties=font) + plt.title('Прогноз потребления на конец каждого квартала', fontproperties=font1) + + plt.xticks(rotation=45, ha='center', fontsize=10, fontproperties=font) + plt.yticks(fontsize=12, fontstyle= 'italic',fontproperties=font) + + plt.savefig(filename) + + +def make_forecast(name, months): + """ + Делает прогноз потребления на заданное количество месяцев. + + :param name: Название продукта + :param months: Количество месяцев для прогноза + :return: Прогноз потребления или сообщение об ошибке + """ + collection = get_mongo_collection("Оборотная ведомость") + name = name.lower().replace(" ", "") + data, dates = fetch_data(collection, name) + aggregated_data = aggregate_data(data, "единиц кредит во") + df = create_dataframe(aggregated_data, ['Kredit', 'дата']) + + if df.empty: + return "Извините, данные оборотной ведомости не найдены для данного товара, невозможно предсказать потребление." + + df = add_missing_dates(df, dates, 'Kredit') + df.sort_values(by='дата', inplace=True) + + plt.figure(figsize=(8, 7)) + + + bars = sns.barplot(x='дата', y='Kredit', hue='дата', palette='Reds', data=df, legend=False) + + font = font_manager.FontProperties(family='serif', style='italic') + font1 = font_manager.FontProperties(family='serif',style='italic', size=16) + + # Add labels to the bars, using the x-axis values and y-axis values + for p in bars.patches: + plt.text(p.get_x() + p.get_width()/2., p.get_height(), f'{p.get_height():.2f}', + ha='center', va='bottom', fontsize=10, fontproperties=font) + + plt.xlabel('Дата', fontsize=14, fontproperties=font) + plt.ylabel('Потребление', ha= 'center', fontsize=14, fontproperties=font) + plt.title('Известное потребление за период в конце квартала', fontproperties=font1) + + plt.xticks(rotation=45, ha='center', fontsize=10, fontproperties=font) + plt.yticks(fontsize=12, fontstyle= 'italic',fontproperties=font) + + + known_consume_filename = ABSOLUTE_PATH + "images/" + str(time_ns()) + ".png" + plt.savefig(known_consume_filename) + if pd.notna(df['Kredit']).sum() == 0 or df['Kredit'].max() == 0: + return ("Извините, кажется, данный товар не тратился в течение всего времени, невозможно предсказать " + "потребление.") + df.set_index('дата', inplace=True) + model = auto_arima(df, seasonal=True, stepwise=True, trace=False, m=4, D=0) + forecast = model.predict(n_periods=ceil(months / 3)) + forecast = forecast.apply(lambda x: round(x)) + + if forecast.max() == 0: + return "Кажется данный товар редко используется, невозможно предсказать потребление.\n" + known_consume_filename + + df_forecast = pd.DataFrame( + {'дата': pd.date_range(start=forecast.index[0], periods=len(forecast), freq='Q'), 'прогноз': forecast}) + predict_consume_filename = ABSOLUTE_PATH + "images/" + str(time_ns()) + ".png" + if months % 3 == 0: + plot_forecast(df_forecast, predict_consume_filename) + else: + monthly_consuming = pd.DataFrame(columns=['дата', 'прогноз']) + for index, row in df_forecast.iterrows(): + avg_consuming = row['прогноз'] / 3 + until_month = 0 + if index == df_forecast.index[-1]: + until_month = 3 - months % 3 + for month in range(2, until_month - 1, -1): + monthly_consuming.loc[len(monthly_consuming)] = [row['дата'] - pd.offsets.MonthEnd(month), + avg_consuming] + + plt.figure(figsize=(8, 7)) + if months == 1: + plt.xlim(-2, 2) + plt.bar(monthly_consuming['дата'].dt.strftime('%Y-%m'), monthly_consuming['прогноз'], color='blue', width=0.4) + + bars = sns.barplot(x='дата', y='прогноз', hue='дата', palette='Reds', data=monthly_consuming, legend=False) + font = font_manager.FontProperties(family='serif', style='italic') + font1 = font_manager.FontProperties(family='serif',style='italic', size=16) + # Add labels to the bars, using the x-axis values and y-axis values + for p in bars.patches: + plt.text(p.get_x() + p.get_width()/2., p.get_height(), f'{p.get_height():.2f}', + ha='center', va='bottom', fontsize=10, fontproperties=font) + plt.xlabel('Дата', fontsize=14, fontproperties=font) + plt.ylabel('Прогноз потребления', ha= 'center', fontsize=14, fontproperties=font) + plt.title('Прогноз потребления по месяцам', fontproperties=font1) + plt.xticks(rotation=45, ha='center', fontsize=10, fontproperties=font) + plt.yticks(fontsize=12, fontstyle= 'italic',fontproperties=font) + + plt.savefig(predict_consume_filename) + + remainings_data = mongo_find({"Название": name}) + df = pd.DataFrame(columns=['дата', 'остатки']) + for document in remainings_data: + df.loc[len(df)] = [document['Дата'], document['Остаток']] + + df['дата'] = pd.to_datetime(df['дата'], dayfirst=True) + df.sort_values(by='дата', inplace=True) + df.reset_index(drop=True, inplace=True) + + if len(df) == 0 or df.iloc[-1]['дата'] < datetime(2022, 12, 31): + rem = 0. + else: + rem = float(df.iloc[-1]['остатки']) + + sum_of_purchase = purchase(months, forecast) + if sum_of_purchase < rem: + return "На складе имеется достаточное количество товаров для данного срока.\n" + known_consume_filename + "\n" + predict_consume_filename + + return "Необходимо докупить " + str(ceil((sum_of_purchase - rem) * 1.1)) + " ед. товара.\n" + known_consume_filename + "\n" + predict_consume_filename + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='Process some arguments.') + parser.add_argument('item_name', type=str) + parser.add_argument('n_months', type=int) + args = parser.parse_args() + print(make_forecast(args.item_name, args.n_months))