🎡 HK Playground 🎢

Practical, code-tested notes on data analysis, machine learning, and the problems that come up in real projects.

Latest posts

Reranking for RAG: a cross-encoder lifted top-1 retrieval from 64% to 85%

Reranking for RAG: a cross-encoder lifted top-1 retrieval from 64% to 85%

Bi-encoder vs cross-encoder, measured on 1,000 SQuAD questions with two small local models. Re-scoring the top 20 search results moved the right paragraph to first place 85% of the time instead of 64%, for 67 ms per query on a laptop GPU. Past 20 candidates the gain flattened while the cost kept rising.

A RAG system first retrieves the passages most similar to the question and then pastes them into the prompt (RAG in 50 lines). If the right passage is ranked 7th and you only paste the top 3, the model never sees it. A reranker is a second, slower model that re-scores the top results before you cut the list.

Python 3.15 frozendict: when to use it, and where pandas still expects a dict

Python 3.15 frozendict: when to use it, and where pandas still expects a dict

frozendict is an immutable, hashable dict that can be a cache key or a safe default argument. It is not a dict subclass, so pandas 3.1 rejects it in fillna and groupby.agg, and pd.DataFrame(frozendict) silently builds the wrong table. Tested on Python 3.15.0.

Python 3.15 adds frozendict, a built-in dictionary that cannot change after it is created (PEP 814). Because it cannot change, it can be hashed. You can put it in a set, use it as a dict key, or pass it to a function wrapped in functools.cache. A normal dict can do none of these.

Speculative decoding, measured on a laptop: 10x fewer model passes, no speedup

Speculative decoding, measured on a laptop: 10x fewer model passes, no speedup

A 0.5B draft model cut Qwen2.5-1.5B’s forward passes from 128 to 13 and produced exactly the same text, but tokens per second did not move. Prompt lookup decoding was 1.30x faster when the answer copied the prompt and slower otherwise. Local transformers test on an Apple M5, with the reasons.

A language model writes one token per forward pass, and for big models each pass is slow. Speculative decoding lets a small, fast draft model guess several tokens ahead, then has the big target model check all of the guesses in a single pass. Every guess the target agrees with is a token it did not need a pass of its own for. With greedy decoding the output is exactly what the target would have written alone.

Target encoding leakage: what scikit-learn’s TargetEncoder cross fitting protects you from

Target encoding leakage: what scikit-learn’s TargetEncoder cross fitting protects you from

Mean-encoding categories before cross-validation reported an MAE of 4,054 USD; the same model scored 4,636 on a holdout. Inside a pipeline, plain mean encoding was fine until an ID-like column appeared, then TargetEncoder’s cross fitting kept the error flat. Tested with scikit-learn 1.9.1 on real salary data.

Target encoding replaces each category with the average target value of its rows. A job title becomes the average salary of people with that title. It turns a column with hundreds of categories into one useful number, and it is also an easy way to leak the answer into your features.

Your first LSTM forecast in PyTorch: from CSV to predictions

Your first LSTM forecast in PyTorch: from CSV to predictions

A step-by-step LSTM tutorial using real daily bike rentals: load and plot a CSV, build 3D input windows, train on CPU, and compare next-day predictions with yesterday’s value. Complete runnable Python code included.

By the end of this tutorial, you will have a small LSTM that takes 14 days of bike rentals and predicts the next day, plus plots and an error score you can check against a simple rule. No GPU, account or API key is needed. Basic Python, arrays and the idea of a training set are enough to follow along.

What is a chat template? The string your LLM actually reads

What is a chat template? The string your LLM actually reads

A chat model never sees your list of messages. apply_chat_template turns it into one string with control tokens, and Qwen2.5 adds a system prompt you did not write. Without the template the same model rambled and added wrong facts; without the generation prompt it wrote the user’s turn. Tested locally with Qwen2.5-0.5B-Instruct.

You send a chat model a list of messages with roles. The model cannot read a list. It reads one sequence of tokens, and the chat template is the rule that turns your messages into that sequence: where each turn starts and ends, which special tokens mark the roles, and what to put at the end so the model knows it is its turn to speak.

How to choose a classification threshold from error costs (scikit-learn TunedThresholdClassifierCV)

How to choose a classification threshold from error costs (scikit-learn TunedThresholdClassifierCV)

On the German Credit data, a logistic regression at the default 0.5 threshold cost 0.866 per applicant, more than rejecting everyone (0.700). The threshold from the cost matrix, 1/6, cut it to 0.545, and TunedThresholdClassifierCV found the same from training data alone. Tuning on the test set looked better than it was.

A classifier’s predict turns a probability into a yes or no at 0.5. That threshold is only right when both kinds of mistake cost the same. On a credit dataset where approving a bad applicant costs five times as much as rejecting a good one, the default threshold did worse than a rule that rejects every applicant:

What is perplexity? Computing it by hand with a small local model

What is perplexity? Computing it by hand with a small local model

Perplexity is exp of the average negative log-probability per token. We computed it by hand with Qwen2.5-0.5B, matched it to the library’s loss, and scored five texts: a normal sentence 35.1, the same words shuffled 1,142.8, Python code 4.0. Why random letters scored lower than the shuffled sentence, and what that says about comparing numbers.

Perplexity measures how surprised a language model is by a text. For every token, take the probability the model gave to the token that actually came next, then:

Recursive vs direct multi-step forecasting: what a one-step score hides

Recursive vs direct multi-step forecasting: what a one-step score hides

A model with lag features scored MAE 24.5 one hour ahead. Forecasting the next 24 hours, the same model averaged 45.7. Recursive, direct and “no short lag” strategies compared on real hourly data with scikit-learn, with code.

In the KFold vs TimeSeriesSplit post, a gradient-boosting model with lag features scored an MAE of 24.6 bikes per hour on the 8 weeks after its training data. That number came with a caveat: every prediction used the real demand of the previous hour. It measured predicting the next hour, not forecasting a day.

Orca review: Claude Code and Codex side by side, and one agent handing work to the other

Orca review: Claude Code and Codex side by side, and one agent handing work to the other

A hands-on look at Orca, an open-source desktop app for running CLI coding agents side by side. What it does, how its orchestration commands let Claude Code hand a code review to Codex, what that review found, and the one setting to check before you rely on it.

Orca is an open-source (MIT) desktop app for running several CLI coding agents at once. Its README lists Codex, Claude Code, OpenCode and Pi, on macOS, Windows and Linux, and each agent runs with your own subscription. Orca does not ask for an API key.

Pagination