# Weather Data Analyst App - Gradio # ============================================================================== import gradio as gr import requests import pandas as pd import numpy as np import warnings from datetime import datetime, timedelta from statsmodels.tsa.stattools import adfuller, kpss from statsmodels.tsa.seasonal import seasonal_decompose from statsmodels.graphics.tsaplots import plot_acf, plot_pacf import matplotlib.pyplot as plt import matplotlib.dates as mdates import seaborn as sns warnings.filterwarnings("ignore") # ------------------------------ # 1. Data Fetching Functions # ------------------------------ def get_lat_lon(location): """ Geocode 'City', 'City, Country', 'City, US' or 'City, Tennessee' via the Open-Meteo geocoding API. Comma qualifiers are matched client-side against country name, ISO country code and admin1 (state/region), because the API itself cannot parse ISO codes or state names. Bare names keep returning the API's top-ranked match (backwards compatible). NOTE: keep in sync with the identical logic in the Climate_Risk_Monitoring Space (_resolve_location) and the other two upstream Spaces. """ try: url = "https://geocoding-api.open-meteo.com/v1/search" def search(query, count=10): resp = requests.get( url, params={"name": query, "count": count, "language": "en", "format": "json"}, timeout=10, ) if resp.status_code == 200: return resp.json().get("results") or [] return [] raw = (location or "").strip().strip(".") city, _, qualifier = raw.partition(",") city, qualifier = city.strip(), qualifier.strip() candidates = search(raw) # handles 'City' and 'City, CountryName' if not candidates and qualifier: qual = qualifier.lower().rstrip(".") qual = { # common aliases -> official country name (keep in sync) "uk": "united kingdom", "u.k": "united kingdom", "great britain": "united kingdom", "usa": "united states", "u.s": "united states", "u.s.a": "united states", "america": "united states", "uae": "united arab emirates", "holland": "netherlands", }.get(qual, qual) candidates = [c for c in search(city) if qual in ( (c.get("country") or "").lower(), (c.get("country_code") or "").lower(), (c.get("admin1") or "").lower(), )] if candidates: result = candidates[0] return { "location": result["name"], "country": result.get("country", "Unknown"), "latitude": result["latitude"], "longitude": result["longitude"], "timezone": result.get("timezone", "UTC") } return {"error": f"Location '{location}' not found"} except Exception as e: return {"error": str(e)} def fetch_historical_weather(latitude: float, longitude: float, days: int = 365) -> tuple: """ Fetch daily historical weather from NASA POWER API. Source: https://power.larc.nasa.gov — NASA servers, no API key, global, data from 1981 to present. Completely independent of open-meteo. Returns (DataFrame, error_string). DataFrame columns: ds, Temperature, Precipitation, Wind_Speed NASA POWER parameters used: T2M — temperature at 2m (°C) PRECTOTCORR — corrected precipitation (mm/day) WS10M — wind speed at 10m (m/s) → converted to km/h """ try: end_date = datetime.now() - timedelta(days=1) start_date = end_date - timedelta(days=days) r = requests.get( "https://power.larc.nasa.gov/api/temporal/daily/point", params={ "parameters": "T2M,PRECTOTCORR,WS10M", "community": "RE", "longitude": round(longitude, 4), "latitude": round(latitude, 4), "start": start_date.strftime("%Y%m%d"), "end": end_date.strftime("%Y%m%d"), "format": "JSON", }, timeout=60, ) if r.status_code != 200: return pd.DataFrame(), f"NASA POWER HTTP {r.status_code}: {r.text[:200]}" data = r.json() params = data.get("properties", {}).get("parameter", {}) if not params: return pd.DataFrame(), "NASA POWER response missing 'parameter' data" t2m = params.get("T2M", {}) prec = params.get("PRECTOTCORR", {}) ws10m = params.get("WS10M", {}) if not t2m: return pd.DataFrame(), "NASA POWER returned no temperature data" dates = sorted(t2m.keys()) df = pd.DataFrame({ "ds": pd.to_datetime(dates, format="%Y%m%d"), "Temperature": [t2m.get(d, np.nan) for d in dates], "Precipitation": [prec.get(d, np.nan) for d in dates], # NASA POWER WS10M is in m/s → convert to km/h "Wind_Speed": [ws10m.get(d, np.nan) * 3.6 for d in dates], }) # NASA POWER uses -999 as fill value for missing data df = df.replace(-999, np.nan).dropna() if len(df) < 30: return pd.DataFrame(), f"Only {len(df)} days of data — need at least 30" return df, None except Exception as e: return pd.DataFrame(), str(e) # ------------------------------ # 2. Statistical Analysis # ------------------------------ def perform_statistical_tests(df, column): ts = df[column].dropna() with warnings.catch_warnings(): warnings.simplefilter("ignore") adf_result = adfuller(ts) adf_stationary = bool(adf_result[1] < 0.05) kpss_result = kpss(ts, regression='c') kpss_stationary = bool(kpss_result[1] > 0.05) rows = [ ["Mean", f"{ts.mean():.4f}"], ["Std Dev", f"{ts.std():.4f}"], ["Min", f"{ts.min():.4f}"], ["Max", f"{ts.max():.4f}"], ["ADF Test Statistic", f"{adf_result[0]:.4f}"], ["ADF p-value", f"{adf_result[1]:.4f}"], ["ADF Stationary", "Yes" if adf_stationary else "No"], ["KPSS Test Statistic", f"{kpss_result[0]:.4f}"], ["KPSS p-value", f"{kpss_result[1]:.4f}"], ["KPSS Stationary", "Yes" if kpss_stationary else "No"], ] result_df = pd.DataFrame(rows, columns=["Parameter", "Value"]) if adf_stationary and kpss_stationary: conclusion = f"{column} is stationary (both ADF and KPSS agree)." elif adf_stationary and not kpss_stationary: conclusion = f"{column} is trend-stationary (ADF stationary, KPSS non-stationary)." elif not adf_stationary and kpss_stationary: conclusion = f"{column} is difference-stationary (ADF non-stationary, KPSS stationary)." else: conclusion = f"{column} is non-stationary (both ADF and KPSS agree)." return result_df, conclusion # ------------------------------ # 3. Visualization Functions # ------------------------------ def plot_lineplot(df, column, location_name): fig, ax = plt.subplots(figsize=(12, 4)) ax.plot(df['ds'], df[column], label=column, color='tab:blue') ax.set_title(f"{column} Over Time - {location_name}", fontsize=12) ax.set_xlabel('Date') ax.set_ylabel(column) ax.grid(True, linestyle='--', alpha=0.7) plt.xticks(rotation=45) plt.tight_layout() return fig def plot_boxplot(df, column, location_name): data_reset = df.reset_index(drop=True).copy() data_reset['Month'] = data_reset['ds'].dt.strftime('%B') months_order = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ] # Keep only months present in data present_months = [m for m in months_order if m in data_reset['Month'].values] fig, ax = plt.subplots(figsize=(12, 4)) sns.boxplot( x='Month', y=column, data=data_reset, order=present_months, palette='Set3', ax=ax ) ax.set_title(f"{column} Distribution per Month - {location_name}", fontsize=12) ax.set_xlabel('Month', fontsize=10) ax.set_ylabel(column, fontsize=10) ax.tick_params(axis='x', rotation=45, labelsize=10) ax.tick_params(axis='y', labelsize=10) ax.grid(True, linestyle='--', alpha=0.7) plt.tight_layout() return fig def plot_autocorrelation(df, column): fig, axs = plt.subplots(nrows=1, ncols=2, figsize=(12, 4)) plot_acf(df[column].dropna(), lags=21, ax=axs[0], color="fuchsia") axs[0].set_title(f"Autocorrelation - {column}") plot_pacf(df[column].dropna(), lags=21, ax=axs[1], color="lime") axs[1].set_title(f"Partial Autocorrelation - {column}") plt.tight_layout() return fig def plot_decomposition(df, column, model, period): try: result = seasonal_decompose(df[column].dropna(), model=model, period=period) fig = result.plot() fig.suptitle(f"Seasonal Decomposition of {column}", fontsize=13, y=1.02) plt.tight_layout() return fig, "" except Exception as e: return None, f"An error occurred during decomposition: {e}" # ------------------------------ # 4. Main Analysis Function # ------------------------------ def analyze_weather(location, variable, decomposition_model, decomposition_period, days): if not location: return ( "Please enter a location.", None, None, None, None, None, None, "", ) # Step 1: Geocode loc_data = get_lat_lon(location) if "error" in loc_data: return ( f"Error: {loc_data['error']}", None, None, None, None, None, None, "", ) location_label = f"{loc_data['location']}, {loc_data['country']}" # Step 2: Fetch weather data df, fetch_error = fetch_historical_weather(loc_data['latitude'], loc_data['longitude'], days=int(days)) if fetch_error or df.empty: return ( f"Failed to fetch weather data: {fetch_error}", None, None, None, None, None, None, "", ) # Step 3: Data Table — show only Date + selected variable display_df = df[['ds', variable]].copy() display_df['ds'] = display_df['ds'].dt.strftime('%Y-%m-%d') display_df = display_df.rename(columns={'ds': 'Date'}) # Step 4: Visualizations lineplot_fig = plot_lineplot(df, variable, location_label) boxplot_fig = plot_boxplot(df, variable, location_label) autocorr_fig = plot_autocorrelation(df, variable) # Step 5: Decomposition decomp_fig, _ = plot_decomposition(df, variable, decomposition_model, int(decomposition_period)) # Step 6: Statistical Tests stats_df, stats_conclusion = perform_statistical_tests(df, variable) status_msg = f"✅ Analysis complete for **{location_label}** | Variable: **{variable}** | {len(df)} days of data loaded." return ( status_msg, display_df, lineplot_fig, boxplot_fig, autocorr_fig, decomp_fig, stats_df, stats_conclusion, ) # ------------------------------ # 5. Gradio Interface # ------------------------------ def build_interface(): with gr.Blocks() as iface: gr.Markdown("# 🌦️ Weather Data Analyst Dashboard") gr.Markdown(""" Enter a location and configure the analysis parameters. Weather data is fetched from the [NASA POWER API](https://power.larc.nasa.gov). """) # Input Section with gr.Row(): location_input = gr.Textbox(label="Location", placeholder="e.g., London, Tokyo, Milan") variable_input = gr.Dropdown( choices=["Temperature", "Precipitation", "Wind_Speed"], value="Temperature", label="Weather Variable" ) decomp_model_display = gr.Textbox( value="additive", label="Decomposition Model (auto)", interactive=False ) decomp_period_input = gr.Number( value=7, label="Decomposition Period (days)", precision=0 ) days_input = gr.Dropdown( choices=[90, 180, 365, 730, 1095, 1825], value=90, label="Historical Window (days)" ) analyze_button = gr.Button("Analyze Weather", variant="primary") status_output = gr.Markdown(label="Status") # Output Tabs with gr.Tabs(): with gr.Tab("Data Table"): data_output = gr.Dataframe(label="Weather Data") with gr.Tab("Data Visualization"): with gr.Row(): lineplot_output = gr.Plot(label="Line Plot") boxplot_output = gr.Plot(label="Box Plot") autocorr_output = gr.Plot(label="Autocorrelation Plots") with gr.Tab("Time Series Decomposition"): decomposition_output = gr.Plot(label="Decomposition Plot") with gr.Tab("Statistical Analysis"): stats_table_output = gr.Dataframe(label="Statistical Tests (ADF & KPSS)") stats_conclusion_output = gr.Textbox(label="Conclusion", interactive=False) def set_decomp_model(variable): return "multiplicative" if variable == "Wind_Speed" else "additive" variable_input.change( set_decomp_model, inputs=[variable_input], outputs=[decomp_model_display] ) analyze_button.click( analyze_weather, inputs=[ location_input, variable_input, decomp_model_display, decomp_period_input, days_input, ], outputs=[ status_output, data_output, lineplot_output, boxplot_output, autocorr_output, decomposition_output, stats_table_output, stats_conclusion_output, ] ) return iface # ------------------------------ # 6. Launch # ------------------------------ if __name__ == "__main__": interface = build_interface() interface.launch()