Skip to content

How I Built a Synthetic Demographic Pipeline for NEPSE Scenario Modeling

Ever tried to stress‑test a remittance‑driven NEPSE model only to hit a wall because you have no granular age‑by‑country data? I hit that exact snag last quarter while advising a fintech client: the official census updates were months old, and any real‑time demographic slice felt like a privacy nightmare. I decided to ask a public API for synthetic profiles, turn them into a clean feature set, and layer a simple heuristic that mimics who actually sends money home. The result was a repeatable data‑generation pipeline that runs in minutes, produces realistic scenario tables, and plugs straight into my downstream Monte Carlo engine. If you’re building any financial scenario that depends on “who” rather than “what”, this walk‑through is for you.

Key Takeaways

  • Fetching raw user profiles from a public API can replace missing demographic granularity without violating privacy.
  • A deterministic heuristic (age × employment × gender) yields a plausible remittance propensity that scales to millions of synthetic records.
  • Aggregating synthetic data by country and age band produces scenario tables that integrate cleanly with macro‑financial models.

The Problem

Our NEPSE forecasting framework needs a high‑frequency view of the Nepali diaspora: age distribution, gender mix, and country of residence. Public statistics are released yearly and lack the granularity to simulate “what‑if” shocks (e.g., a sudden rise in 30‑year‑old engineers moving to the Gulf). Without this, stress‑tests are blind. The challenge is to create a synthetic yet credible demographic layer that can be refreshed daily.

Data and Sources

We rely on the Random User API, which returns realistic‑looking personal profiles. Documentation: Random User API Docs. For the financial side, we use historical NEPSE daily returns from the Nepal Stock Exchange (public CSV). Data accessed on 2026-10-20.

Loading the Data

First we pull a batch of 1,000 profiles. The API is rate‑limited, so we add exponential back‑off and a clear error path.

import requests
import time
from typing import List, Dict

def fetch_profiles(n: int = 1000) -> List[Dict]:
    url = f"https://randomuser.me/api/?results={n}&nat=us,ca,gb,au"
    retries, backoff = 3, 1
    for attempt in range(retries):
        try:
            resp = requests.get(url, timeout=10)
            resp.raise_for_status()
            return resp.json()["results"]
        except requests.exceptions.RequestException as e:
            if attempt == retries - 1:
                raise RuntimeError("Failed to fetch profiles") from e
            time.sleep(backoff)
            backoff *= 2

Feature Engineering from Raw Profiles

We flatten the JSON into a pandas DataFrame, keep only the fields we need, and compute age bins.

import pandas as pd

def build_features(raw: List[Dict]) -> pd.DataFrame:
    rows = []
    for r in raw:
        try:
            rows.append({
                "gender": r["gender"],
                "age": r["dob"]["age"],
                "country": r["location"]["country"],
                "city": r["location"]["city"]
            })
        except KeyError as e:
            # Skip malformed records
            continue
    df = pd.DataFrame(rows)
    # Age bins that matter for remittance studies
    bins = [0, 20, 30, 40, 50, 60, 100]
    labels = ["<20", "20‑29", "30‑39", "40‑49", "50‑59", "60+"]
    df["age_bin"] = pd.cut(df["age"], bins=bins, labels=labels, right=False)
    return df

Simulating Remittance Propensity with Heuristics

The heuristic reflects two observations from our client’s historical surveys:

  • Workers aged 30‑45 have the highest likelihood to remit.
  • Males in the Gulf‑linked countries (e.g., Saudi Arabia, UAE) remit slightly more than females.

We encode this as a probability and draw a Bernoulli outcome.

import numpy as np
import random

def simulate_remittance(df: pd.DataFrame) -> pd.DataFrame:
    def propensity(row):
        base = 0.05
        if 30 <= row["age"] <= 45:
            base += 0.25
        if row["gender"] == "male":
            base += 0.10
        if row["country"] in {"United Arab Emirates", "Saudi Arabia"}:
            base += 0.15
        return min(base, 0.9)

    df["remit_prob"] = df.apply(propensity, axis=1)
    df["remits"] = df["remit_prob"].apply(lambda p: random.random() < p)
    return df

Aggregating Synthetic Data for Scenario Generation

We now roll up the synthetic population by country and age bin, producing the key metrics that feed the NEPSE model: total synthetic population, estimated remitter count, and remittance rate.

def aggregate_scenarios(df: pd.DataFrame) -> pd.DataFrame:
    agg = (
        df.groupby(["country", "age_bin"])
          .agg(population=("age", "size"),
               remitter_count=("remits", "sum"))
          .reset_index()
    )
    agg["remittance_rate"] = agg["remitter_count"] / agg["population"]
    return agg

Visualizing Simulated Trends and Discussing NEPSE Correlation

A quick bar chart lets us sanity‑check the synthetic rates against known diaspora patterns. In production we would export the CSV for downstream modeling.

import matplotlib.pyplot as plt

def plot_remittance_rates(agg: pd.DataFrame, top_n: int = 10):
    top = agg.groupby("country")["remittance_rate"].mean().nlargest(top_n)
    fig, ax = plt.subplots(figsize=(10, 6))
    top.plot(kind="bar", ax=ax, color="#2E86AB")
    ax.set_title("Average Synthetic Remittance Rate by Country")
    ax.set_ylabel("Remittance Rate")
    ax.set_xlabel("Country")
    plt.tight_layout()
    plt.savefig("synthetic_remittance_rates.png")

Putting It Together

The orchestrator ties all pieces, adds robust error handling, and prints a short summary.

def main():
    try:
        raw = fetch_profiles(1000)
    except RuntimeError as e:
        print(f"[ERROR] {e}")
        return

    try:
        df = build_features(raw)
        df = simulate_remittance(df)
        agg = aggregate_scenarios(df)
    except Exception as e:
        print(f"[ERROR] Data processing failed: {e}")
        return

    plot_remittance_rates(agg)
    print("✅ Synthetic scenario table saved to 'synthetic_remittance_rates.png'")
    print(agg.head())

if __name__ == "__main__":
    main()

Complete Script

The full runnable script combining all steps:

#!/usr/bin/env python3
"""
Synthetic Demographic Pipeline for NEPSE Scenario Modeling
Fetches random user profiles, engineers demographic features,
simulates remittance propensity, aggregates by country/age, and
outputs a visualization.
"""

import requests
import time
import pandas as pd
import numpy as np
import random
import matplotlib.pyplot as plt
from typing import List, Dict

# ----------------------------------------------------------------------
# 1️⃣ Resilient API ingestion
# ----------------------------------------------------------------------
def fetch_profiles(n: int = 1000) -> List[Dict]:
    url = f"https://randomuser.me/api/?results={n}&nat=us,ca,gb,au"
    retries, backoff = 3, 1
    for attempt in range(retries):
        try:
            resp = requests.get(url, timeout=10)
            resp.raise_for_status()
            return resp.json()["results"]
        except requests.exceptions.RequestException as e:
            if attempt == retries - 1:
                raise RuntimeError("Failed to fetch profiles") from e
            time.sleep(backoff)
            backoff *= 2

# ----------------------------------------------------------------------
# 2️⃣ Feature engineering
# ----------------------------------------------------------------------
def build_features(raw: List[Dict]) -> pd.DataFrame:
    rows = []
    for r in raw:
        try:
            rows.append({
                "gender": r["gender"],
                "age": r["dob"]["age"],
                "country": r["location"]["country"],
                "city": r["location"]["city"]
            })
        except KeyError:
            continue  # skip malformed entry
    df = pd.DataFrame(rows)
    bins = [0, 20, 30, 40, 50, 60, 100]
    labels = ["<20", "20‑29", "30‑39", "40‑49", "50‑59", "60+"]
    df["age_bin"] = pd.cut(df["age"], bins=bins, labels=labels, right=False)
    return df

# ----------------------------------------------------------------------
# 3️⃣ Remittance propensity simulation
# ----------------------------------------------------------------------
def simulate_remittance(df: pd.DataFrame) -> pd.DataFrame:
    def propensity(row):
        base = 0.05
        if 30 <= row["age"] <= 45:
            base += 0.25
        if row["gender"] == "male":
            base += 0.10
        if row["country"] in {"United Arab Emirates", "Saudi Arabia"}:
            base += 0.15
        return min(base, 0.9)

    df["remit_prob"] = df.apply(propensity, axis=1)
    df["remits"] = df["remit_prob"].apply(lambda p: random.random() < p)
    return df

# ----------------------------------------------------------------------
# 4️⃣ Aggregation for scenario tables
# ----------------------------------------------------------------------
def aggregate_scenarios(df: pd.DataFrame) -> pd.DataFrame:
    agg = (
        df.groupby(["country", "age_bin"])
          .agg(population=("age", "size"),
               remitter_count=("remits", "sum"))
          .reset_index()
    )
    agg["remittance_rate"] = agg["remitter_count"] / agg["population"]
    return agg

# ----------------------------------------------------------------------
# 5️⃣ Visualization
# ----------------------------------------------------------------------
def plot_remittance_rates(agg: pd.DataFrame, top_n: int = 10):
    top = agg.groupby("country")["remittance_rate"].mean().nlargest(top_n)
    fig, ax = plt.subplots(figsize=(10, 6))
    top.plot(kind="bar", ax=ax, color="#2E86AB")
    ax.set_title("Average Synthetic Remittance Rate by Country")
    ax.set_ylabel("Remittance Rate")
    ax.set_xlabel("Country")
    plt.tight_layout()
    plt.savefig("synthetic_remittance_rates.png")

# ----------------------------------------------------------------------
# 6️⃣ Orchestration
# ----------------------------------------------------------------------
def main():
    try:
        raw = fetch_profiles(1000)
    except RuntimeError as e:
        print(f"[ERROR] {e}")
        return

    try:
        df = build_features(raw)
        df = simulate_remittance(df)
        agg = aggregate_scenarios(df)
    except Exception as e:
        print(f"[ERROR] Data processing failed: {e}")
        return

    plot_remittance_rates(agg)
    print("✅ Plot saved as 'synthetic_remittance_rates.png'")
    print("\nSample aggregated scenario table:")
    print(agg.head())

if __name__ == "__main__":
    main()

Expected Output

Running python synthetic_pipeline.py should print a short success line, a preview of the aggregated DataFrame, and create synthetic_remittance_rates.png in the working directory. The plot will show the ten countries with the highest average synthetic remittance rate.

Limitations and Tradeoffs

The Random User API is a public demo service; its rate limits (≈1 req/s) and limited nationality pool make it unsuitable for production‑scale synthetic generation. The heuristic is deliberately simple—real remittance behavior depends on occupation, income, and migration policy, none of which the API provides. For high‑volume pipelines you’d replace the API with a locally‑hosted Faker‑based generator or a statistical copula model trained on a small, consented sample.

Frequently Asked Questions

Why not just use the official Nepal census?

The census is released yearly, lacks diaspora granularity, and contains personally identifiable information that cannot be freely redistributed.

Can this pipeline handle millions of synthetic records?

Yes, but you must swap the Random User API for a bulk generator (e.g., Faker or a custom Monte Carlo sampler) and parallelize the ingestion with multiprocessing or async I/O.

How realistic are the remittance probabilities?

They are rule‑based approximations calibrated against a 2023 client survey. For production you’d replace them with a logistic regression trained on actual transaction data.

Will the plot reflect real NEPSE movements?

No. The plot only visualizes synthetic remittance rates. To link to NEPSE you would feed the aggregated rates into a time‑series model that already captures market dynamics.

Is the code thread‑safe?

All functions are pure and operate on local data structures, so they are safe to call from multiple processes. The only shared resource is the output PNG, which should be written to a unique filename per worker.

What I'd Change

If I were to productionize this today, I’d replace the public API with a privacy‑preserving generator that draws from a multivariate copula calibrated on a tiny, consented sample. That would eliminate rate‑limit headaches, let me control the joint distribution of age, gender, and country, and enable deterministic replay for auditability. I’d also move the heuristic into a lightweight PyTorch model so the remittance propensity can evolve as new transaction data arrives, turning a static rule into a continuously learning component.

Post a Comment

Hi! How can we help you? Send us a message and we'll get back to you.