Changelog¶
All notable changes to this project are documented here.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Unreleased¶
Added¶
monthly_sales(), the time-series dataset that was R-only.cluster_plot(clusters=)draws a clustering computed elsewhere, as the R twin'sclustersargument does, and every cluster plot carries its assignments asp.clusters, one per complete-case row ofdata.power_curve_plot()gainstarget,lower,upper,ribbonandy_labaftertitle, as in the R twin. The target line was fixed at 0.8, and no confidence band was drawn. It also takes a list of dicts, one per row, which is how pilotr returns a power curve in Python, and recognisesn_subjectand<power>_lo/<power>_hicolumns, so pilotr's output plots as it is, with its band.power_curve_plot()gainssolved, which marks a solved sample size and its interval, such as the result of pilotr'starget_n(). The value is drawn as a dashed vertical line and the interval as a grey band behind the curve. The R twin gains the same argument.check_figure()gains anannotation_text_sizerow for the text drawn inside the panel: annotations, the labels of layers such asgeom_text()andgeom_label(), and the cell values and labels of depictr's own plots, the number-at-risk table ofsurvival_plot()among them. The text-size check never saw that text, so a 5.7 pt annotation passed a 6 pt floor while the report gave 8.80 pt as the smallest text. The new row holds it to the same floor at its printed size. It is appended last, so the report now has ten rows. The contrast of that text is not measured. When all of a figure's text is inside the panel, the two rows for the text around it no longer say that the figure draws no text.check_figure(max_text_pt=)checks a ceiling as well as a floor, andsave_plot(check=True)audits the figure at the width it is saved at, so the text sizes are the printed ones. The ceiling adds a last row,text_size_max, for the largest text at its printed size, around the panel and inside it, and that row passes at or below its threshold. Nature, for one, allowed no text above 7 pt but the panel labels. Withoutmax_text_pt, the report keeps its ten rows.save_plot(check=True)gives the saved width, converted to centimetres from inches or millimetres, as bothwidth_cmandrender_width_cm, and warns with each check the figure fails. It still returns the filename alone, where the R twin attaches the report to it.survival_plot()draws the confidence limits and censor marks that the R twin draws. The limits are dotted steps around each curve atconf_level, which was accepted but unused and must now lie strictly between 0 and 1, andconf_level=Noneleaves them out. The newconf_typebuilds them on the log-log scale by default, which gives the interval lifelines computes, or with"log"or"plain". The formulas are the R twin's, so the twins draw the same limits. The newcensor_marks,Trueby default, marks each censored observation with a "+", and the newlogranksays whether to print the log-rank test as a subtitle.depictr_palette(order="contrast"), also accepted byscale_colour_depictr()andscale_fill_depictr(), reorders the built-in palette so that up to three groups pass every colour check ofcheck_figure(), contrast against white included, and up to four keep 3:1. It leads with the brand blue, the vermillion and the reddish purple, then the bluish green. It warns past four colours, and the ramps refuse it. Python has no options object, so the scale is added to each plot that should use the order. The default order is unchanged, and the R twin gives the same order.simulate_figure()shows a finished figure, legend and colourbar included, as a reader with a colour-vision deficiency sees it. It draws a copy of a plot or composition atdpiand passes every pixel through the transform thatsimulate_cvd()applies to a colour. The alpha channel is kept, and the image comes back as an array of floats in [0, 1]. Severity zero returns the figure as drawn. A plot whosedraw()method has already been called is refused, since plotnine does not draw it correctly a second time. The web app's colour-vision toggle now calls it, where the app did the same work with three private names fromdepictr.cvd. The R twin'ssimulate_figure()uses the same matrices on the figure's vector elements and returns a gtable.tidy_estimates(),coefficient_plot()andcompare_models()gainexponentiate, and the two plots gainreference_line, as in the R twin.exponentiate=Trueexponentiates the estimates and interval limits, so the plots draw odds, rate or risk ratios on a log10 axis with the reference line at 1. The axis is labelled "Odds ratio" for a logit link, "Rate ratio" for a log link in a Poisson or negative binomial model, "Risk ratio" for a binomial model with a log link and "exp(Estimate)" otherwise. A tidy frame is read as estimates on the link scale. The reference line was fixed at 0, so an exponentiated table drawn on a log axis warned "divide by zero encountered in log10" and showed no line at 1.reference_line=Nonekeeps the line at 0 unlessexponentiateis set, andfloat("nan")leaves it out.format_terms()tidies raw term names into labels by the rules of the R twin'sformat_terms(), with the sameinteraction,strip_prefix,tidy_interceptandwraparguments. It also reads the names that patsy and formulaic give a formula's terms:treatment[T.enhanced]andC(treatment)[T.enhanced]becometreatment: enhanced, andQ("region")[T.North]becomesregion: North. The README no longer lists it among the functions yet to be ported.coefficient_plot()andcompare_models()gainlabelsandinteraction.labelstakes a dict from raw name to label, which warns about a key that matches no term asposterior_plot()does, a list with one label per term, or"raw"for the names the model gives.coefficient_plot()reads a list in model order, whateverorderis, andcompare_models()in the order the terms first appear, as the R twin reads an unnamed vector.interactionsets how an interaction is written.theme_depictr()gainsbase_family, the font family, as in the R twin, for a journal that names a font. Left asNone, it takesplotnine.options.base_family, which every depictr plot reads, so setting that option once is the equivalent of the R twin'sdepictr_options(base_family = ). The README and the getting-started guide say so.correlation_heatmap()gains the R twin'smethod,show_valuesanddigits, aftercolsas in R. Spearman's and Kendall's coefficients give the same labels as the R twin onwellbeing_survey(), andshow_values=Falseleaves the labels out. A title passed as the third positional argument now goes tomethodand is refused.- A parity manifest accounts for every argument of the R twin's functions.
dev/r_formals.Rwrites their signatures from a checkout of the R package totests/data/r_formals.json, andtests/test_parity_manifest.pymatches each R argument with a Python parameter, through an explicit list of renamed arguments, or lists it as not yet ported, and lists the parameters only this package has. A signature change in either package fails those tests until the manifest records it.
Changed¶
arrange_plots()tags the panels A, B, C and so on, as the R twin'sarrange_plots()does by default. The newtag_levelstakes patchwork's styles,"A","a","1","I"and"i", or the tags themselves, andtag_levels=Nonekeeps the untagged panels of earlier versions. The newtag_sizesets their size in points, such as the 8 pt Nature asks of panel labels. The tags run on through a composition passed in, and aplot_spacer()takes none. Among the package's own composites,model_report()is tagged andresidual_diagnostics_plot(),explore_pairs()and the two-panelestimation_plot()are not, as in the R twin. Each tag belongs to its panel's plot, socheck_figure()measures it with the panel's text,text_size_maxincluded, where the R twin's audit leaves out the tags patchwork draws around the panels.- The README no longer says that the twins share "nearly all of an API". They
share most of their functions, while many of the R functions' arguments are
not yet ported. The Status section now lists the differences that change
what a figure shows or how it is computed, among them the STL decomposition,
the posterior options, the Q-Q band, the ROC confidence band and Youden
point, the forecast, the order of the dumbbell categories and the handling
of a gap in
acf_plot(). It no longer counts the frequentist-over-Bayesian overlay and the survival number-at-risk table among the composites, since each is a single plot. CITATION.cffnames PyPI as the package's repository artifact, as the R twin's names CRAN, and says that its DOI is the Zenodo concept DOI of this repository, which holds the Python package alone, since PyPI assigns no DOI. Its release date, and the date of the 0.3.0 section below, are now 2026-09-24, the day PyPI published 0.3.0 and the GitHub release appeared. Both gave 2026-08-21, the day the release was prepared.- The home page gives the Zenodo concept DOI as a full link,
https://doi.org/10.5281/zenodo.21266311, where it showed the Zenodo badge,
and the About page links every DOI the same way. The BibTeX entry's
urlgives the PyPI page, so a style that prints both fields no longer shows the DOI twice. The README's DOI badge now ends the badge row and names the DOI in its text alternative, and the README's citation section gives the DOI. - The bundled datasets are now the R package's own data, shipped as CSV files,
so an example gives the same numbers in both twins and every column an R
example uses exists here. Their shapes, columns and values change.
crop_yieldgains its field identifier and loses two impossible negative yields.wellbeing_surveygains respondent ids, exercise days and an ordered education factor.lexical_decisionbecomes the crossed design of 24 participants and 40 items, so the mixed-model example is no longer singular. The arms ofclinical_trialare placebo and treatment. Factor columns are categorical in R's level order. The files are written from the R package bydev/export_r_datasets.R, and the tests pin their checksums and a set of values computed in R. - The
random_effects_plot()example and gallery entry fit participant intercepts tolexical_decision, whose participant variance statsmodels and lme4 estimate alike (about 6050). The old example grouped the wellbeing survey by region, whose variance was estimated at zero. - The web app offers a column as a grouping variable only when it has 2 to 12
distinct values, so an identifier such as
participantorpatientis never the default group, and it leaves identifiers out of the model predictors. cluster_plot()clustered the two-dimensional PCA projection, so its clusters could differ fromsilhouette_plot()'s and the R twin's for the samek. On the bundled data with their default columns, the two partitions agreed with an adjusted Rand index of 0.39 forcrop_yieldand 0.63 forwellbeing_surveyatk = 4. It now clusters the full standardised data withsilhouette_plot()'s settings and projects only for display, so cluster assignments change. Each centroid is drawn at the projection of its k-means centre, and the axes give each component's share of the variance, aspca_plot()and the R twin do.power_curve_plot()finds its columns as the R twin does whennandpowerare not given, son,nlevels,x,sample_sizeorn_subjectserves as the sample size andpower,meanorpas the power. A table with several curves and nogroupwas joined into a single zig-zag line. Aneffect,paramortermcolumn with more than one level now separates the curves, with a warning naming the columns used, and rows that share a sample size with no such column raiseValueError. Each curve takes a line type as well as a colour. The y-axis title is "Power" for the usual power columns and the column's name otherwise.binned_residual_plot()bins as the R twin does, so the twins draw the same points for a model. It tookround(sqrt(n))bins where R takesfloor(sqrt(n)), kept between 2 andn // 2, and it cut the ranks of the fitted values elsewhere, so the first bin of the clinical-trial example held 18 rows to R's 17. Tied fitted values were left in whatever order numpy's sort gave them and now follow row order, as in R. The plot gainsband=. Its default,"model", keeps the band the plot drew, which the R twin now draws too, and"empirical"draws the band of R'sarm::binnedplot(), from the spread of the residuals in each bin. The plot's data gain the R twin'sn,se2andoutsidecolumns. Ann_binsbelow 2 raisesValueError, as in R, and so does a model with fewer than four observations.calibration_plot()cuts the scores at their quantiles into bins of nearly equal count, as the R twin does, sizes each point by its bin's count and draws a Wilson interval on its observed rate. Its equal-width bins left the upper points of a rare outcome resting on one to four cases, with nothing on the figure to say so.strategy="uniform"keeps the old bins,conf_levelsets the level of the intervals (Noneleaves them out), andbinsis accepted as the R twin's name forn_bins. The bins are computed with numpy, so the function no longer needs scikit-learn. A missing score spoilt the top bin under scikit-learn and now raisesValueError, as a score outside [0, 1] does. An outcome coded -1/1, which scikit-learn accepted, now raises it too, as in the R twin.- The colour rows of a figure coloured by a continuous scale, such as a
heatmap, said that nothing was distinguished by colour. They now say that
colour is a continuous scale, which these checks do not audit, and the
documentation recommends a perceptually ordered map such as viridis for one.
The docstring of
check_figure()and the README now also give the default palette's contrast limit, which the accessibility page already stated: four of its eight colours fall below the 3:1 that WCAG asks of a graphical object against white, the orange from the second group on. The documentation shows how to audit a figure saved at its final width, with that width given as both widths or neither, and the docstring gives four publishers' text floors, dated and linked, where it called 6 pt a common publisher floor. check_figure()accepts a composition and audits each panel on its own, depth-first in the order the composition holds them, with a leadingpanelcolumn. It used to raiseTypeError, which left the package's own compositions without an audit. Among them are the figures ofarrange_plots(),residual_diagnostics_plot(),model_report(),estimation_plot(two_panel=True)andexplore_pairs(). Each panel's legend is measured with the panel, and aplot_spacer()takes no panel number. The R twin numbers the panels of a patchwork by the same rule.vif_plot()gainsthreshold, placed beforetitleas in the R twin, so a title passed by position must now be named. The reference line is drawn solid in the R twin's grey,#666666, where it was dashed in the accent.- The cycle lines of
seasonal_plot()start from a colour that clears 3:1 against white. They took the sequential ramp, which starts at#e6eff5, 1.16:1 against white, so the earliest cycles were close to invisible. With twelve cycles, four of the lines fell below the 3:1 that WCAG 2.2 asks of a graphical object. The ramp now runs from#4a8fc0, at 3.51:1, through the brand blue to#08315a, as in the R twin, anddepictr_palette(kind="sequential")keeps its pale end for fills. Past about six cycles, neighbouring lines take colours too close to tell apart, as the docstring now says. - Colour separability in
palette_safety()andcheck_figure()is now measured with CIEDE2000 (Sharma, Wu & Dalal, 2005). The 1976 Euclidean distance it replaces overstates differences between saturated colours: ggplot2's default five-colour palette was certified safe under deuteranopia, though two of its colours become almost identical, 2.74 apart in CIEDE2000. The threshold stays at 5, and the Okabe-Ito palette's closest pair now measures 6.43, where it measured 7.4.depictr_palette(9)already warned that interpolated colours lose the guarantee, and its nine colours now fail the check too.metric="cie76"restores the old measure for this release, and the greyscale check is the lightness gap under either. The R twin makes the same change and gives the same numbers. - The accent colour is now the vermillion
#d55e00, as in the R twin. The orange it replaces reaches only 2.25:1 against white, below the 3:1 that WCAG 2.2 asks of a graphical object, and it marked the flagged points ofoutlier_plot(), the missing cells ofmissingness_map(), the cumulative line ofscree_plot()and the rolling mean oftimeseries_plot(). The orange stays in the palette. The smoothers of the residual panels inresidual_diagnostics_plot()andmodel_report()are drawn in the reference grey#999999, as in R, where they took the orange. simulate_cvd()at partial severity now uses Machado et al.'s published matrices, interpolated between the tabulated severities. Interpolating towards normal vision missed them on the default palette by up to 11 units of the 1976 Delta-E, or 6.2 in CIEDE2000. Full-severity results, and sopalette_safety(),check_figure()and the web app's colour-vision view, are unchanged. The authors tabulate a matrix for each tenth of severity, and a severity between two tenths now takes the linear interpolation of the two, as colorspacious does, whose output it matches. The old transform drew the full-severity matrix towards the identity. Between severities 0.3 and 0.7, it moved the palette's colours too little under protan and deutan and too far under tritan, so under a red-green deficiency they looked further apart than the model predicts. The R twin makes the same change, and the two test suites pin the same colours.ridgeline_plot()draws in a fraction of the time, about a twentieth on the bundled datasets, and the figure is unchanged. Each ridge sits in a panel of its own, so the stacking thatgeom_area()applies by default had nothing to stack. The ridges are now drawn unstacked.- The R twin now gives a GLM whose dispersion is estimated (Gaussian, Gamma,
inverse Gaussian) t intervals on the residual degrees of freedom, as R's
summary.glm()tests it, where it gave z intervals. Here the intervals stay those of the result'sconf_int(), which follow itsuse_t: z by default, as statsmodels' own GLM summary reports, and t when the model is fitted withuse_t=True, which gives the R twin's intervals. The effects and interaction bands followuse_tin the same way. Thetidy_estimates()docstring now says so. At 12 residual degrees of freedom, the t interval is about 11% wider. coefficient_plot()andcompare_models()label their terms for a reader, where they printed patsy's names. A factor's coefficient names its variable and level, sotreatment[T.enhanced]is drawn astreatment: enhanced,rainfall:treatment[T.enhanced]asrainfall × treatment: enhancedand a quotedQ("region")[T.North]asregion: North. For a formula fit, the levels are read from the model's design. The R twin labels its terms the same way, and the two test suites pin the same labels. Terms that take the same label share a row incompare_models(), and both functions drop brms'sb_Interceptwith the intercept, as in R. The plot's data keeps the raw names interm, andlabels="raw"draws them as before.- The
appandallextras require Streamlit 1.49 or later, where they allowed 1.30. The web app sizes its figure and table withwidth="stretch", which first reachedst.image()in 1.49 and replaceduse_container_width. Streamlit logged on every rerun that the older argument would be removed. - Packaging: the build backend is no longer held below hatchling 1.32, whose
metadata version twine now accepts, and the licence is declared once, as an
SPDX expression, without the classifier PEP 639 deprecates. The core metadata
gives
License-Expression: MIT, where itsLicensefield held the whole licence text.
Deprecated¶
- The
seedargument of the dataset loaders, which no longer generate data. Passing a value raises aFutureWarningand has no effect. - The log-rank subtitle that
survival_plot()prints by default when there are groups. The new default,logrank=None, keeps it and raises aFutureWarning, since a future release will makelogrank=Falsethe default, as in the R twin. Passlogrank=Trueto keep the subtitle.
Fixed¶
coefficient_plot(order="ascending")andorder="descending"drew the terms the opposite way round from the docstring ("order of the terms up the axis") and from the R twin. They now order up the axis, so"ascending"puts the largest estimate at the top, as in R. Terms with tied estimates keep their model order, as R'sorder()keeps them.tidy_estimates()kept every entry of a MixedLM'sparams, socoefficient_plot()andcompare_models()drew the random-effect variances and covariances beside the fixed effects as if they were coefficients. TheGroup Varentry is not even a variance: statsmodels divides it by the residual scale. Only the fixed effects are kept now, as in the R twin.- A model fitted from arrays, without a formula, made
tidy_estimates()raiseAttributeError, and with itcoefficient_plot(),compare_models()andfrequentist_bayesian_plot(). Its terms are now named from the model'sexog_names, as statsmodels' own summary names them. - A model fitted on a data frame with numbered columns, as
pd.DataFrame()makes from an array, gavetidy_estimates()integer terms, on whichcoefficient_plot()andcompare_models()failed. Terms read from a model are now always strings, as in the R twin. survival_plot()read any non-zeroeventas an event. With thesurvival::Surv()1/2 coding (1 = censored), which the R twin accepts, every subject became an event, and the curves and the log-rank test were wrong without a warning. A 0/0.5 coding was cast to integers and so read as all censored, and a missing event became a large negative integer, which lifelines counted as an event. The 0/1 and 1/2 codings are now accepted, read over all observations together as in R. Any other coding raises, and a missing event is dropped with a warning.- The number-at-risk table rounded its times to one decimal, so short follow-up collapsed onto a single tick and quarter-unit breaks sat under the wrong labels. Times are kept exact and only the labels are formatted. The table also gained a column of zeros at a break past the longest follow-up, outside the time axis. Its breaks now stop at the last follow-up time, as the R twin's do.
- The web app no longer offers the time column as the event indicator. Its
Kaplan-Meier pane offers the columns coded 0/1 or 1/2, opens on one named
eventorstatus, and says so when a dataset has none. - The web app read an uploaded CSV as UTF-8, outside its error handler. A file
that Excel on Windows saves in cp1252 therefore ended the page in a
UnicodeDecodeErrortraceback, and an empty or ragged file in a pandas one. The app now tries UTF-8, then cp1252, then latin-1, and the call it shows names any encoding other than UTF-8, so the call reads the file the same way. A file it cannot read gets an error message. - Panes of the web app whose required choice had nothing to offer showed
"Could not draw that plot for this data: None". The classification curves
and the confusion matrix did so on
wellbeing_survey, and the effect of a predictor onwellbeing_surveyandlexical_decision. Such a pane now says what kind of column it needs and draws nothing. Where no column is coded 0/1, the classification curves no longer offer another two-level column, whose refusal described the wrong problem. The dumbbell offers only a two-level column other than its category and value. It used to fall back to any other category, or to the category itself, and so opened on a column the plot refuses oncrop_yieldandwellbeing_survey. - The web app's 'exact call' did not reproduce several figures. The model
panes referred to
data,smfandmodel, which the call never defined, and wrote the formula unquoted, so a model ofyield, a Python keyword, failed to parse. The estimation call left out its seed and the PCA call its grouping, and the survival call carried a literalgroup=..., which raised aTypeErrorwhen pasted. Each call is now written from the arguments the plot receives, and a model pane shows the import and the fit before its call. The fitted model quotes withQ()only the names that need it, such asyield, where it quoted every name. A new test opens every pane but the calibration curve, which no bundled dataset can score, runs the call it shows and compares the figure with the app's, pixel for pixel. save_plot()wrote files that journals reject. A PDF or EPS carried Type 3 fonts, which Nature refuses. An EPS drew its translucent layers opaque, which matplotlib reported only in its log. A TIFF was uncompressed RGBA, 10.8 MiB at the default 7 by 4.5 inches, where PLOS requires LZW compression and no alpha channel. Fonts in a PDF or EPS are now embedded as TrueType (Type 42), for that save only, and an EPS with translucent layers warns and says how many there are. A TIFF is saved as LZW-compressed RGB at the requested resolution, with any transparent background composited onto white, unlesspil_kwargsis passed. A missing output directory is now created, as in the R twin, where it raisedFileNotFoundError.save_plot()saved a composition, such as the output ofarrange_plots(), at the figure size of its last panel's theme, by default 6.4 by 4.8 inches, whateverwidthandheightasked for. plotnine sizes a composition that way and ignores the size itssave()is given. The size asked for is now written into that theme, so a composition is saved at it, as a single plot is and as the R twin saves a patchwork.effects_plot()andinteraction_plot()held covariates at means taken over every row of the source frame, including rows the fit dropped, and treated every column as a predictor, so an unrelated all-missing column raisedIndexError. They now use the estimation rows and the columns the formula names. A column outside the formula is no longer accepted asvar,xorgroup, where sweeping it drew a flat line, and a group level found only in dropped rows is no longer drawn.- Offset and exposure models were predicted at an offset of zero. They are now predicted at the mean offset over the estimation rows, which for an exposure is its geometric mean, as in the R twin, and the y-axis label says so.
- A formula term computed from statistics of its rows, such as
I(x - x.mean()), shifted the predicted line, sinceget_prediction()computed it afresh over the grid. Such a term is now refused withValueError, as in the R twin, whether statsmodels builds the formula with patsy or with formulaic, the engine statsmodels 0.15 added. patsy'scenter()andstandardize()keep the values of the fit and are unaffected. estimation_plot()drew the differences out of line with the groups they belong to. Its difference panel had one column fewer than there are groups and spread its columns across the full width, so with two groups the single difference sat midway between them, beneath neither. The default reference was the group of the first row, so shuffling the rows could change the reference and with it the sign of every difference, and the interval carried no label. In both layouts, every group now keeps its column, with an empty one for the reference, which comes first as in the R twin. The default reference is the first level of the group, in a categorical's declared order or else in sorted order, with numbers sorted by value. AFutureWarningnames both groups wherever the 0.3.0 rule would have chosen another. A numeric group made the two-panel layout raiseTypeErrorand is now drawn as in R. A caption names the interval ("Bootstrap 95% CI (5,000 resamples)") as in R, and the defaultn_bootrises from 2,000 to the R twin's 5,000. Aseednow fixes the jitter of the raw data as well, so a seeded two-panel figure comes out the same every time. The layout is now called a Cumming estimation plot. Gardner-Altman names a layout that sets the difference beside the data, which this function does not draw.check_figure()measured all text against the plot background, so text on a coloured facet strip or legend box was judged against a background it is not drawn on: white strip text on a dark strip failed and unreadable grey strip text passed. Each text is now measured against its own background, with a translucent fill taken as the colour it shows over what lies beneath it. Large text (18 pt, or 14 pt bold, as printed) is held to the 3:1 that WCAG asks of it, where it used to be held to 4.5:1. Thetext_contrastrow reports the text that falls furthest short of its own threshold and gives that threshold, and its detail names the text's printed size and weight. A translucent plot or panel background is also measured as the colour it shows. Its alpha used to be dropped, sogeometry_contrastmoves for such a figure. The relative-luminance code now cites the current WCAG 2.2 definition and its 0.04045 breakpoint, which changes no ratio.- With a blank panel, as under
theme_depictr(),check_figure()measured geometry against the invisible white axes patch in place of the plot background, so a figure on a dark background passed in Python and failed in R. Both twins now report the same numbers for it. check_figure()measured translucent marks and text as if they were opaque, so a figure could pass contrast, colour-vision and greyscale checks that its drawn colours fail. The accessibility page's 'mended' example was one: its points, drawn at alpha 0.7, passed the geometry check at 3.87:1, and the vermillion a reader sees reaches 2.56:1. Colours are now composited over their background with the alpha each geom applies, outlines drawn opaque are credited, and the colours of each aesthetic are compared among themselves. A translucent colour or text is named in the detail with its alpha and the colour it is drawn as, and the separability rows say whether their closest pair are colours or fill colours. Verdicts on translucent figures can change in either direction. The page's example now draws its points opaque.check_figure()now treats a colour or fill mapped in several layers, one value per layer, as the encoding it is. Such figures were reported as encoding nothing by colour. With red and green drawn one group per layer, every colour row read "not applicable", whereas the same figure drawn in one layer fails the deuteranopia check at 0.37. Shape and line type split across layers count towardsredundant_encodingin the same way. A colour set as a constant, as ingeom_point(colour="red"), is still left out, as the docstring now says.pca_plot()stretched the loading arrows by a different factor on each axis, so the drawn directions were wrong, by up to 22 degrees on thewellbeing_survey()of 0.3.0. Both axes now share one factor, as in the R twin and in base R'sbiplot(). The aspect ratio is fixed as well, so each arrow points along its loading on a figure of any shape. The panel's height and width follow the ranges of its axes, so a figure of another shape gains blank space around the panel. Each label continues its arrow outward, as in R. Arrows and labels are drawn in a neutral dark grey (#333333) in place of the accent, which is also the second group colour and so tied the variables to the second group. TheKeyErrorfor a misspeltgroupnow names the column and the argument, in the R twin's words.residual_diagnostics_plot()andmodel_report()raisedAttributeErrorfor every GLM, Logit and Probit fit, although documented for GLMs. Such a fit now gets a Q-Q panel of randomised quantile residuals, which are standard normal under a correct model whatever the family. A binomial fit getsbinned_residual_plot()as its residual panel, as in the R twin, and other families plot their deviance residuals against the fitted values. Both functions gainglm_panels=True, whereFalsedraws the classic panels, andseed=for a reproducible randomisation.binned_residual_plot()now takes a Logit or Probit fit. Such a result'sfittedvaluesholds the linear predictor, so the panels read its fitted probabilities frompredict().binned_residual_plot()misdrew a grouped binomial model. With a two-column (successes, failures) response, statsmodels' response residuals count successes, so the points were drawn in counts against fitted probabilities, and the band took every row as a single trial, wherever the trials came from. The residuals are now proportions, as in the R twin, and each row's variance is divided by its number of trials, whether these come from the two columns or fromvar_weights. A GLM fitted with an estimated scale, as byfit(scale="X2"), has the variances multiplied by it, as the R twin multiplies them by a quasibinomial fit's dispersion. statsmodels' Pearson estimate leaves out the trials of a two-column response, so for such a fit the scale is recomputed over the trials, which gives R's dispersion.vif_plot()reported a VIF for each dummy column, so a factor with a small reference level was flagged as collinear even when it was independent of everything else, and relevelling changed the verdict. For a three-level factor whose reference level held 14 of 400 rows, each dummy read 7.7, past the line at 5, and 1.04 once another level was the reference. It now reports the generalised VIF of each term (Fox & Monette, 1992), as the R twin does, which is 1.005 for that factor whichever level is the reference. When a term spans several columns, the bars show GVIF^(1/(2 df)) against the square root of the threshold, under the R twin's axis titles. The VIFs come from the covariance of the coefficients, so a weighted fit or a GLM is measured with the weights of its fit. For OLS, WLS, GLM and Logit results, they agree with the R twin and with R'scar::vif(). A WLS or GLS result, which was refused as not fitted, is now accepted. The columns are grouped by the formula, under patsy or formulaic. A model fitted from arrays has no terms, so each of its columns counts as one.vif_plot()drew infinite VIFs for a design whose coefficients are aliased, or VIFs of about 1e15 under statsmodels 0.15, with no more than a warning. Such a model now raisesValueErrornaming the aliased terms, in the R twin's words. A model with fewer than two predictor columns raises as in R, and one without an intercept warns that its VIFs come from uncentred columns.scale_colour_depictr()andscale_fill_depictr()drew a ninth group in the missing-value grey, indistinguishable from missing data, with no warning of depictr's own, and so did every plot that adds them, such asecdf_plot()andraincloud_plot(). Withoutn, the scales now take as many colours as the plot has groups, so past eight groups they interpolate the palette and warn, as the R twin and the README describe. Figures with eight groups or fewer are drawn exactly as before.group_comparison_plot()drew mean ± 1.96 SE, which covers 81% at n = 3 and 88% at n = 5 for a nominal 95%. It now uses the t distribution, as the R twin andestimation_plot()do, and gainsconf_level. The intervals of small groups widen accordingly. A group with a single observation shows its mean only, with a warning in the R twin's words, where plotnine used to drop its interval with a generic message about missing values.confusion_matrix_plot()andcorrelation_heatmap()drew every cell label in the near-black#1a1a1a, although both colour ramps end in a dark blue. The labels were therefore least legible on the darkest cells. The largest count in the docstring's confusion matrix sat at 1.3:1 and the r = 1 diagonal of a correlation heatmap at 2.4:1, where WCAG asks 4.5:1 of text. Each label is now black or white, whichever has the higher contrast against its tile as the fill scale paints it, which is at least 4.58:1 on any colour. Then/aof an undefined correlation is chosen against the scale's grey in the same way.gain_plot()andlift_plot()depended on the order of the rows when scores were tied. They took the cases one at a time, so tied cases entered the curve in the order of their rows. On 200 cases with four distinct scores, the share of positives captured in the top 10% ranged from 0.20 to 0.30 with the row order alone. Tied cases are now targeted together, as in the R twin, and the gains curve runs straight across each run of ties, the expected curve when tied cases are reached in random order (Fawcett, 2006). Curves on distinct scores are unchanged. The bundled biomarker is recorded to two decimals, so 162 of the 300 patients share their value with another, and the gallery's gains and lift charts change slightly. Outcomes and scores of different lengths now raise aValueErrorthat names both arguments. A missing outcome or score raises one too. A missing outcome used to cut the curve short at its rank, and missing scores went last, in the order of their rows.timeseries_plot()dropped missing values before drawing. Without a date index, every later point moved one step to the left for each value dropped, and on a date axis the line ran straight across the gap. The rolling mean was taken over the closed-up series, so the windows next to a gap averaged values from both sides of it. Missing values now keep their place, and the line and the rolling mean break at them, as in the R twin. The rolling overlay no longer warns that it removed rows containing missing values, which it did on complete data as well.timeseries_plot(),acf_plot(),decompose_plot()andseasonal_plot()read a frame or 2-D array row by row, so several columns were interleaved into one series, and a frame lost its index. Several columns now raiseValueError, and a single column is read as the series, so a one-column frame keeps its index. The R twin'stimeseries_plot()draws one line per column of a multivariatets.timeseries_plot()andseasonal_plot()drew the values of an object column, for example one holding Python floats andNone, as categories.timeseries_plot()drew no line at all, andseasonal_plot()spaced the values evenly up the axis, one tick per value. The values are now read as numbers.- The grouped plots each treated a grouping column in their own way. A numeric
code, such as a block numbered 1 and 2, raised
TypeErrorinscatter_trend(),raincloud_plot(),group_comparison_plot()andexplore_categorical(), andexplore_distribution()andecdf_plot()drew it as one ungrouped curve. An unused category, as filtering a categorical leaves it, kept its place in the palette, so the categories after it took the next colour along and were drawn in other colours than in the R twin. Missing groups were drawn as a grey group of their own byscatter_trend(),ecdf_plot(),explore_distribution()andraincloud_plot(), and vanished without a word elsewhere. These functions, withridgeline_plot(), now share the R twin's rule. The group becomes a categorical, so a numeric code is a group, labelled 1 and 2 even when a missing value has made the codes floats, but a numeric column with more than 20 distinct values raisesValueError. Rows with a missing group are dropped with a warning, assurvival_plot()drops them, and unused categories take no colour or legend key. Colours are matched to the levels by name, soridgeline_plot()colours a ridge by its level, as the R twin does, while still stacking the ridges by median. Rows missing the plotted value are left out before the groups are formed, so plotnine no longer warns that it removed them from a grouped plot. Past eight groups, the warning that the palette is interpolated now comes once, when the plot is made, where it came each time the figure was drawn. The R twin applies the same rule inoutlier_plot()andexplore_pairs(), which take no group here. acf_plot(kind="pacf")used statsmodels' default estimator, the adjusted Yule-Walker. It disagreed with R'spacf()by up to 0.18 on R'sAirPassengersand put lags 15, 18 and 20 outside the significance band where R does not. It also multiplied the partial autocorrelation at lag 1 by n / (n - 1), although at that lag it equals the autocorrelation. It now uses the unadjusted estimator (method="ywm") and gives R's values. statsmodels' documentation reports that the adjusted estimator performed consistently worse in simulations.timeseries_plot()drew a moving average over an even window, such asrolling=12for monthly data, half an observation late. Equal weights over twelve observations are centred between two of them, and pandas' centred rolling mean drew the mean of observations 1 to 12 at observation 7, where its centre is 6.5. An even window now gives the 2×12 moving average (Hyndman & Athanasopoulos, 2021, Section 3.3), a 12-point average followed by a 2-point one. It is centred on each observation. Over one seasonal period, it is the trend of a classical decomposition. It spans one observation more than the window, so a series no longer than an even window has no overlay. The R twin drew the same average half an observation early, and the twins now draw the same values. An odd window is unchanged.seasonal_plot()ignored the calendar that a period or datetime index carries. It counted the positions from the first observation, so a monthly series starting in April drew April at position 1, and its cycles ran from April to March. When the frequency of the index implies the period, the position is now the month, the quarter or the weekday (Monday first), and the ticks are labelled Jan to Dec, Q1 to Q4 or Mon to Sun. Each cycle runs from the first season to the last, January to December for months, as in the R twin, and the cycles are numbered from 1, where they were numbered from 0. A quarterlyPeriodIndexwith a fiscal year numbers its quarters from that year's start. A bare array, or a period that the frequency of the index does not imply, still counts from the first observation. The docstring and the web app called the plot a seasonal subseries plot, the name of the R twin's facetedstyle = "subseries", and now call it a seasonal plot (one line per cycle).- The seasonal period was read from the first letter of the index frequency,
so
seasonal_plot()anddecompose_plot()took minutes (min) and milliseconds (ms) for months and used a period of 12. Only monthly, quarterly and daily frequencies imply a period now, and any other index needsperiod.seasonal_plot()also truncated a period that is not a whole number, such as 365.25 / 7, without a word. It now rounds such a period down with aUserWarning, as the R twin rounds a frequency down with a message, and reads a period within 0.00001 of a whole number as that number, as R'sts()reads a frequency. A period below 2, which drew every observation at one position, is now refused, as the R twin refuses such a frequency. - A
conf_levelgiven as a percentage, such as 95, gave NaN intervals intidy_estimates(),coefficient_plot(),compare_models(),effects_plot()andinteraction_plot(), andestimation_plot()failed with NumPy's "Quantiles must be in the range [0, 1]". In the first five, a level of 0 drew zero-width intervals and a level of 1 unbounded ones. Every function that takes aconf_levelnow refuses a value outside (0, 1), or one that is not a number, with the R twin's message: "conf_levelmust be a single number strictly between 0 and 1."survival_plot()andcalibration_plot()still takeNoneto leave the limits out. explore_distribution(group=..., alpha=...)drew a grouped density at an alpha of 0.2 whateveralphasaid. An explicitalphais now honoured. The new default,alpha=None, keeps 0.2 for a grouped density and 0.6 for every other fill, so a figure drawn with the defaults is unchanged. Over a histogram (kind="both"), the grouped density is still an outline.depictr_palette()gave seven colours forn=-1, none forn=0and seven forn=0with a ramp, and a fraction failed inside the slicing or the ramp. Annbelow 1, or one that is not a number, now raises the R twin's "nmust be a single positive number.", and a fraction is truncated, as R truncates it.simulate_cvd()andpalette_safety()read a single colour string as a list of characters:simulate_cvd("#005b96", "deutan")failed on"#", andpalette_safety("cmyk")judged four one-letter colours safe. A string is now one colour, sopalette_safety()refuses it for having fewer than two.simulate_cvd([])returns an empty list, where it failed inside NumPy.coefficient_plot()drew an unknownorder, a typo such as"descendng"among them, in model order without a word. It now raisesValueError, as R'smatch.arg()does, and takes"none", the R twin's name for model order, as an alias of"model".estimation_plot()failed inside NumPy's sampler whenn_bootwas a fraction. It now raises "n_bootmust be a positive integer.", the message it already gave below 1, and takes a whole number held in a float, such as1e4, as the integer it is.summary_table()andexplore_bivariate()took a datetime or timedelta column for a category. Withvars=None,summary_table()dropped such a column without a word when it had many distinct values, as if it were an identifier, and otherwise gave it one row per value, as it did whenever the column was named invars. Dates are now summarised as a range instead of one row per day: a singleRangerow reads2024-01-01 to 2024-04-27overall and in each group, with the time to the minute or second where the column has one. A timedelta is summarised as mean (SD) in the largest of days, hours, minutes and seconds that its median duration reaches, and the statistic names the unit, as inMean (SD), days. The R twin reports adifftimein the units it carries. The identifier columns thatvars=Nonestill skips are named in aUserWarning, as the R twin names them in a message.explore_bivariate()raisedTypeError: Continuous value supplied to discrete scalewhen either column held dates or durations and the other numbers. Dates and durations now count as numeric, as in the R twin. Against a numeric column, they give the scatter plot with a trend ofscatter_trend(), and against a categorical one boxplots of their values across the categories. A duration is drawn as a number in the unit thatsummary_table()reports, and the axis title names it.correlation_heatmap()refuses a column incolsthat is not numeric or boolean with aTypeErrorthat names it, in the R twin's words. A text column failed with "could not convert string to float", and dates or durations were correlated as the integers that store them.- Installation commands in the documentation and in error messages quote
extras and carry no trailing comments, so they paste into zsh, the default
shell on macOS, which reads
[...]as a filename pattern and passes#comments on to pip. The README, the home page and the getting-started guide gave theallextra without quotes and ended each install line with a comment. TheImportErrormessages of the classification, multivariate, survival, mixed-effects, diagnostic, prediction and time-series plots, and three of their module docstrings, also gave their extras without quotes. A pasted command stopped with "no matches found" before pip ran, or with pip's "Invalid requirement: '#'". Extras are now double-quoted, as inpip install "depictr[all]", a form that zsh, bash, fish, PowerShell and cmd read alike. The install blocks hold bare commands, and the prose before them says what each one installs. A test fails on an extra outside double quotes, and on apip installline in a shell block that carries a#comment, in the README, the contributing guide, the documentation, the sources and the app. threshold_plot()evaluated the metrics at 200 quantiles of the scores once there were more than 200 distinct scores, so the curves were read between the points where the confusion matrix changes, and it joined them with straight lines through values that no threshold gives. The metrics are now evaluated at every distinct score from cumulative counts, as in the R twin, and drawn as steps. Every threshold between two adjacent scores predicts what the upper score predicts, so each step changes at a score and runs flat below it. Where no positive case is predicted positive, F1 was missing and is now 0, as in the R twin. A missing outcome or score now raisesValueError, as ingain_plot()andlift_plot(), and so do vectors of different lengths.- Documentation. The posterior forest plots credited their default 66% and 95%
bands to bayesplot and brms, whose defaults are 50% and 90%. The pair is the
default
.widthof the R package ggdist, which the R twin draws with. The docstrings now say so, cite the ggdist paper and say that both bands are equal-tailed (quantile) intervals.scatter_trend()promised a confidence band and suggestedmethod="lowess", which, like"rlm", draws the line alone. Its docstring now says which smoothers give a band.effects_plot()andinteraction_plot()said a categorical predictor is held at its reference level. It is held at its most common level, as the R twin documents, and that level need not be the reference. The docstrings now say so. CITATION.cffgives the author's ORCID iD, as the R twin's does, so the Zenodo record of the next release carries it. The record of 0.3.0 has none. The comment beside its release date names the channels that released 0.3.0 on 2026-09-24: the GitHub release, Zenodo and PyPI.dumbbell_plot()took its two levels in the order they first appeared in the data, so moving one row to the top could reverse the segments and reorder the categories, and a categorical's declared order was ignored. The points took their colours in alphabetical order whatever the first level was. The levels now follow a categorical's declared order, and otherwise sorted order, as in the R twin. The first level starts each segment, orders the categories and takes the first colour.- The ROC, precision-recall, calibration, gains, lift and threshold curves
counted the outcomes equal to 1. A categorical outcome therefore had no
positives and met the single-class refusal, which named the wrong problem. A
1/2 coding was read with 1 as the positive class, and the threshold sweep
counted a 2 as neither class. As in the R twin, a categorical with two
categories is now read as 0/1, its last category the positive class, and any
other outcome must be coded 0/1 or boolean. A 1/2 or -1/1 coding and text
such as "yes"/"no" raise
ValueError, as they already did incalibration_plot(). explore_distribution()drew an empty panel for an unknownkind, such askind="hist", and a density of the category codes for a text column. It now refuses both, as the R twin does.explore_categorical()drew a category that one group never takes as a single bar the width of the whole category, where it read as an ungrouped total. The empty cell is now a bar of height zero, as in the R twin, so every bar keeps its own slot. A continuous numeric column, which was drawn as one bar per distinct value, now raisesValueErrorpast 20 distinct values, and an unknown column raisesKeyErrorat once, both as in the R twin.explore_bivariate()drew a boolean column as a scatter with a regression line through 0 and 1, since pandas counts it as numeric. It now takes a boolean column for a category, as the R twin takes a logical one.coefficient_plot()drew an empty panel when the intercept of an intercept-only model was dropped. It now says that no terms are left, in the R twin's words.correlation_heatmap()drew a single tile labelled 1.00 when one column was named, or when one was left once the zero-variance columns had been dropped. It now raisesValueError, in the R twin's words.- The time-series gallery said that the autocorrelation and partial autocorrelation functions spike at the multiples of twelve. The default lags reach only twelve, where the autocorrelation peaks on a slow decay, and the partial autocorrelation there lies inside the band. The page now describes the figures it draws, and a test holds it to them.
- The README's table of exported functions left out the dataset loaders,
DATASETS,depictr_brand,depictr_accentand thescale_color_depictrspelling. It lists them now, and a test holds the table to__all__. - The documentation deploy checks the forked header template against the
mkdocs-material release it was taken from. The docs extra sets no ceiling, so
the deploy installs the latest release, and the strict build cannot tell that
the template has fallen behind. The release and the upstream file's hash are
recorded in
pyproject.toml, as scopusflow-py records its own, and the deploy stops when the installed file no longer matches. A comment in the stylesheet quoted the dark-mode contrast of the shared stylesheet's teal (2.9:1). depictr's blue on its navy page measures 2.4:1.
0.3.0 - 2026-09-24¶
Added¶
check_figure(), an accessibility and honesty audit of the figure you are about to submit. Until now the package could vouch for its palette and say nothing about a finished plot, which is the thing a reader sees. Give it anything a depictr function returns, including a plot extended afterwards with+, and it introspects the build and returns a tidy DataFrame. The rows cover the separability of the encoding colours under each dichromacy and in greyscale, the smallest text size against a stated physical output width, the WCAG contrast of the text and of the geometry against their backgrounds, and whether any distinction is carried by colour alone. Every row carries the value it measured beside the threshold it was measured against, so a verdict can be argued with. The R twin gained the same function, with the same check names, thresholds, verdicts and measured numbers.- CI now exercises the installed package as well as the checkout. The matrix gains Python 3.14. A new job installs the built wheel into a bare environment outside the repository and imports it there, so packaged data and distribution metadata are tested in the state a user meets them, and not masked by the source tree an editable install sits beside. A second new job installs the declared minimum dependency versions. A weekly schedule runs the suite when nobody has pushed, so upstream drift in the plotting stack shows up as a dated red badge before anyone is surprised by it.
- Linting, matching the rest of the family. This was the only Python package in the
family running no linter, which is why it had quietly accumulated seven findings.
The rule set is stated explicitly in
pyproject.toml(E, F, W, I, UP, B, as in the scopusflow and theoryforge twins) so that it does not shift underneath the package: an inherited default is what turned a sibling's green CI red without a line of that package changing. The job lints the whole repository, where namingsrcandtestshad leftapp/streamlit_app.pyanddocs/_exec.pycovered by nothing, and the app was carrying an over-long line that no gate could see. Naming directories leaves a new top-level script unlinted until somebody remembers to extend the list.
Changed¶
simulate_cvd()names the three deficiencies in its refusal message, in place of the Python tuple it printed before, so the wording matches the R twin byte for byte.- The sRGB-to-XYZ matrix behind the CIE Lab conversion is now given to seven decimal
places instead of four. The rounded form shifted a Delta-E by a few thousandths,
enough to round a reported distance differently from the R twin on the same
colours. The distances
palette_safety()reports for the default palette are unchanged at two decimal places. - The accessibility claim has been narrowed to what is true. The default
eight-colour palette clears every colour-vision check and fails the new greyscale
check: its orange (
#e69f00) and sky blue (#56b4e9) differ by 0.79 in CIE lightness, so a black-and-white printer renders them as the same grey. The Okabe-Ito guarantee is about hue confusion and was never a claim about greyscale. The threshold stays where it is, and the accessibility page now states the limitation where the claim is made, so the package's own defaults are held to the same standard as anybody else's. - Two
zip()calls now passstrict=True: the dendrogram builder pairs scipy'sicoordwithdcoord, and the forest plot pairs two columns of one frame. In both, unequal lengths are impossible by construction, so raising is the right response to the impossible happening, where a silent truncation would have left a plot missing part of its data. Import blocks were sorted and one long line wrapped, with no change in behaviour.
Fixed¶
depictr_palette()interpolated past its accessibility guarantee without saying so. Beyond the eight Okabe-Ito base colours the palette is a ramp, and the colour-vision-deficiency guarantee that is this package's reason for existing stops holding, so the interpolated palette fails the package's ownpalette_safety()check. It now warns at the point of interpolation. The R twin carried the same silence and was fixed with it.survival_plot()drew a phantom arm for a group that does not exist. A missing value ingroupbecame a level matching no observation. Missing groups are now dropped with a count, and an all-missing group is an error.interaction_plot()drew a missing grouping value as a literalnanline and legend entry, the same "a missing value stringified into a legitimate-looking value" defect fixed across the family this round.summary_table()counted missing-group records inOveralland in no group column, so the per-group sizes silently fell short of the headline N. They now get aMissingcolumn. Its group columns are also ordered by level, so the table no longer depends on the order the rows arrived in.seasonal_plot()ordered its cycles lexicographically, sorting cycle 10 between 1 and 2, and coloured them categorically where the sequential ramp belongs. An annual index also inferred a seasonal period of 1, so an explicitperiodis now required.estimation_plot(two_panel=True)omits the interval for a single-observation group. The zero-width interval it drew there before implied a precision the data does not have.palette_safety()rejects a palette of fewer than two colours, having previously called one safe at an infinite distance.gain_plot()andlift_plot()refuse a single-class outcome. Both previously substituted a denominator of 1 and drew a flat curve.survival_plot()raises a clear error for a non-finite follow-up time, where a bareStopIterationescaped before.correlation_heatmap()drops zero-variance columns with a message, and an undefined correlation is labelledn/awhere the stringnanwas being printed in the cell.acf_plot(),decompose_plot()andseasonal_plot()silently dropped internal missing values, closing up the gap: the ACF correlated values across it, the decomposition misaligned, and every post-gap observation landed on the wrong within-period position. An internal missing value is now an error, in the wording ofsurvival_plot()'s refusal of a non-finite follow-up time. Missing values at either end are still trimmed, since trimming only shortens the series.acf_plot()turns an all-missing or empty series away by name, asseasonal_plot()already did. Its default lag count took the base-10 logarithm of the length, so a series with nothing in it came back as anOverflowErrorfrom the surroundingint(), naming neither the argument nor the problem.roc_curve_plot(),pr_curve_plot()andthreshold_plot()accepted a single-class outcome, annotating an AUC of nan or an average precision of 0.000 as if they measured something (scikit-learn computes both under a warning and does not refuse). They now raise the same refusalgain_plot()andlift_plot()already carry, with the R twin's wording per curve.dendrogram_plot()needed scikit-learn despite its documented scipy-only contract, because standardisation went through sklearn'sStandardScaler. It is now a numpy z-score with identical semantics (population standard deviation, and a scale of 1 substituted for a zero-variance column), so the dendrogram runs on the core install and the sklearn-backed plots see the same input as before.- The installation docs called statsmodels optional, when plotnine 0.15 requires it
and every core install therefore already ships it. The README and the docs now
name scikit-learn and lifelines as the optional back-ends, with
depictr[models]kept to pin the tested statsmodels floor. - The contributing guide told contributors to run
ruff check src tests, the command CI stopped using when linting moved to the whole repository. It now namesruff check .and says what the wider scope buys. - The README's list of functions not yet ported left out
format_termsanddepictr_options, and said nothing about the R-onlymonthly_salesdataset. All three are now named, andlegend_insidejoins the table of exported functions, which had omitted it. summary_table()'s documentation promised to sort the levels of an unordered categorical, when any categorical keeps its declared category order, which is the intended behaviour. The docs now say so.posterior_plot()warns when alabelskey matches no parameter. An unmatched key was silently ignored, and the figure came back unrelabelled.- Three declared dependency floors described a stack no install could produce.
pandas>=2.0,matplotlib>=3.6andscipy>=1.7all sat below whatplotnine>=0.15already requires (2.2, 3.8 and 1.8 respectively), and themodelsextra namedstatsmodels>=0.14where plotnine forces 0.14.5. A resolver always took the higher bound, so no install was ever affected, but a floor is a claim about what has been tested and this one was false. The floors now state what plotnine forces, and a new CI job installs them.
0.2.2 - 2026-07-23¶
Added¶
calibration_plotgained the Parameters and Returns sections it lacked, including a note thaty_scoremust hold fitted probabilities.
Fixed¶
- The
calibration_plotexample fits a logistic regression and passes its predicted probabilities. A reliability curve compares predicted probability with observed frequency, so the previous hand-written score misstated the calibration it was meant to demonstrate. - The
seasonal_plotexample carries trend and noise. Its series was a noiseless sine, so all ten cycles coincided exactly and the figure showed one curve behind a ten-entry legend.
0.2.1 - 2026-07-15¶
Added¶
- The gallery now covers every plot family, matching the R package's vignettes.
Fixed¶
- Corrected the
palette_safety()result shape shown in the README, which left outworst_conditionandworst_pair. The function returns those as well as the elements already listed.
0.2.0 - 2026-07-10¶
Changed¶
- Raised the plotnine dependency floor to 0.15, the first release with the
plot composition operators (
|,/) thatarrange_plotsuses. arrange_plots, and the multi-panel reports built on it, warn when atitleis dropped because plotnine compositions cannot carry a figure-level title. Previously the argument was discarded silently.
Fixed¶
- A missing value in a grouped column crashed at draw time. The default colour
for missing (
NA) levels wasgrey80, an R colour name that matplotlib rejects, and it is now the equivalent hex#cccccc. - Error messages named nonexistent extras. The ImportError messages and module
docstrings in the diagnostics, mixed-effects and multivariate modules pointed
at
depictr[diagnostics],depictr[mixed]anddepictr[multivariate], none of which is defined. They now namedepictr[models]anddepictr[classification](scipy is a core dependency).
0.1.1 - 2026-07-08¶
Added¶
- Added an opt-in
legend_inside=Falseparameter toexplore_distribution,ecdf_plot,dumbbell_plot,missingness_mapandsurvival_plot, plus the publiclegend_inside()theme helper, which places the legend inside the panel, over a light background, so the figure needs no right-hand margin. - Added a 'Getting started' guide that walks through a short analysis end to end.
Changed¶
- Rebuilt the number-at-risk table beneath
survival_plot(risk_table=True). The curves now use the full panel width (no left-hand gutter), the group names label the rows on the y-axis, and the counts are coloured to match the curves, so the table reads as a strip under the plot. It used to be text floating in loosely spaced negative space. - The log-rank p-value follows APA style (an italicised p, no leading zero, and p < .001 reported below that threshold). The colour legend and the risk-table rows now list the groups in the same order (a user-set categorical order, otherwise first appearance).
- The README and the PyPI project page now open with a gallery (a grouped density and Kaplan-Meier curves), and the documentation landing page gains the same hero plot and a PyPI install link.
- README image assets are kept out of the source distribution.
Fixed¶
- Grouped histograms were invisible.
explore_distribution(kind="both"or"histogram")with agroupdrew no bars at all, becausegeom_histogram(fill=None)made them fully transparent, where the intention was to defer to the group colour mapping. - Axis and legend titles leaked raw column names. Several plots that
meant to leave a title blank (
labs(x=None, ...)) instead showed the mapped column's literal name (x,value,variable,term,metric, ...) because this plotnine version readsNoneas unset, and only""as blank. Corrected 14 call sites acrossdiagnostics,eda,estimation,mixed,models,multivariate,posterior,predictions,distributions_extra,timeseriesandclassification. - The grouped risk-table path on
survival_plot(risk_table=True)added the colour scale twice, which plotnine warned about and silently replaced with an identical one. It is now added once. - Corrected the Cook (1977) reference title and added DOIs to Cook (1977), Hedges (1981) and Allen et al. (2021).
0.1.0 - 2026-06-27¶
First release. depictr (Python) is a unified, colourblind-safe toolkit for
publication-ready statistical visualisation, built on plotnine and the Python
sibling of the depictr R package. It gives one consistent theme and calling
convention across the whole workflow, and every function returns a plotnine
object you can extend with +.
Accessibility¶
- Okabe-Ito palette and the depictr theme and scales as the default look.
- A Machado-2009 colour-vision-deficiency simulator (
simulate_cvd) and a CIE-Lab palette safety check (palette_safety), which validate the default palette.
Plotting functions, by family¶
The functions fall into nine families.
- Exploratory analysis has
explore_distribution,explore_categorical,explore_bivariate,scatter_trend,correlation_heatmap,missingness_map,ecdf_plot,ridgeline_plot,dumbbell_plot,outlier_plot,group_comparison_plot,explore_pairsandraincloud_plot. - Estimation and tables are served by
estimation_plot(single-panel Cumming or two-panel Gardner-Altman) andsummary_table. - Model estimates are drawn by
coefficient_plot,tidy_estimates(a fitted model or a tidy frame),effects_plot,interaction_plot,compare_models,random_effects_plot,posterior_plot,frequentist_bayesian_plotandpower_curve_plot. - Diagnostics are covered by
qq_plot,influence_plot,vif_plot,binned_residual_plot,residual_diagnostics_plotandmodel_report. - Classification is covered by
roc_curve_plot,pr_curve_plot,confusion_matrix_plot,calibration_plot,gain_plot,lift_plotandthreshold_plot. - Multivariate analysis has
pca_plot,scree_plot,cluster_plot,dendrogram_plotandsilhouette_plot. - Survival has
survival_plot, with an optional number-at-risk table. - Time series have
acf_plot,decompose_plot,seasonal_plotandtimeseries_plot. - Composition has
arrange_plotsandsave_plot.
Design¶
- Computation is delegated to the specialist packages (scikit-learn, statsmodels,
lifelines, scipy) and re-skinned under the shared theme. Each is an optional
dependency installed via an extra (
depictr[classification],depictr[models],depictr[survival]). - Reproducibly simulated datasets (
crop_yield,wellbeing_survey,lexical_decision,clinical_trial) and a Streamlit gallery app with a live colourblind-vision toggle.
Known limitations¶
- plotnine compositions have no figure-level title, so multi-panel grids carry their titles on each panel.
- A handful of functions from the R package are not ported:
optimizer_fixef_plot(there is no clean statsmodels equivalent oflme4::allFit),k_diagnostic,palette_preview,model_fit_tableandts_forecast.