{
 "nbformat": 4,
 "nbformat_minor": 5,
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python"
  },
  "colab": {
   "provenance": []
  }
 },
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "4kG_ZcR7gQtp"
   },
   "source": "# QAPU — Elektrik Analizörü Reaktif Analiz & Kestirimci Bakım\n### Offline Narrator ile 4 kitleye özel rapor üretimi (Google Colab)\n\nBu not defteri `qapu.dump` (PostgreSQL 18 custom format) dosyasını **pg_restore gerektirmeden**\ndoğrudan ayrıştırır ve:\n\n1. **Reaktif enerji uygunluk analizi** — EPDK sınırları (endüktif %20 / kapasitif %15), güç faktörü\n2. **Kestirimci bakım** — harmonik (THD), faz dengesizliği, sıcaklık, trend projeksiyonu,\n   IsolationForest ile anomali tespiti, cihaz başına **sağlık skoru (0–100)**\n3. **Offline narrator** — internet/LLM kullanmadan, şablon tabanlı Türkçe raporlar:\n   * 👷 Teknik uzman &nbsp;·&nbsp; 💼 Özel sektör yöneticisi &nbsp;·&nbsp; 🏛️ Kamu yöneticisi &nbsp;·&nbsp; 👤 Vatandaş\n   * Çıktı formatları: **Word (.docx)**, HTML ve Markdown — grafikler gömülü\n\n**Kullanım:** Hücreleri sırayla çalıştırın. 6. hücre dump dosyasını yüklemenizi ister\n(veya `DUMP_PATH` değişkenine Drive yolunu yazın). Çıktılar `raporlar/` klasörüne ve\nindirilebilir bir ZIP'e yazılır.\n\n> Rapor üretimi tamamen deterministiktir; aynı veri her zaman aynı raporu üretir.\n",
   "id": "4kG_ZcR7gQtp"
  },
  {
   "cell_type": "code",
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/"
    },
    "id": "nd-rraTQgQts",
    "executionInfo": {
     "status": "ok",
     "timestamp": 1793476105743,
     "user_tz": -180,
     "elapsed": 5398,
     "user": {
      "displayName": "Joseph Volkan ÖZCAN",
      "userId": "11389530967736397480"
     }
    },
    "outputId": "ed752c0e-6b78-4691-957b-3bc155a278df"
   },
   "execution_count": 1,
   "outputs": [
    {
     "output_type": "stream",
     "name": "stdout",
     "text": [
      "Hazır.\n"
     ]
    }
   ],
   "source": "# 1) Kurulum — dump ayrıştırma (zstd), HTML ve Word (.docx) çıktıları için\n!pip -q install zstandard markdown python-docx\nprint(\"Hazır.\")",
   "id": "nd-rraTQgQts"
  },
  {
   "cell_type": "code",
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/"
    },
    "id": "GdrG0yRegQtt",
    "executionInfo": {
     "status": "ok",
     "timestamp": 1793476105755,
     "user_tz": -180,
     "elapsed": 13,
     "user": {
      "displayName": "Joseph Volkan ÖZCAN",
      "userId": "11389530967736397480"
     }
    },
    "outputId": "7d48e2d7-f388-43a0-bd5b-3c260a06b2af"
   },
   "execution_count": 2,
   "outputs": [
    {
     "output_type": "stream",
     "name": "stdout",
     "text": [
      "Writing qapu_extract.py\n"
     ]
    }
   ],
   "source": "%%writefile qapu_extract.py\n#!/usr/bin/env python3\n\"\"\"Extract selected tables from a PostgreSQL custom-format dump (v1.16, zstd) to CSV.\n\nWorks without pg_restore: parses the TOC and decompresses data blocks directly.\nUsage: python3 extract_tables.py <dump> <outdir>\n\"\"\"\nimport sys, os, io, re, csv, json\nimport zstandard\n\nTARGET_PREFIXES = (\n    'measurements_energy', 'measurements_power', 'measurements_voltage',\n    'measurements_current', 'measurements_device', 'measurements_environment',\n)\nTARGET_EXACT = {'devices', 'streams', 'variables', 'variable_segments', 'sites',\n                'site_device_assignments', 'manufacturer_models'}\n\n\ndef parse_dump(path):\n    f = open(path, 'rb')\n    magic = f.read(5)\n    assert magic == b'PGDMP', 'Not a pg_dump custom archive'\n    vmaj, vmin, vrev = f.read(1)[0], f.read(1)[0], f.read(1)[0]\n    intSize = f.read(1)[0]\n    offSize = f.read(1)[0]\n    fmt = f.read(1)[0]\n\n    def read_int():\n        sign = f.read(1)[0]\n        v = int.from_bytes(f.read(intSize), 'little')\n        return -v if sign else v\n\n    def read_str():\n        n = read_int()\n        if n < 0:\n            return None\n        return f.read(n).decode('utf-8', 'replace')\n\n    compression = f.read(1)[0]  # v>=1.15: 0 none, 1 gzip, 2 lz4, 3 zstd\n    for _ in range(7):\n        read_int()  # created date parts\n    dbname = read_str(); read_str(); read_str()\n\n    toc_count = read_int()\n    entries = []\n    for _ in range(toc_count):\n        e = {}\n        e['dumpId'] = read_int(); e['hadDumper'] = read_int()\n        e['tableoid'] = read_str(); e['oid'] = read_str()\n        e['tag'] = read_str(); e['desc'] = read_str()\n        e['section'] = read_int()\n        e['defn'] = read_str(); e['dropStmt'] = read_str(); e['copyStmt'] = read_str()\n        e['namespace'] = read_str(); e['tablespace'] = read_str(); e['tableam'] = read_str()\n        e['relkind'] = read_int()\n        e['owner'] = read_str(); e['withOids'] = read_str()\n        deps = []\n        while True:\n            d = read_str()\n            if d is None:\n                break\n            deps.append(d)\n        e['deps'] = deps\n        f.read(1 + offSize)  # offset flag + offset\n        entries.append(e)\n    return f, f.tell(), entries, compression, read_int\n\n\ndef columns_from_copy(copy_stmt):\n    m = re.search(r'\\(([^)]*)\\)', copy_stmt or '')\n    if not m:\n        return None\n    return [c.strip().strip('\"') for c in m.group(1).split(',')]\n\n\ndef main(dump_path, outdir):\n    os.makedirs(outdir, exist_ok=True)\n    f, data_start, entries, compression, read_int = parse_dump(dump_path)\n    byid = {e['dumpId']: e for e in entries}\n\n    def wanted(tag):\n        return tag in TARGET_EXACT or any(tag.startswith(p) for p in TARGET_PREFIXES)\n\n    targets = {e['dumpId']: e for e in entries\n               if e['desc'] == 'TABLE DATA' and wanted(e['tag'])}\n    print(f'{len(targets)} table-data entries to extract')\n\n    f.seek(data_start)\n    writers = {}  # base table name -> (file, header_written)\n    found = 0\n    while True:\n        b = f.read(1)\n        if not b:\n            break\n        blktype = b[0]\n        if blktype not in (1, 3, 49, 51):\n            break\n        did = read_int()\n        e = targets.get(did)\n        grab = e is not None\n        if blktype in (3, 51):  # blobs\n            while True:\n                oid = read_int()\n                if oid == 0:\n                    break\n                while True:\n                    n = read_int()\n                    if n <= 0:\n                        break\n                    f.seek(n, 1)\n            continue\n        dobj = zstandard.ZstdDecompressor().decompressobj() if grab else None\n        chunks = []\n        while True:\n            n = read_int()\n            if n <= 0:\n                break\n            buf = f.read(n)\n            if grab:\n                chunks.append(dobj.decompress(buf))\n        if not grab:\n            continue\n        found += 1\n        data = b''.join(chunks).decode('utf-8', 'replace')\n        tag = e['tag']\n        # fold partitions into base table name\n        base = re.sub(r'_(\\d{4}_\\d{2}|default)$', '', tag)\n        cols = columns_from_copy(e['copyStmt'])\n        path = os.path.join(outdir, base + '.csv')\n        new = base not in writers\n        out = open(path, 'a', newline='')\n        w = csv.writer(out)\n        if new and cols:\n            w.writerow(cols)\n            writers[base] = True\n        for line in data.split('\\n'):\n            if not line or line == '\\\\.':\n                continue\n            row = [None if v == '\\\\N' else v for v in line.split('\\t')]\n            w.writerow(row)\n        out.close()\n    print(f'extracted {found} data blocks into {len(writers)} CSVs: {sorted(writers)}')\n\n\nif __name__ == '__main__':\n    main(sys.argv[1], sys.argv[2])\n",
   "id": "GdrG0yRegQtt"
  },
  {
   "cell_type": "code",
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/"
    },
    "id": "2c8VNAo9gQtu",
    "executionInfo": {
     "status": "ok",
     "timestamp": 1793476105806,
     "user_tz": -180,
     "elapsed": 50,
     "user": {
      "displayName": "Joseph Volkan ÖZCAN",
      "userId": "11389530967736397480"
     }
    },
    "outputId": "35876682-765e-47c6-e4be-0fa9395b6f5b"
   },
   "execution_count": 3,
   "outputs": [
    {
     "output_type": "stream",
     "name": "stdout",
     "text": [
      "Writing qapu_pm.py\n"
     ]
    }
   ],
   "source": "%%writefile qapu_pm.py\n# -*- coding: utf-8 -*-\n\"\"\"\nQAPU Elektrik Analizörü — Reaktif Analiz + Kestirimci Bakım + Offline Narrator\n==============================================================================\nEnerji analizörü (MAX78630) verilerinden:\n  1. Reaktif enerji uygunluk analizi (EPDK endüktif %20 / kapasitif %15 sınırları)\n  2. Kestirimci bakım: güç faktörü, harmonik (THD), faz dengesizliği, trend,\n     anomali tespiti (IsolationForest), cihaz sağlık skoru (0-100)\n  3. Offline narrator: 4 hedef kitle için şablon tabanlı Türkçe rapor\n     (teknik uzman, özel sektör yöneticisi, kamu yöneticisi, vatandaş)\nİnternet/LLM gerektirmez; tamamen deterministik şablonlarla çalışır.\n\"\"\"\nimport os, math, json\nimport numpy as np\nimport pandas as pd\n\n# ----------------------------------------------------------------------------\n# KONFİGÜRASYON — eşikler ve standart referansları\n# ----------------------------------------------------------------------------\nCONFIG = {\n    \"ind_limit\": 0.20,      # EPDK: endüktif reaktif / aktif enerji aylık sınırı\n    \"cap_limit\": 0.15,      # EPDK: kapasitif reaktif / aktif enerji aylık sınırı\n    \"pf_target\": 0.95,      # hedef güç faktörü (yük altında)\n    \"thd_v_limit\": 0.08,    # IEEE 519 esinli gerilim THD sınırı (%8)\n    \"thd_i_limit\": 0.15,    # akım THD uyarı eşiği (%15)\n    \"v_imb_limit\": 0.02,    # gerilim dengesizliği sınırı (%2, IEC/NEMA)\n    \"i_imb_limit\": 0.10,    # akım dengesizliği uyarı eşiği (%10)\n    \"pcb_t_limit\": 60.0,    # elektronik sıcaklık uyarı eşiği (°C)\n    \"load_frac\": 0.05,      # \"yük altında\" kabulü: S > p95(S) * 0.05\n    \"min_days_trend\": 7,    # trend için asgari gün\n    \"min_days_iforest\": 10, # anomali modeli için asgari gün\n    \"anomaly_contamination\": 0.08,\n    \"vrms_plausible\": (120.0, 300.0),  # kalibrasyon kontrolü (faz-nötr V)\n}\n\nRISK_BANDS = [(85, \"İYİ\"), (70, \"İZLENMELİ\"), (50, \"RİSKLİ\"), (-1, \"KRİTİK\")]\n\n\ndef _risk(score):\n    for th, name in RISK_BANDS:\n        if score >= th:\n            return name\n    return \"KRİTİK\"\n\n\n# ----------------------------------------------------------------------------\n# 1. VERİ YÜKLEME / HAZIRLIK\n# ----------------------------------------------------------------------------\ndef load_csvs(data_dir):\n    \"\"\"extract_tables ile üretilen CSV'leri okur; tablolar sözlüğü döner.\"\"\"\n    tabs = {}\n    for name in [\"measurements_energy\", \"measurements_power\", \"measurements_voltage\",\n                 \"measurements_current\", \"measurements_device\",\n                 \"measurements_environment\", \"devices\"]:\n        p = os.path.join(data_dir, name + \".csv\")\n        if os.path.exists(p):\n            df = pd.read_csv(p, low_memory=False)\n            if \"device_time\" in df.columns:\n                df[\"device_time\"] = pd.to_datetime(df[\"device_time\"], utc=True, errors=\"coerce\")\n                df = df.dropna(subset=[\"device_time\"])\n                if \"is_valid\" in df.columns:\n                    df = df[df[\"is_valid\"].astype(str).str.lower().isin([\"t\", \"true\", \"1\"])]\n            tabs[name] = df\n    return tabs\n\n\ndef _thd(df, prefix, phases=(\"r\", \"s\", \"t\"), harms=(3, 5, 7, 9)):\n    \"\"\"sqrt(Σ harmonik²)/temel — faz başına; fazların ortalaması.\"\"\"\n    vals = []\n    for ph in phases:\n        fund = df.get(f\"{prefix}fund_{ph}\")\n        if fund is None:\n            continue\n        h2 = sum(df.get(f\"{prefix}harm_{ph}_{h}\", pd.Series(np.nan, index=df.index)) ** 2\n                 for h in harms)\n        thd = np.sqrt(h2) / fund.where(fund > 1e-6)\n        vals.append(thd)\n    if not vals:\n        return pd.Series(np.nan, index=df.index)\n    return pd.concat(vals, axis=1).mean(axis=1)\n\n\ndef _imbalance(df, cols):\n    \"\"\"Üç faz dengesizliği: max |x_i - ort| / ort (ort anlamlıysa).\"\"\"\n    sub = df[list(cols)]\n    mean = sub.mean(axis=1)\n    dev = sub.sub(mean, axis=0).abs().max(axis=1)\n    return (dev / mean.where(mean > 1e-6)).rename(\"imb\")\n\n\ndef build_daily(tabs, cfg=CONFIG):\n    \"\"\"Cihaz × gün bazında özellik matrisi üretir.\"\"\"\n    frames = []\n\n    e = tabs.get(\"measurements_energy\")\n    if e is not None and len(e):\n        e = e.copy()\n        for c in [\"ae_r\", \"ae_s\", \"ae_t\", \"re_l_r\", \"re_l_s\", \"re_l_t\",\n                  \"re_g_r\", \"re_g_s\", \"re_g_t\"]:\n            if c in e:\n                e[c] = pd.to_numeric(e[c], errors=\"coerce\").clip(lower=0)  # negatifler geçersiz\n        e[\"ae\"] = e[[\"ae_r\", \"ae_s\", \"ae_t\"]].sum(axis=1, min_count=1)\n        e[\"re_l\"] = e[[\"re_l_r\", \"re_l_s\", \"re_l_t\"]].sum(axis=1, min_count=1)\n        e[\"re_g\"] = e[[\"re_g_r\", \"re_g_s\", \"re_g_t\"]].sum(axis=1, min_count=1)\n        g = (e.set_index(\"device_time\").groupby(\"device_id\")\n               .resample(\"1D\")[[\"ae\", \"re_l\", \"re_g\"]].sum(min_count=1))\n        frames.append(g)\n\n    p = tabs.get(\"measurements_power\")\n    if p is not None and len(p):\n        p = p.copy()\n        for c in p.columns:\n            if c not in (\"device_id\", \"device_time\", \"is_valid\"):\n                p[c] = pd.to_numeric(p[c], errors=\"coerce\")\n        p[\"s_tot\"] = p[[\"s_r\", \"s_s\", \"s_t\"]].abs().sum(axis=1, min_count=1)\n        p[\"pf_abs\"] = p[[\"pf_r\", \"pf_s\", \"pf_t\"]].abs().mean(axis=1)\n        # yük eşiği: cihaz bazında p95'in %5'i\n        thr = p.groupby(\"device_id\")[\"s_tot\"].transform(lambda s: s.quantile(0.95) * cfg[\"load_frac\"])\n        p[\"loaded\"] = p[\"s_tot\"] > thr\n        p[\"pf_loaded\"] = p[\"pf_abs\"].where(p[\"loaded\"])\n        p[\"fq_dev\"] = (p[\"fq\"] - 50.0).abs().where(p[\"fq\"].between(40, 70))\n        g = (p.set_index(\"device_time\").groupby(\"device_id\")\n               .resample(\"1D\").agg(pf_loaded=(\"pf_loaded\", \"mean\"),\n                                   load_hours=(\"loaded\", \"sum\"),\n                                   s_max=(\"s_tot\", \"max\"),\n                                   fq_dev=(\"fq_dev\", \"mean\")))\n        g[\"load_hours\"] *= 5 / 60.0  # ~5 dk örnekleme → saat\n        frames.append(g)\n\n    v = tabs.get(\"measurements_voltage\")\n    if v is not None and len(v):\n        v = v.copy()\n        for c in v.columns:\n            if c not in (\"device_id\", \"device_time\", \"is_valid\"):\n                v[c] = pd.to_numeric(v[c], errors=\"coerce\")\n        v[\"thd_v\"] = _thd(v, \"v\")\n        v[\"v_imb\"] = _imbalance(v, [\"vrms_r\", \"vrms_s\", \"vrms_t\"])\n        v[\"vrms_avg\"] = v[[\"vrms_r\", \"vrms_s\", \"vrms_t\"]].mean(axis=1)\n        g = (v.set_index(\"device_time\").groupby(\"device_id\")\n               .resample(\"1D\").agg(thd_v=(\"thd_v\", \"median\"),\n                                   v_imb=(\"v_imb\", \"median\"),\n                                   vrms_med=(\"vrms_avg\", \"median\")))\n        frames.append(g)\n\n    c = tabs.get(\"measurements_current\")\n    if c is not None and len(c):\n        c = c.copy()\n        for col in c.columns:\n            if col not in (\"device_id\", \"device_time\", \"is_valid\"):\n                c[col] = pd.to_numeric(c[col], errors=\"coerce\")\n        c[\"thd_i\"] = _thd(c, \"i\")\n        c[\"i_imb\"] = _imbalance(c, [\"irms_r\", \"irms_s\", \"irms_t\"])\n        c[\"irms_avg\"] = c[[\"irms_r\", \"irms_s\", \"irms_t\"]].mean(axis=1)\n        # yüksüzken dengesizlik anlamsız → medyan akımın %10'u altını at\n        thr = c.groupby(\"device_id\")[\"irms_avg\"].transform(lambda s: max(s.quantile(0.9) * 0.1, 0.2))\n        c.loc[c[\"irms_avg\"] < thr, [\"i_imb\", \"thd_i\"]] = np.nan\n        g = (c.set_index(\"device_time\").groupby(\"device_id\")\n               .resample(\"1D\").agg(thd_i=(\"thd_i\", \"median\"),\n                                   i_imb=(\"i_imb\", \"median\"),\n                                   irms_max=(\"irms_avg\", \"max\")))\n        frames.append(g)\n\n    env = tabs.get(\"measurements_environment\")\n    if env is not None and len(env):\n        env = env.copy()\n        env[\"pcb_t\"] = pd.to_numeric(env.get(\"pcb_t\"), errors=\"coerce\")\n        g = (env.set_index(\"device_time\").groupby(\"device_id\")\n               .resample(\"1D\").agg(pcb_t_max=(\"pcb_t\", \"max\")))\n        frames.append(g)\n\n    d = tabs.get(\"measurements_device\")\n    if d is not None and len(d):\n        d = d.copy()\n        for col in [\"rssi\", \"b_soc\"]:\n            if col in d:\n                d[col] = pd.to_numeric(d[col], errors=\"coerce\")\n        g = (d.set_index(\"device_time\").groupby(\"device_id\")\n               .resample(\"1D\").agg(rssi=(\"rssi\", \"median\"),\n                                   b_soc=(\"b_soc\", \"median\")))\n        frames.append(g)\n\n    if not frames:\n        raise ValueError(\"Hiç ölçüm tablosu bulunamadı\")\n    daily = pd.concat(frames, axis=1)\n    daily = daily.dropna(how=\"all\")\n    daily[\"ind_ratio\"] = daily[\"re_l\"] / daily[\"ae\"].where(daily[\"ae\"] > 0)\n    daily[\"cap_ratio\"] = daily[\"re_g\"] / daily[\"ae\"].where(daily[\"ae\"] > 0)\n    return daily.reset_index()\n\n\n# ----------------------------------------------------------------------------\n# 2. ANALİZ — cihaz özetleri, trendler, anomaliler, sağlık skoru\n# ----------------------------------------------------------------------------\ndef _slope_per_day(sub, col):\n    s = sub[[\"day_idx\", col]].dropna()\n    if len(s) < CONFIG[\"min_days_trend\"]:\n        return np.nan\n    x, y = s[\"day_idx\"].values.astype(float), s[col].values.astype(float)\n    if np.ptp(x) < 1:\n        return np.nan\n    return float(np.polyfit(x, y, 1)[0])\n\n\ndef _days_to_limit(current, slope, limit, rising=True):\n    \"\"\"Mevcut değer + eğimle sınıra kaç günde ulaşılır (365 ile sınırlı).\"\"\"\n    if not np.isfinite(current) or not np.isfinite(slope):\n        return None\n    if rising:\n        if current >= limit:\n            return 0\n        if slope <= 1e-12:\n            return None\n        d = (limit - current) / slope\n    else:\n        if current <= limit:\n            return 0\n        if slope >= -1e-12:\n            return None\n        d = (limit - current) / slope\n    return int(d) if 0 <= d <= 365 else None\n\n\ndef analyze(daily, devices_df=None, cfg=CONFIG):\n    \"\"\"Cihaz başına özet, bulgular ve sağlık skoru üretir.\"\"\"\n    try:\n        from sklearn.ensemble import IsolationForest\n        HAS_SK = True\n    except Exception:\n        HAS_SK = False\n\n    names = {}\n    if devices_df is not None:\n        names = (devices_df.dropna(subset=[\"id\"]).set_index(\"id\")[\"name\"]\n                 .fillna(\"\").to_dict())\n\n    results = {}\n    feat_cols = [\"ind_ratio\", \"cap_ratio\", \"pf_loaded\", \"thd_v\", \"thd_i\",\n                 \"v_imb\", \"i_imb\", \"pcb_t_max\", \"fq_dev\"]\n\n    for dev, sub in daily.groupby(\"device_id\"):\n        sub = sub.sort_values(\"device_time\").copy()\n        sub[\"day_idx\"] = (sub[\"device_time\"] - sub[\"device_time\"].min()).dt.days\n        n_days = int(sub[\"ae\"].notna().sum()) if \"ae\" in sub else len(sub)\n        ae_tot = float(np.nansum(sub.get(\"ae\", np.nan)))\n        if n_days < 3 or ae_tot <= 0:\n            continue  # analiz için yetersiz veri\n\n        r = {\"device_id\": dev,\n             \"name\": names.get(dev, \"\") or dev,\n             \"days\": int(len(sub)),\n             \"first\": str(sub[\"device_time\"].min().date()),\n             \"last\": str(sub[\"device_time\"].max().date()),\n             \"ae_total_kwh\": ae_tot / 1000.0,\n             \"re_l_total_kvarh\": float(np.nansum(sub.get(\"re_l\", np.nan))) / 1000.0,\n             \"re_g_total_kvarh\": float(np.nansum(sub.get(\"re_g\", np.nan))) / 1000.0}\n        r[\"ind_ratio\"] = r[\"re_l_total_kvarh\"] / r[\"ae_total_kwh\"] if r[\"ae_total_kwh\"] > 0 else np.nan\n        r[\"cap_ratio\"] = r[\"re_g_total_kvarh\"] / r[\"ae_total_kwh\"] if r[\"ae_total_kwh\"] > 0 else np.nan\n\n        # son 30 gün oranı (güncel durum)\n        last30 = sub[sub[\"device_time\"] >= sub[\"device_time\"].max() - pd.Timedelta(days=30)]\n        ae30 = np.nansum(last30.get(\"ae\", np.nan))\n        r[\"ind_ratio_30g\"] = float(np.nansum(last30.get(\"re_l\", np.nan)) / ae30) if ae30 > 0 else np.nan\n        r[\"cap_ratio_30g\"] = float(np.nansum(last30.get(\"re_g\", np.nan)) / ae30) if ae30 > 0 else np.nan\n\n        for col in [\"pf_loaded\", \"thd_v\", \"thd_i\", \"v_imb\", \"i_imb\", \"pcb_t_max\", \"rssi\"]:\n            r[col] = float(np.nanmedian(sub[col])) if col in sub and sub[col].notna().any() else np.nan\n\n        vrms = float(np.nanmedian(sub[\"vrms_med\"])) if \"vrms_med\" in sub and sub[\"vrms_med\"].notna().any() else np.nan\n        lo, hi = cfg[\"vrms_plausible\"]\n        r[\"calibrated\"] = bool(np.isfinite(vrms) and lo <= vrms <= hi)\n        r[\"vrms_med\"] = vrms\n\n        # trendler (birim/gün)\n        r[\"trend\"] = {c: _slope_per_day(sub, c) for c in\n                      [\"pf_loaded\", \"thd_i\", \"i_imb\", \"ind_ratio\", \"pcb_t_max\"]}\n        # sınır projeksiyonları\n        r[\"proj\"] = {\n            \"ind_ratio\": _days_to_limit(r[\"ind_ratio_30g\"], r[\"trend\"][\"ind_ratio\"], cfg[\"ind_limit\"], rising=True),\n            \"thd_i\": _days_to_limit(r[\"thd_i\"], r[\"trend\"][\"thd_i\"], cfg[\"thd_i_limit\"], rising=True),\n            \"i_imb\": _days_to_limit(r[\"i_imb\"], r[\"trend\"][\"i_imb\"], cfg[\"i_imb_limit\"], rising=True),\n            \"pf_loaded\": _days_to_limit(r[\"pf_loaded\"], r[\"trend\"][\"pf_loaded\"], cfg[\"pf_target\"], rising=False),\n        }\n\n        # anomali tespiti (IsolationForest, gün bazında)\n        r[\"anomaly_ratio\"] = np.nan\n        r[\"anomaly_days\"] = []\n        X = sub[[c for c in feat_cols if c in sub]].copy()\n        X = X.loc[:, X.notna().mean() > 0.5]\n        X = X.dropna()\n        if HAS_SK and len(X) >= cfg[\"min_days_iforest\"] and X.shape[1] >= 3:\n            iso = IsolationForest(contamination=cfg[\"anomaly_contamination\"], random_state=42)\n            lab = iso.fit_predict((X - X.mean()) / X.std(ddof=0).replace(0, 1))\n            anom_idx = X.index[lab == -1]\n            r[\"anomaly_ratio\"] = float(len(anom_idx) / len(X))\n            r[\"anomaly_days\"] = [str(d.date()) for d in\n                                 sub.loc[anom_idx, \"device_time\"].sort_values().tail(5)]\n\n        r[\"findings\"] = _findings(r, cfg)\n        r[\"score\"] = _score(r, cfg)\n        r[\"risk\"] = _risk(r[\"score\"])\n        results[dev] = r\n\n    return results\n\n\ndef _findings(r, cfg):\n    F = []\n\n    def add(code, severity, value, limit, **kw):\n        F.append(dict(code=code, severity=severity, value=value, limit=limit, **kw))\n\n    ir = r.get(\"ind_ratio_30g\")\n    if np.isfinite(ir or np.nan):\n        if ir > cfg[\"ind_limit\"]:\n            add(\"REAKTIF_IND_ASIM\", \"yüksek\" if ir > 1.5 * cfg[\"ind_limit\"] else \"orta\",\n                ir, cfg[\"ind_limit\"])\n        elif ir > 0.8 * cfg[\"ind_limit\"]:\n            add(\"REAKTIF_IND_YAKIN\", \"düşük\", ir, cfg[\"ind_limit\"])\n    cr = r.get(\"cap_ratio_30g\")\n    if np.isfinite(cr or np.nan):\n        if cr > cfg[\"cap_limit\"]:\n            add(\"REAKTIF_CAP_ASIM\", \"yüksek\" if cr > 1.5 * cfg[\"cap_limit\"] else \"orta\",\n                cr, cfg[\"cap_limit\"])\n        elif cr > 0.8 * cfg[\"cap_limit\"]:\n            add(\"REAKTIF_CAP_YAKIN\", \"düşük\", cr, cfg[\"cap_limit\"])\n\n    pf = r.get(\"pf_loaded\")\n    if np.isfinite(pf or np.nan) and pf < cfg[\"pf_target\"]:\n        add(\"PF_DUSUK\", \"yüksek\" if pf < 0.85 else \"orta\", pf, cfg[\"pf_target\"])\n\n    for key, code in [(\"thd_i\", \"THD_AKIM\"), (\"thd_v\", \"THD_GERILIM\")]:\n        val = r.get(key)\n        lim = cfg[key + \"_limit\"]\n        if np.isfinite(val or np.nan) and val > lim:\n            add(code, \"yüksek\" if val > 2 * lim else \"orta\", val, lim)\n\n    for key, code, lim in [(\"i_imb\", \"AKIM_DENGESIZ\", cfg[\"i_imb_limit\"]),\n                           (\"v_imb\", \"GERILIM_DENGESIZ\", cfg[\"v_imb_limit\"])]:\n        val = r.get(key)\n        if r.get(\"calibrated\") is False and code == \"GERILIM_DENGESIZ\":\n            pass  # dengesizlik oransal, kalibrasyonsuz da anlamlı — yine de raporla\n        if np.isfinite(val or np.nan) and val > lim:\n            add(code, \"yüksek\" if val > 2 * lim else \"orta\", val, lim)\n\n    t = r.get(\"pcb_t_max\")\n    if np.isfinite(t or np.nan) and t > cfg[\"pcb_t_limit\"]:\n        add(\"SICAKLIK\", \"orta\", t, cfg[\"pcb_t_limit\"])\n\n    ar = r.get(\"anomaly_ratio\")\n    if np.isfinite(ar or np.nan) and ar > 0.12:\n        add(\"ANOMALI\", \"orta\", ar, 0.12, days=r.get(\"anomaly_days\", []))\n\n    for key, code in [(\"ind_ratio\", \"TREND_REAKTIF\"), (\"thd_i\", \"TREND_THD\"),\n                      (\"i_imb\", \"TREND_DENGESIZ\")]:\n        d = r[\"proj\"].get(key if key != \"ind_ratio\" else \"ind_ratio\")\n        if d is not None and 0 < d <= 90:\n            add(code, \"orta\" if d > 30 else \"yüksek\", d, 90, metric=key)\n    dpf = r[\"proj\"].get(\"pf_loaded\")\n    if dpf is not None and 0 < dpf <= 90:\n        add(\"TREND_PF\", \"orta\" if dpf > 30 else \"yüksek\", dpf, 90, metric=\"pf_loaded\")\n\n    if not r.get(\"calibrated\", True):\n        add(\"KALIBRASYON\", \"bilgi\", r.get(\"vrms_med\"), None)\n    if r[\"days\"] < 14:\n        add(\"VERI_AZ\", \"bilgi\", r[\"days\"], 14)\n    return F\n\n\ndef _score(r, cfg):\n    s = 100.0\n    ir, cr = r.get(\"ind_ratio_30g\"), r.get(\"cap_ratio_30g\")\n    if np.isfinite(ir or np.nan) and ir > cfg[\"ind_limit\"]:\n        s -= min(25, 25 * (ir - cfg[\"ind_limit\"]) / cfg[\"ind_limit\"] + 10)\n    if np.isfinite(cr or np.nan) and cr > cfg[\"cap_limit\"]:\n        s -= min(15, 15 * (cr - cfg[\"cap_limit\"]) / cfg[\"cap_limit\"] + 6)\n    pf = r.get(\"pf_loaded\")\n    if np.isfinite(pf or np.nan) and pf < cfg[\"pf_target\"]:\n        s -= min(15, (cfg[\"pf_target\"] - pf) * 100)\n    if np.isfinite(r.get(\"thd_i\") or np.nan) and r[\"thd_i\"] > cfg[\"thd_i_limit\"]:\n        s -= min(10, 50 * (r[\"thd_i\"] - cfg[\"thd_i_limit\"]))\n    if np.isfinite(r.get(\"thd_v\") or np.nan) and r[\"thd_v\"] > cfg[\"thd_v_limit\"]:\n        s -= min(10, 100 * (r[\"thd_v\"] - cfg[\"thd_v_limit\"]))\n    if np.isfinite(r.get(\"i_imb\") or np.nan) and r[\"i_imb\"] > cfg[\"i_imb_limit\"]:\n        s -= min(10, 50 * (r[\"i_imb\"] - cfg[\"i_imb_limit\"]))\n    if np.isfinite(r.get(\"v_imb\") or np.nan) and r[\"v_imb\"] > cfg[\"v_imb_limit\"]:\n        s -= min(5, 100 * (r[\"v_imb\"] - cfg[\"v_imb_limit\"]))\n    ar = r.get(\"anomaly_ratio\")\n    if np.isfinite(ar or np.nan):\n        s -= min(10, max(0, (ar - 0.08)) * 100)\n    n_trend = sum(1 for f in r[\"findings\"] if f[\"code\"].startswith(\"TREND\"))\n    s -= min(10, 5 * n_trend)\n    return round(max(0.0, s), 1)\n",
   "id": "2c8VNAo9gQtu"
  },
  {
   "cell_type": "code",
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/"
    },
    "id": "dBR1DzEbgQtv",
    "executionInfo": {
     "status": "ok",
     "timestamp": 1793476105848,
     "user_tz": -180,
     "elapsed": 41,
     "user": {
      "displayName": "Joseph Volkan ÖZCAN",
      "userId": "11389530967736397480"
     }
    },
    "outputId": "a1d5a6d6-51ce-4cc9-97a4-ea55eb2e67dc"
   },
   "execution_count": 4,
   "outputs": [
    {
     "output_type": "stream",
     "name": "stdout",
     "text": [
      "Writing qapu_narrator.py\n"
     ]
    }
   ],
   "source": "%%writefile qapu_narrator.py\n# -*- coding: utf-8 -*-\n\"\"\"\nQAPU Offline Narrator — şablon tabanlı, internet/LLM gerektirmeyen Türkçe rapor üretici.\nDört hedef kitle: teknik uzman, özel sektör yöneticisi, kamu yöneticisi, vatandaş.\n\"\"\"\nimport numpy as np\n\nAUDIENCES = [\"teknik\", \"yonetici_ozel\", \"yonetici_kamu\", \"vatandas\"]\n\nAUDIENCE_META = {\n    \"teknik\": dict(baslik=\"Teknik Uzman Raporu\",\n                   dil=\"mühendislik terminolojisi, sayısal detay, standart referansları\"),\n    \"yonetici_ozel\": dict(baslik=\"Yönetici Özeti (Özel Sektör)\",\n                          dil=\"iş etkisi, maliyet/ceza riski, önceliklendirilmiş aksiyon\"),\n    \"yonetici_kamu\": dict(baslik=\"Yönetici Özeti (Kamu)\",\n                          dil=\"mevzuat uyumu, hizmet sürekliliği, kamu kaynağı verimliliği\"),\n    \"vatandas\": dict(baslik=\"Vatandaş Bilgilendirme Notu\",\n                     dil=\"sade Türkçe, benzetmeli anlatım, teknik terim yok\"),\n}\n\n\n# ---------------------------------------------------------------- yardımcılar\ndef pct(x, nd=1):\n    return \"—\" if x is None or not np.isfinite(x) else f\"%{x*100:.{nd}f}\".replace(\".\", \",\")\n\ndef num(x, nd=2, unit=\"\"):\n    if x is None or not np.isfinite(x):\n        return \"—\"\n    s = f\"{x:,.{nd}f}\".replace(\",\", \"§\").replace(\".\", \",\").replace(\"§\", \".\")\n    return s + ((\" \" + unit) if unit else \"\")\n\ndef _sev_word(sev, aud):\n    tablo = {\n        \"teknik\": {\"yüksek\": \"KRİTİK\", \"orta\": \"UYARI\", \"düşük\": \"İZLEME\", \"bilgi\": \"BİLGİ\"},\n        \"yonetici_ozel\": {\"yüksek\": \"Acil\", \"orta\": \"Önemli\", \"düşük\": \"Takip\", \"bilgi\": \"Not\"},\n        \"yonetici_kamu\": {\"yüksek\": \"Acil\", \"orta\": \"Önemli\", \"düşük\": \"Takip\", \"bilgi\": \"Not\"},\n        \"vatandas\": {\"yüksek\": \"önemli bir sorun\", \"orta\": \"dikkat edilmesi gereken bir durum\",\n                     \"düşük\": \"küçük bir uyarı\", \"bilgi\": \"bilgi notu\"},\n    }\n    return tablo[aud][sev]\n\n\n# ------------------------------------------------- bulgu cümleleri (kitleye göre)\ndef _finding_text(f, r, aud):\n    c, v, lim, sev = f[\"code\"], f.get(\"value\"), f.get(\"limit\"), f[\"severity\"]\n    S = _sev_word(sev, aud)\n\n    if c == \"REAKTIF_IND_ASIM\":\n        t = {\n            \"teknik\": f\"[{S}] Endüktif reaktif/aktif enerji oranı son 30 günde {pct(v)} — EPDK sınırı {pct(lim,0)}'in üzerinde. Kompanzasyon panosu devre dışı ya da kondansatör kademeleri yetersiz/arızalı olabilir; motor yüklerinin cosφ'si düşük.\",\n            \"yonetici_ozel\": f\"[{S}] Bu tesis reaktif enerji sınırını aşıyor (ölçülen {pct(v)}, yasal sınır {pct(lim,0)}). Dağıtım şirketi bu durumda reaktif ceza bedeli tahakkuk ettirir; kompanzasyon yatırımı/bakımı bu cezayı tamamen ortadan kaldırabilir.\",\n            \"yonetici_kamu\": f\"[{S}] Tesis, elektrik mevzuatındaki reaktif enerji sınırını ({pct(lim,0)}) aşmaktadır (ölçülen {pct(v)}). Bu, elektrik faturasına yansıyan ve önlenebilir bir kamu kaynağı kaybıdır; kompanzasyon sisteminin kontrolü gerekir.\",\n            \"vatandas\": f\"Bu tesiste {S} tespit edildi: tesis, şebekeden aldığı elektriğin bir kısmını verimsiz kullanıyor (buna 'reaktif enerji' denir — bardağı doldururken yanına taşan su gibi düşünebilirsiniz). Bu durum faturayı artırır; basit bir elektrik panosu bakımı ile düzeltilebilir.\",\n        }\n    elif c == \"REAKTIF_IND_YAKIN\":\n        t = {\n            \"teknik\": f\"[{S}] Endüktif oran {pct(v)} — sınırın ({pct(lim,0)}) %80'ini geçti. Kademe ayarları gözden geçirilmeli.\",\n            \"yonetici_ozel\": f\"[{S}] Reaktif oran {pct(v)} ile ceza sınırına yaklaşıyor; küçük bir ayarla ceza riski önlenebilir.\",\n            \"yonetici_kamu\": f\"[{S}] Reaktif oran {pct(v)}; yasal sınıra yaklaşılmıştır, önleyici kontrol önerilir.\",\n            \"vatandas\": f\"Tesisin elektrik kullanım verimliliği sınıra yaklaşıyor; şimdiden yapılacak küçük bir bakım ileride fatura artışını önler.\",\n        }\n    elif c == \"REAKTIF_CAP_ASIM\":\n        t = {\n            \"teknik\": f\"[{S}] Kapasitif reaktif oran {pct(v)} — EPDK sınırı {pct(lim,0)} aşıldı. Aşırı kompanzasyon: kondansatör kademeleri düşük yükte devrede kalıyor; RG rölesi ayarı/kademe küçültme gerekli.\",\n            \"yonetici_ozel\": f\"[{S}] Tesis şebekeye gereksiz 'kapasitif' enerji veriyor ({pct(v)}, sınır {pct(lim,0)}) — bu da ceza kalemidir. Kompanzasyon ayarı tek seferlik düşük maliyetli bir müdahaledir.\",\n            \"yonetici_kamu\": f\"[{S}] Kapasitif reaktif sınır aşımı ({pct(v)} > {pct(lim,0)}) tespit edildi; fatura cezası riski için kompanzasyon ayarı gereklidir.\",\n            \"vatandas\": f\"Tesisin elektrik düzeltme sistemi gereğinden fazla çalışıyor; bu da tıpkı eksik çalışması gibi faturaya ceza olarak yansıyabilir. Ayar gerektiren bir durumdur.\",\n        }\n    elif c == \"REAKTIF_CAP_YAKIN\":\n        t = {\n            \"teknik\": f\"[{S}] Kapasitif oran {pct(v)}, sınırın %80'inde. Düşük yük saatlerinde kademe çıkışı kontrol edilmeli.\",\n            \"yonetici_ozel\": f\"[{S}] Kapasitif reaktif oran sınıra yaklaşıyor ({pct(v)}); önleyici ayar önerilir.\",\n            \"yonetici_kamu\": f\"[{S}] Kapasitif oran {pct(v)}; sınır aşımı öncesi kontrol önerilir.\",\n            \"vatandas\": f\"Tesisin elektrik sisteminde küçük bir ayar ihtiyacı görünüyor; şu an sorun yok ama takipte.\",\n        }\n    elif c == \"PF_DUSUK\":\n        t = {\n            \"teknik\": f\"[{S}] Yük altındaki ortalama güç faktörü {num(v)} (hedef ≥ {num(lim)}). Düşük cosφ hat kayıplarını ve trafo/kablo ısınmasını artırır; motor verimsizliği veya kompanzasyon eksikliği göstergesidir.\",\n            \"yonetici_ozel\": f\"[{S}] Güç faktörü {num(v)} — hedefin ({num(lim)}) altında. Aynı iş için şebekeden daha fazla akım çekiliyor: kayıplar, ısınma ve ceza riski artıyor.\",\n            \"yonetici_kamu\": f\"[{S}] Tesiste güç faktörü {num(v)} ölçülmüştür (hedef {num(lim)}). Enerji verimliliği düşüktür; iyileştirme, işletme giderini kalıcı olarak azaltır.\",\n            \"vatandas\": f\"Tesis, kullandığı elektriği ideal verimle kullanamıyor (verim göstergesi {num(v)}; ideali {num(lim)} ve üzeri). Bakımla düzeltilebilir, hizmetin aksaması söz konusu değil.\",\n        }\n    elif c == \"THD_AKIM\":\n        t = {\n            \"teknik\": f\"[{S}] Akım THD medyanı {pct(v)} (eşik {pct(lim,0)}). Harmonik kaynaklı ek ısınma motor/trafo ömrünü kısaltır; sürücü (VFD) filtreleri ve kondansatör rezonansı kontrol edilmeli (IEEE 519 referansı).\",\n            \"yonetici_ozel\": f\"[{S}] Elektrik akımında yüksek bozulma (harmonik) ölçüldü ({pct(v)}). Bu, ekipman ömrünü kısaltan ve arıza sıklığını artıran sinsi bir maliyet kalemidir; filtre yatırımı değerlendirilmelidir.\",\n            \"yonetici_kamu\": f\"[{S}] Tesiste akım harmonik bozulması {pct(v)} seviyesindedir (eşik {pct(lim,0)}). Ekipman ömrü ve arıza riski açısından planlı bakım kapsamına alınmalıdır.\",\n            \"vatandas\": f\"Tesisteki elektrik akımında 'parazit' benzeri bir bozulma ölçüldü. Bu, cihazların zamanla daha çabuk yıpranmasına yol açabilir; teknik ekipler gerekli bakımı planlayabilir.\",\n        }\n    elif c == \"THD_GERILIM\":\n        t = {\n            \"teknik\": f\"[{S}] Gerilim THD {pct(v)} (sınır {pct(lim,0)}, IEEE 519). Şebeke tarafı veya tesis içi harmonik kaynakları araştırılmalı.\",\n            \"yonetici_ozel\": f\"[{S}] Şebeke geriliminde bozulma ölçüldü ({pct(v)}); hassas cihazlar için risk oluşturabilir.\",\n            \"yonetici_kamu\": f\"[{S}] Gerilim kalitesinde bozulma ({pct(v)}) tespit edildi; dağıtım şirketi ile birlikte incelenmesi önerilir.\",\n            \"vatandas\": f\"Tesise gelen elektriğin kalitesinde dalgalanma görüldü; takip ediliyor.\",\n        }\n    elif c == \"AKIM_DENGESIZ\":\n        t = {\n            \"teknik\": f\"[{S}] Üç faz akım dengesizliği {pct(v)} (eşik {pct(lim,0)}). Olası nedenler: tek faz yük dağılımı, gevşek klemens, sargı direnç farkı, kondansatör faz arızası. Dengesizlik negatif bileşen ısınması üzerinden motor ömrünü hızla tüketir.\",\n            \"yonetici_ozel\": f\"[{S}] Motorun üç fazı eşit yüklenmiyor (dengesizlik {pct(v)}). Bu, motor arızasının en sık erken habercisidir; planlı kontrol plansız duruştan kat kat ucuzdur.\",\n            \"yonetici_kamu\": f\"[{S}] Tesiste faz dengesizliği {pct(v)} ölçülmüştür. Pompanın/motorun beklenmedik arızasını önlemek için planlı bakım önerilir; hizmet kesintisi riski azaltılır.\",\n            \"vatandas\": f\"Tesisteki motorun bir tarafı diğerlerinden fazla zorlanıyor — üç tekerleği sağlam, biri inik bir araç gibi. Erken bakım yapılırsa büyük arıza ve hizmet kesintisi yaşanmaz.\",\n        }\n    elif c == \"GERILIM_DENGESIZ\":\n        t = {\n            \"teknik\": f\"[{S}] Gerilim dengesizliği {pct(v)} (sınır {pct(lim,0)}, IEC/NEMA). %1 gerilim dengesizliği ~%6-10 akım dengesizliğine yol açar; besleme ve bağlantılar kontrol edilmeli.\",\n            \"yonetici_ozel\": f\"[{S}] Besleme geriliminde dengesizlik ({pct(v)}) var; motor ısınması ve verim kaybına yol açar.\",\n            \"yonetici_kamu\": f\"[{S}] Besleme gerilimi dengesizliği ({pct(v)}) tespit edildi; şebeke bağlantısı kontrol edilmelidir.\",\n            \"vatandas\": f\"Tesise gelen elektrik hatlarında küçük bir dengesizlik ölçüldü; teknik ekip takibinde.\",\n        }\n    elif c == \"SICAKLIK\":\n        t = {\n            \"teknik\": f\"[{S}] Pano/elektronik sıcaklığı {num(v,1,'°C')} (eşik {num(lim,0,'°C')}). Havalandırma, toz ve bağlantı temasları kontrol edilmeli.\",\n            \"yonetici_ozel\": f\"[{S}] Kontrol panosunda yüksek sıcaklık ({num(v,1,'°C')}); elektronik ömrü ve yangın riski açısından kontrol edilmeli.\",\n            \"yonetici_kamu\": f\"[{S}] Panoda yüksek sıcaklık ({num(v,1,'°C')}) ölçüldü; iş güvenliği ve ekipman ömrü için kontrol planlanmalıdır.\",\n            \"vatandas\": f\"Cihaz dolabında normalden yüksek sıcaklık ölçüldü; kontrol planlandı.\",\n        }\n    elif c == \"ANOMALI\":\n        days = \", \".join(f.get(\"days\", [])[-3:])\n        t = {\n            \"teknik\": f\"[{S}] Makine öğrenmesi (IsolationForest) günlerin {pct(v)}'ini anormal işaretledi (son örnekler: {days}). İşletme kayıtlarıyla karşılaştırılmalı.\",\n            \"yonetici_ozel\": f\"[{S}] Otomatik analiz, çalışma günlerinin {pct(v)}'inde olağan dışı davranış tespit etti; teknik inceleme önerilir.\",\n            \"yonetici_kamu\": f\"[{S}] Otomatik izleme sistemi olağan dışı çalışma günleri tespit etti ({pct(v)}); teknik birim bilgilendirilmelidir.\",\n            \"vatandas\": f\"Otomatik izleme sistemi bazı günlerde alışılmadık çalışma fark etti; uzmanlar inceliyor.\",\n        }\n    elif c in (\"TREND_REAKTIF\", \"TREND_THD\", \"TREND_DENGESIZ\", \"TREND_PF\"):\n        metric_ad = {\"TREND_REAKTIF\": \"reaktif oran\", \"TREND_THD\": \"akım harmoniği\",\n                     \"TREND_DENGESIZ\": \"faz dengesizliği\", \"TREND_PF\": \"güç faktörü\"}[c]\n        t = {\n            \"teknik\": f\"[{S}] Kestirim: mevcut eğilim sürerse {metric_ad} ~{int(v)} gün içinde sınırı aşacak. Kök neden analizi için pencere: önümüzdeki {max(7,int(v)//2)} gün.\",\n            \"yonetici_ozel\": f\"[{S}] Öngörü: {metric_ad} kötüleşiyor; yaklaşık {int(v)} gün içinde sınır aşımı bekleniyor. Şimdi planlanan bakım, ceza/arıza gerçekleşmeden sorunu çözer.\",\n            \"yonetici_kamu\": f\"[{S}] Öngörü: {metric_ad} göstergesi yaklaşık {int(v)} gün içinde sınırı aşabilir; bütçeli planlı bakım için zaman penceresi mevcuttur.\",\n            \"vatandas\": f\"Ölçümler, yaklaşık {int(v)} gün içinde bakım gerekebileceğini gösteriyor. Sorun büyümeden planlama yapılıyor — hizmette kesinti beklenmiyor.\",\n        }\n    elif c == \"KALIBRASYON\":\n        t = {\n            \"teknik\": f\"[{S}] Gerilim ölçümleri kalibre edilmemiş görünüyor (medyan VRMS {num(v,0)}). Mutlak değerler yerine oransal metrikler (dengesizlik, THD, PF, reaktif oran) esas alındı; VT/CT kalibrasyonu önerilir.\",\n            \"yonetici_ozel\": f\"[{S}] Bu cihazın ölçüm kalibrasyonu tamamlanmamış; oransal göstergeler güvenilir, mutlak kW/V değerleri kalibrasyon sonrası netleşecek.\",\n            \"yonetici_kamu\": f\"[{S}] Cihaz kalibrasyonu beklemede; raporda oransal göstergeler esas alınmıştır.\",\n            \"vatandas\": f\"Bu noktadaki ölçüm cihazının ince ayarı henüz tamamlanmadı; genel değerlendirme yine de güvenilirdir.\",\n        }\n    elif c == \"VERI_AZ\":\n        t = {\n            \"teknik\": f\"[{S}] Yalnızca {int(v)} günlük veri mevcut (önerilen ≥ {int(lim)}); trend ve anomali kestirimlerinin güven aralığı geniştir.\",\n            \"yonetici_ozel\": f\"[{S}] Veri geçmişi kısa ({int(v)} gün); sonuçlar ilk izlenimdir, izleme sürüyor.\",\n            \"yonetici_kamu\": f\"[{S}] İzleme yeni başlamıştır ({int(v)} gün); değerlendirme güncellenecektir.\",\n            \"vatandas\": f\"Bu nokta yakın zamanda izlemeye alındı; değerlendirme her gün biraz daha kesinleşiyor.\",\n        }\n    else:\n        t = {a: f\"[{S}] {c}: {v}\" for a in AUDIENCES}\n    return t[aud]\n\n\n# ------------------------------------------------------------- öneri motoru\n_RECS = {\n    \"REAKTIF_IND_ASIM\": {\n        \"teknik\": \"Kompanzasyon panosu: RG rölesi ayarları, kondansatör kapasite ölçümü, kontaktör/sigorta kontrolü; gerekirse kademe ekleme.\",\n        \"mgr\": \"Kompanzasyon bakımı/yatırımı için teknik ekibe iş emri açılması (tipik geri ödeme süresi aylar mertebesinde).\",\n        \"vatandas\": \"Elektrik panosunda planlı bakım yapılacak.\",\n    },\n    \"REAKTIF_CAP_ASIM\": {\n        \"teknik\": \"Düşük yükte kademe çıkışını sağlayacak RG röle ayarı; en küçük kademenin küçültülmesi veya şönt reaktör değerlendirmesi.\",\n        \"mgr\": \"Kompanzasyon ayarı için kısa süreli teknik müdahale planlanması.\",\n        \"vatandas\": \"Sistem ayarı yapılacak.\",\n    },\n    \"PF_DUSUK\": {\n        \"teknik\": \"Motor yüklenme analizi (boşta/az yükte çalışmayı azalt), kompanzasyon kademe yeterliliği, gerekiyorsa VFD parametre kontrolü.\",\n        \"mgr\": \"Verimlilik iyileştirmesi: pompa çalışma programı ve kompanzasyon birlikte gözden geçirilmeli.\",\n        \"vatandas\": \"Verimlilik bakımı planlanacak.\",\n    },\n    \"THD_AKIM\": {\n        \"teknik\": \"Harmonik kaynak taraması (VFD, doğrultucular); pasif/aktif filtre; kondansatörlerde detuned reaktör kontrolü.\",\n        \"mgr\": \"Harmonik filtre yatırımı için teknik fizibilite talep edilmeli.\",\n        \"vatandas\": \"Elektrik kalitesi için ek donanım değerlendirilecek.\",\n    },\n    \"AKIM_DENGESIZ\": {\n        \"teknik\": \"Termal kamera ile klemens taraması, faz akım ölçüm doğrulaması, motor sargı direnç/izolasyon testi (megger), kondansatör faz kontrolü.\",\n        \"mgr\": \"Planlı motor/pano kontrolü — plansız duruş riskine karşı en yüksek öncelik.\",\n        \"vatandas\": \"Motor kontrol bakımı planlanacak.\",\n    },\n    \"GERILIM_DENGESIZ\": {\n        \"teknik\": \"Besleme bağlantıları ve şebeke tarafı ölçüm; dağıtım şirketi ile gerilim kalitesi kaydı paylaşımı.\",\n        \"mgr\": \"Dağıtım şirketi nezdinde gerilim kalitesi başvurusu değerlendirilmeli.\",\n        \"vatandas\": \"Elektrik şirketi ile birlikte kontrol yapılacak.\",\n    },\n    \"SICAKLIK\": {\n        \"teknik\": \"Pano havalandırma/filtre temizliği, termal tarama, bağlantı tork kontrolü.\",\n        \"mgr\": \"Pano bakımı iş emri.\",\n        \"vatandas\": \"Dolap bakımı yapılacak.\",\n    },\n}\n\n\ndef _recs_for(findings, aud):\n    out, seen = [], set()\n    key = \"teknik\" if aud == \"teknik\" else (\"vatandas\" if aud == \"vatandas\" else \"mgr\")\n    for f in sorted(findings, key=lambda x: {\"yüksek\": 0, \"orta\": 1, \"düşük\": 2, \"bilgi\": 3}[x[\"severity\"]]):\n        base = f[\"code\"].replace(\"_YAKIN\", \"_ASIM\").replace(\"TREND_REAKTIF\", \"REAKTIF_IND_ASIM\") \\\n                        .replace(\"TREND_THD\", \"THD_AKIM\").replace(\"TREND_DENGESIZ\", \"AKIM_DENGESIZ\") \\\n                        .replace(\"TREND_PF\", \"PF_DUSUK\")\n        rec = _RECS.get(base, {}).get(key)\n        if rec and base not in seen:\n            out.append(rec)\n            seen.add(base)\n    return out\n\n\n# ------------------------------------------------------------ cihaz raporu\ndef device_report(r, aud):\n    meta = AUDIENCE_META[aud]\n    L = []\n    L.append(f\"## {r['name']} — {meta['baslik']}\")\n    L.append(f\"*Dönem: {r['first']} → {r['last']} ({r['days']} gün) · Rapor: otomatik üretim (offline narrator)*\\n\")\n\n    risk_tr = {\"İYİ\": \"iyi durumda\", \"İZLENMELİ\": \"izlenmesi gereken\",\n               \"RİSKLİ\": \"riskli\", \"KRİTİK\": \"kritik\"}\n\n    if aud == \"teknik\":\n        L.append(f\"**Sağlık skoru: {r['score']}/100 ({r['risk']})**\\n\")\n        L.append(\"| Metrik | Değer | Eşik |\")\n        L.append(\"|---|---|---|\")\n        L.append(f\"| Aktif enerji (dönem) | {num(r['ae_total_kwh'],1,'kWh')} | — |\")\n        L.append(f\"| Endüktif oran (30g) | {pct(r.get('ind_ratio_30g'))} | %20 (EPDK) |\")\n        L.append(f\"| Kapasitif oran (30g) | {pct(r.get('cap_ratio_30g'))} | %15 (EPDK) |\")\n        L.append(f\"| Güç faktörü (yükte) | {num(r.get('pf_loaded'))} | ≥0,95 |\")\n        L.append(f\"| THD-I / THD-V (medyan) | {pct(r.get('thd_i'))} / {pct(r.get('thd_v'))} | %15 / %8 |\")\n        L.append(f\"| Akım / gerilim dengesizliği | {pct(r.get('i_imb'))} / {pct(r.get('v_imb'))} | %10 / %2 |\")\n        if np.isfinite(r.get(\"anomaly_ratio\") or np.nan):\n            L.append(f\"| Anomali oranı (IForest) | {pct(r['anomaly_ratio'])} | %12 |\")\n        L.append(\"\")\n    elif aud == \"vatandas\":\n        durum = risk_tr[r[\"risk\"]]\n        L.append(f\"Bu tesis şu anda **{durum}** olarak değerlendiriliyor \"\n                 f\"(100 üzerinden {int(r['score'])} puan). Değerlendirme, tesisteki elektrik \"\n                 f\"ölçüm cihazının {r['days']} günlük verisine dayanıyor.\\n\")\n    else:\n        kamu = \" Kamu hizmet sürekliliği önceliklidir.\" if aud == \"yonetici_kamu\" else \"\"\n        L.append(f\"**Durum: {r['risk']} (skor {r['score']}/100)** — dönem tüketimi \"\n                 f\"{num(r['ae_total_kwh'],0,'kWh')}.{kamu}\\n\")\n\n    if r[\"findings\"]:\n        L.append(\"**Bulgular**\" if aud != \"vatandas\" else \"**Tespit edilenler**\")\n        for f in sorted(r[\"findings\"], key=lambda x: {\"yüksek\": 0, \"orta\": 1, \"düşük\": 2, \"bilgi\": 3}[x[\"severity\"]]):\n            L.append(f\"- {_finding_text(f, r, aud)}\")\n        L.append(\"\")\n    else:\n        msg = {\"teknik\": \"Tüm metrikler eşik değerlerin içinde; rutin izleme sürdürülmeli.\",\n               \"vatandas\": \"Her şey yolunda görünüyor; düzenli izleme devam ediyor.\"}\n        L.append(msg.get(aud, \"Önemli bir bulgu yok; rutin izleme sürüyor.\") + \"\\n\")\n\n    recs = _recs_for(r[\"findings\"], aud)\n    if recs:\n        L.append(\"**Önerilen aksiyonlar**\" if aud != \"vatandas\" else \"**Ne yapılacak?**\")\n        for i, rec in enumerate(recs, 1):\n            L.append(f\"{i}. {rec}\")\n        L.append(\"\")\n    return \"\\n\".join(L)\n\n\n# -------------------------------------------------------------- filo raporu\ndef fleet_report(results, aud):\n    meta = AUDIENCE_META[aud]\n    rs = sorted(results.values(), key=lambda r: r[\"score\"])\n    n = len(rs)\n    krit = [r for r in rs if r[\"risk\"] == \"KRİTİK\"]\n    risk = [r for r in rs if r[\"risk\"] == \"RİSKLİ\"]\n    izle = [r for r in rs if r[\"risk\"] == \"İZLENMELİ\"]\n    iyi = [r for r in rs if r[\"risk\"] == \"İYİ\"]\n    toplam_kwh = sum(r[\"ae_total_kwh\"] for r in rs)\n    reaktif_asim = [r for r in rs if any(f[\"code\"].startswith(\"REAKTIF\") and f[\"code\"].endswith(\"ASIM\")\n                                         for f in r[\"findings\"])]\n    L = [f\"# Filo Genel Durum — {meta['baslik']}\",\n         f\"*{n} cihaz · toplam {num(toplam_kwh,0,'kWh')} aktif enerji · offline narrator ile otomatik üretildi*\\n\"]\n\n    if aud == \"vatandas\":\n        L.append(f\"İzlenen **{n} tesisin** {len(iyi)} tanesi iyi durumda, \"\n                 f\"{len(izle)} tanesi yakından izleniyor, {len(risk)+len(krit)} tanesinde \"\n                 f\"bakım planlaması yapılıyor. Amaç: arızaları olmadan önce yakalamak, \"\n                 f\"kesintisiz hizmet ve gereksiz masrafların önlenmesi.\\n\")\n        if risk or krit:\n            L.append(\"Bakım planlanan tesisler: \" + \", \".join(r[\"name\"] for r in (krit + risk)) + \".\\n\")\n    else:\n        L.append(f\"**Dağılım:** KRİTİK {len(krit)} · RİSKLİ {len(risk)} · İZLENMELİ {len(izle)} · İYİ {len(iyi)}\\n\")\n        if reaktif_asim:\n            if aud == \"teknik\":\n                L.append(f\"**Reaktif sınır aşımı olan {len(reaktif_asim)} tesis:** \" +\n                         \", \".join(f\"{r['name']} (end. {pct(r.get('ind_ratio_30g'))}, kap. {pct(r.get('cap_ratio_30g'))})\"\n                                   for r in reaktif_asim) + \"\\n\")\n            else:\n                L.append(f\"**Reaktif ceza riski taşıyan {len(reaktif_asim)} tesis var** — bu, doğrudan \"\n                         f\"faturaya yansıyan ve kompanzasyon bakımı ile tamamen önlenebilir bir kalemdir: \" +\n                         \", \".join(r[\"name\"] for r in reaktif_asim) + \"\\n\")\n        L.append(\"| Tesis | Skor | Durum | End. oran | PF | Öncelikli bulgu |\")\n        L.append(\"|---|---|---|---|---|---|\")\n        for r in rs:\n            top = r[\"findings\"][0][\"code\"] if r[\"findings\"] else \"—\"\n            L.append(f\"| {r['name']} | {r['score']} | {r['risk']} | \"\n                     f\"{pct(r.get('ind_ratio_30g'))} | {num(r.get('pf_loaded'))} | {top} |\")\n        L.append(\"\")\n        if aud == \"teknik\":\n            L.append(\"Eşikler: EPDK reaktif sınırları (end. %20 / kap. %15), IEEE 519 THD, \"\n                     \"IEC/NEMA gerilim dengesizliği %2. Anomali: IsolationForest, gün bazlı özellik uzayı.\\n\")\n        else:\n            oncelik = krit + risk\n            if oncelik:\n                L.append(\"**Önerilen öncelik sırası:** \" +\n                         \" → \".join(f\"{i+1}) {r['name']}\" for i, r in enumerate(oncelik[:5])) + \"\\n\")\n    return \"\\n\".join(L)\n",
   "id": "dBR1DzEbgQtv"
  },
  {
   "cell_type": "code",
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/"
    },
    "id": "MA-nSMSygQtw",
    "executionInfo": {
     "status": "ok",
     "timestamp": 1793476105851,
     "user_tz": -180,
     "elapsed": 3,
     "user": {
      "displayName": "Joseph Volkan ÖZCAN",
      "userId": "11389530967736397480"
     }
    },
    "outputId": "1cc37c84-828a-4674-dd59-46ac7822adff"
   },
   "execution_count": 5,
   "outputs": [
    {
     "output_type": "stream",
     "name": "stdout",
     "text": [
      "Writing qapu_report.py\n"
     ]
    }
   ],
   "source": "%%writefile qapu_report.py\n# -*- coding: utf-8 -*-\n\"\"\"Grafik üretimi + rapor dosyaları (MD/HTML) + paketleme.\"\"\"\nimport os, base64, html, zipfile\nimport numpy as np\nimport matplotlib\nmatplotlib.use(\"Agg\")\nimport matplotlib.pyplot as plt\n\nimport qapu_narrator as nar\nfrom qapu_pm import CONFIG\n\nplt.rcParams.update({\"figure.dpi\": 110, \"font.size\": 9, \"axes.grid\": True,\n                     \"grid.alpha\": 0.3, \"axes.spines.top\": False, \"axes.spines.right\": False})\n\nRISK_COLORS = {\"İYİ\": \"#2e7d32\", \"İZLENMELİ\": \"#f9a825\", \"RİSKLİ\": \"#ef6c00\", \"KRİTİK\": \"#c62828\"}\n\n\ndef fleet_chart(results, path):\n    rs = sorted(results.values(), key=lambda r: r[\"score\"])\n    fig, ax = plt.subplots(figsize=(8, max(3, 0.35 * len(rs))))\n    names = [r[\"name\"][:28] for r in rs]\n    scores = [r[\"score\"] for r in rs]\n    colors = [RISK_COLORS[r[\"risk\"]] for r in rs]\n    ax.barh(names, scores, color=colors)\n    for y, s in enumerate(scores):\n        ax.text(s + 1, y, f\"{s:.0f}\", va=\"center\", fontsize=8)\n    ax.axvline(70, color=\"#999\", ls=\"--\", lw=1)\n    ax.axvline(85, color=\"#999\", ls=\":\", lw=1)\n    ax.set_xlim(0, 108)\n    ax.set_xlabel(\"Sağlık skoru (0–100)\")\n    ax.set_title(\"Filo Sağlık Durumu — cihaz bazında\")\n    fig.tight_layout()\n    fig.savefig(path)\n    plt.close(fig)\n\n\ndef device_chart(daily, dev_id, r, path, cfg=CONFIG):\n    sub = daily[daily[\"device_id\"] == dev_id].sort_values(\"device_time\")\n    if len(sub) < 2:\n        return False\n    t = sub[\"device_time\"]\n    fig, axes = plt.subplots(2, 2, figsize=(10, 6), sharex=True)\n    fig.suptitle(f\"{r['name']} — günlük göstergeler\", fontsize=11)\n\n    ax = axes[0, 0]\n    ax.plot(t, sub[\"ind_ratio\"] * 100, \"o-\", ms=3, color=\"#c62828\", label=\"Endüktif\")\n    ax.plot(t, sub[\"cap_ratio\"] * 100, \"s-\", ms=3, color=\"#1565c0\", label=\"Kapasitif\")\n    ax.axhline(cfg[\"ind_limit\"] * 100, color=\"#c62828\", ls=\"--\", lw=1)\n    ax.axhline(cfg[\"cap_limit\"] * 100, color=\"#1565c0\", ls=\":\", lw=1)\n    ax.set_title(\"Reaktif/Aktif oran (%) ve EPDK sınırları\")\n    ax.legend(fontsize=7)\n\n    ax = axes[0, 1]\n    if \"pf_loaded\" in sub:\n        ax.plot(t, sub[\"pf_loaded\"], \"o-\", ms=3, color=\"#2e7d32\")\n    ax.axhline(cfg[\"pf_target\"], color=\"#999\", ls=\"--\", lw=1)\n    ax.set_ylim(0, 1.05)\n    ax.set_title(\"Güç faktörü (yük altında)\")\n\n    ax = axes[1, 0]\n    if \"thd_i\" in sub:\n        ax.plot(t, sub[\"thd_i\"] * 100, \"o-\", ms=3, color=\"#6a1b9a\", label=\"THD-I\")\n    if \"thd_v\" in sub:\n        ax.plot(t, sub[\"thd_v\"] * 100, \"s-\", ms=3, color=\"#00838f\", label=\"THD-V\")\n    ax.axhline(cfg[\"thd_i_limit\"] * 100, color=\"#6a1b9a\", ls=\"--\", lw=1)\n    ax.set_title(\"Harmonik bozulma (%)\")\n    ax.legend(fontsize=7)\n\n    ax = axes[1, 1]\n    if \"i_imb\" in sub:\n        ax.plot(t, sub[\"i_imb\"] * 100, \"o-\", ms=3, color=\"#ef6c00\", label=\"Akım\")\n    if \"v_imb\" in sub:\n        ax.plot(t, sub[\"v_imb\"] * 100, \"s-\", ms=3, color=\"#455a64\", label=\"Gerilim\")\n    ax.axhline(cfg[\"i_imb_limit\"] * 100, color=\"#ef6c00\", ls=\"--\", lw=1)\n    ax.set_title(\"Faz dengesizliği (%)\")\n    ax.legend(fontsize=7)\n\n    for ax in axes.flat:\n        ax.tick_params(axis=\"x\", rotation=30)\n    fig.tight_layout()\n    fig.savefig(path)\n    plt.close(fig)\n    return True\n\n\n# ------------------------------------------------- basit MD → HTML çevirici\ndef _md_to_html(md):\n    try:\n        import markdown\n        return markdown.markdown(md, extensions=[\"tables\"])\n    except Exception:\n        return \"<pre style='white-space:pre-wrap'>\" + html.escape(md) + \"</pre>\"\n\n\n_HTML_TMPL = \"\"\"<!doctype html><html lang=\"tr\"><head><meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title>{title}</title>\n<style>\n body{{font-family:-apple-system,'Segoe UI',Roboto,sans-serif;max-width:860px;margin:2rem auto;\n      padding:0 16px;color:#222;line-height:1.55}}\n table{{border-collapse:collapse;width:100%;font-size:.92em}}\n th,td{{border:1px solid #ddd;padding:5px 8px;text-align:left}}\n th{{background:#f4f4f4}} img{{max-width:100%}}\n h1,h2{{border-bottom:2px solid #eee;padding-bottom:4px}}\n .foot{{color:#888;font-size:.8em;margin-top:2rem}}\n</style></head><body>{body}\n<p class=\"foot\">Bu rapor, enerji analizörü verilerinden offline narrator ile tamamen\nçevrimdışı/deterministik olarak üretilmiştir (LLM kullanılmamıştır).</p></body></html>\"\"\"\n\n\ndef _embed_img(path):\n    with open(path, \"rb\") as fh:\n        b64 = base64.b64encode(fh.read()).decode()\n    return f'<img src=\"data:image/png;base64,{b64}\" alt=\"grafik\">'\n\n\ndef generate_reports(results, daily, outdir=\"raporlar\", audiences=None, top_device_charts=8):\n    \"\"\"Tüm kitleler için filo + cihaz raporlarını MD ve HTML olarak üretir; zip döner.\"\"\"\n    audiences = audiences or nar.AUDIENCES\n    os.makedirs(outdir, exist_ok=True)\n    chart_dir = os.path.join(outdir, \"grafikler\")\n    os.makedirs(chart_dir, exist_ok=True)\n\n    fleet_png = os.path.join(chart_dir, \"filo_saglik.png\")\n    fleet_chart(results, fleet_png)\n\n    rs_sorted = sorted(results.values(), key=lambda r: r[\"score\"])\n    dev_pngs = {}\n    for r in rs_sorted[:top_device_charts]:\n        p = os.path.join(chart_dir, f\"cihaz_{r['device_id']}.png\")\n        if device_chart(daily, r[\"device_id\"], r, p):\n            dev_pngs[r[\"device_id\"]] = p\n\n    written = []\n    for aud in audiences:\n        md = [nar.fleet_report(results, aud)]\n        html_parts = [_md_to_html(md[0]), _embed_img(fleet_png)]\n        for r in rs_sorted:\n            sec = nar.device_report(r, aud)\n            md.append(\"\\n---\\n\\n\" + sec)\n            html_parts.append(\"<hr>\" + _md_to_html(sec))\n            if r[\"device_id\"] in dev_pngs and aud != \"vatandas\":\n                html_parts.append(_embed_img(dev_pngs[r[\"device_id\"]]))\n        base = os.path.join(outdir, f\"rapor_{aud}\")\n        with open(base + \".md\", \"w\") as fh:\n            fh.write(\"\\n\".join(md))\n        title = nar.AUDIENCE_META[aud][\"baslik\"]\n        with open(base + \".html\", \"w\") as fh:\n            fh.write(_HTML_TMPL.format(title=title, body=\"\\n\".join(html_parts)))\n        written += [base + \".md\", base + \".html\"]\n\n    zpath = os.path.join(outdir, \"qapu_raporlar.zip\")\n    with zipfile.ZipFile(zpath, \"w\", zipfile.ZIP_DEFLATED) as z:\n        for p in written + [fleet_png] + list(dev_pngs.values()):\n            z.write(p, os.path.relpath(p, outdir))\n    return zpath, written\n",
   "id": "MA-nSMSygQtw"
  },
  {
   "cell_type": "code",
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/"
    },
    "id": "Zi1DFwT1gQtx",
    "executionInfo": {
     "status": "ok",
     "timestamp": 1793476105865,
     "user_tz": -180,
     "elapsed": 13,
     "user": {
      "displayName": "Joseph Volkan ÖZCAN",
      "userId": "11389530967736397480"
     }
    },
    "outputId": "3ce6e362-1ff8-4622-cafc-6c9fc702e437"
   },
   "execution_count": 6,
   "outputs": [
    {
     "output_type": "stream",
     "name": "stdout",
     "text": [
      "Writing qapu_docx.py\n"
     ]
    }
   ],
   "source": "%%writefile qapu_docx.py\n# -*- coding: utf-8 -*-\n\"\"\"\nQAPU — Profesyonel Word (.docx) rapor üretici\nKapak sayfası, KPI özeti, kurumsal renkli tablolar, risk rozetleri,\ngömülü grafikler ve sayfa numaralı alt bilgi ile.\n\"\"\"\nimport os\nimport numpy as np\nfrom docx import Document\nfrom docx.shared import Pt, Cm, RGBColor\nfrom docx.enum.text import WD_ALIGN_PARAGRAPH\nfrom docx.enum.table import WD_TABLE_ALIGNMENT\nfrom docx.oxml.ns import qn\nfrom docx.oxml import OxmlElement\n\nimport qapu_narrator as nar\nimport qapu_report as rep\n\n# ----------------------------------------------------------- kurumsal palet\nNAVY = RGBColor(0x1F, 0x38, 0x64)\nBLUE = RGBColor(0x2E, 0x74, 0xB5)\nGRAY = RGBColor(0x6B, 0x6B, 0x6B)\nLIGHT = \"F2F5FA\"           # zebra satır dolgusu (hex, # yok)\nRISK_HEX = {\"İYİ\": \"2E7D32\", \"İZLENMELİ\": \"F9A825\", \"RİSKLİ\": \"EF6C00\", \"KRİTİK\": \"C62828\"}\nSEV_HEX = {\"yüksek\": \"C62828\", \"orta\": \"EF6C00\", \"düşük\": \"F9A825\", \"bilgi\": \"6B6B6B\"}\n\n\n# ------------------------------------------------------------- xml yardımcıları\ndef _shade(cell, hexcolor):\n    tcPr = cell._tc.get_or_add_tcPr()\n    shd = OxmlElement(\"w:shd\")\n    shd.set(qn(\"w:val\"), \"clear\")\n    shd.set(qn(\"w:fill\"), hexcolor)\n    tcPr.append(shd)\n\n\ndef _cell_text(cell, text, bold=False, color=None, size=9, align=None):\n    p = cell.paragraphs[0]\n    p.text = \"\"\n    r = p.add_run(text)\n    r.bold = bold\n    r.font.size = Pt(size)\n    if color:\n        r.font.color.rgb = color\n    if align:\n        p.alignment = align\n    return p\n\n\ndef _page_number_footer(section, left_text):\n    foot = section.footer\n    p = foot.paragraphs[0]\n    p.text = \"\"\n    r = p.add_run(left_text + \"   ·   Sayfa \")\n    r.font.size = Pt(8)\n    r.font.color.rgb = GRAY\n    fld = OxmlElement(\"w:fldSimple\")\n    fld.set(qn(\"w:instr\"), \"PAGE\")\n    rpr = OxmlElement(\"w:rPr\")\n    sz = OxmlElement(\"w:sz\"); sz.set(qn(\"w:val\"), \"16\")\n    rpr.append(sz)\n    fld.append(rpr)\n    p._p.append(fld)\n\n\ndef _hr(par):\n    pPr = par._p.get_or_add_pPr()\n    pbdr = OxmlElement(\"w:pBdr\")\n    b = OxmlElement(\"w:bottom\")\n    b.set(qn(\"w:val\"), \"single\"); b.set(qn(\"w:sz\"), \"8\")\n    b.set(qn(\"w:space\"), \"1\"); b.set(qn(\"w:color\"), \"1F3864\")\n    pbdr.append(b)\n    pPr.append(pbdr)\n\n\ndef _styled_table(doc, header, rows, widths_cm, risk_col=None, zebra=True):\n    t = doc.add_table(rows=1 + len(rows), cols=len(header))\n    t.style = \"Table Grid\"\n    t.alignment = WD_TABLE_ALIGNMENT.CENTER\n    for j, h in enumerate(header):\n        c = t.rows[0].cells[j]\n        _cell_text(c, h, bold=True, color=RGBColor(0xFF, 0xFF, 0xFF), size=9)\n        _shade(c, \"1F3864\")\n    for i, row in enumerate(rows, start=1):\n        for j, val in enumerate(row):\n            c = t.rows[i].cells[j]\n            _cell_text(c, str(val), size=9)\n            if zebra and i % 2 == 0:\n                _shade(c, LIGHT)\n            if risk_col is not None and j == risk_col and str(val) in RISK_HEX:\n                _shade(c, RISK_HEX[str(val)])\n                _cell_text(c, str(val), bold=True, color=RGBColor(0xFF, 0xFF, 0xFF), size=9,\n                           align=WD_ALIGN_PARAGRAPH.CENTER)\n    for j, w in enumerate(widths_cm):\n        for row in t.rows:\n            row.cells[j].width = Cm(w)\n    return t\n\n\ndef _heading(doc, text, level=1, color=NAVY, rule=False):\n    h = doc.add_heading(\"\", level=level)\n    r = h.add_run(text)\n    r.font.color.rgb = color\n    if rule:\n        _hr(h)\n    return h\n\n\n# --------------------------------------------------------------- kapak sayfası\ndef _cover(doc, aud, results, customer=None):\n    meta = nar.AUDIENCE_META[aud]\n    rs = list(results.values())\n    first = min(r[\"first\"] for r in rs)\n    last = max(r[\"last\"] for r in rs)\n    krit = sum(1 for r in rs if r[\"risk\"] == \"KRİTİK\")\n    risk = sum(1 for r in rs if r[\"risk\"] == \"RİSKLİ\")\n    reaktif = sum(1 for r in rs if any(f[\"code\"].startswith(\"REAKTIF\") and f[\"code\"].endswith(\"ASIM\")\n                                       for f in r[\"findings\"]))\n    kwh = sum(r[\"ae_total_kwh\"] for r in rs)\n\n    for _ in range(5):\n        doc.add_paragraph()\n    p = doc.add_paragraph()\n    p.alignment = WD_ALIGN_PARAGRAPH.CENTER\n    r = p.add_run(\"QAPU ENERJİ İZLEME\")\n    r.font.size = Pt(14); r.font.color.rgb = BLUE; r.bold = True\n\n    p = doc.add_paragraph()\n    p.alignment = WD_ALIGN_PARAGRAPH.CENTER\n    r = p.add_run(\"Reaktif Enerji ve Kestirimci Bakım Raporu\")\n    r.font.size = Pt(26); r.font.color.rgb = NAVY; r.bold = True\n\n    if customer:\n        p = doc.add_paragraph()\n        p.alignment = WD_ALIGN_PARAGRAPH.CENTER\n        r = p.add_run(customer[\"ad\"])\n        r.font.size = Pt(16); r.font.color.rgb = NAVY; r.bold = True\n        if customer.get(\"birim\"):\n            p = doc.add_paragraph()\n            p.alignment = WD_ALIGN_PARAGRAPH.CENTER\n            r = p.add_run(customer[\"birim\"])\n            r.font.size = Pt(12); r.font.color.rgb = GRAY\n\n    p = doc.add_paragraph()\n    p.alignment = WD_ALIGN_PARAGRAPH.CENTER\n    r = p.add_run(meta[\"baslik\"])\n    r.font.size = Pt(15); r.font.color.rgb = GRAY\n\n    p = doc.add_paragraph()\n    p.alignment = WD_ALIGN_PARAGRAPH.CENTER\n    r = p.add_run(f\"Veri dönemi: {first} – {last}\")\n    r.font.size = Pt(11); r.font.color.rgb = GRAY\n\n    doc.add_paragraph(); doc.add_paragraph()\n\n    # KPI şeridi\n    kpis = [(f\"{len(rs)}\", \"İzlenen Tesis\"),\n            (f\"{nar.num(kwh,0)}\", \"Toplam kWh\"),\n            (f\"{krit + risk}\", \"Kritik/Riskli Tesis\"),\n            (f\"{reaktif}\", \"Reaktif Sınır Aşımı\")]\n    t = doc.add_table(rows=2, cols=4)\n    t.alignment = WD_TABLE_ALIGNMENT.CENTER\n    for j, (val, lab) in enumerate(kpis):\n        c = t.rows[0].cells[j]\n        _cell_text(c, val, bold=True, color=RGBColor(0xFF, 0xFF, 0xFF), size=20,\n                   align=WD_ALIGN_PARAGRAPH.CENTER)\n        _shade(c, \"1F3864\" if j % 2 == 0 else \"2E74B5\")\n        c2 = t.rows[1].cells[j]\n        _cell_text(c2, lab, size=9, color=GRAY, align=WD_ALIGN_PARAGRAPH.CENTER)\n        _shade(c2, \"F2F5FA\")\n        for row in t.rows:\n            row.cells[j].width = Cm(4.1)\n\n    for _ in range(6):\n        doc.add_paragraph()\n    p = doc.add_paragraph()\n    p.alignment = WD_ALIGN_PARAGRAPH.CENTER\n    r = p.add_run(\"Bu rapor, enerji analizörü verilerinden offline narrator ile tamamen \"\n                  \"çevrimdışı ve deterministik olarak üretilmiştir (LLM kullanılmamıştır).\")\n    r.font.size = Pt(8); r.italic = True; r.font.color.rgb = GRAY\n    doc.add_page_break()\n\n\n# -------------------------------------------------------------- filo bölümü\ndef _fleet_section(doc, results, aud, fleet_png):\n    rs = sorted(results.values(), key=lambda r: r[\"score\"])\n    _heading(doc, \"1. Filo Genel Durumu\", level=1, rule=True)\n\n    krit = [r for r in rs if r[\"risk\"] == \"KRİTİK\"]\n    risk = [r for r in rs if r[\"risk\"] == \"RİSKLİ\"]\n    izle = [r for r in rs if r[\"risk\"] == \"İZLENMELİ\"]\n    iyi = [r for r in rs if r[\"risk\"] == \"İYİ\"]\n\n    if aud == \"vatandas\":\n        p = doc.add_paragraph()\n        p.add_run(f\"İzlenen {len(rs)} tesisin {len(iyi)} tanesi iyi durumda, {len(izle)} tanesi \"\n                  f\"yakından izleniyor, {len(risk)+len(krit)} tanesinde bakım planlaması yapılıyor. \"\n                  \"Amaç: arızaları olmadan önce yakalamak, kesintisiz hizmet ve gereksiz \"\n                  \"masrafların önlenmesi.\")\n    else:\n        p = doc.add_paragraph()\n        for lab, grp, hx in [(\"KRİTİK\", krit, \"C62828\"), (\"RİSKLİ\", risk, \"EF6C00\"),\n                             (\"İZLENMELİ\", izle, \"F9A825\"), (\"İYİ\", iyi, \"2E7D32\")]:\n            r = p.add_run(f\" ● {lab}: {len(grp)}   \")\n            r.font.color.rgb = RGBColor(int(hx[0:2], 16), int(hx[2:4], 16), int(hx[4:6], 16))\n            r.bold = True\n\n    if os.path.exists(fleet_png):\n        doc.add_picture(fleet_png, width=Cm(16.5))\n\n    reaktif = [r for r in rs if any(f[\"code\"].startswith(\"REAKTIF\") and f[\"code\"].endswith(\"ASIM\")\n                                    for f in r[\"findings\"])]\n    if reaktif and aud != \"vatandas\":\n        p = doc.add_paragraph()\n        msg = (f\"Reaktif sınır aşımı olan {len(reaktif)} tesis: \" if aud == \"teknik\" else\n               f\"Reaktif ceza riski taşıyan {len(reaktif)} tesis var — kompanzasyon bakımı ile \"\n               f\"tamamen önlenebilir bir fatura kalemidir: \")\n        r = p.add_run(msg)\n        r.bold = True\n        p.add_run(\", \".join(x[\"name\"] for x in reaktif) + \".\")\n\n    if aud != \"vatandas\":\n        header = [\"Tesis\", \"Skor\", \"Durum\", \"End. oran\", \"PF\", \"Öncelikli bulgu\"]\n        rows = []\n        for r in rs:\n            top = r[\"findings\"][0][\"code\"].replace(\"_\", \" \") if r[\"findings\"] else \"—\"\n            rows.append([r[\"name\"], f\"{r['score']:.0f}\", r[\"risk\"],\n                         nar.pct(r.get(\"ind_ratio_30g\")), nar.num(r.get(\"pf_loaded\")), top])\n        _styled_table(doc, header, rows, [5.2, 1.4, 2.4, 2.0, 1.6, 4.0], risk_col=2)\n    else:\n        onc = krit + risk\n        if onc:\n            p = doc.add_paragraph()\n            p.add_run(\"Bakım planlanan tesisler: \").bold = True\n            p.add_run(\", \".join(r[\"name\"] for r in onc) + \".\")\n\n    onc = krit + risk\n    if onc and aud != \"vatandas\":\n        p = doc.add_paragraph()\n        p.add_run(\"Önerilen öncelik sırası: \").bold = True\n        p.add_run(\"  →  \".join(f\"{i+1}) {r['name']}\" for i, r in enumerate(onc[:5])))\n    doc.add_page_break()\n\n\n# -------------------------------------------------------------- cihaz bölümü\ndef _device_section(doc, r, idx, aud, chart_png=None):\n    h = _heading(doc, f\"{idx}. {r['name']}\", level=1, rule=True)\n    badge = h.add_run(f\"   {r['risk']}\")\n    hx = RISK_HEX[r[\"risk\"]]\n    badge.font.color.rgb = RGBColor(int(hx[0:2], 16), int(hx[2:4], 16), int(hx[4:6], 16))\n    badge.font.size = Pt(12)\n\n    p = doc.add_paragraph()\n    rr = p.add_run(f\"Dönem: {r['first']} – {r['last']} ({r['days']} gün)   ·   \"\n                   f\"Sağlık skoru: {r['score']:.0f}/100   ·   \"\n                   f\"Tüketim: {nar.num(r['ae_total_kwh'],0,'kWh')}\")\n    rr.font.size = Pt(9); rr.font.color.rgb = GRAY\n\n    if aud == \"vatandas\":\n        durum = {\"İYİ\": \"iyi durumda\", \"İZLENMELİ\": \"izlenmesi gereken\",\n                 \"RİSKLİ\": \"riskli\", \"KRİTİK\": \"kritik\"}[r[\"risk\"]]\n        doc.add_paragraph(f\"Bu tesis şu anda {durum} olarak değerlendiriliyor \"\n                          f\"(100 üzerinden {int(r['score'])} puan).\")\n    elif aud == \"teknik\":\n        header = [\"Metrik\", \"Değer\", \"Eşik\"]\n        rows = [[\"Endüktif oran (30g)\", nar.pct(r.get(\"ind_ratio_30g\")), \"%20 (EPDK)\"],\n                [\"Kapasitif oran (30g)\", nar.pct(r.get(\"cap_ratio_30g\")), \"%15 (EPDK)\"],\n                [\"Güç faktörü (yükte)\", nar.num(r.get(\"pf_loaded\")), \"≥0,95\"],\n                [\"THD-I / THD-V\", f\"{nar.pct(r.get('thd_i'))} / {nar.pct(r.get('thd_v'))}\", \"%15 / %8\"],\n                [\"Akım / gerilim dengesizliği\", f\"{nar.pct(r.get('i_imb'))} / {nar.pct(r.get('v_imb'))}\", \"%10 / %2\"]]\n        if np.isfinite(r.get(\"anomaly_ratio\") or np.nan):\n            rows.append([\"Anomali oranı (IForest)\", nar.pct(r[\"anomaly_ratio\"]), \"%12\"])\n        _styled_table(doc, header, rows, [6.5, 5.0, 4.0])\n        doc.add_paragraph()\n\n    if chart_png and aud != \"vatandas\" and os.path.exists(chart_png):\n        doc.add_picture(chart_png, width=Cm(16.5))\n\n    if r[\"findings\"]:\n        p = doc.add_paragraph()\n        p.add_run(\"Bulgular\" if aud != \"vatandas\" else \"Tespit edilenler\").bold = True\n        for f in sorted(r[\"findings\"], key=lambda x: {\"yüksek\": 0, \"orta\": 1, \"düşük\": 2, \"bilgi\": 3}[x[\"severity\"]]):\n            text = nar._finding_text(f, r, aud)\n            par = doc.add_paragraph(style=\"List Bullet\")\n            m = text.split(\"]\", 1)\n            if text.startswith(\"[\") and len(m) == 2 and aud != \"vatandas\":\n                tag = par.add_run(m[0] + \"]\")\n                hx = SEV_HEX[f[\"severity\"]]\n                tag.font.color.rgb = RGBColor(int(hx[0:2], 16), int(hx[2:4], 16), int(hx[4:6], 16))\n                tag.bold = True\n                par.add_run(m[1])\n            else:\n                par.add_run(text)\n    else:\n        doc.add_paragraph(\"Önemli bir bulgu yok; rutin izleme sürüyor.\")\n\n    recs = nar._recs_for(r[\"findings\"], aud)\n    if recs:\n        p = doc.add_paragraph()\n        p.add_run(\"Önerilen aksiyonlar\" if aud != \"vatandas\" else \"Ne yapılacak?\").bold = True\n        for rec in recs:\n            doc.add_paragraph(rec, style=\"List Number\")\n\n\n# ------------------------------------------------------------------- ana giriş\ndef build_docx(results, daily, aud, out_path, chart_dir, customer=None):\n    doc = Document()\n    st = doc.styles[\"Normal\"]\n    st.font.name = \"Calibri\"\n    st.font.size = Pt(10.5)\n    for lvl in [\"Heading 1\", \"Heading 2\"]:\n        doc.styles[lvl].font.color.rgb = NAVY\n\n    sec = doc.sections[0]\n    sec.top_margin = sec.bottom_margin = Cm(2.0)\n    sec.left_margin = sec.right_margin = Cm(2.2)\n    kurum = (customer[\"ad\"].split(\"(\")[-1].rstrip(\")\") if customer and \"(\" in customer[\"ad\"]\n             else (customer[\"ad\"] if customer else \"QAPU Enerji İzleme\"))\n    _page_number_footer(sec, f\"{kurum} · {nar.AUDIENCE_META[aud]['baslik']}\")\n\n    _cover(doc, aud, results, customer=customer)\n    fleet_png = os.path.join(chart_dir, \"filo_saglik.png\")\n    _fleet_section(doc, results, aud, fleet_png)\n\n    rs_sorted = sorted(results.values(), key=lambda r: r[\"score\"])\n    for i, r in enumerate(rs_sorted, start=2):\n        png = os.path.join(chart_dir, f\"cihaz_{r['device_id']}.png\")\n        _device_section(doc, r, i, aud, chart_png=png if os.path.exists(png) else None)\n\n    doc.save(out_path)\n    return out_path\n\n\ndef generate_docx_reports(results, daily, outdir=\"raporlar\", audiences=None, top_device_charts=8):\n    \"\"\"4 kitle için profesyonel .docx raporları üretir; dosya listesi döner.\"\"\"\n    audiences = audiences or nar.AUDIENCES\n    os.makedirs(outdir, exist_ok=True)\n    chart_dir = os.path.join(outdir, \"grafikler\")\n    os.makedirs(chart_dir, exist_ok=True)\n\n    fleet_png = os.path.join(chart_dir, \"filo_saglik.png\")\n    if not os.path.exists(fleet_png):\n        rep.fleet_chart(results, fleet_png)\n    rs_sorted = sorted(results.values(), key=lambda r: r[\"score\"])\n    for r in rs_sorted[:top_device_charts]:\n        p = os.path.join(chart_dir, f\"cihaz_{r['device_id']}.png\")\n        if not os.path.exists(p):\n            rep.device_chart(daily, r[\"device_id\"], r, p)\n\n    return [build_docx(results, daily, aud, os.path.join(outdir, f\"rapor_{aud}.docx\"), chart_dir)\n            for aud in audiences]\n\n\ndef generate_customer_reports(results, daily, outdir=\"raporlar_musteri\",\n                              include_test=False, top_device_charts=20):\n    \"\"\"Müşteri bazında rapor setleri üretir: <outdir>/<MUSTERI>/rapor_<kitle>.docx\n    Kitle seçimi müşteri türüne göre: kamu → yonetici_kamu, özel → yonetici_ozel;\n    teknik ve vatandaş raporu her müşteri için üretilir.\"\"\"\n    import qapu_customers as qc\n    os.makedirs(outdir, exist_ok=True)\n    gruplar = qc.split_by_customer(results, include_test=include_test)\n    written = {}\n    for kod, sub_results in gruplar.items():\n        cust = qc.CUSTOMERS[kod]\n        cdir = os.path.join(outdir, kod)\n        chart_dir = os.path.join(cdir, \"grafikler\")\n        os.makedirs(chart_dir, exist_ok=True)\n        rep.fleet_chart(sub_results, os.path.join(chart_dir, \"filo_saglik.png\"))\n        rs_sorted = sorted(sub_results.values(), key=lambda r: r[\"score\"])\n        for r in rs_sorted[:top_device_charts]:\n            p = os.path.join(chart_dir, f\"cihaz_{r['device_id']}.png\")\n            rep.device_chart(daily, r[\"device_id\"], r, p)\n        auds = [\"teknik\", \"vatandas\",\n                \"yonetici_kamu\" if cust[\"tur\"] == \"kamu\" else \"yonetici_ozel\"]\n        if cust[\"tur\"] == \"ic\":\n            auds = [\"teknik\"]\n        files = [build_docx(sub_results, daily, aud,\n                            os.path.join(cdir, f\"rapor_{aud}.docx\"),\n                            chart_dir, customer=cust)\n                 for aud in auds]\n        written[kod] = files\n    return written\n",
   "id": "Zi1DFwT1gQtx"
  },
  {
   "cell_type": "code",
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/"
    },
    "id": "6RSdpYCigQty",
    "executionInfo": {
     "status": "ok",
     "timestamp": 1793476105870,
     "user_tz": -180,
     "elapsed": 4,
     "user": {
      "displayName": "Joseph Volkan ÖZCAN",
      "userId": "11389530967736397480"
     }
    },
    "outputId": "77a9a0a4-511e-45df-c24a-2067295fffc5"
   },
   "execution_count": 7,
   "outputs": [
    {
     "output_type": "stream",
     "name": "stdout",
     "text": [
      "Writing qapu_customers.py\n"
     ]
    }
   ],
   "source": "%%writefile qapu_customers.py\n# -*- coding: utf-8 -*-\n\"\"\"Cihaz → müşteri eşleştirmesi (Cinga AI kurum kaydına göre, 31.10.2026).\"\"\"\n\nCUSTOMERS = {\n    \"KOSKI\": {\n        \"ad\": \"Konya Büyükşehir Belediyesi Su ve Kanalizasyon İdaresi (KOSKİ)\",\n        \"birim\": \"Su Tesisleri Dairesi Başkanlığı\",\n        \"tur\": \"kamu\",\n    },\n    \"KOYUNCU\": {\n        \"ad\": \"Koyuncu A.Ş.\",\n        \"birim\": \"\",\n        \"tur\": \"ozel\",\n    },\n    \"KOP\": {\n        \"ad\": \"KOP (Konya Ovası Projesi)\",\n        \"birim\": \"\",\n        \"tur\": \"kamu\",\n    },\n    \"TEST\": {\n        \"ad\": \"Test / Kurum Atanmamış Cihazlar\",\n        \"birim\": \"\",\n        \"tur\": \"ic\",\n    },\n}\n\nDEVICE_CUSTOMER = {\n    # KOSKİ — 15 cihaz (tümü pompa)\n    \"DE000000F6191870\": \"KOSKI\",  # Ali Taşoluk\n    \"4E000000EAD39270\": \"KOSKI\",  # Yeşildere 2\n    \"1A000000F6356F70\": \"KOSKI\",  # Seyrantepe 2\n    \"6E000000F5F23770\": \"KOSKI\",  # Eğribayat Kuyu\n    \"75000000F5FCEB70\": \"KOSKI\",  # Çatalhöyük 3\n    \"C6000000F6285770\": \"KOSKI\",  # Hayvan Pazarı\n    \"4B000000F5FA7570\": \"KOSKI\",  # Yeşilbahçe 1\n    \"28000000F6231270\": \"KOSKI\",  # Hocacihan Sarayköy\n    \"DD000000F6352970\": \"KOSKI\",  # Hocacihan Sarayköy Pompa\n    \"64000000C466EF70\": \"KOSKI\",  # Meram 2\n    \"45000000F621B470\": \"KOSKI\",  # Akçalar\n    \"18000000F5DB3B70\": \"KOSKI\",  # Orman Bölge\n    \"A6000000F6117F70\": \"KOSKI\",  # Karaaslan Hobi 2\n    \"5A000000F63DF570\": \"KOSKI\",  # 100.Yıl\n    \"BE000000F64BCE70\": \"KOSKI\",  # Şehir Parkı 2\n    # Koyuncu A.Ş. — 2 cihaz\n    \"01000000C39AB270\": \"KOYUNCU\",  # Koyuncu Klor Alkali OG (Endüstriyel)\n    \"4C000000C3A62170\": \"KOYUNCU\",  # Girayhan-3 GES OG (GES)\n    # KOP\n    \"DD000000C41F7670\": \"KOP\",      # KOP - Alibeyhüyüğü Pompası\n    # Test / atanmamış\n    \"400000011D081B70\": \"TEST\",     # Recep Test Cihazı\n    \"C5000000C4965F70\": \"TEST\",     # Sait Test\n    \"CB000000F60B9870\": \"TEST\",     # Cinga 01 (Recep)\n    \"1000000000000001\": \"TEST\",     # Qapu-01\n    \"1000000000000002\": \"TEST\",     # Qapu-02\n    \"1000000000000003\": \"TEST\",     # Qapu-03\n    \"1000000000000004\": \"TEST\",     # Qapu-04\n    \"1000000000000005\": \"TEST\",     # Qapu-05\n}\n\n\ndef customer_of(device_id):\n    return DEVICE_CUSTOMER.get(str(device_id), \"TEST\")\n\n\ndef split_by_customer(results, include_test=False):\n    \"\"\"analyze() çıktısını müşteri koduna göre böler: {musteri_kodu: results_dict}.\"\"\"\n    out = {}\n    for dev, r in results.items():\n        c = customer_of(dev)\n        if c == \"TEST\" and not include_test:\n            continue\n        out.setdefault(c, {})[dev] = r\n    return out\n",
   "id": "6RSdpYCigQty"
  },
  {
   "cell_type": "code",
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 143
    },
    "id": "_DNvsQFPgQty",
    "executionInfo": {
     "status": "ok",
     "timestamp": 1793476682504,
     "user_tz": -180,
     "elapsed": 576635,
     "user": {
      "displayName": "Joseph Volkan ÖZCAN",
      "userId": "11389530967736397480"
     }
    },
    "outputId": "2a1ed7c0-f3d4-488e-8fb1-cf56d5bf235b"
   },
   "execution_count": 8,
   "outputs": [
    {
     "output_type": "display_data",
     "data": {
      "text/plain": [
       "<IPython.core.display.HTML object>"
      ],
      "text/html": [
       "\n",
       "     <input type=\"file\" id=\"files-104506d9-cc53-4d4f-893f-7bac00a97055\" name=\"files[]\" multiple disabled\n",
       "        style=\"border:none\" />\n",
       "     <output id=\"result-104506d9-cc53-4d4f-893f-7bac00a97055\">\n",
       "      Upload widget is only available when the cell has been executed in the\n",
       "      current browser session. Please rerun this cell to enable.\n",
       "      </output>\n",
       "      <script>// Copyright 2017 Google LLC\n",
       "//\n",
       "// Licensed under the Apache License, Version 2.0 (the \"License\");\n",
       "// you may not use this file except in compliance with the License.\n",
       "// You may obtain a copy of the License at\n",
       "//\n",
       "//      http://www.apache.org/licenses/LICENSE-2.0\n",
       "//\n",
       "// Unless required by applicable law or agreed to in writing, software\n",
       "// distributed under the License is distributed on an \"AS IS\" BASIS,\n",
       "// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n",
       "// See the License for the specific language governing permissions and\n",
       "// limitations under the License.\n",
       "\n",
       "/**\n",
       " * @fileoverview Helpers for google.colab Python module.\n",
       " */\n",
       "(function(scope) {\n",
       "function span(text, styleAttributes = {}) {\n",
       "  const element = document.createElement('span');\n",
       "  element.textContent = text;\n",
       "  for (const key of Object.keys(styleAttributes)) {\n",
       "    element.style[key] = styleAttributes[key];\n",
       "  }\n",
       "  return element;\n",
       "}\n",
       "\n",
       "// Max number of bytes which will be uploaded at a time.\n",
       "const MAX_PAYLOAD_SIZE = 100 * 1024;\n",
       "\n",
       "function _uploadFiles(inputId, outputId) {\n",
       "  const steps = uploadFilesStep(inputId, outputId);\n",
       "  const outputElement = document.getElementById(outputId);\n",
       "  // Cache steps on the outputElement to make it available for the next call\n",
       "  // to uploadFilesContinue from Python.\n",
       "  outputElement.steps = steps;\n",
       "\n",
       "  return _uploadFilesContinue(outputId);\n",
       "}\n",
       "\n",
       "// This is roughly an async generator (not supported in the browser yet),\n",
       "// where there are multiple asynchronous steps and the Python side is going\n",
       "// to poll for completion of each step.\n",
       "// This uses a Promise to block the python side on completion of each step,\n",
       "// then passes the result of the previous step as the input to the next step.\n",
       "function _uploadFilesContinue(outputId) {\n",
       "  const outputElement = document.getElementById(outputId);\n",
       "  const steps = outputElement.steps;\n",
       "\n",
       "  const next = steps.next(outputElement.lastPromiseValue);\n",
       "  return Promise.resolve(next.value.promise).then((value) => {\n",
       "    // Cache the last promise value to make it available to the next\n",
       "    // step of the generator.\n",
       "    outputElement.lastPromiseValue = value;\n",
       "    return next.value.response;\n",
       "  });\n",
       "}\n",
       "\n",
       "/**\n",
       " * Generator function which is called between each async step of the upload\n",
       " * process.\n",
       " * @param {string} inputId Element ID of the input file picker element.\n",
       " * @param {string} outputId Element ID of the output display.\n",
       " * @return {!Iterable<!Object>} Iterable of next steps.\n",
       " */\n",
       "function* uploadFilesStep(inputId, outputId) {\n",
       "  const inputElement = document.getElementById(inputId);\n",
       "  inputElement.disabled = false;\n",
       "\n",
       "  const outputElement = document.getElementById(outputId);\n",
       "  outputElement.innerHTML = '';\n",
       "\n",
       "  const pickedPromise = new Promise((resolve) => {\n",
       "    inputElement.addEventListener('change', (e) => {\n",
       "      resolve(e.target.files);\n",
       "    });\n",
       "  });\n",
       "\n",
       "  const cancel = document.createElement('button');\n",
       "  inputElement.parentElement.appendChild(cancel);\n",
       "  cancel.textContent = 'Cancel upload';\n",
       "  const cancelPromise = new Promise((resolve) => {\n",
       "    cancel.onclick = () => {\n",
       "      resolve(null);\n",
       "    };\n",
       "  });\n",
       "\n",
       "  // Wait for the user to pick the files.\n",
       "  const files = yield {\n",
       "    promise: Promise.race([pickedPromise, cancelPromise]),\n",
       "    response: {\n",
       "      action: 'starting',\n",
       "    }\n",
       "  };\n",
       "\n",
       "  cancel.remove();\n",
       "\n",
       "  // Disable the input element since further picks are not allowed.\n",
       "  inputElement.disabled = true;\n",
       "\n",
       "  if (!files) {\n",
       "    return {\n",
       "      response: {\n",
       "        action: 'complete',\n",
       "      }\n",
       "    };\n",
       "  }\n",
       "\n",
       "  for (const file of files) {\n",
       "    const li = document.createElement('li');\n",
       "    li.append(span(file.name, {fontWeight: 'bold'}));\n",
       "    li.append(span(\n",
       "        `(${file.type || 'n/a'}) - ${file.size} bytes, ` +\n",
       "        `last modified: ${\n",
       "            file.lastModifiedDate ? file.lastModifiedDate.toLocaleDateString() :\n",
       "                                    'n/a'} - `));\n",
       "    const percent = span('0% done');\n",
       "    li.appendChild(percent);\n",
       "\n",
       "    outputElement.appendChild(li);\n",
       "\n",
       "    const fileDataPromise = new Promise((resolve) => {\n",
       "      const reader = new FileReader();\n",
       "      reader.onload = (e) => {\n",
       "        resolve(e.target.result);\n",
       "      };\n",
       "      reader.readAsArrayBuffer(file);\n",
       "    });\n",
       "    // Wait for the data to be ready.\n",
       "    let fileData = yield {\n",
       "      promise: fileDataPromise,\n",
       "      response: {\n",
       "        action: 'continue',\n",
       "      }\n",
       "    };\n",
       "\n",
       "    // Use a chunked sending to avoid message size limits. See b/62115660.\n",
       "    let position = 0;\n",
       "    do {\n",
       "      const length = Math.min(fileData.byteLength - position, MAX_PAYLOAD_SIZE);\n",
       "      const chunk = new Uint8Array(fileData, position, length);\n",
       "      position += length;\n",
       "\n",
       "      const base64 = btoa(String.fromCharCode.apply(null, chunk));\n",
       "      yield {\n",
       "        response: {\n",
       "          action: 'append',\n",
       "          file: file.name,\n",
       "          data: base64,\n",
       "        },\n",
       "      };\n",
       "\n",
       "      let percentDone = fileData.byteLength === 0 ?\n",
       "          100 :\n",
       "          Math.round((position / fileData.byteLength) * 100);\n",
       "      percent.textContent = `${percentDone}% done`;\n",
       "\n",
       "    } while (position < fileData.byteLength);\n",
       "  }\n",
       "\n",
       "  // All done.\n",
       "  yield {\n",
       "    response: {\n",
       "      action: 'complete',\n",
       "    }\n",
       "  };\n",
       "}\n",
       "\n",
       "scope.google = scope.google || {};\n",
       "scope.google.colab = scope.google.colab || {};\n",
       "scope.google.colab._files = {\n",
       "  _uploadFiles,\n",
       "  _uploadFilesContinue,\n",
       "};\n",
       "})(self);\n",
       "</script> "
      ]
     },
     "metadata": {}
    },
    {
     "output_type": "stream",
     "name": "stdout",
     "text": [
      "Saving qapu.dump to qapu.dump\n",
      "43 table-data entries to extract\n",
      "extracted 43 data blocks into 13 CSVs: ['devices', 'manufacturer_models', 'measurements_current', 'measurements_device', 'measurements_energy', 'measurements_environment', 'measurements_power', 'measurements_voltage', 'site_device_assignments', 'sites', 'streams', 'variable_segments', 'variables']\n",
      "Çıkarılan CSV'ler: ['devices.csv', 'manufacturer_models.csv', 'measurements_current.csv', 'measurements_device.csv', 'measurements_energy.csv', 'measurements_environment.csv', 'measurements_power.csv', 'measurements_voltage.csv', 'site_device_assignments.csv', 'sites.csv', 'streams.csv', 'variable_segments.csv', 'variables.csv']\n"
     ]
    }
   ],
   "source": "# 6) Veri girişi — qapu.dump dosyasını yükleyin (veya Drive yolu verin)\nimport os\n\nDUMP_PATH = \"\"   # ör. \"/content/drive/MyDrive/qapu.dump\" — boş bırakılırsa yükleme penceresi açılır\nDATA_DIR  = \"data\"\n\nif not DUMP_PATH:\n    try:\n        from google.colab import files\n        up = files.upload()           # qapu.dump seçin\n        DUMP_PATH = list(up)[0]\n    except ImportError:\n        raise SystemExit(\"Colab dışındasınız: DUMP_PATH değişkenine dump yolunu yazın.\")\n\nimport qapu_extract\nqapu_extract.main(DUMP_PATH, DATA_DIR)\nprint(\"Çıkarılan CSV'ler:\", sorted(os.listdir(DATA_DIR)))",
   "id": "_DNvsQFPgQty"
  },
  {
   "cell_type": "code",
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 569
    },
    "id": "vDbDtxDEgQty",
    "executionInfo": {
     "status": "ok",
     "timestamp": 1793476706379,
     "user_tz": -180,
     "elapsed": 23860,
     "user": {
      "displayName": "Joseph Volkan ÖZCAN",
      "userId": "11389530967736397480"
     }
    },
    "outputId": "3e7ff943-a383-4c1d-b1ab-0354e400df2e"
   },
   "execution_count": 9,
   "outputs": [
    {
     "output_type": "stream",
     "name": "stdout",
     "text": [
      "16 cihaz analiz edildi (yeterli verisi olmayanlar atlandı).\n"
     ]
    },
    {
     "output_type": "execute_result",
     "data": {
      "text/plain": [
       "                         Tesis  Gün        kWh  End.% (30g)  Kap.% (30g)  \\\n",
       "0                 Yeşilbahçe 1    6        9.3         78.5          9.9   \n",
       "1            Girayhan-3 GES OG   33      894.6        656.7         10.9   \n",
       "2   KOP - Alibeyhüyüğü Pompası   41       52.3          3.7         45.4   \n",
       "3     Hocacihan Sarayköy Pompa    5     1302.8         56.6          0.2   \n",
       "4            Recep Test Cihazı   55        0.2          3.6         21.4   \n",
       "5               Eğribayat Kuyu    5     2338.1          0.0         62.2   \n",
       "6                      Meram 2   53    36432.8          0.8          8.3   \n",
       "7           Hocacihan Sarayköy    5     1284.0          0.4         59.2   \n",
       "8                      100.Yıl    5      347.6          3.0         10.7   \n",
       "9                Hayvan Pazarı    4     1565.9          0.9          3.9   \n",
       "10               Şehir Parkı 2    4      943.9          0.9          6.4   \n",
       "11                Seyrantepe 2    6     1190.4          0.0         33.5   \n",
       "12      Koyuncu Klor Alkali OG   41  1882792.0         13.9          0.0   \n",
       "13                 Yeşildere 2    6     5060.3          0.2          3.2   \n",
       "14            BE000000F63D0C70   10        1.9          0.4         12.2   \n",
       "15                 Ali Taşoluk    6      779.1          0.3          3.4   \n",
       "\n",
       "       PF  THD-I%  Deng.%   Skor      Durum  Bulgu  \n",
       "0   0.221     9.4    28.6   50.7     RİSKLİ      4  \n",
       "1   0.860     4.3     1.1   62.9     RİSKLİ      3  \n",
       "2   0.098     1.8     2.5   63.2     RİSKLİ      3  \n",
       "3   0.875     2.3     2.7   67.5     RİSKLİ      3  \n",
       "4   0.103     2.1     2.4   72.6  İZLENMELİ      2  \n",
       "5   0.828     4.5     1.0   72.8  İZLENMELİ      3  \n",
       "6   0.814    68.0     4.0   74.8  İZLENMELİ      2  \n",
       "7   0.861     2.3     2.7   76.1  İZLENMELİ      3  \n",
       "8   0.818    61.4     1.4   76.8  İZLENMELİ      3  \n",
       "9   0.871    48.2     1.5   82.1  İZLENMELİ      3  \n",
       "10  0.887    44.9     1.4   83.7  İZLENMELİ      3  \n",
       "11  0.974     4.4    11.6   84.2  İZLENMELİ      3  \n",
       "12  0.977    10.5     0.9   98.2        İYİ      1  \n",
       "13  0.978     4.5     3.7  100.0        İYİ      1  \n",
       "14  0.974     2.6     0.1  100.0        İYİ      3  \n",
       "15  0.973     7.3     2.6  100.0        İYİ      1  "
      ],
      "text/html": [
       "\n",
       "  <div id=\"df-50e3c90f-7fc7-4751-b13a-615906c6cf92\" class=\"colab-df-container\">\n",
       "    <div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>Tesis</th>\n",
       "      <th>Gün</th>\n",
       "      <th>kWh</th>\n",
       "      <th>End.% (30g)</th>\n",
       "      <th>Kap.% (30g)</th>\n",
       "      <th>PF</th>\n",
       "      <th>THD-I%</th>\n",
       "      <th>Deng.%</th>\n",
       "      <th>Skor</th>\n",
       "      <th>Durum</th>\n",
       "      <th>Bulgu</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>Yeşilbahçe 1</td>\n",
       "      <td>6</td>\n",
       "      <td>9.3</td>\n",
       "      <td>78.5</td>\n",
       "      <td>9.9</td>\n",
       "      <td>0.221</td>\n",
       "      <td>9.4</td>\n",
       "      <td>28.6</td>\n",
       "      <td>50.7</td>\n",
       "      <td>RİSKLİ</td>\n",
       "      <td>4</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>Girayhan-3 GES OG</td>\n",
       "      <td>33</td>\n",
       "      <td>894.6</td>\n",
       "      <td>656.7</td>\n",
       "      <td>10.9</td>\n",
       "      <td>0.860</td>\n",
       "      <td>4.3</td>\n",
       "      <td>1.1</td>\n",
       "      <td>62.9</td>\n",
       "      <td>RİSKLİ</td>\n",
       "      <td>3</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>KOP - Alibeyhüyüğü Pompası</td>\n",
       "      <td>41</td>\n",
       "      <td>52.3</td>\n",
       "      <td>3.7</td>\n",
       "      <td>45.4</td>\n",
       "      <td>0.098</td>\n",
       "      <td>1.8</td>\n",
       "      <td>2.5</td>\n",
       "      <td>63.2</td>\n",
       "      <td>RİSKLİ</td>\n",
       "      <td>3</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>Hocacihan Sarayköy Pompa</td>\n",
       "      <td>5</td>\n",
       "      <td>1302.8</td>\n",
       "      <td>56.6</td>\n",
       "      <td>0.2</td>\n",
       "      <td>0.875</td>\n",
       "      <td>2.3</td>\n",
       "      <td>2.7</td>\n",
       "      <td>67.5</td>\n",
       "      <td>RİSKLİ</td>\n",
       "      <td>3</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>Recep Test Cihazı</td>\n",
       "      <td>55</td>\n",
       "      <td>0.2</td>\n",
       "      <td>3.6</td>\n",
       "      <td>21.4</td>\n",
       "      <td>0.103</td>\n",
       "      <td>2.1</td>\n",
       "      <td>2.4</td>\n",
       "      <td>72.6</td>\n",
       "      <td>İZLENMELİ</td>\n",
       "      <td>2</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>Eğribayat Kuyu</td>\n",
       "      <td>5</td>\n",
       "      <td>2338.1</td>\n",
       "      <td>0.0</td>\n",
       "      <td>62.2</td>\n",
       "      <td>0.828</td>\n",
       "      <td>4.5</td>\n",
       "      <td>1.0</td>\n",
       "      <td>72.8</td>\n",
       "      <td>İZLENMELİ</td>\n",
       "      <td>3</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6</th>\n",
       "      <td>Meram 2</td>\n",
       "      <td>53</td>\n",
       "      <td>36432.8</td>\n",
       "      <td>0.8</td>\n",
       "      <td>8.3</td>\n",
       "      <td>0.814</td>\n",
       "      <td>68.0</td>\n",
       "      <td>4.0</td>\n",
       "      <td>74.8</td>\n",
       "      <td>İZLENMELİ</td>\n",
       "      <td>2</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>7</th>\n",
       "      <td>Hocacihan Sarayköy</td>\n",
       "      <td>5</td>\n",
       "      <td>1284.0</td>\n",
       "      <td>0.4</td>\n",
       "      <td>59.2</td>\n",
       "      <td>0.861</td>\n",
       "      <td>2.3</td>\n",
       "      <td>2.7</td>\n",
       "      <td>76.1</td>\n",
       "      <td>İZLENMELİ</td>\n",
       "      <td>3</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>8</th>\n",
       "      <td>100.Yıl</td>\n",
       "      <td>5</td>\n",
       "      <td>347.6</td>\n",
       "      <td>3.0</td>\n",
       "      <td>10.7</td>\n",
       "      <td>0.818</td>\n",
       "      <td>61.4</td>\n",
       "      <td>1.4</td>\n",
       "      <td>76.8</td>\n",
       "      <td>İZLENMELİ</td>\n",
       "      <td>3</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>9</th>\n",
       "      <td>Hayvan Pazarı</td>\n",
       "      <td>4</td>\n",
       "      <td>1565.9</td>\n",
       "      <td>0.9</td>\n",
       "      <td>3.9</td>\n",
       "      <td>0.871</td>\n",
       "      <td>48.2</td>\n",
       "      <td>1.5</td>\n",
       "      <td>82.1</td>\n",
       "      <td>İZLENMELİ</td>\n",
       "      <td>3</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>10</th>\n",
       "      <td>Şehir Parkı 2</td>\n",
       "      <td>4</td>\n",
       "      <td>943.9</td>\n",
       "      <td>0.9</td>\n",
       "      <td>6.4</td>\n",
       "      <td>0.887</td>\n",
       "      <td>44.9</td>\n",
       "      <td>1.4</td>\n",
       "      <td>83.7</td>\n",
       "      <td>İZLENMELİ</td>\n",
       "      <td>3</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>11</th>\n",
       "      <td>Seyrantepe 2</td>\n",
       "      <td>6</td>\n",
       "      <td>1190.4</td>\n",
       "      <td>0.0</td>\n",
       "      <td>33.5</td>\n",
       "      <td>0.974</td>\n",
       "      <td>4.4</td>\n",
       "      <td>11.6</td>\n",
       "      <td>84.2</td>\n",
       "      <td>İZLENMELİ</td>\n",
       "      <td>3</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>12</th>\n",
       "      <td>Koyuncu Klor Alkali OG</td>\n",
       "      <td>41</td>\n",
       "      <td>1882792.0</td>\n",
       "      <td>13.9</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.977</td>\n",
       "      <td>10.5</td>\n",
       "      <td>0.9</td>\n",
       "      <td>98.2</td>\n",
       "      <td>İYİ</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>13</th>\n",
       "      <td>Yeşildere 2</td>\n",
       "      <td>6</td>\n",
       "      <td>5060.3</td>\n",
       "      <td>0.2</td>\n",
       "      <td>3.2</td>\n",
       "      <td>0.978</td>\n",
       "      <td>4.5</td>\n",
       "      <td>3.7</td>\n",
       "      <td>100.0</td>\n",
       "      <td>İYİ</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>14</th>\n",
       "      <td>BE000000F63D0C70</td>\n",
       "      <td>10</td>\n",
       "      <td>1.9</td>\n",
       "      <td>0.4</td>\n",
       "      <td>12.2</td>\n",
       "      <td>0.974</td>\n",
       "      <td>2.6</td>\n",
       "      <td>0.1</td>\n",
       "      <td>100.0</td>\n",
       "      <td>İYİ</td>\n",
       "      <td>3</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>15</th>\n",
       "      <td>Ali Taşoluk</td>\n",
       "      <td>6</td>\n",
       "      <td>779.1</td>\n",
       "      <td>0.3</td>\n",
       "      <td>3.4</td>\n",
       "      <td>0.973</td>\n",
       "      <td>7.3</td>\n",
       "      <td>2.6</td>\n",
       "      <td>100.0</td>\n",
       "      <td>İYİ</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>\n",
       "    <div class=\"colab-df-buttons\">\n",
       "\n",
       "  <div class=\"colab-df-container\">\n",
       "    <button class=\"colab-df-convert\" onclick=\"convertToInteractive('df-50e3c90f-7fc7-4751-b13a-615906c6cf92')\"\n",
       "            title=\"Convert this dataframe to an interactive table.\"\n",
       "            style=\"display:none;\">\n",
       "\n",
       "  <svg xmlns=\"http://www.w3.org/2000/svg\" height=\"24px\" viewBox=\"0 -960 960 960\">\n",
       "    <path d=\"M120-120v-720h720v720H120Zm60-500h600v-160H180v160Zm220 220h160v-160H400v160Zm0 220h160v-160H400v160ZM180-400h160v-160H180v160Zm440 0h160v-160H620v160ZM180-180h160v-160H180v160Zm440 0h160v-160H620v160Z\"/>\n",
       "  </svg>\n",
       "    </button>\n",
       "\n",
       "  <style>\n",
       "    .colab-df-container {\n",
       "      display:flex;\n",
       "      gap: 12px;\n",
       "    }\n",
       "\n",
       "    .colab-df-convert {\n",
       "      background-color: #E8F0FE;\n",
       "      border: none;\n",
       "      border-radius: 50%;\n",
       "      cursor: pointer;\n",
       "      display: none;\n",
       "      fill: #1967D2;\n",
       "      height: 32px;\n",
       "      padding: 0 0 0 0;\n",
       "      width: 32px;\n",
       "    }\n",
       "\n",
       "    .colab-df-convert:hover {\n",
       "      background-color: #E2EBFA;\n",
       "      box-shadow: 0px 1px 2px rgba(60, 64, 67, 0.3), 0px 1px 3px 1px rgba(60, 64, 67, 0.15);\n",
       "      fill: #174EA6;\n",
       "    }\n",
       "\n",
       "    .colab-df-buttons div {\n",
       "      margin-bottom: 4px;\n",
       "    }\n",
       "\n",
       "    [theme=dark] .colab-df-convert {\n",
       "      background-color: #3B4455;\n",
       "      fill: #D2E3FC;\n",
       "    }\n",
       "\n",
       "    [theme=dark] .colab-df-convert:hover {\n",
       "      background-color: #434B5C;\n",
       "      box-shadow: 0px 1px 3px 1px rgba(0, 0, 0, 0.15);\n",
       "      filter: drop-shadow(0px 1px 2px rgba(0, 0, 0, 0.3));\n",
       "      fill: #FFFFFF;\n",
       "    }\n",
       "  </style>\n",
       "\n",
       "    <script>\n",
       "      const buttonEl =\n",
       "        document.querySelector('#df-50e3c90f-7fc7-4751-b13a-615906c6cf92 button.colab-df-convert');\n",
       "      buttonEl.style.display =\n",
       "        google.colab.kernel.accessAllowed ? 'block' : 'none';\n",
       "\n",
       "      async function convertToInteractive(key) {\n",
       "        const element = document.querySelector('#df-50e3c90f-7fc7-4751-b13a-615906c6cf92');\n",
       "        const dataTable =\n",
       "          await google.colab.kernel.invokeFunction('convertToInteractive',\n",
       "                                                    [key], {});\n",
       "        if (!dataTable) return;\n",
       "\n",
       "        const docLinkHtml = 'Like what you see? Visit the ' +\n",
       "          '<a target=\"_blank\" href=https://colab.research.google.com/notebooks/data_table.ipynb>data table notebook</a>'\n",
       "          + ' to learn more about interactive tables.';\n",
       "        element.innerHTML = '';\n",
       "        dataTable['output_type'] = 'display_data';\n",
       "        await google.colab.output.renderOutput(dataTable, element);\n",
       "        const docLink = document.createElement('div');\n",
       "        docLink.innerHTML = docLinkHtml;\n",
       "        element.appendChild(docLink);\n",
       "      }\n",
       "    </script>\n",
       "  </div>\n",
       "\n",
       "\n",
       "  <div id=\"id_023e95fd-6a7d-4866-bd96-9c82fc39be31\">\n",
       "    <style>\n",
       "      .colab-df-generate {\n",
       "        background-color: #E8F0FE;\n",
       "        border: none;\n",
       "        border-radius: 50%;\n",
       "        cursor: pointer;\n",
       "        display: none;\n",
       "        fill: #1967D2;\n",
       "        height: 32px;\n",
       "        padding: 0 0 0 0;\n",
       "        width: 32px;\n",
       "      }\n",
       "\n",
       "      .colab-df-generate:hover {\n",
       "        background-color: #E2EBFA;\n",
       "        box-shadow: 0px 1px 2px rgba(60, 64, 67, 0.3), 0px 1px 3px 1px rgba(60, 64, 67, 0.15);\n",
       "        fill: #174EA6;\n",
       "      }\n",
       "\n",
       "      [theme=dark] .colab-df-generate {\n",
       "        background-color: #3B4455;\n",
       "        fill: #D2E3FC;\n",
       "      }\n",
       "\n",
       "      [theme=dark] .colab-df-generate:hover {\n",
       "        background-color: #434B5C;\n",
       "        box-shadow: 0px 1px 3px 1px rgba(0, 0, 0, 0.15);\n",
       "        filter: drop-shadow(0px 1px 2px rgba(0, 0, 0, 0.3));\n",
       "        fill: #FFFFFF;\n",
       "      }\n",
       "    </style>\n",
       "    <button class=\"colab-df-generate\" onclick=\"generateWithVariable('ozet')\"\n",
       "            title=\"Generate code using this dataframe.\"\n",
       "            style=\"display:none;\">\n",
       "\n",
       "  <svg xmlns=\"http://www.w3.org/2000/svg\" height=\"24px\"viewBox=\"0 0 24 24\"\n",
       "       width=\"24px\">\n",
       "    <path d=\"M7,19H8.4L18.45,9,17,7.55,7,17.6ZM5,21V16.75L18.45,3.32a2,2,0,0,1,2.83,0l1.4,1.43a1.91,1.91,0,0,1,.58,1.4,1.91,1.91,0,0,1-.58,1.4L9.25,21ZM18.45,9,17,7.55Zm-12,3A5.31,5.31,0,0,0,4.9,8.1,5.31,5.31,0,0,0,1,6.5,5.31,5.31,0,0,0,4.9,4.9,5.31,5.31,0,0,0,6.5,1,5.31,5.31,0,0,0,8.1,4.9,5.31,5.31,0,0,0,12,6.5,5.46,5.46,0,0,0,6.5,12Z\"/>\n",
       "  </svg>\n",
       "    </button>\n",
       "    <script>\n",
       "      (() => {\n",
       "      const buttonEl =\n",
       "        document.querySelector('#id_023e95fd-6a7d-4866-bd96-9c82fc39be31 button.colab-df-generate');\n",
       "      buttonEl.style.display =\n",
       "        google.colab.kernel.accessAllowed ? 'block' : 'none';\n",
       "\n",
       "      buttonEl.onclick = () => {\n",
       "        google.colab.notebook.generateWithVariable('ozet');\n",
       "      }\n",
       "      })();\n",
       "    </script>\n",
       "  </div>\n",
       "\n",
       "    </div>\n",
       "  </div>\n"
      ],
      "application/vnd.google.colaboratory.intrinsic+json": {
       "type": "dataframe",
       "variable_name": "ozet",
       "summary": "{\n  \"name\": \"ozet\",\n  \"rows\": 16,\n  \"fields\": [\n    {\n      \"column\": \"Tesis\",\n      \"properties\": {\n        \"dtype\": \"string\",\n        \"num_unique_values\": 16,\n        \"samples\": [\n          \"Ye\\u015filbah\\u00e7e 1\",\n          \"Girayhan-3 GES OG\",\n          \"E\\u011fribayat Kuyu\"\n        ],\n        \"semantic_type\": \"\",\n        \"description\": \"\"\n      }\n    },\n    {\n      \"column\": \"G\\u00fcn\",\n      \"properties\": {\n        \"dtype\": \"number\",\n        \"std\": 19,\n        \"min\": 4,\n        \"max\": 55,\n        \"num_unique_values\": 8,\n        \"samples\": [\n          33,\n          53,\n          6\n        ],\n        \"semantic_type\": \"\",\n        \"description\": \"\"\n      }\n    },\n    {\n      \"column\": \"kWh\",\n      \"properties\": {\n        \"dtype\": \"number\",\n        \"std\": 469912.1104326694,\n        \"min\": 0.2,\n        \"max\": 1882792.0,\n        \"num_unique_values\": 16,\n        \"samples\": [\n          9.3,\n          894.6,\n          2338.1\n        ],\n        \"semantic_type\": \"\",\n        \"description\": \"\"\n      }\n    },\n    {\n      \"column\": \"End.% (30g)\",\n      \"properties\": {\n        \"dtype\": \"number\",\n        \"std\": 163.06185316723634,\n        \"min\": 0.0,\n        \"max\": 656.7,\n        \"num_unique_values\": 13,\n        \"samples\": [\n          0.2,\n          0.9,\n          78.5\n        ],\n        \"semantic_type\": \"\",\n        \"description\": \"\"\n      }\n    },\n    {\n      \"column\": \"Kap.% (30g)\",\n      \"properties\": {\n        \"dtype\": \"number\",\n        \"std\": 20.58577178538614,\n        \"min\": 0.0,\n        \"max\": 62.2,\n        \"num_unique_values\": 16,\n        \"samples\": [\n          9.9,\n          10.9,\n          62.2\n        ],\n        \"semantic_type\": \"\",\n        \"description\": \"\"\n      }\n    },\n    {\n      \"column\": \"PF\",\n      \"properties\": {\n        \"dtype\": \"number\",\n        \"std\": 0.3124765591208403,\n        \"min\": 0.098,\n        \"max\": 0.978,\n        \"num_unique_values\": 15,\n        \"samples\": [\n          0.871,\n          0.974,\n          0.221\n        ],\n        \"semantic_type\": \"\",\n        \"description\": \"\"\n      }\n    },\n    {\n      \"column\": \"THD-I%\",\n      \"properties\": {\n        \"dtype\": \"number\",\n        \"std\": 23.43901217343996,\n        \"min\": 1.8,\n        \"max\": 68.0,\n        \"num_unique_values\": 14,\n        \"samples\": [\n          44.9,\n          10.5,\n          9.4\n        ],\n        \"semantic_type\": \"\",\n        \"description\": \"\"\n      }\n    },\n    {\n      \"column\": \"Deng.%\",\n      \"properties\": {\n        \"dtype\": \"number\",\n        \"std\": 6.995510465053044,\n        \"min\": 0.1,\n        \"max\": 28.6,\n        \"num_unique_values\": 14,\n        \"samples\": [\n          11.6,\n          3.7,\n          28.6\n        ],\n        \"semantic_type\": \"\",\n        \"description\": \"\"\n      }\n    },\n    {\n      \"column\": \"Skor\",\n      \"properties\": {\n        \"dtype\": \"number\",\n        \"std\": 14.838014242703322,\n        \"min\": 50.7,\n        \"max\": 100.0,\n        \"num_unique_values\": 14,\n        \"samples\": [\n          82.1,\n          84.2,\n          50.7\n        ],\n        \"semantic_type\": \"\",\n        \"description\": \"\"\n      }\n    },\n    {\n      \"column\": \"Durum\",\n      \"properties\": {\n        \"dtype\": \"category\",\n        \"num_unique_values\": 3,\n        \"samples\": [\n          \"R\\u0130SKL\\u0130\",\n          \"\\u0130ZLENMEL\\u0130\",\n          \"\\u0130Y\\u0130\"\n        ],\n        \"semantic_type\": \"\",\n        \"description\": \"\"\n      }\n    },\n    {\n      \"column\": \"Bulgu\",\n      \"properties\": {\n        \"dtype\": \"number\",\n        \"std\": 0,\n        \"min\": 1,\n        \"max\": 4,\n        \"num_unique_values\": 4,\n        \"samples\": [\n          3,\n          1,\n          4\n        ],\n        \"semantic_type\": \"\",\n        \"description\": \"\"\n      }\n    }\n  ]\n}"
      }
     },
     "metadata": {},
     "execution_count": 9
    }
   ],
   "source": "# 7) Analiz — günlük özellikler, sağlık skorları, bulgular\nimport pandas as pd, numpy as np\nimport qapu_pm as q\n\ntabs  = q.load_csvs(\"data\")\ndaily = q.build_daily(tabs)\nresults = q.analyze(daily, tabs.get(\"devices\"))\n\nozet = pd.DataFrame([{\n    \"Tesis\": r[\"name\"], \"Gün\": r[\"days\"], \"kWh\": round(r[\"ae_total_kwh\"], 1),\n    \"End.% (30g)\": None if not np.isfinite(r[\"ind_ratio_30g\"] or np.nan) else round(100*r[\"ind_ratio_30g\"], 1),\n    \"Kap.% (30g)\": None if not np.isfinite(r[\"cap_ratio_30g\"] or np.nan) else round(100*r[\"cap_ratio_30g\"], 1),\n    \"PF\": None if not np.isfinite(r[\"pf_loaded\"] or np.nan) else round(r[\"pf_loaded\"], 3),\n    \"THD-I%\": None if not np.isfinite(r[\"thd_i\"] or np.nan) else round(100*r[\"thd_i\"], 1),\n    \"Deng.%\": None if not np.isfinite(r[\"i_imb\"] or np.nan) else round(100*r[\"i_imb\"], 1),\n    \"Skor\": r[\"score\"], \"Durum\": r[\"risk\"], \"Bulgu\": len(r[\"findings\"]),\n} for r in sorted(results.values(), key=lambda x: x[\"score\"])])\nprint(f\"{len(results)} cihaz analiz edildi (yeterli verisi olmayanlar atlandı).\")\nozet",
   "id": "vDbDtxDEgQty"
  },
  {
   "cell_type": "code",
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 246
    },
    "id": "NKWHqe4rgQty",
    "executionInfo": {
     "status": "ok",
     "timestamp": 1793476717710,
     "user_tz": -180,
     "elapsed": 11330,
     "user": {
      "displayName": "Joseph Volkan ÖZCAN",
      "userId": "11389530967736397480"
     }
    },
    "outputId": "83f7a720-38f3-4c1e-80cf-dc5b75525829"
   },
   "execution_count": 10,
   "outputs": [
    {
     "output_type": "stream",
     "name": "stdout",
     "text": [
      "== Koyuncu A.Ş.\n",
      "    raporlar_musteri/KOYUNCU/rapor_teknik.docx\n",
      "    raporlar_musteri/KOYUNCU/rapor_vatandas.docx\n",
      "    raporlar_musteri/KOYUNCU/rapor_yonetici_ozel.docx\n",
      "== Konya Büyükşehir Belediyesi Su ve Kanalizasyon İdaresi (KOSKİ)\n",
      "    raporlar_musteri/KOSKI/rapor_teknik.docx\n",
      "    raporlar_musteri/KOSKI/rapor_vatandas.docx\n",
      "    raporlar_musteri/KOSKI/rapor_yonetici_kamu.docx\n",
      "== KOP (Konya Ovası Projesi)\n",
      "    raporlar_musteri/KOP/rapor_teknik.docx\n",
      "    raporlar_musteri/KOP/rapor_vatandas.docx\n",
      "    raporlar_musteri/KOP/rapor_yonetici_kamu.docx\n",
      "ZIP: raporlar_musteri/qapu_musteri_raporlari.zip\n"
     ]
    },
    {
     "output_type": "display_data",
     "data": {
      "text/plain": [
       "<IPython.core.display.Javascript object>"
      ],
      "application/javascript": [
       "\n",
       "    async function download(id, filename, size) {\n",
       "      if (!google.colab.kernel.accessAllowed) {\n",
       "        return;\n",
       "      }\n",
       "      const div = document.createElement('div');\n",
       "      const label = document.createElement('label');\n",
       "      label.textContent = `Downloading \"${filename}\": `;\n",
       "      div.appendChild(label);\n",
       "      const progress = document.createElement('progress');\n",
       "      progress.max = size;\n",
       "      div.appendChild(progress);\n",
       "      document.body.appendChild(div);\n",
       "\n",
       "      const buffers = [];\n",
       "      let downloaded = 0;\n",
       "\n",
       "      const channel = await google.colab.kernel.comms.open(id);\n",
       "      // Send a message to notify the kernel that we're ready.\n",
       "      channel.send({})\n",
       "\n",
       "      for await (const message of channel.messages) {\n",
       "        // Send a message to notify the kernel that we're ready.\n",
       "        channel.send({})\n",
       "        if (message.buffers) {\n",
       "          for (const buffer of message.buffers) {\n",
       "            buffers.push(buffer);\n",
       "            downloaded += buffer.byteLength;\n",
       "            progress.value = downloaded;\n",
       "          }\n",
       "        }\n",
       "      }\n",
       "      const blob = new Blob(buffers, {type: 'application/binary'});\n",
       "      const a = document.createElement('a');\n",
       "      a.href = window.URL.createObjectURL(blob);\n",
       "      a.download = filename;\n",
       "      div.appendChild(a);\n",
       "      a.click();\n",
       "      div.remove();\n",
       "    }\n",
       "  "
      ]
     },
     "metadata": {}
    },
    {
     "output_type": "display_data",
     "data": {
      "text/plain": [
       "<IPython.core.display.Javascript object>"
      ],
      "application/javascript": [
       "download(\"download_f5e605d3-77a2-4ce5-bfbd-b59ab3e5e0ac\", \"qapu_musteri_raporlari.zip\", 3654406)"
      ]
     },
     "metadata": {}
    }
   ],
   "source": "# 8) MÜŞTERİ BAZLI Word raporları üret ve ZIP olarak indir\n# Her müşteri kendi klasöründe: teknik + vatandaş + (kamu ise yonetici_kamu / özel ise yonetici_ozel)\nimport os, zipfile\nimport qapu_docx as qdocx\nimport qapu_customers as qc\n\nINCLUDE_TEST = False   # test cihazlarını da raporlamak için True yapın\n\nwritten = qdocx.generate_customer_reports(results, daily,\n                                          outdir=\"raporlar_musteri\",\n                                          include_test=INCLUDE_TEST)\nzip_path = \"raporlar_musteri/qapu_musteri_raporlari.zip\"\nwith zipfile.ZipFile(zip_path, \"w\", zipfile.ZIP_DEFLATED) as z:\n    for root, _, fs in os.walk(\"raporlar_musteri\"):\n        for f in fs:\n            p = os.path.join(root, f)\n            if p != zip_path:\n                z.write(p, os.path.relpath(p, \"raporlar_musteri\"))\n\nfor kod, files_ in written.items():\n    print(f\"== {qc.CUSTOMERS[kod]['ad']}\")\n    for f in files_:\n        print(\"   \", f)\nprint(\"ZIP:\", zip_path)\n\ntry:\n    from google.colab import files\n    files.download(zip_path)\nexcept ImportError:\n    pass",
   "id": "NKWHqe4rgQty"
  },
  {
   "cell_type": "code",
   "metadata": {
    "id": "RYBuqepZgQty",
    "executionInfo": {
     "status": "ok",
     "timestamp": 1793476717712,
     "user_tz": -180,
     "elapsed": 1,
     "user": {
      "displayName": "Joseph Volkan ÖZCAN",
      "userId": "11389530967736397480"
     }
    }
   },
   "execution_count": 11,
   "outputs": [],
   "source": "# 8b) (İsteğe bağlı) Tüm filoyu tek raporda toplamak isterseniz\n# import qapu_report as rep\n# zip_path, dosyalar = rep.generate_reports(results, daily, outdir=\"raporlar\")           # MD + HTML\n# docx_dosyalar = qdocx.generate_docx_reports(results, daily, outdir=\"raporlar\")         # Word\npass",
   "id": "RYBuqepZgQty"
  },
  {
   "cell_type": "code",
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 1000
    },
    "id": "xO0g22DWgQtz",
    "executionInfo": {
     "status": "ok",
     "timestamp": 1793476717729,
     "user_tz": -180,
     "elapsed": 13,
     "user": {
      "displayName": "Joseph Volkan ÖZCAN",
      "userId": "11389530967736397480"
     }
    },
    "outputId": "c98ada84-567f-4fd2-98d7-8d6173dbad96"
   },
   "execution_count": 12,
   "outputs": [
    {
     "output_type": "display_data",
     "data": {
      "text/plain": [
       "<IPython.core.display.Markdown object>"
      ],
      "text/markdown": "# Filo Genel Durum — Yönetici Özeti (Kamu)\n*16 cihaz · toplam 1.934.995 kWh aktif enerji · offline narrator ile otomatik üretildi*\n\n**Dağılım:** KRİTİK 0 · RİSKLİ 4 · İZLENMELİ 8 · İYİ 4\n\n**Reaktif ceza riski taşıyan 8 tesis var** — bu, doğrudan faturaya yansıyan ve kompanzasyon bakımı ile tamamen önlenebilir bir kalemdir: Yeşilbahçe 1, Girayhan-3 GES OG, KOP - Alibeyhüyüğü Pompası, Hocacihan Sarayköy Pompa, Recep Test Cihazı, Eğribayat Kuyu, Hocacihan Sarayköy, Seyrantepe 2\n\n| Tesis | Skor | Durum | End. oran | PF | Öncelikli bulgu |\n|---|---|---|---|---|---|\n| Yeşilbahçe 1 | 50.7 | RİSKLİ | %78,5 | 0,22 | REAKTIF_IND_ASIM |\n| Girayhan-3 GES OG | 62.9 | RİSKLİ | %656,7 | 0,86 | REAKTIF_IND_ASIM |\n| KOP - Alibeyhüyüğü Pompası | 63.2 | RİSKLİ | %3,7 | 0,10 | REAKTIF_CAP_ASIM |\n| Hocacihan Sarayköy Pompa | 67.5 | RİSKLİ | %56,6 | 0,87 | REAKTIF_IND_ASIM |\n| Recep Test Cihazı | 72.6 | İZLENMELİ | %3,6 | 0,10 | REAKTIF_CAP_ASIM |\n| Eğribayat Kuyu | 72.8 | İZLENMELİ | %0,0 | 0,83 | REAKTIF_CAP_ASIM |\n| Meram 2 | 74.8 | İZLENMELİ | %0,8 | 0,81 | PF_DUSUK |\n| Hocacihan Sarayköy | 76.1 | İZLENMELİ | %0,4 | 0,86 | REAKTIF_CAP_ASIM |\n| 100.Yıl | 76.8 | İZLENMELİ | %3,0 | 0,82 | PF_DUSUK |\n| Hayvan Pazarı | 82.1 | İZLENMELİ | %0,9 | 0,87 | PF_DUSUK |\n| Şehir Parkı 2 | 83.7 | İZLENMELİ | %0,9 | 0,89 | PF_DUSUK |\n| Seyrantepe 2 | 84.2 | İZLENMELİ | %0,0 | 0,97 | REAKTIF_CAP_ASIM |\n| Koyuncu Klor Alkali OG | 98.2 | İYİ | %13,9 | 0,98 | KALIBRASYON |\n| Yeşildere 2 | 100.0 | İYİ | %0,2 | 0,98 | VERI_AZ |\n| BE000000F63D0C70 | 100.0 | İYİ | %0,4 | 0,97 | REAKTIF_CAP_YAKIN |\n| Ali Taşoluk | 100.0 | İYİ | %0,3 | 0,97 | VERI_AZ |\n\n**Önerilen öncelik sırası:** 1) Yeşilbahçe 1 → 2) Girayhan-3 GES OG → 3) KOP - Alibeyhüyüğü Pompası → 4) Hocacihan Sarayköy Pompa\n"
     },
     "metadata": {}
    },
    {
     "output_type": "display_data",
     "data": {
      "text/plain": [
       "<IPython.core.display.Markdown object>"
      ],
      "text/markdown": "## Yeşilbahçe 1 — Yönetici Özeti (Kamu)\n*Dönem: 2026-10-26 → 2026-10-31 (6 gün) · Rapor: otomatik üretim (offline narrator)*\n\n**Durum: RİSKLİ (skor 50.7/100)** — dönem tüketimi 9 kWh. Kamu hizmet sürekliliği önceliklidir.\n\n**Bulgular**\n- [Acil] Tesis, elektrik mevzuatındaki reaktif enerji sınırını (%20) aşmaktadır (ölçülen %78,5). Bu, elektrik faturasına yansıyan ve önlenebilir bir kamu kaynağı kaybıdır; kompanzasyon sisteminin kontrolü gerekir.\n- [Acil] Tesiste güç faktörü 0,22 ölçülmüştür (hedef 0,95). Enerji verimliliği düşüktür; iyileştirme, işletme giderini kalıcı olarak azaltır.\n- [Acil] Tesiste faz dengesizliği %28,6 ölçülmüştür. Pompanın/motorun beklenmedik arızasını önlemek için planlı bakım önerilir; hizmet kesintisi riski azaltılır.\n- [Not] İzleme yeni başlamıştır (6 gün); değerlendirme güncellenecektir.\n\n**Önerilen aksiyonlar**\n1. Kompanzasyon bakımı/yatırımı için teknik ekibe iş emri açılması (tipik geri ödeme süresi aylar mertebesinde).\n2. Verimlilik iyileştirmesi: pompa çalışma programı ve kompanzasyon birlikte gözden geçirilmeli.\n3. Planlı motor/pano kontrolü — plansız duruş riskine karşı en yüksek öncelik.\n"
     },
     "metadata": {}
    }
   ],
   "source": "# 9) (İsteğe bağlı) Tek bir raporu not defteri içinde görüntüle\nfrom IPython.display import Markdown, display\nimport qapu_narrator as nar\n\nKITLE = \"yonetici_kamu\"   # teknik | yonetici_ozel | yonetici_kamu | vatandas\ndisplay(Markdown(nar.fleet_report(results, KITLE)))\n\n# En riskli cihazın raporu:\nen_riskli = min(results.values(), key=lambda r: r[\"score\"])\ndisplay(Markdown(nar.device_report(en_riskli, KITLE)))",
   "id": "xO0g22DWgQtz"
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "DTOrt7PBgQtz"
   },
   "source": "---\n### Yöntem notları\n* **Reaktif oranlar:** Σ(reaktif Wh) / Σ(aktif Wh), cihaz×gün bazında; \"son 30 gün\" oranı\n  EPDK sınırlarıyla (end. %20 / kap. %15) karşılaştırılır. Negatif enerji örnekleri geçersiz sayılır.\n* **Güç faktörü:** yalnızca yük altındayken (S > cihazın p95 değerinin %5'i) ortalanır —\n  pompa dururken ölçülen anlamsız PF değerleri skoru bozmaz.\n* **THD:** √(h3²+h5²+h7²+h9²)/temel bileşen; faz ortalaması. **Dengesizlik:** max|faz−ort|/ort.\n* **Kalibrasyon farkındalığı:** VRMS medyanı 120–300 V dışındaysa cihaz \"kalibrasyonsuz\" işaretlenir;\n  oransal metrikler (PF, THD, dengesizlik, reaktif oran) kalibrasyondan bağımsız olduğu için yine raporlanır.\n* **Trend/projeksiyon:** günlük serilere doğrusal eğim; sınıra ulaşma süresi ≤90 gün ise bulgu üretilir.\n* **Anomali:** IsolationForest (contamination 0.08), ≥10 günlük cihazlarda, standartlaştırılmış günlük özellik uzayında.\n* Eşikler `qapu_pm.CONFIG` sözlüğünden değiştirilebilir.\n",
   "id": "DTOrt7PBgQtz"
  }
 ]
}