How I turn engineering questions into data analysis.

This portfolio is deliberately detailed. Instead of listing software and claiming skills, I show what I did: the question I started with, what the source looked like, how I cleaned and modelled it, the SQL or reporting logic I used, how I tested the result and what decision the output supports.

The same process across every case study.

The tools change from project to project. The analytical discipline does not.

01

Start with the decision

I write the question before touching the dashboard. If I cannot explain what somebody should be able to decide differently after the analysis, I do not have a useful project yet.

02

Understand the source

I check who produced the data, what one row represents, the period covered, units, revision notes, missing values and anything in the source definition that could change the interpretation.

03

Reshape the raw information

I separate fields that have been buried inside labels, convert dates and numbers to controlled types, create keys and dimensions, remove duplicates only when I can explain why they are duplicates and preserve missing data as missing.

04

Build the analytical model

I organise the cleaned data around the question: activity + month for price analysis, activity + reporting snapshot for project controls, or information event + package + discipline for RFI/submittal reporting.

05

Query, calculate and visualise

I use SQL, Power Query, DAX or browser visualisation depending on the job. The calculation stays visible so a result can be traced back to the source rows.

06

Validate before interpreting

I test row counts, keys, date logic, expected ranges and source totals. Only after the checks pass do I write the finding and explain the limitation.

NEW

The Approval Gap

Why plans approved and buildings completed can tell very different stories about the construction pipeline — and why I refused to call a same-period ratio a conversion rate.

SourceStats SA P5041.1 · May 2026ToolsPython · pandas · SQL · interactive charts
BACKGROUND

A building plan approved today is not a building completed today. The two statistics sit at different stages of the construction pipeline, so comparing them without thinking about time can create a false “conversion” story.

THE PROBLEM

I wanted to measure how the upstream approval side and downstream completion side were moving, then test whether the available monthly history was strong enough to support a lag conclusion.

WHAT I FOUNDIn real terms, plans passed fell 2.9% in Jan–May 2026 while buildings completed fell 8.0%. Non-residential completions fell 20.7% compared with a 6.4% fall in plans passed.
−2.9%real plans passed
−8.0%real buildings completed
−14.3 ppnon-residential completion vs approval movement gap
17 monthslag-test window — too short for a stable lag claim
WHY THIS CASE MATTERS

The useful result is partly what I did not claim.

The same-period completed/plans value for Jan–May 2026 is 47.7%, but I do not call that a conversion rate. The completed buildings generally come from approvals in earlier periods. I tested lags from 0 to 6 months on the 17-month real series; the short window does not show a convincing delayed peak, so the next correct step is to import longer history rather than invent a 9- or 12-month lag.

01

The Bitumen Effect

Why two legitimate construction-material series from the same Stats SA release can give a very different road-cost signal.

SourceStats SA P0151.1 · Oct 2025ToolsExcel · Power Query · SQL · JavaScript
THE QUESTION I STARTED WITHWhen I look at road-material inflation, how much does the answer change depending on whether bitumen is included in the published activity series?
STEP 1

I started from the official release, not from a chart.

The source publishes civil-engineering material indices by activity. For several road activities there are paired series — one includes bitumen and the other excludes it. That immediately creates an analytical problem: if somebody says “road materials increased by X%”, which of those legitimate series are they actually using?

Reference releaseOctober 2025
BaseDecember 2023 = 100
Road comparisonIncluded vs excluded bitumen
Public extract used13 activity rows
STEP 2

I converted the publication into an analysis-ready table.

The raw publication is designed for reading, not querying. Activity names carry analytical meaning inside the text, decimal-comma values need numeric conversion, and some historical values are unavailable. I therefore created separate fields for activity, period, index, pct_yoy and bitumen_treatment.

PUBLICATION LABELRoads - reseal [series including bitumen]
ANALYTICAL ROWactivity = Roads - reseal
bitumen_treatment = included
pct_yoy = 0.1

I did not fill missing historical observations with zero because zero would mean “no movement”; missing means the comparison value is not available.

STEP 3

I paired the two versions of each activity with a SQL self-join.

Once the table was in long format, I could pair the included and excluded versions of the same road activity and calculate the difference directly.

SELECT
    i.activity,
    i.pct_yoy AS incl_bitumen,
    e.pct_yoy AS excl_bitumen,
    ROUND(e.pct_yoy - i.pct_yoy, 1) AS gap_pp
FROM civil_material_index i
JOIN civil_material_index e
  ON e.activity = i.activity
 AND e.period   = i.period
WHERE i.bitumen_treatment = 'included'
  AND e.bitumen_treatment = 'excluded'
  AND i.period = '2025-10'
ORDER BY gap_pp DESC;

The point of the join is not to make the SQL look complicated. It makes the comparison explicit: same activity, same month, only the bitumen treatment changes.

STEP 4

The result showed where the choice of series matters most.

ActivityIncludedExcludedGap
Roads - general
5.1%
7.7%
+2.6 pp
Roads - refurbishment
5.5%
8.1%
+2.6 pp
Roads - reseal
0.1%
8.9%
+8.8 pp
Bulk earthworks
7.0%
7.5%
+0.5 pp
What I noticed

Road reseal is the clearest example: the October 2025 year-on-year signal is 0.1% with bitumen included and 8.9% with bitumen excluded — an 8.8 percentage-point gap. Roads general and refurbishment each show a 2.6-point gap, while bulk earthworks is only 0.5 points apart.

STEP 5

I then checked contribution, not only the biggest percentage move.

A material can have a dramatic percentage increase but a small effect on the total basket if its weight is small. So the second part of the analysis ranks weight × price movement, rather than ranking percentages alone.

SELECT
    product,
    weight,
    pct_yoy,
    ROUND(weight * pct_yoy / 100, 2) AS contribution_pp
FROM construction_input_index
WHERE period = '2025-10'
ORDER BY ABS(contribution_pp) DESC;

This is the practical difference between reporting “the biggest price increase” and explaining “what actually pushed the overall construction-input index”.

STEP 6

I validated the extract before writing the conclusion.

The current validation file checks that all 13 activity rows are present, the October 2025 index is populated, the bitumen treatment field contains only the expected values, and the two known historical comparison gaps remain visible as warnings rather than being silently imputed.

DECISION / TAKEAWAY

For road-material exposure I would not substitute one generic construction headline. I would document which activity-specific series is being used, whether bitumen is included, and why that series matches the exposure being assessed.

VISUAL ANALYSIS

How the road signal changes when bitumen treatment changes

The charts below use the same October 2025 extract. No file download is required to understand the result.

02 · Gap rankingExcluded minus included · percentage points
Roads - reseal
8.8 pp
Roads - general
2.6 pp
Roads - refurbishment
2.6 pp
Bulk earthworks
0.5 pp
What I foundRoad reseal is the clear outlier in this snapshot.
03 · Index contextOct 2024 → Sep 2025 → Oct 2025
Roads - general101.1106.6106.3
Roads - refurbishment101.2107.1106.8
Roads - reseal99.499.899.5
Bulk earthworks101.8109.0108.9
What this answersIs the October result part of a larger move, or only a one-month change?
04 · Road signal matrixIndex, monthly movement, annual movement and bitumen gap
ActivityIndexMoMYoY incl.YoY excl.Gap
Roads - general106.3-0.3%5.1%7.7%2.6 pp
Roads - refurbishment106.8-0.3%5.5%8.1%2.6 pp
Roads - reseal99.5-0.3%0.1%8.9%8.8 pp
Bulk earthworks108.9-0.1%7.0%7.5%0.5 pp
02

Project Controls Performance

Turning activity-level schedule snapshots into a report that shows where the programme is slipping and which packages need attention.

Model800 activities · 24 monthly snapshotsToolsPower Query · SQL · Power BI / DAX
THE QUESTION I STARTED WITHIf management only sees one overall progress percentage, how do I show which packages are falling behind, which activities are losing float and whether the forecast finish is moving?
STEP 1

I treated the programme as a time series, not as one static schedule.

A single activity table tells me the current state, but it does not tell me how that state changed. I therefore used a monthly snapshot fact table: the same activity can appear once per reporting period with the planned progress, actual progress, current forecast finish, remaining duration, float and resource hours recorded for that month.

Activities800
Reporting snapshots24
Fact rows19,200
Packages10
STEP 2

I separated baseline, forecast and actual fields.

Those fields are not interchangeable. Baseline finish is the approved reference. Forecast finish is where the activity is currently expected to finish. Actual finish is evidence that the work has finished. The reporting model keeps all three so slippage can be measured rather than overwritten.

DimActivity / WBS1 → manyFactProjectControlSnapshotmany → 1DimDate
STEP 3

I used window functions to get the latest activity state while retaining history.

WITH ranked AS (
  SELECT *,
         ROW_NUMBER() OVER(
           PARTITION BY activity_id
           ORDER BY snapshot_date DESC
         ) AS rn
  FROM project_control_snapshot
)
SELECT *
FROM ranked
WHERE rn = 1;

The historical snapshots remain available for trend analysis. The row-number query simply produces the latest-state table used for exception reporting.

STEP 4

I built an S-curve view so the direction of travel is visible.

Planned Actual
Jan 2025Dec 2025Dec 2026
SnapshotPlannedActualVarianceActual hours
2025-03-31 7.5% 6.0% -1.5 pp 50,016
2025-08-31 39.9% 37.0% -2.9 pp 311,808
2025-12-31 62.8% 60.8% -2.0 pp 513,032
2026-04-30 81.6% 80.8% -0.8 pp 681,510
2026-08-31 96.9% 95.9% -1.0 pp 807,320
2026-12-31 100.0% 100.0% 0.0 pp 842,773

For example, at the August 2025 snapshot planned progress is 39.9% and actual is 37.0%, a 2.9 percentage-point shortfall. By December 2025 the gap is 2.0 points. That trend is more useful than waiting until completion to discover that the forecast moved.

STEP 5

I added exception reporting for float, milestones and resources.

Forecast delay at final snapshot14 days
Late milestones45
Negative-float activities323
Resource variance55,414 h

The purpose is not to claim that every negative-float activity is equally important. The report gives the project controller a shortlist: which package, which activity, how the forecast changed and whether the issue is becoming more critical over time.

STEP 6

I tested the model before trusting the dashboard.

The validation checks confirm 19,200 snapshot rows, 800 unique activities, 24 reporting periods, no orphan activity keys, no progress outside 0–100%, no missing WBS keys and no forecast finish before the activity start.

DECISION / TAKEAWAY

I would use the dashboard to move the conversation from “the project is 37% complete” to “these packages are below plan, these activities are losing float, these milestones are slipping and this is how the forecast has changed since the previous report.”

VISUAL ANALYSIS

From one progress percentage to a project-controls exception report

Reference snapshot: 2025-12-31.

02 · Package progress varianceActual minus planned · 2025-12-31
Roads South
-3.8 pp
Close-out
-2.7 pp
Roads North
-2.2 pp
Testing & Commissioning
-2.2 pp
Utilities
-2.2 pp
Landscape
-1.9 pp
Structures
-1.7 pp
Electrical
-1.6 pp
Drainage
-1.4 pp
Traffic
-1.4 pp
What this answersWhich packages are pulling the overall project below plan?
03 · Milestone slippageLargest forecast delay at 2025-12-31
ACT-0408 · Roads North
+74 d
ACT-0716 · Roads North
+59 d
ACT-0286 · Traffic
+54 d
ACT-0592 · Utilities
+45 d
ACT-0263 · Drainage
+38 d
ACT-0424 · Landscape
+35 d
ACT-0465 · Close-out
+32 d
ACT-0353 · Close-out
+32 d
What this answersWhich milestone dates have moved furthest from baseline?
04 · Float risk by packageNear-critical and negative-float activity counts
Roads South
20 neg.
Close-out
24 neg.
Roads North
23 neg.
Testing & Commissioning
28 neg.
Utilities
25 neg.
Landscape
27 neg.
Structures
13 neg.
Electrical
24 neg.
Drainage
23 neg.
Traffic
24 neg.
What this answersWhere is schedule resilience being lost before the overall finish date becomes the only story?
05 · Planned vs actual resource hoursCumulative hours over reporting periods
Planned hoursActual hours
What this answersIs resource consumption moving differently from the planned curve?
06 · Forecast completion movementForecast delay against baseline by reporting month
25-01
25-02
25-03
25-04
25-05
25-06
25-07
25-08
25-09
25-10
25-11
25-12
26-01
26-02
26-03
26-04
26-05
26-06
26-07
26-08
26-09
26-10
26-11
26-12
What this answersIs the forecast recovering, stable or progressively moving away from baseline?
03

Engineering Information Performance

Using RFI, submittal and deliverable records to identify ageing, approval bottlenecks and packages where information flow is becoming a project risk.

Records52,000 eventsToolsPower Query · PostgreSQL · SQL · DAX
THE QUESTION I STARTED WITHHow do I turn thousands of RFI, submittal and deliverable records into a management view that tells me where information is stuck?
STEP 1

I defined one information-event structure.

RFIs, submittals and deliverables have different workflows, but the reporting questions overlap: when was it created, when was it due, who owns it, is it still open, how old is it, how long did it take to close and which package or discipline is affected?

record_typepackagedisciplineresponsible_partycreated_datedue_dateclosed_datestatus
STEP 2

I separated open ageing from closed cycle time.

An open RFI does not yet have a completed cycle time. For open items I calculate age from the creation date to the reporting cut-off. For closed items I use the elapsed time between creation and closure. Mixing those two measures would make the KPI difficult to interpret.

SELECT
    package,
    COUNT(*) FILTER (
      WHERE record_type='RFI' AND status='Open'
    ) AS open_rfis,
    PERCENTILE_CONT(0.5) WITHIN GROUP (
      ORDER BY CURRENT_DATE-created_date::date
    ) FILTER (
      WHERE record_type='RFI' AND status='Open'
    ) AS median_open_age_days
FROM engineering_information_event
GROUP BY package;
STEP 3

I compared packages instead of reporting one project-wide average.

PackageEventsOpen RFIsMedian RFI ageFirst-pass approvalDeliverables on time
PKG-ROAD-01 6,486 32 452 d 75.5% 53.4%
PKG-ROAD-02 6,514 19 383 d 75.7% 31.8%
PKG-DRAIN-01 6,738 22 323 d 60.8% 26.1%
PKG-STRUCT-01 6,435 31 356 d 76.0% 38.9%
PKG-ELEC-01 6,435 20 440 d 73.7% 53.2%
PKG-UTIL-01 6,408 21 389 d 61.2% 18.7%
PKG-LAND-01 6,509 16 424 d 76.2% 54.0%
PKG-TRAFFIC-01 6,475 31 367 d 76.2% 52.7%
What I would investigate first

The Utilities package has only 18.7% of deliverables on time and a 61.2% first-pass submittal approval rate. The drainage package also has a low 60.8% first-pass approval rate and 26.1% on-time deliverables. Those numbers do not diagnose the cause by themselves, but they tell me where to drill into revisions, responsible parties and overdue records.

STEP 4

I designed measures that answer operational questions.

Open RFIs

How much unresolved technical information is sitting in the workflow?

Median Open RFI Age

Is the current backlog recent, or has it been sitting unresolved for months?

First Pass Approval %

How often are submittals accepted without another revision cycle?

Deliverables On Time %

Are required engineering outputs arriving by their due dates?

The full evidence pack includes the DAX versions of these measures so the reporting logic can be inspected rather than inferred from the chart.

STEP 5

I validated the event structure before analysing performance.

The checks confirm 52,000 rows, 52,000 unique event IDs, only the three expected record types, no missing created/due dates, no negative cycle days and no closed records missing their closure date.

DECISION / TAKEAWAY

The report should not simply say “there are 203 open RFIs”. It should show which package owns the backlog, how old it is, whether approval performance is deteriorating and where the project team should investigate first.

VISUAL ANALYSIS

Where engineering information is accumulating, ageing or cycling through revisions

Reporting cut-off: 2026-12-18.

01 · Open RFIs by packageCurrent unresolved technical queries
PKG-ROAD-01
32
PKG-STRUCT-01
31
PKG-TRAFFIC-01
31
PKG-DRAIN-01
22
PKG-UTIL-01
21
PKG-ELEC-01
20
PKG-ROAD-02
19
PKG-LAND-01
16
What this answersWhere does the unresolved RFI workload currently sit?
02 · RFI ageing bucketsOpen RFI age at reporting cut-off
0–7 days0
8–14 days0
15–30 days0
31+ days192
What this answersIs the backlog mostly recent, or has it been sitting unresolved for a long period?
03 · RFIs opened vs closed over timeMonthly workflow balance
OpenedClosed
What this answersAre closures keeping pace with new RFIs, or is the backlog likely to grow?
04 · First-pass submittal approvalBy package
PKG-ROAD-01
75.5%
PKG-STRUCT-01
76.0%
PKG-TRAFFIC-01
76.2%
PKG-DRAIN-01
60.8%
PKG-UTIL-01
61.2%
PKG-ELEC-01
73.7%
PKG-ROAD-02
75.7%
PKG-LAND-01
76.2%
What this answersWhere are repeated revision cycles most likely to require investigation?
05 · Deliverables issued on timeBy package
PKG-ROAD-01
52.0%
PKG-STRUCT-01
38.0%
PKG-TRAFFIC-01
51.5%
PKG-DRAIN-01
25.4%
PKG-UTIL-01
18.3%
PKG-ELEC-01
51.7%
PKG-ROAD-02
31.0%
PKG-LAND-01
52.7%
What this answersWhich packages are missing required information dates most often?
06 · Responsible-party workloadOpen and overdue records
Contractor
274 open · 274 overdue
Designer A
266 open · 266 overdue
Designer B
256 open · 256 overdue
Engineer
247 open · 247 overdue
Specialist
245 open · 245 overdue
What this answersWho owns the unresolved workload and where should the team drill into individual records?
04

How I test a report before I trust it.

A dashboard can be visually perfect and still be wrong. I keep the data-quality evidence next to the analysis.

Current run22 testsResult21 PASS · 1 WARN · 0 FAIL
THE QUESTION I STARTED WITHBefore somebody acts on a dashboard, what evidence do I have that the rows, keys, dates and calculations are actually valid?
STEP 1

I write the expected result before I run the test.

Each validation record contains the area being tested, the rule, the expected result, the actual result, a PASS/WARN/FAIL status, severity and the evidence file. That prevents a quality check from becoming an informal “it looks okay” review.

STEP 2

The current validation run is inspectable row by row.

AreaTestExpectedActualStatus
Engineering InformationRow count52,00052000PASS
Engineering Informationevent_id uniqueness52,000 unique52000PASS
Engineering InformationAllowed record typesRFI / SUBMITTAL / DELIVERABLEDELIVERABLE, RFI, SUBMITTALPASS
Engineering InformationMissing created_date00PASS
Engineering InformationMissing due_date00PASS
Engineering InformationNegative cycle_days00PASS
Engineering InformationClosed records missing closed_date00PASS
Engineering InformationPackage dimension coverage>= 5 packages8PASS
Engineering InformationDiscipline dimension coverage>= 5 disciplines8PASS
Project ControlsSnapshot row count1920019200PASS
Project ControlsUnique activity master800 unique800PASS
Project ControlsSnapshot count2424PASS

The one warning in the full run is deliberate: the published Stats SA case has two historical comparison gaps. I leave those as a documented warning instead of converting them to zero and pretending the history is complete.

STEP 3

I use severity to decide whether publication should stop.

PASSThe rule produced the expected result.
WARNThe limitation is understood and remains visible in the analysis.
FAILA high-severity failure blocks publication until the issue is resolved.
DECISION / TAKEAWAY

Data quality is part of the analytical deliverable. If a management number cannot be reconciled back to its source rows and validation rules, I do not consider the report complete.

VISUAL ANALYSIS

Quality evidence before a management number is published

The detailed test rows remain downloadable, but the page shows the quality state first.

21PASS
1WARN
0FAIL
22TOTAL TESTS
01 · Validation by datasetWhere each test result came from
Engineering Information9 pass0 warn0 fail
Project Controls9 pass0 warn0 fail
Published Stats SA case3 pass1 warn0 fail
02 · Validation by categoryCompleteness, domain, keys, dates and source coverage
Completeness9 pass0 warn0 fail
Date logic1 pass0 warn0 fail
Other3 pass0 warn0 fail
Range / domain5 pass0 warn0 fail
Referential integrity1 pass0 warn0 fail
Source coverage0 pass1 warn0 fail
Uniqueness2 pass0 warn0 fail
03 · Items requiring attentionOnly warnings or failures are surfaced here
WARN · Historical comparison gapsPublished Stats SA case — expected Documented source gaps; actual 2; severity Medium
04 · Publication gateA visible control, not a hidden QA note
RAW→ checks →CHECKED→ reconciliation →VALIDATED→ analyst approval →PUBLISHED
RuleA high-severity FAIL stops publication. A WARN stays visible and must be explained.
05

Civil Infrastructure Tender Intelligence

The engineering work behind turning a public procurement API into a controlled dataset instead of manually searching tender notices.

SourceNational Treasury eTender OCDS APIStackREST / JSON · PostgreSQL · PHP
THE QUESTION I STARTED WITHHow can I take public procurement releases, retain the raw evidence and build a repeatable civil-infrastructure classification without pretending the portal is a complete picture of the whole market?
STEP 1

I separated ingestion from publication.

OCDS APIRaw release snapshotSchema / quality checksLatest state per OCIDCivil / road classificationPublished intelligence

A source being reachable does not mean its new values should immediately replace a trusted dashboard. The pipeline records the raw payload first, then checks the structure and completeness before promoting anything.

STEP 2

I kept classification explainable.

For road/asphalt analysis I retain the matched words that caused the classification. Explicit terms such as asphalt, hot mix, premix, overlay or milling and overlay are stronger evidence than a generic “roadworks” label. Seal/reseal/slurry/chip-seal work is kept separate from hot-mix asphalt.

Professional consulting/design tenders can still belong in civil procurement intelligence, but they do not automatically contribute to the asphalt works-demand signal.

STEP 3

The next validation is classifier precision and recall.

I do not publish an accuracy percentage until there is a manually reviewed validation sample. The planned test is straightforward: manually label a sample of real tender descriptions, compare the human label with the Yatify result, then calculate precision, recall, false positives and false negatives.

WHY THIS MATTERS

A recruiter can see the difference between “I can call an API” and “I can build a data pipeline with raw retention, schema checks, explainable classification, validation and a publication gate.”

PIPELINE VISUALS

The pipeline is shown honestly before market charts are published

Production market charts are added only after real imported records pass validation.

01 · Source-to-publication pipelineRaw evidence is retained before classification
OCDS APIRAW JSONSTAGINGQUALITY CHECKSLATEST OCID STATECLASSIFICATIONYATIFY
02 · Data modelCore analytical entities
Procurement ReleaseBuyerTenderLocationClassificationSource Snapshot
03 · Road/asphalt classification treeExplainable matching logic
Explicit asphalt / hot mix / premix / overlay?YES → High asphalt signal
Road rehabilitation / widening / upgrade?YES → Medium signal
Generic road works?YES → Low signal
Reseal / slurry / chip seal?Keep as separate bituminous treatment
04 · Pipeline readinessWhat is implemented and what still needs validation
Official OCDS APIreachable
Raw release snapshotimplemented
Schema / quality checksimplemented
Latest state per OCIDimplemented
Civil / road classificationimplemented
Human-labelled precision / recall testpending
Production publicationpending validation
Next analytical milestoneManually label a production sample and publish precision, recall, false-positive and false-negative results before relying on classifier counts.

The transfer is practical, not decorative.

Layer / quality acceptance

On site I compare test evidence with a specification before accepting a layer. In analytics the equivalent is defining the expected result, running the validation and refusing to publish a failed high-severity check.

Measurement & reconciliation

Engineering measurement teaches me to ask where a total came from, which source record supports it and why two records do not reconcile. That same habit is useful in SQL and management reporting.

Drawings, packages & locations

Construction work is already structured by areas, packages, disciplines and activities. That makes WBS/project-control and engineering-information models intuitive rather than abstract.

Recurring site reporting

A monthly or weekly report has to be repeatable. That is exactly why I am interested in Power Query, SQL pipelines and controlled definitions instead of rebuilding a spreadsheet manually every reporting cycle.

This is the level of work I want the portfolio to be judged on.

For employment opportunities, reporting/data roles or engineering-data collaboration, contact me directly. The individual case-study pages contain additional source notes, SQL and downloadable evidence.