Most of the data is noise. The work is finding what isn't.

I build end-to-end data products — clustering, prediction, retrieval — that take high-volume, messy input and end in something a person can decide on.

Reichman University · Economics & Entrepreneurship Specialization · Data Science
01

About

Hi, I'm Allen. Thanks for stopping by.

I'm a dual-major student at Reichman University — Economics and Entrepreneurship with a Data Science specialization, Class of 2028.

One habit I try to hold onto: an answer nobody understands isn't an answer. So I build end-to-end data products rather than notebooks that stop at a chart — collecting and cleaning the data honestly, engineering the features that carry the signal, choosing a model for a reason I can defend, and deploying it so someone can actually use it.

Technical & Tools
Python, R, SQL (Joins, Aggregations, CTEs, Window Functions), Pandas, NumPy, Scikit-learn, Matplotlib, Seaborn, Advanced Excel
Infrastructure & AI
GCP, GitHub Actions, Git, Streamlit, Prompt Engineering (LLMs)
Core Competencies
Data Analysis (EDA), Predictive Modeling, Feature Engineering, Statistical Inference
Languages
Hebrew (native), English (fluent), Russian (fluent), Spanish (basic)
02

Selected work

0sint

An OSINT triage system for an operational intelligence desk officer. It ingests a high-volume Telegram-style feed, clusters raw messages into distinct real-world events using a domain fine-tuned sentence embedder, scores each event on veracity, relevance and urgency, retrieves comparable past incidents, and generates a grounded situation card with a recommended action set.

Fine-tuning as a response, not a default

Off-the-shelf embeddings plateaued at ARI 0.574 — two incidents in the same city at the same hour merged into one cluster at 0.788 centroid similarity. Contrastive fine-tuning moved the same window to 0.883.

Hard negatives mined from an audit

~600k candidate pairs → 10,204 above 0.70 similarity → ~25 sharing time and place → the 5 most confusable held out of training entirely as the eval set.

Guardrails moved into code

The model kept inventing agencies that don't exist. Prompt instructions reduced it; a recipient whitelist generated in code eliminated it. Battery: 6 cards, 0 violations.

sentence-transformersMiniLM-L6-v2Contrastive fine-tuningMultipleNegativesRankingLossAgglomerative clusteringSemantic searchQwen2.5-7BPrompt engineeringGradioPyTorch
The problem

A desk officer monitoring open Telegram channels faces a specific failure mode: a thin trickle of genuinely relevant information buried inside overwhelming noise. Keyword filters fail in both directions — missing events described in unexpected wording, flooding on incidental matches.

Three harder problems sit underneath. The same event is reported by many accounts in wildly different registers, from official bulletins to panicked eyewitness posts. Two different events in the same city at the same hour look nearly identical to a general-purpose embedder. And an LLM asked to summarize raw intelligence will confidently invent agencies, units and protocols that do not exist — which in this domain is worse than no summary at all.

Architecture
Telegram-style feed (11,774 messages / 1,102 events)
        │
        ▼
0sint-event-embedder ....... MiniLM-L6-v2 fine-tuned
(384-dim, 22.7M params)      MultipleNegativesRankingLoss
        │                    15,293 mined triplets
        ▼
Agglomerative clustering ... average linkage @ 0.50
        │
        ▼
Event scoring .............. veracity / relevance / urgency
        │
   ┌────┴────┐
   ▼         ▼
Similar    Constrained generation → grounded card
past       what happened · does it matter ·
events     action set · recipients
        │
        ▼
Gradio Space ... 4/8/24h window · V-X triage marks
                 world map · semantic search
                 Telegram PDF export
The hardest part

Two events, one city, one hour. An airport hostage incident and a bus-station explosion, reported in the same window by overlapping accounts, sat at 0.788 centroid similarity — well above any threshold that would still merge genuine duplicates. No threshold tuning could resolve it, because the geometry itself was wrong: the base model encoded "something serious happening here" and discarded the distinction that mattered.

Fixing it required changing the embedding space, and the eval had to be built from held-out confusable pairs rather than a random split — a random split would have scored well while leaving the actual failure untouched.

The honest caveat: 0.883 is same-window clustering performance for the production model on an 8-hour, 466-message window at average linkage 0.50, measured against the 0.574 baseline on that same window. The held-out confusable-pair evaluation is a separate exercise reporting centroid similarity and separability, not ARI. Keeping those two numbers distinct matters more than having one headline figure.

Results
  • Fine-tuned embedder published — 22.7M parameters, 384-dimensional, trained on 15,293 triplets in 7.5 minutes on a single GPU.
  • Corpus of 11,774 messages across 1,102 events, category structure verified from the parquet rather than assumed: security events average 8.78 messages against 3.58 for irrelevant ones — a ~2.45× coverage ratio that empirically justifies the breadth term in the veracity score.
  • 6 cards, 0 rule violations under an automated six-rule battery — after a bug was found where an early return meant only the first rule had ever run.
  • Deployed with windowed filtering, V/X triage marking, an interactive world map, semantic search, and Telegram PDF export of marked events.
What I took from it

Measure the baseline's failure precisely before reaching for a bigger model, and let that failure define the eval set. Move constraints from prompts into code wherever the constraint can be expressed deterministically. And design for the user's flow rather than exposing the architecture — the technical layer is something to answer questions about, not something to put on screen.

Cortex

An autonomous financial-intelligence pipeline. It ingests market prices, macro indicators and multi-source news, runs LLM-driven sentiment scoring and narrative synthesis, and delivers bilingual market briefings to Telegram twice daily — with every signal persisted to Cloud SQL as a training corpus for downstream ML.

Stateless compute, stateful logic

Runs entirely on GitHub Actions cron — no server, no container, no persistent disk. All state lives in Cloud SQL, and every write is idempotent, so the pipeline is fully reproducible and re-runnable.

Batched inference over per-item calls

Sentiment scoring collapses N headlines per ticker into one structured-JSON call. A parse failure persists articles unscored rather than dropping ingestion.

Deterministic guardrails on non-deterministic output

Every prompt rule written as a positive constraint, backed by a regex post-filter that drops whole hallucinated sentences. Prompts branch on RSI band so the model can't emit "RSI 25 / neutral."

Python 3.12Gemini 2.5 FlashGoogle Cloud SQLMySQL 8.4GitHub ActionsTelegram Bot APIStreamlitPlotlyyfinanceFRED APIETL pipelinesCI/CD
The problem

Retail market research is fragmented and time-expensive. Price action lives in one tool, macro prints in another, news in a dozen feeds of varying quality — leaving the analyst to reconcile them manually every day. Generic LLM summarizers fail at this because they hallucinate price levels, contradict technical indicators, and re-report the same story every run.

Architecture
GitHub Actions (cron 08:00 / 16:00 UTC)
        │
        ▼
   scheduler.py ──┬── yfinance ...... prices, OHLCV, RSI-14
                  ├── GNews+Finnhub .. dual-source news merge
                  ├── FRED ........... CPI / PPI
                  └── CNN Fear & Greed
        │
        ▼
   Gemini 2.5 Flash → batched sentiment JSON
                      per-ticker narrative
                      macro synthesis
        │
        ▼
   regex post-filter
        │
   ┌────┴────┐
   ▼         ▼
Cloud SQL   Telegram Bot API
4 tables    (markdown + article buttons)
The hardest part

Maintaining alert state on a filesystem that gets wiped every run. GitHub Actions provisions a clean runner per invocation, so a local dedup cache was unavailable and the same catastrophic headline would re-fire on every tick. State moved into Cloud SQL with two key tiers — a permanent URL hash, plus a daily-resetting ticker-keyword pair that catches the same event syndicated across outlets.

The deliberate trade-off: the filter fails open on a database error. An outage lets duplicates through rather than suppressing real alerts, because in market alerting a false positive is cheap and a false negative is not.

Suppressing hallucination without suppressing output. Early versions invented support levels and restated the header price mid-paragraph — and negative instructions ("do not mention X") made the leaks more frequent. The fix was three layers: positive-only constraints, prompts branched on RSI band so indicator and narrative structurally cannot contradict, and a regex backstop that drops offending sentences entirely. When every sentence gets filtered, the function returns empty rather than padding with filler.

Results
  • Autonomous in production — two briefings per day on cron, across a five-month build.
  • Six live data sources unified into one report; four normalized tables accumulating per-ticker sentiment and price history designed as ML training input, not just report backing.
  • Dual delivery surface — the same Cloud SQL layer backs both the Telegram bot and a Streamlit dashboard with TTL-tiered caching.
What I took from it

Treat the LLM as a formatter over verified numeric context rather than a source of facts. Design guardrails as deterministic post-processing rather than trusting prompt compliance. And let infrastructure constraints drive architecture toward reproducibility instead of fighting them.

Country Travel Recommender

A multimodal destination search engine. Describe the trip you want in words or upload a photo of it, and CLIP matches your query against 42,525 curated travel images to return destinations ranked by visual experience rather than geography — each result shown with the exact evidence image behind its score.

The thesis, then the evidence

CLIP groups images by experience type, not country — so a beach photo surfaces Thailand, Brazil and Croatia together. Confirmed three ways: 9 of 10 clusters semantically clean, t-SNE separation, and a world map where one cluster unites Sub-Saharan safari countries and another spans the Andes.

The model cleans its own input

~25-30% of source images were unusable — a laptop keyboard, a showroom Lamborghini. Instead of arbitrary rules, each image was scored against 131 positive and 30 negative travel prompts and the bottom quartile dropped.

K chosen for interpretability, not silhouette

K=8 scored marginally higher but left 17% of data in an incoherent catch-all. K=10 concentrated the noise into a single 9.6% cluster and left 9 readable themes.

CLIP ViT-B/32HuggingFace TransformersMultimodal embeddingsZero-shot classificationPCAKMeanst-SNECosine retrievalGradioNumPy
Open live demo
The problem

Travel search is keyword-shaped and destination-shaped: you search a country and read about it. But people don't plan trips that way. They start from an experience — a saved photo, a phrase like "ancient temples in the jungle" — and the country is the answer, not the query.

Two obstacles. Matching an experience to a place needs a shared representation of images and text, which rules out a vision-only model. And a photograph carries no information about cost, season, or whether a place suits a family — so pure visual matching produces aesthetically satisfying, practically useless recommendations.

How it works

Visual layer. The query is encoded with CLIP and compared against all 42,525 embeddings by cosine similarity, with each country ranked by its single best-matching image. A dynamic threshold at 70% of the top score drops weak matches. For image queries, the country is also predicted by top-K voting.

Metadata layer. On top of the visual ranking sits a hand-curated attribute table covering what a photo can't show — budget, trip type, climate, vibe, best months. Hard filters must match exactly; soft filters allow one step of deviation, with the card disclosing exactly how it deviates. A per-vibe seasonality mapping means "skiing in March" correctly returns France, Switzerland and Bulgaria despite their general best months being warmer.

Ranking by a country's single best image is deliberate. The question is "does this country offer what I'm describing," not "is this country on average like my query" — averaging would penalize diverse countries.

The hardest part

Evaluating a recommender with no ground truth. There is no correct destination for "tropical beach with palm trees," so there is no accuracy metric. Rather than inventing one, evaluation was built from three behavioural checks: the relevance score distribution splitting cleanly into an off-topic low band and a genuine-travel high band; cluster coherence inspected through PCA and t-SNE; and query behaviour verified against expectation. Qualitative evaluation stated as qualitative.

A silent bug cost real time too: a CLIP convenience method intermittently returned a wrapper object instead of a tensor, surfacing far downstream as confusing failures. The fix was to stop using the convenience method and call the model's forward pass directly — explicit and consistently typed.

Known limitations

The metadata for ~204 countries was assigned from a single author's judgement, so it carries subjective bias and goes stale. The 25th-percentile content filter, the 70% quality threshold and the single-best-image rule were all chosen by hand — a different cutoff would change results. These are documented as guesses that should be calibrated on a labelled sample, rather than presented as measured choices.

FPL Player Predictor

An end-to-end ML pipeline over eight Premier League seasons that predicts a player's Fantasy Premier League points for the upcoming gameweek using only pre-match information — framed both as regression and as classification — with a deployed app that explains which features pushed any given prediction up or down.

Features carried it; models didn't

Baseline linear regression: R² 0.112. Same model with engineered features: 0.303. Every subsequent model change added a combined 0.014. All three engineered models converged at ~0.32 — the signature of a feature ceiling, not a model ceiling.

The ceiling was tested, not asserted

GridSearchCV across 27 hyperparameter combinations, 135 fits. The tuned model came in at R² 0.3176 against the default's 0.3178 — imperceptibly worse, which is the strongest possible evidence no signal remained.

The metric came from the domain

73% of rows are the "Blank" class, so accuracy is meaningless. Haul recall won instead: missing a haul means benching a player who explodes; a false alarm just means a disappointing captain.

Scikit-learnHistGradientBoostingRandom ForestLogistic RegressionKMeansGridSearchCVFeature engineeringLeakage preventionImbalanced classificationStreamlitDocker
Open live demo Models & notebook
The problem

Fantasy football decisions are made on intuition dressed as analysis. Managers talk about form, fixtures and price without knowing which of those signals actually predicts next week's points, or how much of the outcome is predictable at all.

The data makes it harder than it looks. 58% of rows score zero points — but only about a fifth of those zeros are players who appeared and underperformed. The rest didn't play. The dataset is dominated by non-appearances, so any useful model has to resolve "will they play" before form and fixture features contribute anything. And the outcomes that decide a season — 10+ point hauls — are 1.9% of the data and depend on events no pre-match feature can encode.

Approach

184,646 player-gameweek records across eight seasons, cleaned and engineered into 178,808 rows and 65 columns. Three families of engineered features: rolling form (points, minutes and bonus-point averages over 3/5/10 gameweek windows, plus volatility and lags), fixture context (team and opponent strength), and player archetype (five KMeans clusters, one-hot encoded).

Every rolling feature is shifted by one gameweek before the window computes. Without that shift, each feature includes the gameweek being predicted, and the model reports excellent scores that would collapse in production. It's the single most consequential correctness decision in the project, and it was verified explicitly on a known player-season.

The hardest part

A player who was two players. Andrew Robertson surfaced in EDA with two separate records — 121 games as a midfielder and 181 as a defender — which initially read as two players sharing a name. The actual cause was structural: player IDs are reassigned each season and his position had been reclassified, so the pipeline treated one player as two entities. The bug threw no error and produced no warning; it would have silently corrupted every per-player aggregate. It was found only because a real player's numbers were checked against what a football fan knows to be true.

Accepting a ceiling instead of throwing models at it. When three models converge at R² 0.32, the instinct is to reach for XGBoost, stacking, ensembles. The project instead spent its remaining compute proving that would be wasted. Reporting a negative result as the finding, instead of quietly discarding it, is what makes the ceiling claim credible.

Results
  • Regression fully attributed: 0.112 → 0.303 (features) → 0.318 (models). Features contributed +0.19; every model change combined contributed +0.014.
  • Classification winner selected against the domain metric: Logistic Regression at 0.489 haul recall, chosen over Random Forest despite RF scoring marginally better on both accuracy and macro-F1.
  • Three EDA findings that contradict FPL folklore — hauls don't mean-revert, ceiling and consistency trade off near-linearly, and league position is a strong proxy for fixture difficulty with a few named outliers.
  • Deployed on Docker with runtime model loading, a plain-English match preview, and a feature-contribution panel showing what drove each prediction.
What I took from it

The metric has to come from the cost structure of the decision, not from convention. Feature engineering beats model selection often enough that model shopping should come last. A negative result from a rigorous test is a finding, not a failure. And the bugs that matter most are the ones that don't raise errors — which makes sanity-checking against known reality non-optional.

Delivery Operations EDA

An exploratory analysis of 38,964 real food-delivery records across Indian cities that tests a specific operational hypothesis — that weather and distance drive delays — and shows the data refuting it. The real bottlenecks are how many orders are stacked onto one trip, and who is delivering them.

The finding lives in the interaction

Weather and traffic separately understate both. Sunny weather buffers even a full jam (23.5 min, barely slower than cloudy with light traffic at 22.4). Fog plus jam is the real worst case at ~37 min.

Cleaning as an iterative step

The first pass looked complete. Then computing Haversine distances exposed 3,410 rows with GPS at (0,0) and 272 physically impossible distances — anomalies invisible in the raw columns.

Outliers kept on purpose

1,024 courier ratings below 3.9 flagged as statistical outliers were retained. They're real low-rated couriers and they're central to the question being asked — removing them by rule would have removed the answer.

PandasNumPyExploratory data analysisData cleaning & validationHaversine distanceGeospatialOutlier detectionCorrelation analysisSeaborn
Dataset & notebook
The problem

Delivery lateness has an intuitive causal story: bad weather slows couriers, long distances take longer. If that story is right, the operational levers are geographic — restaurant density and delivery radius. If it's wrong, the levers are dispatch policy and courier management instead, which are cheaper to change and faster to act on.

The complication is that the raw data doesn't contain the variables the question needs in usable form. Distance has to be derived from GPS pairs, the target needs binning to be comparable across conditions, and the corruption in the data is invisible until those derivations are attempted.

Findings
  • Stacked orders are the bottleneck. A courier handling three deliveries per trip averages 47.8 minutes against 23.1 for a single-stop delivery — more than double, from a dispatch decision rather than a geographic constraint.
  • The correlation ranking inverts the intuition: stacked deliveries +0.384, courier rating −0.360, distance +0.322.
  • Extreme weather isn't the worst weather. Sunny is fastest at a 21-minute median, but fog and cloud (29 min) are slower than storms and sandstorms (26 min, level with plain wind).
  • Traffic is non-linear. Low → Medium is a large jump (21.5 → 26.9 min); Medium → High is negligible (27.4); only a full jam adds meaningfully again (31.4).
The hardest part

The cleaning only the analysis could reveal. After the first pass the data looked clean by every check applied to the raw columns. The corruption became visible only once Haversine distances produced values no urban delivery could plausibly cover, tracing back to coordinates sitting at (0,0). That forced an unplanned second cleaning phase mid-analysis — and the lesson that data preparation isn't a linear stage you complete and leave behind.

Missing data as a per-question decision. The default move is to drop every row with any null. Applied here, that would have discarded ~1,000 records entirely intact for the weather and distance questions and deficient only for the courier-rating one. Retaining them preserved statistical power for two of three research questions, at the cost of slightly different effective sample sizes — an explicit trade-off, documented rather than silently made.

The hypothesis was wrong. The starting expectation was that distance and severe weather would dominate. Rather than reframing the questions around the findings after the fact, the write-up keeps the original hypotheses visible and reports them as refuted — which is what makes the operational conclusion trustworthy.

03

Contact

If any of this is useful to you, I'd like to hear about it.