How to put Python and the survey library on your own laptop, assuming no prior Python. This is what you need in order to keep using the workshop materials once the shared server is gone.
The workshop ran on a shared server, which closes at the end of September 2026. The six sessions are ordinary Jupyter notebooks and run anywhere the software below is installed. If you get stuck, tell us how far you got — open an issue at ligon/hhsurveys or ligon/LSMS_Library, or write. A failure report costs you nothing and is genuinely useful.
The download is roughly 1.3 GB, and the survey data adds a few hundred megabytes more. Please do not start it in the classroom or on mobile data — twenty laptops downloading at once will simply stall, and you will lose the session. Set aside an evening somewhere with a good connection.
This script does everything on the rest of this page: finds or installs
Miniforge, creates the surveys environment,
installs the library, asks for your World Bank key and stores it in the
right place, then reads real survey data to prove it worked. It is safe
to run more than once — anything already done is skipped —
and it does not touch your shell startup files.
Open Terminal and paste these two lines:
curl -fsSL https://hhsurveys-workshop.ligonresearch.org/install_surveys.sh -o install_surveys.sh bash install_surveys.sh
Open PowerShell from the Start menu and paste these three lines:
cd $HOME\Downloads Invoke-WebRequest https://hhsurveys-workshop.ligonresearch.org/install_surveys.ps1 -OutFile install_surveys.ps1 powershell -ExecutionPolicy Bypass -File .\install_surveys.ps1
The manual route is the same thing done by hand, and it is what to fall back on — particularly on Windows, where far fewer people have run this. Either way, please tell us where it stopped: open an issue.
Miniforge gives you Python and a way to install scientific packages. If you already have Anaconda or Miniconda, you can skip this step and use that instead.
Download and run the installer, accepting every default:
Open Terminal (press Cmd + Space,
type terminal, press Return), then paste these three
lines one at a time:
curl -L -O "https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-MacOSX-$(uname -m).sh" bash Miniforge3-MacOSX-$(uname -m).sh -b ~/miniforge3/bin/conda init zsh
Open a terminal and paste these three lines one at a time:
curl -L -O "https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-Linux-$(uname -m).sh" bash Miniforge3-Linux-$(uname -m).sh -b ~/miniforge3/bin/conda init bash
This creates a separate workspace called
surveys, so nothing here can disturb
other software on your computer. Open the
Miniforge Prompt from the Start menu and paste
these two lines, one at a time:
conda create -y -n surveys python=3.12 git conda activate surveys
This creates a separate workspace called
surveys, so nothing here can disturb
other software on your computer. Paste these two lines, one at a
time:
conda create -y -n surveys python=3.12 git conda activate surveys
With (surveys) showing at the start of
your line, paste:
pip install LSMS_Library datamat jupyterlab
Start Jupyter by pasting:
jupyter lab
A page opens in your browser. Click
Notebook → Python 3, paste the
five lines below into the box, and press
Shift + Return.
import lsms_library as ll
ghana = ll.Country('GhanaLSS')
print(ghana.waves)
roster = ghana.household_roster()
print(roster.shape)The years printing tells you only that the library loaded. What matters is what the cell does next, and there are two quite different possibilities:
[*] beside it while it works. It is
downloading the survey data — leave it running, even for
ten minutes or more. This happens only on the first run.
Traceback.
Read its last line, which names the real problem:
NoCredentialsError: Unable to locate
credentials — no World Bank API key. See
below; it is a five-minute fix, and free.
Bad git executable — git is
missing; the surveys environment
is probably not active.
REPORT lines.
On the workshop server these lines returned almost instantly, because the data was already staged there. On your own machine the first run downloads it, so give it time.
['1987-88', '1988-89', '1991-92', '1998-99', '2005-06', '2012-13', '2016-17']
(246594, 8)That is the Ghana Living Standards Survey: seven rounds spanning thirty years, and 246,594 people in the household rosters. If you see those two lines, you are done — please reply and say so.
Lines mentioning DVC unavailable,
Falling back to manual aggregation,
Issue with categorical mapping, or
matplotlib building the font cache
are all normal. They are not errors. Only the red
Traceback blocks matter.
These are the three failures we expect. In every case, the answer is to fix it if it is quick, and otherwise to stop and tell us — you will not be behind.
The library needs Python 3.11 or newer. This usually means step 2
was skipped, or an older Anaconda is in charge. Check with
python --version; if it is below 3.11,
run step 2 again and make sure the line begins with
(surveys).
The library cannot even start without git.
It is installed in step 2, so this points to the same cause: the
surveys environment is not active.
Run conda activate surveys and try again.
The server has both the survey data and the tools already in place,
so it is not a fair comparison. On your own machine the last line is
the one that does real work, and it can fail for two quite different
reasons: it may simply still be downloading, or it may have stopped
with NoCredentialsError, which almost
always means no World Bank API key is configured. Check whether the
cell still shows [*], and read the last
line of any red traceback.
The message names the wrong thing. It nearly always means no World Bank Microdata API key is configured, so the library will not unlock the data it ships with. Get a key (see below), and either re-run the installer script or save it by hand.
A likely second cause, if you are sure you have a key: it was
written to the wrong place. The config file lives where
lsms_library.config._config_file() says
it does, and that differs by platform. Run those two lines in a
notebook and check the file is actually there.
Older versions needed the gpg binary,
which macOS and Windows do not ship — hence the Gpg4win step
that used to be on this page. Since LSMS_Library 0.10 the
decryption is pure Python, and
gpg is only a fallback for old wheels.
If you installed Gpg4win for this workshop you can leave it or
remove it; nothing here needs it.
It is free and takes a few minutes: microdata.worldbank.org. Register, then accept the terms of use for the LSMS collections.
The key is how your access is meant to be authorised. The surveys belong to national statistical offices and the World Bank, whose terms ask each user to accept them individually; holding your own key is how you do that. It is also what lets the library download from the World Bank directly, which is what you will need for anything the shared cache does not already carry — and the cache carries a fraction of the roughly forty countries the library covers.
microdata.worldbank.org/auth/register. Give your name, email, institution and a password.
You cannot log in until you click that link. Check your spam folder — this is where most people get stuck.
Log in, then click the person icon with your name at the top right and choose Profile. Direct link: microdata.worldbank.org/index.php/auth/profile.
Scroll past your details to a section headed API keys. It will say “No API keys found.” Click the blue Generate API key button beside that heading.
The key — 64 characters — appears in a box with the warning that it will not be shown again. That is literal. Afterwards you see only its first few characters. If you lose it, delete it and generate another.
To save the key on your own machine, paste this into a Jupyter cell, putting your key between the quotes:
import yaml
from lsms_library import config
key = "PASTE-YOUR-KEY-HERE"
dst = config._config_file()
dst.parent.mkdir(parents=True, exist_ok=True)
data = yaml.safe_load(dst.read_text()) if dst.exists() else {}
data = {**(data or {}), "microdata_api_key": key}
dst.write_text(yaml.safe_dump(data, sort_keys=True))
print("saved to", dst)Treat the key like a password. It carries your identity and your permissions — anyone holding it acts as you. Never put it in a shared notebook, an email, or a git repository.
It expires after a year, and you may hold at most five at a time. Both are visible on the same profile page.
A bad or expired key reports
LOGIN_REQUIRED, not “bad
key”. If downloads start failing that way, generate a fresh
one rather than assuming you are signed out.
Whether it worked or not, please send one line saying which computer you used and where you stopped. If you want to give us a more precise report, run the fuller check below instead of the five lines in step 4 — it tests each requirement separately and prints two lines to copy into your reply.
"""Workshop setup check: Analysis of Household Surveys, U. of Ghana.
Run this after installing. It checks each requirement in turn and
prints a short report. Nothing here changes your computer; it only
looks at what is already installed and downloads one small table.
Usage:
python check_setup.py
or paste the whole file into a Jupyter cell and run it.
"""
import platform
import shutil
import sys
RESULTS = []
def record(name, ok, detail=""):
RESULTS.append((name, ok, detail))
mark = "PASS" if ok else "FAIL"
line = f"[{mark}] {name}"
if detail:
line += f" -- {detail}"
print(line, flush=True)
return ok
def main():
print("=" * 62)
print("Analysis of Household Surveys -- setup check")
print("=" * 62)
print(f"System: {platform.system()} {platform.machine()}")
print(f"Python: {sys.version.split()[0]}")
print("-" * 62)
# 1. Python version -------------------------------------------------
ver = sys.version_info
record(
"Python 3.11 or newer",
ver >= (3, 11),
f"found {ver.major}.{ver.minor}.{ver.micro}"
+ ("" if ver >= (3, 11) else "; this is too old"),
)
# 2. git on PATH -----------------------------------------------------
git = shutil.which("git")
record(
"git is installed",
git is not None,
git or "not found -- the library cannot even start without it",
)
# 3. gpg on PATH -----------------------------------------------------
# Informational only. LSMS_Library decrypts its own S3 read credentials
# in pure Python (Fernet, via `cryptography', a hard dependency) as of
# GH #741; the bundled s3_reader_creds.enc ships in the wheel. The gpg
# binary -- which macOS and Windows do not ship -- is now only a fallback
# for wheels built before that file existed. Reporting its absence as a
# FAIL sent people off to install Gpg4win for no reason.
gpg = shutil.which("gpg") or shutil.which("gpg2")
record(
"gpg is installed (optional)",
True,
gpg or "not found -- fine; decryption is pure Python since LSMS_Library 0.10",
)
# 3b. the World Bank Microdata API key --------------------------------
# This is the real access gate: a valid key proves acceptance of the
# World Bank's terms of use, and is what unlocks both direct downloads
# and the shared S3 read cache.
try:
from lsms_library import config as _cfg
_key = _cfg.microdata_api_key()
record(
"World Bank Microdata API key",
bool(_key),
(f"configured ({_key[:6]}...)" if _key
else "not set -- data access will fail; see the install page"),
)
except Exception as exc: # noqa: BLE001 -- pre-import, report anything
record("World Bank Microdata API key", False, f"could not check: {exc}")
# 4. the library imports ---------------------------------------------
lib_ok = False
try:
import lsms_library as ll
lib_ok = record(
"lsms_library imports",
True,
f"version {getattr(ll, '__version__', 'unknown')}",
)
except Exception as exc: # noqa: BLE001 -- report anything to the student
record("lsms_library imports", False, f"{type(exc).__name__}: {exc}")
# 4b. pandas matches what THIS lsms_library asks for -------------------
# Do not hard-code a bound. 0.9.x needs pandas < 3; 0.10.1 and later need
# pandas > 3. Read the requirement from the installed package's own
# metadata so this check stays right across that change instead of
# failing a correct installation.
try:
import pandas as _pd
from importlib.metadata import requires as _requires
from packaging.requirements import Requirement as _Req
from packaging.version import Version as _V
_spec = None
for _r in _requires("LSMS_Library") or []:
_req = _Req(_r)
if _req.name.lower() == "pandas" and not _req.marker:
_spec = _req.specifier
break
if _spec is None:
record("pandas version suits the library", True,
f"pandas {_pd.__version__}; library states no bound")
else:
_ok = _V(_pd.__version__) in _spec
record(
"pandas version suits the library",
_ok,
f"pandas {_pd.__version__}, library wants pandas{_spec}"
+ ("" if _ok else '; run: pip install --upgrade LSMS_Library'),
)
except Exception as exc: # noqa: BLE001
record("pandas version suits the library", False,
f"{type(exc).__name__}: {exc}")
# 5. Jupyter ----------------------------------------------------------
jupyter_exe = shutil.which("jupyter")
jupyter_mod = False
for mod in ("jupyterlab", "notebook", "jupyter_server"):
try:
__import__(mod)
jupyter_mod = True
break
except Exception: # noqa: BLE001
continue
record(
"Jupyter is installed",
bool(jupyter_exe or jupyter_mod),
(jupyter_exe or "found as a Python package")
if (jupyter_exe or jupyter_mod)
else "not found -- run: pip install jupyterlab",
)
# 6. real data downloads ----------------------------------------------
if lib_ok:
print("-" * 62)
print("Downloading a table from the Ghana Living Standards Survey.")
print("This is the slow step -- one to several minutes on a first")
print("run, depending on your connection. Later runs are fast,")
print("because the data is cached on your machine.")
print("-" * 62, flush=True)
try:
import lsms_library as ll
ghana = ll.Country("GhanaLSS")
waves = ghana.waves
record("Ghana survey waves found", bool(waves), f"{len(waves)} waves")
roster = ghana.household_roster()
record(
"Ghana data downloads",
len(roster) > 0,
f"{len(roster):,} rows of household roster data",
)
except Exception as exc: # noqa: BLE001
record("Ghana data downloads", False, f"{type(exc).__name__}: {exc}")
# Report ---------------------------------------------------------------
print("=" * 62)
failed = [name for name, ok, _ in RESULTS if not ok]
if not failed:
print("ALL CHECKS PASSED. You are ready for the workshop.")
else:
print("SOME CHECKS FAILED:")
for name in failed:
print(f" - {name}")
print()
print("This is normal and it is not a problem. The workshop runs")
print("on a shared server, so you can take part either way.")
print()
print("Please copy the two lines below into your reply:")
print("-" * 62)
print(f"REPORT {platform.system()}/{platform.machine()} py{ver.major}.{ver.minor}")
print("REPORT " + ("all-passed" if not failed else "failed: " + ", ".join(failed)))
print("-" * 62)
if __name__ == "__main__":
main()