Inspiration

"When worlds collide" -> we wanted to take 2 opposite things and combine them, art and numbers.

What it does

Transforms data into an art piece, specifically air pollution data

How we built it

Pre-process the data, calculate the Air Quality index, assign colours and create the image

import pandas as pd
import numpy as np
from PIL import Image

# -----------------------
# 1. Load & clean dataset
# -----------------------
df = pd.read_csv("beijing-air-quality.csv")
df.columns = df.columns.str.strip().str.lower()
# Replace missing blank strings with NaN
df = df.replace(r"^\s*$", np.nan, regex=True)

# Convert pollutant columns to numeric
for col in ["pm25", "pm10", "o3", "no2", "so2", "co"]:
    df[col] = pd.to_numeric(df[col], errors="coerce")

# -----------------------
# 2. AQI Breakpoints
# -----------------------
breakpoints = {
    "pm25": [
        (0, 12, 0, 50),
        (12.1, 35.4, 51, 100),
        (35.5, 55.4, 101, 150),
        (55.5, 150.4, 151, 200),
        (150.5, 250.4, 201, 300),
        (250.5, 350.4, 301, 400),
        (350.5, 500.4, 401, 500)
    ],
    "pm10": [
        (0, 54, 0, 50),
        (55, 154, 51, 100),
        (155, 254, 101, 150),
        (255, 354, 151, 200),
        (355, 424, 201, 300),
        (425, 504, 301, 400),
        (505, 604, 401, 500),
    ],
    # You can define: o3, no2, so2, co similarly
}

def compute_individual_aqi(C, species):
    if pd.isna(C):
        return np.nan
    for (Clow, Chigh, Ilow, Ihigh) in breakpoints.get(species, []):
        if Clow <= C <= Chigh:
            return ((Ihigh - Ilow) / (Chigh - Clow)) * (C - Clow) + Ilow
    return np.nan

# -----------------------
# 3. Compute AQI for each pollutant
# -----------------------
for sp in ["pm25", "pm10"]:
    df[f"AQI_{sp}"] = df[sp].apply(lambda x: compute_individual_aqi(x, sp))

# -----------------------
# 4. Final AQI = max of pollutants
# -----------------------
df["AQI"] = df[[col for col in df.columns if col.startswith("AQI_")]].max(axis=1)

# Drop rows with no AQI
df = df.dropna(subset=["AQI"])

# -----------------------
# 5. Turn AQI values into colors
# -----------------------
# def aqi_to_color(aqi):
#     if aqi <= 50: return (0, 255, 0)          # Good (green)
#     if aqi <= 100: return (255, 255, 0)       # Moderate (yellow)
#     if aqi <= 150: return (255, 165, 0)       # Unhealthy SG (orange)
#     if aqi <= 200: return (255, 0, 0)         # Unhealthy (red)
#     if aqi <= 300: return (128, 0, 128)       # Very Unhealthy (purple)
#     return (128, 0, 0)                        # Hazardous (maroon)

def gradient_color(aqi):
    # Clamp between 0 and 500
    aqi = max(0, min(500, aqi))

    # Define control points (aqi, (R,G,B))
    points = [
        (0,   (0, 255, 0)),      # green
        (50,  (255, 255, 0)),    # yellow
        (100, (255, 165, 0)),    # orange
        (150, (255, 80, 0)),     # deep orange
        (200, (255, 0, 0)),      # red
        (300, (128, 0, 128)),    # purple
        (500, (80, 0, 0))        # dark maroon
    ]

    # Find the two control points we’re between
    for i in range(len(points)-1):
        aqi1, c1 = points[i]
        aqi2, c2 = points[i+1]
        if aqi1 <= aqi <= aqi2:
            ratio = (aqi - aqi1) / (aqi2 - aqi1)
            r = int(c1[0] + ratio * (c2[0] - c1[0]))
            g = int(c1[1] + ratio * (c2[1] - c1[1]))
            b = int(c1[2] + ratio * (c2[2] - c1[2]))
            return (r, g, b)

    return (0, 0, 0)  # fallback


colors = df["AQI"].apply(gradient_color).tolist()

# -----------------------
# 6. Build an image
# -----------------------
width = len(colors)
height = 1000

img = Image.new("RGB", (width, height))

for x, color in enumerate(colors):
    for y in range(height):
        img.putpixel((x, y), color)

img.save("aqi_visual.png")
print("Saved image as aqi_visual.png")```

Built With

Share this project:

Updates