Spaces:
Sleeping
Sleeping
| import streamlit as st | |
| import pandas as pd | |
| import numpy as np | |
| import os | |
| import xgboost as xgb | |
| import pickle | |
| import datetime | |
| from scipy.sparse import hstack, csr_matrix | |
| from groq import Groq | |
| # ------------------- PAGE CONFIG ------------------- | |
| st.set_page_config( | |
| page_title="AI Crime Predictor", | |
| page_icon="π", | |
| layout="wide", | |
| ) | |
| # ------------------- CUSTOM CSS ------------------- | |
| st.markdown(""" | |
| <style> | |
| /* Animated gradient background */ | |
| @keyframes gradientShift { | |
| 0% { background-position: 0% 50%; } | |
| 50% { background-position: 100% 50%; } | |
| 100% { background-position: 0% 50%; } | |
| } | |
| body, .stApp { | |
| background: linear-gradient(-45deg, #0a0e27, #1a1a2e, #16213e, #0f3460); | |
| background-size: 400% 400%; | |
| animation: gradientShift 15s ease infinite; | |
| color: #ffffff; | |
| } | |
| /* Title with gradient text */ | |
| .big-title { | |
| font-size: 3.5rem; | |
| font-weight: 800; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| text-align: center; | |
| margin-bottom: 10px; | |
| text-shadow: 0 0 30px rgba(102, 126, 234, 0.5); | |
| letter-spacing: -1px; | |
| } | |
| /* Subtitle with glow */ | |
| .sub-title { | |
| text-align: center; | |
| font-size: 1.3rem; | |
| color: #a8b2d1; | |
| margin-bottom: 40px; | |
| font-weight: 300; | |
| } | |
| /* Glassmorphism card */ | |
| .glass-card { | |
| background: rgba(255, 255, 255, 0.05); | |
| backdrop-filter: blur(10px); | |
| -webkit-backdrop-filter: blur(10px); | |
| padding: 30px; | |
| border-radius: 24px; | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37); | |
| transition: all 0.4s ease; | |
| margin-bottom: 25px; | |
| } | |
| .glass-card:hover { | |
| box-shadow: 0 12px 40px 0 rgba(102, 126, 234, 0.4); | |
| transform: translateY(-5px); | |
| border: 1px solid rgba(102, 126, 234, 0.3); | |
| } | |
| /* Premium button styling */ | |
| .stButton>button { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| padding: 0.8rem 2rem; | |
| border-radius: 12px; | |
| border: none; | |
| font-size: 1.1rem; | |
| font-weight: 600; | |
| transition: all 0.3s ease; | |
| box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4); | |
| } | |
| .stButton>button:hover { | |
| background: linear-gradient(135deg, #764ba2 0%, #667eea 100%); | |
| transform: translateY(-2px) scale(1.02); | |
| box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6); | |
| } | |
| /* Sidebar styling */ | |
| [data-testid="stSidebar"] { | |
| background: rgba(15, 23, 42, 0.8); | |
| backdrop-filter: blur(10px); | |
| border-right: 1px solid rgba(255, 255, 255, 0.1); | |
| } | |
| /* Input fields */ | |
| .stTextInput>div>div>input, | |
| .stTextArea>div>div>textarea, | |
| .stNumberInput>div>div>input { | |
| background: rgba(255, 255, 255, 0.8) !important; | |
| border: 1px solid rgba(255, 255, 255, 0.3) !important; | |
| border-radius: 10px !important; | |
| color: #000000 !important; | |
| transition: all 0.3s ease; | |
| } | |
| /* Ensure text is visible when typing */ | |
| .stTextInput input, | |
| .stTextArea textarea { | |
| color: #000000 !important; | |
| } | |
| .stTextInput>div>div>input:focus, | |
| .stTextArea>div>div>textarea:focus, | |
| .stNumberInput>div>div>input:focus { | |
| border: 1px solid rgba(102, 126, 234, 0.8) !important; | |
| box-shadow: 0 0 15px rgba(102, 126, 234, 0.5) !important; | |
| color: #000000 !important; | |
| } | |
| /* Placeholder text styling */ | |
| .stTextInput input::placeholder, | |
| .stTextArea textarea::placeholder { | |
| color: rgba(0, 0, 0, 0.5) !important; | |
| } | |
| /* Chat message styles */ | |
| .user-message { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| padding: 15px 20px; | |
| border-radius: 18px 18px 5px 18px; | |
| margin: 10px 0; | |
| max-width: 80%; | |
| margin-left: auto; | |
| color: white; | |
| font-size: 1rem; | |
| box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3); | |
| } | |
| .ai-message { | |
| background: rgba(255, 255, 255, 0.08); | |
| backdrop-filter: blur(10px); | |
| padding: 15px 20px; | |
| border-radius: 18px 18px 18px 5px; | |
| margin: 10px 0; | |
| max-width: 80%; | |
| margin-right: auto; | |
| color: #e2e8f0; | |
| font-size: 1rem; | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); | |
| } | |
| /* Chat container */ | |
| .chat-container { | |
| background: rgba(255, 255, 255, 0.03); | |
| backdrop-filter: blur(10px); | |
| padding: 25px; | |
| border-radius: 20px; | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| max-height: 500px; | |
| overflow-y: auto; | |
| margin-bottom: 20px; | |
| } | |
| /* Scrollbar styling */ | |
| .chat-container::-webkit-scrollbar { | |
| width: 8px; | |
| } | |
| .chat-container::-webkit-scrollbar-track { | |
| background: rgba(255, 255, 255, 0.05); | |
| border-radius: 10px; | |
| } | |
| .chat-container::-webkit-scrollbar-thumb { | |
| background: rgba(102, 126, 234, 0.5); | |
| border-radius: 10px; | |
| } | |
| .chat-container::-webkit-scrollbar-thumb:hover { | |
| background: rgba(102, 126, 234, 0.8); | |
| } | |
| /* Success/Info boxes */ | |
| .element-container div[data-testid="stMarkdownContainer"] > div[data-testid="stMarkdown"] { | |
| animation: fadeIn 0.5s ease; | |
| } | |
| @keyframes fadeIn { | |
| from { opacity: 0; transform: translateY(10px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # ------------------- TITLE ------------------- | |
| st.markdown('<p class="big-title">π AI Crime Prediction System</p>', unsafe_allow_html=True) | |
| st.markdown('<p class="sub-title">Predict crime category using time, location, and incident description.</p>', unsafe_allow_html=True) | |
| # ------------------- LOAD MODEL ------------------- | |
| def load_artifacts(): | |
| try: | |
| # path relative to streamlit_app.py | |
| pkl_path = "src/crime_xgb_artifacts.pkl" | |
| with open(pkl_path, 'rb') as f: | |
| return pickle.load(f) | |
| except Exception as e: | |
| st.error(f"β Artifact loading error: {e}") | |
| return None | |
| artifacts = load_artifacts() | |
| if not artifacts: | |
| st.warning("Artifacts missing! Add `crime_xgb_artifacts.pkl` in directory.") | |
| st.stop() | |
| model = artifacts['model'] | |
| le_target = artifacts['le_target'] | |
| addr_hasher = artifacts['addr_hasher'] | |
| desc_hasher = artifacts['desc_hasher'] | |
| dense_cols = artifacts['dense_cols'] | |
| # ------------------- GROQ SETUP ------------------- | |
| def get_groq_client(): | |
| return Groq(api_key="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") | |
| def explain_prediction_with_llama(prompt): | |
| """Use Groq's Llama model to explain crime prediction""" | |
| try: | |
| client = get_groq_client() | |
| chat_completion = client.chat.completions.create( | |
| messages=[ | |
| { | |
| "role": "user", | |
| "content": prompt, | |
| } | |
| ], | |
| model="llama-3.3-70b-versatile", | |
| ) | |
| return chat_completion.choices[0].message.content | |
| except Exception as e: | |
| return f"β οΈ Could not generate explanation: {e}" | |
| # ------------------- SIDEBAR ------------------- | |
| st.sidebar.title("π Input Features") | |
| date = st.sidebar.date_input("π Date", datetime.date.today()) | |
| time = st.sidebar.time_input("β° Time", datetime.datetime.now().time()) | |
| default_lat = 37.7749 | |
| default_lng = -122.4194 | |
| lat = st.sidebar.number_input("π Latitude", value=default_lat, format="%.6f") | |
| lng = st.sidebar.number_input("π Longitude", value=default_lng, format="%.6f") | |
| districts = sorted(['BAYVIEW', 'CENTRAL', 'INGLESIDE', 'MISSION', 'NORTHERN', 'PARK', 'RICHMOND', 'SOUTHERN', 'TARAVAL', 'TENDERLOIN']) | |
| district = st.sidebar.selectbox("π’ Police District", districts) | |
| address = st.sidebar.text_input("π Address", "") | |
| description = st.sidebar.text_area("π Description", "") | |
| # ------------------- MAIN PREDICTION CARD ------------------- | |
| with st.container(): | |
| st.markdown("<div class='glass-card'>", unsafe_allow_html=True) | |
| st.subheader("π Prediction Panel") | |
| if st.button("π Predict Crime Category"): | |
| try: | |
| dt_obj = pd.to_datetime(f"{date} {time}") | |
| hour = dt_obj.hour | |
| dense_data = { | |
| 'X': float(lng), | |
| 'Y': float(lat), | |
| 'Year': dt_obj.year, | |
| 'Month': dt_obj.month, | |
| 'Day': dt_obj.day, | |
| 'Minute': dt_obj.minute, | |
| 'Hour': hour, | |
| 'Hour_sin': np.sin(2 * np.pi * hour / 24), | |
| 'Hour_cos': np.cos(2 * np.pi * hour / 24), | |
| 'PdDistrict_enc': districts.index(district), | |
| 'DayOfWeek_enc': dt_obj.dayofweek | |
| } | |
| dense_df = pd.DataFrame([dense_data])[dense_cols] | |
| dense_sparse = csr_matrix(dense_df.values) | |
| addr_hashed = addr_hasher.transform([address.split()]) | |
| desc_hashed = desc_hasher.transform([description.split()]) | |
| features = hstack([dense_sparse, addr_hashed, desc_hashed]) | |
| probs = model.predict_proba(features)[0] | |
| top_idx = np.argmax(probs) | |
| category = le_target.inverse_transform([top_idx])[0] | |
| confidence = probs[top_idx] * 100 | |
| st.success(f"### π¨ Predicted Category: **{category}**") | |
| st.info(f"**Confidence:** {confidence:.2f}%") | |
| # Top 3 chart | |
| top3 = probs.argsort()[-3:][::-1] | |
| chart_data = pd.DataFrame({ | |
| "Category": le_target.inverse_transform(top3), | |
| "Probability": probs[top3] | |
| }).set_index("Category") | |
| st.subheader("π Top 3 Probabilities") | |
| st.bar_chart(chart_data) | |
| st.subheader("π Location Preview") | |
| st.map(pd.DataFrame({"lat": [lat], "lon": [lng]})) | |
| # AI Explanation using Groq | |
| if description: | |
| with st.spinner("π§ Generating AI explanation..."): | |
| explanation = explain_prediction_with_llama( | |
| f"In 2-3 sentences, explain why a crime prediction model might classify an incident as '{category}' based on this description: '{description}'. Be concise and factual." | |
| ) | |
| st.subheader("π§ AI Explanation") | |
| st.write(explanation) | |
| except Exception as e: | |
| st.error(f"β Prediction Error: {e}") | |
| st.markdown("</div>", unsafe_allow_html=True) | |
| # ------------------- INTERACTIVE CHATBOT ------------------- | |
| st.markdown("---") | |
| st.markdown("<div class='glass-card'>", unsafe_allow_html=True) | |
| st.subheader("π¬ AI Crime Safety Assistant") | |
| st.markdown("Ask me anything about crime prediction, safety tips, or how this system works!", unsafe_allow_html=True) | |
| # Initialize chat history in session state | |
| if 'messages' not in st.session_state: | |
| st.session_state.messages = [ | |
| {"role": "assistant", "content": "π Hello! I'm your AI Crime Safety Assistant. I can help you understand crime patterns, provide safety recommendations, and explain how our prediction model works. What would you like to know?"} | |
| ] | |
| # Display chat history | |
| st.markdown("<div class='chat-container'>", unsafe_allow_html=True) | |
| for message in st.session_state.messages: | |
| if message["role"] == "user": | |
| st.markdown(f"<div class='user-message'>π§ {message['content']}</div>", unsafe_allow_html=True) | |
| else: | |
| st.markdown(f"<div class='ai-message'>π€ {message['content']}</div>", unsafe_allow_html=True) | |
| st.markdown("</div>", unsafe_allow_html=True) | |
| # Chat input | |
| col1, col2 = st.columns([5, 1]) | |
| with col1: | |
| user_input = st.text_input("Type your message...", key="chat_input", label_visibility="collapsed", placeholder="Ask about crime safety, predictions, or get recommendations...") | |
| with col2: | |
| send_button = st.button("Send π€", use_container_width=True) | |
| # Handle chat submission | |
| if send_button and user_input: | |
| # Add user message to history | |
| st.session_state.messages.append({"role": "user", "content": user_input}) | |
| # Get AI response using Groq | |
| with st.spinner("π§ Thinking..."): | |
| try: | |
| client = get_groq_client() | |
| # Create system prompt for crime prediction context | |
| system_prompt = """You are an AI Crime Safety Assistant for a crime prediction system. | |
| You help users understand: | |
| - Crime patterns and trends in San Francisco | |
| - How the XGBoost machine learning model predicts crime categories | |
| - Safety tips and recommendations based on location and time | |
| - What factors influence crime predictions (time, location, historical data) | |
| Be helpful, concise, and informative. Keep responses to 2-3 sentences unless more detail is needed. | |
| If asked about the model, explain it uses features like latitude, longitude, time, district, and description to predict crime types.""" | |
| # Prepare messages for Groq API | |
| api_messages = [{"role": "system", "content": system_prompt}] | |
| # Add recent chat history (last 5 messages for context) | |
| for msg in st.session_state.messages[-5:]: | |
| api_messages.append({"role": msg["role"], "content": msg["content"]}) | |
| # Get response from Groq | |
| chat_completion = client.chat.completions.create( | |
| messages=api_messages, | |
| model="llama-3.3-70b-versatile", | |
| temperature=0.7, | |
| max_tokens=500 | |
| ) | |
| ai_response = chat_completion.choices[0].message.content | |
| # Add AI response to history | |
| st.session_state.messages.append({"role": "assistant", "content": ai_response}) | |
| except Exception as e: | |
| error_msg = f"β οΈ Sorry, I encountered an error: {str(e)}" | |
| st.session_state.messages.append({"role": "assistant", "content": error_msg}) | |
| # Rerun to update chat display | |
| st.rerun() | |
| st.markdown("</div>", unsafe_allow_html=True) | |