"""Read-only EPL audit; standard library only. Rebuild with python3 analiz/epl/epl_analiz.py."""
from pathlib import Path
import sqlite3, csv, json, math, statistics, hashlib
from collections import Counter, defaultdict
ROOT=Path(__file__).resolve().parents[2]; OUT=Path(__file__).resolve().parent
CATS=['1/2','2/1','1/X','2/X','X/1','X/2','X/X']
HB=[1,1.5,1.8,2.2,2.8,4,6,1000]; DB=[1,3.2,3.6,4,4.5,5.5,1000]; AB=HB

def dump(name,rows):
 if not rows:return
 with (OUT/name).open('w',encoding='utf-8-sig',newline='') as f:
  w=csv.DictWriter(f,fieldnames=list(rows[0]));w.writeheader();w.writerows(rows)
def ci(k,n):
 if not n:return (0,1)
 p=k/n;z=1.95996398454;den=1+z*z/n
 c=(p+z*z/(2*n))/den;r=z*math.sqrt(p*(1-p)/n+z*z/(4*n*n))/den
 return c-r,c+r
def fisher(k,n,K,N):
 # One-sided hypergeometric enrichment against the disjoint league complement.
 lo=max(k,0,K-(N-n));hi=min(n,K)
 def lc(a,b):return math.lgamma(a+1)-math.lgamma(b+1)-math.lgamma(a-b+1)
 logs=[lc(K,x)+lc(N-K,n-x)-lc(N,n) for x in range(lo,hi+1)]
 if not logs:return 1.
 mx=max(logs);return min(1.,math.exp(mx)*sum(math.exp(v-mx) for v in logs))
def holm(rows):
 running=0
 for i,r in enumerate(sorted(rows,key=lambda x:x['p'])):
  running=max(running,r['p']*(len(rows)-i));r['p_holm']=min(1.,running)
def result(h,a):return '1' if h>a else '2' if h<a else 'X'
def bucket(x,b):return next((i for i in range(len(b)-1) if b[i]<=x<b[i+1]),-1)
def profile(key):
 labels=[]
 for name,b,i in zip(['MS1','MSX','MS2'],[HB,DB,AB],key):
  lo=f'{b[i]:.2f}'.replace('.',',');hi=f'{b[i+1]:.2f}'.replace('.',',')
  labels.append(f'{name} ≥ {lo}' if b[i+1]==1000 else f'{lo} ≤ {name} < {hi}')
 return ' · '.join(labels)
def pct(k,n):return f'{100*k/n:.2f}%' if n else '—'
def med(v):return statistics.median(v) if v else 0

def main():
 conn=sqlite3.connect(f'file:{ROOT / "football_data_v1.db"}?mode=ro',uri=True);conn.row_factory=sqlite3.Row
 raw=[dict(r) for r in conn.execute("select rowid as id,Date,Season,HomeTeam,AwayTeam,FTHG,FTAG,HTHG,HTAG,FTR,HTR,B365H,B365D,B365A from matches where Div='E0'")];conn.close()
 seen=set();valid=[];a=[];bad=[];mismatch=0;dups=0
 for r in raw:
  key=(r['Date'],r['HomeTeam'],r['AwayTeam'])
  if key in seen:dups+=1;bad.append(dict(r,reason='duplicate'));continue
  seen.add(key)
  scores=[r[c] for c in ['FTHG','FTAG','HTHG','HTAG']]
  if not all(isinstance(x,(int,float)) and math.isfinite(x) and x>=0 and x==int(x) for x in scores):bad.append(dict(r,reason='missing_or_invalid_score'));continue
  fh,fa,hh,ha=scores
  if hh>fh or ha>fa:bad.append(dict(r,reason='HT_exceeds_FT'));continue
  if r['Date']>'2026-10-25':bad.append(dict(r,reason='future'));continue
  r['HTFT']=result(hh,ha)+'/'+result(fh,fa)
  mismatch+=int(r['HTR']!={'1':'H','X':'D','2':'A'}[r['HTFT'][0]] or r['FTR']!={'1':'H','X':'D','2':'A'}[r['HTFT'][-1]])
  valid.append(r)
  odds=[r[c] for c in ['B365H','B365D','B365A']]
  if not all(isinstance(x,(int,float)) and math.isfinite(x) and x>1 for x in odds):continue
  r['key']=tuple(bucket(x,b) for x,b in zip(odds,[HB,DB,AB]))
  if -1 in r['key']:continue
  r['part']='discovery' if r['Date']<'2020-07-01' else 'validation' if r['Date']<'2023-07-01' else 'test'
  a.append(r)
 parts={p:[r for r in a if r['part']==p] for p in ['discovery','validation','test']}
 counts=Counter(r['HTFT'] for r in a);grid=defaultdict(list)
 for r in a:grid[r['key']].append(r)
 profiles=[]
 for key,g in sorted(grid.items()):
  for cat in CATS:
   row={'cat':cat,'profile_MS1_MSX_MS2':profile(key),'n':len(g),'k':sum(r['HTFT']==cat for r in g)}
   row['rate']=row['k']/row['n'];row['ci_low'],row['ci_high']=ci(row['k'],row['n'])
   for p in parts:
    sub=[r for r in g if r['part']==p];row[p+'_n']=len(sub);row[p+'_k']=sum(r['HTFT']==cat for r in sub)
   row['key']=key;profiles.append(row)
 selected=[]
 for cat in CATS:
  candidates=[r for r in profiles if r['cat']==cat and r['discovery_n']>=100 and r['discovery_k']>=5]
  if not candidates:continue
  chosen=max(candidates,key=lambda r:ci(r['discovery_k'],r['discovery_n'])[0]);chosen=chosen.copy()
  vn,vk=chosen['validation_n'],chosen['validation_k'];tn,tk=chosen['test_n'],chosen['test_k']
  vb=sum(r['HTFT']==cat for r in parts['validation'])/len(parts['validation'])
  chosen['validation_pass']=vn>=30 and vk>=3 and vk/vn>vb
  K=sum(r['HTFT']==cat for r in parts['test']);N=len(parts['test'])
  chosen['p']=fisher(tk,tn,K,N) if tn else 1;chosen['test_baseline']=K/N
  selected.append(chosen)
 holm(selected)
 for r in selected:r['supported']=r['validation_pass'] and r['test_n']>=30 and r['test_k']/r['test_n']>r['test_baseline'] and r['p_holm']<.05
 # Role-specific team frequencies. Every eligible test is included in one Holm family.
 teams=[]
 for side,col in [('Ev','HomeTeam'),('Deplasman','AwayTeam')]:
  groups=defaultdict(list)
  for r in a:groups[r[col]].append(r)
  for team,g in groups.items():
   if len(g)<80:continue
   for cat in CATS:
    n=len(g);k=sum(r['HTFT']==cat for r in g);N=len(a);K=counts[cat]
    # Descriptive odds adjustment: leave-team-out rate in the same 3D odds cell.
    expected=0
    for match in g:
     peers=[r for r in grid[match['key']] if r[col]!=team]
     expected+=(sum(r['HTFT']==cat for r in peers)+20*K/N)/(len(peers)+20)
    row=dict(cat=cat,role=side,team=team,n=n,k=k,rate=k/n,expected=expected,observed_expected=k/expected if expected else 0,p=fisher(k,n,K,N))
    row['ci_low'],row['ci_high']=ci(k,n)
    for p in parts:
     sub=[r for r in g if r['part']==p];row[p+'_n']=len(sub);row[p+'_k']=sum(r['HTFT']==cat for r in sub)
    teams.append(row)
 holm(teams)
 for r in teams:
  tbase=sum(m['HTFT']==r['cat'] for m in parts['test'])/len(parts['test'])
  r['recent_repeat']=r['test_n']>=30 and r['test_k']>=3 and r['test_k']/r['test_n']>tbase
  r['historical_flag']=r['p_holm']<.05 and r['observed_expected']>1.1
 dump('oran_profilleri.csv',[{k:v for k,v in r.items() if k!='key'} for r in profiles]);dump('takim_tum_sonuclar.csv',teams)
 dump('hedef_maclar.csv',[{k:v for k,v in r.items() if k!='key'} for r in a if r['HTFT'] in CATS]);dump('dislanan_kayitlar.csv',bad)
 seasonal=[]
 for season in sorted({r['Season'] for r in raw}):
  rr=[r for r in raw if r['Season']==season];gg=[r for r in a if r['Season']==season]
  seasonal.append(dict(season=season,raw_n=len(rr),odds_valid_n=len(gg),**{cat:sum(r['HTFT']==cat for r in gg) for cat in CATS}))
 dump('sezon_kapsami.csv',seasonal)
 report=[]
 def add(s=''):report.append(s)
 add('# İngiltere — EPL: MS oranları ile İY/MS analizi');add('\nHazırlanma: 25 Ekim 2026. Yalnızca projedeki yerel veriler incelendi; dış kaynak doğrulaması yapılmadı. Diğer ligler bu rapora dahil edilmedi.\n')
 add('## Kaynak ve veri denetimi\n')
 add(f'- Ana kaynak: `football_data_v1.db`, `matches`, `Div=E0`. Ham kayıt: **{len(raw):,}**; geçerli İY/MS: **{len(valid):,}**; üç MS oranı da geçerli analiz örneklemi: **{len(a):,}**. Geçerli skoru olup oranı eksik/geçersiz: {len(valid)-len(a):,}.')
 add(f'- Oran örneklemi tarihleri: **{min(r["Date"] for r in a)} — {max(r["Date"] for r in a)}**. Skor verisi: {min(r["Date"] for r in valid)} — {max(r["Date"] for r in valid)}.')
 add(f'- Tekrarlanan tarih/ev/deplasman: {dups}; sonuç etiketi ile skordan hesaplanan sonuç uyuşmazlığı: {mismatch}. Sonuçlar tam sayı, negatif olmayan skorlarla yeniden hesaplandı; İY skoru MS skorunu aşan kayıtlar dışlandı.')
 add('- Alternatif `football_data_v2.db` EPL kapsamı 2020/21–2025/26, 2.280 kayıt. Daha dar tarih kapsamı ve sağlayıcısı açıkça belirtilmeyen oran alanları nedeniyle ana veriyle birleştirilmedi; oran sağlayıcıları karıştırılmadı.')
 add('- Tek oran ailesi: **B365H / B365D / B365A**, sırasıyla MS1 / MSX / MS2. Kapanış alanları B365CH/CD/CA kullanılmadı. DB tek başına kesin fiyat alma zamanını kanıtlamaz; bunlara kesin açılış oranı denmiyor.')
 add('- 2003/04 ve 2004/05 sezonlarında DB yalnızca 335’er maç içeriyor; sezonlar eksik. Son DB tarihi bugünden önce: rapor güncel sezon akışı değildir. Yerel verinin gerçek sonuçlara uygunluğu bağımsız doğrulanmadı.')
 add('- `1` ev sahibi, `2` deplasman, `X` beraberliktir. **X/X dahildir.** 1/1 ve 2/2 hedef sonuçlardan çıkarıldı; yüzdelerin paydasında tüm geçerli maçlar tutuldu. “Mimli” yalnızca istatistiksel tekrar demektir, usulsüzlük iddiası değildir.\n')
 add('## Yedi sonucun sıklığı\n');add('| İY/MS | Adet / tüm maç | Sıklık | %95 Wilson aralığı | MS1 / MSX / MS2 medyanı |');add('|---|---:|---:|---|---|')
 for cat in CATS:
  g=[r for r in a if r['HTFT']==cat];k=len(g);lo,hi=ci(k,len(a));mm=' / '.join(f'{med([r[c] for r in g]):.2f}' for c in ['B365H','B365D','B365A'])
  add(f'| {cat} | {k} / {len(a)} | {pct(k,len(a))} | %{100*lo:.2f}–{100*hi:.2f} | {mm} |')
 add(f'\nYedi hedefin toplamı: {sum(counts[c] for c in CATS)} / {len(a)} ({pct(sum(counts[c] for c in CATS),len(a))}). Hariç tutulanlar: 1/1={counts["1/1"]}, 2/2={counts["2/2"]}. Medyanlar olay gerçekleşmiş maçları tarif eder; seçim kuralı değildir.\n')
 add('## Hangi oranlarda hangi sonuç görüldü?\n')
 add('Önceden tanımlı, çakışmayan üçlü oran hücreleri kullanıldı. Oranlar MS1, MSX ve MS2 adıyla yazılır. Örneğin “1,50 ≤ MS1 < 1,80”, 1,50 dahil ve 1,80 hariç demektir; “MS2 ≥ 6,00” ise 6,00 ve üzeridir. Her sonuç için en az 100 maçı olan hücrelerden en yüksek tarihsel sıklıklı üçü aşağıdadır. Bunlar keşif bulgularıdır; sonuç görüldükten sonra sıralandıkları için tahmin gücü kanıtı sayılmaz. Tam hücre dökümü ekte.\n')
 for cat in CATS:
  add(f'\n### {cat}\n');add('| MS1 / MSX / MS2 | Olay / maç | Sıklık | Lig ortalamasına oran |');add('|---|---:|---:|---:|')
  top=sorted([r for r in profiles if r['cat']==cat and r['n']>=100],key=lambda r:-r['rate'])[:3]
  for r in top:add(f'| {r["profile_MS1_MSX_MS2"]} | {r["k"]}/{r["n"]} | {pct(r["k"],r["n"])} | {r["rate"]/(counts[cat]/len(a)):.2f}× |')
 add('\n## Zaman ayrımında sınama\n')
 add('Keşif: 1 Temmuz 2020 öncesi; doğrulama: 1 Temmuz 2020–30 Haziran 2023; test: 1 Temmuz 2023 sonrası. Her sonuç için yalnız keşifte en az 100 maç/5 olay içeren hücreler arasından Wilson alt sınırı en yüksek tek hücre donduruldu. Doğrulama: ≥30 maç, ≥3 olay ve dönem lig ortalamasından yüksek oran. Testte ≥30 maç, artış ve yedi karşılaştırmaya Holm düzeltmeli tek yönlü Fisher p<0,05 arandı. Test dönemi hücre seçiminde kullanılmadı. Bu geniş hücre taraması dar oranların tüm olası analizlerini temsil etmez.\n')
 add('| Sonuç | MS1 / MSX / MS2 | Keşif | Doğrulama | Test | Test lig oranı | Holm p | Destek |');add('|---|---|---:|---:|---:|---:|---:|---|')
 for r in selected:
  s=[f'{r[p+"_k"]}/{r[p+"_n"]} ({pct(r[p+"_k"],r[p+"_n"])})' for p in parts]
  add(f'| {r["cat"]} | {r["profile_MS1_MSX_MS2"]} | '+ ' | '.join(s)+f' | %{100*r["test_baseline"]:.2f} | {r["p_holm"]:.4f} | {"Var" if r["supported"] else "Yok"} |')
 add('\nBu test yalnız tüm maçlara göre zenginleşmeyi ölçer; 1-X-2 fiyatlarının ötesinde ek tahmin gücünü veya bahis kârlılığını kanıtlamaz. İY/MS bahis oranları mevcut analizde olmadığı için getiri/ROI hesaplanamaz.\n')
 add('## Takımlar: hangi rolde, hangi sonuçta tekrar var?\n')
 add(f'En az 80 maçlık ev veya deplasman rolü ayrı değerlendirildi. Sıralama Wilson alt sınırına göredir. Her satırdaki yüzde takımın o roldeki **tüm** maçlarına göredir. Ham sayının tek başına uzun süre ligde kalmayı ödüllendirmesi önlendi. {len(teams)} takım/rol/sonuç testi tek Holm ailesinde düzeltildi.\n')
 add('“Orana göre O/B”: gözlenen olay / aynı üçlü oran hücrelerindeki diğer takım maçlarından beklenen olay. Beklenen hesapta 20 maç ağırlığında lig ortalamasına yumuşatma kullanıldı. Bu, dönem/rakip/kadro etkilerini kontrol eden bir model veya bağımsız anlamlılık testi değildir. “Tarihsel işaret” için düzeltilmiş ham frekans p<0,05 ve O/B>1,10 şartı var; “yakın tekrar” test döneminde ≥30 maç, ≥3 olay ve dönem lig ortalamasının üstünü ifade eder. Yakın tekrar bağımsız doğrulama sayılmaz; takım seçimi tüm geçmişten yapılmıştır.\n')
 for cat in CATS:
  add(f'\n### {cat} — öne çıkan takım/roller\n');add('| Takım | Rol | Olay / maç | Sıklık (%95 aralık) | Orana göre O/B | Holm p | Test dönemi | İşaret / yakın tekrar |');add('|---|---|---:|---|---:|---:|---|---|')
  rr=sorted([r for r in teams if r['cat']==cat and r['k']>=5],key=lambda r:-r['ci_low'])[:6]
  for r in rr:
   add(f'| {r["team"]} | {r["role"]} | {r["k"]}/{r["n"]} | %{100*r["rate"]:.2f} (%{100*r["ci_low"]:.2f}–{100*r["ci_high"]:.2f}) | {r["observed_expected"]:.2f}× | {r["p_holm"]:.4f} | {r["test_k"]}/{r["test_n"]} | {"Var" if r["historical_flag"] else "Yok"} / {"Var" if r["recent_repeat"] else "Yok"} |')
 add('\n## Takım rolünü okuma\n')
 add('- 1/2: ev sahibi önde girdiği maçı kaybetti; deplasman geriden kazandı. 2/1 bunun tersidir.\n- 1/X: ev sahibi üstünlüğünü kaybetti; deplasman geriden beraberlik aldı. 2/X bunun tersidir.\n- X/1: ev sahibi ikinci yarıda kazandı; X/2: deplasman ikinci yarıda kazandı. X/X: hem devre hem maç sonucu beraberlik.\n- Tablolardaki aynı sonuç iki takımın farklı hikâyesini anlatır; takımlar birleştirilerek “geri dönüşçü” diye etiketlenmedi.\n')
 add('## Kısa değerlendirme\n')
 supported=[r['cat'] for r in selected if r['supported']]
 add('Zaman ayrımlı oran testinden geçen sonuçlar: **'+(', '.join(supported) if supported else 'yok')+'**. Geçmeyen profiller tarihsel gözlem olarak kalır.\n')
 for cat in CATS:
  flags=sorted([r for r in teams if r['cat']==cat and r['historical_flag']],key=lambda r:-r['ci_low'])
  add(f'- **{cat}:** '+('; '.join(f'{r["team"]} ({r["role"]}, {r["k"]}/{r["n"]}, O/B {r["observed_expected"]:.2f}×, yakın tekrar {"var" if r["recent_repeat"] else "yok"})' for r in flags[:5]) if flags else 'katı tarihsel işaret ölçütünü karşılayan takım/rol yok.'))
 add('\nSeyrek 1/2 ve 2/1 sonuçlarında yüksek görünen yüzdeler birkaç maçla değişebilir. Geçmişte tekrar eden takım davranışı bugünkü kadro/teknik direktör için otomatik taşınmaz. Bu rapor maç seçimi veya kazanç garantisi değildir.\n')
 add('## Denetim dosyaları\n')
 for name,desc in [('oran_profilleri.csv','Tüm üçlü oran hücreleri ve yedi sonuç; dönem sayımları'),('takim_tum_sonuclar.csv','Tüm uygun takım/rol/sonuçlar; güven aralıkları ve düzeltmeler'),('hedef_maclar.csv','Yedi hedefin maç bazında tarih, takımlar, skorlar ve MS oranları'),('dislanan_kayitlar.csv','Skor/tekrar/tarih nedeniyle dışlanan kayıtlar'),('sezon_kapsami.csv','Sezon bazında kapsam ve sonuç sayımları'),('epl_analiz.py','Tekrar üretim kodu'),('denetim.json','Kaynak özeti ve test seçimleri')]:add(f'- [{name}]({OUT / name}): {desc}.')
 (OUT/'EPL_Raporu.md').write_text('\n'.join(report)+'\n',encoding='utf-8')
 audit=dict(raw=len(raw),valid_scores=len(valid),valid_odds=len(a),duplicates=dups,mismatches=mismatch,excluded=len(bad),parts={p:len(g) for p,g in parts.items()},counts=dict(counts),selected=selected,team_tests=len(teams),source_sha256=hashlib.file_digest((ROOT/'football_data_v1.db').open('rb'),'sha256').hexdigest())
 (OUT/'denetim.json').write_text(json.dumps(audit,ensure_ascii=False,indent=2),encoding='utf-8')
 assert len(a)==sum(counts.values())
 assert sum(r['n'] for r in profiles if r['cat']==CATS[0])==len(a)
 assert len(valid)+len(bad)==len(raw)
 print(json.dumps({k:v for k,v in audit.items() if k not in ['selected','source_sha256']},ensure_ascii=False,indent=2))
 print('SUPPORTED',supported)
 for cat in CATS:
  rr=sorted([r for r in teams if r['cat']==cat],key=lambda r:-r['ci_low'])[:3]
  print(cat,[(r['team'],r['role'],r['k'],r['n'],round(r['p_holm'],4),round(r['observed_expected'],2)) for r in rr])
if __name__=='__main__':main()
