Skip to content

Instantly share code, notes, and snippets.

@akionsight
Created April 8, 2026 09:16
Show Gist options
  • Select an option

  • Save akionsight/2c06ff61577d4f4a3438c4fe7e8beafc to your computer and use it in GitHub Desktop.

Select an option

Save akionsight/2c06ff61577d4f4a3438c4fe7e8beafc to your computer and use it in GitHub Desktop.
Generates Charts for AQI data
"""
AQI April 2025 — Knowledge Park III, Greater Noida
Generates 3 chart PNGs from aqi.csv (place both files in the same folder).
Requirements:
pip install matplotlib pandas
"""
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
from matplotlib.patches import Patch
import warnings, os
warnings.filterwarnings("ignore")
# ── Load data ──────────────────────────────────────────────────────────────────
HERE = os.path.dirname(os.path.abspath(__file__))
CSV = os.path.join(HERE, "aqi.csv")
df = pd.read_csv(CSV)
hours = [c for c in df.columns if c != "Date"]
pivot = df.set_index("Date")[hours].copy()
pivot.columns = range(24)
daily_avg = pivot.mean(axis=1)
daily_max = pivot.max(axis=1)
daily_min = pivot.min(axis=1)
hourly_avg = pivot.mean(axis=0)
days = pivot.index.tolist()
day_labels = [f"Apr {d}" for d in days]
PALETTE = ["#4a9e5c", "#d4c84a", "#e8913a", "#c0392b"]
BOUNDS = [0, 50, 100, 200, 300]
CMAP = mcolors.ListedColormap(PALETTE)
NORM = mcolors.BoundaryNorm(BOUNDS, CMAP.N)
BG = "#f9f9f7"
# ══════════════════════════════════════════════════════════════════════════════
# CHART 1 — HEATMAP
# ══════════════════════════════════════════════════════════════════════════════
fig1, ax = plt.subplots(figsize=(18, 9), facecolor=BG)
fig1.suptitle(
"Hourly AQI Heatmap — April 2025 | Knowledge Park III, Greater Noida",
fontsize=13, fontweight="bold", color="#2c2c2c", y=1.01,
)
data = pivot.values.astype(float)
im = ax.imshow(data, aspect="auto", cmap=CMAP, norm=NORM, interpolation="nearest")
for r, day in enumerate(days):
for c in range(24):
val = data[r, c]
if not np.isnan(val):
fc = "white" if val > 130 else "#333"
ax.text(c, r, str(int(val)), ha="center", va="center",
fontsize=5.5, color=fc, fontweight="500")
ax.set_xticks(range(24))
ax.set_xticklabels([str(h) for h in range(24)], fontsize=8)
ax.set_yticks(range(len(days)))
ax.set_yticklabels(day_labels, fontsize=7.5)
ax.set_xlabel("Hour of day", fontsize=10, labelpad=5)
ax.set_ylabel("Date", fontsize=10, labelpad=5)
ax.set_title("Each cell = one hour reading. Blank cells = missing data.",
fontsize=9, color="#666", pad=6)
cbar = fig1.colorbar(im, ax=ax, orientation="vertical",
fraction=0.018, pad=0.01, shrink=0.95)
cbar.set_ticks([25, 75, 150, 250])
cbar.set_ticklabels(["Good", "Satisfactory", "Moderate", "Poor"], fontsize=8)
legend_patches = [
Patch(facecolor="#4a9e5c", label="Good (0–50)"),
Patch(facecolor="#d4c84a", label="Satisfactory (51–100)"),
Patch(facecolor="#e8913a", label="Moderate (101–200)"),
]
ax.legend(handles=legend_patches, loc="lower right", fontsize=8,
framealpha=0.9, bbox_to_anchor=(1.17, 0))
fig1.tight_layout()
out1 = os.path.join(HERE, "chart1_heatmap.png")
fig1.savefig(out1, dpi=180, bbox_inches="tight", facecolor=BG)
print(f"Saved {out1}")
# ══════════════════════════════════════════════════════════════════════════════
# CHART 2 — DAILY TREND
# ══════════════════════════════════════════════════════════════════════════════
fig2, ax = plt.subplots(figsize=(14, 6), facecolor=BG)
fig2.suptitle(
"Daily AQI Trend — April 2025 | Knowledge Park III, Greater Noida",
fontsize=13, fontweight="bold", color="#2c2c2c", y=1.01,
)
x = np.array(days)
ax.fill_between(x, daily_min, daily_max,
color="#e8913a", alpha=0.15, label="Min–Max range")
ax.plot(x, daily_max, color="#c0392b", linewidth=1.4, linestyle="--",
marker="o", markersize=3.5, label="Daily max")
ax.plot(x, daily_avg, color="#e8913a", linewidth=2,
marker="o", markersize=4, label="Daily avg", zorder=5)
ax.plot(x, daily_min, color="#4a9e5c", linewidth=1.4, linestyle="--",
marker="o", markersize=3.5, label="Daily min")
for level, label, clr in [
(50, "Good / Satisfactory", "#4a9e5c"),
(100, "Satisfactory / Moderate", "#d4aa00"),
(200, "Moderate / Poor", "#c0392b"),
]:
ax.axhline(level, color=clr, linewidth=0.8, linestyle=":", alpha=0.7)
ax.text(30.6, level + 1.5, label, fontsize=7, color=clr, va="bottom")
best_day = daily_avg.idxmin()
worst_day = daily_avg.idxmax()
ax.annotate(
f"Best\nApr {best_day} ({daily_avg[best_day]:.0f})",
xy=(best_day, daily_avg[best_day]),
xytext=(best_day + 1.8, daily_avg[best_day] - 16),
fontsize=8, color="#4a9e5c", fontweight="bold",
arrowprops=dict(arrowstyle="->", color="#4a9e5c", lw=1),
)
ax.annotate(
f"Worst\nApr {worst_day} ({daily_avg[worst_day]:.0f})",
xy=(worst_day, daily_avg[worst_day]),
xytext=(worst_day + 1.8, daily_avg[worst_day] + 4),
fontsize=8, color="#c0392b", fontweight="bold",
arrowprops=dict(arrowstyle="->", color="#c0392b", lw=1),
)
ax.set_xlim(0.5, 31.5)
ax.set_ylim(40, 190)
ax.set_xticks(range(1, 31))
ax.set_xticklabels(day_labels, rotation=45, ha="right", fontsize=8)
ax.set_ylabel("AQI", fontsize=10)
ax.set_title("Shaded band = daily min–max range; line = daily average",
fontsize=9, color="#666", pad=6)
ax.legend(fontsize=8.5, loc="upper left", framealpha=0.9)
ax.grid(axis="y", alpha=0.25, linewidth=0.7)
ax.set_facecolor("#fafaf8")
fig2.tight_layout()
out2 = os.path.join(HERE, "chart2_daily_trend.png")
fig2.savefig(out2, dpi=180, bbox_inches="tight", facecolor=BG)
print(f"Saved {out2}")
# ══════════════════════════════════════════════════════════════════════════════
# CHART 3 — HOURLY PATTERN
# ══════════════════════════════════════════════════════════════════════════════
fig3, ax = plt.subplots(figsize=(9, 9), facecolor=BG)
fig3.suptitle(
"Average AQI by Hour of Day — April 2025 | Knowledge Park III, Greater Noida",
fontsize=12, fontweight="bold", color="#2c2c2c", y=1.01,
)
bar_colors = ["#d4c84a" if v <= 100 else "#e8913a" for v in hourly_avg]
bars = ax.barh(range(24), hourly_avg.values,
color=bar_colors, edgecolor="none", height=0.65)
for i, (bar, val) in enumerate(zip(bars, hourly_avg.values)):
ax.text(val + 0.1, i, f"{val:.1f}", va="center", fontsize=8, color="#555")
ax.set_yticks(range(24))
ax.set_yticklabels([f"{h}:00" for h in range(24)], fontsize=8.5)
ax.set_xlabel("Average AQI (across all 30 days)", fontsize=10)
ax.set_title(
"The near-flat pattern indicates pollution is episode-driven\n"
"(weather / regional transport), not tied to time of day.",
fontsize=9, color="#666", pad=6,
)
ax.set_xlim(100, 120)
ax.invert_yaxis()
ax.grid(axis="x", alpha=0.25, linewidth=0.7)
ax.set_facecolor("#fafaf8")
fig3.tight_layout()
out3 = os.path.join(HERE, "chart3_hourly_pattern.png")
fig3.savefig(out3, dpi=180, bbox_inches="tight", facecolor=BG)
print(f"Saved {out3}")
print("\nDone — 3 charts generated.")
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
from matplotlib.patches import Patch
import warnings, os
warnings.filterwarnings("ignore")
# ── Load data ──────────────────────────────────────────────────────────────────
HERE = os.path.dirname(os.path.abspath(__file__))
CSV = os.path.join(HERE, "aqi_chandni.csv")
df = pd.read_csv(CSV, na_values="NA")
hours = [c for c in df.columns if c != "Date"]
pivot = df.set_index("Date")[hours].copy()
pivot.columns = range(24)
daily_avg = pivot.mean(axis=1)
daily_max = pivot.max(axis=1)
daily_min = pivot.min(axis=1)
hourly_avg = pivot.mean(axis=0)
days = pivot.index.tolist()
day_labels = [f"Apr {d}" for d in days]
# AQI colour scale — extended to cover Poor (201-300) since data reaches 287
PALETTE = ["#4a9e5c", "#d4c84a", "#e8913a", "#c0392b", "#7b241c"]
BOUNDS = [0, 50, 100, 200, 300, 400]
CMAP = mcolors.ListedColormap(PALETTE)
NORM = mcolors.BoundaryNorm(BOUNDS, CMAP.N)
BG = "#f9f9f7"
# ══════════════════════════════════════════════════════════════════════════════
# CHART 1 — HEATMAP
# ══════════════════════════════════════════════════════════════════════════════
fig1, ax = plt.subplots(figsize=(18, 9), facecolor=BG)
fig1.suptitle(
"Hourly AQI Heatmap — April 2025 | Chandni Chowk, Delhi (IITM)",
fontsize=13, fontweight="bold", color="#2c2c2c", y=1.01,
)
data = pivot.values.astype(float)
im = ax.imshow(data, aspect="auto", cmap=CMAP, norm=NORM, interpolation="nearest")
for r, day in enumerate(days):
for c in range(24):
val = data[r, c]
if not np.isnan(val):
fc = "white" if val > 150 else "#333"
ax.text(c, r, str(int(val)), ha="center", va="center",
fontsize=5.5, color=fc, fontweight="500")
ax.set_xticks(range(24))
ax.set_xticklabels([str(h) for h in range(24)], fontsize=8)
ax.set_yticks(range(len(days)))
ax.set_yticklabels(day_labels, fontsize=7.5)
ax.set_xlabel("Hour of day", fontsize=10, labelpad=5)
ax.set_ylabel("Date", fontsize=10, labelpad=5)
ax.set_title("Each cell = one hour reading. Grey = missing data.",
fontsize=9, color="#666", pad=6)
cbar = fig1.colorbar(im, ax=ax, orientation="vertical",
fraction=0.018, pad=0.01, shrink=0.95)
cbar.set_ticks([25, 75, 150, 250, 350])
cbar.set_ticklabels(["Good", "Satisfactory", "Moderate", "Poor", "Very Poor"],
fontsize=8)
legend_patches = [
Patch(facecolor="#4a9e5c", label="Good (0–50)"),
Patch(facecolor="#d4c84a", label="Satisfactory (51–100)"),
Patch(facecolor="#e8913a", label="Moderate (101–200)"),
Patch(facecolor="#c0392b", label="Poor (201–300)"),
]
ax.legend(handles=legend_patches, loc="lower right", fontsize=8,
framealpha=0.9, bbox_to_anchor=(1.17, 0))
fig1.tight_layout()
out1 = os.path.join(HERE, "chandni_chart1_heatmap.png")
fig1.savefig(out1, dpi=180, bbox_inches="tight", facecolor=BG)
print(f"Saved {out1}")
# ══════════════════════════════════════════════════════════════════════════════
# CHART 2 — DAILY TREND
# ══════════════════════════════════════════════════════════════════════════════
fig2, ax = plt.subplots(figsize=(14, 6), facecolor=BG)
fig2.suptitle(
"Daily AQI Trend — April 2025 | Chandni Chowk, Delhi (IITM)",
fontsize=13, fontweight="bold", color="#2c2c2c", y=1.01,
)
x = np.array(days)
ax.fill_between(x, daily_min, daily_max,
color="#c0392b", alpha=0.12, label="Min–Max range")
ax.plot(x, daily_max, color="#7b241c", linewidth=1.4, linestyle="--",
marker="o", markersize=3.5, label="Daily max")
ax.plot(x, daily_avg, color="#c0392b", linewidth=2,
marker="o", markersize=4, label="Daily avg", zorder=5)
ax.plot(x, daily_min, color="#e8913a", linewidth=1.4, linestyle="--",
marker="o", markersize=3.5, label="Daily min")
for level, label, clr in [
(100, "Satisfactory / Moderate", "#d4aa00"),
(200, "Moderate / Poor", "#c0392b"),
(300, "Poor / Very Poor", "#7b241c"),
]:
ax.axhline(level, color=clr, linewidth=0.8, linestyle=":", alpha=0.7)
ax.text(30.6, level + 2, label, fontsize=7, color=clr, va="bottom")
valid_avg = daily_avg.dropna()
best_day = valid_avg.idxmin()
worst_day = valid_avg.idxmax()
ax.annotate(
f"Best\nApr {best_day} ({valid_avg[best_day]:.0f})",
xy=(best_day, valid_avg[best_day]),
xytext=(best_day + 1.8, valid_avg[best_day] - 22),
fontsize=8, color="#e8913a", fontweight="bold",
arrowprops=dict(arrowstyle="->", color="#e8913a", lw=1),
)
ax.annotate(
f"Worst\nApr {worst_day} ({valid_avg[worst_day]:.0f})",
xy=(worst_day, valid_avg[worst_day]),
xytext=(worst_day + 1.8, valid_avg[worst_day] + 5),
fontsize=8, color="#7b241c", fontweight="bold",
arrowprops=dict(arrowstyle="->", color="#7b241c", lw=1),
)
ax.set_xlim(0.5, 31.5)
ax.set_ylim(60, 310)
ax.set_xticks(range(1, 31))
ax.set_xticklabels(day_labels, rotation=45, ha="right", fontsize=8)
ax.set_ylabel("AQI", fontsize=10)
ax.set_title("Shaded band = daily min–max range; line = daily average. Gaps = fully missing days.",
fontsize=9, color="#666", pad=6)
ax.legend(fontsize=8.5, loc="upper left", framealpha=0.9)
ax.grid(axis="y", alpha=0.25, linewidth=0.7)
ax.set_facecolor("#fafaf8")
fig2.tight_layout()
out2 = os.path.join(HERE, "chandni_chart2_daily_trend.png")
fig2.savefig(out2, dpi=180, bbox_inches="tight", facecolor=BG)
print(f"Saved {out2}")
# ══════════════════════════════════════════════════════════════════════════════
# CHART 3 — HOURLY PATTERN
# ══════════════════════════════════════════════════════════════════════════════
fig3, ax = plt.subplots(figsize=(9, 9), facecolor=BG)
fig3.suptitle(
"Average AQI by Hour of Day — April 2025 | Chandni Chowk, Delhi (IITM)",
fontsize=12, fontweight="bold", color="#2c2c2c", y=1.01,
)
def bar_color(v):
if v <= 100: return "#d4c84a"
if v <= 200: return "#e8913a"
return "#c0392b"
bar_colors = [bar_color(v) for v in hourly_avg]
bars = ax.barh(range(24), hourly_avg.values,
color=bar_colors, edgecolor="none", height=0.65)
for i, (bar, val) in enumerate(zip(bars, hourly_avg.values)):
ax.text(val + 0.5, i, f"{val:.1f}", va="center", fontsize=8, color="#555")
ax.set_yticks(range(24))
ax.set_yticklabels([f"{h}:00" for h in range(24)], fontsize=8.5)
ax.set_xlabel("Average AQI (across available days)", fontsize=10)
ax.set_title(
"Unlike Greater Noida, Chandni Chowk shows a diurnal rise —\n"
"higher AQI in late evening / night hours.",
fontsize=9, color="#666", pad=6,
)
xmin = max(0, hourly_avg.min() - 15)
xmax = hourly_avg.max() + 20
ax.set_xlim(xmin, xmax)
for level, clr in [(100, "#d4aa00"), (200, "#c0392b")]:
if xmin < level < xmax:
ax.axvline(level, color=clr, linewidth=0.8, linestyle=":", alpha=0.8)
ax.text(level + 1, 23.5, str(level), fontsize=7, color=clr)
ax.invert_yaxis()
ax.grid(axis="x", alpha=0.25, linewidth=0.7)
ax.set_facecolor("#fafaf8")
fig3.tight_layout()
out3 = os.path.join(HERE, "chandni_chart3_hourly_pattern.png")
fig3.savefig(out3, dpi=180, bbox_inches="tight", facecolor=BG)
print(f"Saved {out3}")
print("\nDone — 3 charts generated.")
@akionsight
Copy link
Copy Markdown
Author

The data used is sourced from the Central Control Room for Air Quality Management

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment