Weather_Data_Analyst / weather_data_analyst_app.py
towardsinnovationlab's picture
Upload weather_data_analyst_app.py
d355e17 verified
Raw
History Blame Contribute Delete
15 kB
# 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()