Projects

Selected work in AI and analytics.

AI and automation

How I approach AI systems, and a retrieval project of my own.

AI · Personal project · 2026

RAG Research Assistant

Answers questions about five well-known papers on retrieval and RAG, cites the paper and page for every claim, and says when the papers don't cover a question. My earlier telecare chatbot never measured whether it found the right passages, so this time I measured it.

Live demo, hosted on Hugging Face Open full screen
01 Test setA model wrote questions from single pages, each with an exact quote as evidence. Questions whose quote wasn't on the page were dropped.
02 ReviewI reviewed the first batch by hand, then built an LLM judge with the same rules. In two runs it agreed with my decisions on 34 and 33 of 36 questions (Cohen's kappa 0.89 and 0.83).
03 CompareBM25, dense, hybrid and reranked hybrid retrieval on whole pages and on 1000, 500 and 250 character passages, with confidence intervals and question-by-question comparisons.
04 AnswerHybrid retrieval feeds six passages to the model, which answers only from them and cites each one. A Gradio app sits on top.

How often the right page was in the top 5 results

36 reviewed questions. Each retriever was tried on whole pages and on passages of 1000, 500 and 250 characters.

RetrieverWhole page1000500250
Keyword (BM25)33 of 3635 of 3632 of 3633 of 36
Hybrid30 of 3632 of 3634 of 3632 of 36
Embeddings (dense)19 of 3627 of 3630 of 3629 of 36

Used in the live demo Highest score

All sixteen results, with every measure

Hit@1 and @5: share of questions where the right page was first, or in the top 5. Evidence@5: stricter, the top-5 passage must also contain the question's whole evidence quote, matched on whole words. MRR and nDCG: how high the right page ranked (1 means always first; nDCG gives more credit to ranks 2 and 3). ms: retrieval time per question on my laptop.

RetrieverPassageHit@1Hit@5Evid@5MRRnDCGms
BM25page0.720.920.920.7880.8322
BM2510000.690.970.830.7880.8392
Hybrid + rerank5000.690.890.580.7770.830869
Hybrid2500.690.890.420.7680.80434
BM255000.670.890.610.7600.8114
Hybrid + rerank2500.670.830.390.7540.793515
Hybrid5000.610.940.610.7500.80630
Hybrid + rerank10000.670.860.690.7460.7991514
BM252500.580.920.360.7180.7745
Hybrid10000.560.890.780.7010.77326
Dense2500.610.810.390.6880.73026
Hybrid + rerankpage0.470.720.720.5980.6682363
Dense10000.470.750.560.5940.65822
Dense5000.390.830.500.5730.65724
Hybridpage0.310.830.830.5220.61923
Densepage0.220.530.530.3590.43221

What I found

No setup is clearly better than the one the demo uses. Compared question by question with 95% intervals, every rival's difference could be chance. Only embeddings alone and whole-page hybrid are clearly worse.

A cross-encoder reranker put the right passage first more often (Hit@1 0.61 to 0.69) but dropped others out of the top 5, and made retrieval about 30 times slower. I left it out of the demo. These Evidence figures use a corrected exact-match rule; an earlier version over-credited partial matches.

Checking the passage, not just the page, showed a trade-off: 1000-character passages held the evidence quote more often than 500 (78% against 61% in the top 5), partly because smaller passages are more likely to cut the quote in two. On the held-out questions the pattern didn't hold (60% for 500, 50% for 1000), so the demo stays at 500.

Keyword search did well partly because the test questions reuse the papers' wording. Embeddings on whole pages were poor because the model only reads the first 256 tokens.

Held-out check. On ten AI-assisted held-out questions, with evidence checked against the PDFs and kept out of choosing the demo's setup, keyword search on 1000 characters (joint best before) fell clearly behind the demo's hybrid setup in ranking (MRR 0.47 against 0.75), so its earlier lead didn't carry over. The reranker found all ten, but with so few questions that gain could still be chance.

What each metric means and how it helps the chatbot answer correctly →

Stack

Python · LangChain · BM25 · sentence-transformers · OpenAI · Pydantic · Gradio · pytest · GitHub Actions

How I work

How I build AI systems

Evaluate before shipping

Give every agent a test set, and checks that run before its output reaches a person.

Design for failure

Let uncertain cases degrade gracefully and go to a person, instead of guessing.

Rules where possible, models where needed

Keep predictable steps in plain code, and use models for language, inside clear constraints.

Keep secrets and personal data contained

Keep keys out of logs, verify incoming webhooks, and send a model only the personal data a task needs.

Stay model-agnostic

Write prompts and interfaces that don't depend on one provider, so models can be swapped without a rewrite.

Make it observable

Log what matters, including failures, so problems show up in the data before they show up in complaints.

AI · Personal project · 2024

Telecare RAG Chatbot

Monitoring staff need fast, correct answers about alarm types, triage and escalation. This assistant answers from a process document and says when the documents don't cover a question, instead of guessing.

01 IngestScrape the portfolio page with BeautifulSoup and Selenium, download the PDF, extract text with PyMuPDF.
02 ChunkRecursive character splitting, with an LLM-driven agentic chunker built for comparison.
03 RetrieveEnsemble of BM25 keyword search and MiniLM sentence embeddings.
04 AnswerA grounded prompt to a local Llama 3 through Ollama, or an OpenAI model, behind a Gradio UI.

Stack

Python · LangChain · rank-bm25 · sentence-transformers · DocArray · Ollama · OpenAI · Gradio

Next steps

Semantic chunking, a persistent vector store, and evaluating each chunking method on a question set.

Analytics

Power BI dashboards and analysis, most of them live and interactive.

Analytics · Real data · Interactive

NZ Electricity Generation and Consumption

MBIE's yearly data turned into a monitoring dashboard for generation by plant type, consumption by sector, and network losses. Pick a year to see it compared with the year before.

Live Power BI report Open full screen

What it shows

  • KPI cards for electricity generated, consumed and lost each year. Each card's call-out compares the selected year with the previous year: actuals, variance, variance % and, for generation, efficiency (the share of generated electricity that was consumed).
  • Conditional colour: call-outs turn green when the year is up on the previous year, and red when it is down.
  • Losses are split into distribution and transmission, with the previous year's total and its variance.
  • Trend chart comparing actual and estimated consumption (lines) against generation (bars), from 2014 to 2022, since actual consumption isn't in the data before 2014.
  • Bookmarks and buttons switch between trends by generation plant type (biogas, hydro, geothermal, wind and more) and by consumption sector (residential, commercial, industrial and more).

Power BI · DAX · bookmarks · conditional formatting

How it's built

DAX: call-out value (PY | Var | Var% | Efficiency%)
PY Actuals | Var | Var% | Efficiency% (CY vs PY) =
VAR PY = [Electricity Generated PY]
VAR var_varie = [Electricity Varience]
VAR var_perc = [Electricity Variance %]
VAR var_effic = [Energy Efficiency]
VAR symbol_pos = "↑"
VAR symbol_neg = "↓"
VAR Result =
    SWITCH(TRUE(), var_varie >= 0, symbol_pos, symbol_neg)
    & " " &
    IF(
        ISBLANK(var_varie),
        FORMAT(var_varie, "+#,0;-#,0;0"),
        FORMAT(PY, "#,0") & " | " &
        FORMAT(var_varie, "+#,0;-#,0;0") & " | " &
        FORMAT(var_perc, "+0.0%;-0.0%;0%") & " | " &
        FORMAT(var_effic, "#,0%")
    )
RETURN
    Result
DAX: call-out colour (green up, red down)
001. Electricity Generated KPI color (fx) =
VAR var_nominal = [Electricity Varience]
VAR result =
    SWITCH(TRUE(),
        var_nominal >= 0, "#358686",
        "#B24A4A"
    )
RETURN
    result

Analytics · Interactive

Ski Resorts Planner

An app-style, two-page report to plan a ski trip across 499 resorts, with monthly snow cover for each one. Start with Check Snow % or Resorts.

Live Power BI report Open full screen

How it works

  • Matched each resort to the nearest snow-cover reading using INDEX, MATCH and ABS on latitude and longitude.
  • A MySQL view surfaces resorts that suit every skill level.

Power BI · Excel · MySQL

Analytics · Interactive

Employee Attrition Analysis

IBM's HR sample dataset, analysed to find what drives people to leave. Logic prototyped in SQL Server, then rebuilt as DAX measures.

Live Power BI report Open full screen

Key findings

  • Separated overall attrition (16.12%) from the period attrition rate (17.5%).
  • 221 of 237 leavers had 20 or fewer years' experience and earned under $10,000 a month.

Power BI · DAX · SQL Server · Python

Walkthrough

One minute, filtering by department.

Plato's Pizzeria sales dashboard: revenue forecast, table occupancy heatmap, pizza popularity and key takeaways

Analytics · Challenge entry

Plato's Pizzeria Sales Analysis

A year of orders analysed for the owners: a sales forecast for the next quarter, busiest hours, and which table sizes and pizzas earn the most.

  • Tables of 1 to 4 people brought in about 86% of revenue ($702,852).
  • Two-person tables earned the most, though solo tables were most frequent.
  • Recommended dropping XL and XXL Greek pizza and stocking the four best sellers.

Power BI · DAX · Excel forecasting · Python (Seaborn)

Want the details behind a project?

Email me