fruitpicker01 commited on
Commit
263298d
·
verified ·
1 Parent(s): 4f21d31

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +83 -66
app.py CHANGED
@@ -6,16 +6,27 @@ from prophet import Prophet
6
  import io
7
  from PIL import Image
8
 
9
- # Первые наборы CSV-файлов
 
 
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
- # Вторые наборы CSV-файлов
 
 
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
- # не узнали структуру: fallback - просто пробуем pd.read_csv напрямую
52
  print(f"[{user_name}] URL не совпадает с raw.githubusercontent.com, читаем напрямую...")
53
- df = pd.read_csv(url, na_values=["Не выбрано"])
 
 
 
 
54
  else:
55
  owner = m.group(1)
56
  repo_name = m.group(2)
57
  branch = m.group(3)
58
- file_path = m.group(4) # например "messages.csv"
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
- df = pd.read_csv(url, na_values=["Не выбрано"])
 
 
 
 
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
- resp2 = requests.get(download_url)
77
- resp2.raise_for_status()
78
- csv_text = resp2.text
79
- df = pd.read_csv(io.StringIO(csv_text), na_values=["Не выбрано"])
 
 
 
 
80
  else:
81
  # Получаем Base64 и декодируем
82
- file_bytes = base64.b64decode(file_content_encoded)
83
- df = pd.read_csv(io.StringIO(file_bytes.decode("utf-8")), na_values=["Не выбрано"])
 
 
 
 
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
- dasha_count_total = dasha_count + dasha_count2
179
- lera_count_total = lera_count + lera_count2
180
- sveta_count_total = sveta_count + sveta_count2
 
 
 
 
181
 
182
- dasha_daily_total = pd.concat([dasha_daily, dasha_daily2], ignore_index=True)
183
- lera_daily_total = pd.concat([lera_daily, lera_daily2], ignore_index=True)
184
- sveta_daily_total = pd.concat([sveta_daily, sveta_daily2], ignore_index=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
 
186
  total_count = dasha_count_total + lera_count_total + sveta_count_total
187
  print(f"Суммарное количество (Д+Л+С): {total_count}")
188
 
189
- # Остальной код (прогресс-бары, объединение DataFrame, графики)
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
- # 4) Первый график: накопительное (все пользователи)
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
- # === ВАЖНО: сортируем легенду (user) по убыванию финального cumulative ===
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
- # 5) Делаем «Всего» для Prophet + средний прогноз
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
- # Prophet
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
- # Сшиваем все в один DataFrame
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
- # 6) Второй график: «История», «Прогноз (Prophet)», «Прогноз (среднее)» — пунктир
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
- # 7) Возвращаем результат
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 и %")