gaza-verified cumulative campaigns statistics

This notebook provides some insights on the distribution of funding to the campaigns in the gaza-verified mutual aid program.

It analyzes:

  • Total daily donations trends
  • Per-capita daily donation trends
  • Fairness of the distribution of the funds across the registered campaigns

We first look at the cumulative donation trends over the past week

In [1]:
# Common imports and constants

import requests
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from datetime import datetime, timedelta

api_url = "https://gaza.onl/api/v1/campaigns/accounts"
end_time = str(datetime.now().date() - timedelta(days=1))
In [2]:
# Fetch the total donation amounts, grouped by date
start_time = str(datetime.now().date() - timedelta(days=30))
group_by = ["donation.created_at:day"]
sort = "donation.created_at"

response = requests.get(
    api_url,
    params={
        "start_time": start_time,
        "end_time": end_time,
        "group_by": group_by,
        "sort": sort,
    },
)

response.raise_for_status()
data = response.json().get('data')
In [3]:
# Group them up and keep a separate day -> n_campaigns map (used later for the mean value)

donations_per_day = {}
campaigns_per_day = {}

for account_record in data:
    for row in account_record.get('data', []):
        account, date = row['group_value']
        amount = row['amount']['amount']
        if date:
            donations_per_day[date] = donations_per_day.get(date, 0) + amount
            campaigns_per_day[date] = {*campaigns_per_day.get(date, set()), account}

campaigns_per_day = {
    item[0]: len(item[1])
    for item in sorted(
        campaigns_per_day.items(),
        key=lambda pair: pair[0],
    )
}

donations_per_day = [
    (item[0], int(item[1]))
    for item in sorted(
        donations_per_day.items(),
        key=lambda pair: pair[0],
    )
]
In [76]:
dates, amounts = zip(*donations_per_day)
amounts = np.array(amounts)

plt.figure(figsize=(12, 8))
plt.plot(
    dates, amounts, marker=".", linestyle="-", label="Daily Donations", color="orange"
)

# Add moving average over the last 3 days
window_size = 4
if len(amounts) >= window_size:
    moving_avg = np.convolve(amounts, np.ones(window_size) / window_size, mode="valid")
    plt.plot(
        dates[window_size - 1 :],
        moving_avg,
        color="blue",
        linestyle="--",
        label=f"{window_size}-Day Moving Average",
    )
    plt.fill_between(dates[window_size - 1 :], moving_avg, color="skyblue", alpha=0.4)
    plt.legend()

plt.xlabel("Date")
plt.ylabel("Amount (USD)")
plt.title(f"Total Daily Donations from {start_time} to {end_time}")
plt.xticks(rotation=45)
plt.grid(True, alpha=0.3)
plt.gca().xaxis.set_major_locator(plt.MaxNLocator(nbins=len(dates)//2))
plt.tight_layout()
plt.show()

Let's take a closer look at how much support each campaign received on average, on a daily basis.

In [83]:
# Plot per-campaign average trend

pro_capita_donations_per_day = [
    (date, amount / campaigns_per_day[date])
    for date, amount in donations_per_day
    if campaigns_per_day.get(date)
]

dates, amounts = zip(*pro_capita_donations_per_day)
amounts = np.array(amounts)

plt.figure(figsize=(12, 8))
plt.plot(
    dates, amounts, marker=".", linestyle="-", label="Avg per-campaign daily donations", color="orange"
)

# Add moving average over the last 3 days
window_size = 4
if len(amounts) >= window_size:
    moving_avg = np.convolve(amounts, np.ones(window_size) / window_size, mode="valid")
    plt.plot(
        dates[window_size - 1 :],
        moving_avg,
        color="blue",
        linestyle="--",
        label=f"{window_size}-Day Moving Average",
    )
    plt.fill_between(dates[window_size - 1 :], moving_avg, color="skyblue", alpha=0.4)
    plt.legend()

plt.xlabel("Date")
plt.ylabel("Amount (USD)")
plt.title(f"Average Daily Donations Per Campaign from {start_time} to {end_time}")
plt.xticks(rotation=45)
plt.gca().xaxis.set_major_locator(plt.MaxNLocator(nbins=len(dates)//2))
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

Per-campaign funding distribution

Finally, let's take a closer look at how fairly funding is distributed across the campaigns

In [84]:
start_time = str(datetime.now().date() - timedelta(days=7))
group_by = ["donation.campaign_url"]
sort = "amount:desc"

response = requests.get(
    api_url,
    params={
        "start_time": start_time,
        "group_by": group_by,
        "sort": sort,
    },
)

response.raise_for_status()
data = response.json().get('data')
In [85]:
amount_by_account = []

for row in data:
    account = row['group_value'][0].split('/')[-1]
    amount = row['amount']['amount']
    amount_by_account.append((account, amount))
In [90]:
accounts, amounts = zip(*amount_by_account)
amounts = np.array(amounts)
plt.figure(figsize=(12, 8))

sns.barplot(
    x=list(accounts),
    y=amounts,
    palette=sns.color_palette("RdYlGn", len(amounts)),
    hue=[amount / max(amounts) for amount in amounts],
)

plt.xlabel("")
plt.ylabel("Amount raised (USD)")
plt.title(f"Distribution of funds raised by the campaigns between {start_time} and {datetime.now().date()}")
plt.xticks(rotation=45, labels=["" for _ in accounts], ticks=["" for _ in accounts])
plt.grid(True, alpha=0.3)
plt.legend([], [], frameon=False)
plt.tight_layout()
plt.show()
/tmp/ipykernel_1668869/2145623161.py:5: UserWarning: The palette list has more values (80) than needed (78), which may not be intended.
  sns.barplot(