Update app.py
Browse files
app.py
CHANGED
@@ -6,16 +6,27 @@ from prophet import Prophet
|
|
6 |
import io
|
7 |
from PIL import Image
|
8 |
|
9 |
-
#
|
|
|
|
|
10 |
URL_DASHA = "https://raw.githubusercontent.com/fruitpicker01/Storage_Dasha_2025/main/messages.csv"
|
11 |
URL_LERA = "https://raw.githubusercontent.com/fruitpicker01/Storage_Lera_2025/main/messages.csv"
|
12 |
URL_SVETA = "https://raw.githubusercontent.com/fruitpicker01/Storage_Sveta_2025/main/messages.csv"
|
13 |
|
14 |
-
#
|
|
|
|
|
15 |
URL_DASHA_2 = "https://raw.githubusercontent.com/fruitpicker01/Storage_2_Dasha_2025/main/messages.csv"
|
16 |
URL_LERA_2 = "https://raw.githubusercontent.com/fruitpicker01/Storage_2_Lera_2025/main/messages.csv"
|
17 |
URL_SVETA_2 = "https://raw.githubusercontent.com/fruitpicker01/Storage_2_Sveta_2025/main/messages.csv"
|
18 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
19 |
def read_and_process_data(url, user_name):
|
20 |
"""
|
21 |
Считывает CSV, отбирает нужные столбцы,
|
@@ -29,41 +40,33 @@ def read_and_process_data(url, user_name):
|
|
29 |
|
30 |
print(f"\n=== [{user_name}] чтение CSV ===")
|
31 |
|
32 |
-
# 1) Предположим, что в url указано что-то вроде
|
33 |
-
# "https://github.com/username/repo/blob/main/messages.csv"
|
34 |
-
# или "https://raw.githubusercontent.com/..."
|
35 |
-
# Чтобы использовать API, нужно получить путь (owner, repo, path).
|
36 |
-
# Если у вас уже есть "https://raw.githubusercontent.com/<owner>/<repo>/main/messages.csv",
|
37 |
-
# то придётся вручную подставить значения owner/repo/file_path для Contents API.
|
38 |
-
|
39 |
-
# Пример разбора url (упрощённо):
|
40 |
-
# - Здесь у нас raw-ссылки, например:
|
41 |
-
# "https://raw.githubusercontent.com/fruitpicker01/Storage_Lera_2025/main/messages.csv"
|
42 |
-
# => owner = "fruitpicker01", repo = "Storage_Lera_2025", path = "messages.csv"
|
43 |
-
# В зависимости от структуры URL меняйте parse_* как нужно
|
44 |
-
|
45 |
-
# !!! ВАЖНО: Если у вас несколько веток/папок, подставьте их правильно ниже.
|
46 |
import re
|
47 |
-
|
48 |
pattern = re.compile(r"https://raw\.githubusercontent\.com/([^/]+)/([^/]+)/([^/]+)/(.+)")
|
49 |
m = pattern.match(url)
|
50 |
if not m:
|
51 |
-
#
|
52 |
print(f"[{user_name}] URL не совпадает с raw.githubusercontent.com, читаем напрямую...")
|
53 |
-
|
|
|
|
|
|
|
|
|
54 |
else:
|
55 |
owner = m.group(1)
|
56 |
repo_name = m.group(2)
|
57 |
branch = m.group(3)
|
58 |
-
file_path = m.group(4)
|
59 |
|
60 |
-
# 2) Обращаемся к GitHub Contents API
|
61 |
api_url = f"https://api.github.com/repos/{owner}/{repo_name}/contents/{file_path}?ref={branch}"
|
62 |
print(f"[{user_name}] Пытаемся Contents API: {api_url}")
|
63 |
resp = requests.get(api_url)
|
64 |
if resp.status_code != 200:
|
65 |
print(f"[{user_name}] Не удалось получить JSON (статус={resp.status_code}), читаем напрямую...")
|
66 |
-
|
|
|
|
|
|
|
|
|
67 |
else:
|
68 |
data_json = resp.json()
|
69 |
size = data_json.get("size", 0)
|
@@ -73,18 +76,25 @@ def read_and_process_data(url, user_name):
|
|
73 |
if not file_content_encoded or size > 1_000_000:
|
74 |
# Большой файл или отсутствует content => используем download_url
|
75 |
print(f"[{user_name}] Файл крупнее 1 МБ или content отсутствует, скачиваем по download_url={download_url}")
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
|
|
|
|
|
|
|
|
|
80 |
else:
|
81 |
# Получаем Base64 и декодируем
|
82 |
-
|
83 |
-
|
|
|
|
|
|
|
|
|
84 |
|
85 |
print(f"[{user_name}] Исходное кол-во строк: {len(df)}")
|
86 |
|
87 |
-
# Дальше та же логика, что у вас была
|
88 |
cols = ["gender", "generation", "industry", "opf", "timestamp"]
|
89 |
df = df[[c for c in cols if c in df.columns]].copy()
|
90 |
print(f"[{user_name}] После отбора столбцов: {df.shape}")
|
@@ -106,7 +116,6 @@ def read_and_process_data(url, user_name):
|
|
106 |
|
107 |
return unique_count, df_daily
|
108 |
|
109 |
-
|
110 |
def make_average_forecast(total_by_date, end_date_str="2025-03-31"):
|
111 |
"""
|
112 |
Делает «прогноз по среднему» до указанной даты (end_date_str).
|
@@ -142,16 +151,15 @@ def make_average_forecast(total_by_date, end_date_str="2025-03-31"):
|
|
142 |
|
143 |
return pd.DataFrame(forecast_data)
|
144 |
|
145 |
-
|
146 |
def process_data():
|
147 |
print("\n=== Начинаем process_data (Seaborn + Prophet + средний) ===")
|
148 |
|
149 |
-
# Чтение
|
150 |
dasha_count, dasha_daily = read_and_process_data(URL_DASHA, "Даша")
|
151 |
lera_count, lera_daily = read_and_process_data(URL_LERA, "Лера")
|
152 |
sveta_count, sveta_daily = read_and_process_data(URL_SVETA, "Света")
|
153 |
|
154 |
-
# Чтение
|
155 |
try:
|
156 |
dasha_count2, dasha_daily2 = read_and_process_data(URL_DASHA_2, "Даша (2)")
|
157 |
dasha_daily2["user"] = "Даша"
|
@@ -161,7 +169,6 @@ def process_data():
|
|
161 |
|
162 |
try:
|
163 |
lera_count2, lera_daily2 = read_and_process_data(URL_LERA_2, "Лера (2)")
|
164 |
-
# Переопределяем имя пользователя, чтобы объединить данные
|
165 |
lera_daily2["user"] = "Лера"
|
166 |
except Exception as e:
|
167 |
print(f"[Лера (2)] Ошибка при чтении дополнительного CSV: {e}")
|
@@ -174,20 +181,42 @@ def process_data():
|
|
174 |
print(f"[Света (2)] Ошибка при чтении дополнительного CSV: {e}")
|
175 |
sveta_count2, sveta_daily2 = 0, pd.DataFrame(columns=["date", "count", "user"])
|
176 |
|
177 |
-
#
|
178 |
-
|
179 |
-
|
180 |
-
|
|
|
|
|
|
|
|
|
181 |
|
182 |
-
|
183 |
-
|
184 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
185 |
|
186 |
total_count = dasha_count_total + lera_count_total + sveta_count_total
|
187 |
print(f"Суммарное количество (Д+Л+С): {total_count}")
|
188 |
|
189 |
-
#
|
190 |
-
# замените исходные переменные на объединённые *_total
|
191 |
dasha_percent = round((dasha_count_total / 234) * 100) if 234 else 0
|
192 |
lera_percent = round((lera_count_total / 234) * 100) if 234 else 0
|
193 |
sveta_percent = round((sveta_count_total / 234) * 100) if 234 else 0
|
@@ -212,28 +241,24 @@ def process_data():
|
|
212 |
get_progress_bar("Всего", total_count, total_percent)
|
213 |
)
|
214 |
|
215 |
-
#
|
216 |
daily_all = pd.concat([dasha_daily_total, lera_daily_total, sveta_daily_total], ignore_index=True)
|
217 |
daily_all = daily_all.dropna(subset=["date"])
|
218 |
daily_all = daily_all.sort_values(["user", "date"])
|
219 |
-
|
220 |
-
# Приведение столбца "count" к числовому типу
|
221 |
daily_all["count"] = pd.to_numeric(daily_all["count"], errors="coerce").fillna(0)
|
222 |
-
|
223 |
-
# Вычисление накопительной суммы
|
224 |
daily_all["cumulative"] = daily_all.groupby("user")["count"].cumsum()
|
225 |
|
226 |
-
# «Всего»
|
227 |
total_by_date = daily_all.groupby("date")["count"].sum().reset_index(name="count")
|
228 |
total_by_date = total_by_date.sort_values("date")
|
229 |
total_by_date["cumulative"] = total_by_date["count"].cumsum()
|
230 |
total_by_date["user"] = "Всего"
|
231 |
|
232 |
-
#
|
233 |
daily_all_final = pd.concat([daily_all, total_by_date], ignore_index=True)
|
234 |
daily_all_final["date_dt"] = pd.to_datetime(daily_all_final["date"])
|
235 |
|
236 |
-
#
|
237 |
last_values = daily_all_final.groupby("user")["cumulative"].last().sort_values(ascending=False)
|
238 |
sorted_users = last_values.index.tolist()
|
239 |
|
@@ -242,7 +267,7 @@ def process_data():
|
|
242 |
data=daily_all_final,
|
243 |
x="date_dt", y="cumulative",
|
244 |
hue="user",
|
245 |
-
hue_order=sorted_users,
|
246 |
ax=ax1, marker="o"
|
247 |
)
|
248 |
ax1.set_title("Накопительное количество SMS")
|
@@ -255,23 +280,21 @@ def process_data():
|
|
255 |
buf1.seek(0)
|
256 |
image1_pil = Image.open(buf1)
|
257 |
|
258 |
-
#
|
259 |
df_prophet = total_by_date[["date", "cumulative"]].copy()
|
260 |
df_prophet.columns = ["ds", "y"]
|
261 |
df_prophet["ds"] = pd.to_datetime(df_prophet["ds"])
|
262 |
|
263 |
-
# Prophet-модель
|
264 |
model = Prophet()
|
265 |
model.fit(df_prophet)
|
266 |
|
267 |
-
# Прогноз до 31 марта 2025
|
268 |
end_date = pd.to_datetime("2025-03-31")
|
269 |
last_date = df_prophet["ds"].max()
|
270 |
additional_days = (end_date - last_date).days
|
271 |
future = model.make_future_dataframe(periods=additional_days if additional_days>0 else 0)
|
272 |
forecast = model.predict(future)
|
273 |
|
274 |
-
#
|
275 |
df_plot = pd.merge(
|
276 |
forecast[["ds", "yhat"]],
|
277 |
df_prophet[["ds", "y"]],
|
@@ -284,26 +307,21 @@ def process_data():
|
|
284 |
# Прогноз по среднему
|
285 |
df_avg = make_average_forecast(total_by_date, "2025-03-31")
|
286 |
|
287 |
-
# Преобразуем для Seaborn
|
288 |
-
# История
|
289 |
df_history["type"] = "История"
|
290 |
df_history["value"] = df_history["y"]
|
291 |
-
|
292 |
df_future["type"] = "Прогноз (Prophet)"
|
293 |
df_future["value"] = df_future["yhat"]
|
294 |
|
295 |
-
# Средний
|
296 |
df_avg["type"] = "Прогноз (среднее)"
|
297 |
df_avg["value"] = df_avg["yhat"]
|
298 |
df_avg.rename(columns={"ds":"ds"}, inplace=True)
|
299 |
|
300 |
-
# Сшиваем
|
301 |
df_combined = pd.concat([df_history, df_future, df_avg], ignore_index=True)
|
302 |
-
|
303 |
-
# Для удобства
|
304 |
df_combined["ds"] = pd.to_datetime(df_combined["ds"])
|
305 |
|
306 |
-
#
|
307 |
line_styles = {
|
308 |
"История": "",
|
309 |
"Прогноз (Prophet)": (2,2),
|
@@ -336,14 +354,13 @@ def process_data():
|
|
336 |
buf2.seek(0)
|
337 |
image2_pil = Image.open(buf2)
|
338 |
|
339 |
-
#
|
340 |
return bars_html, image1_pil, image2_pil
|
341 |
|
342 |
|
343 |
# Gradio-интерфейс
|
344 |
with gr.Blocks() as demo:
|
345 |
gr.Markdown("<h2>Количество сохраненных SMS (Даша, Лера, Света, Всего) + Прогноз</h2>")
|
346 |
-
# gr.Markdown("<h2>Временно закрыто на ремонт")
|
347 |
btn = gr.Button("Обновить данные и показать результат")
|
348 |
|
349 |
html_output = gr.HTML(label="Прогресс-бары: количество SMS и %")
|
|
|
6 |
import io
|
7 |
from PIL import Image
|
8 |
|
9 |
+
# =====================
|
10 |
+
# Первый набор CSV-файлов
|
11 |
+
# =====================
|
12 |
URL_DASHA = "https://raw.githubusercontent.com/fruitpicker01/Storage_Dasha_2025/main/messages.csv"
|
13 |
URL_LERA = "https://raw.githubusercontent.com/fruitpicker01/Storage_Lera_2025/main/messages.csv"
|
14 |
URL_SVETA = "https://raw.githubusercontent.com/fruitpicker01/Storage_Sveta_2025/main/messages.csv"
|
15 |
|
16 |
+
# =====================
|
17 |
+
# Второй набор CSV-файлов
|
18 |
+
# =====================
|
19 |
URL_DASHA_2 = "https://raw.githubusercontent.com/fruitpicker01/Storage_2_Dasha_2025/main/messages.csv"
|
20 |
URL_LERA_2 = "https://raw.githubusercontent.com/fruitpicker01/Storage_2_Lera_2025/main/messages.csv"
|
21 |
URL_SVETA_2 = "https://raw.githubusercontent.com/fruitpicker01/Storage_2_Sveta_2025/main/messages.csv"
|
22 |
|
23 |
+
# =====================
|
24 |
+
# Третий набор CSV-файлов (messages_2.csv)
|
25 |
+
# =====================
|
26 |
+
URL_DASHA_3 = "https://raw.githubusercontent.com/fruitpicker01/Storage_2_Dasha_2025/main/messages_2.csv"
|
27 |
+
URL_LERA_3 = "https://raw.githubusercontent.com/fruitpicker01/Storage_2_Lera_2025/main/messages_2.csv"
|
28 |
+
URL_SVETA_3 = "https://raw.githubusercontent.com/fruitpicker01/Storage_2_Sveta_2025/main/messages_2.csv"
|
29 |
+
|
30 |
def read_and_process_data(url, user_name):
|
31 |
"""
|
32 |
Считывает CSV, отбирает нужные столбцы,
|
|
|
40 |
|
41 |
print(f"\n=== [{user_name}] чтение CSV ===")
|
42 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
43 |
import re
|
|
|
44 |
pattern = re.compile(r"https://raw\.githubusercontent\.com/([^/]+)/([^/]+)/([^/]+)/(.+)")
|
45 |
m = pattern.match(url)
|
46 |
if not m:
|
47 |
+
# Если URL не совпадает с raw.githubusercontent.com, пробуем напрямую
|
48 |
print(f"[{user_name}] URL не совпадает с raw.githubusercontent.com, читаем напрямую...")
|
49 |
+
try:
|
50 |
+
df = pd.read_csv(url, na_values=["Не выбрано"])
|
51 |
+
except Exception as e:
|
52 |
+
print(f"[{user_name}] Ошибка при pd.read_csv напрямую: {e}")
|
53 |
+
return 0, pd.DataFrame(columns=["date", "count", "user"])
|
54 |
else:
|
55 |
owner = m.group(1)
|
56 |
repo_name = m.group(2)
|
57 |
branch = m.group(3)
|
58 |
+
file_path = m.group(4)
|
59 |
|
|
|
60 |
api_url = f"https://api.github.com/repos/{owner}/{repo_name}/contents/{file_path}?ref={branch}"
|
61 |
print(f"[{user_name}] Пытаемся Contents API: {api_url}")
|
62 |
resp = requests.get(api_url)
|
63 |
if resp.status_code != 200:
|
64 |
print(f"[{user_name}] Не удалось получить JSON (статус={resp.status_code}), читаем напрямую...")
|
65 |
+
try:
|
66 |
+
df = pd.read_csv(url, na_values=["Не выбрано"])
|
67 |
+
except Exception as e:
|
68 |
+
print(f"[{user_name}] Ошибка при pd.read_csv напрямую: {e}")
|
69 |
+
return 0, pd.DataFrame(columns=["date", "count", "user"])
|
70 |
else:
|
71 |
data_json = resp.json()
|
72 |
size = data_json.get("size", 0)
|
|
|
76 |
if not file_content_encoded or size > 1_000_000:
|
77 |
# Большой файл или отсутствует content => используем download_url
|
78 |
print(f"[{user_name}] Файл крупнее 1 МБ или content отсутствует, скачиваем по download_url={download_url}")
|
79 |
+
try:
|
80 |
+
resp2 = requests.get(download_url)
|
81 |
+
resp2.raise_for_status()
|
82 |
+
csv_text = resp2.text
|
83 |
+
df = pd.read_csv(io.StringIO(csv_text), na_values=["Не выбрано"])
|
84 |
+
except Exception as e:
|
85 |
+
print(f"[{user_name}] Ошибка при чтении по download_url: {e}")
|
86 |
+
return 0, pd.DataFrame(columns=["date", "count", "user"])
|
87 |
else:
|
88 |
# Получаем Base64 и декодируем
|
89 |
+
try:
|
90 |
+
file_bytes = base64.b64decode(file_content_encoded)
|
91 |
+
df = pd.read_csv(io.StringIO(file_bytes.decode("utf-8")), na_values=["Не выбрано"])
|
92 |
+
except Exception as e:
|
93 |
+
print(f"[{user_name}] Ошибка декодирования Base64: {e}")
|
94 |
+
return 0, pd.DataFrame(columns=["date", "count", "user"])
|
95 |
|
96 |
print(f"[{user_name}] Исходное кол-во строк: {len(df)}")
|
97 |
|
|
|
98 |
cols = ["gender", "generation", "industry", "opf", "timestamp"]
|
99 |
df = df[[c for c in cols if c in df.columns]].copy()
|
100 |
print(f"[{user_name}] После отбора столбцов: {df.shape}")
|
|
|
116 |
|
117 |
return unique_count, df_daily
|
118 |
|
|
|
119 |
def make_average_forecast(total_by_date, end_date_str="2025-03-31"):
|
120 |
"""
|
121 |
Делает «прогноз по среднему» до указанной даты (end_date_str).
|
|
|
151 |
|
152 |
return pd.DataFrame(forecast_data)
|
153 |
|
|
|
154 |
def process_data():
|
155 |
print("\n=== Начинаем process_data (Seaborn + Prophet + средний) ===")
|
156 |
|
157 |
+
# ====== Чтение данных (первый набор) ======
|
158 |
dasha_count, dasha_daily = read_and_process_data(URL_DASHA, "Даша")
|
159 |
lera_count, lera_daily = read_and_process_data(URL_LERA, "Лера")
|
160 |
sveta_count, sveta_daily = read_and_process_data(URL_SVETA, "Света")
|
161 |
|
162 |
+
# ====== Чтение (второй набор) ======
|
163 |
try:
|
164 |
dasha_count2, dasha_daily2 = read_and_process_data(URL_DASHA_2, "Даша (2)")
|
165 |
dasha_daily2["user"] = "Даша"
|
|
|
169 |
|
170 |
try:
|
171 |
lera_count2, lera_daily2 = read_and_process_data(URL_LERA_2, "Лера (2)")
|
|
|
172 |
lera_daily2["user"] = "Лера"
|
173 |
except Exception as e:
|
174 |
print(f"[Лера (2)] Ошибка при чтении дополнительного CSV: {e}")
|
|
|
181 |
print(f"[Света (2)] Ошибка при чтении дополнительного CSV: {e}")
|
182 |
sveta_count2, sveta_daily2 = 0, pd.DataFrame(columns=["date", "count", "user"])
|
183 |
|
184 |
+
# ====== Чтение (третий набор: messages_2.csv) ======
|
185 |
+
try:
|
186 |
+
dasha_count3, dasha_daily3 = read_and_process_data(URL_DASHA_3, "Даша (3)")
|
187 |
+
# Объединяем с "Дашей"
|
188 |
+
dasha_daily3["user"] = "Даша"
|
189 |
+
except Exception as e:
|
190 |
+
print(f"[Даша (3)] Ошибка при чтении messages_2.csv: {e}")
|
191 |
+
dasha_count3, dasha_daily3 = 0, pd.DataFrame(columns=["date", "count", "user"])
|
192 |
|
193 |
+
try:
|
194 |
+
lera_count3, lera_daily3 = read_and_process_data(URL_LERA_3, "Лера (3)")
|
195 |
+
lera_daily3["user"] = "Лера"
|
196 |
+
except Exception as e:
|
197 |
+
print(f"[Лера (3)] Ошибка при чтении messages_2.csv: {e}")
|
198 |
+
lera_count3, lera_daily3 = 0, pd.DataFrame(columns=["date", "count", "user"])
|
199 |
+
|
200 |
+
try:
|
201 |
+
sveta_count3, sveta_daily3 = read_and_process_data(URL_SVETA_3, "Света (3)")
|
202 |
+
sveta_daily3["user"] = "Света"
|
203 |
+
except Exception as e:
|
204 |
+
print(f"[Света (3)] Ошибка при чтении messages_2.csv: {e}")
|
205 |
+
sveta_count3, sveta_daily3 = 0, pd.DataFrame(columns=["date", "count", "user"])
|
206 |
+
|
207 |
+
# ====== Итоговые суммы ======
|
208 |
+
dasha_count_total = dasha_count + dasha_count2 + dasha_count3
|
209 |
+
lera_count_total = lera_count + lera_count2 + lera_count3
|
210 |
+
sveta_count_total = sveta_count + sveta_count2 + sveta_count3
|
211 |
+
|
212 |
+
dasha_daily_total = pd.concat([dasha_daily, dasha_daily2, dasha_daily3], ignore_index=True)
|
213 |
+
lera_daily_total = pd.concat([lera_daily, lera_daily2, lera_daily3 ], ignore_index=True)
|
214 |
+
sveta_daily_total = pd.concat([sveta_daily, sveta_daily2, sveta_daily3], ignore_index=True)
|
215 |
|
216 |
total_count = dasha_count_total + lera_count_total + sveta_count_total
|
217 |
print(f"Суммарное количество (Д+Л+С): {total_count}")
|
218 |
|
219 |
+
# ====== Проценты ======
|
|
|
220 |
dasha_percent = round((dasha_count_total / 234) * 100) if 234 else 0
|
221 |
lera_percent = round((lera_count_total / 234) * 100) if 234 else 0
|
222 |
sveta_percent = round((sveta_count_total / 234) * 100) if 234 else 0
|
|
|
241 |
get_progress_bar("Всего", total_count, total_percent)
|
242 |
)
|
243 |
|
244 |
+
# ====== Ежедневные данные + накопительное ======
|
245 |
daily_all = pd.concat([dasha_daily_total, lera_daily_total, sveta_daily_total], ignore_index=True)
|
246 |
daily_all = daily_all.dropna(subset=["date"])
|
247 |
daily_all = daily_all.sort_values(["user", "date"])
|
|
|
|
|
248 |
daily_all["count"] = pd.to_numeric(daily_all["count"], errors="coerce").fillna(0)
|
|
|
|
|
249 |
daily_all["cumulative"] = daily_all.groupby("user")["count"].cumsum()
|
250 |
|
251 |
+
# «Всего» по датам
|
252 |
total_by_date = daily_all.groupby("date")["count"].sum().reset_index(name="count")
|
253 |
total_by_date = total_by_date.sort_values("date")
|
254 |
total_by_date["cumulative"] = total_by_date["count"].cumsum()
|
255 |
total_by_date["user"] = "Всего"
|
256 |
|
257 |
+
# ====== Первый график (накопительные кривые) ======
|
258 |
daily_all_final = pd.concat([daily_all, total_by_date], ignore_index=True)
|
259 |
daily_all_final["date_dt"] = pd.to_datetime(daily_all_final["date"])
|
260 |
|
261 |
+
# Сортируем легенду по убыванию финальной точки
|
262 |
last_values = daily_all_final.groupby("user")["cumulative"].last().sort_values(ascending=False)
|
263 |
sorted_users = last_values.index.tolist()
|
264 |
|
|
|
267 |
data=daily_all_final,
|
268 |
x="date_dt", y="cumulative",
|
269 |
hue="user",
|
270 |
+
hue_order=sorted_users,
|
271 |
ax=ax1, marker="o"
|
272 |
)
|
273 |
ax1.set_title("Накопительное количество SMS")
|
|
|
280 |
buf1.seek(0)
|
281 |
image1_pil = Image.open(buf1)
|
282 |
|
283 |
+
# ====== Prophet + Прогноз по среднему (всего) ======
|
284 |
df_prophet = total_by_date[["date", "cumulative"]].copy()
|
285 |
df_prophet.columns = ["ds", "y"]
|
286 |
df_prophet["ds"] = pd.to_datetime(df_prophet["ds"])
|
287 |
|
|
|
288 |
model = Prophet()
|
289 |
model.fit(df_prophet)
|
290 |
|
|
|
291 |
end_date = pd.to_datetime("2025-03-31")
|
292 |
last_date = df_prophet["ds"].max()
|
293 |
additional_days = (end_date - last_date).days
|
294 |
future = model.make_future_dataframe(periods=additional_days if additional_days>0 else 0)
|
295 |
forecast = model.predict(future)
|
296 |
|
297 |
+
# Подготовка данных для графика
|
298 |
df_plot = pd.merge(
|
299 |
forecast[["ds", "yhat"]],
|
300 |
df_prophet[["ds", "y"]],
|
|
|
307 |
# Прогноз по среднему
|
308 |
df_avg = make_average_forecast(total_by_date, "2025-03-31")
|
309 |
|
|
|
|
|
310 |
df_history["type"] = "История"
|
311 |
df_history["value"] = df_history["y"]
|
312 |
+
|
313 |
df_future["type"] = "Прогноз (Prophet)"
|
314 |
df_future["value"] = df_future["yhat"]
|
315 |
|
|
|
316 |
df_avg["type"] = "Прогноз (среднее)"
|
317 |
df_avg["value"] = df_avg["yhat"]
|
318 |
df_avg.rename(columns={"ds":"ds"}, inplace=True)
|
319 |
|
320 |
+
# Сшиваем
|
321 |
df_combined = pd.concat([df_history, df_future, df_avg], ignore_index=True)
|
|
|
|
|
322 |
df_combined["ds"] = pd.to_datetime(df_combined["ds"])
|
323 |
|
324 |
+
# Второй график
|
325 |
line_styles = {
|
326 |
"История": "",
|
327 |
"Прогноз (Prophet)": (2,2),
|
|
|
354 |
buf2.seek(0)
|
355 |
image2_pil = Image.open(buf2)
|
356 |
|
357 |
+
# Результат
|
358 |
return bars_html, image1_pil, image2_pil
|
359 |
|
360 |
|
361 |
# Gradio-интерфейс
|
362 |
with gr.Blocks() as demo:
|
363 |
gr.Markdown("<h2>Количество сохраненных SMS (Даша, Лера, Света, Всего) + Прогноз</h2>")
|
|
|
364 |
btn = gr.Button("Обновить данные и показать результат")
|
365 |
|
366 |
html_output = gr.HTML(label="Прогресс-бары: количество SMS и %")
|