import glob import dash import dash_bootstrap_components as dbc import numpy as np import pandas as pd import plotly.express as px import plotly.graph_objects as go from dash import Input, Output, dcc, html # ─── VERİ YÜKLEMESİ ────────────────────────────────────────────────────────── def load_data(): files = sorted(glob.glob("ookla_turkey_*.parquet")) if not files: raise FileNotFoundError( "Parquet dosyası bulunamadı. Önce index.py'yi çalıştırın." ) latest = files[-1] df = pd.read_parquet(latest) df["download_mbps"] = (df["avg_d_kbps"] / 1000).round(2) df["upload_mbps"] = (df["avg_u_kbps"] / 1000).round(2) df["latency_ms"] = df["avg_lat_ms"].astype(float) df["lat_down_ms"] = pd.to_numeric(df["avg_lat_down_ms"], errors="coerce") df["lat_up_ms"] = pd.to_numeric(df["avg_lat_up_ms"], errors="coerce") # Coğrafi bölge etiketi (0.5° grid) df["lon_bin"] = (df["tile_x"] // 2 * 2).astype(int) df["lat_bin"] = (df["tile_y"] // 2 * 2 + 0.5).round(1) return df, latest df, DATA_FILE = load_data() # Harita için örneklenmiş veri (scatter modunda performans) DF_SAMPLE = df.sample(min(len(df), 20_000), random_state=42) # ─── STİL SABİTLERİ ────────────────────────────────────────────────────────── BG_BASE = "#0d0d1a" BG_CARD = "#16162a" BG_HEADER = "#12122a" BORDER_CLR = "#2a2a42" TEXT_PRI = "#e8e8f0" TEXT_MUT = "#8888aa" PLOTLY_LAYOUT = dict( paper_bgcolor=BG_CARD, plot_bgcolor=BG_CARD, font=dict(color=TEXT_PRI, family="Inter, system-ui, sans-serif", size=11), margin=dict(l=12, r=12, t=36, b=12), xaxis=dict(gridcolor=BORDER_CLR, zerolinecolor=BORDER_CLR, linecolor=BORDER_CLR), yaxis=dict(gridcolor=BORDER_CLR, zerolinecolor=BORDER_CLR, linecolor=BORDER_CLR), title_font=dict(size=13, color=TEXT_PRI), ) # Metrik konfigürasyonları METRICS = { "download_mbps": { "label": "📥 Download (Mbps)", "short": "Download", "unit": "Mbps", "color": "#00d4aa", "scale": "Plasma", "cap": 500, }, "upload_mbps": { "label": "📤 Upload (Mbps)", "short": "Upload", "unit": "Mbps", "color": "#4facfe", "scale": "Viridis", "cap": 200, }, "latency_ms": { "label": "⏱ Gecikme (ms)", "short": "Gecikme", "unit": "ms", "color": "#f6d860", "scale": "RdYlGn_r", "cap": 200, }, "lat_down_ms": { "label": "🔽 Yük-altı DL Gecikme (ms)", "short": "DL Gecikme", "unit": "ms", "color": "#fb923c", "scale": "RdYlGn_r", "cap": 200, }, "lat_up_ms": { "label": "🔼 Yük-altı UL Gecikme (ms)", "short": "UL Gecikme", "unit": "ms", "color": "#c084fc", "scale": "RdYlGn_r", "cap": 200, }, "tests": { "label": "🧪 Test Sayısı", "short": "Test", "unit": "adet", "color": "#a78bfa", "scale": "Blues", "cap": None, }, } # ─── BİLEŞEN YARDIMCILARI ──────────────────────────────────────────────────── def stat_card(icon, title, value, unit, color, card_id=None): return dbc.Col( dbc.Card( dbc.CardBody([ html.Div([ html.Span(icon, style={"fontSize": "1.5rem"}), html.Div([ html.P(title, className="mb-0", style={"fontSize": "0.7rem", "color": TEXT_MUT, "textTransform": "uppercase", "letterSpacing": "0.08em"}), html.H4( id=card_id or f"stat-{title}", children=f"{value} {unit}", className="mb-0 fw-bold", style={"color": color, "fontSize": "1.3rem"}, ), ], className="ms-3"), ], className="d-flex align-items-center"), ]), className="border-0 h-100", style={"background": BG_CARD, "borderLeft": f"3px solid {color} !important", "borderRadius": "10px"}, ), md=3, className="mb-3", ) def section_header(title): return html.Div( html.H6(title, className="mb-0 fw-semibold", style={"color": TEXT_MUT, "fontSize": "0.75rem", "textTransform": "uppercase", "letterSpacing": "0.1em"}), className="mb-3 pb-2", style={"borderBottom": f"1px solid {BORDER_CLR}"}, ) # ─── LAYOUT ────────────────────────────────────────────────────────────────── app = dash.Dash( __name__, external_stylesheets=[ dbc.themes.CYBORG, "https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap", ], meta_tags=[{"name": "viewport", "content": "width=device-width, initial-scale=1"}], ) app.title = "Ookla TR Analytics" app.layout = dbc.Container( [ # ── HEADER ──────────────────────────────────────────────────────────── dbc.Row( dbc.Col( html.Div([ html.Div([ html.H4("🇹🇷 Ookla Speedtest — Türkiye Analiz Paneli", className="mb-0 fw-bold", style={"color": TEXT_PRI, "fontSize": "1.1rem"}), html.Small( f"{DATA_FILE} • {len(df):,} tile • " "Kaynak: Ookla Open Data (CC BY-NC-SA 4.0)", style={"color": TEXT_MUT}, ), ]), html.Span("LIVE", className="badge rounded-pill", style={"background": "#00d4aa22", "color": "#00d4aa", "border": "1px solid #00d4aa55", "padding": "4px 10px"}), ], className="d-flex justify-content-between align-items-center py-3"), ), ), html.Hr(style={"borderColor": BORDER_CLR, "margin": "0 0 20px 0"}), # ── STAT KARTLARI ───────────────────────────────────────────────────── dbc.Row(id="stat-row", children=[ stat_card("⬇️", "Ort. Download", f"{df.download_mbps.mean():.1f}", "Mbps", "#00d4aa", "stat-dl"), stat_card("⬆️", "Ort. Upload", f"{df.upload_mbps.mean():.1f}", "Mbps", "#4facfe", "stat-ul"), stat_card("🏓", "Ort. Gecikme", f"{df.latency_ms.mean():.0f}", "ms", "#f6d860", "stat-lat"), stat_card("📍", "Toplam Tile", f"{len(df):,}", "adet", "#a78bfa", "stat-tiles"), ], className="g-3 mb-3"), # ── YÜK-ALTI GECİKME ÖZET KARTLARI ─────────────────────────────────── dbc.Row([ stat_card("🔽", "Yük-altı DL Gecikme", f"{df.lat_down_ms.mean():.0f}", "ms", "#fb923c", "stat-lat-dl"), stat_card("🔼", "Yük-altı UL Gecikme", f"{df.lat_up_ms.mean():.0f}", "ms", "#c084fc", "stat-lat-ul"), stat_card("🧪", "Toplam Test", f"{df.tests.sum():,}", "test", "#38bdf8", "stat-tests"), stat_card("📱", "Toplam Cihaz", f"{df.devices.sum():,}", "cihaz", "#34d399", "stat-devices"), ], className="g-3 mb-4"), # ── HARİTA + FİLTRELER ──────────────────────────────────────────────── dbc.Row([ # Sol: kontroller dbc.Col([ dbc.Card([ dbc.CardBody([ section_header("Harita Ayarları"), html.Label("Gösterilecek Metrik", className="text-muted mb-1", style={"fontSize": "0.8rem"}), dcc.Dropdown( id="metric-select", options=[{"label": v["label"], "value": k} for k, v in METRICS.items()], value="download_mbps", clearable=False, className="mb-3", style={"fontSize": "0.85rem"}, ), html.Label("Harita Modu", className="text-muted mb-1", style={"fontSize": "0.8rem"}), dbc.RadioItems( id="map-type", options=[ {"label": " Yoğunluk (Heatmap)", "value": "density"}, {"label": " Nokta (Scatter)", "value": "scatter"}, ], value="density", className="mb-3", inputClassName="me-1", ), html.Hr(style={"borderColor": BORDER_CLR}), section_header("Filtrele"), html.Label("Download Hızı (Mbps)", className="text-muted mb-1", style={"fontSize": "0.8rem"}), dcc.RangeSlider( id="dl-range", min=0, max=500, step=10, value=[0, 500], marks={0: "0", 100: "100", 250: "250", 500: "500+"}, tooltip={"placement": "bottom", "always_visible": False}, className="mb-3", ), html.Label("Gecikme (ms)", className="text-muted mb-1", style={"fontSize": "0.8rem"}), dcc.RangeSlider( id="lat-range", min=0, max=200, step=5, value=[0, 200], marks={0: "0", 50: "50", 100: "100", 200: "200+"}, tooltip={"placement": "bottom", "always_visible": False}, className="mb-3", ), html.Hr(style={"borderColor": BORDER_CLR}), html.Div(id="filter-info", className="text-muted", style={"fontSize": "0.78rem"}), ]) ], className="border-0 h-100", style={"background": BG_CARD, "borderRadius": "10px"}), ], md=3), # Sağ: Harita dbc.Col([ dbc.Card([ dbc.CardBody( dcc.Graph( id="map-graph", style={"height": "470px"}, config={"scrollZoom": True, "displayModeBar": True, "modeBarButtonsToRemove": ["select2d", "lasso2d"]}, ), className="p-1", ) ], className="border-0", style={"background": BG_CARD, "borderRadius": "10px"}), ], md=9), ], className="g-3 mb-4"), # ── FOOTER ──────────────────────────────────────────────────────────── html.Hr(style={"borderColor": BORDER_CLR}), html.P( "Speedtest® by Ookla® — CC BY-NC-SA 4.0 | Türkiye Mobil Ağ Performansı", className="text-center pb-3", style={"color": TEXT_MUT, "fontSize": "0.75rem"}, ), ], fluid=True, style={ "background": BG_BASE, "minHeight": "100vh", "fontFamily": "Inter, system-ui, sans-serif", "padding": "0 24px", }, ) # ─── CALLBACKS ─────────────────────────────────────────────────────────────── def apply_filters(dl_range, lat_range): mask = ( (df["download_mbps"] >= dl_range[0]) & (df["download_mbps"] <= dl_range[1]) & (df["latency_ms"] >= lat_range[0]) & (df["latency_ms"] <= lat_range[1]) ) return df[mask] # Filtre bilgisi @app.callback( Output("filter-info", "children"), Input("dl-range", "value"), Input("lat-range", "value"), ) def update_filter_info(dl_range, lat_range): dff = apply_filters(dl_range, lat_range) pct = len(dff) / len(df) * 100 return [ html.Span(f"Filtrelenmiş: ", style={"color": TEXT_MUT}), html.Span(f"{len(dff):,} tile ", style={"color": TEXT_PRI, "fontWeight": "600"}), html.Span(f"({pct:.1f}%)", style={"color": TEXT_MUT}), ] # Harita @app.callback( Output("map-graph", "figure"), Input("metric-select", "value"), Input("map-type", "value"), Input("dl-range", "value"), Input("lat-range", "value"), ) def update_map(metric, map_type, dl_range, lat_range): dff = apply_filters(dl_range, lat_range) m = METRICS[metric] colorbar = dict( bgcolor=BG_CARD, tickfont=dict(color=TEXT_PRI, size=10), title=dict(text=m["unit"], font=dict(color=TEXT_MUT, size=10)), thickness=12, len=0.75, ) if map_type == "density": fig = px.density_mapbox( dff, lat="tile_y", lon="tile_x", z=metric, radius=7, zoom=5, center={"lat": 39.0, "lon": 35.5}, mapbox_style="carto-darkmatter", color_continuous_scale=m["scale"], opacity=0.85, ) else: sample = dff.sample(min(len(dff), 20_000), random_state=42) fig = px.scatter_mapbox( sample, lat="tile_y", lon="tile_x", color=metric, zoom=5, center={"lat": 39.0, "lon": 35.5}, mapbox_style="carto-darkmatter", color_continuous_scale=m["scale"], opacity=0.75, ) fig.update_traces(marker=dict(size=4)) fig.update_layout( paper_bgcolor=BG_CARD, margin=dict(l=0, r=0, t=0, b=0), coloraxis_colorbar=colorbar, uirevision="map", # zoom/pan korunur ) return fig # (grafik bölümleri kaldırıldı) def update_charts(dl_range, lat_range): # artık çağrılmıyor dff = apply_filters(dl_range, lat_range) dl_cap = dff["download_mbps"].quantile(0.99) ul_cap = dff["upload_mbps"].quantile(0.99) lat_cap = min(dff["latency_ms"].quantile(0.99), 200) # ── Download histogram ──────────────────────────────────────────────────── hist_dl = px.histogram( dff[dff["download_mbps"] <= dl_cap], x="download_mbps", nbins=60, color_discrete_sequence=["#00d4aa"], title="📥 Download Dağılımı", labels={"download_mbps": "Mbps"}, ) hist_dl.update_traces(marker_line_width=0, opacity=0.85) hist_dl.add_vline( x=dff["download_mbps"].median(), line_dash="dash", line_color="#ffffff55", annotation_text=f"Med: {dff['download_mbps'].median():.0f}", annotation_font_color=TEXT_PRI, annotation_font_size=10, ) hist_dl.update_layout(**PLOTLY_LAYOUT, yaxis_title="Tile Sayısı", height=240) # ── Upload histogram ────────────────────────────────────────────────────── hist_ul = px.histogram( dff[dff["upload_mbps"] <= ul_cap], x="upload_mbps", nbins=60, color_discrete_sequence=["#4facfe"], title="📤 Upload Dağılımı", labels={"upload_mbps": "Mbps"}, ) hist_ul.update_traces(marker_line_width=0, opacity=0.85) hist_ul.add_vline( x=dff["upload_mbps"].median(), line_dash="dash", line_color="#ffffff55", annotation_text=f"Med: {dff['upload_mbps'].median():.0f}", annotation_font_color=TEXT_PRI, annotation_font_size=10, ) hist_ul.update_layout(**PLOTLY_LAYOUT, yaxis_title="Tile Sayısı", height=240) # ── Gecikme histogram ───────────────────────────────────────────────────── hist_lat = px.histogram( dff[dff["latency_ms"] <= lat_cap], x="latency_ms", nbins=50, color_discrete_sequence=["#f6d860"], title="⏱ Gecikme Dağılımı", labels={"latency_ms": "ms"}, ) hist_lat.update_traces(marker_line_width=0, opacity=0.85) hist_lat.add_vline( x=dff["latency_ms"].median(), line_dash="dash", line_color="#ffffff55", annotation_text=f"Med: {dff['latency_ms'].median():.0f}ms", annotation_font_color=TEXT_PRI, annotation_font_size=10, ) hist_lat.update_layout(**PLOTLY_LAYOUT, yaxis_title="Tile Sayısı", height=240) # ── Download vs Upload scatter ──────────────────────────────────────────── sample = dff.sample(min(len(dff), 6_000), random_state=42) scatter = px.scatter( sample, x="download_mbps", y="upload_mbps", color="latency_ms", title="📊 Download vs Upload (gecikmeyle renklendirilmiş)", labels={"download_mbps": "Download (Mbps)", "upload_mbps": "Upload (Mbps)", "latency_ms": "Gecikme (ms)"}, color_continuous_scale="RdYlGn_r", range_x=[0, dl_cap], range_y=[0, ul_cap], opacity=0.55, ) scatter.update_traces(marker=dict(size=3)) scatter.update_layout( **PLOTLY_LAYOUT, height=280, coloraxis_colorbar=dict(bgcolor=BG_CARD, tickfont=dict(color=TEXT_PRI, size=9), title=dict(text="ms", font=dict(color=TEXT_MUT, size=10)), thickness=10, len=0.8), ) # ── Box plot: DL / UL hız dağılımı ─────────────────────────────────────── box_data = pd.DataFrame({ "Mbps": pd.concat([ dff.loc[dff["download_mbps"] <= dl_cap, "download_mbps"], dff.loc[dff["upload_mbps"] <= ul_cap, "upload_mbps"], ], ignore_index=True), "Tür": (["Download"] * (dff["download_mbps"] <= dl_cap).sum() + ["Upload"] * (dff["upload_mbps"] <= ul_cap).sum()), }) box = px.box( box_data, x="Tür", y="Mbps", color="Tür", title="📦 Hız Dağılımı (Box Plot)", color_discrete_map={"Download": "#00d4aa", "Upload": "#4facfe"}, points=False, ) box.update_layout(**PLOTLY_LAYOUT, height=280, showlegend=False) # ════════════════════════════════════════════════════════════════════════════ # YÜK-ALTI GECİKME SEKMESI # ════════════════════════════════════════════════════════════════════════════ dff_ld = dff.dropna(subset=["lat_down_ms", "lat_up_ms"]) ld_cap = min(dff_ld["lat_down_ms"].quantile(0.99), 200) lu_cap = min(dff_ld["lat_up_ms"].quantile(0.99), 200) hist_lat_dl = px.histogram( dff_ld[dff_ld["lat_down_ms"] <= ld_cap], x="lat_down_ms", nbins=50, color_discrete_sequence=["#fb923c"], title="🔽 Yük-altı İndirme Gecikmesi", labels={"lat_down_ms": "ms"}, ) hist_lat_dl.update_traces(marker_line_width=0, opacity=0.85) hist_lat_dl.add_vline( x=dff_ld["lat_down_ms"].median(), line_dash="dash", line_color="#ffffff55", annotation_text=f"Med: {dff_ld['lat_down_ms'].median():.0f}ms", annotation_font_color=TEXT_PRI, annotation_font_size=10, ) hist_lat_dl.update_layout(**PLOTLY_LAYOUT, yaxis_title="Tile", height=260) hist_lat_ul = px.histogram( dff_ld[dff_ld["lat_up_ms"] <= lu_cap], x="lat_up_ms", nbins=50, color_discrete_sequence=["#c084fc"], title="🔼 Yük-altı Yükleme Gecikmesi", labels={"lat_up_ms": "ms"}, ) hist_lat_ul.update_traces(marker_line_width=0, opacity=0.85) hist_lat_ul.add_vline( x=dff_ld["lat_up_ms"].median(), line_dash="dash", line_color="#ffffff55", annotation_text=f"Med: {dff_ld['lat_up_ms'].median():.0f}ms", annotation_font_color=TEXT_PRI, annotation_font_size=10, ) hist_lat_ul.update_layout(**PLOTLY_LAYOUT, yaxis_title="Tile", height=260) # İndirme vs Yükleme yük-altı gecikme scatter samp_ld = dff_ld.sample(min(len(dff_ld), 5_000), random_state=42) scatter_lat = px.scatter( samp_ld, x="lat_down_ms", y="lat_up_ms", color="latency_ms", opacity=0.5, title="🔀 DL vs UL Yük-altı Gecikme", labels={"lat_down_ms": "DL Gecikme (ms)", "lat_up_ms": "UL Gecikme (ms)", "latency_ms": "Boşta (ms)"}, color_continuous_scale="RdYlGn_r", range_x=[0, ld_cap], range_y=[0, lu_cap], ) scatter_lat.update_traces(marker=dict(size=3)) scatter_lat.update_layout( **PLOTLY_LAYOUT, height=260, coloraxis_colorbar=dict(bgcolor=BG_CARD, tickfont=dict(color=TEXT_PRI, size=9), title=dict(text="ms", font=dict(color=TEXT_MUT, size=10)), thickness=10, len=0.8), ) # Tüm gecikme türleri box plot box_lat_data = pd.DataFrame({ "ms": pd.concat([ dff.loc[dff["latency_ms"] <= lat_cap, "latency_ms"], dff_ld.loc[dff_ld["lat_down_ms"] <= ld_cap, "lat_down_ms"], dff_ld.loc[dff_ld["lat_up_ms"] <= lu_cap, "lat_up_ms"], ], ignore_index=True), "Tür": ( ["Boşta (idle)"] * (dff["latency_ms"] <= lat_cap).sum() + ["Yük-altı İndirme"] * (dff_ld["lat_down_ms"] <= ld_cap).sum() + ["Yük-altı Yükleme"] * (dff_ld["lat_up_ms"] <= lu_cap).sum() ), }) box_lat = px.box( box_lat_data, x="Tür", y="ms", color="Tür", title="📦 Gecikme Türleri Karşılaştırması", color_discrete_map={ "Boşta (idle)": "#f6d860", "Yük-altı İndirme": "#fb923c", "Yük-altı Yükleme": "#c084fc", }, points=False, ) box_lat.update_layout(**PLOTLY_LAYOUT, height=280, showlegend=False) # ════════════════════════════════════════════════════════════════════════════ # BÖLGE ANALİZİ SEKMESI # ════════════════════════════════════════════════════════════════════════════ # 2°x2° grid hücrelerine göre ortalama region = ( dff.groupby(["lon_bin", "lat_bin"]) .agg( download_mbps=("download_mbps", "mean"), upload_mbps=("upload_mbps", "mean"), latency_ms=("latency_ms", "mean"), tile_count=("download_mbps", "count"), ) .reset_index() ) region["label"] = region.apply( lambda r: f"{r.lat_bin:.1f}°N / {r.lon_bin}°E", axis=1 ) # Isı haritası: lon vs lat bazında download ortalaması heatmap_pivot = region.pivot_table( index="lat_bin", columns="lon_bin", values="download_mbps" ) fig_hm = go.Figure(data=go.Heatmap( z=heatmap_pivot.values, x=heatmap_pivot.columns.tolist(), y=heatmap_pivot.index.tolist(), colorscale="Plasma", colorbar=dict( bgcolor=BG_CARD, tickfont=dict(color=TEXT_PRI, size=9), title=dict(text="Mbps", font=dict(color=TEXT_MUT, size=10)), thickness=12, ), hoverongaps=False, hovertemplate="Lon: %{x}°E
Lat: %{y}°N
Download: %{z:.1f} Mbps", )) fig_hm.update_layout( **PLOTLY_LAYOUT, title="🗺️ Bölgesel Download Ortalaması (2°×2° grid)", xaxis_title="Boylam (°E)", yaxis_title="Enlem (°N)", height=350, ) # En iyi/kötü 15 bölge bar chart top15 = region.nlargest(15, "download_mbps") bot15 = region.nsmallest(15, "download_mbps") bar_data = pd.concat([top15.assign(Grup="En Hızlı 15"), bot15.assign(Grup="En Yavaş 15")]) fig_bar = px.bar( bar_data, x="download_mbps", y="label", color="Grup", orientation="h", title="🏆 Bölge Bazlı Download (En Hızlı / En Yavaş)", labels={"download_mbps": "Download (Mbps)", "label": ""}, color_discrete_map={"En Hızlı 15": "#00d4aa", "En Yavaş 15": "#f87171"}, barmode="group", ) fig_bar.update_layout(**PLOTLY_LAYOUT, height=280) fig_bar.update_yaxes(tickfont=dict(size=9), gridcolor=BORDER_CLR) # Download vs Gecikme bölge scatter fig_reg_sc = px.scatter( region, x="download_mbps", y="latency_ms", size="tile_count", color="upload_mbps", title="📍 Bölge: Download vs Gecikme (boyut=tile, renk=upload)", labels={ "download_mbps": "Download (Mbps)", "latency_ms": "Gecikme (ms)", "upload_mbps": "Upload (Mbps)", "tile_count": "Tile Sayısı", }, color_continuous_scale="Viridis", hover_name="label", opacity=0.8, ) fig_reg_sc.update_layout( **PLOTLY_LAYOUT, height=280, coloraxis_colorbar=dict(bgcolor=BG_CARD, tickfont=dict(color=TEXT_PRI, size=9), title=dict(text="UL Mbps", font=dict(color=TEXT_MUT, size=10)), thickness=10, len=0.8), ) return (hist_dl, hist_ul, hist_lat, scatter, box, hist_lat_dl, hist_lat_ul, scatter_lat, box_lat, fig_hm, fig_bar, fig_reg_sc) # ─── BAŞLAT ────────────────────────────────────────────────────────────────── if __name__ == "__main__": import sys sys.stdout.reconfigure(encoding="utf-8", errors="replace") print("Dashboard baslatiliyor -> http://127.0.0.1:8050") app.run(debug=False, port=8050)