Last week, I showed you the messy reality of solar power generation. The numbers were sobering: 24% capacity factors, wild price swings, and generation that drops 30 MW in seconds. But here's the thing—those were averages from specific locations. What about YOUR city? What about an off-grid project to consider in Senegal? Or that rooftop installation in Cairo? Thanks for reading! Subscribe for free to receive new posts and support my work. Today, we're going to build something practical: a solar variability dashboard in python that works for any location on Earth. In 30 minutes, you'll have a tool that can analyze solar patterns from Dakar to Oslo, complete with capacity factors, seasonal variations, and those crucial "solar cliff" events that make grid operators nervous. The best part? We're doing this entirely in Google Colab. No installation headaches. No environment conflicts. Just open a browser and start analyzing. Why Build Your Own Dashboard? Before we dive into code, let's talk about why this matters: * Location, Location, Location: Solar installers love to quote generic capacity factors. "20% is typical!" But Oslo isn't Cairo. Nairobi isn't London. Your actual generation depends on latitude, weather patterns, and local climate. * Design Decisions: Knowing your solar resource helps size batteries, plan backup power, and estimate revenue. A few percentage points difference in capacity factor can make or break project economics. * Investor Confidence: When you can show month-by-month generation estimates based on real data, investors listen. Hand-waving about "sunny locations" doesn't cut it anymore. * Grid Integration: Understanding variability patterns helps predict grid impact. Does your location have gradual dawn/dusk transitions (good) or sudden cloud fronts (challenging)? What We're Building By the end of this tutorial, you'll have: * A web dashboard showing solar generation for 6 cities across 3 continents * Interactive charts comparing daily profiles, seasonal patterns, and variability * Downloadable data for your own analysis * Capacity factor calculations that you can explain and defend * Code you understand and can modify for any location Here's a sneak peek: Let's Build It! Step 0: Open Google Colab Head to Google Colab and create a new notebook. If you've never used Colab before, it's Google's free cloud-based Jupyter notebook environment. Think of it as Excel for programmers, but way more powerful. Step 1: Install and Import Libraries First, let's get our tools ready. Copy this into your first cell: # Install required packages (only need to run once per session) !pip install pvlib pandas plotly folium -q !pip install windrose matplotlib seaborn -q # Import everything we need import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns from datetime import datetime, timedelta import pvlib from pvlib import location from pvlib import irradiance import plotly.graph_objects as go import plotly.express as px from plotly.subplots import make_subplots import folium from IPython.display import display, HTML import warnings warnings.filterwarnings('ignore') # Set up nice plot formatting plt.style.use('seaborn-v0_8-darkgrid') colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#FFA07A', '#98D8C8', '#6C5CE7'] print("✅ All libraries loaded successfully!") print(f"📍 pvlib version: {pvlib.__version__}") Why these libraries? * pvlib: The gold standard for solar calculations. Developed by Sandia National Labs. * plotly: Creates interactive charts you can zoom, pan, and explore * folium: Makes maps to visualize our locations * pandas: Data manipulation (think Excel on steroids) Step 2: Define Our Locations Now let's set up our six cities. Each represents a different solar resource challenge: # Define our study locations with metadata LOCATIONS = { 'Dakar': { 'lat': 14.6928, 'lon': -17.4467, 'tz': 'Africa/Dakar', 'country': 'Senegal', 'climate': 'Tropical savanna', 'challenge': 'Dust storms and seasonal variations' }, 'Nairobi': { 'lat': -1.2921, 'lon': 36.8219, 'tz': 'Africa/Nairobi', 'country': 'Kenya', 'climate': 'Subtropical highland', 'challenge': 'Altitude effects and bimodal rainfall' }, 'Cairo': { 'lat': 30.0444, 'lon': 31.2357, 'tz': 'Africa/Cairo', 'country': 'Egypt', 'climate': 'Desert', 'challenge': 'Extreme heat and sandstorms' }, 'Cape Town': { 'lat': -33.9249, 'lon': 18.4241, 'tz': 'Africa/Johannesburg', 'country': 'South Africa', 'climate': 'Mediterranean', 'challenge': 'Winter rainfall and coastal clouds' }, 'London': { 'lat': 51.5074, 'lon': -0.1278, 'tz': 'Europe/London', 'country': 'UK', 'climate': 'Oceanic', 'challenge': 'Persistent cloud cover' }, 'Oslo': { 'lat': 59.9139, 'lon': 10.7522, 'tz': 'Europe/Oslo', 'country': 'Norway', 'climate': 'Humid continental', 'challenge': 'Extreme latitude and winter darkness' } } # Create a map showing all locations def create_location_map(): # Center the map on Africa/Europe m = folium.Map(location=[20, 10], zoom_start=3) for city, data in LOCATIONS.items(): folium.Marker( location=[data['lat'], data['lon']], popup=f"{city}, {data['country']}{data['climate']}{data['challenge']}", tooltip=city, icon=folium.Icon(color='red', icon='info-sign') ).add_to(m) return m # Display the map print("🗺️ Our six study locations:") create_location_map() Why these cities? * Latitude range: From 60°N (Oslo) to 34°S (Cape Town) - covering extreme solar angles * Climate diversity: Desert to oceanic - every weather pattern * Development context: Mix of developed/developing markets with different energy needs * Grid challenges: Each has unique integration issues Step 3: Generate Solar Data Now comes the fun part - calculating actual solar generation. We'll use pvlib's proven models: def generate_solar_data(city_name, location_data, year=2023): """ Generate hourly solar data for a full year using pvlib Why hourly? It's the sweet spot between accuracy and computation time. More frequent data (15-min) doesn't improve capacity factor estimates much. """ print(f"☀️ Generating solar data for {city_name}...") # Create location object site = location.Location( location_data['lat'], location_data['lon'], tz=location_data['tz'] ) # Generate timestamps for full year times = pd.date_range( start=f'{year}-01-01', end=f'{year}-12-31 23:00', freq='H', tz=location_data['tz'] ) # Calculate clear-sky irradiance (no clouds) clearsky = site.get_clearsky(times) # Calculate solar position solar_position = site.get_solarposition(times) # Add realistic cloud effects based on climate # This is simplified - real clouds are more complex! cloud_impact = simulate_clouds(city_name, times, location_data['climate']) # Calculate actual GHI (Global Horizontal Irradiance) ghi_actual = clearsky['ghi'] * cloud_impact # Create comprehensive dataframe solar_data = pd.DataFrame({ 'ghi_clear': clearsky['ghi'], 'ghi_actual': ghi_actual, 'dni_clear': clearsky['dni'], 'dhi_clear': clearsky['dhi'], 'solar_zenith': solar_position['zenith'], 'solar_azimuth': solar_position['azimuth'], 'cloud_impact': cloud_impact, 'hour': times.hour, 'month': times.month, 'season': times.month%12 // 3 + 1 }, index=times) # Calculate PV system output (100 MW reference system) solar_data['power_output'] = calculate_pv_power( solar_data['ghi_actual'], solar_data['solar_zenith'], ambient_temp=25 # Simplified - would vary in reality ) return solar_data def simulate_clouds(city_name, times, climate): """ Simple cloud simulation based on climate type Real clouds are much more complex - this gives realistic patterns """ np.random.seed(42) # Reproducibility # Base cloud probability by climate type cloud_prob = { 'Desert': 0.1, # Rare clouds 'Tropical savanna': 0.3, # Seasonal 'Mediterranean': 0.4, # Winter clouds 'Subtropical highland': 0.5, # Variable 'Oceanic': 0.7, # Frequent clouds 'Humid continental': 0.6 # Variable } base_prob = cloud_prob.get(climate, 0.5) # Add seasonal variation month = times.month seasonal_factor = 1 + 0.3 * np.sin(2 * np.pi * (month - 3) / 12) # Generate cloud impact (1 = clear, 0 = fully clouded) cloud_impact = np.ones(len(times)) for i in range(len(times)): if np.random.random() Step 4: Calculate Key Metrics Now let's extract the insights that matter: def calculate_metrics(solar_data, city_name): """ Calculate key performance metrics for each location """ metrics = {} # Annual capacity factor (the big one!) total_generation = solar_data['power_output'].sum() theoretical_max = 100 * len(solar_data) # 100 MW * hours metrics['annual_capacity_factor'] = total_generation / theoretical_max # Capacity factor during daylight hours only daylight = solar_data[solar_data['ghi_actual'] > 0] metrics['daylight_capacity_factor'] = daylight['power_output'].mean() / 100 # Peak sun hours (equivalent hours at 1000 W/m²) metrics['peak_sun_hours'] = solar_data['ghi_actual'].sum() / 1000 / 365 # Variability score (standard deviation of hourly changes) hourly_changes = solar_data['