Skip to main content

Hourly pedestrian count data from Heart of the City Auckland's monitoring system (2019–2025), covering 21 sensor locations across Auckland CBD.

Project description

akl-ped-counts

PyPI version Python License: CC BY 4.0

Hourly pedestrian count data from Heart of the City Auckland's pedestrian monitoring system, covering 21 sensor locations across Auckland CBD from 2019 to 2025.


Installation

# Core (pandas only)
pip install akl-ped-counts

# With Polars support
pip install "akl-ped-counts[polars]"

# With plotting (matplotlib + seaborn)
pip install "akl-ped-counts[plot]"

# With mapping (folium)
pip install "akl-ped-counts[geo]"

# Everything
pip install "akl-ped-counts[all]"

Quick start

from akl_ped_counts import load_hourly, load_locations, list_sensors

# Load all hourly counts (61,000+ rows × 21 sensors)
counts = load_hourly()

# Load sensor coordinates (WGS 84)
locations = load_locations()

# See all 21 sensor names
print(list_sensors())

API reference — pandas

load_hourly(years=None, sensors=None, dropna=False)

Load hourly pedestrian counts. Returns a DataFrame with columns date, hour, year, plus one column per sensor location.

# Filter by year
df_2024 = load_hourly(years=[2024])

# Filter by sensor
df = load_hourly(sensors=["45 Queen Street", "210 Queen Street"])

# Drop rows with any missing values
df_clean = load_hourly(dropna=True)

load_daily(years=None, sensors=None, dropna=False)

Daily totals aggregated from hourly data. Same filtering parameters.

daily = load_daily(years=[2023, 2024])

load_monthly(years=None, sensors=None, dropna=False)

Monthly totals. Returns columns year_month (Period), year, month, plus sensor totals.

monthly = load_monthly()

load_locations()

Sensor metadata: Address, Latitude, Longitude (WGS 84).

list_sensors()

Returns the list of all 21 sensor location names.

describe_missing()

Returns a DataFrame summarising missing data by year and sensor, with columns year, sensor, total_hours, missing_hours, pct_missing.

from akl_ped_counts import describe_missing
report = describe_missing()
print(report.query("pct_missing > 1"))

API reference — Polars

All Polars functions live in akl_ped_counts.polars_loader and mirror the pandas API. Polars must be installed (pip install polars).

load_hourly(years=None, sensors=None, dropna=False)

from akl_ped_counts.polars_loader import load_hourly

df = load_hourly(years=[2023, 2024])
print(df.shape)  # (17543, 24)

scan_hourly(years=None, sensors=None) — LazyFrame

Returns a pl.LazyFrame for deferred execution. Useful for pushing filters and aggregations down before collecting.

from akl_ped_counts.polars_loader import scan_hourly
import polars as pl

# Lazy daily totals for one sensor
daily = (
    scan_hourly(years=[2024])
    .group_by("date")
    .agg(pl.col("45 Queen Street").sum())
    .sort("date")
    .collect()
)

load_daily, load_monthly, load_locations, describe_missing

All have the same signatures as the pandas versions but return pl.DataFrame.

from akl_ped_counts.polars_loader import load_daily, load_locations

daily = load_daily(years=[2024], dropna=True)
locs = load_locations()

Visualisation examples

March daily trajectories by year

Compare how footfall evolves through March at different sensors, year by year. The COVID-19 impact (2020–2021) and post-recovery trend are clearly visible.

March Trajectories

import matplotlib.pyplot as plt
import matplotlib.cm as cm
import numpy as np
from akl_ped_counts import load_hourly

df = load_hourly()
df_march = df[df["date"].dt.month == 3].copy()
df_march["day"] = df_march["date"].dt.day

sensors = [
    "45 Queen Street", "210 Queen Street", "297 Queen Street",
    "107 Quay Street", "2 High Street", "150 K Road",
    "183 K Road", "Commerce Street West", "19 Shortland Street",
]
years = sorted(df_march["year"].unique())
colours = cm.viridis(np.linspace(0, 1, len(years)))

fig, axes = plt.subplots(3, 3, figsize=(14, 10), sharex=True)
for ax, sensor in zip(axes.flat, sensors):
    for yr, colour in zip(years, colours):
        sub = df_march[df_march["year"] == yr]
        daily = sub.groupby("day")[sensor].sum()
        ax.plot(daily.index, daily.values, label=str(yr),
                color=colour, linewidth=1.2)
    ax.set_title(sensor, fontsize=10)
    ax.set_xlabel("Day of March")
    ax.set_ylabel("Daily Footfall")

handles, labels = axes[0, 0].get_legend_handles_labels()
fig.legend(handles, labels, loc="upper center", ncol=len(years),
           title="Year", fontsize=9)
plt.tight_layout(rect=[0, 0, 1, 0.95])
plt.savefig("march_trajectories.png", dpi=300)

Heatmap: average footfall by hour and day of week

Reveals the weekly rhythm of the CBD — weekday lunchtime peaks, quiet weekend mornings, and Friday/Saturday evening activity.

Heatmap

import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd
from akl_ped_counts import load_hourly

df = load_hourly(dropna=True)

# Extract start hour from "6:00-6:59" format
df["start_hour"] = df["hour"].str.split(":").str[0].astype(int)
df["dow"] = df["date"].dt.dayofweek

sensor_cols = [c for c in df.columns if c not in
               ("date", "hour", "year", "start_hour", "dow")]

# Mean footfall across all sensors
df["mean_count"] = df[sensor_cols].mean(axis=1)

pivot = df.pivot_table(
    values="mean_count", index="start_hour", columns="dow",
    aggfunc="mean"
)
pivot.columns = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]

fig, ax = plt.subplots(figsize=(10, 8))
sns.heatmap(pivot, annot=True, fmt=".0f", cmap="YlOrRd",
            cbar_kws={"label": "Mean Pedestrian Count"}, ax=ax)
ax.set_title("Average Hourly Pedestrian Footfall by Day of Week\n"
             "(Auckland CBD, 2019–2025)")
ax.set_ylabel("Hour of Day")
ax.set_xlabel("Day of Week")
plt.tight_layout()
plt.savefig("heatmap_hour_dow.png", dpi=300)

Above / below average footfall by sensor

Identifies which streets consistently attract more or fewer pedestrians than the city-wide average. Queen Street dominates; side streets and Karangahape Road sensors sit below the mean.

Above Below Average

import matplotlib.pyplot as plt
from akl_ped_counts import load_hourly

df = load_hourly()
sensor_cols = [c for c in df.columns if c not in ("date", "hour", "year")]

totals = df[sensor_cols].sum().sort_values()
avg = totals.mean()
deviation = totals - avg

colours = ["#2ecc71" if v > 0 else "#e74c3c" for v in deviation]

fig, ax = plt.subplots(figsize=(10, 8))
ax.barh(deviation.index, deviation.values, color=colours)
ax.axvline(0, color="black", linestyle="--", linewidth=0.8)
ax.set_xlabel("Deviation from Average Footfall")
ax.set_title("Sensor Footfall Relative to Average\n"
             "(Auckland CBD, 2019–2025)")
plt.tight_layout()
plt.savefig("above_below_average.png", dpi=300)

Interactive sensor map (Folium)

An interactive map showing all 21 sensor locations with circle markers sized by total footfall. Clicking a marker shows the sensor name and count.

import folium
import pandas as pd
from akl_ped_counts import load_hourly, load_locations

locs = load_locations()
df = load_hourly()
sensor_cols = [c for c in df.columns if c not in ("date", "hour", "year")]
totals = df[sensor_cols].sum()

centre = [locs["Latitude"].mean(), locs["Longitude"].mean()]
m = folium.Map(location=centre, zoom_start=15, tiles="OpenStreetMap")

max_total = totals.max()
for _, row in locs.iterrows():
    name = row["Address"]
    total = totals.get(name, 0)
    radius = 5 + 25 * (total / max_total)

    # Colour: blue (low) → red (high)
    ratio = total / max_total
    r = int(255 * ratio)
    b = int(255 * (1 - ratio))
    colour = f"#{r:02x}00{b:02x}"

    folium.CircleMarker(
        location=[row["Latitude"], row["Longitude"]],
        radius=radius,
        color=colour, fill=True, fill_color=colour, fill_opacity=0.7,
        popup=f"<b>{name}</b><br>Total: {total:,.0f}",
        tooltip=name,
    ).add_to(m)

m.save("sensor_map.html")

Open sensor_map.html in a browser to explore interactively. Top-5 busiest sensors: 45 Queen Street (40.0M), 30 Queen Street (38.8M), 210 Queen Street (37.7M), 261 Queen Street (34.5M), and 297 Queen Street (24.7M).


Data coverage

Year Rows Sensors Missing (%) Notes
2019 8,760 19 0.0 Complete
2020 8,784 19 0.0 Complete (leap year)
2021 8,760 19 0.0 Complete
2022 8,760 21 8.2 Two new sensors added; startup gaps
2023 8,760 21 0.1 Near-complete
2024 8,783 21 0.0 Complete (leap year + DST adjustments)
2025 8,760 21 0.0 Camera upgrades on 5 Mar for 5 sensors

Handling missing data

Missing values appear as NaN (pandas) or null (Polars) in the count columns. They arise from three sources: sensors not yet installed, sensor downtime/maintenance, and data transmission failures.

Structural missingness

The two 188 Quay Street Lower Albert sensors (EW and NS) were installed in 2022 and are therefore NaN for 2019–2021. To work with a uniform panel across all years, filter to the 19 original sensors:

from akl_ped_counts import load_hourly, SENSORS_ADDED_2022

df = load_hourly()
original = [c for c in df.columns
            if c not in ("date", "hour", "year")
            and c not in SENSORS_ADDED_2022]
df_uniform = df[["date", "hour", "year"] + original]

Sensor-level gaps

107 Quay Street has the highest non-structural missingness (~5.6% overall), concentrated in 2022 during extended sensor maintenance. 150 K Road has a minor gap (~1.6%) in 2023.

Recommended approaches to fill missing data

1. Drop missing rows — simplest approach, recommended when completeness matters:

# pandas
df = load_hourly(dropna=True)

# polars
from akl_ped_counts.polars_loader import load_hourly as pl_load
df = pl_load(dropna=True)

2. Forward/backward fill — suitable for short gaps of a few hours:

df = load_hourly()
sensor_cols = [c for c in df.columns if c not in ("date", "hour", "year")]
df[sensor_cols] = df[sensor_cols].ffill(limit=3)  # fill up to 3 consecutive hours

3. Linear interpolation — smooth estimation across moderate gaps:

df = load_hourly()
sensor_cols = [c for c in df.columns if c not in ("date", "hour", "year")]
df[sensor_cols] = df[sensor_cols].interpolate(method="linear", limit=6)

4. Seasonal median fill — use the median for the same hour and day-of-week:

df = load_hourly()
sensor_cols = [c for c in df.columns if c not in ("date", "hour", "year")]
df["dow"] = df["date"].dt.dayofweek

for sensor in sensor_cols:
    mask = df[sensor].isna()
    if mask.any():
        medians = df.groupby(["dow", "hour"])[sensor].transform("median")
        df.loc[mask, sensor] = medians[mask]

2025 camera upgrades

On 5 March 2025, five sensors were upgraded to wider recording zones: 30 Queen Street, 205 Queen Street, 210 Queen Street, 261 Queen Street, and 297 Queen Street. Counts from these sensors may show a step-change from this date. Heart of the City captures the additional area separately — contact them for disaggregated data.

Sensor locations

The 21 sensors span Auckland CBD from the Viaduct Harbour in the north to Karangahape Road in the south:

  • Waterfront: 107 Quay Street, 188 Quay Street Lower Albert (EW/NS), Te Ara Tahuhu Walkway
  • Lower Queen Street: Commerce Street West, 7 Custom Street East, 45 Queen Street, 30 Queen Street
  • Mid-city: 19 Shortland Street, 2 High Street, 1 Courthouse Lane, 61 Federal Street, 59 High Street
  • Upper Queen Street: 210 Queen Street, 205 Queen Street, 8 Darby Street (EW/NS), 261 Queen Street, 297 Queen Street
  • Karangahape Road: 150 K Road, 183 K Road

Data source and licence

Data collected by Heart of the City Auckland using automated pedestrian counting cameras. The system records movements (not images), so no individual information is collected.

Licensed under Creative Commons Attribution 4.0 International (CC BY 4.0).

Citation

If you use this data in research, please cite:

Heart of the City Auckland. Pedestrian Monitoring System Data (2019–2025).
https://www.hotcity.co.nz/pedestrian-counts

Contributing

For package maintainers and contributors, see CONTRIBUTING.md for build and publishing instructions.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

akl_ped_counts-0.1.0.tar.gz (2.3 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

akl_ped_counts-0.1.0-py3-none-any.whl (2.4 MB view details)

Uploaded Python 3

File details

Details for the file akl_ped_counts-0.1.0.tar.gz.

File metadata

  • Download URL: akl_ped_counts-0.1.0.tar.gz
  • Upload date:
  • Size: 2.3 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.20 {"installer":{"name":"uv","version":"0.9.20","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for akl_ped_counts-0.1.0.tar.gz
Algorithm Hash digest
SHA256 8a132cfa9a161aa04dec7261ef220c86aef644700735e2b22d4b4a2512b4aa73
MD5 da91167108c9715b4c9d3ee0213f928d
BLAKE2b-256 fef323c184f8d7243acbc6b945ae172cee7ae14c0c301f4d1224d8a34c761437

See more details on using hashes here.

File details

Details for the file akl_ped_counts-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: akl_ped_counts-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 2.4 MB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.20 {"installer":{"name":"uv","version":"0.9.20","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for akl_ped_counts-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 68eca253ee90890f1415279840f09f3815e461a77008a822bbdcf959e88cfc12
MD5 57fbabcfc7c931dbc7debf2bc0ff558f
BLAKE2b-256 2e7cd7809f4b005933881724d49cbecf401a346bc366246f0a8314d1bd3a1090

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page