diff --git a/app_stats/app_metrics.json b/app_stats/app_metrics.json new file mode 100644 index 0000000..07ce86a --- /dev/null +++ b/app_stats/app_metrics.json @@ -0,0 +1,236 @@ +{ + "generated_at": "2026-06-16T17:30:19", + "cur_year": 2026, + "prev_year": 2025, + "period_label": "с 1 января по 15 июня", + "range": { + "start": "2026-01-01", + "end": "2026-06-15" + }, + "metrics": [ + { + "key": "my_services", + "label": "Мои услуги", + "prev": 1459571, + "cur": 1769470, + "growth": 0.21232197680003234, + "is_new": false + }, + { + "key": "traffic", + "label": "Детализация трафика", + "prev": 1079736, + "cur": 1271563, + "growth": 0.17766102084213178, + "is_new": false + }, + { + "key": "payments", + "label": "Платежи", + "prev": 553808, + "cur": 730185, + "growth": 0.3184804119839367, + "is_new": false + }, + { + "key": "orders", + "label": "Заявки", + "prev": 635621, + "cur": 826255, + "growth": 0.2999177182629271, + "is_new": false + }, + { + "key": "loyalty", + "label": "Лояльность", + "prev": 464365, + "cur": 470969, + "growth": 0.014221571393192856, + "is_new": false + }, + { + "key": "pay", + "label": "Оплата", + "prev": 302510, + "cur": 337103, + "growth": 0.11435324452084229, + "is_new": false + }, + { + "key": "billing_detail", + "label": "Детали счета", + "prev": 358290, + "cur": 496915, + "growth": 0.3869072539004717, + "is_new": false + }, + { + "key": "viktorina", + "label": "Викторина KT Club", + "prev": 298475, + "cur": 213879, + "growth": -0.28342742273222216, + "is_new": false + }, + { + "key": "partners", + "label": "Акции партнеров", + "prev": 94639, + "cur": 197009, + "growth": 1.08168936696288, + "is_new": false + }, + { + "key": "tv_plus", + "label": "TV+", + "prev": 95647, + "cur": 64104, + "growth": -0.32978556567377965, + "is_new": false + }, + { + "key": "boosters", + "label": "Бустеры", + "prev": 53649, + "cur": 121065, + "growth": 1.2566124252082984, + "is_new": false + }, + { + "key": "roaming", + "label": "Роуминг", + "prev": 39200, + "cur": 22160, + "growth": -0.4346938775510204, + "is_new": false + }, + { + "key": "pereoform", + "label": "Переоформление", + "prev": 23537, + "cur": 34570, + "growth": 0.46875132769681777, + "is_new": false + }, + { + "key": "aitu_music", + "label": "Aitu Music", + "prev": 0, + "cur": 8651, + "growth": null, + "is_new": true + }, + { + "key": "online_booking", + "label": "Онлайн очередь", + "prev": 5421, + "cur": 22144, + "growth": 3.084855192768862, + "is_new": false + }, + { + "key": "my_docs", + "label": "Мои документы", + "prev": 0, + "cur": 53376, + "growth": null, + "is_new": true + }, + { + "key": "dz_statement", + "label": "Справка о ДЗ", + "prev": 0, + "cur": 132795, + "growth": null, + "is_new": true + }, + { + "key": "new_boosters_roaming_kcell", + "label": "Новая линейка бустеров и роумингов Кселл", + "prev": 0, + "cur": 28626, + "growth": null, + "is_new": true + }, + { + "key": "adsl", + "label": "ADSL отключение услуги", + "prev": 0, + "cur": 69, + "growth": null, + "is_new": true + }, + { + "key": "law_and_order", + "label": "Закон и порядок", + "prev": 0, + "cur": 1555, + "growth": null, + "is_new": true + }, + { + "key": "acs", + "label": "ACS", + "prev": 0, + "cur": 9154, + "growth": null, + "is_new": true + }, + { + "key": "kaspi_freedom_pay", + "label": "Прием платежей через Freedom и Kaspi", + "prev": 0, + "cur": 61481, + "growth": null, + "is_new": true + }, + { + "key": "csat", + "label": "CSAT", + "prev": 0, + "cur": 2486, + "growth": null, + "is_new": true + }, + { + "key": "multicustomer", + "label": "Мультикастомер", + "prev": 0, + "cur": 164, + "growth": null, + "is_new": true + }, + { + "key": "tv_plus_setup", + "label": "Настройка TV+", + "prev": 0, + "cur": 4545, + "growth": null, + "is_new": true + }, + { + "key": "static_ip", + "label": "Статический IP", + "prev": 0, + "cur": 108, + "growth": null, + "is_new": true + }, + { + "key": "turbo_button", + "label": "Turbo кнопка", + "prev": 0, + "cur": 4312, + "growth": null, + "is_new": true + }, + { + "key": "real_estate_docs", + "label": "Справка о недвижимости", + "prev": 0, + "cur": 78, + "growth": null, + "is_new": true + } + ] +} diff --git a/app_stats/index.html b/app_stats/index.html new file mode 100644 index 0000000..984935e --- /dev/null +++ b/app_stats/index.html @@ -0,0 +1,560 @@ + + + + + +Метрики МП — Казахтелеком 2026 + + + + + + + +
+
+ +

Метрики МП

+

Казахтелеком · 2026

+ + +

+
+
+ + +
+
+
+ +
+

📱 Метрики МП

+

Использование функций мобильного приложения

+
+
+
+ + ← KPI Dashboard +
+
+ +
+
Загрузка данных…
+ + +
+
+ + + + diff --git a/app_stats/Результаты по метрикам 202606.xlsx b/app_stats/Результаты по метрикам 202606.xlsx new file mode 100644 index 0000000..bafefe4 Binary files /dev/null and b/app_stats/Результаты по метрикам 202606.xlsx differ diff --git a/index.html b/index.html index 4d7af66..1984ebc 100644 --- a/index.html +++ b/index.html @@ -280,6 +280,7 @@ body { font-family: var(--font-base); background: var(--color-bg); color: var(--
+ 📱 Метрики МП
diff --git a/updater/run_update.bat b/updater/run_update.bat index 2ac40f1..5b2cc0e 100644 --- a/updater/run_update.bat +++ b/updater/run_update.bat @@ -13,8 +13,16 @@ if exist "venv\Scripts\python.exe" ( set "PYEXE=python" ) +REM 1) KPI dashboard (drb_iliyas_kpi_2026.csv) "%PYEXE%" "%~dp0update_kpi.py" -set "RC=%ERRORLEVEL%" +set "RC_KPI=%ERRORLEVEL%" -echo Exit code: %RC% +REM 2) Метрики МП (app_stats/app_metrics.json) +"%PYEXE%" "%~dp0update_app_metrics.py" +set "RC_APP=%ERRORLEVEL%" + +echo KPI exit code: %RC_KPI% App-metrics exit code: %RC_APP% + +REM Ненулевой код, если упал хотя бы один +set /a RC=%RC_KPI%+%RC_APP% exit /b %RC% diff --git a/updater/update_app_metrics.py b/updater/update_app_metrics.py new file mode 100644 index 0000000..ec5462a --- /dev/null +++ b/updater/update_app_metrics.py @@ -0,0 +1,236 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Ежедневное обновление статистики «Метрики МП» из Impala. + +Сравнивает использование функций мобильного приложения за текущий год +(с 1 января по ВЧЕРАШНИЙ день) с аналогичным периодом прошлого года. +Результат пишется в ../app_stats/app_metrics.json и пушится в ветку pages — +его читает страница app_stats/index.html. + +Подключение к Impala, конфиг и git-push переиспользуются из update_kpi.py. +""" + +import sys +import json +import datetime as dt +from pathlib import Path + +import update_kpi as base # общие функции: load_config, connect_impala, git_commit_push, ... + +log = base.log +REPO_DIR = base.REPO_DIR +OUT_PATH = REPO_DIR / "app_stats" / "app_metrics.json" +OUT_REL = "app_stats/app_metrics.json" + +# ─── Метрики: ключ в SQL → человекочитаемое название (в порядке SELECT) ─── +METRICS = [ + ("my_services", "Мои услуги"), + ("traffic", "Детализация трафика"), + ("payments", "Платежи"), + ("orders", "Заявки"), + ("loyalty", "Лояльность"), + ("pay", "Оплата"), + ("billing_detail", "Детали счета"), + ("viktorina", "Викторина KT Club"), + ("partners", "Акции партнеров"), + ("tv_plus", "TV+"), + ("boosters", "Бустеры"), + ("roaming", "Роуминг"), + ("pereoform", "Переоформление"), + ("aitu_music", "Aitu Music"), + ("online_booking", "Онлайн очередь"), + ("my_docs", "Мои документы"), + ("dz_statement", "Справка о ДЗ"), + ("new_boosters_roaming_kcell", "Новая линейка бустеров и роумингов Кселл"), + ("adsl", "ADSL отключение услуги"), + ("law_and_order", "Закон и порядок"), + ("acs", "ACS"), + ("kaspi_freedom_pay", "Прием платежей через Freedom и Kaspi"), + ("csat", "CSAT"), + ("multicustomer", "Мультикастомер"), + ("tv_plus_setup", "Настройка TV+"), + ("static_ip", "Статический IP"), + ("turbo_button", "Turbo кнопка"), + ("real_estate_docs", "Справка о недвижимости"), +] + +_MONTHS_RU = ["", "января", "февраля", "марта", "апреля", "мая", "июня", + "июля", "августа", "сентября", "октября", "ноября", "декабря"] + + +def date_range(today: dt.date | None = None): + """Возвращает (cur_year, prev_year, start_cur, end_cur, start_prev, end_prev) как date.""" + today = today or dt.date.today() + end_cur = today - dt.timedelta(days=1) # вчера + cur_year = today.year + prev_year = cur_year - 1 + start_cur = dt.date(cur_year, 1, 1) + start_prev = dt.date(prev_year, 1, 1) + # та же дата прошлого года; подстраховка на 29 февраля + try: + end_prev = dt.date(prev_year, end_cur.month, end_cur.day) + except ValueError: + end_prev = dt.date(prev_year, end_cur.month, 28) + return cur_year, prev_year, start_cur, end_cur, start_prev, end_prev + + +def build_sql(start_cur, end_cur, start_prev, end_prev) -> str: + return f""" +with t as ( + select round(report_period_id/100) as report_year, + count(case when event_type = 'OPENWSCREENMYSERVICES' then 1 end) as my_services, + count(case when event_type = 'OPENWINDOWDETALIZTION' then 1 end) as traffic, + count(case when event_type = 'OPENWINDOWPAYMENT' then 1 end) as payments, + count(case when event_type = 'OPENSCREENAPPEALS' then 1 end) as orders, + count(case when event_type in ('banner_auth', 'banner_unauth', 'loyalty_banner_slider_auth', 'loyalty_banner_slider_unauth', 'get_bonus_opened', 'bonus_opened','promo_partners_opened','company_promo_opened') then 1 end) as loyalty, + count(case when event_type = 'WINDOWPAYMENT' then 1 end) as pay, + count(case when event_type = 'OPENWINDOWBILLING' then 1 end) as billing_detail, + count(case when event_type = 'game_page' then 1 end) as viktorina, + count(case when event_type = 'promo_partners_opened' then 1 end) as partners, + count(case when event_type = 'OPENWINDOWTVPLUS' then 1 end) as tv_plus, + count(case when event_type = 'MOBCONNECTIONOPENWINDOWADDITIONALTRAFFIC' then 1 end) as boosters, + count(case when event_type = 'OPENWINDOWROAMING' then 1 end) as roaming, + count(case when event_type = 'reregistration_comm_start' then 1 end) as pereoform, + count(case when event_type = 'aitu_music_banner_clicked' then 1 end) as aitu_music, + count(case when event_type = 'ONLINE_BOOKING_SERVICES' then 1 end) as online_booking, + count(case when event_type in ('EMPTYLISTDOCS', 'HASLISTDOCS') then 1 end) as my_docs, + count(case when event_type = 'PDFSTATEMENT' then 1 end) as dz_statement, + count(case when event_type in ('booster_success_screen_kcell', 'ROAMINGPACKAGEMOBILEKCELL') then 1 end) as new_boosters_roaming_kcell, + count(case when event_type = 'law_and_order_service_clicked' then 1 end) as law_and_order, + count(case when event_type = 'ACS_DEVICE_SELECTION_OPEN' then 1 end) as acs, + count(case when event_type in ('PAYMENTWASSUCCESSFULFREEDOM', 'PAYWITHKASPI') then 1 end) as kaspi_freedom_pay, + count(case when event_type = 'csat_screen_sent' then 1 end) as csat, + count(case when event_type = 'multicustomer_completed_screen_viewed' then 1 end) as multicustomer, + count(case when event_type = 'tv_plus_setup_success_viewed' then 1 end) as tv_plus_setup, + count(case when event_type = 'static_ip_connect_success_viewed' then 1 end) as static_ip, + count(case when event_type = 'turbo_activation_success_viewed' then 1 end) as turbo_button, + count(case when event_type = 'real_estate_docs_screen_shown' then 1 end) as real_estate_docs + from drb.drb_iliyas_amplitude_metrics_full + where entry_date between '{start_cur:%Y-%m-%d}' and '{end_cur:%Y-%m-%d}' + or entry_date between '{start_prev:%Y-%m-%d}' and '{end_prev:%Y-%m-%d}' + group by 1 +) +, a as ( + select year(created_at) as report_year, count(order_id) as adsl + from telecomkz.telecomkz_retention_service_prod_tariff_change_validations + group by 1 +) +select t.report_year, my_services, traffic, payments, orders, loyalty, pay, billing_detail, viktorina, partners, tv_plus, boosters, roaming, pereoform, aitu_music, online_booking, my_docs, dz_statement, new_boosters_roaming_kcell, adsl, law_and_order, acs, kaspi_freedom_pay, csat, multicustomer, +tv_plus_setup, static_ip, turbo_button, real_estate_docs +from t +left join a on t.report_year = a.report_year +order by t.report_year +""".strip() + + +def fetch_by_year(conn, sql): + cur = conn.cursor() + log.info("Выполнение запроса метрик МП...") + cur.execute(sql) + rows = cur.fetchall() + names = [d[0].lower() for d in cur.description] + cur.close() + idx = {n: i for i, n in enumerate(names)} + by_year = {} + for r in rows: + year = int(round(float(r[idx["report_year"]]))) + by_year[year] = {name: r[idx[name]] for name in idx} + log.info("Получено годовых строк: %s", sorted(by_year)) + return by_year, idx + + +def _num(v): + return int(v) if v is not None else 0 + + +def build_payload(by_year, cur_year, prev_year, start_cur, end_cur): + cur_row = by_year.get(cur_year, {}) + prev_row = by_year.get(prev_year, {}) + + metrics = [] + for key, label in METRICS: + cur_v = _num(cur_row.get(key)) + prev_v = _num(prev_row.get(key)) + is_new = prev_v == 0 + growth = None if is_new else (cur_v - prev_v) / prev_v + metrics.append({ + "key": key, "label": label, + "prev": prev_v, "cur": cur_v, + "growth": growth, "is_new": is_new, + }) + + period_label = (f"с 1 января по {end_cur.day} {_MONTHS_RU[end_cur.month]}") + return { + "generated_at": dt.datetime.now().isoformat(timespec="seconds"), + "cur_year": cur_year, + "prev_year": prev_year, + "period_label": period_label, + "range": {"start": f"{start_cur:%Y-%m-%d}", "end": f"{end_cur:%Y-%m-%d}"}, + "metrics": metrics, + } + + +def write_if_changed(payload) -> bool: + text = json.dumps(payload, ensure_ascii=False, indent=2) + "\n" + old = "" + if OUT_PATH.exists(): + old = OUT_PATH.read_text(encoding="utf-8") + + # Сравниваем без учёта generated_at (чтобы не коммитить, если данные те же) + def strip_ts(s): + try: + d = json.loads(s) + d.pop("generated_at", None) + return json.dumps(d, ensure_ascii=False, sort_keys=True) + except Exception: + return s + + if strip_ts(old) == strip_ts(text): + log.info("Метрики МП не изменились — коммит не требуется.") + return False + + OUT_PATH.parent.mkdir(parents=True, exist_ok=True) + OUT_PATH.write_text(text, encoding="utf-8") + log.info("app_metrics.json обновлён (%d метрик).", len(payload["metrics"])) + return True + + +def main() -> int: + log.info("=" * 60) + log.info("Старт обновления Метрик МП") + cfg = base.load_config() + + cur_year, prev_year, start_cur, end_cur, start_prev, end_prev = date_range() + log.info("Период: %s..%s (тек.) и %s..%s (пред.)", + start_cur, end_cur, start_prev, end_prev) + sql = build_sql(start_cur, end_cur, start_prev, end_prev) + + base.patch_thrift_ssl() + conn = base.connect_impala(cfg) + try: + by_year, _ = fetch_by_year(conn, sql) + finally: + try: + conn.close() + except Exception: + pass + + if cur_year not in by_year: + log.error("В ответе нет данных за %s — JSON НЕ перезаписан.", cur_year) + return 1 + + payload = build_payload(by_year, cur_year, prev_year, start_cur, end_cur) + if write_if_changed(payload): + base.git_commit_push(cfg, [OUT_REL], + f"data: update app metrics {dt.date.today():%Y-%m-%d}") + log.info("Готово (Метрики МП).") + return 0 + + +if __name__ == "__main__": + try: + sys.exit(main()) + except Exception as e: # noqa: BLE001 + log.exception("ОШИБКА (Метрики МП): %s", e) + sys.exit(1) diff --git a/updater/update_kpi.py b/updater/update_kpi.py index ab468ed..4e06139 100644 --- a/updater/update_kpi.py +++ b/updater/update_kpi.py @@ -241,26 +241,27 @@ def push_url(cfg: dict, mask_token: str): return url -def commit_and_push(cfg: dict): +def git_commit_push(cfg: dict, rel_paths, message: str): + """Коммитит указанные файлы (пути относительно корня репо) и пушит в ветку. + + Переиспользуется и для KPI, и для метрик МП. Если изменений нет — выходит молча. + """ git = cfg.get("git", {}) or {} branch = git.get("branch", "pages") token = (git.get("token") or "") url = push_url(cfg, token) - # Коммитим только CSV — daily-диффы остаются чистыми. - _run_git(["add", "--", CSV_PATH.name]) + _run_git(["add", "--", *rel_paths]) - rc, out = _run_git(["status", "--porcelain", "--", CSV_PATH.name], check=True) + rc, out = _run_git(["status", "--porcelain", "--", *rel_paths], check=True) if not out.strip(): - log.info("Нет изменений CSV для коммита.") + log.info("Нет изменений (%s) для коммита.", ", ".join(rel_paths)) return - msg = f"data: update KPI {datetime.now():%Y-%m-%d}" - _run_git(["commit", "-m", msg]) + _run_git(["commit", "-m", message]) # Пуш с автоматическим rebase при гонке (веб-приложение тоже пушит ai-cache.json через API) for attempt in range(1, 4): - # подтянуть свежий remote и переставить наш коммит сверху _run_git(["fetch", url, branch], check=True, mask=token or None) # --autostash: не падать, если в дереве есть посторонние незакоммиченные правки _run_git(["rebase", "--autostash", "FETCH_HEAD"], check=True) @@ -272,6 +273,11 @@ def commit_and_push(cfg: dict): raise RuntimeError("Не удалось запушить изменения после 3 попыток.") +def commit_and_push(cfg: dict): + # Коммитим только CSV — daily-диффы остаются чистыми. + git_commit_push(cfg, [CSV_PATH.name], f"data: update KPI {datetime.now():%Y-%m-%d}") + + # ═══════════════════════ main ════════════════════════════════ def main() -> int: log.info("=" * 60)