Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
cc1f31a
first commit
GiuliaZobrist Jun 19, 2026
1029e12
add initial pipeline diagrams and project summary
sing-git Jun 19, 2026
4b11864
the merged architecture (after combining with feature/giulia)
sing-git Jun 19, 2026
09dbca4
minor adds, changing some parts of architecture scoring, reducting fa…
GiuliaZobrist Jun 19, 2026
89cb061
docs: add Risk Factor as a fifth scoring pillar
sing-git Jun 19, 2026
dcbcb28
feat: add RetailerContext schema and editable scoring config
sing-git Jun 19, 2026
ba725d4
feat: add signal pipeline (collect, clean, dedup, score, leadlag, syn…
sing-git Jun 19, 2026
1912572
feat: add AI chat assistant grounded in computed opportunities
sing-git Jun 19, 2026
ed80833
feat: add Streamlit chat+dashboard frontend and deploy docs
sing-git Jun 19, 2026
8370d3f
vibe-coded version
GiuliaZobrist Jun 19, 2026
69c2eb1
feat: add real scrape-target registry and a working Reddit fetcher
sing-git Jun 19, 2026
c58bd0f
feat: expand source registry with Swiss retail landscape
sing-git Jun 19, 2026
158b6b1
docs: fill out SUBMISSION.md
sing-git Jun 19, 2026
25061ac
feat: add named scoring sub-dimensions for explainability
sing-git Jun 19, 2026
5105054
feat: add instant reusability-demo scenario switcher
sing-git Jun 19, 2026
eecdec1
lovable version
GiuliaZobrist Jun 19, 2026
9dc0fbe
feat: add Lovable production frontend + backend integration mapping
sing-git Jun 19, 2026
c3fff1a
feat: add market alias normalization (DACH support) + .env template
sing-git Jun 19, 2026
fdfcab9
fix: update default Anthropic model to claude-sonnet-4-6
sing-git Jun 19, 2026
7055bc0
style: add clean Streamlit theme; doc newspaper4k as next news-source…
sing-git Jun 19, 2026
ee69d01
Add overview
katkat-123 Jun 19, 2026
c20b6d1
docs: flag for reviewers that the submission is on feature/giulia, no…
sing-git Jun 19, 2026
e0f7c43
Merge remote-tracking branch 'origin/feature/giulia' into feature/giulia
sing-git Jun 19, 2026
40eb480
final version
GiuliaZobrist Jun 19, 2026
73eb88f
Merge pull request #1 from angelaschereraiza/feature/vibe-code-from-o…
GiuliaZobrist Jun 19, 2026
9155eac
lovable code
GiuliaZobrist Jun 19, 2026
685524e
Merge branch 'feature/giulia' of https://github.com/angelaschereraiza…
GiuliaZobrist Jun 19, 2026
0c46468
_
sing-git Jun 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
ANTHROPIC_API_KEY=sk-ant-...

# Optional — Reddit API credentials for live Reddit scraping
REDDIT_CLIENT_ID=
REDDIT_CLIENT_SECRET=
360 changes: 360 additions & 0 deletions ARCHITECTURE.md

Large diffs are not rendered by default.

60 changes: 60 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Context

## Glossary

**RetailerContext**
The structured configuration object produced by the Q&A entrypoint. Captures: target market, comparison markets, niche/category, demographic filters (gender, age range), competitor URLs, risk factors, and score weights. All downstream modules receive a `RetailerContext` — nothing is hardcoded to Switzerland or outdoor retail.

**Comparison Market**
A market the user identifies as a credible signal source for the target market. Example: Sweden and Canada for a Swiss outdoor retailer. Signals are scraped *from* comparison markets. Transferability scoring reasons over the specific pair: comparison market → target market.

**Target Market**
The market the retailer operates in. Switzerland for the demo scenario. All transferability judgments are made relative to the target market.

**Signal**
A single piece of evidence from one source about one emerging opportunity. Has a source type, a market, a keyword or phrase, a score (0–10), and a URL. A signal is only emitted if it passes its source-specific detection threshold. Defined by the fields in `docs/data-contract.md`.

**Detection Rule**
The threshold applied per source type to decide whether a raw data point becomes a Signal. Each rule is explicit: a minimum score or count that must be exceeded. Three rules are active: Search (Google Trends slope × 10 ≥ 2.0), Marketplace (Amazon title matches ≥ 1), Curated (always emitted). Rules are constants at the top of `scraper.py`.

**Source Type**
The category of a signal's origin: `search`, `marketplace`, `manual`. Used as the unit of Signal Breadth counting. `search` = Google Trends. `marketplace` = Amazon bestsellers. `manual` = expert-curated.

**Opportunity**
A candidate product, material, brand, or feature that the scoring pipeline has surfaced and ranked. An opportunity is backed by one or more signals across one or more source types.

**Signal Breadth**
Count of distinct source types that independently surface the same opportunity. Higher = more independent confirmation. Preserved during Deduplication so all contributing source types are counted.

**Trend Score**
Pillar score with a single deterministic sub-dimension: Growth. Normalized 0–1.

**Growth**
Rate of increase of an opportunity's keyword over the last 90 days, measured via Google Trends for the comparison market(s). Normalized 0–10. Slope computed with `numpy.polyfit`. The only sub-dimension of Trend Score. For non-search signals, growth is estimated as 70% of the best_signal_score.

**Trend Stage**
Classification derived from Growth score: `growing` (≥7.5), `emerging` (≥5.0), `mainstream` (≥2.5), `declining` (<2.5). Drives the Buy Recommendation shown in the output.

**Transferability Score**
Pillar score derived from two LLM-assessed sub-dimensions: Outdoor Relevance and DACH Availability Gap. The LLM receives the opportunity, the specific comparison market → target market pair, and the RetailerContext. Each sub-dimension is scored 1–5 with a one-sentence justification. Pillar total = average of sub-dimensions, normalized to 0–1.

**Opportunity Score**
Pillar score derived from three LLM-assessed sub-dimensions: Availability Gap, Retail Saturation, and Brand Availability. Grounded in RetailerContext competitor URLs and niche. Each 1–5, normalized to 0–1.

**Red-Flag Scoring**
Fourth scoring pillar. LLM-assessed sub-dimensions: Supply Chain Risk, Regulatory Risk, Brand Concentration. Each scored 1–5 where 5 = highest risk. The pillar total is **inverted** in the Composite Score (`1 - total`) — high risk lowers the composite.

**Composite Score**
Weighted average of four normalized pillar totals: `(w_trend × Trend + w_transfer × Transferability + w_opp × Opportunity + w_rf × (1 − Red-Flag)) / (w_trend + w_transfer + w_opp + w_rf)`. Default weights are equal (1.0 each). Configurable via UI sliders.

**Urgency**
Binary classification of each opportunity: `act_now` or `watch`. Set by the LLM as part of the single scoring call. `act_now` = window is open now (growing trend + low saturation + accessible supply). Used to split the output into two sections.

**Relevance Filter**
Pre-scoring step that removes signals not matching the RetailerContext niche keywords and not originating from a known comparison or target market.

**Deduplication**
Pre-scoring step that collapses signals referring to the same opportunity across multiple source types into a single opportunity record, preserving all contributing source types for Breadth counting.

**Explainability**
Four one-sentence judgments per opportunity, generated by the LLM in the same call as the scoring: `why_trending`, `why_fits_switzerland`, `why_opportunity_now`, `why_to_be_cautious`. Shown directly in the UI — no post-processing layer.
78 changes: 78 additions & 0 deletions PIPELINE_DIAGRAM.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Retail Radar — System Flow

## 전체 파이프라인 (7단계 + 페르소나 + lead-lag)

```mermaid
flowchart TB
subgraph SOURCES["1. 신호 수집 (Collect)"]
direction LR
WS["🌐 Web Search<br/>(Claude)"]
GT["📈 Google Trends<br/>(pytrends, geo별)"]
RD["💬 Reddit RSS<br/>(약신호)"]
MP["🛒 Marketplace<br/>(Galaxus.ch)"]
CP["🏪 Competitor<br/>(Bächli/Transa)"]
end

SOURCES --> NORM["2. 정규화 (Normalise)<br/>→ data-contract Signal 행"]
NORM --> CLEAN["2.5 정제 (Clean)<br/>검증·spam제거·브랜드/시장 표준화<br/>(움라우트 보존)"]
CLEAN --> DEDUP["3. 중복제거 (Deduplicate)<br/>hash(keyword, brand, market)"]
DEDUP --> SCORE["4. 스코어링 (Score)<br/>4요소 투명 공식 0–1"]

SCORE --> CLUSTER["클러스터링<br/>신호 → 기회 그룹"]

CLUSTER --> LEADLAG["📍 Lead-Lag 분석<br/>geo별 시계열 시차<br/>= 트렌드 최초 출현 시장"]
CLUSTER --> TRANSFER["5. 이전성 (Transferability)<br/>LLM + 스위스 매대 공백 체크<br/>coverage_status"]

LEADLAG --> SYNTH
TRANSFER --> SYNTH["6. 추천 생성 (Synthesise)<br/>Recommendation 행 + 랭킹"]

PERSONA["👤 페르소나<br/>(config.py)<br/>대형 / 부티크 / 개인"] -.조정.-> SCORE
PERSONA -.조정.-> SYNTH

SYNTH --> OUT["7. 출력 (Present)"]
OUT --> CSV["📄 signals.csv"]
OUT --> JSON["📄 recommendations.json"]
OUT --> DASH["📊 Streamlit 대시보드"]

style SOURCES fill:#e8f0e8
style PERSONA fill:#fff3e0
style LEADLAG fill:#e3f2fd
style TRANSFER fill:#e3f2fd
style OUT fill:#f3e5f5
```

## 핵심 차별점이 어디서 나오는가

```mermaid
flowchart LR
A["글로벌 트렌드<br/>(US/Nordics에서 먼저)"] --> C{"교차 검증"}
B["스위스 매대 현황<br/>(Bächli/Transa)"] --> C
C -->|"상승 + 매대 없음"| D["✅ 매대 공백<br/>= 최고 기회<br/>coverage: absent"]
C -->|"상승 + 이미 입고"| E["⚠️ 이미 늦음<br/>coverage: covered"]
C -->|"화제만 + 제품 없음"| F["👁 모니터링만<br/>방향성 신호<br/>confidence: low"]

style D fill:#c8e6c9
style E fill:#ffe0b2
style F fill:#ffcdd2
```

## 신뢰도 그라데이션 → 재고 전략

```mermaid
flowchart TB
S["신호 신뢰도 + 트렌드 단계"] --> H["High + Emerging<br/>상업적 증거 + 공백"]
S --> M["Medium<br/>초기 신호"]
S --> L["Low<br/>방향성만"]

H --> H2["🟢 테스트 캡슐<br/>2–3 SKU 소량 + 빠른 재주문"]
M --> M2["🟡 소규모 큐레이션<br/>저MOQ부터 (예: Ciele 모자)"]
L --> L2["🔴 재고 0<br/>모니터링, 2027 Q1 재평가"]

H2 --> GATE["sell-through 게이트<br/>4주 30% 미만 → 재주문 중단<br/>kill criteria 명시"]
M2 --> GATE

style H fill:#c8e6c9
style M fill:#fff9c4
style L fill:#ffcdd2
style GATE fill:#e1f5fe
```
209 changes: 209 additions & 0 deletions PROJECT_SUMMARY_EN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
# Retail Radar — Project Summary (English)
> HerCode × Zenline Hackathon · Consolidated decisions & rationale

---

## Unique Selling Point (USP)

> **"We don't just detect global trends — we score them against the Swiss shelf. A trend rising in Google Trends CH but absent from Bächli/Transa is a quantified assortment gap, not a guess."**

One-sentence pitch version:
> *Retail Radar quantifies the gap between what's trending globally and what's actually on Swiss shelves — turning noisy signals into ranked, evidence-linked buy/test/monitor decisions, with calibrated confidence that tells the buyer when NOT to act.*

### Why it's unique (4 pillars)
1. **Assortment-gap scoring** — cross-reference global momentum against actual Swiss retailer assortment → `coverage_status: absent/partially_covered/covered`. Turns a trend into a buy decision.
2. **Calibrated honesty as a feature** — explicitly separate directional signals from commercial proof; flag low-confidence items as *monitor, don't buy*.
3. **Transparent scoring** — 4-component formula where every point is traceable, not a black box.
4. **Demonstrated reusability** — config swap → live re-run for another category/persona. Shown, not claimed.

---

## Directional Signal vs Commercial Proof (the core demo contrast)

**Commercial proof → "BUY / TEST"**
- *Gravel running.* Named SKUs already shipping (La Sportiva Prodigio 2, Keen Roam, Mount to Coast H1), priced, reviewed, A-TPU foam confirmed spec. Rising in Google Trends CH AND absent from Bächli/Transa.
- → `confidence: high`, `coverage_status: absent`, action = **test capsule, contact La Sportiva IT**.

**Directional signal → "MONITOR, don't buy"**
- *Mycelium / bio-synthetic membranes.* Cited as a 2026 fabric trend, real market CAGR — but **zero retail SKUs exist**. Press releases and R&D only.
- → `confidence: low`, `coverage_status: not_relevant`, action = **monitor, re-evaluate Q1 2027, no buy**.

**Stage line for the jury:**
> "Most opportunity reports treat every trend as a buy. Ours doesn't. Gravel running is commercial proof — named SKUs, prices, a Swiss shelf gap — so we say buy. Mycelium is a directional signal — real buzz, but nothing you can order — so we say monitor, not buy. Telling a buyer when NOT to act is what makes the rest of the list trustworthy."

### The confidence gradient (not binary)
| Confidence | Trend stage | Action |
|---|---|---|
| High + Emerging | big gap | Full buy / test capsule |
| High + Growth (partially covered) | Bio-Dyneema | Buy flagship SKUs + material story; flag supply risk |
| Medium-high | Run-culture brands | Staged buy — start with Ciele headwear (low MOQ) |
| Medium | Ultralight crossover | Small curated test, anchor to UTMB community |
| Low | Mycelium | No stock, monitor only |

---

## Market Roles: Target vs Early-Signal (corrected)

**DACH ≠ Nordics.** Two separate roles:

```python
TARGET_MARKETS = {"CH", "DE", "AT"} # DACH — where we SELL
EARLY_MARKETS = {"US", "Nordics", "Japan", "Korea", "UK"} # where trends appear FIRST
```

- Signal in `EARLY_MARKETS` → **early-signal bonus** in scoring (we're ahead).
- Signal in `TARGET_MARKETS` → input to **transferability** assessment.
- Never merge the two. Nordics is an early-warning radar, not a target market.

**Stage line:**
> "DACH is the target. Nordics, US, and Japan are our early-warning radar. We never mix them."

---

## Proving "trends appear first in X market" (lead-lag)

Don't claim it — show it with data.

**(a) Google Trends lead-lag comparison (strongest)**
Pull the same keyword across multiple geos as a 12-month time series; show which market rose first.
- Example slide: "gravel running search — US rose from 2025 Q2, CH only from 2026 Q1 → **9-month lag**."

**(b) Publication & launch timestamps**
- US media coverage date vs DACH media date; brand launch date vs DACH availability date.
- Uses existing `observed_at` + `url` fields per market.

**Honest caveat (state it):** Google Trends lead-lag is correlation, not causation. Say "in this signal US led by 9 months, and we monitor whether the pattern repeats" — not "US is always first."

```python
def detect_lead_market(keyword, geos=["US","Nordics","DE","CH"]):
onset = {}
for geo in geos:
df = get_trends(keyword, geo)
onset[geo] = first_month_above_threshold(df, threshold=50)
return sorted(onset.items(), key=lambda x: x[1]) # earliest = lead market
```

---

## Persona Approach (buyer decision profiles)

Split by **decision scale & risk tolerance**, not just seller size:

| Persona | Who | Risk tolerance | Same opportunity → different action |
|---|---|---|---|
| **Large retailer** (Bächli, Transa) | buying team, big budget | low — proven only | High-confidence only. Buy when commercial proof |
| **Boutique / small-batch** (specialist shop) | curation, differentiation | medium | Bets on medium too — small test for differentiation (low-MOQ entry like Ciele caps) |
| **Individual / DTC seller** (online edit shop) | fast, agile | high | Front-runs at emerging stage. Small buy, fast turn |

```python
PERSONA = {
"large_retailer": {"min_confidence": "high", "buy_action": "commercial_proof_only"},
"boutique": {"min_confidence": "medium", "buy_action": "small_curated_test"},
"individual_dtc": {"min_confidence": "medium", "buy_action": "fast_small_bet"},
}
```

Persona is a `config.py` parameter that **auto-adjusts scoring thresholds and recommended actions** — proves reusability beyond category swaps.

**Build note:** finish `large_retailer` as default; show 1 more persona as a demo of extensibility (don't implement all 3 under time pressure).

---

## Trend Lifecycle & Inventory Strategy

Recommendations shouldn't end at "buy" — they need a monitoring loop.

### Add these fields to Recommendation
```python
trend_stage: str # "emerging" | "growth" | "mainstream" | "peak" | "declining"
expected_window: str # e.g. "12-24 month first-mover advantage window"
monitor_triggers: list[str] # if seen → stop / reduce stock
reversal_signals: list[str] # trend is breaking
```

### Kill criteria example (gravel running)
```
monitor_triggers:
- "Google Trends CH momentum declines 3 months straight"
- "Bächli/Transa stock the same category → no longer a gap (first-mover edge gone)"
- "Key supplier raises price 30%+ or tariff shock"
reversal_signals:
- "Review media reports the category label isn't sticking"
- "Initial sell-through < 20% after 4 weeks"
```

### Inventory strategy by confidence × stage
| Confidence | Stage | Inventory strategy |
|---|---|---|
| High + Emerging | big gap | **Test capsule**: 2–3 SKUs, small qty, fast re-order option |
| High + Growth | partially covered | **Staged expansion**: re-order every 4–6 weeks based on sell-through |
| Medium | early | **Small curated**: start with low-MOQ SKUs (e.g. Ciele caps) |
| Low | directional | **Zero stock, monitor only** |

**Two principles (reduce risk, maximise utility):**
1. **Start small, re-order fast** — small test + short-lead-time supplier beats a big first order. Small loss if wrong, fast scale if right.
2. **Sell-through gate** — bake an exit condition into the recommendation: "stop re-ordering if 4-week sell-through < 30%."

**Stage line:**
> "We don't just say buy. We say how much, for how long, and what to watch for to stop. Trends turn."

**Honest caveat:** trend stage and sell-through gates are estimates, not validated sales data. Say "signal-based estimate, re-validated by actual sell-through after stocking."

---

## Where to collect Swiss/DACH data

**Layer A — Global early-signal sources (trends appear first):**
- Claude web search, Reddit RSS (no auth), gear media (GearJunkie, RoadTrailRun, Treeline), ISPO/OutDoor trade shows.

**Layer B — Swiss/DACH sources (transferability + assortment gap):**
- **Google Trends (geo=CH/DE/AT)** via pytrends — search momentum
- **Galaxus.ch / Digitec** — CH's biggest marketplace, CHF prices + ranks
- **Bächli Bergsport, Transa** — premium assortment gap check
- **Ochsner Sport / SportXX** — mass-market coverage
- **Amazon.de bestsellers** — DACH demand proxy

**Killer move:** rising in Google Trends CH + absent from Bächli/Transa = quantified assortment gap = highest opportunity.

---

## Scoring (transparent 4-component formula, 0–1)

1. **Source credibility (0–0.25)** — Google Trends / marketplace highest (hard data); Reddit lowest (weak signal)
2. **Evidence strength (0–0.25)** — URL + brand + product_name + price/rank
3. **Momentum / novelty (0–0.30)** — emerging keywords + Google Trends momentum value
4. **Early-market bonus (0–0.20)** — foreign origin = ahead of CH

**Opportunity rank** rewards multi-source diversity + transferability + `absent` coverage (the real gap).

Every score is traceable: "0.78 = credible source 0.22 + URL/brand 0.17 + emerging keyword 0.20 + US origin 0.20."

---

## Signal Cleaning (between Normalise and Deduplicate)

1. **Validation** — drop no-name, no-URL, fragment rows
2. **Text normalisation** — lowercase, collapse whitespace; **keep umlauts/accents** (äöüéè) for DACH terms
3. **Entity canonicalisation** — hand-curated `BRAND_ALIASES` + `MARKET_ALIASES` (not fuzzy matching)
4. **Numeric cleanup** — parse "$325" / "CHF 189.-" / "#3" to clean values
5. **Spam filter** — drop "buy now", "affiliate", "[deleted]", etc.
6. **Date sanity** — drop/flag stale or malformed dates

**Log what you drop:** "ingested 140 → cleaned 95 → deduped 60." Concrete evidence-quality story for the jury.

**Caveat:** don't over-clean. Keep aliases a curated allowlist — fuzzy matching at 16:00 with no test set silently corrupts output.

---

## Reusability (config swap)

Change `config.py` inputs only; collectors/scorer/clusterer code unchanged:
```python
SCENARIO = {
"category": "cycling", # outdoor → cycling
"web_queries": ["gravel bike emerging brands 2026", ...],
"trend_keywords": ["gravel bike", "rennrad", ...], # incl. German
"competitor_urls": ["https://www.bike-components.de/", ...],
}
```
Caveat: far-off categories need new `CLUSTER_TAGS` / `BRAND_ALIASES`. Demo with an *adjacent* category (outdoor→cycling) for safety.
Loading