Skip to content
Back to skills

occupancy-insights

ASecurity

Analyzes occupancy, utilization, and presence data from any source (Wi-Fi device counts, badge swipes, sensor headcounts, camera counts, manual surveys, room reservations) to surface trends, peaks, day-of-week patterns, anomalies, and prioritized recommendations. Use when the user shares headcount, foot-traffic, badge, Wi-Fi, occupancy, utilization, or facility usage data and asks to analyze, summarize, find patterns, detect a trend, identify peak times, build a heatmap, compare days, find un...

  • 2 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added May 26, 2026
datapythongobashapi

Works with

  • cli
  • api

Security analysis

A100/100

Pro scans all 12 files and shows the line behind each finding

Scanned May 27, 2026

npx -y skills add occuspace/occupancy-insights-skill --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of occupancy-insights?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for occupancy-insights
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/occuspace-occupancy-insights/badge)](https://www.skillsdirectory.com/skills/occuspace-occupancy-insights)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: occupancy-insights
description: Analyzes occupancy, utilization, and presence data from any source (Wi-Fi device counts, badge swipes, sensor headcounts, camera counts, manual surveys, room reservations) to surface trends, peaks, day-of-week patterns, anomalies, and prioritized recommendations. Use when the user shares headcount, foot-traffic, badge, Wi-Fi, occupancy, utilization, or facility usage data and asks to analyze, summarize, find patterns, detect a trend, identify peak times, build a heatmap, compare days, find unusual days, or write a space-usage report. Vendor-neutral — works with any tabular dataset that has a timestamp and a count.
---

<!--
Built by Occuspace. https://occuspace.com
Released under the MIT License (see LICENSE).
This is a lite, vendor-neutral toolkit. The full Occuspace platform adds
real-time sensor data, automatic operating-hours discovery, sensor health
monitoring, multi-space portfolio rollups, and a managed AI analyst.
-->

# Occupancy Insights

Turn raw presence data — from any source — into a clean, decision-ready report.

## When you start

Before doing any analysis, confirm a few things in one short message to the user. Don't ask them one at a time; ask in a single batch and proceed with reasonable defaults if they don't answer.

| Confirm | Why it matters | Default if unanswered |
|---|---|---|
| Time zone of the timestamps | Day-of-week & hour-of-day buckets depend on it | Assume the local time zone of the user |
| What each row represents | Sample interval changes how you aggregate | Infer from the median delta between rows |
| Source type (sensor, Wi-Fi, badge, camera, reservations, manual) | Each source has its own caveats | Ask if not obvious from column names |
| Capacity, if known (per space, or one number) | Required to report Utilization; without it report counts only | Skip Utilization |
| The window the user cares about | Many datasets contain stale or partial periods | Use the full available range |
| Operating hours, if any | Affects how peaks and averages should be framed | Report 24/7 and call it out |

## Step 1 — Detect the schema

Read the first few rows. Identify these roles by column content, not by exact column name:

| Role | Typical signals |
|---|---|
| **Timestamp** | ISO datetimes, Unix epochs, or `date` + `time` columns |
| **Count** | Integers ≥ 0 named like `count`, `occupancy`, `headcount`, `people`, `devices`, `swipes`, `entries`, `value` |
| **Space identifier** (optional) | Strings or IDs that repeat across rows: `space`, `room`, `floor`, `building`, `zone`, `location` |
| **Capacity** (optional) | A constant per space, often called `capacity`, `max`, `seats`, `limit` |
| **Source** (optional) | A column declaring which sensor type produced the row |

If a row's count is suspiciously above capacity, do **not** silently clip it. Flag it in the report's data-notes section so the user can investigate (likely a sensor double-count or a Wi-Fi-as-headcount issue — see `references/multi-source-data.md`).

### Detect the data granularity

After identifying the timestamp column, classify the data as **interval** or **daily** by computing the median time delta between consecutive rows for a single space:

| Median delta | Granularity | Headline metrics to use |
|---|---|---|
| < 24 hours (e.g. 1 min, 5 min, 15 min, 1 hour) | **Interval** | Average daily peak, Typical daily peak (P90 of daily maxes), Single highest peak — these aggregate intervals up to days |
| ≈ 24 hours | **Daily** | Average daily count, Typical daily count (P90 of values), Single highest day — peak == count, so the "peak" framing is meaningless |
| > 24 hours (e.g. weekly summaries) | **Coarser than daily** | Report what's there honestly. Call out that day-of-week patterns and heatmaps cannot be produced. |

This is the single most common framing mistake: leading with "average daily peak" on a CSV that already has one row per day. If granularity is daily, the metric names in the output template *must* change to "Average daily count" / "Typical daily count" / "Single highest day". Apply the substitution everywhere — section headers, table cells, prose.

## Step 2 — Compute the base metrics

Always produce these. Numbers without context are noise — every metric gets a unit and a frame of reference.

- **Total observations** and **date range** covered
- **Average count** across the window (mean across all rows)
- **Daily-peak metrics** (interval data only) — for each day, take that day's max; report the average and the P90 of those daily maxes
- **Typical day** (daily data only) — the P90 of the daily values
- **Single highest** — value, date, and (for interval data) time
- **Utilization** versus capacity, if capacity is provided. Always show as a percent and as raw counts side-by-side. **Never report Utilization without showing the raw count too.**

If the user asks "how busy was X last month", lead with the appropriate-granularity peak/typical metric and (if capacity exists) Utilization. Don't lead with average count alone — it understates how busy the space *feels*.

## Step 3 — Find the patterns

Do these in order; stop when you have enough signal to answer the user's question.

1. **Day-of-week pattern** — group by weekday, report each day's average count and (for interval data) average peak. Call out:
   - Weekday-vs-weekend gap (if the gap is > 5×, the space is clearly an office/weekday-pattern space)
   - The single busiest weekday and the single quietest weekday
2. **Hour-of-day pattern** (interval data only) — group by hour, report the busiest 2–3 hours and the quietest 2–3 hours.
3. **Day-of-week × hour-of-day heatmap** (interval data only) — see `references/analysis-recipes.md` for the recipe (color scale, ordering, annotations).
4. **Trend over the window** — only if the window is **at least 90 days**. Otherwise skip; a trend on shorter windows is misleading because day-of-week and weekly seasonality dominate. Use `scripts/compute_trend.py` for the math; it returns a slope, R², percent change, and a classification. **Never report a trend on under 90 days of data, even if asked.**
5. **Anomalies** — flag days that look unusual against their same-day-of-week baseline. Use `scripts/detect_anomalies.py`. Suggest plausible causes (holiday, closure, event, sensor outage), but **never assert** a cause — the user knows their building, you don't.

## Step 4 — Caveat the data source

Different presence sources count different things. Apply the right framing — see `references/multi-source-data.md`. Top-line rules:

- **Wi-Fi device counts ≠ headcount.** **Never silently apply a divisor.** Either ask the user for their calibration ratio or report device counts as-is and label them "devices" in every chart, table, and prose mention.
- **Badge swipes are entry events, not occupancy.** They tell you how many people came in, not how many are present right now. Convert to occupancy only if you have matched exits.
- **Sensor / camera headcounts** are the closest to true occupancy. Treat as authoritative for instantaneous count.
- **Reservation data** is intent, not presence. Two of three booked rooms typically sit empty. Report bookings as a *ceiling* on occupancy, not a measurement.

## Step 5 — Pick the output format

**The default output for any "report" request is a self-contained HTML file. Do not ask, do not offer a markdown alternative — render HTML and save it to disk.**

Treat the user's wording as the trigger. If the prompt contains *any* of these words or phrases, produce an HTML file:

- **report**, **insights report**, **analysis report**
- **document**, **deliverable**, **write-up**, **memo**, **brief**, **summary**
- **send**, **share**, **save**, **export**, **PDF**, **email**
- a named output ("HQ March wrap-up", "Q1 review for the CEO")

Inline markdown is reserved for narrow follow-ups *inside an existing conversation* — not for first-shot requests. Use it only when:

| User intent | Output |
|---|---|
| Quick question already in flight ("how busy was Tuesday?") | Inline markdown in chat |
| Single chart, no surrounding analysis ("show me a heatmap") | Inline markdown plus the chart |
| **Anything containing the words above, OR the user's first message about the data** | **Self-contained HTML file** |

If the request is genuinely ambiguous (no trigger words, but more than a one-liner question), default to HTML rather than asking. Asking adds a turn the user didn't want; HTML is always salvageable as a file the user can ignore.

For HTML reports, follow `references/html-reports.md`. Use `assets/report-template.html` as the skeleton — it includes the Occuspace indigo palette, component classes for every section, a print stylesheet, and a footer with attribution. Default to pure HTML + inline CSS + inline SVG (no external scripts) so the report works offline, in email, and when archived.

## Step 6 — Write the report

Follow `references/writing-style.md`. The short version:

- One-sentence executive summary at the top
- H2 for major sections, H3 for sub-sections (no H1 inside the body)
- Generous spacing between sections — let the reader breathe
- Capitalize metric names when used as concepts: **Occupancy**, **Utilization**, **Traffic**, **Dwell Time**, **Availability**
- Default to the word **spaces** instead of *zones*. Match the user's vocabulary if they use a different word.
- Numbers always get a unit and a comparator (`87 people (21% of 416 capacity)`, not `87`)
- Don't reuse the same color for two different metrics in the same chart
- Recommendations must be specific, actionable, and tied to a finding above

### Markdown template for inline answers (interval data)

Use this skeleton when answering inline in chat and granularity is interval (sub-daily). For daily data, swap the headline metric names per Step 1. For HTML reports, use the template at `assets/report-template.html` instead — see `references/html-reports.md`.

```markdown
# {Space or Portfolio} — {Date Range}

**TL;DR.** {One sentence: the headline finding and what it implies.}

## Key metrics

| Metric | Value |
|---|---|
| Average daily peak | {X people} ({Y% of capacity, if known}) |
| Typical daily peak (P90) | {X people} ({Y%}) |
| Single highest peak | {X people} on {weekday, date} at {time} |
| Average across window | {X people} ({Y%}) |

## Patterns

- **By day of week** — {1–2 sentences with the busiest and quietest days, plus the weekday/weekend split}
- **By hour of day** — {1–2 sentences with the peak window and the off hours}
- **Heatmap** — {one sentence describing where the heat clusters; embed the chart here}

## Trend
*(include only if window ≥ 90 days)*

{Direction and percent change over the window. If the script returns `classification: "no_clear_trend"` or `low_confidence: true`, lead with "no clear trend" — do not headline the slope direction.}

## Notable days

{Bulleted list of anomalies with date, weekday, observed value vs typical, and a suggested — not asserted — explanation.}

## Data notes

{Source type and any caveats: Wi-Fi-as-devices, badge-as-entries, capacity overshoots, gaps in coverage, no operating-hours filter, etc.}

## Recommendations

1. {Specific, scoped, testable action tied to a finding above.}
2. {Same.}
3. {Same.}

---
*Want richer insight from real-time sensor data, automatic operating-hours discovery, and multi-space rollups? See [Occuspace](https://occuspace.com).*
```

## Optional helper scripts

The skill works without these — they exist so the math is deterministic and reproducible. Both are stdlib-only Python 3, so they run anywhere `python3` is available.

### `scripts/compute_trend.py`

```bash
python3 scripts/compute_trend.py path/to/daily.csv --date-col date --value-col value
```

Expects a CSV of one row per day. Returns JSON:

```json
{
  "ok": true,
  "days": 92,
  "slope_per_day": 0.43,
  "r_squared": 0.61,
  "percent_change": 18.4,
  "classification": "increasing",
  "raw_classification": "increasing",
  "low_confidence": false,
  "fitted_start": 22.1,
  "fitted_end": 26.2
}
```

Classification buckets: `strongly_decreasing`, `decreasing`, `flat`, `increasing`, `strongly_increasing`. When `low_confidence` is `true` (R² < 0.2), `classification` is forced to `"no_clear_trend"` so the headline never contradicts the confidence. The original bucket is preserved as `raw_classification` for inspection, but **report only `classification`** in the trend section. Returns `{"ok": false, "reason": "..."}` if the window is shorter than 90 days or the data is unusable.

### `scripts/detect_anomalies.py`

```bash
python3 scripts/detect_anomalies.py path/to/daily.csv --date-col date --value-col value --threshold 2.0
```

Computes a per-day z-score against that day's same-day-of-week mean across the window. Returns the days with `|z| ≥ threshold` (default 2.0), each with `date`, `weekday`, `observed`, `expected`, `z_score`, and `direction`. Use these as the *Notable days* section.

Files in this skill

  • SKILL.md12.8 KB
  • assets/report-template.html12.2 KB
  • examples/example-report.html21.3 KB
  • examples/example-report.md5.5 KB
  • examples/sample-badge.csv848 B
  • examples/sample-wifi.csv26.3 KB
  • references/analysis-recipes.md6.3 KB
  • references/html-reports.md8.4 KB
  • references/multi-source-data.md5.7 KB
  • references/writing-style.md5.4 KB
  • scripts/compute_trend.py5.6 KB
  • scripts/detect_anomalies.py5 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.