open-navigator / web_docs /docs /development /trending-causes-by-geography.md
jcbowyer's picture
Clean HuggingFace deployment without binary files
e59d91d
|
Raw
History Blame Contribute Delete
7.71 kB
---
sidebar_position: 12
---
# Dynamic Trending Causes by Geography
This guide explains how trending causes are computed and displayed based on the selected geography.
## Overview
The Open Navigator homepage displays **trending causes** - policy areas that have received the most attention in recent government meetings. These causes are dynamically computed based on:
- **Geography**: National, State, County, or City level
- **Time window**: Last 90 days of decisions
- **Data source**: AI-analyzed meeting transcripts from `bronze_decisions` table
## How It Works
### Data Flow
```
bronze_decisions (meetings)
↓ (dbt staging)
stg_bronze_decisions (cleaned, filtered to last 90 days)
↓ (dbt intermediate)
int_trending_causes_by_jurisdiction (aggregated by cause & jurisdiction)
↓ (dbt marts)
jurisdiction_state_aggregate.trending_causes (JSONB column)
↓ (API)
GET /api/stats?state=AL&city=Mobile
↓ (Frontend)
Home.tsx displays location-specific trending causes
```
### Example User Flow
1. **User lands on homepage** β†’ Shows national trending causes
2. **User searches for "Mobile, AL"** β†’ Shows Mobile's trending causes (last 90 days)
3. **User searches for "Alabama"** β†’ Shows Alabama's trending causes (aggregated from all AL cities)
4. **No data?** β†’ Falls back to global trending causes from `/api/trending`
## Technical Implementation
### Database Schema
The `jurisdiction_state_aggregate` table contains pre-computed statistics at multiple levels:
```sql
CREATE TABLE jurisdiction_state_aggregate (
level VARCHAR(20), -- 'national', 'state', 'county', 'city'
state_code VARCHAR(2),
county VARCHAR(100),
city VARCHAR(100),
jurisdictions_count INTEGER,
nonprofits_count INTEGER,
events_count INTEGER,
contacts_count INTEGER,
trending_causes JSONB, -- ← Dynamic trending causes
last_updated TIMESTAMP
);
```
### Trending Causes JSON
The structure varies by aggregation level:
**City Level** (most detailed):
```json
[
{
"cause": "Education and Workforce",
"code": "COFOG-09",
"decision_count": 5,
"topics": 3,
"most_recent": "2024-05-22",
"rank": 1,
"sample_headlines": [
"MPS highlights literacy strategies",
"Board approves new curriculum"
]
}
]
```
**State Level** (aggregated):
```json
[
{
"cause": "Education and Workforce",
"decision_count": 127,
"jurisdictions": 15
}
]
```
**National Level**:
```json
[
{
"cause": "Education and Workforce",
"decision_count": 1543,
"states": 42
}
]
```
### Frontend Component
The `Home.tsx` component fetches trending causes from the stats endpoint:
```typescript
// Fetch location stats (includes trending_causes)
const { data: locationStats } = useQuery({
queryKey: ['location-stats', location],
queryFn: async () => {
const response = await api.get('/stats', {
params: { state: 'AL', city: 'Mobile' }
});
return response.data;
}
});
// Use location-specific causes if available
const trendingTopics = React.useMemo(() => {
if (locationStats?.trending_causes) {
// Transform database format to UI format
return locationStats.trending_causes.map(cause => ({
name: cause.cause,
icon: getCauseIcon(cause.cause),
description: `${cause.decision_count} recent decisions`
}));
}
// Fallback to global trending
return trendingData?.causes || [];
}, [locationStats, trendingData]);
```
## Updating Trending Causes
### Automated Updates (Recommended)
Set up a daily cron job to refresh trending causes:
```bash
# Add to crontab
0 2 * * * cd /path/to/open-navigator && ./scripts/data/update_trending_causes.sh
```
### Manual Updates
Run the dbt models to recompute trending causes:
```bash
# Quick update
./scripts/data/update_trending_causes.sh
# Or step-by-step
cd dbt_project
dbt run --select stg_bronze_decisions
dbt run --select int_trending_causes_by_jurisdiction
dbt run --select jurisdiction_state_aggregate
```
### Verification
Check that trending causes are populated:
```sql
-- Count jurisdictions with trending causes
SELECT
level,
COUNT(*) as total,
COUNT(CASE WHEN trending_causes IS NOT NULL THEN 1 END) as with_causes
FROM jurisdiction_state_aggregate
GROUP BY level;
-- View sample trending causes
SELECT
city,
state_code,
jsonb_pretty(trending_causes) as causes
FROM jurisdiction_state_aggregate
WHERE level = 'city'
AND trending_causes IS NOT NULL
LIMIT 5;
```
## Cause Categories
Trending causes are mapped to **COFOG** (Classification of Functions of Government) categories:
| Code | Category | Icon | Example Topics |
|------|----------|------|----------------|
| COFOG-01 | General Public Services | πŸ›οΈ | Council procedures, budgets |
| COFOG-04 | Economic Affairs | πŸ’Ό | Business incentives, development |
| COFOG-05 | Environmental Protection | 🌍 | Parks, recycling, climate |
| COFOG-06 | Housing and Community Amenities | 🏠 | Zoning, affordable housing |
| COFOG-07 | Health | πŸ₯ | Public health, hospitals |
| COFOG-08 | Recreation, Culture, Religion | 🎨 | Libraries, museums, sports |
| COFOG-09 | Education and Workforce | πŸ“š | Schools, training programs |
| COFOG-10 | Social Protection | 🀝 | Social services, elderly care |
## Performance Considerations
### Why Pre-compute in dbt?
Instead of computing trending causes on-demand in the API, we use dbt to:
- βœ… **Speed**: Query takes ~10ms vs 3-5 seconds for on-the-fly aggregation
- βœ… **Consistency**: All users see the same data (updated daily)
- βœ… **Scalability**: No expensive computations at request time
- βœ… **Testing**: dbt tests ensure data quality
### Cache Strategy
The API caches stats for 5 minutes:
```python
# In api/routes/stats_neon.py
CACHE_DURATION = timedelta(minutes=5)
```
The frontend also caches for 5 minutes:
```typescript
staleTime: 5 * 60 * 1000 // 5 minutes
```
## Troubleshooting
### No trending causes shown?
Check if data exists:
```sql
SELECT COUNT(*)
FROM jurisdiction_state_aggregate
WHERE trending_causes IS NOT NULL;
```
If count is 0, run the dbt models:
```bash
./scripts/data/update_trending_causes.sh
```
### Causes not updating?
Clear the cache:
```bash
# Restart API to clear server-side cache
pkill -f "python main.py serve"
python main.py serve
# Frontend cache clears automatically after 5 minutes
```
### Wrong causes displayed?
Verify the bronze_decisions table has recent data:
```sql
SELECT
COUNT(*) as total,
MIN(decision_date) as oldest,
MAX(decision_date) as newest
FROM bronze_decisions
WHERE decision_date >= CURRENT_DATE - INTERVAL '90 days';
```
If no recent decisions exist, run the data ingestion pipeline:
```bash
# Load meeting transcripts
python scripts/datasources/gemini/load_meeting_transcripts_bronze.py
# Then update trending causes
./scripts/data/update_trending_causes.sh
```
## Related Documentation
- [dbt ETL Strategy](./dbt-etl-strategy.md) - Overall data pipeline architecture
- [Bronze to Production Merge](./bronze-to-production-merge.md) - Entity resolution strategy
- [dbt Project README](../../dbt_project/README.md) - dbt models and configuration
- [API Stats Endpoint](../api-reference/stats-endpoint.md) - Stats API documentation
## Future Enhancements
Potential improvements to trending causes:
1. **Real-time updates**: Use CDC (Change Data Capture) instead of daily batch
2. **Personalization**: Show causes relevant to user's interests
3. **Trend arrows**: Show if a cause is rising or falling
4. **Time comparison**: "Education up 23% vs last month"
5. **Geographic clustering**: Show regional trends (e.g., "Southern states")