Update app.py
Browse files
app.py
CHANGED
@@ -1,6 +1,8 @@
|
|
1 |
import gradio as gr
|
2 |
import pandas as pd
|
3 |
import plotly.express as px
|
|
|
|
|
4 |
from prophet import Prophet
|
5 |
|
6 |
# Ссылки на CSV-файлы
|
@@ -38,46 +40,6 @@ def read_and_process_data(url, user_name):
|
|
38 |
|
39 |
return unique_count, df_daily
|
40 |
|
41 |
-
def make_average_forecast(total_by_date, end_date_str="2025-02-28"):
|
42 |
-
"""
|
43 |
-
Строит «прогноз по среднему» до указанной даты (end_date_str),
|
44 |
-
считая средний дневной прирост по будням (Mon-Fri).
|
45 |
-
В выходные прирост считаем 0.
|
46 |
-
|
47 |
-
Возвращает DataFrame с колонками ["ds", "yhat"],
|
48 |
-
начиная с (last_date+1) по end_date_str (включительно).
|
49 |
-
"""
|
50 |
-
if total_by_date.empty:
|
51 |
-
return pd.DataFrame(columns=["ds", "yhat"])
|
52 |
-
|
53 |
-
df_tmp = total_by_date.copy()
|
54 |
-
df_tmp["date"] = pd.to_datetime(df_tmp["date"])
|
55 |
-
|
56 |
-
# Средний дневной прирост (столбец "count") только по будням (Monday=0 ... Friday=4)
|
57 |
-
df_weekdays = df_tmp[df_tmp["date"].dt.weekday < 5]
|
58 |
-
avg_inc = df_weekdays["count"].mean() if len(df_weekdays) else 0
|
59 |
-
|
60 |
-
last_date = df_tmp["date"].max()
|
61 |
-
last_cumulative = df_tmp["cumulative"].iloc[-1]
|
62 |
-
|
63 |
-
end_date = pd.to_datetime(end_date_str)
|
64 |
-
|
65 |
-
# Движемся по календарю день за днём
|
66 |
-
current_date = last_date
|
67 |
-
forecast_data = []
|
68 |
-
running_total = last_cumulative
|
69 |
-
|
70 |
-
while current_date < end_date:
|
71 |
-
current_date += pd.Timedelta(days=1)
|
72 |
-
if current_date > end_date:
|
73 |
-
break
|
74 |
-
# Если будний день
|
75 |
-
if current_date.weekday() < 5:
|
76 |
-
running_total += avg_inc
|
77 |
-
forecast_data.append({"ds": current_date, "yhat": running_total})
|
78 |
-
|
79 |
-
return pd.DataFrame(forecast_data)
|
80 |
-
|
81 |
def process_data():
|
82 |
# Считываем и обрабатываем все репозитории
|
83 |
dasha_count, dasha_daily = read_and_process_data(URL_DASHA, "Даша")
|
@@ -95,7 +57,11 @@ def process_data():
|
|
95 |
|
96 |
# Генерируем HTML-прогресс-бары
|
97 |
def get_progress_bar(label, abs_val, pct):
|
98 |
-
|
|
|
|
|
|
|
|
|
99 |
return f"""
|
100 |
<div style='margin-bottom: 1em;'>
|
101 |
<div><strong>{label}</strong></div>
|
@@ -114,127 +80,107 @@ def process_data():
|
|
114 |
get_progress_bar("Всего", total_count, total_percent)
|
115 |
)
|
116 |
|
117 |
-
#
|
118 |
daily_all = pd.concat([dasha_daily, lera_daily, sveta_daily], ignore_index=True)
|
|
|
119 |
daily_all = daily_all.dropna(subset=["date"])
|
120 |
|
121 |
-
# Считаем
|
122 |
daily_all = daily_all.sort_values(by=["user", "date"])
|
123 |
daily_all["cumulative"] = daily_all.groupby("user")["count"].cumsum()
|
124 |
|
125 |
-
# "Всего"
|
126 |
total_by_date = daily_all.groupby("date")["count"].sum().reset_index(name="count")
|
127 |
total_by_date = total_by_date.sort_values(by="date")
|
128 |
total_by_date["cumulative"] = total_by_date["count"].cumsum()
|
129 |
total_by_date["user"] = "Всего"
|
130 |
|
131 |
-
# Объединим
|
132 |
daily_all_final = pd.concat([daily_all, total_by_date], ignore_index=True)
|
133 |
|
134 |
-
#
|
|
|
135 |
last_values = daily_all_final.groupby("user")["cumulative"].last().sort_values(ascending=False)
|
136 |
-
sorted_users = last_values.index.tolist()
|
137 |
|
138 |
-
# Явно
|
139 |
color_map = {
|
140 |
-
"Даша": "#1f77b4",
|
141 |
-
"Лера": "#2ca02c",
|
142 |
-
"Света": "#d62728",
|
143 |
-
"Всего": "#9467bd"
|
144 |
}
|
145 |
|
146 |
-
#
|
147 |
fig = px.line(
|
148 |
daily_all_final,
|
149 |
x="date",
|
150 |
y="cumulative",
|
151 |
color="user",
|
152 |
title="Накопительное количество SMS",
|
153 |
-
labels={
|
154 |
-
|
|
|
|
|
|
|
|
|
155 |
color_discrete_map=color_map
|
156 |
)
|
157 |
|
158 |
-
#
|
159 |
-
# Прогноз (Prophet) с учётом, что по выходным не должно быть прироста
|
160 |
-
# делается через freq='B' в make_future_dataframe (пропускает выходные).
|
161 |
-
# Второй — «Прогноз по среднему», также без прироста в выходные.
|
162 |
-
# --------------------------------------------------------------------------------
|
163 |
-
|
164 |
forecast_fig = None
|
165 |
-
|
166 |
-
if not total_by_date.empty:
|
167 |
-
# Подготовка данных для Prophet (только "Всего")
|
168 |
df_prophet = total_by_date[["date", "cumulative"]].copy()
|
169 |
df_prophet.columns = ["ds", "y"]
|
170 |
df_prophet["ds"] = pd.to_datetime(df_prophet["ds"])
|
171 |
|
172 |
-
model = Prophet(
|
173 |
-
# Часто Prophet сам распознаёт weekly_seasonality, но укажем явно
|
174 |
-
|
175 |
model.fit(df_prophet)
|
176 |
|
177 |
-
|
|
|
178 |
last_date = df_prophet["ds"].max()
|
|
|
179 |
additional_days = (end_date - last_date).days
|
180 |
-
|
181 |
-
# Создаём будущее ТОЛЬКО по рабочим дням (freq='B')
|
182 |
-
future = model.make_future_dataframe(periods=0, freq='B')
|
183 |
if additional_days > 0:
|
184 |
-
future = model.make_future_dataframe(periods=additional_days
|
185 |
|
186 |
forecast = model.predict(future)
|
187 |
|
188 |
-
#
|
189 |
df_plot = pd.merge(
|
190 |
forecast[["ds", "yhat"]],
|
191 |
df_prophet[["ds", "y"]],
|
192 |
on="ds",
|
193 |
how="left"
|
194 |
)
|
195 |
-
df_history = df_plot.dropna(subset=["y"])
|
196 |
-
df_future = df_plot[df_plot["y"].isna()]
|
197 |
|
198 |
-
#
|
199 |
-
df_avg_forecast = make_average_forecast(total_by_date, "2025-02-28")
|
200 |
-
|
201 |
-
# Один общий график
|
202 |
forecast_fig = px.line(
|
203 |
df_history,
|
204 |
x="ds",
|
205 |
y="y",
|
206 |
title="Прогноз до конца февраля 2025 (всего)",
|
207 |
-
labels={"ds": "Дата", "y": "
|
208 |
)
|
209 |
-
|
210 |
-
# Prophet (пунктир красный)
|
211 |
forecast_fig.add_scatter(
|
212 |
x=df_future["ds"],
|
213 |
y=df_future["yhat"],
|
214 |
mode="lines",
|
215 |
-
name="Прогноз
|
216 |
line=dict(dash="dash", color="red")
|
217 |
)
|
218 |
-
|
219 |
-
# Прогноз «по среднему» (пунктир зелёный)
|
220 |
-
if not df_avg_forecast.empty:
|
221 |
-
forecast_fig.add_scatter(
|
222 |
-
x=df_avg_forecast["ds"],
|
223 |
-
y=df_avg_forecast["yhat"],
|
224 |
-
mode="lines",
|
225 |
-
name="Прогноз (по среднему)",
|
226 |
-
line=dict(dash="dash", color="green")
|
227 |
-
)
|
228 |
-
|
229 |
forecast_fig.update_layout(showlegend=True)
|
230 |
-
|
231 |
return (bars_html, fig, forecast_fig)
|
232 |
|
233 |
with gr.Blocks() as demo:
|
234 |
gr.Markdown("<h2>Количество сохраненных SMS (Даша, Лера, Света)</h2>")
|
235 |
btn = gr.Button("Обновить данные и показать результат")
|
236 |
html_output = gr.HTML(label="Прогресс-бары: количество SMS и %")
|
237 |
-
plot_output = gr.Plot(label="Накопительный график (Даша, Лера, Света, Всего)")
|
238 |
forecast_output = gr.Plot(label="Прогноз до конца февраля 2025 (всего)")
|
239 |
|
240 |
btn.click(fn=process_data, outputs=[html_output, plot_output, forecast_output])
|
|
|
1 |
import gradio as gr
|
2 |
import pandas as pd
|
3 |
import plotly.express as px
|
4 |
+
|
5 |
+
# Для Prophet
|
6 |
from prophet import Prophet
|
7 |
|
8 |
# Ссылки на CSV-файлы
|
|
|
40 |
|
41 |
return unique_count, df_daily
|
42 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
43 |
def process_data():
|
44 |
# Считываем и обрабатываем все репозитории
|
45 |
dasha_count, dasha_daily = read_and_process_data(URL_DASHA, "Даша")
|
|
|
57 |
|
58 |
# Генерируем HTML-прогресс-бары
|
59 |
def get_progress_bar(label, abs_val, pct):
|
60 |
+
# Определяем "ёмкость"
|
61 |
+
if label in ["Даша", "Лера", "Света"]:
|
62 |
+
capacity = 234
|
63 |
+
else:
|
64 |
+
capacity = 702
|
65 |
return f"""
|
66 |
<div style='margin-bottom: 1em;'>
|
67 |
<div><strong>{label}</strong></div>
|
|
|
80 |
get_progress_bar("Всего", total_count, total_percent)
|
81 |
)
|
82 |
|
83 |
+
# Собираем единый DataFrame по дням
|
84 |
daily_all = pd.concat([dasha_daily, lera_daily, sveta_daily], ignore_index=True)
|
85 |
+
# Уберём NaT
|
86 |
daily_all = daily_all.dropna(subset=["date"])
|
87 |
|
88 |
+
# Считаем кумулятивное количество SMS отдельно для каждого пользователя
|
89 |
daily_all = daily_all.sort_values(by=["user", "date"])
|
90 |
daily_all["cumulative"] = daily_all.groupby("user")["count"].cumsum()
|
91 |
|
92 |
+
# Чтобы отразить "Всего", добавим user="Всего"
|
93 |
total_by_date = daily_all.groupby("date")["count"].sum().reset_index(name="count")
|
94 |
total_by_date = total_by_date.sort_values(by="date")
|
95 |
total_by_date["cumulative"] = total_by_date["count"].cumsum()
|
96 |
total_by_date["user"] = "Всего"
|
97 |
|
98 |
+
# Объединим с daily_all
|
99 |
daily_all_final = pd.concat([daily_all, total_by_date], ignore_index=True)
|
100 |
|
101 |
+
# Определим порядок легенды, отсортировав по последнему кумулятивному значению
|
102 |
+
# (у кого больше — тот выше)
|
103 |
last_values = daily_all_final.groupby("user")["cumulative"].last().sort_values(ascending=False)
|
104 |
+
sorted_users = last_values.index.tolist() # в порядке убывания
|
105 |
|
106 |
+
# Явно зададим цвета
|
107 |
color_map = {
|
108 |
+
"Даша": "#1f77b4", # синий
|
109 |
+
"Лера": "#2ca02c", # зелёный
|
110 |
+
"Света": "#d62728", # красный
|
111 |
+
"Всего": "#9467bd" # фиолетовый
|
112 |
}
|
113 |
|
114 |
+
# Строим накопительный (кумулятивный) график (ли��ии)
|
115 |
fig = px.line(
|
116 |
daily_all_final,
|
117 |
x="date",
|
118 |
y="cumulative",
|
119 |
color="user",
|
120 |
title="Накопительное количество SMS",
|
121 |
+
labels={
|
122 |
+
"date": "Дата",
|
123 |
+
"cumulative": "Накопительное количество SMS",
|
124 |
+
"user": "Редактор"
|
125 |
+
},
|
126 |
+
category_orders={"user": sorted_users}, # порядок в легенде
|
127 |
color_discrete_map=color_map
|
128 |
)
|
129 |
|
130 |
+
# Прогноз с помощью Prophet (делаем для "Всего" до 28.02.2025)
|
|
|
|
|
|
|
|
|
|
|
131 |
forecast_fig = None
|
132 |
+
if len(total_by_date) > 1:
|
|
|
|
|
133 |
df_prophet = total_by_date[["date", "cumulative"]].copy()
|
134 |
df_prophet.columns = ["ds", "y"]
|
135 |
df_prophet["ds"] = pd.to_datetime(df_prophet["ds"])
|
136 |
|
137 |
+
model = Prophet()
|
|
|
|
|
138 |
model.fit(df_prophet)
|
139 |
|
140 |
+
# Рассчитаем, сколько дней до 28.02.2025
|
141 |
+
future = model.make_future_dataframe(periods=0) # на случай, если уже после
|
142 |
last_date = df_prophet["ds"].max()
|
143 |
+
end_date = pd.to_datetime("2025-02-28")
|
144 |
additional_days = (end_date - last_date).days
|
|
|
|
|
|
|
145 |
if additional_days > 0:
|
146 |
+
future = model.make_future_dataframe(periods=additional_days)
|
147 |
|
148 |
forecast = model.predict(future)
|
149 |
|
150 |
+
# Сопоставим исторические данные и прогноз
|
151 |
df_plot = pd.merge(
|
152 |
forecast[["ds", "yhat"]],
|
153 |
df_prophet[["ds", "y"]],
|
154 |
on="ds",
|
155 |
how="left"
|
156 |
)
|
157 |
+
df_history = df_plot.dropna(subset=["y"]) # исторические точки
|
158 |
+
df_future = df_plot[df_plot["y"].isna()] # будущее
|
159 |
|
160 |
+
# Отдельный график
|
|
|
|
|
|
|
161 |
forecast_fig = px.line(
|
162 |
df_history,
|
163 |
x="ds",
|
164 |
y="y",
|
165 |
title="Прогноз до конца февраля 2025 (всего)",
|
166 |
+
labels={"ds": "Дата", "y": "Накопительное количество SMS"}
|
167 |
)
|
|
|
|
|
168 |
forecast_fig.add_scatter(
|
169 |
x=df_future["ds"],
|
170 |
y=df_future["yhat"],
|
171 |
mode="lines",
|
172 |
+
name="Прогноз",
|
173 |
line=dict(dash="dash", color="red")
|
174 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
175 |
forecast_fig.update_layout(showlegend=True)
|
176 |
+
|
177 |
return (bars_html, fig, forecast_fig)
|
178 |
|
179 |
with gr.Blocks() as demo:
|
180 |
gr.Markdown("<h2>Количество сохраненных SMS (Даша, Лера, Света)</h2>")
|
181 |
btn = gr.Button("Обновить данные и показать результат")
|
182 |
html_output = gr.HTML(label="Прогресс-бары: количество SMS и %")
|
183 |
+
plot_output = gr.Plot(label="Накопительный график по датам (Даша, Лера, Света, Всего)")
|
184 |
forecast_output = gr.Plot(label="Прогноз до конца февраля 2025 (всего)")
|
185 |
|
186 |
btn.click(fn=process_data, outputs=[html_output, plot_output, forecast_output])
|