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×timeobservation (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
timecolumn.
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 numericsThe 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:
- an explicit per-call
label=/labels=argument (always wins), - the
labelstored ondf.attrs(fromset_labels), - the variable’s
labelcolumn indf_def, then itsvar_def, - finally the bare
var_namewhen 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.