Calgary 311 project and interview guide

Aftab Ahmed · Independent portfolio study · Source snapshot October 3 2026

This guide explains the project, its analytical decisions, its working browser dashboard and its Power BI source. Use it to prepare a short demonstration and answer technical interview questions. The strongest part of the project is the connection between a reproducible pipeline, transparent measure definitions and careful interpretation of real operational records.

The analysis covers 385,723 service requests submitted from January through September 2026. After excluding 3,800 explicit duplicate records, 381,923 requests remain. At the source snapshot, 25,636 were open. A total of 17,898 had been open at least 30 calendar days. These are descriptive workload measures; they do not establish that service standards were missed.

Why this project fits the portfolio

The project adds request-level operational analysis alongside the existing Edmonton retail time-series project. It demonstrates SQL joins and window functions, Python data preparation, a dimensional model, filter-aware metrics, data-quality decisions and communication of limitations. The browser version gives a reviewer immediate access to the analysis without requiring Power BI or a sign-in.

The generated Power BI project provides editable source for three report pages and 14 DAX measures. JSON structures and embedded data have been checked, but Desktop execution remains pending. Describe it as Power BI project source until opening, refresh and rendering checks are completed.

The problem and intended audience

An operations analyst needs to distinguish incoming demand from requests that remain open. A large volume category may have relatively little open workload, while a smaller category may contain older open requests. Looking only at average closure duration can obscure both the slower tail of closed records and records that never entered the closed sample.

This portfolio dashboard supports exploration by submission month and service category. Its intended audience is a reviewer or analyst examining where to investigate further. No City stakeholder commissioned it, and no operational improvement or financial saving has been measured.

A three minute demonstration

  1. Start with scope. Explain that January to September defines when requests were submitted. October 3 is the date used for observed status and open age.
  2. Show Overview. Point to 381,923 requests, 25,636 open, a two-day median and a 21-day P90. Explain that the duration sample contains 356,287 valid Closed records.
  3. Filter to September. The cards should show 40,697 requests, 8,526 open, a one-day median and a nine-day P90. Explain that September requests have shorter follow-up than January requests.
  4. Show Open workload. Reset filters, then compare age bands and categories. Roads Signs Missing Damaged has 2,746 open requests, including 2,502 aged 30 days or more.
  5. Finish with Data quality. Show the 3,800 duplicate exclusions, 1,621 Open records with a closure date and 24,237 records without a community code. Explain why those records are treated differently.
Browser dashboard overview with KPI cards and demand charts
Actual browser demonstration. Power BI visuals have not yet been validated in Desktop.

How the data moves through the project

The Node.js downloader queries the public Socrata API in pages of 10,000 records, ordered by request ID. It selects only ten useful fields and checks the source update timestamp before and after extraction. It verifies uniqueness and row counts, then writes a CSV plus a SHA-256 manifest.

Python validates that preserved input and derives dates, flags, dimensions and durations. The same fact and dimension tables are loaded into an in-memory SQLite database. SQL computes independent summaries and percentiles, and assertions compare them with Python. The browser consumes compact aggregates with exact duration histograms. A separate generator embeds compressed CSV tables in the native Power BI source.

City public API
  → selected CSV + provenance manifest
  → Python request fact + dimensions
  → independent SQLite reconciliation
  → browser aggregates + portable Power BI project

The pipeline deliberately avoids packages for its main build. Python 3.10 or later and Node.js 18 or later are sufficient. Viewing the HTML dashboard needs only a modern browser. Native Power BI authoring requires Power BI Desktop on Windows. No paid cloud service or scheduled refresh has been configured.

The model and its grain

TableGrainRole
FactRequestsOne source request IDFlags, dates, closure duration and open age
DimServiceService name plus agencyPreserves source category distinctions
DimChannelSource channelApp, Other and Web
DimCommunityCommunity code plus nameRetains missing values as Unknown
DimDateCalendar daySubmission month and weekday
SnapshotOne metadata recordAs-of date and cohort boundaries

Four dimension relationships filter the fact in one direction. Snapshot is disconnected. Dimension keys are deterministic within the snapshot, but they are not guaranteed to remain stable after a new download. Service categories with similar names are kept separate because no verified consolidation map exists.

SQL you should be able to explain

This query joins requests to the submission calendar, excludes explicit duplicates and uses a window function to return the preceding month's volume. The previous value is descriptive; comparing it does not establish seasonality.

SELECT d.month,
       COUNT(*) AS requests,
       SUM(f.is_open) AS open_at_snapshot,
       LAG(COUNT(*)) OVER (ORDER BY d.month)
         AS previous_month_requests
FROM FactRequests f
JOIN DimDate d ON d.date = f.requested_date
WHERE f.duplicate = 0
GROUP BY d.month
ORDER BY d.month;

COUNT counts every nonduplicate request, regardless of whether it is now open or closed. SUM of the binary open flag counts currently Open requests. The date join uses submission date, so the open count cannot be interpreted as historical month-end backlog.

The percentile query in sql/analysis.sql ranks valid closure durations using ROW_NUMBER, obtains the sample size with a window count and interpolates the positions 1 + (n − 1) × p. This independently checks Python's median and P90 without relying on a database-specific percentile extension.

DAX and filter context

Requests =
CALCULATE (
    COUNTROWS ( FactRequests ),
    FactRequests[duplicate] = 0
)

Median Closure Days =
MEDIAN ( FactRequests[closure_days] )

P90 Closure Days =
PERCENTILEX.INC (
    FILTER (
        FactRequests,
        NOT ISBLANK ( FactRequests[closure_days] )
    ),
    FactRequests[closure_days],
    0.9
)

Dimension selections filter the fact before measures are evaluated. Closure duration is populated only for valid nonduplicate Closed records, so blank values keep Open and duplicate records out of the duration sample. Zero-day closures remain valid observations. P90 explicitly filters blanks. The model definitions have not yet been executed in Desktop, so these explanations describe the intended calculation and the independently checked Python and SQL equivalents.

Findings and reasonable follow up

Demand peaked in June within this cohort. June had 54,163 nonduplicate requests, followed by July with 52,529. A sensible follow-up is to compare several years with consistent service definitions before calling this a seasonal pattern.

The closed-duration distribution has a slower tail. The median is two days and P90 is 21 days. This indicates a substantial difference between the middle and upper portion of the closed sample. It does not include unresolved waiting time, and the mix of services contributes to the combined distribution.

Older open work is visible. Of 25,636 Open requests, 17,898 are at least 30 days old and 10,481 are at least 90 days old. A reasonable next step would be to obtain service-specific targets, priority and scheduled-work information before assessing timeliness or recommending staffing changes.

Status conflicts need explicit treatment. The 1,621 records marked Open despite containing a closure date could reflect reopening or source workflow behavior. This project does not assume a cause; it follows current status for open workload and flags the inconsistency.

Data quality page showing inclusion, exclusions and source limitations
The quality page exposes decisions that change the interpretation of the headline measures.

Interview questions and answer points

What was the hardest analytical decision

Separating administrative closure from actual repair, and separating closed duration from open age. Combining them into a single resolution-time figure would misrepresent both the source semantics and incomplete outcomes.

Why use median and P90 instead of just an average

The median describes the midpoint and P90 makes the slower tail visible. Both are affected by service mix and the exclusion of still-open records. The SQL service output also includes a mean, which can support further distribution comparisons.

Why can you not average category medians

A median depends on the ordered observations, not just the medians of groups. Even weighting group medians by their counts does not generally recover the combined median. The browser instead combines counts for each duration and recalculates the desired rank.

How did you validate the figures

The build checks source checksum, row count, unique IDs and cohort dates. Python and SQL independently compute counts and percentiles. Browser aggregation is compared with those outputs. Separate fixtures test duplicates, missing dates, zero-day closure, future dates, unknown status and percentile interpolation. Model checks verify that embedded CSVs decode to exactly the source bytes and that dimension joins contain no orphan facts.

Why not treat every blank closure date as an error

Open requests normally lack a closure date. A missing date is flagged as a closure-quality issue when the status is Closed. Open records are instead evaluated for their age at the snapshot.

Does a September filter show September backlog

No. It selects requests submitted in September and uses their observed status at the later source snapshot. Historical backlog requires status history or archived snapshots covering all relevant submission cohorts.

What business recommendation can you support

The evidence supports targeted review of older open categories and verification of status anomalies. It does not support a specific headcount, savings estimate or SLA judgment. Those require service targets, work schedules, complexity, costs and outcome information.

What would a production version need

Incremental ingestion with durable keys, archived snapshots or status events, source change monitoring, validated service mappings, agreed targets, access controls where needed, observability and ownership. Those are proposed extensions, not implemented features.

How does it relate to Power Platform

The dimensional model and explicit measures are relevant to Power BI work. A future controlled intake or review workflow could use Power Apps and Power Automate, but this project does not contain those applications. Do not claim them as delivered components.

Practice before presenting

  1. Run the three-minute walkthrough without reading a script.
  2. Explain why the 3,800 duplicates remain in source records but disappear from Requests.
  3. Use September and WRS Cart Management together. Expect 2,475 requests, 42 open, median one day, P90 four days and 2,433 valid closures.
  4. Open the SQL and DAX files and explain the join grain, flags and filter context.
  5. State the native Power BI validation status accurately.

Rebuild and continue

python -m unittest discover -s tests -v
python scripts/build.py
python scripts/build_powerbi.py
python scripts/check_model.py
node tests/test_dashboard.cjs

Use the preserved CSV for reproducibility. A live download changes the snapshot and must be followed by a new build, narrative review and replacement screenshots. The generated report and semantic-model files are overwritten by the generator. The README and validation document identify remaining acceptance checks.

Sources and attribution

Contains information licensed under the Open Government Licence – City of Calgary. The official 311 dataset supplies the records. Calgary's 311 FAQs explain administrative closure. This independent project implies no City endorsement.