Data model

expdpy follows ExPanDaR’s panel-data model. This page describes how to structure both the data and its data dictionary (df_def) so that every figure and table renders clearly — in particular, so axes, legends and headers carry full, human-readable labels rather than bare column names.

The data: a tidy panel

Your data frame should be tidy (long):

  • one row per entity × time observation (e.g. one row per country-year);
  • one column per variable — the entity id(s) and the time id are ordinary columns, sitting alongside the variables you analyse;
  • balanced or unbalanced panels both work (units may enter, exit or have interior gaps), and a pure cross-section simply omits the time column.

A few rows of the bundled kuznets panel illustrate the shape — each (country, year) pair is one row, every variable its own column:

country year gini_regional gdp_pc log_gdp_pc
country 1 2015 0.31 812 6.70
country 1 2016 0.30 845 6.74
country 2 2015 0.12 41 980 10.65

Do not pre-aggregate or pivot to wide form — the Explore and Analyze functions reshape internally (melting, grouping, demeaning) and need the long layout.

Identifiers

  • entity — one or more columns identifying the cross-section / unit (e.g. country + ISO code).
  • time — a single column identifying the time dimension, coercible to an ordered factor (e.g. fiscal year).

Declare them once with set_panel(df, entity=..., time=...), pass them directly to a function, or describe them via a df_def table (below) and let set_labels(df, df_def, set_panel=True) declare them for you.

The data dictionary (df_def)

df_def is a small DataFrame with one row per variable you want to expose. At a minimum include the entity row(s) and the time row (so panel views know the identifiers), plus the numeric/factor variables you analyse. Variables absent from df_def simply fall back to default type inference and their bare column name.

Its columns:

column required? meaning
var_name required the column name in the data (must match exactly)
type required one of entity, time, factor, logical, numeric (made authoritative for the app’s variable menus)
label recommended a concise, human-readable label used for axis titles, legends and table headers (with units where helpful, e.g. "GDP per capita (USD)")
var_def optional a longer description (or, for var_def tables, an expression). Used as the label only when label is absent
role optional marks the key variables: one outcome, any number of covariate, and one entity_name (the column holding readable unit names). Blank for everything else
can_be_na optional if False, rows missing this variable are dropped from the analysis sample (only the entity/time ids are required by default)
from expdpy.data import load_kuznets_data_def

# columns: var_name, var_def, label, type, role, can_be_na
load_kuznets_data_def()  # entity (country, iso), time (year), factors and numerics

The bundled *_data_def tables (kuznets, gapminder, firms, staggered_did) ship a curated label for every variable.

Key variables: main outcome & covariates

Mark the variable you most care about as the main outcome and the explanatory variable(s) of interest as covariates — in the dictionary’s role column, or directly with set_roles(df, outcome=..., covariates=[...]). Once declared, the figures, tables and apps default to these key variables instead of the first numeric column:

  • a scatter plots the covariate against the outcome; a regression uses the outcome as the dependent variable and the covariates as the regressors;
  • single-variable views (histogram, trend, extreme observations, spaghetti, …) default to the outcome, and the descriptive / correlation tables lead with the key variables;
  • in the apps, the key variables are pre-selected and floated to the top of every dropdown.

The bundled *_data_def dictionaries already mark their key variables, so the defaults work the moment you attach them — and set_roles declares the same for your own data:

import expdpy as ex
from expdpy.data import load_kuznets, load_kuznets_data_def

# kuznets ships role=outcome on gini_regional and role=covariate on the log-GDP terms
df = ex.set_labels(load_kuznets(), load_kuznets_data_def(), set_panel=True)
ex.explore_scatter_plot(df).fig          # x = log_gdp_pc, y = gini_regional
ex.analyze_regression_table(df).etable    # the cubic Kuznets curve, by default

# declare the same on a frame of your own
df = ex.set_roles(df, outcome="gini_regional", covariates=["log_gdp_pc"])

Entity names vs entity ids

A panel keys on an entity id (e.g. an ISO code or a numeric province id), but a separate column often holds a readable name for each unit (e.g. the country or province name). Tag that column role = entity_name (or let build_data_def auto-detect it — any text column that is constant within, and ~1:1 with, the entity id), and every figure that labels individual units shows Name (id) — e.g. Bolivia (BOL) in a spaghetti legend or a hover tooltip — by reading df.attrs. No per-figure arguments are needed; the .df you get back keeps the raw ids.

Readable labels in figures and tables

Readability is a key component of any figure or table, so the best practice is to label with the data dictionary. Attach the labels once and every downstream figure and table picks them up automatically:

import expdpy as ex
from expdpy.data import load_kuznets, load_kuznets_data_def

df = ex.set_labels(load_kuznets(), load_kuznets_data_def(), set_panel=True)

ex.explore_trend_plot(df, var="gini_regional").fig   # y-axis: "Regional inequality (Gini)"
ex.explore_descriptive_table(df).gt                  # rows labelled, not "gini_regional"

set_labels stores the labels on df.attrs (just as set_panel stores the panel ids and set_roles stores the key variables), so they survive across calls — though, like the panel ids, some pandas operations drop attrs; re-apply set_labels / set_panel / set_roles after such steps, or pass the values explicitly. One call does it all: set_labels(df, df_def, set_panel=True) reads the dictionary’s labels and declares the panel, the entity_name, and the outcome / covariate roles it carries.

The label for each variable is resolved in order of preference:

  1. an explicit per-call label= / labels= argument (always wins),
  2. the label stored on df.attrs (from set_labels),
  3. the variable’s label column in df_def, then its var_def,
  4. finally the bare var_name when no readable label exists.

So a dictionary without a label column still works (it falls back to var_def, then the name), and a data frame with no labels at all simply shows column names — labelling is a readability upgrade, never a requirement. The returned .df / .df_corr of every result keeps the raw variable names; only the displayed axes and headers use the labels.

Variable categories

Inside the app, columns are classified for the selectors: a factor is categorical/object or a numeric column with at most factor_cutoff (default 10) distinct values; a two-level column has exactly two values; the rest are numeric or logical. When the dictionary declares a type, that declaration is authoritative — a numeric column you tag factor becomes a grouping variable, for instance, regardless of its dtype. The marked outcome and covariates are also floated to the top of the matching selectors.

Time-axis conversion

The time-trend functions coerce the time id for nicer ticks: full date strings become dates, numeric-looking values (including factor years like "2013") become numbers, and anything else becomes an ordered categorical — matching ExPanDaR’s try_convert_ts_id.