antiperfect/refugee-movement-api
0
1from flask import Flask, render_template, request, jsonify2from flask_cors import CORS3import joblib4import pandas as pd5import json6import os7import requests as http_requests8 9app = Flask(__name__)10CORS(app)11 12# Load models13BASE_DIR = os.path.dirname(os.path.abspath(__file__))14 15time_model = joblib.load(os.path.join(BASE_DIR, 'time_model.pkl'))16resource_model = joblib.load(os.path.join(BASE_DIR, 'resource_model.pkl'))17 18# Load mapping19with open(os.path.join(BASE_DIR, 'origin_mapping.json')) as f:20 mapping = json.load(f)21 22# Load historical data for lags23DATA_PATH = os.path.join(BASE_DIR, 'data', 'persons_of_concern.csv')24historical_df = pd.read_csv(DATA_PATH)25historical_df['Total_Refugees'] = historical_df['Refugees'] + historical_df['Asylum-seekers']26 27# India's neighboring / nearby countries (subset for highlight)28NEIGHBORS = [29 'Afghanistan', 'Bangladesh', 'China', 'Myanmar',30 'Pakistan', 'Sri Lanka', 'Nepal', 'Bhutan'31]32 33# Cache for predictions34PREDICTION_CACHE = {}35SERIES_CACHE = {}36 37def precompute_predictions(year=2026):38 """Precompute all predictions for a given year to avoid slow runtime calculations."""39 print(f"Precomputing predictions for {year}...", flush=True)40 for country in mapping.keys():41 result = predict_for_country(country, year)42 if result:43 PREDICTION_CACHE[country] = result44 45 # Precompute All Origins Series46 MAX_HIST_YEAR = int(historical_df['Year'].max())47 print(f"Precomputing All Origins Series (2000-2030). Last historical year: {MAX_HIST_YEAR}", flush=True)48 series = []49 50 # Historical51 hist_agg = historical_df.groupby('Year')['Total_Refugees'].sum().reset_index()52 hist_agg.fillna(0, inplace=True)53 for _, row in hist_agg.iterrows():54 y = int(row['Year'])55 if 2000 <= y <= MAX_HIST_YEAR:56 series.append({'x': int(y), 'y': int(row['Total_Refugees'])})57 58 # Predicted59 pred_map = {}60 for c in mapping.keys():61 c_df = historical_df[historical_df['Country of Origin'] == c].sort_values('Year')62 if c_df.empty: continue63 64 last_y = int(c_df.iloc[-1]['Year'])65 l1 = int(c_df.iloc[-1]['Total_Refugees'])66 l2 = int(c_df.iloc[-2]['Total_Refugees'] if len(c_df) > 1 else l1)67 68 if pd.isna(l1): l1 = 069 if pd.isna(l2): l2 = 070 71 enc = mapping[c]72 cy = last_y + 173 74 while cy <= 2030:75 input_df = pd.DataFrame({'Year': [cy], 'Origin_Encoded': [enc], 'Lag_1': [l1], 'Lag_2': [l2]})76 p = max(0, int(time_model.predict(input_df)[0]))77 78 # Only add to pred_map if it's AFTER the historical data for this country79 # and ALSO after the global historical data cutoff to avoid overlaps80 if cy > MAX_HIST_YEAR:81 pred_map[cy] = pred_map.get(cy, 0) + p82 83 l2, l1, cy = l1, p, cy + 184 85 for y, val in sorted(pred_map.items()):86 series.append({'x': int(y), 'y': int(val)})87 88 SERIES_CACHE['all'] = series89 print("Precomputation complete.", flush=True)90 91def predict_for_country(country, year):92 """Run both models for a single country+year and return dict."""93 if country not in mapping:94 return None95 96 origin_encoded = mapping[country]97 98 # Filter historical data for this country99 country_df = historical_df[historical_df['Country of Origin'] == country].sort_values('Year')100 101 if country_df.empty:102 return None103 104 # ---------------- TIME MODEL (Iterative Lags) ----------------105 # If year is in the past or present data106 if year <= country_df.iloc[-1]['Year']:107 row = country_df[country_df['Year'] == year]108 if not row.empty:109 refugees = int(row['Total_Refugees'].values[0])110 else:111 # Default to last known value if specific year missing from history112 refugees = int(country_df.iloc[-1]['Total_Refugees'])113 114 # If year is in the future115 else:116 lag_1 = country_df.iloc[-1]['Total_Refugees']117 lag_2 = country_df.iloc[-2]['Total_Refugees'] if len(country_df) > 1 else lag_1118 119 if pd.isna(lag_1): lag_1 = 0120 if pd.isna(lag_2): lag_2 = 0121 122 current_year = country_df.iloc[-1]['Year'] + 1123 refugees = lag_1 # fallback124 125 while current_year <= year:126 input_df = pd.DataFrame({127 'Year': [current_year],128 'Origin_Encoded': [origin_encoded],129 'Lag_1': [lag_1],130 'Lag_2': [lag_2]131 })132 133 pred = int(time_model.predict(input_df)[0])134 pred = max(0, pred)135 136 lag_2 = lag_1137 lag_1 = pred138 refugees = pred139 current_year += 1140 141 # ---------------- RESOURCE MODEL ----------------142 growth = 0.1 # baseline assumption143 input_data = pd.DataFrame({144 'Year': [year],145 'Origin_Encoded': [origin_encoded],146 'Total_Refugees': [refugees],147 'Refugee_Growth': [growth]148 })149 pred = resource_model.predict(input_data)150 151 return {152 'country': country,153 'year': year,154 'refugees': refugees,155 'food': max(0, int(pred[0][0])),156 'shelter': max(0, int(pred[0][1])),157 'medical': max(0, int(pred[0][2])),158 'water': max(0, int(pred[0][3])),159 'is_neighbor': country in NEIGHBORS160 }161 162 163# ================== HTML ROUTES (original) ==================164 165@app.route('/')166def home():167 return jsonify({168 "status": "online",169 "message": "Refugee Movement Prediction API is running",170 "version": "1.0.0"171 })172 173@app.route('/predict', methods=['POST'])174def predict():175 year = int(request.form['year'])176 country = request.form['country']177 result = predict_for_country(country, year)178 179 if result is None:180 return "Country not found", 404181 182 return render_template('result.html',183 refugees=result['refugees'],184 food=result['food'],185 shelter=result['shelter'],186 medical=result['medical'],187 water=result['water'],188 country=country,189 year=year)190 191 192# ================== JSON API ROUTES (for React frontend) ==================193 194@app.route('/api/predict', methods=['GET'])195def api_predict():196 """Single country prediction.197 GET /api/predict?country=Afghanistan&year=2026198 """199 country = request.args.get('country')200 year = request.args.get('year', 2026, type=int)201 202 if not country or country not in mapping:203 return jsonify({'error': 'Invalid country'}), 400204 205 result = predict_for_country(country, year)206 return jsonify(result)207 208 209@app.route('/api/predict-all', methods=['GET'])210def api_predict_all():211 """Predict for ALL countries in the model.212 GET /api/predict-all?year=2026213 Returns array of predictions sorted by refugee count descending.214 """215 year = request.args.get('year', 2026, type=int)216 217 # If it's the default year and we have a cache, use it218 if year == 2026 and PREDICTION_CACHE:219 results = list(PREDICTION_CACHE.values())220 else:221 results = []222 for country in mapping.keys():223 result = predict_for_country(country, year)224 if result:225 results.append(result)226 227 # Sort by refugee count descending228 results.sort(key=lambda x: x['refugees'], reverse=True)229 return jsonify(results)230 231 232@app.route('/api/countries', methods=['GET'])233def api_countries():234 """Return list of all available countries."""235 countries = list(mapping.keys())236 countries.sort()237 return jsonify({238 'countries': countries,239 'neighbors': [c for c in NEIGHBORS if c in mapping],240 'total': len(countries)241 })242 243 244@app.route('/api/series', methods=['GET'])245def api_series():246 """Return combined historical (2000-MAX_HIST_YEAR) and predicted (MAX_HIST_YEAR+1 - 2030) series.247 GET /api/series?country=Afghanistan248 If no country provided, returns sum of all countries.249 """250 country = request.args.get('country')251 series = []252 253 if country:254 # Single Country255 if country not in mapping:256 return jsonify({'error': 'Country not found'}), 404257 258 # Historical259 MAX_HIST_YEAR = int(historical_df['Year'].max())260 hist = historical_df[historical_df['Country of Origin'] == country].copy()261 hist.fillna(0, inplace=True)262 for _, row in hist.iterrows():263 y = int(row['Year'])264 if 2000 <= y <= MAX_HIST_YEAR:265 series.append({'x': int(y), 'y': int(row['Total_Refugees'])})266 267 # Predicted268 country_df = historical_df[historical_df['Country of Origin'] == country].sort_values('Year')269 if not country_df.empty:270 last_y = int(country_df.iloc[-1]['Year'])271 lag_1 = int(country_df.iloc[-1]['Total_Refugees'])272 lag_2 = int(country_df.iloc[-2]['Total_Refugees'] if len(country_df) > 1 else lag_1)273 274 if pd.isna(lag_1): lag_1 = 0275 if pd.isna(lag_2): lag_2 = 0276 277 origin_encoded = mapping[country]278 279 curr_y = last_y + 1280 while curr_y <= 2030:281 input_df = pd.DataFrame({282 'Year': [curr_y],283 'Origin_Encoded': [origin_encoded],284 'Lag_1': [lag_1],285 'Lag_2': [lag_2]286 })287 pred = int(time_model.predict(input_df)[0])288 pred = max(0, pred)289 290 if curr_y > MAX_HIST_YEAR:291 series.append({'x': int(curr_y), 'y': int(pred)})292 293 lag_2, lag_1 = lag_1, pred294 curr_y += 1295 else:296 # All Origins (Aggregated)297 if 'all' in SERIES_CACHE:298 return jsonify(SERIES_CACHE['all'])299 300 # Historical301 MAX_HIST_YEAR = int(historical_df['Year'].max())302 hist_agg = historical_df.groupby('Year')['Total_Refugees'].sum().reset_index()303 hist_agg.fillna(0, inplace=True)304 for _, row in hist_agg.iterrows():305 y = int(row['Year'])306 if 2000 <= y <= MAX_HIST_YEAR:307 series.append({'x': int(y), 'y': int(row['Total_Refugees'])})308 309 # Predicted for All Origins310 pred_map = {} # year -> sum311 for c in mapping.keys():312 c_df = historical_df[historical_df['Country of Origin'] == c].sort_values('Year')313 if c_df.empty: continue314 315 last_y = int(c_df.iloc[-1]['Year'])316 l1 = int(c_df.iloc[-1]['Total_Refugees'])317 l2 = int(c_df.iloc[-2]['Total_Refugees'] if len(c_df) > 1 else l1)318 enc = mapping[c]319 cy = last_y + 1320 321 while cy <= 2030:322 input_df = pd.DataFrame({'Year': [cy], 'Origin_Encoded': [enc], 'Lag_1': [l1], 'Lag_2': [l2]})323 p = max(0, int(time_model.predict(input_df)[0]))324 if cy > MAX_HIST_YEAR:325 pred_map[cy] = pred_map.get(cy, 0) + p326 l2, l1, cy = l1, p, cy + 1327 328 for y, val in sorted(pred_map.items()):329 series.append({'x': y, 'y': val})330 331 series.sort(key=lambda x: x['x'])332 return jsonify(series)333 334 335@app.route('/api/news', methods=['GET'])336def api_news():337 """Fetch humanitarian news โ tries GDELT first, then UN News RSS fallback."""338 # Try GDELT339 try:340 gdelt_url = 'https://api.gdeltproject.org/api/v2/doc/doc'341 params = {342 'query': 'refugee displacement India humanitarian',343 'mode': 'ArtList',344 'format': 'json',345 'maxrecords': 15,346 'sort': 'DateDesc'347 }348 resp = http_requests.get(gdelt_url, params=params, timeout=15)349 if resp.status_code == 200:350 data = resp.json()351 if 'articles' in data and len(data['articles']) > 0:352 articles = []353 for art in data['articles'][:15]:354 articles.append({355 'title': art.get('title', 'Untitled'),356 'url': art.get('url', '#'),357 'seendate': art.get('seendate', ''),358 'domain': art.get('domain', 'News'),359 })360 return jsonify({'articles': articles, 'source': 'GDELT'})361 except Exception:362 pass # Fall through to RSS fallback363 364 # Fallback: UN News RSS feed (refugee/migration topic)365 try:366 import xml.etree.ElementTree as ET367 rss_url = 'https://news.un.org/feed/subscribe/en/news/topic/migrants-and-refugees/feed/rss.xml'368 resp = http_requests.get(rss_url, timeout=10)369 if resp.status_code == 200:370 root = ET.fromstring(resp.content)371 articles = []372 for item in root.findall('.//item')[:15]:373 title = item.find('title')374 link = item.find('link')375 pubdate = item.find('pubDate')376 articles.append({377 'title': title.text if title is not None else 'Untitled',378 'url': link.text if link is not None else '#',379 'seendate': pubdate.text if pubdate is not None else '',380 'domain': 'UN News',381 })382 if articles:383 return jsonify({'articles': articles, 'source': 'UN News RSS'})384 except Exception:385 pass386 387 return jsonify({'articles': [], 'source': 'none', 'error': 'All news sources unavailable'})388 389 390# ================== RUN ==================391if __name__ == '__main__':392 # Precompute for the default year (2026) to ensure fast startup393 precompute_predictions(2026)394 app.run(debug=False, port=5000)