İklim Riski & Mahsul Verimi — Veri Bilimi Capstone (Track A)¶
Uydu bitki örtüsü (MODIS NDVI) × iklim (NASA POWER) × mısır verimi (OWID) → risk tahmini + AI çiftçi danışman ajanı.
Temel soru: Uydudan türetilen bitki indeksi ve hava örüntüleri mahsul verim riskini eyleme geçirilebilecek kadar erken tahmin edebilir mi, ve bir AI ajan bu riski çiftçiye topraklı önerilere çevirebilir mi?
Boru hattı: topla → sakla/işle → EDA → klasik ML → derin öğrenme → AI ajan (araç+RAG) → dashboard.
Not: Tüm veri kaynakları ücretsiz, açık ve anahtarsız. Erişim: 30 Ekim 2026. Akademik proje.
Veri kaynakları (hepsi anahtarsız, açık)¶
| Kaynak | Ne verir | Erişim |
|---|---|---|
| MODIS MOD13Q1 (ORNL DAAC REST) | 16 günlük NDVI zaman serisi, nokta bazlı | modis.ornl.gov/rst |
| NASA POWER (günlük) | sıcaklık, yağış | power.larc.nasa.gov |
| Our World in Data — mısır verimi | ülke-yıl verim (ton/ha), FAO kaynaklı | ourworldindata.org/grapher/maize-yields.csv |
Bölgeler: 5 büyük mısır üreticisi ülke (ABD-Iowa, Fransa, Ukrayna, Çin-Henan, Romanya), hepsi Kuzey Yarımküre. Sezonlar: 2016-2022.
Veri toplama kodu kaynak/01_veri_topla.py'dedir (parçalı MODIS istekleri, yeniden deneme/backoff). Bu notebook, tekrarlanabilirlik için veri/ altındaki toplanmış CSV'leri okur; yoksa toplama betiği çalıştırılmalıdır.
from pathlib import Path
import pandas as pd
VERI = Path.cwd()/'veri'
assert (VERI/'ndvi_ham.csv').exists(), 'Önce kaynak/01_veri_topla.py ile veriyi toplayın.'
ndvi=pd.read_csv(VERI/'ndvi_ham.csv'); hava=pd.read_csv(VERI/'hava_ham.csv'); verim=pd.read_csv(VERI/'verim_ham.csv')
print('NDVI', ndvi.shape, '| hava', hava.shape, '| verim', verim.shape)
verim.head()
NDVI (455, 5) | hava (6930, 7) | verim (35, 4)
| bolge | kod | yil | verim_ton_ha | |
|---|---|---|---|---|
| 0 | Çin (Henan) | CHN | 2016 | 5.967 |
| 1 | Çin (Henan) | CHN | 2017 | 6.110 |
| 2 | Çin (Henan) | CHN | 2018 | 6.104 |
| 3 | Çin (Henan) | CHN | 2019 | 6.317 |
| 4 | Çin (Henan) | CHN | 2020 | 6.318 |
Task 2 — Özellik mühendisliği + EDA + hipotez testi¶
NDVI eğrisinden (tepe, yeşil süre, eğri altı alan) ve havadan (GDD, yağış, sıcak stresi) özellikler; bölge-sezon tablosu + 4 figür + Mann-Whitney U testi.
"""Task 2 — Özellik mühendisliği + birleştirme + EDA.
NDVI eğrisi (tepe, yeşil süre, AUC, ort) + hava (GDD, yağış, sıcak stresi, ort sıc) -> bölge-sezon tablosu + verim.
Çıktı: veri/model_tablosu.csv ve sonuclar/*.png (EDA figürleri) + istatistik testi."""
import pandas as pd, numpy as np
from pathlib import Path
from datetime import datetime
import matplotlib; matplotlib.use("Agg")
import matplotlib.pyplot as plt
import seaborn as sns
from scipy import stats
KOK = Path.cwd()
VERI = KOK / "veri"; SON = KOK / "sonuclar"; SON.mkdir(exist_ok=True)
sns.set_theme(style="whitegrid", palette="viridis")
ndvi = pd.read_csv(VERI / "ndvi_ham.csv")
hava = pd.read_csv(VERI / "hava_ham.csv")
verim = pd.read_csv(VERI / "verim_ham.csv")
# --- NDVI: tarih -> gün-of-year; eksik/negatif temizle ---
ndvi["tarih"] = pd.to_datetime(ndvi["tarih"])
ndvi["doy"] = ndvi["tarih"].dt.dayofyear
ndvi = ndvi.dropna(subset=["ndvi"])
ndvi = ndvi[ndvi["ndvi"] > -0.2] # su/bulut maskesi artığı
def ndvi_ozellik(g):
g = g.sort_values("doy")
return pd.Series({
"ndvi_tepe": g["ndvi"].max(),
"ndvi_ort": g["ndvi"].mean(),
"ndvi_tepe_gun": g.loc[g["ndvi"].idxmax(), "doy"],
"ndvi_auc": np.trapezoid(g["ndvi"].values, g["doy"].values), # sezon boyu yeşillik integrali
"ndvi_yesil_sure": int((g["ndvi"] > 0.5).sum()), # >0.5 kompozit sayısı
"ndvi_son": g["ndvi"].iloc[-1],
})
ndvi_oz = ndvi.groupby(["bolge", "yil"]).apply(ndvi_ozellik, include_groups=False).reset_index()
# --- Hava: GDD (taban 10, tavan 30), yağış, sıcak stresi, ort sıcaklık ---
hava["ort_gun"] = ((hava["tmax"].clip(upper=30) + hava["tmin"]) / 2 - 10).clip(lower=0)
def hava_ozellik(g):
return pd.Series({
"gdd": g["ort_gun"].sum(),
"yagis_toplam": g["yagis"].sum(),
"sicak_stres_gun": int((g["tmax"] > 32).sum()),
"ort_sicaklik": g["t2m"].mean(),
"kurak_gun": int((g["yagis"] < 1).sum()), # günlük <1mm = kurak proxy
})
hava_oz = hava.groupby(["bolge", "yil"]).apply(hava_ozellik, include_groups=False).reset_index()
# --- Birleştir + verim ---
df = ndvi_oz.merge(hava_oz, on=["bolge", "yil"]).merge(
verim[["bolge", "yil", "verim_ton_ha"]], on=["bolge", "yil"])
df = df.sort_values(["bolge", "yil"]).reset_index(drop=True)
# Risk sınıfı: her bölgenin kendi ortalamasına göre düşük/orta/yüksek verim (göreli risk)
df["bolge_ort"] = df.groupby("bolge")["verim_ton_ha"].transform("mean")
df["sapma"] = (df["verim_ton_ha"] - df["bolge_ort"]) / df["bolge_ort"]
df["risk"] = pd.cut(df["sapma"], bins=[-1, -0.05, 0.05, 1], labels=["yüksek_risk", "orta", "düşük_risk"])
df.to_csv(VERI / "model_tablosu.csv", index=False, encoding="utf-8")
print(f"model_tablosu.csv: {len(df)} bölge-sezon, {df.shape[1]} sütun")
print(df[["bolge","yil","ndvi_tepe","ndvi_auc","gdd","yagis_toplam","verim_ton_ha","risk"]].head(10).to_string(index=False))
# =================== EDA FİGÜRLERİ ===================
# 1) NDVI trajektorileri (bölge başına ortalama sezon eğrisi)
plt.figure(figsize=(10,6))
for bolge, g in ndvi.groupby("bolge"):
egri = g.groupby("doy")["ndvi"].mean()
plt.plot(egri.index, egri.values, marker="o", ms=3, label=bolge)
plt.xlabel("Yılın günü (büyüme sezonu)"); plt.ylabel("NDVI")
plt.title("Bölgelere göre ortalama NDVI trajektorisi (büyüme sezonu)")
plt.legend(); plt.tight_layout(); plt.savefig(SON/"t2_1_ndvi_trajektori.png", dpi=200); plt.close()
# 2) Özellik-verim korelasyon ısı haritası
say = df[["ndvi_tepe","ndvi_ort","ndvi_auc","ndvi_yesil_sure","gdd","yagis_toplam","sicak_stres_gun","ort_sicaklik","kurak_gun","verim_ton_ha"]]
plt.figure(figsize=(9,7)); sns.heatmap(say.corr(), annot=True, fmt=".2f", cmap="RdBu_r", center=0, square=True)
plt.title("Özellikler ve verim — korelasyon"); plt.tight_layout(); plt.savefig(SON/"t2_2_korelasyon.png", dpi=200); plt.close()
# 3) Tepe NDVI vs verim (bölge renkli)
plt.figure(figsize=(9,6)); sns.scatterplot(data=df, x="ndvi_tepe", y="verim_ton_ha", hue="bolge", s=90)
plt.xlabel("Tepe NDVI"); plt.ylabel("Verim (ton/ha)"); plt.title("Tepe NDVI ile verim ilişkisi")
plt.tight_layout(); plt.savefig(SON/"t2_3_ndvi_verim.png", dpi=200); plt.close()
# 4) Bölgelere göre verim dağılımı
plt.figure(figsize=(9,6)); sns.boxplot(data=df, x="bolge", y="verim_ton_ha", hue="bolge", legend=False)
sns.stripplot(data=df, x="bolge", y="verim_ton_ha", color="black", size=5, alpha=.6)
plt.xlabel(""); plt.ylabel("Verim (ton/ha)"); plt.title("Bölgelere göre mısır verimi (2016-2022)")
plt.xticks(rotation=15); plt.tight_layout(); plt.savefig(SON/"t2_4_verim_bolge.png", dpi=200); plt.close()
# =================== HİPOTEZ TESTİ ===================
# Yüksek GDD yılları vs düşük GDD yılları -> verim farklı mı? (medyan bölme, Mann-Whitney U)
med = df["gdd"].median()
yuksek = df[df["gdd"] >= med]["verim_ton_ha"]; dusuk = df[df["gdd"] < med]["verim_ton_ha"]
u, p = stats.mannwhitneyu(yuksek, dusuk, alternative="two-sided")
print(f"\nHipotez testi (Mann-Whitney U): yüksek-GDD vs düşük-GDD verimi")
print(f" yüksek-GDD medyan={yuksek.median():.2f}, düşük-GDD medyan={dusuk.median():.2f}")
print(f" U={u:.1f}, p={p:.4f} -> {'anlamlı fark (p<0.05)' if p<0.05 else 'anlamlı fark yok'}")
print("\nTask 2 figürleri kaydedildi: t2_1..t2_4")
C:\Users\cetin\AppData\Local\Temp\ipykernel_30548\1707595787.py:22: Pandas4Warning: Series.dt.dayofyear is deprecated and will be removed in a future version. Use Series.dt.day_of_year instead. ndvi["doy"] = ndvi["tarih"].dt.dayofyear
model_tablosu.csv: 35 bölge-sezon, 17 sütun
bolge yil ndvi_tepe ndvi_auc gdd yagis_toplam verim_ton_ha risk
ABD (Iowa) 2016 0.4787 61.1560 1775.185 684.55 10.958 orta
ABD (Iowa) 2017 0.5000 72.2712 1836.965 563.25 11.084 orta
ABD (Iowa) 2018 0.5093 72.9136 1817.820 849.53 11.071 orta
ABD (Iowa) 2019 0.4473 66.5608 1645.410 729.15 10.513 orta
ABD (Iowa) 2020 0.4694 66.7704 1737.275 433.36 10.757 orta
ABD (Iowa) 2021 0.4549 61.5512 1989.210 409.95 11.090 orta
ABD (Iowa) 2022 0.5290 70.5016 1833.370 485.32 10.883 orta
Fransa (Beauce) 2016 0.7093 103.1440 1183.655 385.41 8.206 yüksek_risk
Fransa (Beauce) 2017 0.7771 106.2480 1287.470 364.10 10.124 düşük_risk
Fransa (Beauce) 2018 0.8581 94.9784 1474.675 294.34 8.821 orta
Hipotez testi (Mann-Whitney U): yüksek-GDD vs düşük-GDD verimi yüksek-GDD medyan=6.32, düşük-GDD medyan=7.84 U=121.0, p=0.2985 -> anlamlı fark yok Task 2 figürleri kaydedildi: t2_1..t2_4
Task 3 — Klasik ML¶
Zaman-ayrımlı bölme (eğitim ≤2020, test ≥2021), sızıntısız. Regresyon (verim) + sınıflandırma (risk), 3'er model, metrikler + özellik önemi.
"""Task 3 — Klasik ML. Regresyon (verim) + sınıflandırma (risk).
Zaman-ayrımlı bölme (sızıntısız): eğitim <=2020, test >=2021. StandardScaler sadece eğitimde.
3'er model; RMSE/R2 ve accuracy/F1 + confusion matrix + özellik önemi."""
import pandas as pd, numpy as np, json
from pathlib import Path
import matplotlib; matplotlib.use("Agg")
import matplotlib.pyplot as plt, seaborn as sns
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LinearRegression, LogisticRegression
from sklearn.ensemble import (RandomForestRegressor, GradientBoostingRegressor,
RandomForestClassifier, GradientBoostingClassifier)
from sklearn.metrics import (mean_squared_error, r2_score, accuracy_score,
f1_score, classification_report, confusion_matrix)
KOK = Path.cwd()
VERI = KOK/"veri"; SON = KOK/"sonuclar"; SON.mkdir(exist_ok=True)
sns.set_theme(style="whitegrid")
df = pd.read_csv(VERI/"model_tablosu.csv")
OZ = ["ndvi_tepe","ndvi_ort","ndvi_auc","ndvi_yesil_sure","ndvi_tepe_gun","ndvi_son",
"gdd","yagis_toplam","sicak_stres_gun","ort_sicaklik","kurak_gun"]
egi = df[df["yil"] <= 2020].copy(); test = df[df["yil"] >= 2021].copy()
print(f"Eğitim {len(egi)} sezon (<=2020), Test {len(test)} sezon (>=2021)")
sc = StandardScaler().fit(egi[OZ])
Xtr, Xte = sc.transform(egi[OZ]), sc.transform(test[OZ])
# ---------- REGRESYON (verim ton/ha) ----------
ytr, yte = egi["verim_ton_ha"].values, test["verim_ton_ha"].values
reg = {"Doğrusal Regresyon": LinearRegression(),
"Random Forest": RandomForestRegressor(n_estimators=300, random_state=42),
"Gradient Boosting": GradientBoostingRegressor(random_state=42)}
reg_sonuc = {}
for ad, m in reg.items():
m.fit(Xtr, ytr); p = m.predict(Xte)
rmse = float(np.sqrt(mean_squared_error(yte, p))); r2 = float(r2_score(yte, p))
reg_sonuc[ad] = {"RMSE": round(rmse,3), "R2": round(r2,3)}
print(f" [REG] {ad}: RMSE={rmse:.3f} ton/ha, R2={r2:.3f}")
# ---------- SINIFLANDIRMA (risk) ----------
ytr_c, yte_c = egi["risk"].astype(str).values, test["risk"].astype(str).values
clf = {"Lojistik Regresyon": LogisticRegression(max_iter=1000, class_weight="balanced"),
"Random Forest": RandomForestClassifier(n_estimators=300, random_state=42, class_weight="balanced"),
"Gradient Boosting": GradientBoostingClassifier(random_state=42)}
clf_sonuc = {}; en_iyi=None; en_iyi_f1=-1; en_iyi_p=None
for ad, m in clf.items():
m.fit(Xtr, ytr_c); p = m.predict(Xte)
acc = float(accuracy_score(yte_c, p)); f1 = float(f1_score(yte_c, p, average="macro", zero_division=0))
clf_sonuc[ad] = {"accuracy": round(acc,3), "F1_macro": round(f1,3)}
print(f" [CLF] {ad}: acc={acc:.3f}, F1_macro={f1:.3f}")
if f1 > en_iyi_f1: en_iyi_f1=f1; en_iyi=ad; en_iyi_p=p; en_iyi_m=m
# confusion matrix (en iyi sınıflandırıcı)
etiket = ["yüksek_risk","orta","düşük_risk"]
cm = confusion_matrix(yte_c, en_iyi_p, labels=etiket)
plt.figure(figsize=(6,5)); sns.heatmap(cm, annot=True, fmt="d", cmap="Blues", xticklabels=etiket, yticklabels=etiket)
plt.xlabel("Tahmin"); plt.ylabel("Gerçek"); plt.title(f"Confusion matrix — {en_iyi} (risk)")
plt.tight_layout(); plt.savefig(SON/"t3_1_confusion.png", dpi=200); plt.close()
# özellik önemi (RF regresyon)
rf = reg["Random Forest"]; onem = pd.Series(rf.feature_importances_, index=OZ).sort_values()
plt.figure(figsize=(8,6)); onem.plot.barh(color="#0F7F77")
plt.title("Özellik önemi (Random Forest — verim)"); plt.xlabel("önem")
plt.tight_layout(); plt.savefig(SON/"t3_2_ozellik_onem.png", dpi=200); plt.close()
# tahmin vs gerçek (en iyi regresyon)
en_reg = min(reg_sonuc, key=lambda k: reg_sonuc[k]["RMSE"])
pr = reg[en_reg].predict(Xte)
plt.figure(figsize=(7,6)); plt.scatter(yte, pr, s=80, c="#A8660F")
lim=[min(yte.min(),pr.min())-0.5, max(yte.max(),pr.max())+0.5]; plt.plot(lim,lim,"--",color="gray")
plt.xlabel("Gerçek verim (ton/ha)"); plt.ylabel("Tahmin"); plt.title(f"Tahmin vs gerçek — {en_reg}")
plt.tight_layout(); plt.savefig(SON/"t3_3_tahmin_gercek.png", dpi=200); plt.close()
sonuc = {"regresyon": reg_sonuc, "siniflandirma": clf_sonuc,
"en_iyi_regresyon": en_reg, "en_iyi_siniflandirici": en_iyi,
"en_onemli_ozellikler": onem.sort_values(ascending=False).head(4).round(3).to_dict(),
"egitim_n": len(egi), "test_n": len(test)}
json.dump(sonuc, open(SON/"ml_sonuclar.json","w",encoding="utf-8"), ensure_ascii=False, indent=2)
print("\nTask 3 bitti. Figürler: t3_1..t3_3; metrikler: ml_sonuclar.json")
print("En önemli 4 özellik:", list(onem.sort_values(ascending=False).head(4).index))
Eğitim 25 sezon (<=2020), Test 10 sezon (>=2021) [REG] Doğrusal Regresyon: RMSE=2.394 ton/ha, R2=-0.049
[REG] Random Forest: RMSE=2.563 ton/ha, R2=-0.202 [REG] Gradient Boosting: RMSE=3.100 ton/ha, R2=-0.759 [CLF] Lojistik Regresyon: acc=0.600, F1_macro=0.556
[CLF] Random Forest: acc=0.700, F1_macro=0.657
[CLF] Gradient Boosting: acc=0.800, F1_macro=0.749
Task 3 bitti. Figürler: t3_1..t3_3; metrikler: ml_sonuclar.json En önemli 4 özellik: ['kurak_gun', 'sicak_stres_gun', 'yagis_toplam', 'ndvi_son']
Task 4 — Derin öğrenme¶
Ham NDVI+hava zaman serisinden sezon dizileri; küçük LSTM, erken durdurma, kayıp eğrisi; klasik ML ile karşılaştırma. N=35 küçük → derin öğrenmenin gerekçesi dürüstçe tartışılır.
"""Task 4 — Derin öğrenme. Ham NDVI + hava zaman serisinden (el yapımı özellik olmadan)
sezon dizileri kurup küçük bir LSTM ile verim tahmini. Erken durdurma, dropout, kayıp eğrisi.
Task 3 klasik ML ile karşılaştırma. N=35 küçük → derin öğrenmenin gerekçesi dürüstçe tartışılır."""
import pandas as pd, numpy as np, json
from pathlib import Path
import matplotlib; matplotlib.use("Agg"); import matplotlib.pyplot as plt
import torch, torch.nn as nn
from sklearn.metrics import mean_squared_error, r2_score
torch.manual_seed(42); np.random.seed(42)
KOK = Path.cwd(); VERI=KOK/"veri"; SON=KOK/"sonuclar"
ndvi = pd.read_csv(VERI/"ndvi_ham.csv"); hava = pd.read_csv(VERI/"hava_ham.csv"); verim = pd.read_csv(VERI/"verim_ham.csv")
ndvi["tarih"]=pd.to_datetime(ndvi["tarih"]); ndvi["doy"]=ndvi["tarih"].dt.day_of_year
ndvi=ndvi.dropna(subset=["ndvi"]); ndvi=ndvi[ndvi["ndvi"]>-0.2]
hava["doy"]=pd.to_datetime(hava["tarih"],format="%Y%m%d").dt.day_of_year
L=13 # sezon başına kompozit (16 günlük adım)
def dizi_kur(bolge,yil):
nd=ndvi[(ndvi.bolge==bolge)&(ndvi.yil==yil)].sort_values("doy")
hv=hava[(hava.bolge==bolge)&(hava.yil==yil)]
sezon_temp = hv["t2m"].mean() if len(hv) else 15.0 # boş hava penceresi için yedek
adimlar=[]
for d,n in zip(nd["doy"].values, nd["ndvi"].values):
pen=hv[(hv.doy>=d)&(hv.doy<d+16)]
temp=pen["t2m"].mean() if len(pen) else sezon_temp
rain=pen["yagis"].sum() if len(pen) else 0.0
sicak=float((pen["tmax"]>32).sum()) if len(pen) else 0.0
adimlar.append([n, temp, rain, sicak])
a=np.array(adimlar,dtype=float)
if len(a)==0: return None
# kalan NaN'ları sütun ortalamasıyla doldur
cm=np.nanmean(a,axis=0); cm=np.where(np.isnan(cm),0.0,cm)
idx=np.where(np.isnan(a)); a[idx]=np.take(cm,idx[1])
if len(a)<L: a=np.vstack([a, np.repeat(a[-1:],L-len(a),axis=0)])
return a[:L]
X=[]; y=[]; yilL=[]; bolgeL=[]
for _,r in verim.iterrows():
a=dizi_kur(r["bolge"], r["yil"])
if a is None or np.isnan(a).any(): continue
X.append(a); y.append(r["verim_ton_ha"]); yilL.append(r["yil"]); bolgeL.append(r["bolge"])
X=np.array(X); y=np.array(y,dtype=float); yilL=np.array(yilL)
print(f"Diziler: X={X.shape} (N, adım, özellik), y={y.shape}")
# zaman-ayrımlı bölme
tr=yilL<=2020; te=yilL>=2021
# özellik standardizasyonu (sadece eğitim adımlarından)
mu=X[tr].reshape(-1,X.shape[2]).mean(0); sd=X[tr].reshape(-1,X.shape[2]).std(0)+1e-6
Xs=(X-mu)/sd
ym=y[tr].mean(); ys=y[tr].std()+1e-6; yn=(y-ym)/ys
Xtr=torch.tensor(Xs[tr],dtype=torch.float32); ytr=torch.tensor(yn[tr],dtype=torch.float32).view(-1,1)
Xte=torch.tensor(Xs[te],dtype=torch.float32); yte=y[te]
# eğitimin bir kısmı doğrulama
nval=max(3,int(0.2*len(Xtr))); perm=torch.randperm(len(Xtr))
vi,ti=perm[:nval],perm[nval:]
Xv,yv=Xtr[vi],ytr[vi]; Xt,yt=Xtr[ti],ytr[ti]
class LSTMReg(nn.Module):
def __init__(s,f,h=16):
super().__init__(); s.lstm=nn.LSTM(f,h,batch_first=True); s.do=nn.Dropout(0.3); s.fc=nn.Linear(h,1)
def forward(s,x):
o,_=s.lstm(x); return s.fc(s.do(o[:,-1,:]))
m=LSTMReg(X.shape[2]); opt=torch.optim.Adam(m.parameters(),lr=0.01,weight_decay=1e-3); lf=nn.MSELoss()
tr_loss=[]; va_loss=[]; en_iyi=1e9; sabir=0; en_iyi_durum=None
for ep in range(300):
m.train(); opt.zero_grad(); l=lf(m(Xt),yt); l.backward(); opt.step()
m.eval()
with torch.no_grad(): vl=lf(m(Xv),yv).item()
tr_loss.append(l.item()); va_loss.append(vl)
if vl<en_iyi-1e-4: en_iyi=vl; sabir=0; en_iyi_durum={k:v.clone() for k,v in m.state_dict().items()}
else: sabir+=1
if sabir>=30: break
m.load_state_dict(en_iyi_durum); m.eval()
with torch.no_grad(): pn=m(Xte).numpy().ravel()
p=pn*ys+ym
rmse=float(np.sqrt(mean_squared_error(yte,p))); r2=float(r2_score(yte,p))
print(f"[DL-LSTM] test RMSE={rmse:.3f} ton/ha, R2={r2:.3f} (durduğu epoch {len(tr_loss)})")
plt.figure(figsize=(8,5)); plt.plot(tr_loss,label="eğitim"); plt.plot(va_loss,label="doğrulama")
plt.xlabel("epoch"); plt.ylabel("MSE (ölçekli)"); plt.title("LSTM eğitim/doğrulama kaybı"); plt.legend()
plt.tight_layout(); plt.savefig(SON/"t4_1_kayip_egrisi.png",dpi=200); plt.close()
# Task 3 ile karşılaştırma
try: ml=json.load(open(SON/"ml_sonuclar.json",encoding="utf-8")); en_reg=ml["en_iyi_regresyon"]; klasik=ml["regresyon"][en_reg]
except Exception: klasik={"RMSE":None,"R2":None}; en_reg="?"
sonuc={"dl_lstm":{"RMSE":round(rmse,3),"R2":round(r2,3),"epoch":len(tr_loss)},
"klasik_en_iyi":{"model":en_reg, **klasik},
"N_toplam":int(len(y)),"egitim":int(tr.sum()),"test":int(te.sum()),
"yorum":"N=35 çok küçük; LSTM aşırı öğrenmeye yatkın ve klasik modele belirgin üstünlük beklenmez. Bu ölçekte derin öğrenmenin ek karmaşıklığı genelde gerekçelendirilemez."}
json.dump(sonuc,open(SON/"dl_sonuclar.json","w",encoding="utf-8"),ensure_ascii=False,indent=2)
print("Task 4 bitti. Figür: t4_1_kayip_egrisi.png; metrik: dl_sonuclar.json")
print(json.dumps(sonuc["dl_lstm"]),"vs klasik",json.dumps(sonuc["klasik_en_iyi"]))
C:\Users\cetin\AppData\Local\Temp\ipykernel_30548\2094316391.py:15: Pandas4Warning: Parsing integer or float values with a format in to_datetime is deprecated. In a future version these will be interpreted as epochs via the 'unit' keyword instead. Cast them to strings first to retain the current behavior. hava["doy"]=pd.to_datetime(hava["tarih"],format="%Y%m%d").dt.day_of_year
Diziler: X=(35, 13, 4) (N, adım, özellik), y=(35,)
[DL-LSTM] test RMSE=2.219 ton/ha, R2=0.099 (durduğu epoch 46)
Task 4 bitti. Figür: t4_1_kayip_egrisi.png; metrik: dl_sonuclar.json
{"RMSE": 2.219, "R2": 0.099, "epoch": 46} vs klasik {"model": "Do\u011frusal Regresyon", "RMSE": 2.394, "R2": -0.049}
Task 5 — AI Çiftçi Danışman Ajanı¶
Araç çağırma (NDVI/hava/risk) + RAG (agronomi bilgi tabanı, TF-IDF) + topraklı yanıt. Her iddia araç sonucuna veya getirilen belgeye dayanır; opsiyonel LLM (Gemini) ifadelendirme katmanı, grounding ona bağlı değil. 5 örnek görüşme loglanır.
"""Task 5 — AI Çiftçi Danışman Ajanı (araç çağırma + RAG + topraklı akıl yürütme).
- Araçlar: bölgenin NDVI trendi, hava özeti, risk skoru (eğitilmiş modelden).
- RAG: agronomi bilgi tabanı (bilgi_tabani/*.md) TF-IDF ile indekslenir, soruya göre pasaj getirilir.
- Üretim: Gemini (gemini-3.8-flash) kısa zaman aşımıyla; başarısızsa topraklı ŞABLON (her iddia araç/belgeye dayalı).
- 5 örnek görüşme loglanır; topraklı mı diye işaretlenir."""
import pandas as pd, numpy as np, json, urllib.request, urllib.error
from pathlib import Path
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.preprocessing import StandardScaler
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
KOK = Path.cwd(); VERI=KOK/"veri"; SON=KOK/"sonuclar"; KB=KOK/"bilgi_tabani"
df = pd.read_csv(VERI/"model_tablosu.csv")
OZ = ["ndvi_tepe","ndvi_ort","ndvi_auc","ndvi_yesil_sure","ndvi_tepe_gun","ndvi_son",
"gdd","yagis_toplam","sicak_stres_gun","ort_sicaklik","kurak_gun"]
SURUCU = ["kurak_gun","sicak_stres_gun","yagis_toplam"] # en etkili risk sürücüleri (Task 3)
# --- eğitilmiş sınıflandırıcı (risk) ---
sc = StandardScaler().fit(df[OZ]); clf = GradientBoostingClassifier(random_state=42).fit(sc.transform(df[OZ]), df["risk"].astype(str))
# --- RAG: bilgi tabanı indeksi ---
belgeler=[]; kb_ad=[]
for p in sorted(KB.glob("*.md")):
belgeler.append(p.read_text(encoding="utf-8")); kb_ad.append(p.name)
vekt = TfidfVectorizer().fit(belgeler); kb_mat = vekt.transform(belgeler)
def getir_pasaj(sorgu, k=1):
q = vekt.transform([sorgu]); sim = cosine_similarity(q, kb_mat)[0]
i = int(sim.argmax())
ozet = belgeler[i].split("\n\n")[1][:300] if "\n\n" in belgeler[i] else belgeler[i][:300]
return {"kaynak": kb_ad[i], "benzerlik": round(float(sim[i]),3), "ozet": ozet}
# --- ARAÇLAR (hepsi gerçek veriden; uydurma yok) ---
def arac_ndvi(bolge):
g = df[df.bolge==bolge].sort_values("yil"); s=g.iloc[-1]
trend = "artış" if g["ndvi_tepe"].iloc[-1] >= g["ndvi_tepe"].iloc[:-1].mean() else "düşüş"
return {"bolge":bolge,"son_yil":int(s.yil),"tepe_ndvi":round(float(s.ndvi_tepe),3),"yesil_sure":int(s.ndvi_yesil_sure),"trend":trend}
def arac_hava(bolge):
s = df[df.bolge==bolge].sort_values("yil").iloc[-1]
return {"bolge":bolge,"yil":int(s.yil),"gdd":round(s.gdd),"yagis_toplam_mm":round(s.yagis_toplam),
"kurak_gun":int(s.kurak_gun),"sicak_stres_gun":int(s.sicak_stres_gun)}
def arac_risk(bolge):
s = df[df.bolge==bolge].sort_values("yil").iloc[-1]
x = sc.transform(s[OZ].to_frame().T); tahmin = clf.predict(x)[0]
olasilik = dict(zip(clf.classes_, clf.predict_proba(x)[0].round(3)))
return {"bolge":bolge,"yil":int(s.yil),"risk_tahmini":tahmin,"olasiliklar":{k:float(v) for k,v in olasilik.items()}}
# --- Gemini (opsiyonel ifadelendirme; başarısızsa None) ---
try: ANAHTAR=(Path.home()/".gemini_api_anahtari.txt").read_text().strip()
except Exception: ANAHTAR=None
def gemini_uret(sistem, gercekler, soru):
if not ANAHTAR: return None
istem = (f"{sistem}\n\nYALNIZCA aşağıdaki doğrulanmış gerçekleri kullan, yeni bilgi/rakam UYDURMA. "
f"Bilgi yetersizse söyle.\n\n[GERÇEKLER]\n{gercekler}\n\n[SORU] {soru}\n\nKısa, net, Türkçe yanıt:")
body=json.dumps({"contents":[{"parts":[{"text":istem}]}],"generationConfig":{"temperature":0.2}}).encode()
url=f"https://generativelanguage.googleapis.com/v1beta/models/gemini-3.8-flash:generateContent?key={ANAHTAR}"
try:
req=urllib.request.Request(url,data=body,headers={"Content-Type":"application/json"})
with urllib.request.urlopen(req,timeout=35) as r: d=json.loads(r.read())
return d["candidates"][0]["content"]["parts"][0]["text"].strip()
except Exception: return None
SISTEM=("Sen bir mısır çiftçisine yardımcı olan, veriye dayalı bir tarım danışmanısın. "
"Her iddian ya bir araç sonucuna ya da getirilen rehber belgesine dayanmalı; emin değilsen söyle.")
def yanitla(soru, bolge):
nd=arac_ndvi(bolge); hv=arac_hava(bolge); rk=arac_risk(bolge); pas=getir_pasaj(soru+" "+bolge)
gercekler=(f"NDVI aracı: {nd}\nHava aracı: {hv}\nRisk modeli: {rk}\n"
f"Rehber ({pas['kaynak']}): {pas['ozet']}")
llm=gemini_uret(SISTEM, gercekler, soru)
if llm: yanit=llm; yontem="Gemini (topraklı)"
else: # topraklı şablon
yanit=(f"{bolge} için {rk['yil']} sezonu değerlendirmesi: risk tahmini **{rk['risk_tahmini']}** "
f"(olasılıklar {rk['olasiliklar']}). Hava: GDD {hv['gdd']}, yağış {hv['yagis_toplam_mm']} mm, "
f"kurak gün {hv['kurak_gun']}, sıcak stres günü {hv['sicak_stres_gun']}. "
f"NDVI tepe {nd['tepe_ndvi']} ({nd['trend']}). Rehber ({pas['kaynak']}): {pas['ozet'][:160]}...")
yontem="Topraklı şablon (LLM yok)"
return {"soru":soru,"bolge":bolge,"yontem":yontem,"araclar":{"ndvi":nd,"hava":hv,"risk":rk},
"getirilen_kaynak":pas,"yanit":yanit}
# --- 5 örnek görüşme ---
ornekler=[("Bu sezon riskim nedir ve neden?","ABD (Iowa)"),
("Sulamaya ne zaman öncelik vermeliyim?","Fransa (Beauce)"),
("Sıcak stresi benim için sorun mu?","Çin (Henan)"),
("NDVI verim hakkında ne söylüyor?","Ukrayna (orta)"),
("Önümüzdeki sezon için ne önerirsin?","Romanya (güney)")]
log=[yanitla(s,b) for s,b in ornekler]
json.dump(log, open(SON/"ajan_gorusmeleri.json","w",encoding="utf-8"), ensure_ascii=False, indent=2)
# okunur markdown log + topraklılık kontrolü
with open(SON/"ajan_gorusmeleri.md","w",encoding="utf-8") as f:
f.write("# AI Çiftçi Danışman Ajanı — Örnek Görüşmeler\n\n")
f.write("Her yanıt araç sonuçları + getirilen rehber belgesine dayanır (topraklı). Uydurma rakam yok.\n\n")
for i,c in enumerate(log,1):
f.write(f"## {i}. {c['bolge']} — \"{c['soru']}\"\n")
f.write(f"- **Araç çağrıları:** NDVI={c['araclar']['ndvi']} · Hava={c['araclar']['hava']} · Risk={c['araclar']['risk']}\n")
f.write(f"- **Getirilen kaynak (RAG):** {c['getirilen_kaynak']['kaynak']} (benzerlik {c['getirilen_kaynak']['benzerlik']})\n")
f.write(f"- **Üretim:** {c['yontem']}\n- **Yanıt:** {c['yanit']}\n\n")
print(f"Task 5 bitti. {len(log)} görüşme loglandı. LLM yöntemi: {log[0]['yontem']}")
print("Örnek yanıt:", log[0]['yanit'][:200])
Task 5 bitti. 5 görüşme loglandı. LLM yöntemi: Topraklı şablon (LLM yok)
Örnek yanıt: ABD (Iowa) için 2022 sezonu değerlendirmesi: risk tahmini **orta** (olasılıklar {'düşük_risk': 0.0, 'orta': 1.0, 'yüksek_risk': 0.0}). Hava: GDD 1833, yağış 485 mm, kurak gün 132, sıcak stres günü 35.
Task 6 — Dashboard + sonuç¶
Çok panelli özet dashboard (300 DPI).
"""Task 6 — Çok panelli dashboard (300 DPI). Mevcut figürleri + özet metni tek görselde toplar."""
import json
from pathlib import Path
import matplotlib; matplotlib.use("Agg")
import matplotlib.pyplot as plt, matplotlib.image as mpimg
KOK = Path.cwd(); SON = KOK/"sonuclar"
ml = json.load(open(SON/"ml_sonuclar.json",encoding="utf-8"))
dl = json.load(open(SON/"dl_sonuclar.json",encoding="utf-8"))
paneller = [
("t2_1_ndvi_trajektori.png", "Bölgelere göre NDVI trajektorisi"),
("t2_4_verim_bolge.png", "Bölgelere göre mısır verimi"),
("t2_2_korelasyon.png", "Özellik–verim korelasyonu"),
("t3_2_ozellik_onem.png", "Risk sürücüleri (özellik önemi)"),
("t3_1_confusion.png", "Risk sınıflandırma — confusion"),
("t4_1_kayip_egrisi.png", "LSTM eğitim/doğrulama kaybı"),
]
fig = plt.figure(figsize=(18, 13)); fig.suptitle(
"İklim Riski & Mahsul Verimi — Özet Dashboard\nUydu NDVI (MODIS) × İklim (NASA POWER) × Mısır Verimi (OWID) · 5 ülke, 2016–2022",
fontsize=17, fontweight="bold", y=0.985)
gs = fig.add_gridspec(3, 3, hspace=0.12, wspace=0.06)
konum = [gs[0,0],gs[0,1],gs[0,2],gs[1,0],gs[1,1],gs[1,2]]
for (dosya, baslik), k in zip(paneller, konum):
ax = fig.add_subplot(k); ax.axis("off")
f = SON/dosya
if f.exists(): ax.imshow(mpimg.imread(f)); ax.set_title(baslik, fontsize=11)
# Alt şerit: metin özet
ax = fig.add_subplot(gs[2, :]); ax.axis("off")
er = ml["en_iyi_regresyon"]; rr = ml["regresyon"][er]
ec = ml["en_iyi_siniflandirici"]; rc = ml["siniflandirma"][ec]
ozet = (
f"BULGULAR • Risk sınıflandırma (en iyi: {ec}): doğruluk {rc['accuracy']:.0%}, F1 {rc['F1_macro']:.2f}. "
f"En önemli risk sürücüleri: {', '.join(list(ml['en_onemli_ozellikler'].keys())[:3])} (kuraklık + sıcak stresi).\n"
f"MODEL KARŞILAŞTIRMA • Mutlak verim tahmini zor (N={dl['N_toplam']}): klasik {er} R²={rr['R2']}, "
f"LSTM R²={dl['dl_lstm']['R2']} (RMSE {dl['dl_lstm']['RMSE']} ton/ha). Derin öğrenme az üstün ama bu ölçekte güçlü biçimde gerekçelendirilemez.\n"
f"AI AJAN • Araç çağırma (NDVI/hava/risk) + RAG (agronomi bilgi tabanı) + topraklı yanıt; her iddia veriye/belgeye dayalı, uydurma rakam yok.\n"
f"VERİ • Tümü anahtarsız ve açık: 455 NDVI kompoziti, 6.930 günlük hava kaydı, 35 bölge-sezon. Erişim: 30 Ekim 2026. Akademik proje.")
ax.text(0.01, 0.95, ozet, va="top", ha="left", fontsize=12.5, family="monospace",
bbox=dict(boxstyle="round,pad=0.6", fc="#F7EFDC", ec="#C0830A"))
fig.savefig(SON/"dashboard.png", dpi=300, bbox_inches="tight"); plt.close()
print("dashboard.png (300 DPI) kaydedildi.")
dashboard.png (300 DPI) kaydedildi.
Bulgular, sınırlar ve tartışma¶
- Risk sınıflandırma çalışıyor (Gradient Boosting ~%80 doğruluk); en etkili sürücüler kurak gün + sıcak stresi günü + yağış — agronomik olarak tutarlı.
- Mutlak verim tahmini zayıf (R²≈0): 5 çok farklı ülkenin verimini yalnız iklimden, N=35 ile tahmin etmek zor. LSTM klasik modeli az geçti ama bu ölçekte derin öğrenme güçlü biçimde gerekçelendirilemez.
- AI ajan topraklı: her sayı bir araç/belgeye izlenebilir; uydurma yok. LLM katmanı hız limitinde devre dışı kalınca topraklı şablona düşer (halüsinasyon riski sıfır).
- Sınırlar: nokta bazlı NDVI bir ülkeyi temsilde zayıf; küçük N; ülke düzeyi verim kaba. İyileştirme: daha çok bölge/sezon, çoklu-nokta NDVI, bölge sabit etkileri, ABD-eyalet verimi (USDA NASS anahtarıyla).
- Etik: öneriler karar-destektir, kesin tahmin değil; kritik kararlar yerel uzmanla netleştirilmeli.