# -*- coding: utf-8 -*-
"""Moving ceilings: Out-of-sample evidence on growth-curve forecasts of national wind and solar power diffusion

GOMD arastirma makalesi 02 - makaledeki tum sayilari, tablolari ve sekilleri ureten kod (Google Colab surumu).
Colab: kodu tek bir hucreye yapistirip calistirin ya da .ipynb dosyasini Dosya > Not defteri yukle ile acin.
Kaynak: GOMD acik veri platformu (https://gomd.pirireis.edu.tr) - Piri Reis Universitesi ve
T.C. Cevre, Sehircilik ve Iklim Degisikligi Bakanligi Cevre Yonetimi Genel Mudurlugu.
"""

# # Moving ceilings: Out-of-sample evidence on growth-curve forecasts of national wind and solar power diffusion
#
# Bu not defteri makaledeki **tüm sayıları, tabloları ve şekilleri** üreten kodun Colab sürümüdür (makalenin replikasyon paketindeki betiklerle aynı kod, hücrelere bölünmüş hâlde).
#
# **Çalıştırma:** Çalışma zamanı → Tümünü çalıştır (*Runtime → Run all*). Tahmini süre: 20–35 dakika (13.440 tahmin, çoklu başlangıç değerli eğri uydurma).
#
# **Veri:** Varsayılan olarak veriler GOMD açık veri API'sinden canlı çekilir (`VERI_KAYNAGI = "api"`). Makaledeki sayıları birebir yeniden üretmek için replikasyon paketi ZIP'ini yükleyebilirsiniz (`VERI_KAYNAGI = "zip"`): API'deki seriler kaynak kurumlar tarafından güncellenirse sonuçlar küçük farklar gösterebilir. Veri çekim tarihi makalede 8 Kasım 2026'dır.
#
# Şekiller hem hücre altında görünür hem `fig/` klasörüne, sayısal sonuçlar `out/` klasörüne yazılır. Son hücre hepsini ZIP olarak indirir.

# ## 0. Kurulum

# Installs the packages used in the paper and the GOMD Python client. If Colab asks to restart the session, restart and run all cells again.
# [Colab Magic] !pip install -q statsmodels scipy
# [Colab Magic] !pip install -q --upgrade gomd

VERI_KAYNAGI = "api"   # "api": live from GOMD; "zip": upload the replication package ZIP (exact reproduction)
import os
os.makedirs("/content/rp02", exist_ok=True); os.chdir("/content/rp02")
for d in ("data", "out", "fig"): os.makedirs(d, exist_ok=True)
if VERI_KAYNAGI == "zip":
    import zipfile, glob, shutil
    from google.colab import files
    up = files.upload()                      # choose *_replication_package.zip
    with zipfile.ZipFile(next(iter(up))) as z:
        for n in z.namelist():
            if n.startswith("data/"): z.extract(n, ".")
    print(sorted(os.listdir("data")))
print("Calisma klasoru:", os.getcwd())

# ## 1. Veri

# Data from the public GOMD open data API (entity lists as used in the paper). Requests run in a few parallel threads with retries.
import json, time
from concurrent.futures import ThreadPoolExecutor
import pandas as pd
from gomd import GOMDClient
client = GOMDClient("https://gomd.pirireis.edu.tr")
JOBS = [["ember_wind_electricity_generation_twh", "wind"], ["ember_solar_electricity_generation_twh", "solar"]]
ENTITIES = json.loads("{\"ember_wind_electricity_generation_twh\": [[\"TUR\", \"Turkiye\", \"country\"], [\"AFG\", \"Afghanistan\", \"country\"], [\"ALB\", \"Albania\", \"country\"], [\"DZA\", \"Algeria\", \"country\"], [\"DEU\", \"Germany\", \"country\"], [\"ASM\", \"American Samoa\", \"country\"], [\"AGO\", \"Angola\", \"country\"], [\"ATG\", \"Antigua and Barbuda\", \"country\"], [\"ARG\", \"Argentina\", \"country\"], [\"ARM\", \"Armenia\", \"country\"], [\"ABW\", \"Aruba\", \"country\"], [\"AUS\", \"Australia\", \"country\"], [\"AUT\", \"Austria\", \"country\"], [\"EUU\", \"European Union\", \"aggregate\"], [\"AZE\", \"Azerbaijan\", \"country\"], [\"BHS\", \"Bahamas, The\", \"country\"], [\"BHR\", \"Bahrain\", \"country\"], [\"BGD\", \"Bangladesh\", \"country\"], [\"BRB\", \"Barbados\", \"country\"], [\"BLR\", \"Belarus\", \"country\"], [\"BEL\", \"Belgium\", \"country\"], [\"BLZ\", \"Belize\", \"country\"], [\"BEN\", \"Benin\", \"country\"], [\"BMU\", \"Bermuda\", \"country\"], [\"BTN\", \"Bhutan\", \"country\"], [\"BOL\", \"Bolivia\", \"country\"], [\"BIH\", \"Bosnia and Herzegovina\", \"country\"], [\"BWA\", \"Botswana\", \"country\"], [\"BRA\", \"Brazil\", \"country\"], [\"VGB\", \"British Virgin Islands\", \"country\"], [\"BRN\", \"Brunei Darussalam\", \"country\"], [\"BGR\", \"Bulgaria\", \"country\"], [\"BFA\", \"Burkina Faso\", \"country\"], [\"BDI\", \"Burundi\", \"country\"], [\"CPV\", \"Cabo Verde\", \"country\"], [\"CMR\", \"Cameroon\", \"country\"], [\"CAN\", \"Canada\", \"country\"], [\"CYM\", \"Cayman Islands\", \"country\"], [\"CAF\", \"Central African Republic\", \"country\"], [\"TCD\", \"Chad\", \"country\"], [\"CHL\", \"Chile\", \"country\"], [\"TWN\", \"Chinese Taipei\", \"country\"], [\"CHN\", \"China\", \"country\"], [\"COL\", \"Colombia\", \"country\"], [\"COM\", \"Comoros\", \"country\"], [\"COG\", \"Congo, Rep.\", \"country\"], [\"COK\", \"Cook Islands\", \"country\"], [\"CRI\", \"Costa Rica\", \"country\"], [\"CIV\", \"Cote d'Ivoire\", \"country\"], [\"HRV\", \"Croatia\", \"country\"], [\"CUB\", \"Cuba\", \"country\"], [\"CYP\", \"Greek Cypriot Administration of Southern Cyprus (GCA)\", \"country\"], [\"CZE\", \"Czechia\", \"country\"], [\"COD\", \"Democratic Republic of Congo\", \"country\"], [\"DNK\", \"Denmark\", \"country\"], [\"DJI\", \"Djibouti\", \"country\"], [\"DMA\", \"Dominica\", \"country\"], [\"DOM\", \"Dominican Republic\", \"country\"], [\"WLD\", \"World (Global)\", \"aggregate\"], [\"ECU\", \"Ecuador\", \"country\"], [\"EGY\", \"Egypt\", \"country\"], [\"SLV\", \"El Salvador\", \"country\"], [\"GNQ\", \"Equatorial Guinea\", \"country\"], [\"ERI\", \"Eritrea\", \"country\"], [\"EST\", \"Estonia\", \"country\"], [\"SWZ\", \"Eswatini\", \"country\"], [\"ETH\", \"Ethiopia\", \"country\"], [\"FLK\", \"Falkland Islands (Malvinas)\", \"country\"], [\"FRO\", \"Faroe Islands\", \"country\"], [\"FJI\", \"Fiji\", \"country\"], [\"FIN\", \"Finland\", \"country\"], [\"FRA\", \"France\", \"country\"], [\"PYF\", \"French Polynesia\", \"country\"], [\"GAB\", \"Gabon\", \"country\"], [\"GMB\", \"Gambia, The\", \"country\"], [\"GEO\", \"Georgia\", \"country\"], [\"GHA\", \"Ghana\", \"country\"], [\"GIB\", \"Gibraltar\", \"country\"], [\"GRL\", \"Greenland\", \"country\"], [\"GRD\", \"Grenada\", \"country\"], [\"GLP\", \"Guadeloupe\", \"country\"], [\"GUM\", \"Guam\", \"country\"], [\"GTM\", \"Guatemala\", \"country\"], [\"GIN\", \"Guinea\", \"country\"], [\"GNB\", \"Guinea-Bissau\", \"country\"], [\"GUY\", \"Guyana\", \"country\"], [\"HTI\", \"Haiti\", \"country\"], [\"HND\", \"Honduras\", \"country\"], [\"HKG\", \"Hong Kong SAR, China\", \"country\"], [\"HUN\", \"Hungary\", \"country\"], [\"ISL\", \"Iceland\", \"country\"], [\"IND\", \"India\", \"country\"], [\"IDN\", \"Indonesia\", \"country\"], [\"IRN\", \"Iran, Islamic Rep.\", \"country\"], [\"IRQ\", \"Iraq\", \"country\"], [\"IRL\", \"Ireland\", \"country\"], [\"ESP\", \"Spain\", \"country\"], [\"ISR\", \"Israel\", \"country\"], [\"ITA\", \"Italy\", \"country\"], [\"JAM\", \"Jamaica\", \"country\"], [\"JPN\", \"Japan\", \"country\"], [\"JOR\", \"Jordan\", \"country\"], [\"KAZ\", \"Kazakhstan\", \"country\"], [\"KEN\", \"Kenya\", \"country\"], [\"KIR\", \"Kiribati\", \"country\"], [\"KOR\", \"Korea Rep\", \"country\"], [\"PRK\", \"Korea, Dem. People's Rep.\", \"country\"], [\"KWT\", \"Kuwait\", \"country\"], [\"KGZ\", \"Kyrgyzstan\", \"country\"], [\"LAO\", \"Laos\", \"country\"], [\"LVA\", \"Latvia\", \"country\"], [\"LBN\", \"Lebanon\", \"country\"], [\"LSO\", \"Lesotho\", \"country\"], [\"LBR\", \"Liberia\", \"country\"], [\"LBY\", \"Libya\", \"country\"], [\"LTU\", \"Lithuania\", \"country\"], [\"LUX\", \"Luxembourg\", \"country\"], [\"MAC\", \"Macao SAR, China\", \"country\"], [\"MDG\", \"Madagascar\", \"country\"], [\"MWI\", \"Malawi\", \"country\"], [\"MYS\", \"Malaysia\", \"country\"], [\"MDV\", \"Maldives\", \"country\"], [\"MLI\", \"Mali\", \"country\"], [\"MLT\", \"Malta\", \"country\"], [\"MTQ\", \"Martinique\", \"country\"], [\"MRT\", \"Mauritania\", \"country\"], [\"MUS\", \"Mauritius\", \"country\"], [\"MEX\", \"Mexico\", \"country\"], [\"MDA\", \"Moldova\", \"country\"], [\"MNG\", \"Mongolia\", \"country\"], [\"MNE\", \"Montenegro\", \"country\"], [\"MSR\", \"Montserrat\", \"country\"], [\"MAR\", \"Morocco\", \"country\"], [\"MOZ\", \"Mozambique\", \"country\"], [\"MMR\", \"Myanmar\", \"country\"], [\"NAM\", \"Namibia\", \"country\"], [\"NRU\", \"Nauru\", \"country\"], [\"NPL\", \"Nepal\", \"country\"], [\"NLD\", \"Netherlands\", \"country\"], [\"NCL\", \"New Caledonia\", \"country\"], [\"NZL\", \"New Zealand\", \"country\"], [\"NIC\", \"Nicaragua\", \"country\"], [\"NER\", \"Niger\", \"country\"], [\"NGA\", \"Nigeria\", \"country\"], [\"NIU\", \"Niue\", \"country\"], [\"MKD\", \"North Macedonia\", \"country\"], [\"NOR\", \"Norway\", \"country\"], [\"OMN\", \"Oman\", \"country\"], [\"PAK\", \"Pakistan\", \"country\"], [\"PAN\", \"Panama\", \"country\"], [\"PNG\", \"Papua New Guinea\", \"country\"], [\"PRY\", \"Paraguay\", \"country\"], [\"PER\", \"Peru\", \"country\"], [\"PHL\", \"Philippines\", \"country\"], [\"POL\", \"Poland\", \"country\"], [\"PRT\", \"Portugal\", \"country\"], [\"PRI\", \"Puerto Rico\", \"country\"], [\"QAT\", \"Qatar\", \"country\"], [\"REU\", \"R\u00e9union\", \"country\"], [\"ROU\", \"Romania\", \"country\"], [\"RUS\", \"Russian Federation\", \"country\"], [\"RWA\", \"Rwanda\", \"country\"], [\"SHN\", \"Saint Helena\", \"country\"], [\"SPM\", \"Saint Pierre and Miquelon\", \"country\"], [\"WSM\", \"Samoa\", \"country\"], [\"STP\", \"Sao Tome and Principe\", \"country\"], [\"SAU\", \"Saudi Arabia\", \"country\"], [\"SEN\", \"Senegal\", \"country\"], [\"SRB\", \"Serbia\", \"country\"], [\"SYC\", \"Seychelles\", \"country\"], [\"SLE\", \"Sierra Leone\", \"country\"], [\"SGP\", \"Singapore\", \"country\"], [\"SVK\", \"Slovakia\", \"country\"], [\"SVN\", \"Slovenia\", \"country\"], [\"SLB\", \"Solomon Islands\", \"country\"], [\"SOM\", \"Somalia\", \"country\"], [\"ZAF\", \"South Africa\", \"country\"], [\"SSD\", \"South Sudan\", \"country\"], [\"LKA\", \"Sri Lanka\", \"country\"], [\"KNA\", \"St. Kitts and Nevis\", \"country\"], [\"LCA\", \"St. Lucia\", \"country\"], [\"VCT\", \"St. Vincent and the Grenadines\", \"country\"], [\"SDN\", \"Sudan\", \"country\"], [\"SUR\", \"Suriname\", \"country\"], [\"SWE\", \"Sweden\", \"country\"], [\"CHE\", \"Switzerland\", \"country\"], [\"SYR\", \"Syrian Arab Republic\", \"country\"], [\"TJK\", \"Tajikistan\", \"country\"], [\"TZA\", \"Tanzania\", \"country\"], [\"THA\", \"Thailand\", \"country\"], [\"TLS\", \"Timor-Leste\", \"country\"], [\"TGO\", \"Togo\", \"country\"], [\"TON\", \"Tonga\", \"country\"], [\"TTO\", \"Trinidad and Tobago\", \"country\"], [\"TUN\", \"Tunisia\", \"country\"], [\"TKM\", \"Turkmenistan\", \"country\"], [\"TCA\", \"Turks and Caicos Islands\", \"country\"], [\"UGA\", \"Uganda\", \"country\"], [\"GBR\", \"UK\", \"country\"], [\"UKR\", \"Ukraine\", \"country\"], [\"ARE\", \"United Arab Emirates\", \"country\"], [\"VIR\", \"United States Virgin Islands\", \"country\"], [\"URY\", \"Uruguay\", \"country\"], [\"USA\", \"USA\", \"country\"], [\"UZB\", \"Uzbekistan\", \"country\"], [\"VUT\", \"Vanuatu\", \"country\"], [\"VEN\", \"Venezuela, RB\", \"country\"], [\"VNM\", \"Vietnam\", \"country\"], [\"PSE\", \"West Bank and Gaza\", \"country\"], [\"ESH\", \"Western Sahara\", \"country\"], [\"YEM\", \"Yemen\", \"country\"], [\"GRC\", \"Greece\", \"country\"], [\"ZMB\", \"Zambia\", \"country\"], [\"ZWE\", \"Zimbabwe\", \"country\"]], \"ember_solar_electricity_generation_twh\": [[\"TUR\", \"Turkiye\", \"country\"], [\"AFG\", \"Afghanistan\", \"country\"], [\"ALB\", \"Albania\", \"country\"], [\"DZA\", \"Algeria\", \"country\"], [\"DEU\", \"Germany\", \"country\"], [\"ASM\", \"American Samoa\", \"country\"], [\"AGO\", \"Angola\", \"country\"], [\"ATG\", \"Antigua and Barbuda\", \"country\"], [\"ARG\", \"Argentina\", \"country\"], [\"ARM\", \"Armenia\", \"country\"], [\"ABW\", \"Aruba\", \"country\"], [\"AUS\", \"Australia\", \"country\"], [\"AUT\", \"Austria\", \"country\"], [\"EUU\", \"European Union\", \"aggregate\"], [\"AZE\", \"Azerbaijan\", \"country\"], [\"BHS\", \"Bahamas, The\", \"country\"], [\"BHR\", \"Bahrain\", \"country\"], [\"BGD\", \"Bangladesh\", \"country\"], [\"BRB\", \"Barbados\", \"country\"], [\"BLR\", \"Belarus\", \"country\"], [\"BEL\", \"Belgium\", \"country\"], [\"BLZ\", \"Belize\", \"country\"], [\"BEN\", \"Benin\", \"country\"], [\"BMU\", \"Bermuda\", \"country\"], [\"BTN\", \"Bhutan\", \"country\"], [\"BOL\", \"Bolivia\", \"country\"], [\"BIH\", \"Bosnia and Herzegovina\", \"country\"], [\"BWA\", \"Botswana\", \"country\"], [\"BRA\", \"Brazil\", \"country\"], [\"VGB\", \"British Virgin Islands\", \"country\"], [\"BRN\", \"Brunei Darussalam\", \"country\"], [\"BGR\", \"Bulgaria\", \"country\"], [\"BFA\", \"Burkina Faso\", \"country\"], [\"BDI\", \"Burundi\", \"country\"], [\"CPV\", \"Cabo Verde\", \"country\"], [\"KHM\", \"Cambodia\", \"country\"], [\"CMR\", \"Cameroon\", \"country\"], [\"CAN\", \"Canada\", \"country\"], [\"CYM\", \"Cayman Islands\", \"country\"], [\"CAF\", \"Central African Republic\", \"country\"], [\"TCD\", \"Chad\", \"country\"], [\"CHL\", \"Chile\", \"country\"], [\"TWN\", \"Chinese Taipei\", \"country\"], [\"CHN\", \"China\", \"country\"], [\"COL\", \"Colombia\", \"country\"], [\"COM\", \"Comoros\", \"country\"], [\"COG\", \"Congo, Rep.\", \"country\"], [\"COK\", \"Cook Islands\", \"country\"], [\"CRI\", \"Costa Rica\", \"country\"], [\"CIV\", \"Cote d'Ivoire\", \"country\"], [\"HRV\", \"Croatia\", \"country\"], [\"CUB\", \"Cuba\", \"country\"], [\"CYP\", \"Greek Cypriot Administration of Southern Cyprus (GCA)\", \"country\"], [\"CZE\", \"Czechia\", \"country\"], [\"COD\", \"Democratic Republic of Congo\", \"country\"], [\"DNK\", \"Denmark\", \"country\"], [\"DJI\", \"Djibouti\", \"country\"], [\"DMA\", \"Dominica\", \"country\"], [\"DOM\", \"Dominican Republic\", \"country\"], [\"WLD\", \"World (Global)\", \"aggregate\"], [\"ECU\", \"Ecuador\", \"country\"], [\"EGY\", \"Egypt\", \"country\"], [\"SLV\", \"El Salvador\", \"country\"], [\"GNQ\", \"Equatorial Guinea\", \"country\"], [\"ERI\", \"Eritrea\", \"country\"], [\"EST\", \"Estonia\", \"country\"], [\"SWZ\", \"Eswatini\", \"country\"], [\"ETH\", \"Ethiopia\", \"country\"], [\"FLK\", \"Falkland Islands (Malvinas)\", \"country\"], [\"FRO\", \"Faroe Islands\", \"country\"], [\"FJI\", \"Fiji\", \"country\"], [\"FIN\", \"Finland\", \"country\"], [\"FRA\", \"France\", \"country\"], [\"GUF\", \"French Guiana\", \"country\"], [\"PYF\", \"French Polynesia\", \"country\"], [\"GAB\", \"Gabon\", \"country\"], [\"GMB\", \"Gambia, The\", \"country\"], [\"GEO\", \"Georgia\", \"country\"], [\"GHA\", \"Ghana\", \"country\"], [\"GIB\", \"Gibraltar\", \"country\"], [\"GRL\", \"Greenland\", \"country\"], [\"GRD\", \"Grenada\", \"country\"], [\"GLP\", \"Guadeloupe\", \"country\"], [\"GUM\", \"Guam\", \"country\"], [\"GTM\", \"Guatemala\", \"country\"], [\"GIN\", \"Guinea\", \"country\"], [\"GNB\", \"Guinea-Bissau\", \"country\"], [\"GUY\", \"Guyana\", \"country\"], [\"HTI\", \"Haiti\", \"country\"], [\"HND\", \"Honduras\", \"country\"], [\"HKG\", \"Hong Kong SAR, China\", \"country\"], [\"HUN\", \"Hungary\", \"country\"], [\"ISL\", \"Iceland\", \"country\"], [\"IND\", \"India\", \"country\"], [\"IDN\", \"Indonesia\", \"country\"], [\"IRN\", \"Iran, Islamic Rep.\", \"country\"], [\"IRQ\", \"Iraq\", \"country\"], [\"IRL\", \"Ireland\", \"country\"], [\"ESP\", \"Spain\", \"country\"], [\"ISR\", \"Israel\", \"country\"], [\"ITA\", \"Italy\", \"country\"], [\"JAM\", \"Jamaica\", \"country\"], [\"JPN\", \"Japan\", \"country\"], [\"JOR\", \"Jordan\", \"country\"], [\"KAZ\", \"Kazakhstan\", \"country\"], [\"KEN\", \"Kenya\", \"country\"], [\"KIR\", \"Kiribati\", \"country\"], [\"KOR\", \"Korea Rep\", \"country\"], [\"PRK\", \"Korea, Dem. People's Rep.\", \"country\"], [\"KWT\", \"Kuwait\", \"country\"], [\"KGZ\", \"Kyrgyzstan\", \"country\"], [\"LAO\", \"Laos\", \"country\"], [\"LVA\", \"Latvia\", \"country\"], [\"LBN\", \"Lebanon\", \"country\"], [\"LSO\", \"Lesotho\", \"country\"], [\"LBR\", \"Liberia\", \"country\"], [\"LBY\", \"Libya\", \"country\"], [\"LTU\", \"Lithuania\", \"country\"], [\"LUX\", \"Luxembourg\", \"country\"], [\"MAC\", \"Macao SAR, China\", \"country\"], [\"MDG\", \"Madagascar\", \"country\"], [\"MWI\", \"Malawi\", \"country\"], [\"MYS\", \"Malaysia\", \"country\"], [\"MDV\", \"Maldives\", \"country\"], [\"MLI\", \"Mali\", \"country\"], [\"MLT\", \"Malta\", \"country\"], [\"MTQ\", \"Martinique\", \"country\"], [\"MRT\", \"Mauritania\", \"country\"], [\"MUS\", \"Mauritius\", \"country\"], [\"MEX\", \"Mexico\", \"country\"], [\"MDA\", \"Moldova\", \"country\"], [\"MNG\", \"Mongolia\", \"country\"], [\"MNE\", \"Montenegro\", \"country\"], [\"MSR\", \"Montserrat\", \"country\"], [\"MAR\", \"Morocco\", \"country\"], [\"MOZ\", \"Mozambique\", \"country\"], [\"MMR\", \"Myanmar\", \"country\"], [\"NAM\", \"Namibia\", \"country\"], [\"NRU\", \"Nauru\", \"country\"], [\"NPL\", \"Nepal\", \"country\"], [\"NLD\", \"Netherlands\", \"country\"], [\"NCL\", \"New Caledonia\", \"country\"], [\"NZL\", \"New Zealand\", \"country\"], [\"NIC\", \"Nicaragua\", \"country\"], [\"NER\", \"Niger\", \"country\"], [\"NGA\", \"Nigeria\", \"country\"], [\"NIU\", \"Niue\", \"country\"], [\"MKD\", \"North Macedonia\", \"country\"], [\"NOR\", \"Norway\", \"country\"], [\"OMN\", \"Oman\", \"country\"], [\"PAK\", \"Pakistan\", \"country\"], [\"PAN\", \"Panama\", \"country\"], [\"PNG\", \"Papua New Guinea\", \"country\"], [\"PRY\", \"Paraguay\", \"country\"], [\"PER\", \"Peru\", \"country\"], [\"PHL\", \"Philippines\", \"country\"], [\"POL\", \"Poland\", \"country\"], [\"PRT\", \"Portugal\", \"country\"], [\"PRI\", \"Puerto Rico\", \"country\"], [\"QAT\", \"Qatar\", \"country\"], [\"REU\", \"R\u00e9union\", \"country\"], [\"ROU\", \"Romania\", \"country\"], [\"RUS\", \"Russian Federation\", \"country\"], [\"RWA\", \"Rwanda\", \"country\"], [\"SHN\", \"Saint Helena\", \"country\"], [\"SPM\", \"Saint Pierre and Miquelon\", \"country\"], [\"WSM\", \"Samoa\", \"country\"], [\"STP\", \"Sao Tome and Principe\", \"country\"], [\"SAU\", \"Saudi Arabia\", \"country\"], [\"SEN\", \"Senegal\", \"country\"], [\"SRB\", \"Serbia\", \"country\"], [\"SYC\", \"Seychelles\", \"country\"], [\"SLE\", \"Sierra Leone\", \"country\"], [\"SGP\", \"Singapore\", \"country\"], [\"SVK\", \"Slovakia\", \"country\"], [\"SVN\", \"Slovenia\", \"country\"], [\"SLB\", \"Solomon Islands\", \"country\"], [\"SOM\", \"Somalia\", \"country\"], [\"ZAF\", \"South Africa\", \"country\"], [\"SSD\", \"South Sudan\", \"country\"], [\"LKA\", \"Sri Lanka\", \"country\"], [\"KNA\", \"St. Kitts and Nevis\", \"country\"], [\"LCA\", \"St. Lucia\", \"country\"], [\"VCT\", \"St. Vincent and the Grenadines\", \"country\"], [\"SDN\", \"Sudan\", \"country\"], [\"SUR\", \"Suriname\", \"country\"], [\"SWE\", \"Sweden\", \"country\"], [\"CHE\", \"Switzerland\", \"country\"], [\"SYR\", \"Syrian Arab Republic\", \"country\"], [\"TJK\", \"Tajikistan\", \"country\"], [\"TZA\", \"Tanzania\", \"country\"], [\"THA\", \"Thailand\", \"country\"], [\"TLS\", \"Timor-Leste\", \"country\"], [\"TGO\", \"Togo\", \"country\"], [\"TON\", \"Tonga\", \"country\"], [\"TTO\", \"Trinidad and Tobago\", \"country\"], [\"TUN\", \"Tunisia\", \"country\"], [\"TKM\", \"Turkmenistan\", \"country\"], [\"TCA\", \"Turks and Caicos Islands\", \"country\"], [\"UGA\", \"Uganda\", \"country\"], [\"GBR\", \"UK\", \"country\"], [\"UKR\", \"Ukraine\", \"country\"], [\"ARE\", \"United Arab Emirates\", \"country\"], [\"VIR\", \"United States Virgin Islands\", \"country\"], [\"URY\", \"Uruguay\", \"country\"], [\"USA\", \"USA\", \"country\"], [\"UZB\", \"Uzbekistan\", \"country\"], [\"VUT\", \"Vanuatu\", \"country\"], [\"VEN\", \"Venezuela, RB\", \"country\"], [\"VNM\", \"Vietnam\", \"country\"], [\"PSE\", \"West Bank and Gaza\", \"country\"], [\"ESH\", \"Western Sahara\", \"country\"], [\"YEM\", \"Yemen\", \"country\"], [\"GRC\", \"Greece\", \"country\"], [\"ZMB\", \"Zambia\", \"country\"], [\"ZWE\", \"Zimbabwe\", \"country\"]]}")
CAP = {}

def fetch(indicator, entity, retries=6):
    for k in range(retries):
        try:
            rows = client.get_series(indicator=indicator, entity=entity, limit=50000, as_df=False)
            return [(str(r["period"]), float(r["value"])) for r in rows if str(r.get("entity_code") or "") == entity and r.get("value") is not None]
        except Exception as e:
            if k == retries - 1: raise
            time.sleep(2 ** k)

def fetch_panel(indicator, out_csv):
    ents = ENTITIES[indicator]
    with ThreadPoolExecutor(max_workers=4) as ex:
        res = list(ex.map(lambda e: fetch(indicator, e[0]), ents))
    recs = [(c, n, t, p, v) for (c, n, t), rows in zip(ents, res) for p, v in rows]
    df = pd.DataFrame(recs, columns=["entity", "name", "etype", "period", "value"]).drop_duplicates(["entity", "period"])
    if indicator in CAP: df = df[df.period <= CAP[indicator]]
    df.to_csv(out_csv, index=False)
    return df

if VERI_KAYNAGI == "api":
    for ind, f in JOBS:
        t = time.time(); df = fetch_panel(ind, f"data/{f}.csv")
        print(f"{f:28s} {len(df):7d} rows  {df.entity.nunique():4d} entities  {df.period.min()}-{df.period.max()}  ({time.time() - t:.0f} s)")

# ## 2. Büyüme eğrileri, kayan başlangıçlı değerlendirme ve tavan kayması

# Paper 2: out-of-sample evaluation of growth-curve models for national wind and solar electricity diffusion. Data: Ember / Energy Institute annual generation (TWh), retrieved through the GOMD API (see p2_fetch.py).
import json, warnings
import numpy as np, pandas as pd
from scipy.optimize import curve_fit
from scipy import stats
from statsmodels.tsa.holtwinters import ExponentialSmoothing
import matplotlib
import matplotlib.pyplot as plt
warnings.filterwarnings("ignore")
plt.rcParams.update({"font.family": "DejaVu Serif", "font.size": 9, "axes.titlesize": 10, "legend.fontsize": 8,
                     "savefig.dpi": 300, "axes.spines.top": False, "axes.spines.right": False})
R = {}
TAKEOFF = 0.1          # TWh: first year at or above this level starts the diffusion record
MIN_OBS = 6            # minimum post-takeoff observations at a forecast origin
ORIGINS = list(range(2014, 2020))   # targets up to 2024, the last year reported for every country
H = 5

def load(name):
    d = pd.read_csv(f"data/{name}.csv", dtype={"period": str})
    d = d[d.etype == "country"].copy(); d["year"] = d.period.astype(int)
    return d.pivot(index="year", columns="entity", values="value").sort_index()
GEN = {"wind": load("wind"), "solar": load("solar")}
names = pd.concat([pd.read_csv("data/wind.csv")[["entity", "name"]], pd.read_csv("data/solar.csv")[["entity", "name"]]]).drop_duplicates("entity").set_index("entity").name

# ---------------- growth curves ----------------
def logistic(t, K, r, t0): return K / (1 + np.exp(-r * (t - t0)))
def gompertz(t, K, b, c): return K * np.exp(-b * np.exp(-c * t))
def bass(t, m, p, q):
    e = np.exp(-(p + q) * t)
    return m * (1 - e) / (1 + (q / p) * e)

def fit(model, t, y):
    """Least squares on levels with several starting values; ceiling constrained to at least the last observed level."""
    ymax = y.max(); best = None
    if model == "logistic":
        f = logistic; lo = [ymax, 0.01, t.min() - 30]; hi = [ymax * 200, 3.0, t.max() + 60]
        starts = [[ymax * k, r, t.max() + d] for k in (1.5, 3, 10) for r in (0.2, 0.4, 0.8) for d in (-3, 3, 10)]
    elif model == "gompertz":
        f = gompertz; lo = [ymax, 1e-3, 1e-3]; hi = [ymax * 200, 1e4, 3.0]
        starts = [[ymax * k, b, c] for k in (1.5, 3, 10) for b in (5, 50, 500) for c in (0.1, 0.3, 0.6)]
    else:
        f = bass; lo = [ymax, 1e-6, 1e-3]; hi = [ymax * 200, 0.5, 3.0]
        starts = [[ymax * k, p, q] for k in (1.5, 3, 10) for p in (1e-4, 1e-3, 1e-2) for q in (0.2, 0.5, 1.0)]
    for s0 in starts:
        try:
            par, _ = curve_fit(f, t, y, p0=s0, bounds=(lo, hi), maxfev=20000)
            sse = np.sum((f(t, *par) - y) ** 2)
            if best is None or sse < best[1]:
                best = (par, sse)
        except Exception:
            pass
    return (f, best[0]) if best else (None, None)

def forecasts(y, origin):
    """y: Series indexed by year (post-takeoff, up to origin). Returns dict model -> array of h-step forecasts."""
    t0 = y.index.min(); t = (y.index.values - t0).astype(float); v = y.values.astype(float)
    tt = np.arange(origin + 1, origin + H + 1) - t0
    out, pars = {}, {}
    for m in ("logistic", "gompertz", "bass"):
        f, p = fit(m, t, v)
        if f is not None:
            out[m] = f(tt, *p); pars[m] = p
    out["naive"] = np.repeat(v[-1], H)
    k = min(5, len(v) - 1)
    g = np.polyfit(np.arange(k + 1), np.log(v[-(k + 1):]), 1)[0]
    out["exponential"] = v[-1] * np.exp(g * np.arange(1, H + 1))
    try:
        es = ExponentialSmoothing(v, trend="add", damped_trend=True, initialization_method="estimated").fit()
        out["damped"] = np.maximum(es.forecast(H), 0)
    except Exception:
        pass
    if all(m in out for m in ("logistic", "gompertz", "damped")):
        out["combination"] = (out["logistic"] + out["gompertz"] + out["damped"]) / 3
    return out, pars, t0

# ---------------- rolling-origin evaluation ----------------
rows, krows = [], []
for tech, G in GEN.items():
    for c in G.columns:
        s = G[c].dropna()
        s = s[s.index <= 2024]
        if s.empty or s.max() < 1 or 2024 not in s.index:
            continue
        on = s[s >= TAKEOFF]
        if on.empty:
            continue
        start = on.index.min(); s = s[s.index >= start]
        if (s <= 0).any():
            s = s.clip(lower=1e-4)
        for o in ORIGINS:
            yo = s[s.index <= o]
            if len(yo) < MIN_OBS:
                continue
            fc, pars, t0 = forecasts(yo, o)
            for m, f in fc.items():
                for h in range(1, H + 1):
                    yr = o + h
                    if yr in s.index:
                        rows.append((tech, c, o, h, m, float(f[h - 1]), float(s[yr]), len(yo)))
            if "logistic" in pars:
                krows.append((tech, c, o, float(pars["logistic"][0]), float(yo.iloc[-1])))
E = pd.DataFrame(rows, columns=["tech", "country", "origin", "h", "model", "f", "a", "n_obs"])
E["lar"] = np.log(np.maximum(E.f, 1e-6) / E.a); E["alar"] = E.lar.abs()
E.to_csv("out/rolling_forecasts.csv", index=False)
MODELS = ["naive", "exponential", "damped", "logistic", "gompertz", "bass", "combination"]
R["pairs"] = {t: int(E[E.tech == t].country.nunique()) for t in GEN}
R["n_forecasts"] = int(len(E[E.model == "logistic"]))
summ = E.groupby(["tech", "model"]).agg(med_alar=("alar", "median"), med_lar=("lar", "median"), under=("lar", lambda x: float((x < 0).mean())),
                                         mean_alar=("alar", "mean")).reset_index()
R["summary"] = {f"{r.tech}|{r.model}": {"median_abs_LAR": round(r.med_alar, 4), "median_LAR": round(r.med_lar, 4),
                "share_under": round(r.under, 4), "mean_abs_LAR": round(r.mean_alar, 4)} for r in summ.itertuples()}
byh = E.groupby(["tech", "model", "h"]).agg(med_alar=("alar", "median"), med_lar=("lar", "median")).reset_index()
R["by_h"] = {f"{t}|{m}": [round(float(v), 4) for v in byh[(byh.tech == t) & (byh.model == m)].sort_values("h").med_alar] for t in GEN for m in MODELS}
R["bias_by_h"] = {f"{t}|{m}": [round(float(v), 4) for v in byh[(byh.tech == t) & (byh.model == m)].sort_values("h").med_lar] for t in GEN for m in MODELS}
# paired tests per forecast: model vs logistic and vs damped
tests = {}
piv = E.pivot_table(index=["tech", "country", "origin", "h"], columns="model", values="alar").dropna()
for a, b in [("combination", "logistic"), ("damped", "logistic"), ("combination", "damped"), ("logistic", "exponential"), ("gompertz", "logistic"), ("bass", "logistic")]:
    for t in GEN:
        p = piv.loc[t]
        w = stats.wilcoxon(p[a], p[b])
        tests[f"{t}|{a}_vs_{b}"] = {"n": int(len(p)), "median_diff": round(float((p[a] - p[b]).median()), 4), "share_a_better": round(float((p[a] < p[b]).mean()), 4), "p": float(w.pvalue)}
R["wilcoxon"] = tests
# Friedman ranks across forecast cases
for t in GEN:
    p = piv.loc[t][MODELS]
    rk = p.rank(axis=1)
    fr = stats.friedmanchisquare(*[p[m] for m in MODELS])
    R[f"ranks_{t}"] = {m: round(float(rk[m].mean()), 3) for m in MODELS}
    R[f"friedman_{t}"] = {"chi2": round(float(fr.statistic), 1), "p": float(fr.pvalue), "N": int(len(p))}

# ---------------- ceiling drift ----------------
K = pd.DataFrame(krows, columns=["tech", "country", "origin", "K", "last"])
final = {}
for tech, G in GEN.items():
    for c in K[K.tech == tech].country.unique():
        s = G[c].dropna(); s = s[s.index <= 2024]; s = s[s.index >= s[s >= TAKEOFF].index.min()]
        f, p = fit("logistic", (s.index.values - s.index.min()).astype(float), s.values.astype(float))
        if f is not None:
            final[(tech, c)] = (p, s.index.min(), float(s.iloc[-1]))
K["K_final"] = [final.get((r.tech, r.country), (np.array([np.nan]),))[0][0] for r in K.itertuples()]
K["ratio"] = K.K_final / K.K
K["obs2024"] = [GEN[r.tech].loc[2024, r.country] for r in K.itertuples()]
K["breach"] = K.obs2024 > K.K
k14 = K[K.origin == 2014]
R["ceiling"] = {t: {"n": int((k14.tech == t).sum()), "median_Kfinal_over_K2014": round(float(k14[k14.tech == t].ratio.median()), 3),
                    "share_breached_by_2024": round(float(k14[k14.tech == t].breach.mean()), 3)} for t in GEN}
R["ceiling_by_origin"] = {f"{t}|{o}": {"median_ratio": round(float(K[(K.tech == t) & (K.origin == o)].ratio.median()), 3),
                                       "breach": round(float(K[(K.tech == t) & (K.origin == o)].breach.mean()), 3)} for t in GEN for o in ORIGINS}
K.to_csv("out/ceilings.csv", index=False)

# ---------------- final fits (data to 2024) ----------------
fin = []
for (tech, c), (p, start, last) in final.items():
    fin.append((tech, c, names.get(c, c), start, p[0], p[1], start + p[2], last, p[0] / last))
FIN = pd.DataFrame(fin, columns=["tech", "country", "name", "takeoff", "K", "r", "inflection", "y2024", "headroom"])
FIN.to_csv("out/final_fits.csv", index=False)
R["final"] = {t: {"n": int((FIN.tech == t).sum()), "share_pre_inflection": round(float((FIN[FIN.tech == t].inflection > 2024).mean()), 3),
                  "median_r": round(float(FIN[FIN.tech == t].r.median()), 3), "median_headroom": round(float(FIN[FIN.tech == t].headroom.median()), 2)} for t in GEN}
R["turkiye"] = {t: FIN[(FIN.tech == t) & (FIN.country == "TUR")].drop(columns=["tech", "country", "name"]).round(3).to_dict("records") for t in GEN}
R["turkiye_obs"] = {t: {int(y): round(float(v), 2) for y, v in GEN[t]["TUR"].dropna().items() if y >= 2010} for t in GEN}
# does stage at origin explain error? regress |LAR| of logistic at h=5 on years since takeoff and growth
L = E[(E.model == "logistic") & (E.h == 5)].copy()
L["years_since_takeoff"] = L.n_obs
X = np.column_stack([np.ones(len(L)), L.years_since_takeoff, (L.tech == "solar").astype(float)])
beta, *_ = np.linalg.lstsq(X, L.lar.values, rcond=None)
res = L.lar.values - X @ beta; s2 = res @ res / (len(L) - 3); se = np.sqrt(np.diag(s2 * np.linalg.inv(X.T @ X)))
R["stage_regression_lar_h5"] = {"n": int(len(L)), "coef": [round(float(b), 4) for b in beta], "se": [round(float(x), 4) for x in se],
                                "labels": ["const", "years_since_takeoff", "solar"]}
json.dump(R, open("out/results.json", "w"), indent=1, default=str)

# ================= FIGURES =================
COL = {"naive": "#9e9e9e", "exponential": "#8c564b", "damped": "#1f77b4", "logistic": "#d62728", "gompertz": "#ff7f0e", "bass": "#2ca02c", "combination": "#000000"}
LAB = {"naive": "Naive", "exponential": "Exponential", "damped": "Damped trend", "logistic": "Logistic", "gompertz": "Gompertz", "bass": "Bass", "combination": "Combination"}
# Fig 1 takeoffs and world generation
fig, ax = plt.subplots(1, 2, figsize=(9, 3.2))
for tech, c in [("wind", "#1f77b4"), ("solar", "#ff7f0e")]:
    G = GEN[tech]; to = (G >= TAKEOFF).idxmax()[(G >= TAKEOFF).any()]
    yrs = np.arange(1985, 2025); ax[0].plot(yrs, [(to <= y).sum() for y in yrs], color=c, label=tech.capitalize())
    wl = pd.read_csv(f"data/{tech}.csv", dtype={"period": str}); wl = wl[wl.entity == "WLD"]; wl["year"] = wl.period.astype(int)
    ax[1].plot(wl.year, wl.value, color=c, label=tech.capitalize())
ax[0].set_title("(a) Countries past take-off (≥ 0.1 TWh)"); ax[0].set_ylabel("Number of countries"); ax[0].legend(frameon=False)
ax[1].set_title("(b) World generation"); ax[1].set_ylabel("TWh"); ax[1].set_xlim(1990, 2025); ax[1].legend(frameon=False)
fig.tight_layout(); fig.savefig("fig/fig1_overview.png"); plt.show()
# Fig 2 example fans
fig, axs = plt.subplots(2, 2, figsize=(9, 5.4))
for a, (tech, c) in zip(axs.flat, [("wind", "TUR"), ("solar", "TUR"), ("solar", "DEU"), ("wind", "CHN")]):
    s = GEN[tech][c].dropna(); s = s[s.index <= 2024]; st = s[s >= TAKEOFF].index.min(); s = s[s.index >= st]
    a.plot(s.index, s.values, "ko", ms=2.5, label="Observed")
    for o, col in [(2014, "#fdae6b"), (2016, "#f16913"), (2018, "#d94801"), (2024, "#7f2704")]:
        yo = s[s.index <= o]
        if len(yo) < MIN_OBS: continue
        f, p = fit("logistic", (yo.index.values - st).astype(float), yo.values.astype(float))
        tt = np.arange(st, 2036)
        a.plot(tt, f(tt - st, *p), color=col, lw=1.1, label=f"Logistic, data to {o}")
    a.set_title(f"{names.get(c, c)} – {tech}"); a.set_ylabel("TWh")
axs[0, 0].legend(frameon=False, fontsize=7); fig.tight_layout(); fig.savefig("fig/fig2_fans.png"); plt.show()
# Fig 3 accuracy and bias by horizon
fig, axs = plt.subplots(2, 2, figsize=(9, 5.6), sharex=True)
for j, tech in enumerate(GEN):
    for m in MODELS:
        d = byh[(byh.tech == tech) & (byh.model == m)].sort_values("h")
        axs[0, j].plot(d.h, d.med_alar, marker="o", ms=2.5, color=COL[m], lw=1.3 if m in ("logistic", "combination", "damped") else 0.8, label=LAB[m])
        axs[1, j].plot(d.h, d.med_lar, marker="o", ms=2.5, color=COL[m], lw=1.3 if m in ("logistic", "combination", "damped") else 0.8)
    axs[0, j].set_title(f"({'ab'[j]}) {tech.capitalize()}: median |log(F/A)|"); axs[1, j].set_title(f"({'cd'[j]}) {tech.capitalize()}: median log(F/A)")
    axs[1, j].axhline(0, color="k", lw=0.7); axs[1, j].set_xlabel("Horizon (years)")
axs[0, 0].legend(frameon=False, ncol=2, fontsize=7); fig.tight_layout(); fig.savefig("fig/fig3_accuracy.png"); plt.show()
# Fig 4 ceiling drift
fig, ax = plt.subplots(1, 2, figsize=(9, 3.2))
for tech, c in [("wind", "#1f77b4"), ("solar", "#ff7f0e")]:
    d = K[(K.tech == tech) & (K.origin == 2014)].ratio.dropna()
    ax[0].hist(np.log10(d), bins=20, alpha=0.55, color=c, label=f"{tech.capitalize()} (n = {len(d)})")
    ax[1].plot(ORIGINS, [R["ceiling_by_origin"][f"{tech}|{o}"]["breach"] * 100 for o in ORIGINS], marker="o", color=c, label=tech.capitalize())
ax[0].axvline(0, color="k", lw=0.8); ax[0].set_xlabel("log$_{10}$(K estimated with data to 2024 / K estimated with data to 2014)"); ax[0].set_ylabel("Countries"); ax[0].legend(frameon=False)
ax[1].set_xlabel("Forecast origin"); ax[1].set_ylabel("Ceiling exceeded by 2024 (%)"); ax[1].legend(frameon=False)
fig.tight_layout(); fig.savefig("fig/fig4_ceilings.png"); plt.show()
# Fig 5 final landscape
fig, ax = plt.subplots(figsize=(6.5, 3.8))
for tech, c in [("wind", "#1f77b4"), ("solar", "#ff7f0e")]:
    d = FIN[(FIN.tech == tech) & (FIN.inflection.between(1990, 2060))]
    ax.scatter(d.inflection, d.r, s=10 + 4 * np.sqrt(d.y2024), alpha=0.55, color=c, edgecolor="none", label=tech.capitalize())
    tr = d[d.country == "TUR"]
    if len(tr): ax.scatter(tr.inflection, tr.r, s=80, facecolor="none", edgecolor="k", lw=1.2); ax.annotate(f"Türkiye ({tech})", (tr.inflection.iloc[0], tr.r.iloc[0]), xytext=(5, 5), textcoords="offset points", fontsize=7)
ax.axvline(2024, color="grey", ls=":", lw=0.8); ax.set_xlabel("Estimated inflection year (logistic, data to 2024)"); ax.set_ylabel("Growth parameter r")
ax.legend(frameon=False); fig.tight_layout(); fig.savefig("fig/fig5_landscape.png"); plt.show()
print(json.dumps({k: R[k] for k in ["pairs", "n_forecasts", "ceiling", "final"]}, indent=1))
for t in GEN: print(t, R[f"ranks_{t}"])

# ## 3. Tavan tanımlanabilirliği, 2025 örneklem dışı kontrolü ve son şekiller

# Redraws Figures 2 and 3 of paper 2 from saved outputs and computes identification diagnostics for the logistic ceiling.
import json
from matplotlib.ticker import MaxNLocator
R = json.load(open("out/results.json"))
E = pd.read_csv("out/rolling_forecasts.csv"); K = pd.read_csv("out/ceilings.csv"); FIN = pd.read_csv("out/final_fits.csv")

# --- identification diagnostics: ceilings at the bounds [y_max, 200 y_max]
K["at_lower"] = K.K <= 1.01 * K["last"].clip(lower=1e-9)
K["at_upper"] = K.K >= 199 * K["last"]
FIN["at_lower"] = FIN.headroom <= 1.01; FIN["at_upper"] = FIN.headroom >= 199
diag = {}
for t in ("wind", "solar"):
    k = K[K.tech == t]; f = FIN[FIN.tech == t]
    diag[t] = {"rolling_n": int(len(k)), "rolling_at_lower": round(float(k.at_lower.mean()), 3), "rolling_at_upper": round(float(k.at_upper.mean()), 3),
               "final_n": int(len(f)), "final_at_lower": round(float(f.at_lower.mean()), 3), "final_at_upper": round(float(f.at_upper.mean()), 3),
               "final_interior_median_headroom": round(float(f[~f.at_lower & ~f.at_upper].headroom.median()), 2)}
R["ceiling_bounds"] = diag
# post-sample check: logistic fits with data to 2024 against 2025 generation (not used in estimation)
chk = {}
for t in ("wind", "solar"):
    f = FIN[FIN.tech == t].copy()
    f["y2025"] = [GEN[t].loc[2025, c] if (2025 in GEN[t].index and c in GEN[t].columns) else np.nan for c in f.country]
    f["pred2025"] = f.K / (1 + np.exp(-f.r * (2025 - f.inflection)))
    g = f.dropna(subset=["y2025"])
    chk[t] = {"n2025": int(len(g)), "share_above_K": round(float((g.y2025 > g.K).mean()), 3), "median_LAR_2025": round(float(np.log(g.pred2025 / g.y2025).median()), 3),
              "share_under": round(float((g.pred2025 < g.y2025).mean()), 3)}
R["check2025"] = chk
json.dump(R, open("out/results.json", "w"), indent=1, default=str)
print(json.dumps(diag, indent=1))
COL = {"naive": "#9e9e9e", "exponential": "#8c564b", "damped": "#1f77b4", "logistic": "#d62728", "gompertz": "#ff7f0e", "bass": "#2ca02c", "combination": "#000000"}
LAB = {"naive": "Naive", "exponential": "Exponential", "damped": "Damped trend", "logistic": "Logistic", "gompertz": "Gompertz", "bass": "Bass", "combination": "Combination"}
MODELS = list(LAB)
NAME = {"TUR": "Türkiye", "DEU": "Germany", "CHN": "China"}
fig, axs = plt.subplots(2, 2, figsize=(9, 5.4))
for a, (tech, c) in zip(axs.flat, [("wind", "TUR"), ("solar", "TUR"), ("solar", "DEU"), ("wind", "CHN")]):
    full = GEN[tech][c].dropna(); s = full[full.index <= 2024]; st = s[s >= TAKEOFF].index.min(); s = s[s.index >= st]
    a.plot(s.index, s.values, "ko", ms=2.5, label="Observed (used)")
    if 2025 in full.index: a.plot([2025], [full[2025]], "o", mfc="white", mec="k", ms=4, label="2025 (held out)")
    for o, col in [(2014, "#fdae6b"), (2016, "#f16913"), (2018, "#d94801"), (2024, "#7f2704")]:
        yo = s[s.index <= o]
        if len(yo) < MIN_OBS: continue
        f, p = fit("logistic", (yo.index.values - st).astype(float), yo.values.astype(float))
        tt = np.arange(st, 2036); a.plot(tt, f(tt - st, *p), color=col, lw=1.1, label=f"Logistic, data to {o}")
    a.set_title(f"{NAME[c]} – {tech}"); a.set_ylabel("TWh"); a.xaxis.set_major_locator(MaxNLocator(integer=True))
axs[0, 0].legend(frameon=False, fontsize=7); axs[0, 1].legend(frameon=False, fontsize=7)
fig.tight_layout(); fig.savefig("fig/fig2_fans.png"); plt.show()
byh = E.groupby(["tech", "model", "h"]).agg(med_alar=("alar", "median"), med_lar=("lar", "median")).reset_index()
fig, axs = plt.subplots(2, 2, figsize=(9, 5.6), sharex=True)
for j, tech in enumerate(("wind", "solar")):
    for m in MODELS:
        d = byh[(byh.tech == tech) & (byh.model == m)].sort_values("h")
        lw = 1.6 if m in ("logistic", "combination", "damped") else 0.8
        axs[0, j].plot(d.h, d.med_alar, marker="o", ms=2.5, color=COL[m], lw=lw, label=LAB[m])
        axs[1, j].plot(d.h, d.med_lar, marker="o", ms=2.5, color=COL[m], lw=lw)
    axs[0, j].set_title(f"({'ab'[j]}) {tech.capitalize()}: median |log(F/A)|"); axs[1, j].set_title(f"({'cd'[j]}) {tech.capitalize()}: median log(F/A)")
    axs[1, j].axhline(0, color="k", lw=0.7); axs[1, j].set_xlabel("Horizon (years)"); axs[1, j].set_xticks(range(1, 6))
axs[0, 0].legend(frameon=False, ncol=2, fontsize=7); fig.tight_layout(); fig.savefig("fig/fig3_accuracy.png"); plt.show()

# ## Sonuçlar ve çıktılar

# Packs figures and numerical results for download.
import shutil
from google.colab import files
shutil.make_archive("/content/rp02_outputs", "zip", ".", ".")
files.download("/content/rp02_outputs.zip")
