letudiant_admin / scripts /extract_journey_data.py
Luli Maruyama
Deploy app to Hugging Face Space (no binaries)
9e42d3b
Raw History Blame Contribute Delete
6.92 kB
#!/usr/bin/env python3
"""
Extract journey tracking data from the database.
Usage: python3 extract_journey_data.py [query_name]
"""
import sqlite3
import json
import pandas as pd
from pathlib import Path
# Database path
DB_PATH = Path(__file__).parent.parent / "backend" / "app" / "db" / "local.db"
def get_connection():
"""Connect to SQLite database"""
conn = sqlite3.connect(str(DB_PATH))
conn.row_factory = sqlite3.Row
return conn
def extract_stand_visits_detailed():
"""Extract all stand visits with dwell time"""
query = """
SELECT
sp.full_name,
st.stand_code,
st.exhibitor_name,
sv.visit_start,
ROUND(sv.dwell_time_seconds / 60.0, 1) as dwell_minutes,
sv.conversation_happened,
sv.materials_photographed,
sv.primary_vs_secondary
FROM stand_visits sv
LEFT JOIN student_profiles sp ON sv.student_id = sp.student_id
LEFT JOIN stands st ON sv.stand_id = st.id
ORDER BY sv.visit_start;
"""
conn = get_connection()
df = pd.read_sql_query(query, conn)
conn.close()
return df
def extract_high_intent_leads():
"""Extract Léa & Thomas type leads (high intent signals)"""
query = """
SELECT
sp.full_name,
sp.email,
COUNT(DISTINCT sv.stand_id) as stands_visited,
ROUND(SUM(sv.dwell_time_seconds) / 60.0, 1) as total_dwell_minutes,
SUM(CASE WHEN sv.conversation_happened = 1 THEN 1 ELSE 0 END) as conversations,
SUM(CASE WHEN sv.materials_photographed = 1 THEN 1 ELSE 0 END) as materials_photos,
COUNT(DISTINCT dcs.id) as direct_emails_sent,
COALESCE(pt.fair_influenced_decision, 0) as parcoursup_influenced,
COALESCE(js.journey_completeness_score, 0) as journey_score
FROM student_profiles sp
LEFT JOIN stand_visits sv ON sp.student_id = sv.student_id
LEFT JOIN direct_contact_signals dcs ON sp.student_id = dcs.student_id
LEFT JOIN parcoursup_tracking pt ON sp.student_id = pt.student_id
LEFT JOIN journey_scores js ON sp.student_id = js.student_id
WHERE sv.stand_id IS NOT NULL
GROUP BY sp.student_id
HAVING
SUM(sv.dwell_time_seconds) > 600 -- 10+ minutes
OR SUM(CASE WHEN sv.conversation_happened = 1 THEN 1 ELSE 0 END) >= 1
OR COUNT(DISTINCT dcs.id) > 0
ORDER BY journey_score DESC;
"""
conn = get_connection()
df = pd.read_sql_query(query, conn)
conn.close()
return df
def extract_discovery_sources():
"""Breakdown of how students found the fair"""
query = """
SELECT
source_channel,
COUNT(*) as count,
AVG(registration_time_seconds) as avg_reg_time_sec,
GROUP_CONCAT(DISTINCT sp.full_name, ', ') as students
FROM discovery_sources ds
LEFT JOIN student_profiles sp ON ds.student_id = sp.student_id
GROUP BY source_channel
ORDER BY count DESC;
"""
conn = get_connection()
df = pd.read_sql_query(query, conn)
conn.close()
return df
def extract_engagement_timeline(student_id):
"""Get complete engagement timeline for a student"""
query = """
SELECT
'Pre-Fair App Opens' as phase,
pfe.app_opens_before_fair as value,
pfe.created_at as timestamp
FROM pre_fair_engagement pfe
WHERE pfe.student_id = ?
UNION ALL
SELECT
'Stand Visit: ' || st.exhibitor_name,
ROUND(sv.dwell_time_seconds / 60.0, 1),
sv.visit_start
FROM stand_visits sv
LEFT JOIN stands st ON sv.stand_id = st.id
WHERE sv.student_id = ?
UNION ALL
SELECT
'Email to Exhibitor',
1,
dcs.contact_time
FROM direct_contact_signals dcs
WHERE dcs.student_id = ? AND dcs.contact_type = 'email_to_exhibitor'
ORDER BY timestamp;
"""
conn = get_connection()
df = pd.read_sql_query(query, conn, params=[student_id, student_id, student_id])
conn.close()
return df
def extract_parcoursup_changes():
"""Show students who changed Parcoursup choices post-fair"""
query = """
SELECT
sp.full_name,
sp.email,
pt.parcoursup_programmes_before_fair,
pt.parcoursup_programmes_after_fair,
(pt.parcoursup_programmes_after_fair - pt.parcoursup_programmes_before_fair) as net_change,
pt.programmes_added,
pt.programmes_removed,
pt.confidence_change,
pt.updated_at
FROM parcoursup_tracking pt
LEFT JOIN student_profiles sp ON pt.student_id = sp.student_id
WHERE pt.programmes_added IS NOT NULL OR pt.programmes_removed IS NOT NULL
ORDER BY pt.updated_at DESC;
"""
conn = get_connection()
df = pd.read_sql_query(query, conn)
conn.close()
return df
def extract_churn_risk():
"""Identify students at risk of churning"""
query = """
SELECT
sp.full_name,
sp.email,
ch.last_engagement_date,
ch.days_to_first_churn,
ch.reason_inferred,
ch.re_engagement_attempts,
ch.re_engaged
FROM churn_signals ch
LEFT JOIN student_profiles sp ON ch.student_id = sp.student_id
ORDER BY ch.last_engagement_date DESC;
"""
conn = get_connection()
df = pd.read_sql_query(query, conn)
conn.close()
return df
def export_to_formats(df, name):
"""Export dataframe to multiple formats"""
csv_path = f"journey_data_{name}.csv"
json_path = f"journey_data_{name}.json"
df.to_csv(csv_path, index=False)
df.to_json(json_path, orient='records', indent=2)
print(f"✓ Exported to {csv_path}")
print(f"✓ Exported to {json_path}")
return csv_path, json_path
def main():
print("=" * 70)
print("JOURNEY DATA EXTRACTION")
print("=" * 70)
# 1. High-intent leads (like Léa)
print("\n📊 HIGH-INTENT LEADS (Léa & Thomas profiles)...")
df_high_intent = extract_high_intent_leads()
print(df_high_intent.to_string())
export_to_formats(df_high_intent, "high_intent_leads")
# 2. Stand visits with dwell time
print("\n\n📊 STAND VISITS WITH DWELL TIME...")
df_stands = extract_stand_visits_detailed()
print(df_stands.head(20).to_string())
export_to_formats(df_stands, "stand_visits")
# 3. Discovery sources
print("\n\n📊 HOW STUDENTS FOUND THE FAIR...")
df_discovery = extract_discovery_sources()
print(df_discovery.to_string())
export_to_formats(df_discovery, "discovery_sources")
# 4. Parcoursup changes
print("\n\n📊 PARCOURSUP DECISIONS INFLUENCED BY FAIR...")
df_parco = extract_parcoursup_changes()
print(df_parco.to_string())
export_to_formats(df_parco, "parcoursup_changes")
# 5. Churn risk
print("\n\n📊 STUDENTS AT CHURN RISK...")
df_churn = extract_churn_risk()
print(df_churn.to_string())
export_to_formats(df_churn, "churn_risk")
print("\n" + "=" * 70)
print("✓ All data exported to journey_data_*.csv and journey_data_*.json")
print("=" * 70)
if __name__ == "__main__":
main()