import gradio as gr
import pandas as pd
import plotly.express as px
from prophet import Prophet
# Ссылки на CSV-файлы
URL_DASHA = "https://raw.githubusercontent.com/fruitpicker01/Storage_Dasha_2025/main/messages.csv"
URL_LERA = "https://raw.githubusercontent.com/fruitpicker01/Storage_Lera_2025/main/messages.csv"
URL_SVETA = "https://raw.githubusercontent.com/fruitpicker01/Storage_Sveta_2025/main/messages.csv"
def read_and_process_data(url, user_name):
"""
Возвращает:
1) unique_count: количество уникальных SMS (по gender, generation, industry, opf)
2) df_daily: дата, пользователь, дневное кол-во уникальных SMS (НЕ накопленное),
уже после удаления дубликатов по 4 столбцам.
"""
df = pd.read_csv(url, na_values=["Не выбрано"])
cols = ["gender", "generation", "industry", "opf", "timestamp"]
df = df[[c for c in cols if c in df.columns]].copy()
# Убираем дубликаты по ключевым столбцам
df_unique = df.drop_duplicates(subset=["gender", "generation", "industry", "opf"])
# Количество уникальных SMS
unique_count = len(df_unique)
# Преобразуем timestamp -> date
if "timestamp" in df_unique.columns:
df_unique["timestamp"] = pd.to_numeric(df_unique["timestamp"], errors='coerce')
df_unique["date"] = pd.to_datetime(df_unique["timestamp"], unit="s", origin="unix", errors='coerce').dt.date
else:
df_unique["date"] = pd.NaT
# Сгруппируем по дате, чтобы получить кол-во за каждый день
df_daily = df_unique.groupby("date").size().reset_index(name="count")
df_daily["user"] = user_name
return unique_count, df_daily
def make_average_forecast(total_by_date, end_date_str="2025-02-28"):
"""
Строит «прогноз по среднему» до указанной даты (end_date_str),
считая средний дневной прирост (по всем дням, без исключения выходных).
Возвращает DataFrame с колонками ["ds", "yhat"],
начиная с (last_date+1) по end_date_str (включительно).
"""
if total_by_date.empty:
return pd.DataFrame(columns=["ds", "yhat"])
df_tmp = total_by_date.copy()
df_tmp["date"] = pd.to_datetime(df_tmp["date"])
# Средний дневной прирост (столбец "count") по всем дням
avg_inc = df_tmp["count"].mean() if len(df_tmp) else 0
last_date = df_tmp["date"].max()
last_cumulative = df_tmp["cumulative"].iloc[-1]
end_date = pd.to_datetime(end_date_str)
# Движемся по календарю день за днём
current_date = last_date
forecast_data = []
running_total = last_cumulative
while current_date < end_date:
current_date += pd.Timedelta(days=1)
if current_date > end_date:
break
# Прибавляем средний прирост независимо от выходного или буднего дня
running_total += avg_inc
forecast_data.append({"ds": current_date, "yhat": running_total})
return pd.DataFrame(forecast_data)
def process_data():
# Шаг 1: Считываем CSV по каждому репозиторию (Даша, Лера, Света)
dasha_count, dasha_daily = read_and_process_data(URL_DASHA, "Даша")
lera_count, lera_daily = read_and_process_data(URL_LERA, "Лера")
sveta_count, sveta_daily = read_and_process_data(URL_SVETA, "Света")
# Сумма
total_count = dasha_count + lera_count + sveta_count
# Подсчитываем проценты
dasha_percent = round((dasha_count / 234) * 100) if 234 else 0
lera_percent = round((lera_count / 234) * 100) if 234 else 0
sveta_percent = round((sveta_count / 234) * 100) if 234 else 0
total_percent = round((total_count / 702) * 100) if 702 else 0
# Генерируем HTML для прогресс-баров
def get_progress_bar(label, abs_val, pct):
capacity = 234 if label in ["Даша", "Лера", "Света"] else 702
return f"""
{label}
{abs_val} SMS ({pct}% из {capacity})
"""
bars_html = (
get_progress_bar("Даша", dasha_count, dasha_percent) +
get_progress_bar("Лера", lera_count, lera_percent) +
get_progress_bar("Света", sveta_count, sveta_percent) +
get_progress_bar("Всего", total_count, total_percent)
)
# Шаг 2: Готовим общий датафрейм по датам
daily_all = pd.concat([dasha_daily, lera_daily, sveta_daily], ignore_index=True)
daily_all = daily_all.dropna(subset=["date"]) # убираем NaT
# Считаем кумулятивное значение для каждого пользователя
daily_all = daily_all.sort_values(by=["user", "date"])
daily_all["cumulative"] = daily_all.groupby("user")["count"].cumsum()
# «Всего»
total_by_date = daily_all.groupby("date")["count"].sum().reset_index(name="count")
total_by_date = total_by_date.sort_values(by="date")
total_by_date["cumulative"] = total_by_date["count"].cumsum()
total_by_date["user"] = "Всего"
# Объединяем
daily_all_final = pd.concat([daily_all, total_by_date], ignore_index=True)
# Сортируем легенду: у кого итог больше, тот сверху
last_values = daily_all_final.groupby("user")["cumulative"].last().sort_values(ascending=False)
sorted_users = last_values.index.tolist()
color_map = {
"Даша": "#1f77b4",
"Лера": "#2ca02c",
"Света": "#d62728",
"Всего": "#9467bd"
}
# Строим накопительный график
fig = px.line(
daily_all_final,
x="date",
y="cumulative",
color="user",
title="Накопительное количество SMS",
labels={"date": "Дата", "cumulative": "Накопленное количество SMS", "user": "Редактор"},
category_orders={"user": sorted_users},
color_discrete_map=color_map
)
# Шаг 3: Два прогноза
forecast_fig = None
# Если есть данные "Всего", делаем прогноз
if not total_by_date.empty:
df_prophet = total_by_date[["date", "cumulative"]].copy()
df_prophet.columns = ["ds", "y"]
df_prophet["ds"] = pd.to_datetime(df_prophet["ds"])
# Прогноз Prophet
model = Prophet()
model.fit(df_prophet)
end_date = pd.to_datetime("2025-02-28")
last_date = df_prophet["ds"].max()
additional_days = (end_date - last_date).days
future = model.make_future_dataframe(periods=0) # если уже после
if additional_days > 0:
future = model.make_future_dataframe(periods=additional_days)
forecast = model.predict(future)
# Совмещаем
df_plot = pd.merge(
forecast[["ds", "yhat"]],
df_prophet[["ds", "y"]],
on="ds",
how="left"
)
df_history = df_plot.dropna(subset=["y"])
df_future = df_plot[df_plot["y"].isna()]
# Прогноз по среднему (без учёта выходных — т. е. на каждый календарный день)
df_avg = make_average_forecast(total_by_date, "2025-02-28")
# Общий график для сравнения
forecast_fig = px.line(
df_history,
x="ds",
y="y",
title="Прогноз до конца февраля 2025 (всего)",
labels={"ds": "Дата", "y": "Накопленное число SMS"}
)
# Prophet-пунктир
forecast_fig.add_scatter(
x=df_future["ds"],
y=df_future["yhat"],
mode="lines",
name="Прогноз (Prophet)",
line=dict(dash="dash", color="red")
)
# Средний-пунктир
if not df_avg.empty:
forecast_fig.add_scatter(
x=df_avg["ds"],
y=df_avg["yhat"],
mode="lines",
name="Прогноз (по среднему)",
line=dict(dash="dash", color="green")
)
forecast_fig.update_layout(showlegend=True)
# Возвращаем всё в Gradio
return (bars_html, fig, forecast_fig)
with gr.Blocks() as demo:
gr.Markdown("Количество сохраненных SMS (Даша, Лера, Света)
")
btn = gr.Button("Обновить данные и показать результат")
html_output = gr.HTML(label="Прогресс-бары: количество SMS и %")
plot_output = gr.Plot(label="Накопительный график (Даша, Лера, Света, Всего)")
forecast_output = gr.Plot(label="Прогноз до конца февраля 2025 (всего)")
btn.click(fn=process_data, outputs=[html_output, plot_output, forecast_output])
if __name__ == "__main__":
demo.launch()