Changelog
0.5.2 (2026-07-01)
Calmer animation controls + a richer Explore app.
- The animated views (scatter, distribution-over-time, box, strip, treemap, sunburst) now carry a single unobtrusive ▶ button in the bottom-left corner, beside the time slider (pause by dragging the slider) instead of a Play/Pause pair above the plot.
explore_strip_plotnames each point’s entity in the hover box (“Name (id)” when a readable name is declared).- The Explore Streamlit app gains full controls for the box/strip/composition charts (log scale, ordering, orientation, box points, strip opacity & sampling, treemap/sunburst leaf sampling), copy-paste “Export & reproduce” code snippets for each, and an Animated views callout; the Composition page now sits right after By group.
- The Explore docs page & notebook demonstrate the new parameters and the box/strip
.interpret().
0.5.1 (2026-07-01)
Richer animated exploration for panel data. Four new time-animated Explore views make “watch this evolve year by year” a one-call operation, each degrading gracefully to a static figure when no time id is available:
explore_treemap_plot/explore_sunburst_plot— hierarchical composition (a flexiblepath=[...], defaulting to the panel entity) sized by a value and colored by another, animated over time with a fixed color scale.explore_box_plot/explore_strip_plot— distribution-by-group box and strip plots that animate over time, with a pinned value axis, an optional log scale, and (for strip) point sampling on large panels. Both carry a plain-language.interpret().
Consistency + polish. All animated figures now share one control surface via a new internal _animate helper (unified “▶ Play” / “⏸ Pause” buttons, frame speed, and a time-labelled slider), and explore_animated_scatter_plot gains log_x / log_y toggles (the canonical Gapminder log-income axis).
0.5.0 (2026-06-29)
expdpy is now first-class for LLMs and AI agents. A new agent-facing surface lets you point an LLM at the library and have it work correctly — carrying expdpy’s econometric guardrails (associations, not causation; entity/time panel vocabulary; the .interpret() / explain() contract). Everything derives from one source of truth (expdpy._meta), so the agent surface cannot silently drift from the API.
Added
llms.txt+llms-full.txt(llmstxt.org convention) at the site root: a curated, guardrailed index of every function, article, dataset and concept, and a full-text concatenation for one-shot context. Every docs page also gets a clean Markdown twin at<page>.html.md.- MCP server (
pip install "expdpy[mcp]", console scriptexpdpy-mcp): a stdio Model Context Protocol server exposing a curated set of Explore/Analyze tools plusexplain/list_topics/learn_concept. Each tool returns the result’s.interpret()reading, its tables, and a saved figure path. Pass data by bundled-dataset name, file path, or inline records. - Static tool schemas: committed Anthropic and OpenAI function-calling JSON (published under
/tools/), generated from the same registry the MCP server uses. use-expdpyagent skill (.claude/skills/use-expdpy/) and the For AI / LLMs hub page: an end-user skill that teaches an agent to declare the panel, pick the right function, read the result contract, and interpret responsibly.
All artifacts are regenerated in the docs build and guarded by a CI freshness check. The change is purely additive — no existing API changes.
0.4.23 (2026-06-29)
The data dictionary (df_def) becomes a first-class driver of labels, key variables and entity names — across both the library and the no-code apps. Additive and backward-compatible: a dictionary without the new column behaves exactly as before.
Data dictionary — a new role column
df_defgains a sixth column,role, with valuesoutcome/covariate/entity_name(blank otherwise). Mark the main outcome and main covariate(s) once, and the figures, tables and apps default to them; mark anentity_namecolumn (a readable label for each unit) and figures render units asName (id)(e.g.Bolivia (BOL)).build_data_defnow emits therolecolumn and auto-detects the entity-name column (a text column that is constant within, and ~1:1 with, the entity id).- Every bundled
*_data_defships curated roles, so the flagship datasets demonstrate the defaults out of the box — loadingkuznetsdefaults a regression to the cubic Kuznets curve,bolivia112_gdppclabels provinces asName (id), and so on. - New
set_roles(df, outcome=, covariates=)utility (stored ondf.attrs, likeset_panel/set_labels);set_panelgains anentity_name=argument; andset_labels(df, df_def, set_panel=True)now also declares the roles + entity-name a dictionary carries.
Library — figures and entry functions use the metadata
- Spaghetti, panel-structure / value heatmaps, within-persistence, within/between scatter, animated scatter, β-convergence, convergence clubs, Kuznets waves and the extreme-observations table now show
Name (id)when an entity-name column is declared. - When their primary variable is omitted,
explore_scatter_plot(x = first covariate, y = outcome),analyze_regression_table(dv = outcome, idvs = covariates),explore_histogramandexplore_trend_plotdefault to the declared roles; the descriptive and correlation tables lead with the key variables.
Apps — labels, defaults and the dictionary editor
- Variable menus now display
Label (var_name)when the dictionary provides labels; the selected value stays the raw column name. - The marked outcome / covariate(s) are pre-selected as defaults and floated to the top of each dropdown.
- The data-dictionary editor gains a Role selector, the declared
typeis now authoritative for the variable categories, andcan_be_na = Falseis enforced (rows missing a required variable are dropped from the analysis sample).
0.4.22 (2026-06-27)
A teaching + internals release: four new concept sandboxes, an internal helper refactor, and Learn-app polish. Additive and backward-compatible.
Learn — four new concept sandboxes
learn_hausman_test— a panel whose regressor is correlated with the unit effects, so random effects is biased and the Hausman test rejects it in favour of fixed effects.learn_correlated_random_effects— the Mundlak device, whose slope on the original regressor equals the fixed-effects estimate, with the Mundlak (regression-form Hausman) test.learn_nickell_bias— the within estimate of a lagged dependent variable is biased downward in short panels and the bias shrinks asTgrows.learn_measurement_error— classical attenuation: noise in a regressor pulls its OLS coefficient toward zero by the reliability ratio.
Each ships an explainer (nickell_bias and measurement_error are new), a .interpret() reading, and now a .data field — the raw simulated frame (every sandbox carries it).
Learn app
- The concept sandboxes are now a lazy picker (one selectbox) instead of nine eager tabs — only the chosen sandbox simulates and renders.
- The explainer index is searchable (filter by name, title or description).
- Each sandbox offers a CSV download of its simulated dataset.
0.4.21 (2026-06-27)
A low-risk polish release: a broader teaching layer, consistent missing-data reporting, and opt-in accessible theming. All additive and backward-compatible.
Learn — .interpret() on nine more result types
- Plain-language
.interpret()now ships on the result types that lacked it:analyze_coefficient_plot,analyze_fixef_plot,analyze_predictions,analyze_joint_test,analyze_robust_inferenceandanalyze_panel_view(Analyze), plusexplore_scatter_plot,explore_missing_values_plotandexplore_value_heatmap(Explore). Each reads the result in associational, non-causal language (the existinganalyze_joint_test().summary()is unchanged).
Explore — consistent missing-data reporting + clearer errors
- Functions that drop rows with missing values now emit a single advisory
ExpdpyWarning(“dropped N of M rows … with missing values in […]”) instead of dropping silently, across the Explore and Analyze surfaces. Silence one category-wide withwarnings.filterwarnings("ignore", category=expdpy.ExpdpyWarning). - Several vague panel/validation errors now name the offending column(s) and disambiguate “entirely missing” from “no overlapping observations”.
Theming — colorblind-safe palette + unified title API
set_palette("colorblind")/get_palette()— an opt-in, process-global toggle to the Okabe-Ito colorblind-safe qualitative palette plus colorblind-safe sequential and diverging scales. The default look is unchanged until you opt in.- Every Explore plotting function and table now accepts
title=/subtitle=; tables keepcaption=working as a backward-compatible alias. Known limitation: a few decorative fixed-color lines do not follow the palette toggle.
0.4.20 (2026-06-27)
Analyze — instrumental variables, panel IV, and marginal effects
analyze_iv_regression— instrumental-variables / two-stage least squares (2SLS) via pyfixest’s native IV formula, with fixed effects, clustered standard errors, and the first-stage weak-instrument F.analyze_panel_iv_regressionis the panel-aware companion (theentity/timevocabulary, two-way fixed effects and cluster-by-entity by default — the equivalent of Stata’sxtivreg2 ... fe). Both carry.interpret()and aninstrumental_variablesconcept explainer.analyze_marginal_effects_plot— for a model with an interactionfocal * moderator, traces the marginal effect of the focal regressor across the moderator’s range with a delta-method confidence band, plus the average marginal effect.
Explore — an animated bubble scatter
explore_animated_scatter_plot— a Gapminder-style time-slider bubble chart: each unit is a bubble whose position, size and color move across periods, with a play button and pinned axes so motion is comparable over time.
Data — two canonical IV teaching datasets
load_colonial_origins— the Acemoglu-Johnson-Robinson (2001) “Colonial Origins” 163-country cross-section. The 64-countrybase_samplereproduces the famous instrumented expropriation-risk slope of ≈ 0.94.load_regional_conflict— a focused subset of the African region-year conflict panel (night-lights instrumented by lagged rainfall and drought, region + year fixed effects); the first-stage F reproduces the published 24–40 range.
Apps — exports, copy-code, and a results-comparison panel
- Every chart and table now has an “⬇ Export & reproduce” expander — download the figure as a self-contained interactive HTML file (or the table as CSV) and copy the exact expdpy code that reproduces the view.
- Save & compare regression specifications. On the Regression page, “➕ Save spec for comparison” stashes the current model; saved specs render side by side as one table and a dodged coefficient plot.
0.4.19 (2026-06-26)
Apps — a richer, more robust sample selector
- The Sample menu now filters by category, by value range, and by period. Pick one or more factor variables and the categories to keep; one or more continuous variables and a min–max range; and a dedicated Period slider over the time id. All filters combine (AND), and an always-visible Active sample summary (dataset · period · filters · “rows kept = n / N”) with a Reset filters button keeps you oriented on every page.
- Single-year → cross-section. Collapse the Period slider to one year and the app switches to cross-sectional mode — the panel / over-time views (within-between, trends, dynamics, convergence, event-study, panel-models, missing-by-time) hide with a notice, and the cross-sectional views keep working.
- Fixed the disappearing-selection bug. Filter options are now computed from the pre-subset frame, so choosing
continent = Xno longer collapses the factor and resets the selection — selections persist across pages, and every analysis updates as you filter. - Missing-values view no longer looks blank. When a sample has no missing values (e.g. the unbalanced firms panel), it now says so clearly and points to Panel structure for the structural gaps, instead of drawing an all-zero heatmap. (A mixed-type balance-summary table also renders cleanly now.)
- Export reproduces the filters as code. “Export notebook + data” now ships the unfiltered working frame plus a runnable Subset the sample cell (
df = df[… isin/between …]) and the outlier-treatment step, so the notebook rebuilds the exact analysis sample from the full data.
0.4.18 (2026-06-26)
Apps — the two-file (data + dictionary) workflow
- The ExPdPy apps now revolve around two files: a data file and a data dictionary (
df_def). Upload your own data and (optionally) its dictionary, and every figure and table updates with the right labels and panel structure. Upload data only and the app auto-builds an editable dictionary in the sidebar — set theentityandtimerows to unlock the panel views, tidy the labels, then Apply. A new public helper,build_data_def, infers that dictionary (types + entity/time) from any frame, so the same workflow is available in notebooks:ex.set_labels(df, ex.build_data_def(df), set_panel=True). - The data dictionary is applied throughout — labelled axes/legends/headers now render for the bundled datasets too (previously the labels were never attached in the app), and switching datasets resets stale variable selections cleanly. This fixes views that could silently disappear on an upload (e.g. the missing-values map) because the panel was never declared.
- The dataset picker now offers every bundled dataset —
ProductivityandBolivia (provinces)join Kuznets, Gapminder, Staggered DiD and Firms. - Export now ships the dictionary, and the notebook is Colab-ready. “Export notebook + data” writes
expdpy_data_def.csvalongside the sample, and the generated notebook reloads both withex.set_labels(df, data_def, set_panel=True). The.ipynbis a Google Colab notebook: a pinned install cell (expdpy==<version>, NumPy/Numba upgrade) restarts the runtime once so NumPy loads cleanly, with markdown explaining the setup. The.pyscript stays a plain local-run script.
0.4.17 (2026-06-25)
Learn
- The Learn page is now a single-pass tutorial — “the ideas behind the case study.” A complete redesign opens by interpreting a real two-way fixed-effects Kuznets model (
.interpret()/.explain()), browses the full concept index (list_topics()— 27 topics — grouped by theme), then isolates each idea in a simulated sandbox where the truth is known: the within-transformation identity (learn_first_differences,learn_within_vs_lsdv), why fixed effects matter (learn_pooled_vs_fixed_effects), two inference classics (learn_omitted_variable_bias,learn_clustering_se), convergence (learn_beta_convergence,learn_sigma_convergence,learn_convergence_clubs), and the Kuznets wave (learn_kuznets_waves). Everylearn_*sandbox now appears once each, removing the old tour/gallery duplication and adding the previously-undemonstratedlearn_kuznets_waves. - The Learn Streamlit app mirrors the tutorial. The Concept sandboxes page grows from seven to nine tabs — adding
learn_sigma_convergenceandlearn_convergence_clubs— reordered to the case-study sequence. The Concept explainers page browses all 27 topics. - The Learn Colab notebook is regenerated so the website, notebook and app present the same sequence.
0.4.16 (2026-06-25)
Analyze
- The Analyze page is now a single-pass Kuznets case study. A complete redesign walks newcomers through all 17
analyze_*functions once each, in the order an analyst actually works: fit a first model and add fixed effects (analyze_regression_table,analyze_coefficient_plot,analyze_fwl_plot) → enrich the estimation (analyze_estimation) → read the fitted model (analyze_predictions,analyze_fixef_plot,analyze_joint_test) → stress-test the inference (analyze_robust_inference) → choose the panel estimator (analyze_panel_table,analyze_hausman_test,analyze_cre_table) → the flagship Kuznets-waves curve (analyze_kuznets_waves) → a related income-convergence question (analyze_beta_convergence,analyze_sigma_convergence,analyze_convergence_clubs) → a causal design (analyze_panel_view,analyze_event_study). The four previously-undemonstrated functions (the convergence trio and Kuznets waves) now appear in the narrative. - The Analyze Streamlit app mirrors the case study. Its pages are reorganized to the same workflow — Regression, Post-estimation (new), Panel models, Kuznets waves, Convergence (β / σ / clubs consolidated), Event study & DiD — surfacing the seven functions the app did not previously render (
analyze_estimation,analyze_predictions,analyze_fixef_plot,analyze_joint_test,analyze_robust_inference,analyze_beta_convergence,analyze_panel_view). Results only; plain-language readings sit behind a collapsed expander. - The Analyze Colab notebook is regenerated from the redesigned page, so the website, notebook and app present the same sequence of tools.
0.4.15 (2026-06-25)
Explore
- The Explore page is now a single-pass Kuznets case study. A complete redesign walks newcomers through all 21
explore_*functions (and theset_panel/resolve_panel/treat_outliersutilities) once each, in the order an analyst actually works: know the panel’s skeleton (explore_panel_structure,explore_missing_values_plot,explore_value_heatmap) → describe variables → split within vs between variation (explore_xtsum_table,explore_spaghetti_plot) → trends (incl.explore_distribution_over_time) → compare groups → relationships and the N-shaped curve (incl.explore_scatter_plot_within_between) → dynamics (explore_transition_matrix,explore_within_persistence). The eight previously-undemonstrated panel-aware functions now appear in the narrative. - The Explore Streamlit app mirrors the case study. Its pages are reorganized to the same workflow — Overview & Data, Describe variables, Within & between, Trends, By group, Relationships, Dynamics — replacing the single catch-all “Panel structure” page. The Colab notebook is regenerated from the redesigned page.
0.4.14 (2026-06-24)
Explore
explore_scatter_plotno longer draws a LOESS confidence band. The shaded band was an unweighted bootstrap while the LOESS line is size-weighted underloess=2, so the line could fall outside its own band; the smoother now shows just the line.explore_spaghetti_plotno longer shows a legend. It was a single “mean (…)” entry that ate horizontal plot space; highlighted units still render in saturated colour.
0.4.13 (2026-06-24)
Explore
explore_descriptive_tableis now panel-aware. When atimecolumn is known (declared viaset_panel/set_labels, or passed explicitly) each statistic is shown by period — by default at the first and last period — under a spanning column header (e.g.Meanover2015and2025); without a time dimension it falls back to one column per statistic. The default statistics are now Mean, Std. dev., Median, Min., Max., rows are labelled from the data dictionary, and the notes report the number of observations and any variable with missing data. Breaking: the old length-8digitsvector is replaced by astats=selection (any of the eight statistics), a scalar-or-mappingdigits=, and a newperiods=argument; the result gains a tidy.by_periodframe (.dfstill carries all eight pooled statistics).explore_histogramgains opt-in density overlays. Newkde=andnormal=flags draw a Gaussian kernel-density estimate and/or a normal curve on the Density scale (off by default; the Count/Density toggle hides them in Count view).- The trend / time-series plots no longer show a draggable range slider (
explore_trend_plot,explore_quantile_trend_plot,explore_spaghetti_plot).
Data dictionary (df_def) everywhere
- Every function now leans on the data dictionary for readable output, while still working without it. Regression / estimation / CRE tables relabel their coefficient and dependent-variable rows from the dictionary (the tidy
.dfkeeps raw term names), and the panel estimators (analyze_panel_table,analyze_hausman_test,analyze_cre_table) and DiD views (analyze_event_study,analyze_panel_view) now resolveentity/time/unitfrom the declared panel, so those arguments can be omitted afterset_panel/set_labels. - Examples across the library now illustrate the dictionary, opening with
df = ex.set_labels(load_kuznets(), load_kuznets_data_def(), set_panel=True).
0.4.12 (2026-06-24)
Docs
- The API reference now shows live output and the source code of every function. Each reference page renders its docstring examples as executed cells — interactive Plotly figures, Great Tables and data frames appear directly below the code — and carries a source link to a new splot-style source page (
reference/modules/) that lists the full, syntax-highlighted module source, anchored at the function (with a back-link to the docs). Two build steps (tools/build_source_pages.py,tools/build_reference_enrichment.py) run insidedocs-buildbetweenquartodoc buildandquarto render. - The API reference index is easier to scan. Each function now shows a brief, splot-style argument signature beside its name (e.g.
analyze_beta_convergence(df, var[, controls, …])), and the distracting link underline is dropped from the listing.
0.4.11 (2026-06-23)
Fixed
- Convergence clubs (
analyze_convergence_clubs) — three correctness fixes to the Phillips-Sul clustering, plus a reporting fix:- The default
method="adjust"(Schnurbus et al. 2016) club refinement scored each candidate against the core group rather than the growing club and had no final joint-test fallback, so it could emit a group labelled a convergence “club” whose own log(t) t-statistic was below the threshold. It now scores against the accumulating club and falls back to the core when the refined club still fails its joint test (matching themethod="ps"branch). - A variable whose per-period cross-sectional mean is at or near zero (e.g. a demeaned, centered or growth series) made the relative transition
h_it = x_it / mean_i(x_it)blow up toinfand silently corrupt every returned frame and figure. It now raises a clear error. - A constant / already-identical panel produced a non-finite global log(t) statistic that was silently reported as “divergent” (printing a literal
NaN). It now raises a clear “not estimable” error. - The user-supplied
tcritthreshold is now threaded into the summary table’sconvergingcolumn, the table source-note and the plain-language.interpret()(all previously hardcoded to-1.65), and is exposed onConvergenceClubsResult.tcrit.
- The default
0.4.10 (2026-06-23)
Added
- Kuznets-waves analysis (
analyze_kuznets_waves). Tests the extended Kuznets curve — the inequality-development relationship taken up to a quartic,gini = b_1 g + b_2 g^2 + b_3 g^3 + b_4 g^4withg = logGDP per capita — under three panel estimators laid out side by side: pooled OLS, the between estimator (the cross-country curve, a polynomial in the entity means) and the within estimator (two-way country + year fixed effects). Each is a cumulative-stepwise (csw) comparison table — the linear model, then the quadratic, up to the fulldegree-order polynomial — and three figures tell the pooled → between → within story: a raw scatter with the pooled wave overlaid, and between and within Frisch–Waugh–Lovell partial-residual (component) plots that draw the fitted wave once optionalcontrols(and, for the within view, the two-way fixed effects) are partialled out. The result exposesgt_pooled/gt_between/gt_within, the three figures, a per-estimator curvaturesummary(turning points, peak, top-order term), the fittedmodels, and.interpret()/.explain(). A pairedlearn_kuznets_waves()sandbox, akuznets_wavesconcept explainer, a Streamlit Kuznets waves tab, and a Quarto → Colab notebook ship with it.
0.4.9 (2026-06-23)
Added
- Data: a new bundled
bolivia112_gdppcdataset — a real-world balanced panel of 112 Bolivian provinces (nested within 9 departments) over 1990-2024 with GDP per capita and its natural log; the empirical counterpart to the syntheticproductivitypanel, for the convergence workflows (analyze_beta_convergence/analyze_sigma_convergence/analyze_convergence_clubs) and general subnational exploration. Load withload_bolivia112_gdppc()/load_bolivia112_gdppc_data_def(). Source: Kummu, Kosonen & Masoumzadeh Sayyar, “Downscaled gridded global dataset for GDP per capita PPP over 1990-2022,” Sci Data 12, 178 (2025), https://doi.org/10.1038/s41597-025-04487-x.
0.4.8 (2026-06-22)
Added
- Club-convergence analysis (
analyze_convergence_clubs). A faithful Python port of the Phillips & Sul (2007/2009) log(t) test and data-driven clustering algorithm (the Statapsectapackage). It runs the full workflow on one variable: a per-unit Hodrick-Prescott filter (lambda = 400for annual data), the relative transition pathh_it = x_it / mean_i(x_it), the log(t) regression test for the whole panel and — when global convergence is rejected — the clustering algorithm that splits units into convergence clubs, followed by merging of adjacent clubs that jointly converge. The log(t) statistic uses the Phillips-Sul scalar long-run-variance HAC (Andrews 1991 quadratic-spectral kernel with an AR(1) automatic bandwidth), hand-coded in NumPy to match the reference since standard OLS engines do not provide it. The result exposes a tidy long frame (withrelativeandclub), three figures (within-club averages, all paths by club, and per-club small multiples), a classification table and anentity -> clubmembership frame. Configurable trimmingr, sievemethod(Schnurbus-adjusted or original PS),mergemode and HPfilter. - Learn: a runnable
learn_convergence_clubssandbox plants a known club structure and shows the algorithm recover it, and anexplain("convergence_clubs")concept explainer covers the log(t) test and clustering. A new “Convergence clubs” page in the Analyze app exposes the analysis on your data. - Data: a new bundled
productivitydataset — a balanced 108-country × 25-year Penn World Table panel of (raw) log GDP per capita and log labor productivity with region / income grouping factors — for the club-convergence workflow (load_productivity).
Fixed
analyze_beta_convergence: a conditional control that is constant or collinear with the initial level was silently dropped by the estimator, so the reported “conditional” fit was identical to the unconditional one with no warning. The conditional model is now skipped with an explicit note naming the offending control(s), rather than mislabelling the unconditional slope as conditional.analyze_beta_convergence: duplicate(entity, time)rows were de-duplicated with an order-sensitive, NaN-blind rule, so a missing-valued duplicate ordered first could silently evict a valid observation. De-duplication now keeps the first non-missing value per cell (matchinganalyze_sigma_convergence/analyze_convergence_clubs).
0.4.7 (2026-06-22)
Added
- Sigma-convergence analysis (
analyze_sigma_convergence). It tracks the cross-sectional dispersion of a panel variable over time — the standard deviation, the Gini index and the coefficient of variation — and tests whether that dispersion shrinks by regressing the log dispersion on time (a negative trend is σ-convergence). The headline figure is a dual-axis time series with the standard deviation on the left axis and the Gini index on the right. The variable is used as supplied and the panel must be balanced; the Gini and coefficient of variation degrade gracefully (toNaNwith a note) on negative values or a near-zero mean. - Learn: a runnable
learn_sigma_convergencesandbox and anexplain("sigma_convergence")concept explainer demonstrate σ-convergence on a geometric-narrowing panel whose dispersion shrinks at a known log-rateln(rho), which the function recovers exactly. A new “Sigma convergence” page in the Analyze app exposes the same analysis on your data.
0.4.6 (2026-06-22)
Added
- Beta-convergence analysis (
analyze_beta_convergence). From a single panel variable it runs the standard cross-country β-convergence workflow: the unconditional growth-vs-initial-level scatter (with the country on hover and a regression annotation), the conditional version that partials out steady-state controls via the Frisch–Waugh–Lovell theorem, a comparison table reporting the slope, the speed of convergence λ and the half-life, and a rolling fixed-width-window view of how the convergence slope evolves. The variable is used as supplied (pass log GDP per capita for the canonical case), so it works for income, schooling, health and any other catch-up question. - Learn: a runnable
learn_beta_convergencesandbox and anexplain("beta_convergence")concept explainer demonstrate absolute vs. conditional convergence on a known-parameter panel — the unconditional slope is biased by an omitted steady-state determinant; conditioning on it recovers the truth. A new “Beta convergence” tab in the Learn app exposes the same demo.
Fixed
- Colab notebooks install cleanly on a fresh runtime. Because expdpy needs
numpy>=2.1but Google Colab pre-installs an oldernumbathat capsnumpy<2.1, the notebooks’ install cell hit a numpy/numba dependency conflict. The cell now co-resolvesnumba>=0.61in the same pip pass, so pip upgrades both to a compatible pair (numpy 2.4.x + numba 0.65.x). Restart the runtime if Colab prompts after the install.
0.4.5 (2026-06-21)
Fixed
- Saturated / Sun–Abraham event study crashed on older NumPy (e.g. Google Colab’s default NumPy 2.0.x):
analyze_event_study(..., estimator="saturated")raisedTypeError: reshape() got an unexpected keyword argument 'shape'. The underlyingpyfixestestimator uses NumPy 2.1’snp.reshape(..., shape=...), so expdpy now requiresnumpy>=2.1(was>=1.24). In Colab, re-run the install cell and restart the runtime when prompted.
0.4.4 (2026-06-21)
Added
- Human-readable labels on every figure and table. Variables can now carry a concise
label(a new column in the data dictionary,df_def); declare them once withset_labels(df, df_def, set_panel=True)and every Explore/Analyze axis title, legend title and table header reads with the full label (e.g. “Regional inequality (Gini)”) instead of the bare column name, falling back tovar_defand then the name when no label exists. The four bundled datasets (kuznets,gapminder,firms,staggered_did) ship curated labels. The returned.df/.df_corrkeep the raw names. See the data model page for how to structure the data and dictionary. - “Run in Google Colab” call-out on every module page.
explore,analyzeandlearnnow show the official Colab badge under the title, opening the matchingnotebooks/<slug>.ipynbstraight from GitHub — no install required. A Colab badge was also added to the README badge row and to each module card on the docs landing page.
Fixed
explore_trend_plotnow labels its y-axis (it was blank): a single variable shows that variable’s label, several variables show “Value”.- Notebook title timestamp. The generated Colab notebooks showed the literal
{BUILD_STAMP}instead of the build time; the title cell now readsbuilt <YYYY-MM-DD HH:MM UTC>, so each notebook states when it was last regenerated.
Changed
- CI now guarantees the committed notebooks never drift from their
.qmdsources. A new docs-workflow job rebuilds the notebooks and fails if they differ (ignoring the per-build timestamp line).
0.4.3 (2026-06-21)
Changed
- Trend range sliders are now a near-invisible sliver. The shared
blank_rangesliderhelper renders a much thinner strip (thickness0.06 → 0.02) in a near-white#FAFAFAfill, so the slider beneathexplore_trend_plot,explore_quantile_trend_plot, andexplore_spaghetti_plotstays draggable but no longer competes with the chart.
0.4.2 (2026-06-21)
API-wide function rename. Every analysis function is now prefixed with its module, so the name tells you which workflow it belongs to and reads consistently across the library. This is a breaking change with no backward-compatible aliases — update call sites to the new names using the map below.
Changed (breaking)
prepare_*→explore_*/analyze_*, andsandbox_*→learn_*. Plotly-figure functions now end in_plotand Great-Tables functions in_table, and scope qualifiers move to the end of the name (e.g.prepare_by_group_violin_graph→explore_violin_plot_by_group).- Utilities keep their current names and are grouped under a new Utilities heading in the API:
set_panel,resolve_panel,treat_outliers,explain,list_topicsare unchanged. - Result dataclasses, the
ExploreApp/AnalyzeApp/LearnAppclasses, and all function parameters are unchanged.
Rename map
Explore
| Old | New |
|---|---|
prepare_descriptive_table |
explore_descriptive_table |
prepare_correlation_table |
explore_correlation_table |
prepare_ext_obs_table |
explore_ext_obs_table |
prepare_xtsum_table |
explore_xtsum_table |
prepare_histogram |
explore_histogram |
prepare_bar_chart |
explore_bar_plot |
prepare_correlation_graph |
explore_correlation_plot |
prepare_trend_graph |
explore_trend_plot |
prepare_quantile_trend_graph |
explore_quantile_trend_plot |
prepare_by_group_bar_graph |
explore_bar_plot_by_group |
prepare_by_group_trend_graph |
explore_trend_plot_by_group |
prepare_by_group_violin_graph |
explore_violin_plot_by_group |
prepare_missing_values_graph |
explore_missing_values_plot |
prepare_scatter_plot |
explore_scatter_plot |
prepare_within_between_scatter |
explore_scatter_plot_within_between |
prepare_spaghetti_graph |
explore_spaghetti_plot |
prepare_value_heatmap |
explore_value_heatmap |
prepare_panel_structure |
explore_panel_structure |
prepare_distribution_over_time |
explore_distribution_over_time |
prepare_transition_matrix |
explore_transition_matrix |
prepare_within_persistence |
explore_within_persistence |
Analyze
| Old | New |
|---|---|
prepare_regression_table |
analyze_regression_table |
prepare_estimation |
analyze_estimation |
prepare_fwl_plot |
analyze_fwl_plot |
prepare_coefficient_plot |
analyze_coefficient_plot |
prepare_panel_table |
analyze_panel_table |
prepare_cre_table |
analyze_cre_table |
prepare_hausman_test |
analyze_hausman_test |
prepare_fixef_plot |
analyze_fixef_plot |
prepare_predictions |
analyze_predictions |
prepare_joint_test |
analyze_joint_test |
prepare_robust_inference |
analyze_robust_inference |
prepare_event_study |
analyze_event_study |
prepare_panel_view |
analyze_panel_view |
Learn
| Old | New |
|---|---|
sandbox_omitted_variable_bias |
learn_omitted_variable_bias |
sandbox_pooled_vs_fixed_effects |
learn_pooled_vs_fixed_effects |
sandbox_clustering_se |
learn_clustering_se |
sandbox_first_differences |
learn_first_differences |
sandbox_within_vs_lsdv |
learn_within_vs_lsdv |
0.4.1 (2026-06-21)
A panel-aware Explore release: the exploratory functions now make the across-units-vs-over-time structure of panel data explicit, and the identifier vocabulary is standardized. This includes breaking changes to the Explore API.
Added
- Within / between variation —
explore_xtsum_table(Stataxtsum-style decomposition of each variable into overall / between / within standard deviations) andexplore_scatter_plot_within_between(the pooled, between and within slopes of anx–yrelationship, side by side). - Per-unit trajectories —
explore_spaghetti_plotdraws every unit’s path over time with a bold central-tendency overlay, optional highlighting, sampling for large panels and small-multiple faceting. - Panel-structure diagnostics —
explore_panel_structure(balance/gaps summary + unit-by-period presence grid) andexplore_value_heatmap(a unit-by-time value heatmap, optionally standardized within period or unit). - Distribution & transition dynamics —
explore_distribution_over_time(ridgeline or animated),explore_transition_matrix(period-to-period state transitions, binning numeric variables) andexplore_within_persistence(within-unit serial correlation). set_panel(df, entity=, time=)stores the panel identifiers on the frame so Explore functions can omit them; explicit per-call arguments always win.- New explainer topics (
within_between_variation,panel_structure,transition_matrix) and.interpret()on every new result, plus a newfirmsbundled dataset — a small unbalanced panel (staggered entry/exit, interior gaps, a discrete size class, persistent revenue) for the structure / transition / persistence views. - A Panel structure page in the Explore app surfacing all of the above.
Changed (breaking)
- Identifiers standardized to
entity(unit) andtime. The Explore functions that tookts_idnow taketime(keyword-only):explore_trend_plot,explore_quantile_trend_plot,explore_trend_plot_by_group,explore_missing_values_plot.explore_ext_obs_table’scs_id/ts_idbecomeentity/time.explore_trend_plot_by_groupnow takesgroup_var,varpositionally withtimekeyword-only. - Every Explore function returns a wrapped result.
explore_scatter_plot,explore_missing_values_plotandexplore_violin_plot_by_grouppreviously returned a bare Plotly figure; they now return a result object — access the figure via.fig. - The app launchers, dataset metadata and bundle format use
entity/timetoo. TheExploreApp/AnalyzeApp/LearnAppkeyword argumentscs_id/ts_idare renamed toentity/time(hard rename — the old names are no longer accepted). The bundleddf_deftypevaluescs_id/ts_id(returned byload_*_data_def()) becomeentity/time, and the launch bundle manifest keys change to match. So the whole package now speaks one panel vocabulary.
Fixed
explore_trend_plotno longer drops a row from all series when any one variable is missing (it now drops missing values per series), removing a multi-variable mean bias.explore_missing_values_plotorders periods by their true (numeric/date) value rather than lexically;explore_trend_plot_by_grouporders group levels numerically when possible.
Visual
- A refreshed Plotly theme (modern font stack with Arial fallback for stable static exports, softer gridlines, theme-aware hover cards, unified heatmap colorbars) and richer hover detail, range sliders and toggles across the figures.
0.4.0 (2026-06-20)
A reorganization release. expdpy is now structured around three conceptual modules — Explore, Analyze and Learn — across the public API, the documentation site, and the apps. The estimator surface is focused on linear panel methods. This is a breaking change.
Removed (breaking)
- IV / 2SLS, Poisson, and logit / probit estimators are gone.
analyze_estimationno longer acceptsmodel="iv"/"poisson"/"logit"/"probit"(nor theendog/instrumentsarguments); it now covers OLS with stepwise / multiple-outcome comparison, serial-correlation-robust standard errors (Newey–West, Driscoll–Kraay) and weights. Migrate IV / GLM code to OLS-based or other panel methods. - The
ivandglmconcept-explainer topics were removed fromexplain()/list_topics(). - The single combined
ExPdPyapp and theexpdpy-streamlitconsole script are gone, replaced by three module apps (see below).
Added
analyze_cre_table— a correlated-random-effects (Mundlak) estimator that brings FE-consistent within estimates into a random-effects frame, alongsideanalyze_panel_tableandanalyze_hausman_test. A joint test on its unit-mean terms is the regression-form Hausman test.- Two Learn sandboxes —
learn_first_differencesandlearn_within_vs_lsdv(first differences ≈ demeaning ≈ least-squares dummy variables) — plus matching explainer topics (first_differences,within_transformation/demeaning,dummy_variables/lsdv,correlated_random_effects/mundlak). - Three no-code apps —
ExploreApp,AnalyzeApp,LearnApp(console scriptsexpdpy-explore,expdpy-analyze,expdpy-learn; deploy scriptsapp_explore.py,app_analyze.py,app_learn.py). They share the common shell and differ only in which pages they expose; the Learn app adds an in-app concept-explainer browser.
Changed
linearmodelsis now a required dependency (previously the optionalpanelextra). Pooled / between / fixed / random effects, CRE and the Hausman test work out of the box;pip install "expdpy[panel]"is kept as a no-op alias for compatibility.- Reorganized into three modules. The public
__all__, the README, the documentation reference, and the app navigation are grouped under Explore / Analyze / Learn. The docs site now has one page per module (folding in the old Quickstart and Examples gallery), and the home page leads with three module cards. No import paths change for retained functions.
0.3.0 (2026-06-19)
Removed
- The Shiny app has been removed. Streamlit is now the sole interactive
ExPdPyapp. Theexpdpy[app]install extra and theexpdpy.appmodule are gone; launch the app withfrom expdpy.streamlit_app import ExPdPy(orpip install "expdpy[streamlit]"). The framework-agnostic app core (sample pipeline, the safevar_defevaluator, config save/load, component renderers, notebook export) now lives underexpdpy.streamlit_app.
0.2.0 (2026-06-19)
A major feature release: modern fixest / pyfixest econometrics, a panel-data toolkit, and a pedagogy layer for teaching — all surfaced through the interactive apps. Requires pyfixest>=0.60; adds an optional panel extra (linearmodels).
New estimators
analyze_estimation— a unified estimator adding IV / 2SLS, Poisson (fepois) and logit / probit (feglm) to OLS; a wider choice of standard errors (heteroskedastic HC1–HC3, cluster-robust CRV1 / CRV3, and the serial-correlation-robust Newey–West and Driscoll–Kraay); and stepwise / multiple-outcome model comparison (sw()/csw(), multiple left-hand sides) for watching estimates move as the specification changes.- Post-estimation helpers:
analyze_fixef_plot(visualize the estimated fixed effects),analyze_predictions, andanalyze_joint_test(a Wald joint-significance test). analyze_robust_inference— randomization inference (and the wild cluster bootstrap when the optionalwildboottestpackage is installed), for credible inference with few clusters.
Event study & staggered difference-in-differences
analyze_event_studywrapping the modern estimators — Gardner’s two-stagedid2s, Sun–Abraham (saturated), local-projections DiD (lpdid) and dynamic two-way fixed effects (twfe) — returning a themed Plotly event-study plot with a pre-trend diagnostic.analyze_panel_view— a Plotly reimplementation ofpanelviewthat derives the binary treatment indicator from a first-treatment cohort column for you.- A bundled
load_staggered_did()teaching dataset.
Panel models (optional panel extra, via linearmodels)
analyze_panel_table— pooled OLS, between, fixed-effects and random-effects estimates side by side (the fixed-effects estimate matches the pyfixest path).analyze_hausman_test— the classic fixed-vs-random-effects specification test.
Visualization
analyze_coefficient_plot— coefficient plots with confidence intervals for one or several models (a beginner-friendly alternative to reading a coefficient table).- A dark Plotly theme variant (
apply_default_layout(fig, dark=True)).
Pedagogy
- A concept-explainer registry —
explain(topic)andlist_topics()— covering fixed effects, clustering, IV, GLMs, event studies, parallel trends, omitted-variable bias, winsorizing vs truncating, Pearson vs Spearman, and more. - Result objects gain
.interpret()(plain-language, strictly associational — never causal unless an FE/IV design is present),.explain(), and broom-style.tidy()/.glance(). - Concept sandboxes that simulate data so a concept can be seen and tuned:
learn_omitted_variable_bias,learn_pooled_vs_fixed_effects,learn_clustering_se.
Interactive apps
- Both the Streamlit and Shiny apps now show a plain-language interpretation and a method explainer on the regression card, plus a coefficient plot.
- New pages / cards in both apps: Event study & DiD and Panel models (shown for panel data); the Streamlit app also gains a data-free Concept sandboxes page. The Staggered DiD dataset is available in the picker.
0.1.0 (unreleased)
Initial release — a Python port of ExPanDaR.
- Analytical functions returning Plotly figures and Great Tables / pyfixest output:
explore_descriptive_table,explore_correlation_table,explore_correlation_plot,explore_ext_obs_table,explore_trend_plot,explore_quantile_trend_plot,explore_bar_plot_by_group,explore_trend_plot_by_group,explore_violin_plot_by_group,explore_histogram,explore_bar_plot,explore_missing_values_plot,explore_scatter_plot,analyze_regression_table, andtreat_outliers. - The interactive
ExPdPyapp (Shiny for Python) with in-app upload, save/load of configurations, and reproducible notebook/script export. - A second interactive
ExPdPyapp (Streamlit): the same no-code exploration UI in a multipage layout with native tables, the subset / outlier / user-defined-variable pipeline, config save/load (interchangeable with the Shiny app’s configs) and reproducible notebook export — runnable locally and on Streamlit Community Cloud (streamlit run streamlit_app.py). - Bundled datasets:
kuznets(a synthetic panel whose regional inequality traces an N-shaped Kuznets curve — the default showcase across the docs and the app picker) andgapminder(plus their definition tables and configurations).