Violin Plot

Global Air Quality Index

Annual AQI distribution with health impact zones

Output
Global Air Quality Index
Python
import matplotlib.pyplot as plt
import numpy as np

# AQI data by city
np.random.seed(42)
cities = ['Stockholm', 'Vancouver', 'Tokyo', 'Los Angeles', 'Beijing', 'Delhi']
aqi_data = [
    np.random.gamma(3, 5, 365),           # Stockholm - excellent
    np.random.gamma(4, 6, 365),           # Vancouver - good
    np.random.gamma(6, 7, 365),           # Tokyo - moderate
    np.random.gamma(8, 8, 365),           # LA - moderate-unhealthy
    np.random.gamma(15, 10, 365),         # Beijing - unhealthy
    np.random.gamma(20, 12, 365),         # Delhi - hazardous
]

# AQI color scale
def get_aqi_color(val):
    if val < 50: return '#22C55E'      # Good
    elif val < 100: return '#EAB308'   # Moderate
    elif val < 150: return '#F97316'   # Unhealthy sensitive
    elif val < 200: return '#EF4444'   # Unhealthy
    elif val < 300: return '#9333EA'   # Very unhealthy
    else: return '#7F1D1D'             # Hazardous

colors = [get_aqi_color(np.median(d)) for d in aqi_data]

# Create figure
fig, ax = plt.subplots(figsize=(12, 7), facecolor='#F0FDF4')
ax.set_facecolor('#F0FDF4')

vp = ax.violinplot(aqi_data, positions=range(len(cities)), widths=0.75,
                   showmeans=False, showmedians=False, showextrema=False)

for i, body in enumerate(vp['bodies']):
    body.set_facecolor(colors[i])
    body.set_edgecolor('white')
    body.set_linewidth(2)
    body.set_alpha(0.75)

# AQI bands
bands = [(0, 50, '#22C55E', 'Good'), (50, 100, '#EAB308', 'Moderate'),
         (100, 150, '#F97316', 'USG'), (150, 200, '#EF4444', 'Unhealthy'),
         (200, 300, '#9333EA', 'Very Unhealthy'), (300, 400, '#7F1D1D', 'Hazardous')]
for y1, y2, color, label in bands:
    ax.axhspan(y1, y2, color=color, alpha=0.1, zorder=0)
    if y2 <= 250:
        ax.text(5.55, (y1 + y2) / 2, label, fontsize=8, color=color, 
                va='center', fontweight='500')

# Statistics
for i, aqi in enumerate(aqi_data):
    median = np.median(aqi)
    p95 = np.percentile(aqi, 95)
    
    ax.scatter(i, median, c='white', s=100, zorder=10,
               edgecolor=colors[i], linewidth=2.5)
    
    # 95th percentile marker
    ax.scatter(i, p95, c=colors[i], s=40, marker='^', zorder=10,
               edgecolor='white', linewidth=1)
    
    # Healthy days percentage
    healthy_pct = (aqi < 100).mean() * 100
    ax.text(i, -25, f'{healthy_pct:.0f}%', ha='center', fontsize=10,
            color='#22C55E' if healthy_pct > 80 else '#F97316',
            fontweight='bold')

# Legend
ax.scatter([], [], c='white', s=80, edgecolor='#6366F1', linewidth=2, label='Median')
ax.scatter([], [], c='#6366F1', s=40, marker='^', edgecolor='white', label='95th percentile')
ax.legend(loc='upper left', frameon=True, facecolor='white',
          edgecolor='#E5E7EB', fontsize=9)

# Label
ax.text(-0.5, -20, 'Healthy Days', fontsize=9, color='#6B7280')

# Styling
ax.set_xticks(range(len(cities)))
ax.set_xticklabels(cities, fontsize=11, fontweight='600')
ax.set_ylabel('Air Quality Index (AQI)', fontsize=12, fontweight='500', color='#374151')
ax.set_ylim(-35, 400)

ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.spines['left'].set_color('#D1D5DB')
ax.spines['bottom'].set_color('#D1D5DB')
ax.tick_params(colors='#6B7280', labelsize=10)

plt.tight_layout()
plt.show()
Library

Matplotlib

Category

Statistical

Did this help you?

Support PyLucid to keep it free & growing

Support