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_plot names 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 flexible path=[...], 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 script expdpy-mcp): a stdio Model Context Protocol server exposing a curated set of Explore/Analyze tools plus explain / 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-expdpy agent 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_def gains a sixth column, role, with values outcome / covariate / entity_name (blank otherwise). Mark the main outcome and main covariate(s) once, and the figures, tables and apps default to them; mark an entity_name column (a readable label for each unit) and figures render units as Name (id) (e.g. Bolivia (BOL)).
  • build_data_def now emits the role column and auto-detects the entity-name column (a text column that is constant within, and ~1:1 with, the entity id).
  • Every bundled *_data_def ships curated roles, so the flagship datasets demonstrate the defaults out of the box — loading kuznets defaults a regression to the cubic Kuznets curve, bolivia112_gdppc labels provinces as Name (id), and so on.
  • New set_roles(df, outcome=, covariates=) utility (stored on df.attrs, like set_panel / set_labels); set_panel gains an entity_name= argument; and set_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_histogram and explore_trend_plot default 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 type is now authoritative for the variable categories, and can_be_na = False is 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 as T grows.
  • 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).

Internals — shared helpers in _common.py

  • The genuinely cross-module private helpers (numeric-then-lexical level sorting, the sample-size default opacity, and the time-axis / standard-error / x-axis builders) moved from the feature modules (scatter.py, trends.py) into a new expdpy._common. No public API change.

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_inference and analyze_panel_view (Analyze), plus explore_scatter_plot, explore_missing_values_plot and explore_value_heatmap (Explore). Each reads the result in associational, non-causal language (the existing analyze_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 with warnings.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 keep caption= 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_regression is the panel-aware companion (the entity/time vocabulary, two-way fixed effects and cluster-by-entity by default — the equivalent of Stata’s xtivreg2 ... fe). Both carry .interpret() and an instrumental_variables concept explainer.
  • analyze_marginal_effects_plot — for a model with an interaction focal * 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-country base_sample reproduces 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 = X no 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 the entity and time rows 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 datasetProductivity and Bolivia (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.csv alongside the sample, and the generated notebook reloads both with ex.set_labels(df, data_def, set_panel=True). The .ipynb is 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 .py script 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). Every learn_* sandbox now appears once each, removing the old tour/gallery duplication and adding the previously-undemonstrated learn_kuznets_waves.
  • The Learn Streamlit app mirrors the tutorial. The Concept sandboxes page grows from seven to nine tabs — adding learn_sigma_convergence and learn_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 the set_panel / resolve_panel / treat_outliers utilities) 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_plot no longer draws a LOESS confidence band. The shaded band was an unweighted bootstrap while the LOESS line is size-weighted under loess=2, so the line could fall outside its own band; the smoother now shows just the line.
  • explore_spaghetti_plot no 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_table is now panel-aware. When a time column is known (declared via set_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. Mean over 2015 and 2025); 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-8 digits vector is replaced by a stats= selection (any of the eight statistics), a scalar-or-mapping digits=, and a new periods= argument; the result gains a tidy .by_period frame (.df still carries all eight pooled statistics).
  • explore_histogram gains opt-in density overlays. New kde= and normal= 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 .df keeps 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 resolve entity / time / unit from the declared panel, so those arguments can be omitted after set_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 inside docs-build between quartodoc build and quarto 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 the method="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 to inf and 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 tcrit threshold is now threaded into the summary table’s converging column, the table source-note and the plain-language .interpret() (all previously hardcoded to -1.65), and is exposed on ConvergenceClubsResult.tcrit.

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^4 with g = log GDP 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 full degree-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 optional controls (and, for the within view, the two-way fixed effects) are partialled out. The result exposes gt_pooled / gt_between / gt_within, the three figures, a per-estimator curvature summary (turning points, peak, top-order term), the fitted models, and .interpret() / .explain(). A paired learn_kuznets_waves() sandbox, a kuznets_waves concept 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_gdppc dataset — 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 synthetic productivity panel, for the convergence workflows (analyze_beta_convergence / analyze_sigma_convergence / analyze_convergence_clubs) and general subnational exploration. Load with load_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 Stata psecta package). It runs the full workflow on one variable: a per-unit Hodrick-Prescott filter (lambda = 400 for annual data), the relative transition path h_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 (with relative and club), three figures (within-club averages, all paths by club, and per-club small multiples), a classification table and an entity -> club membership frame. Configurable trimming r, sieve method (Schnurbus-adjusted or original PS), merge mode and HP filter.
  • Learn: a runnable learn_convergence_clubs sandbox plants a known club structure and shows the algorithm recover it, and an explain("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 productivity dataset — 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 (matching analyze_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 (to NaN with a note) on negative values or a near-zero mean.
  • Learn: a runnable learn_sigma_convergence sandbox and an explain("sigma_convergence") concept explainer demonstrate σ-convergence on a geometric-narrowing panel whose dispersion shrinks at a known log-rate ln(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_convergence sandbox and an explain("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.1 but Google Colab pre-installs an older numba that caps numpy<2.1, the notebooks’ install cell hit a numpy/numba dependency conflict. The cell now co-resolves numba>=0.61 in 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") raised TypeError: reshape() got an unexpected keyword argument 'shape'. The underlying pyfixest estimator uses NumPy 2.1’s np.reshape(..., shape=...), so expdpy now requires numpy>=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 with set_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 to var_def and then the name when no label exists. The four bundled datasets (kuznets, gapminder, firms, staggered_did) ship curated labels. The returned .df / .df_corr keep 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, analyze and learn now show the official Colab badge under the title, opening the matching notebooks/<slug>.ipynb straight 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_plot now 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 reads built <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 .qmd sources. 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_rangeslider helper renders a much thinner strip (thickness 0.06 → 0.02) in a near-white #FAFAFA fill, so the slider beneath explore_trend_plot, explore_quantile_trend_plot, and explore_spaghetti_plot stays 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_*, and sandbox_*learn_*. Plotly-figure functions now end in _plot and Great-Tables functions in _table, and scope qualifiers move to the end of the name (e.g. prepare_by_group_violin_graphexplore_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_topics are unchanged.
  • Result dataclasses, the ExploreApp / AnalyzeApp / LearnApp classes, 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 variationexplore_xtsum_table (Stata xtsum-style decomposition of each variable into overall / between / within standard deviations) and explore_scatter_plot_within_between (the pooled, between and within slopes of an xy relationship, side by side).
  • Per-unit trajectoriesexplore_spaghetti_plot draws every unit’s path over time with a bold central-tendency overlay, optional highlighting, sampling for large panels and small-multiple faceting.
  • Panel-structure diagnosticsexplore_panel_structure (balance/gaps summary + unit-by-period presence grid) and explore_value_heatmap (a unit-by-time value heatmap, optionally standardized within period or unit).
  • Distribution & transition dynamicsexplore_distribution_over_time (ridgeline or animated), explore_transition_matrix (period-to-period state transitions, binning numeric variables) and explore_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 new firms bundled 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) and time. The Explore functions that took ts_id now take time (keyword-only): explore_trend_plot, explore_quantile_trend_plot, explore_trend_plot_by_group, explore_missing_values_plot. explore_ext_obs_table’s cs_id / ts_id become entity / time. explore_trend_plot_by_group now takes group_var, var positionally with time keyword-only.
  • Every Explore function returns a wrapped result. explore_scatter_plot, explore_missing_values_plot and explore_violin_plot_by_group previously 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/time too. The ExploreApp / AnalyzeApp / LearnApp keyword arguments cs_id / ts_id are renamed to entity / time (hard rename — the old names are no longer accepted). The bundled df_def type values cs_id / ts_id (returned by load_*_data_def()) become entity / time, and the launch bundle manifest keys change to match. So the whole package now speaks one panel vocabulary.

Fixed

  • explore_trend_plot no 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_plot orders periods by their true (numeric/date) value rather than lexically; explore_trend_plot_by_group orders 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_estimation no longer accepts model="iv" / "poisson" / "logit" / "probit" (nor the endog / instruments arguments); 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 iv and glm concept-explainer topics were removed from explain() / list_topics().
  • The single combined ExPdPy app and the expdpy-streamlit console 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, alongside analyze_panel_table and analyze_hausman_test. A joint test on its unit-mean terms is the regression-form Hausman test.
  • Two Learn sandboxes — learn_first_differences and learn_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 appsExploreApp, AnalyzeApp, LearnApp (console scripts expdpy-explore, expdpy-analyze, expdpy-learn; deploy scripts app_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

  • linearmodels is now a required dependency (previously the optional panel extra). 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 ExPdPy app. The expdpy[app] install extra and the expdpy.app module are gone; launch the app with from expdpy.streamlit_app import ExPdPy (or pip install "expdpy[streamlit]"). The framework-agnostic app core (sample pipeline, the safe var_def evaluator, config save/load, component renderers, notebook export) now lives under expdpy.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, and analyze_joint_test (a Wald joint-significance test).
  • analyze_robust_inference — randomization inference (and the wild cluster bootstrap when the optional wildboottest package is installed), for credible inference with few clusters.

Event study & staggered difference-in-differences

  • analyze_event_study wrapping the modern estimators — Gardner’s two-stage did2s, 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 of panelview that 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) and list_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, and treat_outliers.
  • The interactive ExPdPy app (Shiny for Python) with in-app upload, save/load of configurations, and reproducible notebook/script export.
  • A second interactive ExPdPy app (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) and gapminder (plus their definition tables and configurations).